Skip to content

feat(compression-coordinator): Introduce max_concurrent_tasks config to limit job-handler concurrency. - #2435

Open
Bill-hbrhbr wants to merge 20 commits into
y-scope:mainfrom
Bill-hbrhbr:coordinator/limit-job-submission-concurrency
Open

feat(compression-coordinator): Introduce max_concurrent_tasks config to limit job-handler concurrency.#2435
Bill-hbrhbr wants to merge 20 commits into
y-scope:mainfrom
Bill-hbrhbr:coordinator/limit-job-submission-concurrency

Conversation

@Bill-hbrhbr

@Bill-hbrhbr Bill-hbrhbr commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Description

This PR limits the number of compression jobs processed concurrently to prevent the coordinator from creating an unbounded number of job-handler tasks and consuming excessive memory when a large backlog accumulates.

The coordinator now only fetches as many new jobs as it has capacity to process. Once the configured concurrency limit is reached, no additional jobs are fetched until existing jobs finish and capacity becomes available.

On startup, the coordinator recovers work from the previous instance before scheduling new jobs. This includes both jobs that were already submitted to Spider and jobs that were dispatched but did not progress far enough to be marked as running. The former resume tracking their existing Spider jobs, while the latter are re-dispatched.


NOTE

The is_first_fetch flag and its two-query fetch logic have been removed because they do not work well with bounded concurrency. Previously, the first fetch after startup returned all pending jobs, including those with an existing dispatch_time, to recover jobs that may not have completed dispatch before the previous coordinator exited.

With bounded concurrency, recovery can no longer rely on the first fetch returning everything, since the coordinator should only fetch as many new jobs as it has capacity to process. Recovery is therefore handled explicitly by recover_previous_jobs, which identifies all pending jobs with an existing dispatch_time and re-dispatches them before normal scheduling begins.

This leaves the steady-state dispatcher with a single, clear eligibility rule: only pending jobs without a dispatch_time are considered new work.


The concurrency limit is configured through a new max_concurrent_tasks field in ClpConfig, with a default of 1000.

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

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added a configurable limit for concurrent compression tasks, defaulting to 1,000.
    • Pending and interrupted compression jobs are recovered and resumed more reliably.
    • Scheduling now adapts to available capacity and avoids dispatching work when capacity is unavailable.
  • Bug Fixes

    • Improved startup handling for previously dispatched jobs.
    • Invalid concurrency settings, including zero or unsupported values, now produce a clear configuration error.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The change adds max_concurrent_tasks to Python and Rust configuration. The coordinator validates this limit, controls handlers with a semaphore, recovers existing work, and bounds new-job scheduling.

Changes

Compression coordinator concurrency

Layer / File(s) Summary
Concurrency configuration
components/clp-py-utils/clp_py_utils/clp_config.py, components/clp-rust-utils/src/clp_config/package/config.rs
CompressionCoordinator exposes a non-zero max_concurrent_tasks setting with a default of 1000.
Coordinator validation and recovery
components/compression-coordinator/src/error.rs, components/compression-coordinator/src/coordination.rs
The coordinator validates the limit, initializes permits, recovers running and dispatched pending jobs, and marks invalid configurations as failed.
Bounded pending-job scheduling
components/compression-coordinator/src/coordination.rs
Scheduling fetches undispatched pending jobs within available capacity, marks selected jobs as dispatched, and retains permits for spawned handlers.

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

Sequence Diagram(s)

sequenceDiagram
  participant CompressionCoordinator
  participant JobDatabase
  participant JobHandler
  CompressionCoordinator->>CompressionCoordinator: Check semaphore capacity
  CompressionCoordinator->>JobDatabase: Fetch bounded pending jobs
  CompressionCoordinator->>JobDatabase: Mark selected jobs as dispatched
  CompressionCoordinator->>JobHandler: Spawn handlers with retained permits
  JobHandler->>CompressionCoordinator: Release permit on completion
Loading

Possibly related PRs

  • y-scope/clp#2401: Adds the Error enum that this change extends with InvalidConfiguration.
  • y-scope/clp#2417: Introduces coordinator scheduling and recovery logic that this change extends.
  • y-scope/clp#2421: Introduces the CompressionCoordinator configuration extended with max_concurrent_tasks.

Suggested reviewers: linzhihao-723

🚥 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 clearly and concisely describes the main change: adding configurable max_concurrent_tasks to limit job-handler concurrency.
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.
✨ 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.

