[core][gcs] Publish node death before persist; drop RocksDB soft-durability (REP-64) - #64702
[core][gcs] Publish node death before persist; drop RocksDB soft-durability (REP-64)#64702jhasm wants to merge 13 commits into
Conversation
…bility Fixes the root-cause pub/sub issue proven in ray-project#64187 and removes the soft-durability workaround shipped with the RocksDB GCS backend (ray-project#63657). Root cause: GCS published the node-death notification from inside the node table write's completion callback (publish-after-persist). Owners of objects/tasks react to a node's death ONLY from that single pushed pub/sub notification (core_worker.cc on_node_change, DEAD branch) with no owner-side fallback, so gating the publish on the durable write couples cluster-wide death detection to persist latency. Under a slow backend (the RocksDB GCS with per-write fsync) the notification is delayed enough to strand object recovery and hang dynamic-generator reconstruction. Fix (F3): publish node death on the in-memory DEAD transition, posted onto the GCS io_context and decoupled from the durable write. This relaxes broadcast-after-durable ordering only for the node channel, which is safe because node death is a terminal, restart-re-derivable transition (re-established by health checks on GCS restart). It is not generalized to the actor channel, whose state can resurrect across a GCS crash (and which already self-heals via fetch-on-subscribe). With the root cause fixed, remove the RocksDB soft-durability relaxation: all GCS tables now use strict per-write WAL fsync (sync=true) again. Deletes SoftDurableTables() and simplifies SyncWriteOptions(). Updates the test_dynamic_generator_reconstruction_nondeterministic comment to reference the real fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Santosh Jha <santosh.m.jha@gmail.com>
There was a problem hiding this comment.
Code Review
This pull request decouples GCS node-death notifications from the durable storage write, publishing them immediately upon the in-memory state transition to DEAD. This resolves a reconstruction race hang under slow durable backends like RocksDB. Consequently, the "soft-durability" workaround (skipping fsync) for certain tables in the RocksDB store client has been removed, restoring full synchronous fsync for all writes. The review feedback suggests optimizing SyncWriteOptions in RocksDbStoreClient by returning a const reference to a static rocksdb::WriteOptions instance to avoid unnecessary copy overhead on every write operation.
Avoid constructing a rocksdb::WriteOptions on every mutating GCS write. The options are identical for all synchronous writes (sync=true), so return a reference to a single thread-safe static instance instead of a fresh value. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Santosh Jha <santosh.m.jha@gmail.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Reviewed by Cursor Bugbot for commit 5d89fcb. Configure here.
…g tests Address Cursor Bugbot: HandleUnregisterNode still published node death from the NodeTable().Put completion callback, re-coupling graceful-unregistration death detection to fsync latency. Apply the same publish-before-persist pattern used in InternalOnNodeFailure (post the publish onto io_context, decoupled from the durable write). Add two GcsNodeManager unit tests that deterministically prove death is broadcast before the durable write completes, for both the health-check failure path (OnNodeFailure) and the graceful unregistration path (HandleUnregisterNode). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Santosh Jha <santosh.m.jha@gmail.com>
Wrap OnNodeFailure call to satisfy clang-format 12.0.1 (microcheck lint). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Santosh Jha <santosh.m.jha@gmail.com>
The on_done lambda captured node_id, but node_id stopped being used there once the publish was moved ahead of the persist (F3). clang14 in CI builds with -Werror,-Wunused-lambda-capture, failing the corebuild. Drop the capture. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Santosh Jha <santosh.m.jha@gmail.com>
| } | ||
|
|
||
| TEST_F(GcsNodeManagerTest, TestNodeFailurePublishesDeathBeforePersist) { | ||
| // Regression test for the lost-wakeup fixed in #64187: a health-check node |
There was a problem hiding this comment.
#64187 is probably not a correct reference. We never merged it.
There was a problem hiding this comment.
Good catch — fixed in 0081cde. You're right that #64187 was never merged; the fix actually lives in this PR. #64187 is the provenance/proof PR that demonstrates the lost-wakeup deterministically on the in-memory GCS (with injected timing), so I kept it only as an explicitly-labeled "root-cause proof, not merged" pointer and removed the misleading "fixed in #64187" wording from the test comment (and the secondary cross-reference in gcs_node_manager.cc). Happy to drop the #64187 mention entirely if you'd prefer no reference to an unmerged PR in the code.
| // gate cluster-wide death detection on persist latency either. Posted onto | ||
| // io_context_ so it runs outside mutex_ (held here) yet independent of the | ||
| // persist completing. | ||
| boost::asio::post(io_context_, [this, node_id, node_info_delta]() { |
There was a problem hiding this comment.
Done in 0081cde — switched to io_context_.post(handler, "GcsNodeManager.PublishNodeDeathOnUnregister"). Using the instrumented member (instead of the boost::asio::post free function) restores the handler count/queueing/execution stats and a debug name, and matches the convention used elsewhere in gcs (e.g. gcs_placement_group_manager.cc, postable.h). Also dropped the now-unused <boost/asio/post.hpp> include.
| // | ||
| // Posted onto io_context_ so it runs outside mutex_ (held by our caller | ||
| // OnNodeFailure) yet independent of the persist completing. | ||
| boost::asio::post(io_context_, |
There was a problem hiding this comment.
Done in 0081cde — same change here, now io_context_.post(handler, "GcsNodeManager.PublishNodeDeathOnFailure").
…ance reference - Replace boost::asio::post(io_context_, ...) with the instrumented io_context_.post(handler, name) member in both node-death publish sites (HandleUnregisterNode and InternalOnNodeFailure), matching the repo convention and restoring handler stats/naming. Drop the now-unused <boost/asio/post.hpp> include. - Fix comments that referenced the unmerged provenance PR ray-project#64187: the regression test no longer claims the lost-wakeup was "fixed in ray-project#64187" (it is fixed in this PR); ray-project#64187 is now labeled as the unmerged root-cause proof where cited. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Santosh Jha <santosh.m.jha@gmail.com>
|
LGTM. Let's wait for tests to pass. |

Why are these changes needed?
Follow-up to #63657 (embedded RocksDB GCS backend, REP-64) that fixes the underlying pub/sub issue at its root and removes the temporary "soft-durability" workaround.
Root cause (proven in #64187)
GCS published the node-death notification from inside the node table write's completion callback — publish-after-persist. Owners of objects/tasks react to a node's death only from that single pushed pub/sub notification (
core_worker.ccon_node_change, DEAD branch), and once subscribed there is no periodic owner-side re-poll. Gating the publish on the durable write therefore couples cluster-wide death detection to persist latency: under a slow backend (the RocksDB GCS with per-writefsync) the notification is delayed enough to strand object recovery and hangtest_dynamic_generator_reconstruction_nondeterministic[None-*]inlist(gen).The provenance PR #64187 demonstrates this deterministically on the in-memory GCS (timing injected), confirming the bug is in Ray core and backend-agnostic.
Fix (F3 — publish node death before persist)
Publish node death on the in-memory
DEADtransition, posted onto the GCSio_contextand decoupled from the durable write. Applied to both node-death paths:GcsNodeManager::InternalOnNodeFailure— health-check failure.GcsNodeManager::HandleUnregisterNode— graceful unregistration.Properties:
AsyncResubscribe), and neither periodically re-polls an already-established subscription. The difference is durability semantics: actor state is not terminal and can legitimately change (e.g.RESTARTING→ALIVE) and must survive a GCS crash in durable order, so its publish must stay gated on persist. Node liveness is terminal and re-derivable, so relaxing the ordering is safe.We deliberately did not add the optional owner-side fallback re-poll (F5 in #64187): the demonstrated failure is a slow publish, not a dropped one, and F5 would add a permanent periodic GCS poll to every owner for a hypothetical the pub/sub layer already makes unlikely. It remains an isolated future option if a genuinely dropped notification is ever observed.
Remove the soft-durability workaround
With the root cause fixed, all GCS tables use strict per-write WAL
fsync(sync = true) again. DeletesSoftDurableTables()and simplifiesRocksDbStoreClient::SyncWriteOptions()(now returns aconst &to a shared constant, no per-write copy). TheNODE/ACTORrelaxation is no longer needed.How do we know this is safe?
The ordering guarantee (death is broadcast before, and independently of, the durable write) is covered end-to-end and at the unit level:
test_dynamic_generator_reconstruction_nondeterministic[None-*](python/ray/tests/test_generators.py) is the exact scenario the bug hangs. It passes on the RocksDB GCS with strict fsync and no soft-durability, i.e. purely because of F3.gcs_node_manager_test.cc, new):TestNodeFailurePublishesDeathBeforePersist— health-check path: afterOnNodeFailure, running exactly one queued handler broadcasts DEAD while the persist-completion callback has not yet run.TestUnregisterNodePublishesDeathBeforePersist— graceful-unregister path: the DEAD publish fires before the persist-completion callback (which is what writes the DEAD export event).Both fail against the old publish-after-persist behavior and pass with F3, so they lock in the fix.
Practically-possible scenarios and why they remain safe: (a) slow persist — publish is already decoupled; (b) GCS restart before persist — node death is re-derived by health checks and clients re-fetch on resubscribe; (c) dropped push (not observed) — out of scope for this PR and independently addressable via the F5 backstop without touching F3.
Related issue number
Checks
gcs_node_manager_test(incl. new ordering tests),node_manager_export_event_testpassrocksdb_store_client_test,rocksdb_smoke_testpassgcs_server_lib+rocksdb_store_clientbuild cleanly