Skip to content

Commit 3e18e7d

Browse files
committed
fix(engine): isolate search store from async load crashes
Replace Task.async with Task.Supervisor.async_nolink in State.prepare_backend_async/2 so a crashing index build no longer kills the Store GenServer. Add a {:DOWN, ...} handler that logs the error and clears async_load_ref, allowing the Store to retry on the next project_compiled event. Demonitor the ref on successful completion to avoid processing the normal DOWN.
1 parent 59489c7 commit 3e18e7d

6 files changed

Lines changed: 48 additions & 1 deletion

File tree

apps/engine/lib/engine/search/store.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,18 @@ defmodule Engine.Search.Store do
154154

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

161+
def handle_info(
162+
{:DOWN, ref, :process, _pid, reason},
163+
{update_ref, %State{async_load_ref: ref} = state}
164+
) do
165+
Logger.error("Search index async load crashed: #{inspect(reason)}")
166+
{:noreply, {update_ref, %State{state | async_load_ref: nil}}}
167+
end
168+
160169
def handle_info(:flush_updates, {_, %State{} = state}) do
161170
{:ok, state} = State.flush_buffered_updates(state)
162171
ref = schedule_flush()

apps/engine/lib/engine/search/store/state.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ defmodule Engine.Search.Store.State do
234234

235235
defp prepare_backend_async(%__MODULE__{async_load_ref: nil} = state, backend_result) do
236236
task =
237-
Task.async(fn ->
237+
Task.Supervisor.async_nolink(Engine.TaskSupervisor, fn ->
238238
case state.backend.prepare(backend_result) do
239239
{:ok, :empty} ->
240240
Logger.info("backend reports empty")

apps/engine/test/engine/code_intelligence/references_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ defmodule Engine.CodeIntelligence.ReferencesTest do
2020
Backends.Ets.destroy_all(project)
2121
Engine.set_project(project)
2222

23+
start_supervised!({Task.Supervisor, name: Engine.TaskSupervisor})
2324
start_supervised!(Document.Store)
2425
start_supervised!(Engine.Dispatch)
2526
start_supervised!(Backends.Ets)

apps/engine/test/engine/dispatch/handlers/indexer_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ defmodule Engine.Dispatch.Handlers.IndexingTest do
2828

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

31+
start_supervised!({Task.Supervisor, name: Engine.TaskSupervisor})
3132
start_supervised!(Engine.Dispatch)
3233
start_supervised!(Commands.Reindex)
3334
start_supervised!(Search.Store.Backends.Ets)

apps/engine/test/engine/search/store/backends/ets_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ defmodule Engine.Search.Store.Backend.EtsTest do
6161
end
6262

6363
defp start_supervised_store(%Project{} = project, create_fn, update_fn, backend) do
64+
start_supervised!({Task.Supervisor, name: Engine.TaskSupervisor})
6465
start_supervised!(Dispatch)
6566
start_supervised!(Backends.Ets)
6667
start_supervised!({Store, [project, create_fn, update_fn, backend]})

apps/engine/test/engine/search/store_test.exs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ defmodule Engine.Search.StoreTest do
368368
defp with_a_started_store(project, backend) do
369369
destroy_backend(backend, project)
370370

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

404+
start_supervised!({Task.Supervisor, name: Engine.TaskSupervisor})
403405
start_supervised!(Dispatch)
404406
start_supervised!(Ets)
405407

@@ -458,4 +460,37 @@ defmodule Engine.Search.StoreTest do
458460
assert received_project == project
459461
end
460462
end
463+
464+
describe "async load crash recovery" do
465+
setup %{project: project} do
466+
destroy_backend(Ets, project)
467+
468+
start_supervised!({Task.Supervisor, name: Engine.TaskSupervisor})
469+
start_supervised!(Dispatch)
470+
start_supervised!(Ets)
471+
472+
crashing_create = fn _project ->
473+
raise "index build exploded"
474+
end
475+
476+
start_supervised!({Store, [project, crashing_create, &default_update/2, Ets]})
477+
478+
assert_eventually alive?()
479+
480+
on_exit(fn ->
481+
after_each_test(Ets, project)
482+
end)
483+
484+
{:ok, project: project}
485+
end
486+
487+
test "store survives when async load task crashes" do
488+
pid = Process.whereis(Store)
489+
Store.enable()
490+
# Allow time for the task to crash and the DOWN message to be processed.
491+
Process.sleep(100)
492+
493+
assert Process.alive?(pid)
494+
end
495+
end
461496
end

0 commit comments

Comments
 (0)