Skip to content

Commit 12d09be

Browse files
authored
Tolerate slow or unreachable cross-node SharedDoc during session cleanup (#4845)
The collaborative editor Session calls SharedDoc.unobserve/1 during cleanup, which issues a cross-node GenServer.call to a SharedDoc that may live on another node. When that node is unreachable (:noconnection) or slow to reply (:timeout), the call exited and crashed the Session; a Phoenix channel blocked in save_workflow then inherited the exit and dropped the client's editor connection. Wrap both unobserve call sites (terminate/2 and the parent :DOWN handler) in safe_unobserve/1, which catches the exit and logs at warning instead of crashing. Skipping the unobserve is safe: the SharedDoc monitors each observer and runs the same cleanup from its own :DOWN handler when the Session dies. Closes #4817
1 parent 2bfd363 commit 12d09be

3 files changed

Lines changed: 46 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ and this project adheres to
4242

4343
### Fixed
4444

45+
- Stop the collaborative editor's Session (and the Phoenix channel calling it)
46+
from crashing when the cross-node `SharedDoc.unobserve/1` during cleanup hits
47+
a SharedDoc on a node that is unreachable (`:noconnection`) or slow to reply
48+
(`:timeout`); the failed unobserve is now tolerated as a no-op since the
49+
SharedDoc cleans up observers via its own monitor.
50+
[#4817](https://github.com/OpenFn/lightning/issues/4817)
4551
- Fix email format validation not displaying in the Add Collaborators and Invite
4652
Collaborator modal. [#4765](https://github.com/OpenFn/lightning/issues/4765)
4753
- Fix a `workflows_pkey` duplicate-key crash when reconnecting to the

lib/lightning/collaboration/session.ex

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,11 @@ defmodule Lightning.Collaboration.Session do
139139
Process.demonitor(state.parent_ref)
140140
end
141141

142-
# Don't check Process.alive? - it only works for local PIDs
143-
# and shared_doc_pid can be on another node in a distributed cluster.
144-
# Sending to a dead process is safe (message is discarded).
142+
# Don't check Process.alive? - it only works for local PIDs and
143+
# shared_doc_pid can be on another node in a distributed cluster.
144+
# safe_unobserve/1 tolerates the remote being slow or gone (see #4817).
145145
if shared_doc_pid do
146-
SharedDoc.unobserve(shared_doc_pid)
146+
safe_unobserve(shared_doc_pid)
147147
end
148148

149149
Presence.untrack_user_presence(
@@ -155,6 +155,17 @@ defmodule Lightning.Collaboration.Session do
155155
:ok
156156
end
157157

158+
defp safe_unobserve(shared_doc_pid) do
159+
SharedDoc.unobserve(shared_doc_pid)
160+
catch
161+
:exit, reason ->
162+
Logger.warning(
163+
"SharedDoc.unobserve skipped during session cleanup: #{inspect(reason)}"
164+
)
165+
166+
:ok
167+
end
168+
158169
def lookup_shared_doc(document_name) do
159170
case :pg.get_members(@pg_scope, document_name) do
160171
[] -> nil
@@ -451,10 +462,11 @@ defmodule Lightning.Collaboration.Session do
451462
if ref == parent_ref do
452463
Process.demonitor(parent_ref)
453464

454-
# Don't check Process.alive? - it only works for local PIDs
455-
# and shared_doc_pid can be on another node in a distributed cluster.
465+
# Don't check Process.alive? - it only works for local PIDs and
466+
# shared_doc_pid can be on another node in a distributed cluster.
467+
# safe_unobserve/1 tolerates the remote being slow or gone (see #4817).
456468
if shared_doc_pid do
457-
SharedDoc.unobserve(shared_doc_pid)
469+
safe_unobserve(shared_doc_pid)
458470
end
459471

460472
{:stop, :normal, %{state | parent_ref: nil, shared_doc_pid: nil}}

test/lightning/collaboration/session_test.exs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,27 @@ defmodule Lightning.SessionTest do
655655
# %{}
656656
# )
657657
end
658+
659+
@tag :capture_log
660+
test "terminate tolerates an unreachable SharedDoc", %{user: user} do
661+
# The cross-node `unobserve` issued during cleanup can exit when the
662+
# SharedDoc's node is gone (:noconnection) or slow (:timeout). terminate/2
663+
# must swallow that and still finish cleanup rather than crash (#4817).
664+
# A dead pid stands in for the unreachable remote: the GenServer.call to
665+
# it exits :noproc, the same :exit class caught by safe_unobserve/1.
666+
dead_pid = spawn(fn -> :ok end)
667+
refute_eventually(Process.alive?(dead_pid))
668+
669+
state = %Session{
670+
parent_ref: nil,
671+
shared_doc_pid: dead_pid,
672+
user: user,
673+
workflow: %Workflow{id: Ecto.UUID.generate()},
674+
document_name: "workflow:#{Ecto.UUID.generate()}"
675+
}
676+
677+
assert :ok = Session.terminate(:shutdown, state)
678+
end
658679
end
659680

660681
defp get_expected_value(model, key) do

0 commit comments

Comments
 (0)