Skip to content

feat(compression-coordinator): Add the compression coordinator implementation. - #2417

Merged
LinZhihao-723 merged 17 commits into
y-scope:mainfrom
LinZhihao-723:compression-coordinator-impl
Jul 29, 2026
Merged

feat(compression-coordinator): Add the compression coordinator implementation.#2417
LinZhihao-723 merged 17 commits into
y-scope:mainfrom
LinZhihao-723:compression-coordinator-impl

Conversation

@LinZhihao-723

@LinZhihao-723 LinZhihao-723 commented Jul 23, 2026

Copy link
Copy Markdown
Member

Description

This PR is my birthday gift, and it depends on #2416.

This PR adds the Coordinator that drives CLP's compression pipeline: it recovers jobs left in-flight by a previous instance, polls the metadata DB for pending compression jobs, and dispatches each one to Spider — plus the configuration and clp-rust-utils support the coordinator needs. This is the layer above the job handle (#2405) and the Spider submitter (#2402); with it, the coordinator binary can discover work, hand it off, and survive a restart.

WARNING: Compression coordinator will not work until the methods marked as TODO in #2405 are implemented.

Poll loop

Coordinator::run loops until cancelled:

  • Fetches the pending jobs to dispatch. The first fetch after startup returns every pending job (WHERE status = PENDING ORDER BY id ASC) so that jobs a previous instance had already picked up are re-dispatched on a restart; every subsequent fetch returns only the jobs this instance hasn't dispatched yet (WHERE status = PENDING AND dispatch_time IS NULL ORDER BY id ASC).
  • For each job, deserializes its Brotli-MessagePack clp_config, builds an S3CompressionJobHandle via create_job_handle, and spawns a detached task to drive it to completion.
  • Sleeps for the configured polling interval, waking early when the cancellation token fires.
  • Once the sleep elapses, stamps the jobs dispatched in this iteration with the database's current time (UPDATE ... SET dispatch_time = CURRENT_TIMESTAMP() WHERE id IN (...), chunked at 1000 IDs and run in a single transaction) so the next fetch skips them. Deferring this update until after the sleep keeps it from contending with concurrent job submissions during the poll interval.

De-duplication via dispatch_time

Each dispatched row is marked with a dedicated dispatch_time column (initialized to NULL) rather than tracked by a monotonic in-memory ID cursor. A job with ID N can be committed and become visible before job N - 1, so an ID-ordered cursor that advances past N would skip the later-committed N - 1 permanently. Keying off a per-row dispatch_time marker makes the fetch independent of ID ordering, while re-fetching all pending jobs on the first poll after startup ensures nothing a crashed instance left behind is lost.

Startup recovery

Coordinator::new recovers jobs that a previous coordinator instance had already submitted to Spider — those still RUNNING with a non-null spider_id. For each, it rebuilds the handle and spawns a detached task that resumes waiting on the existing Spider job (recover) rather than resubmitting it, so a coordinator restart doesn't duplicate or abandon work. A recovered job whose stored config can't be deserialized is marked FAILED and skipped.

new also connects to the Spider cluster and resolves the coordinator's Spider resource group, and hands back a CancellationToken the binary uses to request a graceful shutdown.

Resource-group registration

get_or_create_resource_group_id idempotently registers the configured resource group with Spider and caches the assigned ID in a spider_resource_groups table, so a restart reuses the same group rather than creating a new one.

Per-job failure handling

Handle construction is centralized in create_job_handle, shared by both scheduling and recovery. A job whose config can't be deserialized, or whose handle can't be constructed, is marked FAILED with a status message rather than left stranded in PENDING/RUNNING — except an unsupported input config, which is only warned and left for another handler. Failures that occur once a handle is running are owned by the handle itself.

Configuration

Adds compression_coordinator and spider sections to the package config. The coordinator section is #[serde(default)] with sensible defaults (polling interval, result-poll backoff, task retry limits, DB pool size, termination and commit-task timeouts); intervals and sizes use NonZero* types so a zero can't be configured.

