Skip to content

fix(spider-execution-manager): Send prev_assignment at most once per next_task call (fixes #429). - #430

Merged
LinZhihao-723 merged 3 commits into
y-scope:mainfrom
jackluo923:fix/em-resends-prev-assignment
Aug 7, 2026
Merged

fix(spider-execution-manager): Send prev_assignment at most once per next_task call (fixes #429).#430
LinZhihao-723 merged 3 commits into
y-scope:mainfrom
jackluo923:fix/em-resends-prev-assignment

Conversation

@jackluo923

@jackluo923 jackluo923 commented Aug 6, 2026

Copy link
Copy Markdown
Member

Description

Fixes #429.

GrpcSchedulerClient::next_task re-sent prev_assignment on every iteration of its long-poll loop, so the scheduler logged a warning for each iteration after the first. This calls Option::take on the field, which sends it on the first iteration and None on every later one. #429 carries the mechanism and the log output.

Not addressed here

When the first RPC fails at the transport layer, the completion is lost and the assignment stays in the scheduler's registry until the execution manager's liveness expires. That predates this change and is worth handling separately.

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

Cluster reproduction. A cluster of 1 storage, 1 scheduler, and 4 execution managers, backed by MariaDB 10.11.16, emitted a steady 3 warnings/sec once every job had reached Succeeded. Details in #429.

Suites. cargo test -p spider-execution-manager and task lint:check-rust pass on this branch.

…r `next_task` call (fixes y-scope#429).

`GrpcSchedulerClient::next_task` long-polls the scheduler in an internal loop
until a task becomes available, but re-sent the same `prev_assignment` on every
iteration. The scheduler completes an assignment by removing it from its
registry, so only the first iteration succeeded; each subsequent one made the
scheduler fail to complete an assignment it had already dropped, logging a
warning on every poll for as long as the execution manager stayed idle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jackluo923
jackluo923 requested review from a team and sitaowang1998 as code owners August 6, 2026 07:50
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The scheduler now consumes prev_assignment during the first poll. Later polls send None, which prevents repeated submission of the same completed assignment.

Changes

Scheduler polling

Layer / File(s) Summary
Consume the previous assignment once
components/spider-execution-manager/src/client/grpc/scheduler.rs
next_task makes prev_assignment mutable and uses take() so the assignment is included only in the first polling request.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • y-scope/spider#395: Modifies scheduler polling and assignment rescheduling through next_task.

Suggested reviewers: sitaowang1998

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The change sends prev_assignment only once per next_task call, which stops repeated completion attempts described in issue #429.
Out of Scope Changes check ✅ Passed The changes are limited to scheduler polling behaviour and directly support the requirements in issue #429.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: sending prev_assignment only once during each next_task call.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 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.

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

The fix is correct. Let's remove the unit tests as explained in my comment.

Comment on lines +152 to +289
use std::net::TcpListener as StdTcpListener;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;

use spider_core::types::id::SchedulerId;
use spider_core::types::id::TaskAssignmentId;
use spider_proto_rust::common;
use spider_proto_rust::common::TaskId as ProtoTaskId;
use spider_proto_rust::common::task_id::Kind as ProtoTaskIdKind;
use spider_proto_rust::scheduler::SchedulerAssignment;
use spider_proto_rust::scheduler::SchedulerService;
use spider_proto_rust::scheduler::SchedulerServiceServer;
use spider_proto_rust::scheduler::next_task_response;
use tonic::Request;
use tonic::Response;
use tonic::transport::Server;

use super::*;

/// The number of `NoTask` replies the fake scheduler sends before handing out an assignment.
const NUM_NO_TASK_REPLIES: usize = 3;

/// The maximum time spent waiting for the test server to accept connections.
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);

/// The delay between connection attempts while the test server is still binding.
const CONNECT_RETRY_DELAY: Duration = Duration::from_millis(50);

/// Connects to `endpoint`, retrying for up to [`CONNECT_TIMEOUT`] while the test server binds
/// its listener.
///
/// # Returns
///
/// A connected [`GrpcSchedulerClient`] on success.
///
/// # Errors
///
/// Forwards [`GrpcSchedulerClient::connect`]'s failure once [`CONNECT_TIMEOUT`] elapses.
async fn connect_with_retries(endpoint: &Endpoint) -> anyhow::Result<GrpcSchedulerClient> {
let pool_size = NonZeroUsize::new(1).expect("1 is non-zero");
let deadline = tokio::time::Instant::now() + CONNECT_TIMEOUT;
loop {
match GrpcSchedulerClient::connect(endpoint.clone(), pool_size).await {
Ok(client) => return Ok(client),
Err(error) if tokio::time::Instant::now() >= deadline => return Err(error.into()),
Err(_) => tokio::time::sleep(CONNECT_RETRY_DELAY).await,
}
}
}

/// A fake scheduler that records every request's `prev_assignment`, replying `NoTask`
/// [`NUM_NO_TASK_REPLIES`] times before handing out an assignment.
struct FakeScheduler {
observed: Arc<Mutex<Vec<Option<scheduler::TaskAssignmentRecord>>>>,
}

#[async_trait]
impl SchedulerService for FakeScheduler {
async fn next_task(
&self,
request: Request<scheduler::NextTaskRequest>,
) -> Result<Response<scheduler::NextTaskResponse>, Status> {
let num_requests = {
let mut observed = self.observed.lock().expect("lock shouldn't be poisoned");
observed.push(request.into_inner().prev_assignment);
observed.len()
};
let result = if num_requests > NUM_NO_TASK_REPLIES {
next_task_response::Result::Assignment(SchedulerAssignment {
id: 1,
resource_group_id: 2,
job_id: 3,
task_id: Some(ProtoTaskId {
kind: Some(ProtoTaskIdKind::Index(0)),
}),
scheduler_id: 4,
session_id: 5,
})
} else {
next_task_response::Result::NoTask(common::Void {})
};
Ok(Response::new(scheduler::NextTaskResponse {
result: Some(result),
}))
}

async fn heartbeat(
&self,
_request: Request<scheduler::HeartbeatRequest>,
) -> Result<Response<common::Void>, Status> {
Ok(Response::new(common::Void {}))
}

async fn shutdown(
&self,
_request: Request<scheduler::ShutdownRequest>,
) -> Result<Response<common::Void>, Status> {
Ok(Response::new(common::Void {}))
}
}

/// Tests that `prev_assignment` is sent exactly once even when the client long-polls several
/// times before a task becomes available. The scheduler completes it by removing it from its
/// registry, so re-sending it makes the scheduler fail to complete an already-dropped
/// assignment.
#[tokio::test]
async fn prev_assignment_is_sent_once_across_long_poll_iterations() -> anyhow::Result<()> {
let observed = Arc::new(Mutex::new(Vec::new()));
// Reserve an ephemeral port, then release it so the server can bind it.
let address = StdTcpListener::bind("127.0.0.1:0")?.local_addr()?;
let service = SchedulerServiceServer::new(FakeScheduler {
observed: Arc::clone(&observed),
});
let server = tokio::spawn(Server::builder().add_service(service).serve(address));

let endpoint = Endpoint::from_shared(format!("http://{address}"))?;
let client = connect_with_retries(&endpoint).await?;
let prev_assignment =
TaskAssignmentRecord::new(TaskAssignmentId::from(7), SchedulerId::from(4));
client
.next_task(ExecutionManagerId::from(2), Some(prev_assignment), 0)
.await?;
server.abort();

let observed = observed.lock().expect("lock shouldn't be poisoned").clone();
assert_eq!(observed.len(), NUM_NO_TASK_REPLIES + 1);
assert_eq!(
observed[0],
Some(scheduler::TaskAssignmentRecord { id: 7, from: 4 })
);
assert!(
observed[1..].iter().all(Option::is_none),
"later poll iterations resent the assignment: {observed:?}"
);
Ok(())
}

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.

Prefer not to add this unit test since it adds too much unnecessary scaffolding to test this simple behavior. This increases the lines of code changes needed when we need to make any change to the scheduler gRPC trait.

…sion test.

Per review: the fake `SchedulerService` is more scaffolding than the
one-line behaviour warrants, and it couples this file to every future
change to the scheduler gRPC trait.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…the `next_task` loop comment.

The comment used three pronouns for two referents, so `it must be sent at most
once` read as if the scheduler's registry were the thing being sent. Naming the
field removes the ambiguity.
@jackluo923 jackluo923 closed this Aug 7, 2026
@jackluo923 jackluo923 reopened this Aug 7, 2026
@LinZhihao-723
LinZhihao-723 merged commit b0c66b2 into y-scope:main Aug 7, 2026
31 of 41 checks passed
@jackluo923
jackluo923 deleted the fix/em-resends-prev-assignment branch August 7, 2026 04:34
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.

spider-execution-manager: prev_assignment is re-sent on every next_task long-poll iteration.

2 participants