Skip to content

feat(spider-scheduler): Add gRPC scheduler server: - #369

Merged
LinZhihao-723 merged 23 commits into
y-scope:mainfrom
sitaowang1998:scheduler-grpc-server
Jul 6, 2026
Merged

feat(spider-scheduler): Add gRPC scheduler server:#369
LinZhihao-723 merged 23 commits into
y-scope:mainfrom
sitaowang1998:scheduler-grpc-server

Conversation

@sitaowang1998

@sitaowang1998 sitaowang1998 commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator
  • Add gPRC service implementation.
  • Add server main entry and service-level config.
  • Update scheduler-client error handling in spider-execution-manager.

Description

This PR:

  • Adds scheduler requests unpack.
  • Adds scheduler gRPC service handlers and error mapping to gRPC status.
  • Adds scheduler gRPC server binary.

Note

This PR does not add new tests.

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

  • GitHub workflows pass.

Summary by CodeRabbit

  • New Features
    • Added a scheduler gRPC server CLI entrypoint that boots from a YAML config file.
    • Implemented gRPC handling for next task, heartbeat, and shutdown requests.
    • Expanded scheduler configuration with top-level server settings (including storage endpoint and connection pool sizing).
  • Bug Fixes
    • Improved protobuf-to-core task assignment conversion.
    • Enhanced gRPC error mapping and cancellation/shutdown behavior for scheduler RPCs.
  • Tests
    • Added unit tests for task assignment conversion and gRPC status-to-error mapping.

@sitaowang1998
sitaowang1998 requested a review from a team as a code owner July 1, 2026 03:13
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds scheduler protobuf conversions and unpacking, a tonic gRPC adapter and server binary, shared server configuration, and client-side gRPC error classification for scheduler responses.

Changes

Scheduler gRPC server

Layer / File(s) Summary
Proto-to-core assignment conversion
components/spider-proto-rust/src/assignment.rs, components/spider-proto-rust/src/unpack/mod.rs, components/spider-proto-rust/src/unpack/scheduler.rs
Adds From<ProtoTaskAssignmentRecord> for TaskAssignmentRecord, registers the scheduler unpack module, and implements RequestUnpack for scheduler requests.
GrpcSchedulerService adapter
components/spider-scheduler/src/grpc.rs, components/spider-scheduler/src/lib.rs
Adds GrpcSchedulerService, maps scheduler errors to tonic Status, converts next_task responses, and exports the new gRPC module.
Server configuration and derives
components/spider-scheduler/src/config.rs, components/spider-scheduler/src/runtime.rs, components/spider-scheduler/src/execution_manager_registry.rs, components/spider-scheduler/src/lib.rs
Adds ServerConfig, updates related derives, and re-exports ServerConfig from the scheduler crate.
gRPC server binary and Cargo wiring
components/spider-scheduler/src/bin/grpc_server.rs, components/spider-scheduler/Cargo.toml
Adds the spider_scheduler_grpc_server binary, config loading, storage connection, shutdown handling, and dependency/bin target updates.
Scheduler client error classification
components/spider-execution-manager/src/client/grpc/scheduler.rs, components/spider-execution-manager/src/client/scheduler.rs
Adds SchedulerError::Server, maps tonic status codes to transport vs server errors, and updates client docs/tests.
Storage gRPC doc update
components/spider-storage/src/grpc.rs
Updates the module doc comment to describe the storage service adapter.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • y-scope/spider#342: Adds related protobuf-to-core task assignment handling used by the scheduler RPC flow.
  • y-scope/spider#365: Introduces scheduler service state and error types consumed by the new gRPC adapter.
  • y-scope/spider#350: Touches the same task-assignment protocol path used by the new unpacking and conversion code.

Suggested reviewers: LinZhihao-723

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a gRPC scheduler server to spider-scheduler.
✨ 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.

@sitaowang1998

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

🧹 Nitpick comments (4)
components/spider-proto-rust/src/unpack/scheduler.rs (1)

22-32: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Unbounded client-supplied wait_time_ms can hold a request open indefinitely.

Duration::from_millis(self.wait_time_ms) accepts any client-supplied u64 unmodified and is later passed straight to dispatch_source.dequeue(wait_time). A malicious or buggy client can request an effectively unbounded wait (e.g., u64::MAX ms), tying up server resources for that RPC until an assignment appears or the connection drops. Consider clamping wait_time_ms to a sane maximum before converting to Duration.

