Skip to content

feat(spider-execution-manager): Add liveness actor with session ID tracker; Refactor integration tests to extract common helpers into test-utils. - #328

Merged
LinZhihao-723 merged 21 commits into
y-scope:mainfrom
LinZhihao-723:liveness-actor
Jun 2, 2026

Conversation

@LinZhihao-723

@LinZhihao-723 LinZhihao-723 commented May 22, 2026

Copy link
Copy Markdown
Member

Description

This PR depends on #327.

This PR adds the liveness actor and the shared session-tracker primitive that the rest of the execution-manager runtime will plug into.

spider_core::session::SessionTracker — a forward-only counter wrapping Arc<AtomicU64> for the runtime's view of storage's current session id. Cloneable, with current() / try_advance() semantics: writers always move the stored value forward via a CAS loop, and reads observe the latest committed value. Lives in spider-core so the future scheduler service can reuse the same primitive.

spider_execution_manager::liveness — a tokio-actor driving the periodic storage heartbeat:

  • A tokio::time::interval ticks every heartbeat_interval; each tick calls LivenessClient::heartbeat(em_id) and forwards storage's reply to the shared SessionTracker. The interval uses MissedTickBehavior::Skip as a defensive guard against starvation-induced burst-replay.
  • A LivenessCommand::Refresh lets the rest of the runtime ask for an off-schedule heartbeat. The command does not advance the tracker directly — storage's heartbeat reply is the only source of truth for the current session id, so the actor always re-checks rather than trusting the caller's observation.
  • interval.reset() runs at the end of every heartbeat call, so a Refresh-triggered heartbeat naturally rate-limits the next scheduled tick. Two consecutive heartbeats are never closer together than heartbeat_interval.
  • Terminal errors (MarkedDead / IllegalId) cancel the actor's CancellationToken, which the rest of the runtime will observe to tear everything down.
  • A storage reply with a session ID older than the locally tracked value is treated as a protocol invariant violation and also cancels the runtime.

This PR also creates test-utils under tests/huntsman to collect all reusable test helpers.

Checklist

  • The PR satisfies the contribution guidelines.
  • This is a breaking change and that has been indicated in the PR title, OR this isn't a
    breaking change.
  • Necessary docs have been updated, OR no docs need to be updated.

Validation performed

  • Ensure all workflows pass.
  • Add unit tests to cover basic actor behaviors.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added session tracking to maintain runtime session progression.
    • Introduced an execution manager with liveness heartbeats and automatic shutdown on terminal errors.
    • Added process-pool management to drive external task executors and recover from crashes/timeouts.
  • Tests

    • Large expansion of end-to-end integration tests covering success, task errors, crashes and timeouts.
    • New shared test utilities and subprocess harness for deterministic integration testing.
  • Chores

    • Workspace reorganized with additional test and component crates added.

@LinZhihao-723
LinZhihao-723 requested review from a team and sitaowang1998 as code owners May 22, 2026 22:01
@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8f1252ef-1b5d-43b9-a117-69fb2692f643

📥 Commits

Reviewing files that changed from the base of the PR and between b726a61 and d82aaec.

📒 Files selected for processing (1)
  • components/spider-core/src/session.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/spider-core/src/session.rs

Walkthrough

Adds a forward-only SessionTracker, LivenessClient types and a spawnable heartbeat actor, a subprocess executor test harness and MockLiveness, end-to-end tests for executor and process pool, and workspace/Cargo manifest updates to include new crates.

Changes

Execution Manager Liveness and Test Infrastructure