@Bill-hbrhbr Bill-hbrhbr changed the title feat(compression-coordinator): Limit concurrent job-handler tasks. feat(compression-coordinator): Introduce max_concurrent_tasks config to limit job-handler task concurrency. Aug 1, 2026
@Bill-hbrhbr Bill-hbrhbr changed the title feat(compression-coordinator): Introduce max_concurrent_tasks config to limit job-handler task concurrency. feat(compression-coordinator): Introduce max_concurrent_tasks config to limit job-handler concurrency. Aug 1, 2026
@Bill-hbrhbr
Bill-hbrhbr marked this pull request as ready for review August 1, 2026 06:51
@Bill-hbrhbr
Bill-hbrhbr requested a review from a team as a code owner August 1, 2026 06:51

@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: 5

🤖 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/clp-py-utils/clp_py_utils/clp_config.py`:
- Line 798: Add a shared Tokio semaphore upper-bound validation for
max_concurrent_tasks in components/clp-py-utils/clp_py_utils/clp_config.py:798
and components/clp-rust-utils/src/clp_config/package/config.rs:488, preserving
the existing positive-value validation. In
components/compression-coordinator/src/coordination.rs:137-138, validate the
configured value before constructing job_handler_sem and return the established
configuration error when it exceeds the supported limit.

In `@components/compression-coordinator/src/coordination.rs`:
- Around line 49-52: Update the recovery admission logic around the
coordinator’s recovered-handler tracking so every recovered handler contributes
to the active count, including handlers that do not hold a permit. Prevent new
jobs from being admitted until the total active recovered and newly started
handlers is below max_concurrent_tasks, and revise the recovery documentation
near the constructor to describe this limit-enforced behavior.
- Around line 270-275: Update the pending-job refill flow around
fetch_new_job_rows so it retrieves eligible jobs in bounded pages rather than
loading and retaining the entire backlog in pending_job_queue. Add durable
paging state, such as a cursor that advances only after successful processing or
equivalent recovery-safe state, and preserve first-fetch recovery semantics so
retries resume without skipping jobs or causing unbounded memory growth.

In `@components/package-template/src/etc/clp-config.template.json.yaml`:
- Line 72: Update the commented max_concurrent_tasks configuration entry in the
template to explicitly state that its value must be greater than zero,
distinguishing it from compression_scheduler.max_concurrent_tasks_per_job where
zero disables the limit.
- Line 67: Update the compression coordinator configuration note near the
“Compression coordinator config” comment to document every runtime prerequisite:
`spider`, `logs_input.type: s3`, and `archive_output.storage.type: s3`; clarify
that the related template examples must use these required S3 settings rather
than `fs` when enabling the configuration.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8fd3f43d-d433-437d-ab55-235ab39cfb5f

📥 Commits

Reviewing files that changed from the base of the PR and between 155fbda and e083517.

📒 Files selected for processing (4)
  • components/clp-py-utils/clp_py_utils/clp_config.py
  • components/clp-rust-utils/src/clp_config/package/config.rs
  • components/compression-coordinator/src/coordination.rs
  • components/package-template/src/etc/clp-config.template.json.yaml

Comment thread components/clp-py-utils/clp_py_utils/clp_config.py
Comment thread components/compression-coordinator/src/coordination.rs Outdated
Comment thread components/compression-coordinator/src/coordination.rs Outdated
Comment thread components/package-template/src/etc/clp-config.template.json.yaml Outdated
Comment thread components/package-template/src/etc/clp-config.template.json.yaml Outdated

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/compression-coordinator/src/coordination.rs (1)

294-300: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Record a job as dispatched only after handler creation succeeds.

dispatched_job_ids.push(job_id) runs before configuration deserialisation and create_job_handle. If either operation fails, run still passes the ID to mark_jobs_dispatched. An Error::UnsupportedInputConfig job therefore receives dispatch_time without a coordinator handler, which removes it from the subsequent pending-job query and can interfere with the documented legacy-scheduler hand-off.

Move the push after create_job_handle returns Ok:

Proposed fix
 let job_id = job_row.id;
- dispatched_job_ids.push(job_id);

 let clp_io_config: ClpIoConfig =
     match BrotliMsgpack::deserialize(&job_row.serialized_clp_io_config) {
         Ok(clp_io_config) => clp_io_config,
         Err(e) => {
             ...
         }
     };

 let Ok(job_handle) = self.create_job_handle(job_id, clp_io_config).await else {
     continue;
 };
+dispatched_job_ids.push(job_id);
🤖 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/compression-coordinator/src/coordination.rs` around lines 294 -
300, Move dispatched_job_ids.push(job_id) in the run flow to after configuration
deserialization and successful create_job_handle completion, so only jobs with a
coordinator handler are marked dispatched; preserve failure handling without
passing failed job IDs to mark_jobs_dispatched.
🤖 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/package-template/src/etc/clp-config.template.json.yaml`:
- Line 67: Update the compression coordinator configuration note near the
existing logs_input.type and spider requirements to also state that
archive_output.storage.type must be "s3". Keep the documentation aligned with
the coordinator prerequisites and the archive output setting shown in the
template.

---

Outside diff comments:
In `@components/compression-coordinator/src/coordination.rs`:
- Around line 294-300: Move dispatched_job_ids.push(job_id) in the run flow to
after configuration deserialization and successful create_job_handle completion,
so only jobs with a coordinator handler are marked dispatched; preserve failure
handling without passing failed job IDs to mark_jobs_dispatched.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b74354c7-b2d8-4659-a5ac-82902ed1fabb

📥 Commits

Reviewing files that changed from the base of the PR and between e083517 and 46e219c.

📒 Files selected for processing (3)
  • components/compression-coordinator/src/coordination.rs
  • components/compression-coordinator/src/error.rs
  • components/package-template/src/etc/clp-config.template.json.yaml

Comment thread components/package-template/src/etc/clp-config.template.json.yaml Outdated
Comment thread components/package-template/src/etc/clp-config.template.json.yaml Outdated
Comment thread components/compression-coordinator/src/coordination.rs Outdated
Comment thread components/package-template/src/etc/clp-config.template.json.yaml
job_polling_interval: Duration,
cancellation_token: CancellationToken,
job_handler_sem: Arc<Semaphore>,
pending_job_queue: VecDeque<PendingJobRowProjection>,

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 we need this. Can you explain why we can't do the following instead:

  • On the main loop, fetch compression jobs with a limit and ID ordering (sth like SELECT * FROM t ORDER BY id ASC LIMIT 100;)
  • Get a permit before spawning the coroutine. The permit automatially drops itself if the coroutine exits, aborts, or crashed.
  • Let the semaphore to block coroutine creation implicitly. The main loop may be blocked, which is fine because it shouldn't push more jobs into Spider.

This should lead to the behavior we expect iiuc.

@Bill-hbrhbr

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Already reviewed.

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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
components/compression-coordinator/src/coordination.rs (2)

46-49: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the constructor docstring to cover both recovery categories.

The docstring states only that recovery covers jobs that are still Running with a Spider job ID. recover_previous_jobs also resumes Pending jobs whose dispatch_time is populated. The docstring at Lines 204-217 already describes both categories.

📝 Proposed doc fix
-    /// On construction, this begins recovering all compression jobs that a previous coordinator
-    /// instance had already submitted to Spider (those still [`CompressionJobStatus::Running`] with
-    /// a Spider job ID). During the restart phase, no concurrency limit is imposed, so all
-    /// recovered jobs are resumed immediately.
+    /// On construction, this begins recovering all compression jobs that a previous coordinator
+    /// instance had already claimed: jobs still [`CompressionJobStatus::Running`] with a Spider job
+    /// ID, and jobs still [`CompressionJobStatus::Pending`] with a populated `dispatch_time`.
+    /// During the restart phase, no concurrency limit is imposed, so all recovered jobs are resumed
+    /// immediately.
🤖 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/compression-coordinator/src/coordination.rs` around lines 46 - 49,
Update the constructor documentation describing recovery to include both
categories handled by recover_previous_jobs: Running jobs with a Spider job ID
and Pending jobs with a populated dispatch_time. Preserve the existing note that
recovered jobs resume immediately without a concurrency limit.