🛡️ Proposed clamp
+const MAX_WAIT_TIME_MS: u64 = 60_000;
+
 impl RequestUnpack for NextTaskRequest {
     type Unpacked = (ExecutionManagerId, Option<TaskAssignmentRecord>, Duration);

     fn unpack(self) -> Result<Self::Unpacked, UnpackError> {
         Ok((
             ExecutionManagerId::from(self.execution_manager_id),
             self.prev_assignment.map(ProtoTaskAssignmentRecord::into),
-            Duration::from_millis(self.wait_time_ms),
+            Duration::from_millis(self.wait_time_ms.min(MAX_WAIT_TIME_MS)),
         ))
     }
 }
🤖 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-proto-rust/src/unpack/scheduler.rs` around lines 22 - 32,
The NextTaskRequest::unpack path currently converts client-controlled
wait_time_ms directly into a Duration, allowing an effectively unbounded wait.
Update NextTaskRequest::unpack to clamp self.wait_time_ms to a sensible maximum
before calling Duration::from_millis, and keep the rest of the unpacked tuple
unchanged. Make the limit explicit near the Duration::from_millis conversion so
the behavior is easy to find and maintain.
components/spider-scheduler/src/grpc.rs (1)

161-212: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor inconsistency: TAG constant only used in next_task.

next_task defines a local const TAG while heartbeat/shutdown pass string literals directly to service_error_handler. Purely cosmetic, no functional issue.

🤖 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-scheduler/src/grpc.rs` around lines 161 - 212, The local
TAG constant in next_task is inconsistent with heartbeat and shutdown, which
pass string literals directly to service_error_handler. Clean this up by either
reusing a shared tag symbol across next_task, heartbeat, and shutdown, or by
removing TAG and passing the same literal style everywhere so the error handling
calls are uniform in grpc.rs.
components/spider-scheduler/src/bin/grpc_server.rs (2)

44-59: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No SIGTERM handling for graceful shutdown.

Shutdown is only wired to the cancellation token and Ctrl-C (SIGINT). In containerized/orchestrated deployments (e.g., Kubernetes, systemd), shutdown is typically signalled via SIGTERM, which this server would not currently handle gracefully, likely resulting in a hard kill instead of a clean drain/stop.

🤖 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-scheduler/src/bin/grpc_server.rs` around lines 44 - 59, The
gRPC server shutdown path in serve_with_shutdown only reacts to the cancellation
token and tokio::signal::ctrl_c, so SIGTERM is not handled. Update the shutdown
future in grpc_server.rs to also listen for SIGTERM alongside Ctrl-C and
cancellation_token, and trigger cancellation when SIGTERM is received. Use the
existing serve_with_shutdown, cancellation_token, and select! shutdown block so
the server can drain cleanly in orchestrated environments.

61-64: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Stop error is silently swallowed when serve also fails.

stop_result is computed unconditionally, but if serve_result is Err, the ? on line 62 returns before line 63 is evaluated, so any error from runtime.stop() is dropped without ever being surfaced (not even logged).

Suggested fix
     let stop_result = runtime.stop().await;
-    serve_result?;
-    stop_result?;
+    if let Err(ref error) = stop_result {
+        tracing::error!(error = %error, "Failed to stop scheduler runtime cleanly.");
+    }
+    serve_result?;
+    stop_result?;
     Ok(())
🤖 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-scheduler/src/bin/grpc_server.rs` around lines 61 - 64, The
grpc_server main flow is dropping a runtime.stop() failure whenever serve_result
is Err because the serve_result? return short-circuits before stop_result? is
reached. Update the grpc_server logic to evaluate and report both outcomes
explicitly in this block around runtime.stop(), serve_result, and stop_result,
so a stop error is not silently lost even when serving also fails.
🤖 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 `@components/spider-proto-rust/src/unpack/scheduler.rs`:
- Around line 22-32: The NextTaskRequest::unpack path currently converts
client-controlled wait_time_ms directly into a Duration, allowing an effectively
unbounded wait. Update NextTaskRequest::unpack to clamp self.wait_time_ms to a
sensible maximum before calling Duration::from_millis, and keep the rest of the
unpacked tuple unchanged. Make the limit explicit near the Duration::from_millis
conversion so the behavior is easy to find and maintain.