Layer / File(s) Summary
Session tracking foundation
components/spider-core/Cargo.toml, components/spider-core/src/lib.rs, components/spider-core/src/session.rs
SessionTracker wraps an Arc<AtomicU64> and uses a CAS loop to advance SessionId only forward, rejecting equal or backward moves; includes unit tests for single-thread advancement, stale rejection, and concurrent convergence.
Liveness client interface and crate wiring
components/spider-execution-manager/src/client/liveness.rs, components/spider-execution-manager/src/lib.rs, components/spider-execution-manager/Cargo.toml
LivenessClient async trait defines register and heartbeat methods; RegistrationResponse carries em_id and session_id; LivenessResponseError covers MarkedDead, Transport, and IllegalId. Execution-manager crate exposes client, liveness, process_pool; tokio-util feature rt added.
Liveness heartbeat actor
components/spider-execution-manager/src/liveness.rs
Tokio actor spawned on a dedicated task, driven by heartbeat interval ticks, command-channel refreshes, and a CancellationToken; updates SessionTracker on success, cancels runtime on MarkedDead/IllegalId, logs and retries on Transport; includes tests with a scripted mock client and deterministic Notify coordination.
Executor subprocess harness and payload builders
tests/huntsman/test-utils/Cargo.toml, tests/huntsman/test-utils/src/executor.rs
ExecutorHandle spawns spider-task-executor, performs length-delimited bincode framing for Request/Response, and provides helpers to build TaskContext, encode/decode inputs/outputs via msgpack; env helpers read SPIDER_TASK_EXECUTOR_BIN and SPIDER_TDL_PACKAGE_DIR.
Mock liveness client for unit testing
tests/huntsman/test-utils/src/mock.rs
MockLiveness is an Arc-backed cloneable mock that records register IPs, queues heartbeat responses or returns a default session, counts heartbeats, and provides wait_for_heartbeats for tests.
Test-utils crate and re-exports
tests/huntsman/test-utils/src/lib.rs
Creates test-utils crate and re-exports executor and mock utilities for downstream integration tests.
Executor subprocess end-to-end tests
tests/huntsman/task-executor/src/lib.rs, tests/huntsman/task-executor/tests/test_executor.rs, tests/huntsman/task-executor/tests/overhead_instrument.rs
Three ignored E2E tests spawn the executor, send framed Execute requests, and validate outcomes: success with decoded output, in-task failure with error payload inspection, and process crash observed by EOF and non-zero exit. Imports adjusted to use test-utils.
Process pool end-to-end integration tests
tests/huntsman/task-executor/tests/test_process_pool.rs
Four ignored tests validate ProcessPool::execute across success, in-task failure, executor crash recovery/respawn, and hard-timeout kill-and-respawn; includes helpers to build pool and requests and tuned timeouts/workloads.
Workspace manifest updates
Cargo.toml, tests/huntsman/task-executor/Cargo.toml, components/spider-core/Cargo.toml, components/spider-execution-manager/Cargo.toml
Adds components/spider-execution-manager and huntsman test crates to workspace members; moves task-executor deps to dev-dependencies, adds test-utils crate manifest, and updates tokio-util features where required.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • y-scope/spider#327: Introduces or overlaps with the same LivenessClient trait and response types used by the liveness actor and mocks.

Suggested reviewers

  • sitaowang1998
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding a liveness actor with session ID tracker and refactoring integration tests into test-utils.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
components/spider-execution-manager/src/process_pool.rs (1)

163-173: ⚡ Quick win

Serialize the request before taking the executor mutex.

build_request() only does local encoding, but it currently runs after Line 163 inside the same mutex scope that guards the child process. Moving it ahead of the lock keeps large input serializations and local encoding failures from extending head-of-line blocking on the single executor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/spider-execution-manager/src/process_pool.rs` around lines 163 -
173, Call build_request(request) before acquiring the executor mutex so local
serialization/encoding work does not hold the child-process lock; specifically,
move the build_request(request)? invocation out of the critical section that
surrounds self.handle.lock().await and handle.run(...).await, so you compute
frame_request (via build_request) first, then acquire the mutex
(self.handle.lock().await), get handle
(handle_guard.as_mut().ok_or(InternalError::NotRunning)?), log and call
handle.run(frame_request, hard_timeout).await.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/spider-execution-manager/src/process_pool.rs`:
- Around line 278-325: The current run function (async fn run(&mut self,
request: Request, hard_timeout: Duration) -> Outcome) only starts the
tokio::select! timeout after awaiting self.requests.send(...).await, so a
blocked send can prevent the hard_timeout from ever firing; change run to cover
the full send+receive window by moving the timeout to wrap both send and
response handling (e.g., use tokio::time::timeout(hard_timeout, async {
self.requests.send(Bytes::from(bytes)).await?; self.responses.next().await }) or
include the send future in the same tokio::select! alongside responses and the
sleep), ensuring the send call (self.requests.send) is protected by hard_timeout
and still returns Outcome::Timeout on expiration.