clp-rust-utils support

  • BrotliMsgpack::deserialize — the inverse of the existing serializer, so the coordinator can read the clp_config the ingestor wrote.
  • ClpIoConfig and its input/output config structs now derive Deserialize.
  • CompressionJobStatus derives sqlx::Type, so it binds directly as the integer status column.
  • A MsgpackDecode error variant (mapped to MalformedData in the API server).

Binary entry point

The compression-coordinator binary takes a single --config <PATH> argument. On startup it initializes logging, loads the YAML package config, and reads the metadata-DB credentials from the CLP_DB_USER/CLP_DB_PASS environment variables. It requires both the compression_coordinator and spider config sections, erroring out if either is missing. It then builds the metadata-DB connection pool (sized by database_connection_pool_size), constructs the Coordinator — which performs startup recovery — and spawns its poll loop before handing off to the shutdown handling below.

Shutdown

The binary spawns the coordinator, then waits on SIGTERM/ctrl-c or the coordinator returning on its own. On a signal it cancels the token and joins the coordinator within a configurable termination timeout, aborting if it overruns.

Checklist

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

Validation performed

  • Ensure all workflows pass.
  • E2e-tested in a dev branch with a real Spider cluster.

Summary by CodeRabbit

  • New Features

    • Added a compression coordinator service that dispatches, monitors, and recovers compression jobs.
    • Added configuration options for Spider connectivity, resource groups, polling, retries, timeouts, and database capacity.
    • Added command-line configuration loading and graceful shutdown handling.
    • Added support for reading compressed MessagePack data and deserializing job configurations.
  • Bug Fixes

    • MessagePack decoding failures are now reported as malformed data.
    • Invalid job configurations are marked as failed with an explanatory status.

@LinZhihao-723
LinZhihao-723 requested a review from a team as a code owner July 23, 2026 04:06
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: efbc4151-8555-4480-a0c0-dd85c10bae69

📥 Commits

Reviewing files that changed from the base of the PR and between 34e88e0 and 3530a63.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • components/api-server/src/error.rs
  • components/clp-rust-utils/src/clp_config/package/config.rs
  • components/clp-rust-utils/src/error.rs
  • components/compression-coordinator/Cargo.toml
  • components/compression-coordinator/src/bin/compression_coordinator.rs
  • components/compression-coordinator/src/error.rs

Walkthrough

Adds Spider and compression-coordinator configuration, Brotli MessagePack deserialization, coordinator job polling and recovery, and a CLI executable with database setup and signal-based shutdown.

Changes

Compression coordinator

Layer / File(s) Summary
Configuration and serialization contracts
components/clp-rust-utils/src/clp_config/package/config.rs, components/clp-rust-utils/src/job_config/clp_io_config.rs, components/clp-rust-utils/src/serde/brotli_msgpack.rs, components/clp-rust-utils/src/error.rs, components/api-server/src/error.rs
Adds Spider and coordinator configuration types, JSON deserialization for I/O configuration, Brotli MessagePack decoding, and malformed-data mapping for decode errors.
Coordinator polling and recovery
components/compression-coordinator/src/lib.rs, components/compression-coordinator/src/coordination.rs, components/compression-coordinator/src/error.rs
Adds Coordinator construction, Spider resource-group persistence, pending-job dispatch, running-job recovery, failure updates, and cancellation-aware polling.
CLI entrypoint and shutdown
components/compression-coordinator/Cargo.toml, components/compression-coordinator/src/bin/compression_coordinator.rs
Adds runtime dependencies, YAML configuration loading, S3 validation, credential handling, MySQL pool creation, signal handling, termination timeout, and coordinator result reporting.

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

Possibly related PRs

  • y-scope/clp#2375: Updates the same API-server error conversion for MessagePack-related malformed data.
  • y-scope/clp#2404: Introduces the compression-coordinator crate scaffolding extended by this PR.
  • y-scope/clp#2418: Wires the Spider/compression-coordinator integration into Helm deployment configuration.

Suggested reviewers: jackluo923

🚥 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 matches the main change: adding the compression coordinator implementation.
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.

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

🤖 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/Cargo.toml`:
- Line 28: Update the tokio dependency features in Cargo.toml to explicitly
include macros alongside time, rt-multi-thread, and signal, ensuring the
#[tokio::main] usage in compression_coordinator is directly supported.

In `@components/compression-coordinator/src/coordination.rs`:
- Around line 460-523: Update get_or_create_resource_group_id to self-heal
duplicate-key races during INSERT_QUERY execution: detect a MySQL duplicate-key
error, re-query SELECT_QUERY for the existing rg_id, and return that ID instead
of failing or requiring a restart. Preserve propagation and logging for
non-duplicate database errors, and keep the existing successful insert path
unchanged.
- Around line 343-370: Update fetch_new_job_rows so it no longer advances
last_polled_job_id based on the highest fetched id or relies solely on id >
last_polled_job_id, preventing late-committing lower-id pending jobs from being
skipped; use a cursor strategy that preserves those jobs. Add a bounded SQL
LIMIT to the pending-job queries before fetch_all, using the coordinator’s
existing batch-size configuration or constant, while preserving ascending
ordering and dispatch behavior.
- Around line 79-84: Configure a finite connection timeout on the Tonic Endpoint
in the SpiderClient builder flow before calling connect(). Use the existing
endpoint construction associated with SpiderClient and preserve the current
error logging and propagation behavior.
🪄 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: 75551da2-2624-4fdd-bb06-bfde1ffc008d

📥 Commits

Reviewing files that changed from the base of the PR and between fd42277 and 0ddd1e5.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • components/api-server/src/error.rs
  • components/clp-rust-utils/src/clp_config/package/config.rs
  • components/clp-rust-utils/src/error.rs
  • components/clp-rust-utils/src/job_config/clp_io_config.rs
  • components/clp-rust-utils/src/job_config/compression.rs
  • components/clp-rust-utils/src/serde/brotli_msgpack.rs
  • components/compression-coordinator/Cargo.toml
  • components/compression-coordinator/src/bin/compression_coordinator.rs
  • components/compression-coordinator/src/coordination.rs
  • components/compression-coordinator/src/error.rs
  • components/compression-coordinator/src/lib.rs
  • tools/docker-images/clp-package/Dockerfile

thiserror = "2.0.18"
tokio = { version = "1.52.3", features = ["time"] }
tokio-util = "0.7.18"
tokio = { version = "1.52.3", features = ["time", "rt-multi-thread", "signal"] }

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cargo tree \
  --manifest-path components/compression-coordinator/Cargo.toml \
  -e features -i tokio

Repository: y-scope/clp

Length of output: 396


🏁 Script executed:

#!/bin/bash
set -e

echo "== locate Cargo.toml and relevant source =="
fd -a 'Cargo\.toml$|compression_coordinator\.rs$' . | sed 's#^\./##' | sort

echo "== compression-coordinator Cargo.toml =="
cat -n components/compression-coordinator/Cargo.toml

echo "== main file tokio references =="
cat -n components/compression-coordinator/src/bin/compression_coordinator.rs | sed -n '1,120p'

echo "== lockfile tokio entries if present =="
if [ -f components/compression-coordinator/Cargo.lock ]; then
  rg -n 'name = "tokio"|macros|version = "1\.52\.3"' components/compression-coordinator/Cargo.lock -A2 -B2
else
  echo "No components/compression-coordinator/Cargo.lock"
fi

echo "== top-level workspace Cargo.toml =="
if [ -f Cargo.toml ]; then
  cat -n Cargo.toml | sed -n '1,220p'
fi

echo "== parse dependency tokens for tokio feature list =="
python3 - <<'PY'
from pathlib import Path
p = Path('components/compression-coordinator/Cargo.toml')
text = p.read_text()
for line in text.splitlines():
    if line.startswith('tokio') or ('macros' in line and 'tokio' in line):
        print(line)
PY

Repository: y-scope/clp

Length of output: 7459


🏁 Script executed:

#!/bin/bash
set -e

echo "== workspace dependency definitions for tokio =="
rg -n '(^\[workspace\.dependencies\]|[^\[]|^)tokio(\s*=|[^[:alnum:]])' Cargo.toml components -g 'Cargo.toml' || true

echo "== workspace member Cargo.toml tokio-like deps =="
for f in components/*/Cargo.toml; do
  if grep -qE '(^|[^[:alnum:]])tokio' "$f" 2>/dev/null; then
    echo "-- $f"
    grep -nE '(^|[^[:alnum:]])tokio' "$f" || true
  fi
done

echo "== inspect workspace dependency syntax around all members if any =="
python3 - <<'PY'
from pathlib import Path
for p in [Path("Cargo.toml")] + [Path(f) for f in __import__('glob').glob("components/*/Cargo.toml")]:
    if not p.exists() or p == Path("components/compression-coordinator/Cargo.toml"):
        continue
    text = p.read_text()
    print(f"== {p} ==")
    lines = text.splitlines()
    i = 0
    while i < len(lines):
        line = lines[i]
        if "tokio" in line or "[dependencies" in line or "[workspace.dependencies" in line:
            start = max(0, i-2)
            end = min(len(lines), i+8)
            for j in range(start, end):
                print(f"{j+1}: {lines[j]}")
            print("---")
        i += 1
PY

Repository: y-scope/clp

Length of output: 7461


Enable Tokio’s macros feature explicitly.

#[tokio::main] is used in components/compression-coordinator/src/bin/compression_coordinator.rs, but this crate’s tokio dependency only enables time, rt-multi-thread, and signal. Add macros so the macro dependency does not depend on unrelated workspace crates enabling it transitively.

Proposed fix
-tokio = { version = "1.52.3", features = ["time", "rt-multi-thread", "signal"] }
+tokio = { version = "1.52.3", features = ["time", "rt-multi-thread", "signal", "macros"] }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tokio = { version = "1.52.3", features = ["time", "rt-multi-thread", "signal"] }
tokio = { version = "1.52.3", features = ["time", "rt-multi-thread", "signal", "macros"] }
🤖 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/Cargo.toml` at line 28, Update the tokio
dependency features in Cargo.toml to explicitly include macros alongside time,
rt-multi-thread, and signal, ensuring the #[tokio::main] usage in
compression_coordinator is directly supported.

Comment on lines +79 to +84
let spider_client = SpiderClient::builder(endpoint)
.connect()
.await
.inspect_err(|e| {
tracing::error!(error = % e, "Failed to connect to Spider.");
})?;

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does tonic 0.14's Endpointbuilder have aconnect_timeout method?

💡 Result:

Yes, the Endpoint builder in tonic 0.14 has a connect_timeout method [1][2]. This method allows you to apply a timeout to the connection process for the URI [1][3]. The method has the following signature: pub fn connect_timeout(self, dur: Duration) -> Self [1][4] It is used as follows: rust builder.connect_timeout(Duration::from_secs(5)); [1][3] When called, it sets a timeout for the connection attempt, and it defaults to no timeout if not configured [1][2]. The configured timeout is applied when connect or connect_with_connector is called on the Endpoint [1][3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
fd -a 'coordination\.rs$|compression-coordinator' . | sed 's#^\./##' | head -50

echo "== coordination.rs endpoint/connection section =="
if [ -f components/compression-coordinator/src/coordination.rs ]; then
  nl -ba components/compression-coordinator/src/coordination.rs | sed -n '55,95p'
fi

echo "== tonic dependency/version references =="
rg -n --glob 'Cargo.toml|Cargo.lock' 'tonic|tonic-build' . | head -80

echo "== endpoint imports/usages in compression-coordinator =="
rg -n 'Endpoint::from_shared|connect_timeout|connect\(\)' components/compression-coordinator/src || true

Repository: y-scope/clp

Length of output: 380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== coordination.rs endpoint/connection section =="
awk '{printf "%6d  %s\n", NR, $0}' components/compression-coordinator/src/coordination.rs | sed -n '55,100p'

echo "== tonic dependency/version references =="
rg -n --glob 'Cargo.toml|Cargo.lock' 'tonic|tonic-build' . | head -80

echo "== endpoint imports/usages in compression-coordinator =="
rg -n 'Endpoint::from_shared|connect_timeout|connect\(\)' components/compression-coordinator/src || true

echo "== imports mentioning duration =="
awk '{printf "%6d  %s\n", NR, $0}' components/compression-coordinator/src/coordination.rs | sed -n '1,45p'

Repository: y-scope/clp

Length of output: 2658


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Cargo files =="
fd -a 'Cargo\.toml|Cargo\.lock$' . | sed 's#^\./##'

echo "== tonic dependency/version references =="
[ -f Cargo.lock ] && rg -n 'package\s*=.*tonic|name\s*=\s*"tonic"|version\s*=\s*' Cargo.lock | head -80 || true
[ -f components/compression-coordinator/Cargo.toml ] && rg -n 'tonic|tonic-build' components/compression-coordinator/Cargo.toml || true
[ -f Cargo.toml ] && rg -n 'tonic|tonic-build' Cargo.toml || true

echo "== endpoint imports/usages in compression-coordinator =="
rg -n 'Endpoint::from_shared|connect_timeout|SpiderClient::builder|connect\(\)' components/compression-coordinator/src || true

echo "== imports mentioning duration/transport/channel =="
awk '{printf "%6d  %s\n", NR, $0}' components/compression-coordinator/src/coordination.rs | sed -n '1,30p'

Repository: y-scope/clp

Length of output: 4033


Add a connect timeout for the Spider gRPC endpoint.

connect_timeout() is available on Tonic’s Endpoint, but this path builds the endpoint without one, so coordinator startup can block indefinitely if Spider is unreachable.

🔒 Suggested fix
-        let endpoint = Endpoint::from_shared(endpoint_str)
-            .inspect_err(|e| {
-                tracing::error!(error = % e, "Failed to create Spider endpoint.");
-            })
-            .map_err(|e| Error::InvalidEndpoint(e.to_string()))?;
+        let endpoint = Endpoint::from_shared(endpoint_str)
+            .inspect_err(|e| {
+                tracing::error!(error = % e, "Failed to create Spider endpoint.");
+            })
+            .map_err(|e| Error::InvalidEndpoint(e.to_string()))?
+            .connect_timeout(Duration::from_secs(10));
🤖 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 79 - 84,
Configure a finite connection timeout on the Tonic Endpoint in the SpiderClient
builder flow before calling connect(). Use the existing endpoint construction
associated with SpiderClient and preserve the current error logging and
propagation behavior.

Comment thread components/compression-coordinator/src/coordination.rs
Comment on lines +460 to +523
async fn get_or_create_resource_group_id(
resource_group_config: &SpiderResourceGroup,
spider_client: &SpiderClient,
db_pool: &sqlx::MySqlPool,
) -> Result<ResourceGroupId, Error> {
const SPIDER_RESOURCE_GROUP_TABLE_NAME: &str = "spider_resource_groups";

const CREATE_TABLE_QUERY: &str = formatcp!(
"CREATE TABLE IF NOT EXISTS `{table}` (
`rg_name` VARCHAR(255) NOT NULL,
`rg_id` BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (`rg_name`) USING BTREE
) ROW_FORMAT=DYNAMIC",
table = SPIDER_RESOURCE_GROUP_TABLE_NAME,
);
const SELECT_QUERY: &str = formatcp!(
"SELECT `rg_id` FROM `{table}` WHERE `rg_name` = ?;",
table = SPIDER_RESOURCE_GROUP_TABLE_NAME,
);
const INSERT_QUERY: &str = formatcp!(
"INSERT INTO `{table}` (`rg_name`, `rg_id`) VALUES (?, ?);",
table = SPIDER_RESOURCE_GROUP_TABLE_NAME,
);

sqlx::query(CREATE_TABLE_QUERY).execute(db_pool).await?;

let resource_group = resource_group_config.name.as_str();
let existing_rg_id: Option<u64> = sqlx::query_scalar(SELECT_QUERY)
.bind(resource_group)
.fetch_optional(db_pool)
.await?;
if let Some(spider_rg_id) = existing_rg_id {
tracing::info!(
resource_group = % resource_group,
spider_rg_id = % spider_rg_id,
"Resource group already registered. Returning Spider resource group ID."
);
return Ok(ResourceGroupId::from(spider_rg_id));
}

// NOTE: For now, Spider does not enforce resource group credential validation. The password is
// hardcoded to be the same as the username.
let resource_group_id = spider_client
.add_resource_group(
resource_group.to_owned(),
resource_group.as_bytes().to_vec(),
)
.await?;

sqlx::query(INSERT_QUERY)
.bind(resource_group)
.bind(resource_group_id.get())
.execute(db_pool)
.await
.inspect_err(|e| {
tracing::error!(
error = % e,
"Failed to insert resource group into database. This might be a race condition. \
Restart the service to retry."
);
})?;

Ok(resource_group_id)
}

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.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Resource-group registration race requires a manual restart to recover.

The comment at lines 500-501/514-519 already acknowledges a race between the SELECT and INSERT here; on conflict, the operator is told to "Restart the service to retry." Since the failure mode is a benign duplicate-key race, this can self-heal without operator intervention.

♻️ Suggested self-healing approach
-    sqlx::query(INSERT_QUERY)
-        .bind(resource_group)
-        .bind(resource_group_id.get())
-        .execute(db_pool)
-        .await
-        .inspect_err(|e| {
-            tracing::error!(
-                error = % e,
-                "Failed to insert resource group into database. This might be a race condition. \
-                 Restart the service to retry."
-            );
-        })?;
-
-    Ok(resource_group_id)
+    match sqlx::query(INSERT_QUERY)
+        .bind(resource_group)
+        .bind(resource_group_id.get())
+        .execute(db_pool)
+        .await
+    {
+        Ok(_) => Ok(resource_group_id),
+        Err(sqlx::Error::Database(e)) if e.is_unique_violation() => {
+            // Another coordinator won the race; re-read the value it inserted.
+            let winning_rg_id: u64 = sqlx::query_scalar(SELECT_QUERY)
+                .bind(resource_group)
+                .fetch_one(db_pool)
+                .await?;
+            Ok(ResourceGroupId::from(winning_rg_id))
+        }
+        Err(e) => Err(e.into()),
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async fn get_or_create_resource_group_id(
resource_group_config: &SpiderResourceGroup,
spider_client: &SpiderClient,
db_pool: &sqlx::MySqlPool,
) -> Result<ResourceGroupId, Error> {
const SPIDER_RESOURCE_GROUP_TABLE_NAME: &str = "spider_resource_groups";
const CREATE_TABLE_QUERY: &str = formatcp!(
"CREATE TABLE IF NOT EXISTS `{table}` (
`rg_name` VARCHAR(255) NOT NULL,
`rg_id` BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (`rg_name`) USING BTREE
) ROW_FORMAT=DYNAMIC",
table = SPIDER_RESOURCE_GROUP_TABLE_NAME,
);
const SELECT_QUERY: &str = formatcp!(
"SELECT `rg_id` FROM `{table}` WHERE `rg_name` = ?;",
table = SPIDER_RESOURCE_GROUP_TABLE_NAME,
);
const INSERT_QUERY: &str = formatcp!(
"INSERT INTO `{table}` (`rg_name`, `rg_id`) VALUES (?, ?);",
table = SPIDER_RESOURCE_GROUP_TABLE_NAME,
);
sqlx::query(CREATE_TABLE_QUERY).execute(db_pool).await?;
let resource_group = resource_group_config.name.as_str();
let existing_rg_id: Option<u64> = sqlx::query_scalar(SELECT_QUERY)
.bind(resource_group)
.fetch_optional(db_pool)
.await?;
if let Some(spider_rg_id) = existing_rg_id {
tracing::info!(
resource_group = % resource_group,
spider_rg_id = % spider_rg_id,
"Resource group already registered. Returning Spider resource group ID."
);
return Ok(ResourceGroupId::from(spider_rg_id));
}
// NOTE: For now, Spider does not enforce resource group credential validation. The password is
// hardcoded to be the same as the username.
let resource_group_id = spider_client
.add_resource_group(
resource_group.to_owned(),
resource_group.as_bytes().to_vec(),
)
.await?;
sqlx::query(INSERT_QUERY)
.bind(resource_group)
.bind(resource_group_id.get())
.execute(db_pool)
.await
.inspect_err(|e| {
tracing::error!(
error = % e,
"Failed to insert resource group into database. This might be a race condition. \
Restart the service to retry."
);
})?;
Ok(resource_group_id)
}
match sqlx::query(INSERT_QUERY)
.bind(resource_group)
.bind(resource_group_id.get())
.execute(db_pool)
.await
{
Ok(_) => Ok(resource_group_id),
Err(sqlx::Error::Database(e)) if e.is_unique_violation() => {
// Another coordinator won the race; re-read the value it inserted.
let winning_rg_id: u64 = sqlx::query_scalar(SELECT_QUERY)
.bind(resource_group)
.fetch_one(db_pool)
.await?;
Ok(ResourceGroupId::from(winning_rg_id))
}
Err(e) => Err(e.into()),
}
🤖 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 460 -
523, Update get_or_create_resource_group_id to self-heal duplicate-key races
during INSERT_QUERY execution: detect a MySQL duplicate-key error, re-query
SELECT_QUERY for the existing rg_id, and return that ID instead of failing or
requiring a restart. Preserve propagation and logging for non-duplicate database
errors, and keep the existing successful insert path unchanged.

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

LGTM


// `None` if a shutdown signal arrived while the coordinator is still running; `Some` if the
// coordinator returned on its own (an early exit, possibly on error).
let early_exit = tokio::select! {

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.

early_exit is a confusing name since it holds coordinator result.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Changed to early_exit_result.

@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 (1)
components/compression-coordinator/src/coordination.rs (1)

254-289: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not mark skipped legacy jobs as dispatched.

Line 254 collects every fetched ID before deserialisation and handle creation. Consequently, an UnsupportedInputConfig job that Line 276 intentionally leaves for Celery is still returned and receives dispatch_time in mark_jobs_dispatched, preventing later pending-job polling from seeing it. Collect IDs only after successfully spawning the Spider job.

Proposed fix
-        let dispatched_job_ids: Vec<CompressionJobId> =
-            new_job_rows.iter().map(|row| row.id).collect();
+        let mut dispatched_job_ids = Vec::with_capacity(new_job_rows.len());
         for job_row in new_job_rows {
             let job_id = job_row.id;
             // ...
             tokio::spawn(async move {
                 let _ = job_handle.run().await.inspect_err(|e| {
                     tracing::error!(
                         error = % e,
                         job_id = % job_id,
                         "Failed to schedule S3 compression job."
                     );
                 });
             });
+            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 254 -
289, Update the dispatched_job_ids collection in the new-job scheduling flow so
IDs are added only after create_job_handle succeeds and the Spider job is
spawned. Remove the upfront collection of all new_job_rows, preserving skipped
deserialization or handle-creation failures as undispatched for later polling.
🤖 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 254-289: Update the dispatched_job_ids collection in the new-job
scheduling flow so IDs are added only after create_job_handle succeeds and the
Spider job is spawned. Remove the upfront collection of all new_job_rows,
preserving skipped deserialization or handle-creation failures as undispatched
for later polling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8c95d3e2-0ab6-408f-925a-15e9a9e1605e

📥 Commits

Reviewing files that changed from the base of the PR and between 0ddd1e5 and f1623b0.

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

@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 (3)
components/compression-coordinator/src/bin/compression_coordinator.rs (3)

129-134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Preserve the original task errors.

Both failure branches log the actual error, then discard it and return a constant message. Return or wrap e and err instead so callers retain the coordinator failure or join/panic cause. anyhow supports preserving an underlying error when it is wrapped. (docs.rs)

Proposed fix
-            Err(anyhow::anyhow!(
-                "Compression coordinator returned on error."
-            ))
+            Err(e.into())
...
-            Err(anyhow::anyhow!(ERROR_MESSAGE))
+            Err(err.into())

Also applies to: 135-136, 138-138

🤖 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/bin/compression_coordinator.rs` around
lines 129 - 134, Update both failure branches around the task result handling:
preserve the original errors by returning or wrapping the branch variables e and
err with anyhow context instead of returning constant messages. Keep the
existing tracing::error! logging and ensure coordinator failures and join/panic
causes remain available to callers.

Source: MCP tools


71-81: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Include recovered job tasks in shutdown ownership.

Coordinator::new starts recovery work with tokio::spawn, and that future has no cancellation token. When shutdown cancels coordinator.run()/main, those recovery tasks can continue unaware, so keep their join handles and drain them before returning, or pass the cancellation token into recovery and cancel those tasks explicitly.

🤖 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/bin/compression_coordinator.rs` around
lines 71 - 81, Update Coordinator::new and the compression_coordinator startup
flow to retain ownership of every recovery task spawned during job recovery,
rather than dropping their JoinHandles. Either return those handles alongside
the coordinator and cancellation token and await/drain them during shutdown, or
propagate the cancellation token into recovery tasks and cancel them explicitly
before returning; ensure recovered tasks cannot outlive coordinator.run()/main.

53-53: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Pass the log message as a format string.

tracing::error!(ERROR_MESSAGE) and tracing::error!(error = % err, ERROR_MESSAGE) treat ERROR_MESSAGE as a field/message parameter in the wrong position rather than the unstructured event message. Use a format string after the fields so these log events compile and display the intended text.

Proposed fix
-        tracing::error!(ERROR_MESSAGE);
+        tracing::error!("{}", ERROR_MESSAGE);
...
-            tracing::error!(error = % err, ERROR_MESSAGE);
+            tracing::error!(error = % err, "{}", ERROR_MESSAGE);
🤖 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/bin/compression_coordinator.rs` at
line 53, Update the tracing::error! invocation in the compression coordinator
error path to pass ERROR_MESSAGE as the format string after any structured
fields, preserving the intended unstructured event message and ensuring the
macro compiles correctly.

Source: MCP tools

🤖 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/bin/compression_coordinator.rs`:
- Around line 102-103: Update the shutdown flow around
cancellation_token.cancel() to drain or complete dispatch bookkeeping for the
current batch before cancelling the coordinator poll loop. Ensure jobs already
submitted to Spider are marked dispatched via mark_jobs_dispatched before
return, or make submission and status updates atomic/idempotent so restart
cannot resubmit Pending jobs.

---

Outside diff comments:
In `@components/compression-coordinator/src/bin/compression_coordinator.rs`:
- Around line 129-134: Update both failure branches around the task result
handling: preserve the original errors by returning or wrapping the branch
variables e and err with anyhow context instead of returning constant messages.
Keep the existing tracing::error! logging and ensure coordinator failures and
join/panic causes remain available to callers.
- Around line 71-81: Update Coordinator::new and the compression_coordinator
startup flow to retain ownership of every recovery task spawned during job
recovery, rather than dropping their JoinHandles. Either return those handles
alongside the coordinator and cancellation token and await/drain them during
shutdown, or propagate the cancellation token into recovery tasks and cancel
them explicitly before returning; ensure recovered tasks cannot outlive
coordinator.run()/main.
- Line 53: Update the tracing::error! invocation in the compression coordinator
error path to pass ERROR_MESSAGE as the format string after any structured
fields, preserving the intended unstructured event message and ensuring the
macro compiles correctly.
🪄 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: c86b252c-0cda-463c-84c6-74aa3003dd4c

📥 Commits

Reviewing files that changed from the base of the PR and between f1623b0 and 34e88e0.

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

Comment on lines +102 to +103
// Request a graceful stop. A no-op if the coordinator has already returned.
cancellation_token.cancel();

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

Drain dispatch bookkeeping before cancelling the poll loop.

components/compression-coordinator/src/coordination.rs:160-206 breaks on cancellation during the polling sleep before calling mark_jobs_dispatched. Cancelling here can therefore leave jobs already submitted to Spider in Pending metadata state, allowing them to be selected and submitted again after restart. Finish the status transition for the current batch, or make submission and status updates atomic/idempotent, 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/bin/compression_coordinator.rs` around
lines 102 - 103, Update the shutdown flow around cancellation_token.cancel() to
drain or complete dispatch bookkeeping for the current batch before cancelling
the coordinator poll loop. Ensure jobs already submitted to Spider are marked
dispatched via mark_jobs_dispatched before return, or make submission and status
updates atomic/idempotent so restart cannot resubmit Pending jobs.

sitaowang1998
sitaowang1998 previously approved these changes Jul 27, 2026
@20001020ycx

20001020ycx commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Environment

Procedure and results
Ingest logs with log-ingestor from S3, compress with Spider and search with Celery. Results returned as expected.

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

Confirmed e2e working with the methodology explained in #2417 (comment)

@LinZhihao-723
LinZhihao-723 merged commit 66a7874 into y-scope:main Jul 29, 2026
27 checks passed
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.

4 participants