In `@components/spider-scheduler/src/bin/grpc_server.rs`:
- Around line 44-59: The gRPC server shutdown path in serve_with_shutdown only
reacts to the cancellation token and tokio::signal::ctrl_c, so SIGTERM is not
handled. Update the shutdown future in grpc_server.rs to also listen for SIGTERM
alongside Ctrl-C and cancellation_token, and trigger cancellation when SIGTERM
is received. Use the existing serve_with_shutdown, cancellation_token, and
select! shutdown block so the server can drain cleanly in orchestrated
environments.
- Around line 61-64: The grpc_server main flow is dropping a runtime.stop()
failure whenever serve_result is Err because the serve_result? return
short-circuits before stop_result? is reached. Update the grpc_server logic to
evaluate and report both outcomes explicitly in this block around
runtime.stop(), serve_result, and stop_result, so a stop error is not silently
lost even when serving also fails.

In `@components/spider-scheduler/src/grpc.rs`:
- Around line 161-212: The local TAG constant in next_task is inconsistent with
heartbeat and shutdown, which pass string literals directly to
service_error_handler. Clean this up by either reusing a shared tag symbol
across next_task, heartbeat, and shutdown, or by removing TAG and passing the
same literal style everywhere so the error handling calls are uniform in
grpc.rs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2e12ec90-561a-4584-889b-6dfd122af107

📥 Commits

Reviewing files that changed from the base of the PR and between dca2b8a and 41f0a80.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • components/spider-proto-rust/src/assignment.rs
  • components/spider-proto-rust/src/unpack/mod.rs
  • components/spider-proto-rust/src/unpack/scheduler.rs
  • components/spider-scheduler/Cargo.toml
  • components/spider-scheduler/src/bin/grpc_server.rs
  • components/spider-scheduler/src/config.rs
  • components/spider-scheduler/src/execution_manager_registry.rs
  • components/spider-scheduler/src/grpc.rs
  • components/spider-scheduler/src/lib.rs
  • components/spider-scheduler/src/runtime.rs

@LinZhihao-723 LinZhihao-723 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm ok with some other small mistakes, but I'm not ok with:

  • When it's obvious that you didn't read AI-generated docstring. This is a huge RED flag: assuming you are marked on each PR, every time this is found it should be an auto fail.
    • Even it's just docstrings for config types, it is still your responsibility to go through the every single line AI generates.
  • When I've done sth as an example in some other PRs but you didn't follow. I think you should review my PRs more carefully to get a sense of "if I need to do sth similar, where should I get started from".
    • On the other hand, the gRPC side is in a relative better shape and I think that's an improvement.

Comment on lines +18 to +21
///
/// Wraps the scheduler [`RuntimeConfig`] (which also supplies the gRPC listen `host`/`port`) and
/// adds the storage endpoint and connection-pool size that [`crate::create_runtime`] deliberately
/// leaves out, since it receives the storage client as a parameter.

Copy link
Copy Markdown
Member

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 this is your judgment to keep it, or you just missed it: this part is 100% AI-generated and it should be removed.

/// The number of connections per pool used to reach the storage service.
pub storage_connection_pool_size: NonZeroUsize,

/// The scheduler runtime configuration (also supplies the gRPC listen host/port).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do you think (also supplies the gRPC listen host/port). is a good docstring?
It's expected that this kind of docstring is reviewed as part of your self-review process.

/// The storage gRPC endpoint the scheduler registers with and polls for ready tasks.
pub storage_endpoint: EndpointConfig,

/// The number of connections per pool used to reach the storage service.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We already have an example of how to document this, right?

/// The number of connections each gRPC client pool eagerly establishes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I have implemented the EM binary as an example and asked you to review in #362.
And clearly this PR doesn't follow what I've done there.

