Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/engine/lib/engine/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ defmodule Engine.Application do
Engine.Build.CaptureServer,
Engine.Plugin.Runner.Supervisor,
Engine.Plugin.Runner.Coordinator,
{Task.Supervisor, name: Engine.TaskSupervisor},
Engine.Search.Store.Backends.Ets,
{Engine.Search.Store,
[
Expand Down
9 changes: 9 additions & 0 deletions apps/engine/lib/engine/search/store.ex
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,18 @@ defmodule Engine.Search.Store do

# handle the result from `State.async_load/1`
def handle_info({ref, result}, {update_ref, %State{async_load_ref: ref} = state}) do
Process.demonitor(ref, [:flush])
{:noreply, {update_ref, State.async_load_complete(state, result)}}
end

def handle_info(
{:DOWN, ref, :process, _pid, reason},
{update_ref, %State{async_load_ref: ref} = state}
) do
Logger.error("Search index async load crashed: #{inspect(reason)}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Logger.error("Search index async load crashed: #{inspect(reason)}")
Logger.error("Failed to prepare search store backend: #{inspect(reason)}")

{:noreply, {update_ref, %State{state | async_load_ref: nil}}}
end

def handle_info(:flush_updates, {_, %State{} = state}) do
{:ok, state} = State.flush_buffered_updates(state)
ref = schedule_flush()
Expand Down
2 changes: 1 addition & 1 deletion apps/engine/lib/engine/search/store/state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ defmodule Engine.Search.Store.State do

defp prepare_backend_async(%__MODULE__{async_load_ref: nil} = state, backend_result) do
task =
Task.async(fn ->
Task.Supervisor.async_nolink(Engine.TaskSupervisor, fn ->
case state.backend.prepare(backend_result) do
{:ok, :empty} ->
Logger.info("backend reports empty")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ defmodule Engine.CodeIntelligence.ReferencesTest do
Backends.Ets.destroy_all(project)
Engine.set_project(project)

start_supervised!({Task.Supervisor, name: Engine.TaskSupervisor})
start_supervised!(Document.Store)
start_supervised!(Engine.Dispatch)
start_supervised!(Backends.Ets)
Expand Down
1 change: 1 addition & 0 deletions apps/engine/test/engine/dispatch/handlers/indexer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ defmodule Engine.Dispatch.Handlers.IndexingTest do

patch(Engine.Dispatch, :erpc_cast, fn Expert.Progress, _function, _args -> true end)

start_supervised!({Task.Supervisor, name: Engine.TaskSupervisor})
start_supervised!(Engine.Dispatch)
start_supervised!(Commands.Reindex)
start_supervised!(Search.Store.Backends.Ets)
Expand Down
1 change: 1 addition & 0 deletions apps/engine/test/engine/search/store/backends/ets_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ defmodule Engine.Search.Store.Backend.EtsTest do
end

defp start_supervised_store(%Project{} = project, create_fn, update_fn, backend) do
start_supervised!({Task.Supervisor, name: Engine.TaskSupervisor})
start_supervised!(Dispatch)
start_supervised!(Backends.Ets)
start_supervised!({Store, [project, create_fn, update_fn, backend]})
Expand Down
35 changes: 35 additions & 0 deletions apps/engine/test/engine/search/store_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ defmodule Engine.Search.StoreTest do
defp with_a_started_store(project, backend) do
destroy_backend(backend, project)

start_supervised!({Task.Supervisor, name: Engine.TaskSupervisor})
start_supervised!(Dispatch)
start_supervised!(backend)
start_supervised!({Store, [project, &default_create/1, &default_update/2, backend]})
Expand Down Expand Up @@ -400,6 +401,7 @@ defmodule Engine.Search.StoreTest do
setup %{project: project} do
destroy_backend(Ets, project)

start_supervised!({Task.Supervisor, name: Engine.TaskSupervisor})
start_supervised!(Dispatch)
start_supervised!(Ets)

Expand Down Expand Up @@ -458,4 +460,37 @@ defmodule Engine.Search.StoreTest do
assert received_project == project
end
end

describe "async load crash recovery" do
setup %{project: project} do
destroy_backend(Ets, project)

start_supervised!({Task.Supervisor, name: Engine.TaskSupervisor})
start_supervised!(Dispatch)
start_supervised!(Ets)

crashing_create = fn _project ->
raise "index build exploded"
end

start_supervised!({Store, [project, crashing_create, &default_update/2, Ets]})

assert_eventually alive?()

on_exit(fn ->
after_each_test(Ets, project)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
after_each_test(Ets, project)
destroy_backend(Ets, project)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this after each test function can probably be deleted, but we can deal with that in a different pr (not you, just as a note for myself)

end)

{:ok, project: project}
end

test "store survives when async load task crashes" do
pid = Process.whereis(Store)
Store.enable()
# Allow time for the task to crash and the DOWN message to be processed.
Process.sleep(100)
Comment on lines +490 to +491
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest having the function that raises send a messsage to the test process instead, and we can assert_receive on that instead of waiting.


assert Process.alive?(pid)
end
end
end