Skip to content

Commit

Permalink
Ensure that we don't crash when a table is started twice
Browse files Browse the repository at this point in the history
  • Loading branch information
whitfin committed Sep 7, 2016
1 parent 5bb87d5 commit 5d6d0f9
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 8 deletions.
4 changes: 3 additions & 1 deletion coveralls.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"def.+(.+\\\\.+).+do",
"^\\s+use\\s+"
],
"custom_stop_words": [ ],
"custom_stop_words": [
"with"
],
"coverage_options": {
"treat_no_relevant_lines_as_covered": true
},
Expand Down
37 changes: 30 additions & 7 deletions lib/eternal/supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ defmodule Eternal.Supervisor do
{ :ok, pid, Table.t } | :ignore |
{ :error, { :already_started, pid } | { :shutdown, term } | term }
def start_link(name, ets_opts \\ [], opts \\ []) when is_opts(name, ets_opts, opts) do
super_tab = :ets.new(name, [ :public ] ++ ets_opts)
super_args = { super_tab, opts, self() }
super_opts = [ name: Table.to_name(super_tab, true) ]
super_proc = Supervisor.start_link(__MODULE__, super_args, super_opts)
detect_clash(name, ets_opts, fn ->
super_tab = :ets.new(name, [ :public ] ++ ets_opts)
super_args = { super_tab, opts, self() }
super_opts = [ name: Table.to_name(super_tab, true) ]
super_proc = Supervisor.start_link(__MODULE__, super_args, super_opts)

with { :ok, pid } <- super_proc do
{ :ok, pid, super_tab }
end
with { :ok, pid } <- super_proc do
{ :ok, pid, super_tab }
end
end)
end

@doc false
Expand All @@ -58,4 +60,25 @@ defmodule Eternal.Supervisor do
supervise(children, strategy: :one_for_one)
end

# Detects a potential name clash inside ETS. If we have a named table and the
# table is already in use, we return a link to the existing Supervisor. This
# means we can be transparent to any crashes caused by starting the same ETS
# table twice. Otherwise, we execute the callback which will create the table.
defp detect_clash(name, ets_opts, fun) do
if exists?(name, ets_opts) do
{ :ok, Process.whereis(name), name }
else
fun.()
end
end

# Shorthand function to determine if an ETS table exists or not. We calculate
# this by looking for the name inside the list of ETS tables, but only if the
# options specify that we should name the ETS table. If it's not named, there
# won't be a table clash when starting a new table, so we're safe to continue.
defp exists?(name, ets_opts) do
Enum.member?(ets_opts, :named_table) and
Enum.member?(:ets.all(), name)
end

end
7 changes: 7 additions & 0 deletions test/eternal_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ defmodule EternalTest do
assert(Regex.match?(~r/\[debug\] \[eternal\] Table 'logging_output' gifted to #PID<\d\.\d+\.\d> via #PID<\d\.\d+\.\d>/, msg))
end

test "starting a table twice finds the previous owner" do
{ :ok, pid1 } = Eternal.start_link(:existing_table, [], [ quiet: true ])
{ :ok, pid2 } = Eternal.start_link(:existing_table, [], [ quiet: true ])

assert(pid1 == pid2)
end

test "deprecation warnings when using new/3" do
msg = capture_log(fn ->
tab = Eternal.new(:deprecation_new, [], [ quiet: true ])
Expand Down

0 comments on commit 5d6d0f9

Please sign in to comment.