323-342: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Record a job as dispatched only after the handler is spawned.

Line 330 pushes job_id into dispatched_job_ids before deserialization and handle creation. Both later steps can continue.

Two consequences follow:

  1. create_job_handle returns Error::UnsupportedInputConfig for inputs that the legacy Celery scheduler must handle. The job stays Pending, but mark_jobs_dispatched now stamps dispatch_time on it. On every later restart, fetch_dispatched_pending_jobs selects that row (Pending with a non-null dispatch_time) and recover_previous_jobs re-dispatches it through run(), which fails the same way. The row is recovered again on each restart and consumes a spawn each time.
  2. Jobs that fail deserialization are already marked Failed, so the extra UPDATE is wasted work.

Move the push to the point where the handler is spawned.

🐛 Proposed fix
             let job_id = job_row.id;
-            dispatched_job_ids.push(job_id);
-
             let Some(clp_io_config) = self
                 .try_deserialize_clp_io_config(job_id, &job_row.serialized_clp_io_config)
                 .await
             else {
                 continue;
             };
 
             tracing::info!(job_id = % job_id, "Scheduling new job.");
             let Ok(job_handle) = self.create_job_handle(job_id, clp_io_config).await else {
                 continue;
             };
 
+            dispatched_job_ids.push(job_id);
             tokio::spawn(async move {
🤖 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/compression-coordinator/src/coordination.rs` around lines 323 -
342, Move the dispatched_job_ids.push(job_id) call in the new-job scheduling
loop until after create_job_handle succeeds and the handler is spawned. Do not
record jobs that fail try_deserialize_clp_io_config or create_job_handle;
preserve the existing continue behavior for those failures.
🤖 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/compression-coordinator/src/coordination.rs`:
- Around line 158-161: Update the coordination loop so jobs spawned by
schedule_new_jobs are passed to mark_jobs_dispatched before the interruptible
polling wait, or are marked during cancellation shutdown before returning.
Ensure cancellation cannot bypass dispatch_time persistence, preventing
fetch_new_job_rows from selecting those jobs again after restart.
- Around line 534-540: Update the deserialization function’s rustdoc to state
that it returns the deserialized ClpIoConfig on success and None when
deserialization fails, and revise the failure description to use separate,
grammatically complete clauses instead of joining them with a comma.

---

Outside diff comments:
In `@components/compression-coordinator/src/coordination.rs`:
- Around line 46-49: Update the constructor documentation describing recovery to
include both categories handled by recover_previous_jobs: Running jobs with a
Spider job ID and Pending jobs with a populated dispatch_time. Preserve the
existing note that recovered jobs resume immediately without a concurrency
limit.
- Around line 323-342: Move the dispatched_job_ids.push(job_id) call in the
new-job scheduling loop until after create_job_handle succeeds and the handler
is spawned. Do not record jobs that fail try_deserialize_clp_io_config or
create_job_handle; preserve the existing continue behavior for those failures.
🪄 Autofix

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9a5e4d29-81d2-47df-bd08-f80e303b3542

📥 Commits

Reviewing files that changed from the base of the PR and between c6ea927 and 283018f.

📒 Files selected for processing (1)
  • components/compression-coordinator/src/coordination.rs

Comment on lines +158 to +161
/// `dispatch_time` marks jobs that have already been dispatched by the current coordinator,
/// preventing them from being dispatched again before their handlers persist the `Running`
/// state. These updates are batched and applied after the polling interval to reduce
/// contention with the handlers' `Running` state updates.

@coderabbitai coderabbitai Bot Aug 7, 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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

The documented dispatch marking does not happen on the cancellation path.

The docstring states that dispatch_time prevents jobs from being dispatched again before their handlers persist the Running state. The loop breaks on cancellation during the polling interval, so mark_jobs_dispatched never runs for the jobs that schedule_new_jobs just spawned in that iteration. After a restart, those rows still have dispatch_time IS NULL, so fetch_new_job_rows selects them again and the new coordinator dispatches them a second time.

Mark the jobs as dispatched before the interruptible wait, or mark them during shutdown before returning.

🤖 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/compression-coordinator/src/coordination.rs` around lines 158 -
161, Update the coordination loop so jobs spawned by schedule_new_jobs are
passed to mark_jobs_dispatched before the interruptible polling wait, or are
marked during cancellation shutdown before returning. Ensure cancellation cannot
bypass dispatch_time persistence, preventing fetch_new_job_rows from selecting
those jobs again after restart.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@LinZhihao-723 @sitaowang1998 This does seem like a real issue, although I think addressing it is outside the scope of this PR.

One possible solution would be to make claiming a job atomic by setting dispatch_time as part of the fetch operation. For example, fetch_new_job_rows could first claim a bounded set of pending jobs with:

UPDATE compression_jobs
SET dispatch_time = CURRENT_TIMESTAMP()
WHERE status = 'PENDING' AND dispatch_time IS NULL
ORDER BY id ASC
LIMIT ?

Once a row is claimed, it can no longer be picked up as new work, regardless of whether the coordinator is cancelled or crashes between claiming and spawning the handler.

The remaining failure case is when a job is claimed but its handler never starts. In that case, the job remains Pending with dispatch_time set, so recover_previous_jobs can identify and re-dispatch it after restart.

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.

As explain https://github.com/y-scope/clp/pull/2435/changes#r3736798212: I think it should be fine if we fix the dispatch time cleaning.
dispatch_time is just a marker and it should be reset if spider_id is not set yet. It is possible to see cases where dispatch time is not set but it has a spider ID, which is fine and it can be properly handled by the recovery path.

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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment thread components/compression-coordinator/src/coordination.rs Outdated

@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

🤖 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/compression-coordinator/src/coordination.rs`:
- Around line 290-292: Update the documentation for the job-fetching/spawning
method around create_job_handle to state that UnsupportedInputConfig is logged
and left pending rather than marked failed, while retaining the failure
description for other handle-construction errors.
🪄 Autofix

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d268beb8-e4d6-4e45-8161-c9f29e6f20c9

📥 Commits

Reviewing files that changed from the base of the PR and between 283018f and e044371.

📒 Files selected for processing (1)
  • components/compression-coordinator/src/coordination.rs

Comment thread components/compression-coordinator/src/coordination.rs
@Bill-hbrhbr

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 7, 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
components/compression-coordinator/src/coordination.rs (2)

322-334: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add job IDs only after a handler is ready to spawn.

dispatched_job_ids is updated before deserialisation and create_job_handle. For Error::UnsupportedInputConfig, create_job_handle returns None and leaves the job pending, but mark_jobs_dispatched still sets its dispatch_time. The coordinator then excludes the row from normal fetches and treats it as previously dispatched during recovery, although no handler exists. Move the push after successful handle creation.

Proposed fix
             let job_id = job_row.id;
-            dispatched_job_ids.push(job_id);

             let Some(clp_io_config) = self
                 .try_deserialize_clp_io_config(job_id, &job_row.serialized_clp_io_config)
                 .await
@@
             let Some(job_handle) = self.create_job_handle(job_id, clp_io_config).await else {
                 continue;
             };
+            dispatched_job_ids.push(job_id);

             tokio::spawn(async move {
🤖 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/compression-coordinator/src/coordination.rs` around lines 322 -
334, Move the dispatched_job_ids.push(job_id) call in the job scheduling flow to
immediately after create_job_handle successfully returns a handle, while
retaining the existing deserialization and creation failure continues. Ensure
only jobs with a ready-to-spawn handler are included when mark_jobs_dispatched
processes the IDs.

492-520: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Bound the pending recovery result set.

fetch_dispatched_pending_jobs loads every pending row with a non-null dispatch_time into one Vec. recover_previous_jobs then spawns one detached task per row. A large recovery backlog can allocate excessive memory and create tasks independently of max_concurrent_tasks. Fetch recovery rows in bounded pages and route excess work through the same tracked admission path.

🤖 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/compression-coordinator/src/coordination.rs` around lines 492 -
520, The fetch_dispatched_pending_jobs and recover_previous_jobs flow must stop
loading and spawning the entire recovery backlog at once. Change recovery-row
retrieval to bounded pages, iterating until no rows remain, and submit each
page’s jobs through the existing tracked admission mechanism governed by
max_concurrent_tasks rather than detached per-row tasks; preserve the current
pending-status and dispatch_time filtering.
🤖 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.

Outside diff comments:
In `@components/compression-coordinator/src/coordination.rs`:
- Around line 322-334: Move the dispatched_job_ids.push(job_id) call in the job
scheduling flow to immediately after create_job_handle successfully returns a
handle, while retaining the existing deserialization and creation failure
continues. Ensure only jobs with a ready-to-spawn handler are included when
mark_jobs_dispatched processes the IDs.
- Around line 492-520: The fetch_dispatched_pending_jobs and
recover_previous_jobs flow must stop loading and spawning the entire recovery
backlog at once. Change recovery-row retrieval to bounded pages, iterating until
no rows remain, and submit each page’s jobs through the existing tracked
admission mechanism governed by max_concurrent_tasks rather than detached
per-row tasks; preserve the current pending-status and dispatch_time filtering.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b8b78f55-7627-4e58-b027-2c77782a6634

📥 Commits

Reviewing files that changed from the base of the PR and between 283018f and e044371.

📒 Files selected for processing (1)
  • components/compression-coordinator/src/coordination.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.

There's a risk that we won't be able to merge this PR before the release since it's not in an expected shape yet. Let's fix the recovery path bug first.

Comment on lines +528 to +549
async fn try_deserialize_clp_io_config(
&self,
job_id: CompressionJobId,
serialized_config: &[u8],
) -> Option<ClpIoConfig> {
match BrotliMsgpack::deserialize(serialized_config) {
Ok(clp_io_config) => Some(clp_io_config),
Err(e) => {
tracing::error!(
error = % e,
job_id = % job_id,
"Failed to deserialize CLP I/O config. Skipping."
);
self.mark_job_failed(
job_id,
&format!("Failed to deserialize CLP I/O config: {e}"),
)
.await;
None
}
}
}

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.

It's a really bad idea to return an option instead of a result, and log inside the helper. The best practice for designing this type of helper is:

  • Return a result. Let the caller decide whether it wants to inspect the error or just propagate.
  • Log on the caller side.
    • You may see create_job_handle logs inside the helper. That is because it needs to print different logs based on the type of the error and execute different reaction calls accordingly. This makes it an exception since logging it inside would actually make things cleaner. In this helper, errors are printed unconditionally so it should be logged outside.

Comment on lines -275 to +333
let Ok(job_handle) = self.create_job_handle(job_id, clp_io_config).await else {
let Some(job_handle) = self.create_job_handle(job_id, clp_io_config).await else {

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.

Similar to https://github.com/y-scope/clp/pull/2435/changes#r3736535562: you didn't change the implementation of create_job_handle, then why would you need to update the return type? This is polluting the diff in some sense and making the PR harder to review.

Comment on lines +249 to +250
Some(id) => job_handle.recover(id).await,
None => job_handle.run().await,

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 think this is actually a bug from my previous PR: at this stage, we should not handle any job that is already dispatched but doesn't have a spider ID.
The correct thing to do is (which is also what we actually expected), at the beginning of the factory, we should reset dispatch to NULL for all jobs that have a dispatch time set but no spider ID. These jobs should fall into the normal run loop instead of being handled in this recovery stage.

/// A projection of the columns read from a compression job row.
#[derive(Debug, sqlx::FromRow)]
struct RunningJobRowProjection {
struct JobRowProjection {

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.

It's a bad idea to merge PendingJobRowProjection and RunningJobRowProjection into one type: the user needs to check whether spider_job_id is NULL now. Since we will have https://github.com/y-scope/clp/pull/2435/changes#r3736798212, let roll back to two separate types instead of one.
In Rust, if you really want a type to cover both, a better idea is probably to make an enum containing both types.

Comment on lines +158 to +161
/// `dispatch_time` marks jobs that have already been dispatched by the current coordinator,
/// preventing them from being dispatched again before their handlers persist the `Running`
/// state. These updates are batched and applied after the polling interval to reduce
/// contention with the handlers' `Running` state updates.

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.

As explain https://github.com/y-scope/clp/pull/2435/changes#r3736798212: I think it should be fine if we fix the dispatch time cleaning.
dispatch_time is just a marker and it should be reset if spider_id is not set yet. It is possible to see cases where dispatch time is not set but it has a spider ID, which is fine and it can be properly handled by the recovery path.

continue;
};

let permit = self.job_handler_sem.clone().try_acquire_owned().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.

I'm not sure if we really need to enforce the permit here: the recovery may not necessarily happen from a failure recovery, but a restart with clp config changed. If the new max_concurrent_jobs is changed to a smaller number, this recovery path will be blocked for an improper reason: eventually, a number of compression jobs are already submitted to Spider that exceeds the configured concurrency.
My proposal:

  • For now, it's probably easier if we don't do anything to the permit: let the already-submitted jobs run in background. As we would assume the recovery is a rare case, the number of jobs should be roughly bounded by old_max_concurrent_jobs + new_max_concurrent_jobs. There are extreme cases where this bound can be broken by a series of frequent restarts, but it should be safe to ignore this case for now.
  • To improve the naive implementation, we could compare the number of jobs to recover vs. the currently configured max_concurrent_jobs:
    • If num_jobs_to_recover > max_concurrent_jobs: print a warning, make a join of all the recovered job handlers, so the main loop only starts when all previously submitted jobs are finished.
    • Otherwise, do the current implementation to recover jobs while each job holds an acquired permit.

class CompressionCoordinator(BaseModel):
resource_group: SpiderResourceGroup = SpiderResourceGroup(name="compression-coordinator")
job_polling_interval_millisecs: PositiveInt = 100
max_concurrent_tasks: PositiveInt = 1000

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 concurrency control is in the job level not the task level. Shouldn't it be called max_concurrent_jobs?

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.

3 participants