Comment thread components/spider-scheduler/src/grpc.rs Outdated
Comment on lines +115 to +127
SchedulerServiceError::EMRegistry(
ExecutionManagerRegistryError::TaskAssignmentNotFound(em_id, assignment_id),
) => {
tracing::warn!(
error = % error,
service = SERVICE_NAME,
tag,
em_id = % em_id,
assignment_id = % assignment_id,
"Task assignment not found."
);
Status::not_found("task assignment not found")
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This can never happen for user-interfacing API.

Comment thread components/spider-scheduler/src/grpc.rs Outdated
Comment on lines +93 to +102
SchedulerServiceError::Scheduler(SchedulerError::InvalidSessionId(session_id)) => {
tracing::warn!(
error = % error,
service = SERVICE_NAME,
tag,
session_id,
"Invalid session ID."
);
Status::failed_precondition(error.to_string())
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This can never happen for user-interfacing API.

Comment on lines +161 to +211
async fn next_task(
&self,
request: Request<scheduler::NextTaskRequest>,
) -> Result<Response<NextTaskResponse>, Status> {
const TAG: &str = "next_task";

let (em_id, prev_assignment, wait_time) = request.into_inner().unpack()?;
tracing::info!(em_id = em_id.get(), "NextTask request received.");

match self
.inner
.next_task(em_id, prev_assignment, wait_time)
.await
{
Ok(Some((session_id, assignment))) => Ok(Response::new(make_next_task_response(
assignment,
self.inner.scheduler_id(),
session_id,
))),
Ok(None) => Ok(Response::new(NextTaskResponse {
result: Some(next_task_response::Result::NoTask(common::Void {})),
})),
Err(error) => Err(self.service_error_handler(error, TAG)),
}
}

async fn heartbeat(
&self,
request: Request<scheduler::HeartbeatRequest>,
) -> Result<Response<common::Void>, Status> {
let em_id = request.into_inner().unpack()?;
tracing::info!(em_id = em_id.get(), "Heartbeat request received.");

match self.inner.heartbeat(em_id).await {
Ok(()) => Ok(Response::new(common::Void {})),
Err(error) => Err(self.service_error_handler(error, "heartbeat")),
}
}

async fn shutdown(
&self,
request: Request<scheduler::ShutdownRequest>,
) -> Result<Response<common::Void>, Status> {
let (em_id, prev_assignments) = request.into_inner().unpack()?;
tracing::info!(em_id = em_id.get(), "Shutdown request received.");

match self.inner.shutdown(em_id, prev_assignments).await {
Ok(()) => Ok(Response::new(common::Void {})),
Err(error) => Err(self.service_error_handler(error, "shutdown")),
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: I don't think the current implementation is wrong, but I will refactor it into inspect_err form to be consistent with the storage server side.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The client side update is missing: I don't think the current scheduler client on the EM side handles the status returns properly. That should be included as part of this PR.

@LinZhihao-723 LinZhihao-723 changed the title feat(spider-scheduler): Add gRPC binary. feat(spider-scheduler): Add gRPC scheduler server: Jul 6, 2026

@LinZhihao-723 LinZhihao-723 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Directly modified the PR title.

@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 (3)
components/spider-scheduler/src/grpc.rs (2)

41-185: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No tests added for error-status mapping and RPC handlers.

service_error_handler and the next_task/heartbeat/shutdown handlers implement critical error-to-Status mapping that downstream (spider-execution-manager) relies on to classify SchedulerError::Server vs Transport. The PR notes no new tests were added; unit tests covering each match arm (especially the NOT_FOUND and cancellation-on-Internal paths) would guard this contract.

🤖 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-scheduler/src/grpc.rs` around lines 41 - 185, Add unit
tests for GrpcSchedulerService::service_error_handler and the SchedulerService
RPC methods next_task, heartbeat, and shutdown. Cover each status-mapping
branch, especially ExecutionManagerRegistryError::EmNotFound returning NOT_FOUND
and SchedulerError::Internal triggering cancellation_token.cancel() and
INTERNAL. Also verify the handlers propagate mapped Status errors correctly from
inner.next_task, inner.heartbeat, and inner.shutdown so the downstream error
classification contract stays intact.

96-105: 🩺 Stability & Availability | 🔵 Trivial

Single request's internal error cancels the entire scheduler service.

Any SchedulerError::Internal from a single RPC triggers self.cancellation_token.cancel(), shutting down the whole service, not just failing that request. This appears intentional (log message says "Cancelling service"), but it's worth confirming this fail-fast blast radius is the desired behaviour and that operators have alerting on this path, since one bad request can take down the scheduler for all execution managers.

🤖 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-scheduler/src/grpc.rs` around lines 96 - 105, The
`SchedulerServiceError::Scheduler(SchedulerError::Internal(e))` branch in
`SchedulerService` is cancelling the shared `cancellation_token`, which turns
one RPC failure into a full service shutdown. Revisit this fail-fast behavior
and either limit the failure to the current request by removing the global
cancel call or make the shutdown path explicitly intentional with stronger
operator-facing handling; keep the logic localized around the `internal` error
arm and `cancellation_token.cancel()`.
components/spider-execution-manager/src/client/grpc/scheduler.rs (1)

144-174: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a DeadlineExceeded test case.

Tests cover Unavailable, Internal, and NotFound, but not DeadlineExceeded, which is the code most likely to reveal the classification gap noted above.

🤖 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/client/grpc/scheduler.rs` around
lines 144 - 174, Add a test in the scheduler.rs test module to cover
Status::deadline_exceeded in status_to_error, since the current coverage only
verifies Unavailable, Internal, and NotFound. Use the existing test pattern in
status_maps_unavailable_to_transport and status_maps_internal_to_server, and
assert the returned SchedulerError variant matches the intended classification
for DeadlineExceeded so the mapping behavior is explicitly exercised.
🤖 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/client/grpc/scheduler.rs`:
- Around line 9-13: Update status_to_error in scheduler.rs so it maps
tonic::Code::DeadlineExceeded to SchedulerError::Transport as well, alongside
Code::Unavailable. Keep the existing handling in the scheduler client path
unchanged otherwise, and make sure the match in status_to_error clearly treats
deadline expiries as transport-level failures for retry/backoff behavior.

---

Nitpick comments:
In `@components/spider-execution-manager/src/client/grpc/scheduler.rs`:
- Around line 144-174: Add a test in the scheduler.rs test module to cover
Status::deadline_exceeded in status_to_error, since the current coverage only
verifies Unavailable, Internal, and NotFound. Use the existing test pattern in
status_maps_unavailable_to_transport and status_maps_internal_to_server, and
assert the returned SchedulerError variant matches the intended classification
for DeadlineExceeded so the mapping behavior is explicitly exercised.

In `@components/spider-scheduler/src/grpc.rs`:
- Around line 41-185: Add unit tests for
GrpcSchedulerService::service_error_handler and the SchedulerService RPC methods
next_task, heartbeat, and shutdown. Cover each status-mapping branch, especially
ExecutionManagerRegistryError::EmNotFound returning NOT_FOUND and
SchedulerError::Internal triggering cancellation_token.cancel() and INTERNAL.
Also verify the handlers propagate mapped Status errors correctly from
inner.next_task, inner.heartbeat, and inner.shutdown so the downstream error
classification contract stays intact.
- Around line 96-105: The
`SchedulerServiceError::Scheduler(SchedulerError::Internal(e))` branch in
`SchedulerService` is cancelling the shared `cancellation_token`, which turns
one RPC failure into a full service shutdown. Revisit this fail-fast behavior
and either limit the failure to the current request by removing the global
cancel call or make the shutdown path explicitly intentional with stronger
operator-facing handling; keep the logic localized around the `internal` error
arm and `cancellation_token.cancel()`.
🪄 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: 475c2a43-c6b5-46cb-98d4-e5924230f7d1

📥 Commits

Reviewing files that changed from the base of the PR and between 3fe90a5 and 5574bfe.

📒 Files selected for processing (6)
  • components/spider-execution-manager/src/client/grpc/scheduler.rs
  • components/spider-execution-manager/src/client/scheduler.rs
  • components/spider-scheduler/src/bin/grpc_server.rs
  • components/spider-scheduler/src/config.rs
  • components/spider-scheduler/src/grpc.rs
  • components/spider-storage/src/grpc.rs
✅ Files skipped from review due to trivial changes (1)
  • components/spider-storage/src/grpc.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/spider-scheduler/src/bin/grpc_server.rs

Comment on lines +9 to +13
use tonic::{
Code,
Status,
transport::{Channel, Endpoint},
};

@coderabbitai coderabbitai Bot Jul 6, 2026

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== scheduler.rs ==\n'
cat -n components/spider-execution-manager/src/client/grpc/scheduler.rs | sed -n '1,220p'

printf '\n== search SchedulerError and DeadlineExceeded ==\n'
rg -n "SchedulerError::Transport|SchedulerError::Server|DeadlineExceeded|status_to_error|timed out" components/spider-execution-manager -S

Repository: y-scope/spider

Length of output: 12302


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== grpc/liveness.rs timeout/error mapping ==\n'
cat -n components/spider-execution-manager/src/client/grpc/liveness.rs | sed -n '90,260p'

printf '\n== grpc/storage.rs timeout/error mapping ==\n'
cat -n components/spider-execution-manager/src/client/grpc/storage.rs | sed -n '145,230p'

printf '\n== direct search for DeadlineExceeded in grpc clients ==\n'
rg -n "DeadlineExceeded|Unavailable" components/spider-execution-manager/src/client/grpc -S

Repository: y-scope/spider

Length of output: 9517


Map Code::DeadlineExceeded to Transport too. SchedulerError::Transport covers requests that timed out, but status_to_error only treats UNAVAILABLE as transport, so gRPC deadline expiries are currently classified as Server instead of Transport. That skews retry/backoff handling.

Suggested fix
 fn status_to_error(status: &Status) -> SchedulerError {
     match status.code() {
-        Code::Unavailable => to_transport_error(status.message()),
+        Code::Unavailable | Code::DeadlineExceeded => to_transport_error(status.message()),
         _ => SchedulerError::Server(status.message().to_owned()),
     }
 }
🤖 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/client/grpc/scheduler.rs` around
lines 9 - 13, Update status_to_error in scheduler.rs so it maps
tonic::Code::DeadlineExceeded to SchedulerError::Transport as well, alongside
Code::Unavailable. Keep the existing handling in the scheduler client path
unchanged otherwise, and make sure the match in status_to_error clearly treats
deadline expiries as transport-level failures for retry/backoff behavior.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you create an issue for this? Might be worth to apply this change to all the other clients as well.

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "to_transport_error|status_to_error|Code::Unavailable" components/spider-execution-manager/src/client/grpc -S

Length of output: 13350


🏁 Script executed:

#!/bin/bash
set -euo pipefail

body=$(cat <<'EOF'
## Summary

`status_to_error` in `components/spider-execution-manager/src/client/grpc/scheduler.rs` only maps `Code::Unavailable` to a transport-level error. `Code::DeadlineExceeded` (a timed-out request) currently falls through to the generic "server" error variant, which skews retry/backoff handling that is meant to apply to transport failures.

The same `status_to_error` pattern (and the same gap) exists in the other gRPC clients under `components/spider-execution-manager/src/client/grpc/`:
- `scheduler.rs` (`SchedulerError`)
- `storage.rs` (`StorageResponseError`)
- `liveness.rs` (`LivenessResponseError`)

## Required change

For each of the three `status_to_error` functions above, add `Code::DeadlineExceeded` alongside `Code::Unavailable` (or the equivalent transport-classified branch) so that deadline-exceeded responses are classified as transport errors rather than server errors, keeping retry/backoff behaviour consistent across all scheduler/storage/liveness gRPC clients.

## Affected files

- `components/spider-execution-manager/src/client/grpc/scheduler.rs`
- `components/spider-execution-manager/src/client/grpc/storage.rs`
- `components/spider-execution-manager/src/client/grpc/liveness.rs`

## Acceptance criteria

- `Code::DeadlineExceeded` is classified as a transport error in all three `status_to_error` implementations.
- Unit tests are added/updated in each file's `#[cfg(test)]` module asserting that a `Status::deadline_exceeded(...)` maps to the transport error variant.

## References

- PR: https://github.com/y-scope/spider/pull/369
- Review comment: https://github.com/y-scope/spider/pull/369#discussion_r3526014537
- Requested by: `@LinZhihao-723`
EOF
)

gh issue create \
  --repo y-scope/spider \
  --title "Classify Code::DeadlineExceeded as a transport error across scheduler/storage/liveness gRPC clients" \
  --body "$body" \
  --assignee LinZhihao-723

Length of output: 2133


I've created #375 to track applying the Code::DeadlineExceeded → transport-error mapping consistently across scheduler.rs, storage.rs, and liveness.rs, and assigned it to you.


🧬 Code Graph Analysis Results

components/spider-execution-manager/src/client/grpc/scheduler.rs

GrpcSchedulerClient::connect

Lines 35-46

pub async fn connect(
    endpoint: Endpoint,
    pool_size: NonZeroUsize,
) -> Result<Self, SchedulerError> {
    let connection_pool = ConnectionPool::connect(endpoint, pool_size, |channel| {
        SchedulerServiceClient::new(channel)
    })
    .await
    .map_err(to_transport_error)?;

    Ok(Self { connection_pool })
}

SchedulerClient for GrpcSchedulerClient (next_task, heartbeat, shutdown)

Lines 51-109

async fn next_task(
    &self,
    em_id: ExecutionManagerId,
    prev_assignment: Option<TaskAssignmentRecord>,
    wait_time_ms: u64,
) -> Result<SchedulerResponse, SchedulerError> {
    loop {
        let response = self
            .connection_pool
            .get_client()
            .next_task(scheduler::NextTaskRequest {
                execution_manager_id: em_id.get(),
                prev_assignment: prev_assignment.map(Into::into),
                wait_time_ms,
            })
            .await
            .map_err(|status| status_to_error(&status))?
            .into_inner();

        let assignment: Option<SchedulerResponse> =
            response.try_into().map_err(to_protocol_error)?;
        if let Some(assignment) = assignment {
            return Ok(assignment);
        }
    }
}

async fn heartbeat(&self, em_id: ExecutionManagerId) -> Result<(), SchedulerError> {
    self.connection_pool
        .get_client()
        .heartbeat(scheduler::HeartbeatRequest {
            execution_manager_id: em_id.get(),
        })
        .await
        .map_err(|status| status_to_error(&status))?;
    Ok(())
}

async fn shutdown(
    &self,
    em_id: ExecutionManagerId,
    prev_assignments: Vec<TaskAssignmentRecord>,
) {
    if let Err(error) = self
        .connection_pool
        .get_client()
        .shutdown(scheduler::ShutdownRequest {
            execution_manager_id: em_id.get(),
            prev_assignments: prev_assignments.into_iter().map(Into::into).collect(),
        })
        .await
    {
        tracing::warn!(
            em_id = ? em_id,
            error = ? error,
            "Failed to notify scheduler shutdown."
        );
    }
}

gRPC Status error mapping helpers

Lines 112-131

fn status_to_error(status: &Status) -> SchedulerError {
    match status.code() {
        Code::Unavailable => to_transport_error(status.message()),
        _ => SchedulerError::Server(status.message().to_owned()),
    }
}

fn to_transport_error(error: impl std::fmt::Display) -> SchedulerError {
    SchedulerError::Transport(error.to_string())
}

fn to_protocol_error(error: impl std::fmt::Display) -> SchedulerError {
    SchedulerError::Protocol(error.to_string())
}

components/spider-execution-manager/src/client/scheduler.rs

SchedulerClient trait (interface for all scheduler clients)

Lines 30-90

pub trait SchedulerClient: Send + Sync {
    async fn next_task(
        &self,
        em_id: ExecutionManagerId,
        prev_assignment: Option<TaskAssignmentRecord>,
        wait_time_ms: u64,
    ) -> Result<SchedulerResponse, SchedulerError>;

    async fn heartbeat(&self, em_id: ExecutionManagerId) -> Result<(), SchedulerError>;

    async fn shutdown(
        &self,
        em_id: ExecutionManagerId,
        prev_assignments: Vec<TaskAssignmentRecord>,
    );
}

components/spider-utils/src/grpc/client.rs

ConnectionPool::connect (used by multiple gRPC clients)

Lines 44-65

pub async fn connect<ClientFactory: Fn(Channel) -> GrpcServiceClientType>(
    endpoint: Endpoint,
    pool_size: NonZeroUsize,
    client_factory: ClientFactory,
) -> Result<Self, Error> {
    let mut connections = Vec::with_capacity(pool_size.get());
    for _ in 0..pool_size.get() {
        let channel = endpoint
            .clone()
            .connect()
            .await
            .map_err(Error::TonicTransport)?;
        connections.push(client_factory(channel));
    }

    Ok(Self {
        inner: Arc::new(ConnectionPoolInner {
            connections,
            next: AtomicUsize::new(0),
        }),
    })
}

components/spider-execution-manager/src/client/grpc/storage.rs

GrpcStorageClient::connect (pattern parallel to scheduler client)

Lines 44-55

pub async fn connect(
    endpoint: Endpoint,
    pool_size: NonZeroUsize,
) -> Result<Self, StorageResponseError> {
    let connection_pool = ConnectionPool::connect(endpoint, pool_size, |channel| {
        TaskInstanceManagementServiceClient::new(channel)
    })
    .await
    .map_err(to_transport_error)?;

    Ok(Self { connection_pool })
}

status_to_error mapping (parallel error-handling differences)

Lines 152-160

fn status_to_error(status: &Status) -> StorageResponseError {
    match status.code() {
        Code::NotFound => StorageResponseError::StaleSession(status.message().to_owned()),
        Code::FailedPrecondition => StorageResponseError::CacheStale(status.message().to_owned()),
        Code::InvalidArgument => StorageResponseError::InvalidInput(status.message().to_owned()),
        Code::Unavailable => to_transport_error(status.message()),
        _ => StorageResponseError::Server(status.message().to_owned()),
    }
}

to_transport_error

Lines 167-169

fn to_transport_error(error: impl std::fmt::Display) -> StorageResponseError {
    StorageResponseError::Transport(error.to_string())
}

components/spider-execution-manager/src/client/grpc/liveness.rs

GrpcLivenessClient::connect (pattern parallel to scheduler client)

Lines 38-49

pub async fn connect(
    endpoint: Endpoint,
    pool_size: NonZeroUsize,
) -> Result<Self, LivenessResponseError> {
    let connection_pool = ConnectionPool::connect(endpoint, pool_size, |channel| {
        ExecutionManagerLivenessServiceClient::new(channel)
    })
    .await
    .map_err(to_transport_error)?;

    Ok(Self { connection_pool })
}

status_to_error

Lines 97-103

fn status_to_error(status: &Status) -> LivenessResponseError {
    match status.code() {
        Code::FailedPrecondition => LivenessResponseError::MarkedDead,
        Code::InvalidArgument => LivenessResponseError::IllegalId(status.message().to_owned()),
        _ => LivenessResponseError::Transport(status.message().to_owned()),
    }
}

to_transport_error

Lines 137-139

fn to_transport_error(error: impl std::fmt::Display) -> LivenessResponseError {
    LivenessResponseError::Transport(error.to_string())
}

components/spider-proto-rust/src/generated/scheduler.rs

Request message structs

Lines 3-10

pub struct NextTaskRequest {
    #[prost(uint64, tag = "1")]
    pub execution_manager_id: u64,
    #[prost(message, optional, tag = "2")]
    pub prev_assignment: ::core::option::Option<TaskAssignmentRecord>,
    #[prost(uint64, tag = "3")]
    pub wait_time_ms: u64,
}

Lines 34-37

pub struct HeartbeatRequest {
    #[prost(uint64, tag = "1")]
    pub execution_manager_id: u64,
}

Lines 39-44

pub struct ShutdownRequest {
    #[prost(uint64, tag = "1")]
    pub execution_manager_id: u64,
    #[prost(message, repeated, tag = "2")]
    pub prev_assignments: ::prost::alloc::vec::Vec<TaskAssignmentRecord>,
}

SchedulerServiceClient<T> and constructor/connect

Lines 72-74

pub struct SchedulerServiceClient<T> {
        inner: tonic::client::Grpc<T>,
    }

Lines 75-85

impl SchedulerServiceClient<tonic::transport::Channel> {
    pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
    where
        D: TryInto<tonic::transport::Endpoint>,
        D::Error: Into<StdError>,
    {
        let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
        Ok(Self::new(conn))
    }
}

Lines 93-96

pub fn new(inner: T) -> Self {
    let inner = tonic::client::Grpc::new(inner);
    Self { inner }
}

RPC method calls on the generated client

Summary (from implementations at 151-198 and 175-222):

/// Methods on `SchedulerServiceClient<T>`:
/// - `next_task(&mut self, request: impl tonic::IntoRequest<super::NextTaskRequest>)
///      -> Result<tonic::Response<super::NextTaskResponse>, tonic::Status>`
///   Waits for service readiness (`self.inner.ready().await`) and performs a unary call
///   to path "/scheduler.SchedulerService/NextTask".
/// - `heartbeat(&mut self, request: impl tonic::IntoRequest<super::HeartbeatRequest>)
///      -> Result<tonic::Response<super::super::common::Void>, tonic::Status>`
///   Unary call to "/scheduler.SchedulerService/Heartbeat".
/// - `shutdown(&mut self, request: impl tonic::IntoRequest<super::ShutdownRequest>)
///      -> Result<tonic::Response<super::super::common::Void>, tonic::Status>`
///   Unary call to "/scheduler.SchedulerService/Shutdown".
/// Error handling: if `ready()` fails, returns `tonic::Status::unknown(...)`.

components/spider-core/src/types/scheduler.rs

TaskAssignmentRecord (used as input for scheduler client calls)

Lines 31-44

pub struct TaskAssignmentRecord {
    /// The unique ID of the task assignment.
    pub id: TaskAssignmentId,

    /// The scheduler where the record was issued from.
    pub from: SchedulerId,
}

impl TaskAssignmentRecord {
    #[must_use]
    pub const fn new(id: TaskAssignmentId, from: SchedulerId) -> Self {
        Self { id, from }
    }
}

🧠 Learnings used
Learnt from: LinZhihao-723
Repo: y-scope/spider PR: 288
File: components/spider-core/src/task/task_graph.rs:429-438
Timestamp: 2026-03-21T22:52:26.685Z
Learning: If a crate’s Cargo.toml sets `edition = "2024"`, treat Rust 2024 features as available without requiring separate feature flags for that syntax (e.g., `let_chains` is stabilized in Rust 1.85.0+). During code review, do not flag usage of `if let ... && let ...` (or other Rust 2024–stabilized features) as an MSRV/feature-gating concern when the crate is on `edition = "2024"`.

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