In `@components/spider-task-executor/src/bin/spider_task_executor.rs`:
- Around line 73-77: The package identifier is used directly to build a
filesystem path in the else branch (the block that calls manager.get(package)
and manager.load(&path)), allowing path traversal; before joining
pkg_dir.join(package) validate/sanitize `package` (e.g., reject empty strings,
any path separators like '/' or '\\', any ".." components, and allow only a safe
whitelist such as [A-Za-z0-9_-]); if validation fails return an error instead of
constructing the path; apply this check where you construct `path` and before
calling `manager.load` so only safe package names are used.

---

Nitpick comments:
In `@components/spider-execution-manager/src/process_pool.rs`:
- Around line 163-173: Call build_request(request) before acquiring the executor
mutex so local serialization/encoding work does not hold the child-process lock;
specifically, move the build_request(request)? invocation out of the critical
section that surrounds self.handle.lock().await and handle.run(...).await, so
you compute frame_request (via build_request) first, then acquire the mutex
(self.handle.lock().await), get handle
(handle_guard.as_mut().ok_or(InternalError::NotRunning)?), log and call
handle.run(frame_request, hard_timeout).await.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fed459f0-c414-490d-9e84-a0fc3c793720

📥 Commits

Reviewing files that changed from the base of the PR and between aadb9eb and 49b34d2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • Cargo.toml
  • components/spider-core/Cargo.toml
  • components/spider-core/src/lib.rs
  • components/spider-core/src/session.rs
  • components/spider-execution-manager/Cargo.toml
  • components/spider-execution-manager/src/client.rs
  • components/spider-execution-manager/src/client/liveness.rs
  • components/spider-execution-manager/src/client/scheduler.rs
  • components/spider-execution-manager/src/client/storage.rs
  • components/spider-execution-manager/src/lib.rs
  • components/spider-execution-manager/src/liveness.rs
  • components/spider-execution-manager/src/process_pool.rs
  • components/spider-task-executor/Cargo.toml
  • components/spider-task-executor/src/bin/spider_task_executor.rs
  • components/spider-task-executor/src/error.rs
  • components/spider-task-executor/src/lib.rs
  • components/spider-task-executor/src/manager.rs
  • components/spider-task-executor/src/protocol.rs
  • taskfiles/test.yaml
  • tests/huntsman/integration-test-tasks/Cargo.toml
  • tests/huntsman/integration-test-tasks/src/lib.rs
  • tests/huntsman/task-executor/Cargo.toml
  • tests/huntsman/task-executor/src/lib.rs
  • tests/huntsman/task-executor/tests/overhead_instrument.rs
  • tests/huntsman/task-executor/tests/test_executor.rs
  • tests/huntsman/task-executor/tests/test_process_pool.rs
  • tests/huntsman/tdl-integration/tests/complex.rs

Comment thread components/spider-execution-manager/src/process_pool.rs
Comment thread components/spider-task-executor/src/bin/spider_task_executor.rs
LinZhihao-723 and others added 3 commits May 26, 2026 21:43
Introduces tests/huntsman/test-utils, a shared support crate that consolidates the
executor subprocess harness, TDL wire-payload helpers, and in-process mock client
implementations (scheduler/storage/liveness) so multiple integration suites can reuse
them. task-executor-tests is converted to a thin test-only crate that depends on it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The scheduler and storage mocks are only needed by the execution-manager runtime
tests, which land on a later branch. Drop them here so this branch's test-utils
exposes just the liveness client mock that the liveness work uses.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@LinZhihao-723 LinZhihao-723 changed the title feat(spider-execution-manager): Add liveness actor with session ID tracker. feat(spider-execution-manager): Add liveness actor with session ID tracker; Refactor integration tests to extract common helpers into test-utils. May 27, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/huntsman/test-utils/src/mock.rs (1)

