Skip to content

refactor(remote): make replay and query ports async, drop actor - #4157

Merged
ScriptedAlchemy merged 1 commit into
masterfrom
refactor/storage-remote-replay-async
Oct 17, 2026
Merged

ScriptedAlchemy merged 1 commit into
masterfrom
refactor/storage-remote-replay-async

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Step 5 of the storage redesign (docs/plans/storage/README.md).

The tracedecay-remote-replay actor thread is deleted. The remote replay port and the remote exact-observation query port are now async, so neither parks a thread on the writer or on reader admission, and no native_runtime::block_on remains on those paths. Step 1 (#4118) left the remote query port synchronous and bridged it with block_on; this removes that bridge. The change cascades through the remote protocol trait family and its implementations.

Test plan

  • Targeted Bazel unit tests and suites for the touched crates, after rebasing onto master
  • bazel build --config=clippy on the touched crates
  • rustfmt_check_2024 on the touched crates

Devin Review

@changeset-bot

changeset-bot Bot commented Oct 17, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 454ca32

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

Devin Review

.begin_replay_attempt(event_id, observed_at)
.map_err(RemoteReplayApplicationErrorV1::Persistence)?;
let operation_error = match operation(replay_attempt) {
let operation_error = match operation(replay_attempt).await {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Cancelled replay strands its attempt

If a caller drops replay during the transaction await, with_replay_attempt skips cleanup. The open spool marker rejects subsequent replays until startup recovery.

Learn more

A replay attempt writes a durable open marker before it submits the project-store transaction. The async transaction can yield, so dropping a direct caller's future at that point drops with_replay_attempt without calling abandon_replay_attempt. Later begin_replay_attempt rejects the still-open marker, even if the project write completed. Only recover_interrupted_replay_attempts releases it on a later recovery pass. HTTP's detached blocking-pool execution keeps that particular caller alive, but the exported async service has no such ownership guarantee.

Example: A client polls service.replay until its project-store commit is pending, then cancels the task. A retry of the same event gets Corruption rather than a duplicate receipt, until the node runs startup recovery.

Recommended fix: Give the attempt operation independent settlement ownership before opening its marker, or install a cancellation-safe cleanup guard that abandons the exact attempt on drop without racing a still-running commit. Test dropping the future while commit is pending, then retry the event without restarting the store.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +409 to +413
tracedecay_rusqlite_runtime::native_runtime::on_blocking_pool(async move {
service
.execute_controlled(request.request, credential, control)
.await
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Pending remote requests occupy blocking threads

When project-store dispatches await, on_blocking_pool parks one thread per remote request. Enough pending requests exhaust the bounded pool, delaying unrelated remote operations.

Learn more

The route moves the entire async protocol future onto one blocking-pool thread. on_blocking_pool parks that thread whenever the future yields. Replay, query, and promotion now await project-store operations inside that future, so they retain a thread during reader admission or writer waits rather than freeing it. The pool has a 512-thread ceiling, shared with other synchronous storage work.

Example: If 512 remote queries wait on project-store reads, each holds a parked pool thread. Another remote request cannot begin its synchronous credential or storage work until one of those queries finishes.

Recommended fix: Isolate only synchronous exact-SQL segments on the blocking pool and poll project-store awaits on an async executor, while preserving detached settlement of accepted writes. Cover many pending project-store requests with an independent synchronous operation to demonstrate that the blocking pool remains available.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@ScriptedAlchemy
ScriptedAlchemy merged commit 3a8a08d into master Oct 17, 2026
15 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant