Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
739dd2a
First implementation
Bill-hbrhbr Jul 29, 2026
51ede0e
Merge branch 'main' into coordinator/limit-job-submission-concurrency
Bill-hbrhbr Jul 29, 2026
fb1a960
Merge branch 'main' into coordinator/limit-job-submission-concurrency
Bill-hbrhbr Jul 30, 2026
4830f76
Merge branch 'main' into coordinator/limit-job-submission-concurrency
Bill-hbrhbr Jul 30, 2026
2d83f7b
Pass max concurrency limit through clp config. Complete clp config te…
Bill-hbrhbr Jul 31, 2026
95074ce
revise docstring and rename variables
Bill-hbrhbr Jul 31, 2026
2fb4d8a
Fix syntax and docstrings
Bill-hbrhbr Aug 1, 2026
e083517
Minor improvements
Bill-hbrhbr Aug 1, 2026
a256362
Add invalid config error for exceeding sem max
Bill-hbrhbr Aug 1, 2026
ccebe54
Clarify that the concurrency limit may be exceeded with extended time…
Bill-hbrhbr Aug 1, 2026
46e219c
Update config wording
Bill-hbrhbr Aug 1, 2026
46c47f9
Merge branch 'main' into coordinator/limit-job-submission-concurrency
Bill-hbrhbr Aug 6, 2026
c6ea927
Merge branch 'main' into coordinator/limit-job-submission-concurrency
LinZhihao-723 Aug 7, 2026
a75016f
First implementation
Bill-hbrhbr Jul 29, 2026
191d127
Remove the config for docker compose as it is not ready yet
Bill-hbrhbr Aug 6, 2026
0986378
Address review comment by using bounded fetch
Bill-hbrhbr Aug 7, 2026
eee03da
recover not only running jobs but also dispatched jobs
Bill-hbrhbr Aug 7, 2026
96de148
Always take a limit argument for fetch_new_job_rows
Bill-hbrhbr Aug 7, 2026
283018f
Improve docstring
Bill-hbrhbr Aug 7, 2026
e044371
Fix docstrings and make create_job_handle return Some instead of Resu…
Bill-hbrhbr Aug 7, 2026
1bcf75a
Change max_concurrent_tasks to max_concurrent_jobs
Bill-hbrhbr Aug 7, 2026
6aa5533
Add job handler dispatch time update
Bill-hbrhbr Aug 8, 2026
dad216c
Merge branch 'coordinator/ensure-running-job-with-dispatch-time' into…
Bill-hbrhbr Aug 8, 2026
c3f6f16
Merge branch 'main' into coordinator/ensure-running-job-with-dispatch…
Bill-hbrhbr Aug 8, 2026
1df15e1
Update components/compression-coordinator/src/coordination.rs
Bill-hbrhbr Aug 9, 2026
10eb46a
Merge branch 'coordinator/ensure-running-job-with-dispatch-time' into…
Bill-hbrhbr Aug 9, 2026
87365af
Redo design
Bill-hbrhbr Aug 9, 2026
2bb0d35
Merge branch 'main' into coordinator/limit-job-submission-concurrency
LinZhihao-723 Aug 11, 2026
233fca1
Merge branch 'main' into coordinator/limit-job-submission-concurrency
LinZhihao-723 Aug 11, 2026
14cdf18
Apply suggestion from @LinZhihao-723
Bill-hbrhbr Aug 11, 2026
765be0d
Address review concern
Bill-hbrhbr Aug 11, 2026
4b94045
Merge branch 'main' into coordinator/limit-job-submission-concurrency
Bill-hbrhbr Aug 11, 2026
2586181
Merge branch 'main' into coordinator/limit-job-submission-concurrency
Bill-hbrhbr Aug 11, 2026
b7bca67
Make first fetch only fetch already dispatched jobs
Bill-hbrhbr Aug 11, 2026
031edd6
Add recovery unbounded comment
Bill-hbrhbr Aug 11, 2026
d19ab1f
Done.
LinZhihao-723 Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions components/clp-py-utils/clp_py_utils/clp_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,7 @@ class PollingBackoff(BaseModel):
class CompressionCoordinator(BaseModel):
resource_group: SpiderResourceGroup = SpiderResourceGroup(name="compression-coordinator")
job_polling_interval_millisecs: PositiveInt = 100
max_concurrent_jobs: PositiveInt = 1000
result_polling: PollingBackoff = PollingBackoff(
init_backoff_millisecs=100, max_backoff_millisecs=1000
)
Expand Down
4 changes: 4 additions & 0 deletions components/clp-rust-utils/src/clp_config/package/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::num::NonZeroU32;
use std::num::NonZeroU64;
use std::num::NonZeroUsize;
use std::path::Path;
use std::path::PathBuf;

Expand Down Expand Up @@ -482,6 +483,7 @@ impl Default for Telemetry {
pub struct CompressionCoordinator {
pub resource_group: SpiderResourceGroup,
pub job_polling_interval_millisecs: NonZeroU64,
pub max_concurrent_jobs: NonZeroUsize,
pub result_polling: PollingBackoff,
pub compression_task_max_retry: u32,
pub commit_task_max_retry: u32,
Expand All @@ -500,6 +502,8 @@ impl Default for CompressionCoordinator {
},
job_polling_interval_millisecs: NonZeroU64::new(100)
.expect("default jobs poll delay should not be zero"),
max_concurrent_jobs: NonZeroUsize::new(1000)
.expect("default maximum number of concurrent jobs should not be zero"),
result_polling: PollingBackoff {
init_backoff_millisecs: NonZeroU64::new(100)
.expect("default result polling init backoff should not be zero"),
Expand Down
89 changes: 74 additions & 15 deletions components/compression-coordinator/src/coordination.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
//! The coordinator poll loop that discovers pending CLP compression jobs and dispatches them to
//! Spider.
//!
//! The coordinator is responsible for the compression jobs in the `compression_jobs` table that
//! are in one of the following states:
//!
//! | `status` | `spider_id` | `dispatch_time` | Description |
//! |----------|-------------|-----------------|--------------------------------------------------|
//! | PENDING | NULL | NULL | New jobs awaiting dispatch. |
//! | PENDING | NULL | NOT NULL | Jobs dispatched but not yet submitted to Spider. |
//! | RUNNING | NOT NULL | NOT NULL | Jobs submitted to Spider. |
//!
//! NOTE:
//!
//! * These are the only legal states for a job that hasn't terminated.
//! * A non-NULL `dispatch_time` indicates that the coordinator has picked up the job and granted it
//! permission to run under the concurrency limit.
Comment on lines +3 to +17

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.

Adding a section to show all legal states.


use std::sync::Arc;
use std::time::Duration;
Expand All @@ -19,6 +34,7 @@ use spider_core::task::TimeoutPolicy;
use spider_core::types::id::JobId as SpiderJobId;
use spider_core::types::id::ResourceGroupId;
use tokio::select;
use tokio::sync::Semaphore;
use tokio::time::Instant;
use tokio_util::sync::CancellationToken;
use tonic::transport::Endpoint;
Expand All @@ -37,6 +53,7 @@ pub struct Coordinator {
is_first_fetch: bool,
job_polling_interval: Duration,
cancellation_token: CancellationToken,
job_handler_sem: Arc<Semaphore>,
}

impl Coordinator {
Expand All @@ -57,6 +74,7 @@ impl Coordinator {
///
/// Returns an error if:
///
/// * [`Error::InvalidConfiguration`] if the compression coordinator configuration is invalid.
/// * [`Error::InvalidEndpoint`] if the Spider host and port do not form a valid endpoint.
/// * Forwards [`SpiderClient::builder`]'s connection return values on failure.
/// * Forwards [`get_or_create_resource_group_id`]'s return values on failure.
Expand All @@ -67,6 +85,14 @@ impl Coordinator {
db_pool: sqlx::MySqlPool,
db_config: DatabaseConfig,
) -> Result<(Self, CancellationToken), Error> {
let max_concurrent_jobs = coordinator_config.max_concurrent_jobs.get();
if max_concurrent_jobs > Semaphore::MAX_PERMITS {
return Err(Error::InvalidConfiguration(format!(
"`max_concurrent_jobs` must not exceed {}, got {max_concurrent_jobs}",
Semaphore::MAX_PERMITS,
)));
}

let spider_host = spider_config.host.as_str();
let spider_port = spider_config.port;
let endpoint_str = format!("http://{spider_host}:{spider_port}");
Expand Down Expand Up @@ -128,8 +154,11 @@ impl Coordinator {
coordinator_config.job_polling_interval_millisecs.get(),
),
cancellation_token: cancellation_token.clone(),
job_handler_sem: Arc::new(Semaphore::new(max_concurrent_jobs)),
};

// NOTE: The current implementation does not enforce concurrency limits for recovered jobs
// since they were already submitted to Spider. See #2472.
Comment on lines +160 to +161

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.

Downgrade this to a code-level comment: this should be more like a TODO instead of a formal behavior to documenet in the method-level docstring.

for (job_id, spider_job_id, clp_io_config) in
coordinator.fetch_submitted_running_jobs().await?
{
Expand Down Expand Up @@ -229,8 +258,8 @@ impl Coordinator {
}
}

/// Fetches the pending compression jobs and spawns a detached handle to drive each one.
///
/// Fetches pending compression jobs and spawns a detached handle to drive each one as permitted
/// by the job-handler semaphore.
///
/// A job whose config cannot be deserialized is marked [`CompressionJobStatus::Failed`] and
/// skipped; a job whose handle cannot be constructed is skipped as well (and marked
Expand All @@ -245,11 +274,17 @@ impl Coordinator {
///
/// Returns an error if:
///
/// * [`Error::Semaphore`] if acquiring a job handler permit from `job_handler_sem` fails.
/// * Forwards [`Self::fetch_new_job_rows`]'s return values on failure.
async fn schedule_new_jobs(&mut self) -> Result<Vec<CompressionJobId>, Error> {
if self.job_handler_sem.available_permits() == 0 {
return Ok(Vec::new());
}

let new_job_rows = self.fetch_new_job_rows().await.inspect_err(|e| {
tracing::error!(error = % e, "Failed to fetch new jobs from database.");
})?;

let dispatched_job_ids: Vec<CompressionJobId> =
new_job_rows.iter().map(|row| row.id).collect();
for job_row in new_job_rows {
Expand All @@ -275,7 +310,18 @@ impl Coordinator {
let Ok(job_handle) = self.create_job_handle(job_id, clp_io_config).await else {
continue;
};

let permit = self
.job_handler_sem
.clone()
.acquire_owned()
.await
.map_err(|e| {
Error::Semaphore(format!("failed to acquire a job handler permit: {e}"))
})?;

tokio::spawn(async move {
let _permit = permit;
let _ = job_handle.run().await.inspect_err(|e| {
tracing::error!(
error = % e,
Expand Down Expand Up @@ -375,12 +421,20 @@ impl Coordinator {
result
}

/// Fetches the pending compression jobs to dispatch.
/// Fetches pending compression jobs eligible for dispatch.
///
/// The first fetch after startup returns every [`CompressionJobStatus::Pending`] job whose
/// `dispatch_time` is set, so that jobs dispatched but not started by the previous coordinator
/// instance can be re-dispatched. No explicit limit is imposed because:
///
/// The first fetch after startup returns every [`CompressionJobStatus::Pending`] job so that
/// jobs a previous coordinator instance had already dispatched but not started are
/// re-dispatched. Every subsequent fetch returns only [`CompressionJobStatus::Pending`] jobs
/// whose dispatch time is still not set.
/// * This query runs only once, so limiting it could leave previously dispatched jobs
/// unfetched.
/// * The recovery set is bounded by the previous coordinator's concurrency limit.
Comment on lines +430 to +432

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 should use bullet point since they are unordered.

///
/// Every subsequent fetch returns only [`CompressionJobStatus::Pending`] jobs whose dispatch
/// time is not set. The available permit count determines how many rows are fetched, ensuring
/// that the coordinator does not fetch more jobs than it can dispatch during the current
/// polling iteration.
///
/// # Returns
///
Expand All @@ -394,25 +448,30 @@ impl Coordinator {
/// * Forwards [`sqlx::query::QueryAs::fetch_all`]'s return values on failure.
async fn fetch_new_job_rows(&mut self) -> Result<Vec<PendingJobRowProjection>, Error> {
const FIRST_FETCH_QUERY: &str = formatcp!(
"SELECT `id`, `clp_config` FROM `{table}` WHERE `status` = ? ORDER BY `id` ASC;",
"SELECT `id`, `clp_config` FROM `{table}` WHERE `status` = ? AND `dispatch_time` IS \
NOT NULL ORDER BY `id` ASC;",
table = COMPRESSION_JOB_TABLE_NAME,
);
const SUBSEQUENT_FETCH_QUERY: &str = formatcp!(
"SELECT `id`, `clp_config` FROM `{table}` WHERE `status` = ? AND `dispatch_time` IS \
NULL ORDER BY `id` ASC;",
NULL ORDER BY `id` ASC LIMIT ?;",
table = COMPRESSION_JOB_TABLE_NAME,
);

let query = if self.is_first_fetch {
self.is_first_fetch = false;
FIRST_FETCH_QUERY
sqlx::query_as::<_, PendingJobRowProjection>(FIRST_FETCH_QUERY)
.bind(CompressionJobStatus::Pending)
} else {
SUBSEQUENT_FETCH_QUERY
sqlx::query_as::<_, PendingJobRowProjection>(SUBSEQUENT_FETCH_QUERY)
.bind(CompressionJobStatus::Pending)
.bind(
i64::try_from(self.job_handler_sem.available_permits())
.expect("limit is bounded by Semaphore::MAX_PERMITS, which fits in i64"),
)
};
let rows = sqlx::query_as::<_, PendingJobRowProjection>(query)
.bind(CompressionJobStatus::Pending)
.fetch_all(&self.db_pool)
.await?;

let rows = query.fetch_all(&self.db_pool).await?;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Ok(rows)
}
Expand Down
6 changes: 6 additions & 0 deletions components/compression-coordinator/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub enum Error {
field: &'static str,
},

#[error("invalid configuration: {0}")]
InvalidConfiguration(String),

#[error("invalid dataset: {0}")]
InvalidDataset(String),

Expand Down Expand Up @@ -57,6 +60,9 @@ pub enum Error {
#[error("sqlx error: {0}")]
Sqlx(#[from] sqlx::Error),

#[error("semaphore error: {0}")]
Semaphore(String),

/// Failed to build or serialize the compression task graph.
#[error("failed to build the compression task graph: {0}")]
TaskGraph(#[from] spider_core::task::Error),
Expand Down
Loading