162-170: ⚡ Quick win

Validate em_id in the mock heartbeat path.

The mock currently accepts any manager ID, which can hide ID-propagation regressions in tests. Fail fast on mismatched IDs to keep the mock aligned with the client contract.

Proposed fix
     async fn heartbeat(
         &self,
-        _em_id: ExecutionManagerId,
+        em_id: ExecutionManagerId,
     ) -> Result<SessionId, LivenessResponseError> {
+        if em_id != self.inner.em_id {
+            return Err(LivenessResponseError::IllegalId);
+        }
         self.inner.heartbeat_count.fetch_add(1, Ordering::Relaxed);
         self.inner.heartbeat_notify.notify_waiters();
         let queued = lock(&self.inner.heartbeat_responses).pop_front();
         queued.unwrap_or_else(|| Ok(self.inner.default_session.load(Ordering::Relaxed)))
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/huntsman/test-utils/src/mock.rs` around lines 162 - 170, In heartbeat,
stop ignoring the ExecutionManagerId parameter and validate it against the
mock's expected ID (e.g., compare the incoming em_id to
self.inner.expected_em_id or self.inner.em_id), returning an appropriate
LivenessResponseError on mismatch and only proceeding with
heartbeat_count/notify/pop_front when they match; also remove the underscore
from the parameter name so the value is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/huntsman/test-utils/src/mock.rs`:
- Around line 162-170: In heartbeat, stop ignoring the ExecutionManagerId
parameter and validate it against the mock's expected ID (e.g., compare the
incoming em_id to self.inner.expected_em_id or self.inner.em_id), returning an
appropriate LivenessResponseError on mismatch and only proceeding with
heartbeat_count/notify/pop_front when they match; also remove the underscore
from the parameter name so the value is used.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a30c9f45-0de5-4f77-b528-cc277dc61535

📥 Commits

Reviewing files that changed from the base of the PR and between 49b34d2 and b3badb7.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • Cargo.toml
  • tests/huntsman/task-executor/Cargo.toml
  • tests/huntsman/task-executor/src/lib.rs
  • tests/huntsman/task-executor/tests/overhead_instrument.rs
  • tests/huntsman/task-executor/tests/test_executor.rs
  • tests/huntsman/task-executor/tests/test_process_pool.rs
  • tests/huntsman/test-utils/Cargo.toml
  • tests/huntsman/test-utils/src/executor.rs
  • tests/huntsman/test-utils/src/lib.rs
  • tests/huntsman/test-utils/src/mock.rs
✅ Files skipped from review due to trivial changes (3)
  • tests/huntsman/test-utils/Cargo.toml
  • tests/huntsman/task-executor/src/lib.rs
  • Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/huntsman/task-executor/Cargo.toml
  • tests/huntsman/task-executor/tests/overhead_instrument.rs
  • tests/huntsman/task-executor/tests/test_executor.rs

Comment thread tests/huntsman/test-utils/src/executor.rs
Comment thread components/spider-execution-manager/src/client/liveness.rs Outdated
Comment thread components/spider-core/src/session.rs Outdated
Comment on lines +134 to +139
if self.session_tracker.try_advance(session_id) {
tracing::info!(
from = previous,
to = session_id,
"Session advanced by heartbeat."
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think apart from bumping the id internally, we need a way to signal that heartbeat has been updated. Otherwise the user have to poll the session id.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm not sure if I got what u mean.
Which component do we need to signal for?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does execution manager need to stop running tasks when the session id changes? Or should we just let them run and reject them when they finish?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Similar to the process pool design: to keep things simple, we should just let it run and reject when they finish.
Adding a signal protocol is non-trivial, and the benefit is limited: a session bump should be rare in reality.

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.

2 participants