Skip to content

Skip duplicate managed service launch when already alive - #50

Merged
volen-silo merged 3 commits into
mainfrom
fix/duplicate-managed-service
Jun 25, 2026
Merged

Skip duplicate managed service launch when already alive#50
volen-silo merged 3 commits into
mainfrom
fix/duplicate-managed-service

Conversation

@volen-silo

Copy link
Copy Markdown
Collaborator

Problem

rocm serve <model> --managed spawns a managed-service process unconditionally. Running it a second time for the same engine+model — e.g. the chat assistant re-issuing rocm serve <path> --engine <e> --managed via the rocm_command tool, or re-confirming the serve wizard — spawns a second process for the same model/port. Both run simultaneously, causing port conflicts and a doubled entry in the dashboard.

The TUI job-bridge has an idempotency guard, but it only covers TUI job state — not the OS process. Once the first job completes (the managed daemon detaches quickly), the guard clears and a second launch succeeds.

Fix

start_managed_service is the single choke point for every managed launch. Added an idempotency guard there:

  • existing_live_managed_service(paths, service_id) reuses the existing load_managed_service (which refreshes liveness) + managed_service_is_live. Since service_id = generate_service_id(engine, canonical_model_id), the same engine+model always collides and is detectable.
  • If a live service is found, surface it (endpoint, status), log a managed_service_launch_skipped audit event, and return Ok(())no second process spawned. Treated as idempotent success so the chat assistant sees the repeat request as satisfied, not failed.
  • Stale/dead manifests refresh to stopped and yield None, so a genuine relaunch still proceeds.

The file-based manifest persists across the job-bridge race, so this is robust where the TUI guard wasn't.

Tests

  • duplicate_managed_launch_is_detected_when_live — live PID ⇒ detected.
  • dead_managed_service_allows_relaunch — dead PID ⇒ relaunch allowed.
  • missing_manifest_allows_launch — no manifest ⇒ launch proceeds.

cargo test -p rocm (managed suite) and cargo clippy -p rocm --all-targets both green.

Known limitation

Out of scope: two truly-concurrent launches within ~200ms before either writes a live manifest. The realistic "asked twice" sequence is fully serialized, so the file guard catches it.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds an idempotency check to rocm serve <model> --managed so repeating the command doesn’t spawn a second managed-service process for the same model, reducing port conflicts and duplicate dashboard entries.

Changes:

  • Adds an early-exit guard in start_managed_service intended to detect an already-live managed service and skip spawning.
  • Introduces a helper (existing_live_managed_service) to load/refresh service liveness and decide whether to skip.
  • Adds unit tests covering live, dead, and missing-manifest scenarios for the guard behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/rocm/src/main.rs
Comment on lines +3714 to +3736
// Idempotency guard: if a managed service for this engine+model is already
// alive, surface it and spawn nothing. A second `serve --managed` (e.g. the
// chat assistant re-issuing the same request) is treated as satisfied, not
// an error. Stale/dead services fall through and relaunch normally.
if let Some(existing) = existing_live_managed_service(&paths, service_id) {
println!("managed service already running");
println!(" service_id: {service_id}");
println!(" endpoint: {}", existing.endpoint_url);
println!(" status: {}", existing.status);
println!(" note: existing service detected; no second process spawned");
record_cli_audit_event(
&paths,
"service",
"managed_service_launch_skipped",
"info",
format!(
"skipped duplicate managed launch service_id={service_id} status={}",
existing.status
),
Some(service_id),
);
return Ok(());
}
Comment thread apps/rocm/src/main.rs Outdated
Comment on lines +11931 to +11946
/// Idempotency guard for managed launches: returns the existing record when a
/// managed service with this `service_id` is already alive. `load_managed_service`
/// refreshes liveness, so a stale manifest (dead PID) demotes to "stopped" and
/// yields `None` — letting a genuine relaunch proceed. Prevents a second
/// `serve --managed` for the same engine+model from spawning a duplicate process
/// once the TUI job-bridge guard has cleared.
fn existing_live_managed_service(
paths: &AppPaths,
service_id: &str,
) -> Option<ManagedServiceRecord> {
if !paths.service_manifest_path(service_id).exists() {
return None;
}
let record = load_managed_service(paths, service_id).ok()?;
managed_service_is_live(&record).then_some(record)
}
Comment thread apps/rocm/src/main.rs Outdated
Comment on lines +17408 to +17432
let mut record = ManagedServiceRecord::new(
&paths,
"svc-dup-live",
"lemonade",
"qwen",
"qwen",
"127.0.0.1",
11500,
"managed",
std::process::id(),
None,
None,
None,
);
record.status = "starting".to_owned();
record.engine_pid = Some(std::process::id());
record.write()?;

let found = existing_live_managed_service(&paths, "svc-dup-live");
let _ = fs::remove_dir_all(root);

let found = found.expect("a live managed service should be detected");
assert_eq!(found.service_id, "svc-dup-live");
assert!(managed_service_is_live(&found));
Ok(())
Comment thread apps/rocm/src/main.rs Outdated
record.engine_pid = Some(999_999_999);
record.write()?;

let found = existing_live_managed_service(&paths, "svc-dup-dead");
Comment thread apps/rocm/src/main.rs Outdated
fn missing_manifest_allows_launch() {
// No manifest on disk → nothing to detect, launch proceeds.
let (root, paths) = test_paths("dup-managed-missing");
let found = existing_live_managed_service(&paths, "svc-absent");
@volen-silo

Copy link
Copy Markdown
Collaborator Author

Good catch from Copilot — this was a real defect. generate_service_id appends unix_time_millis(), so every launch produces a unique service_id; the original guard keyed on it would never match an existing service, and the duplicate would still spawn. The unit tests only passed because they reused a hardcoded id for both write and lookup.

Fixed in 05e7180:

  • existing_live_managed_service now keys on (engine, canonical_model_id) and scans load_managed_services (which refreshes liveness and returns records newest-first), skipping on the first live match.
  • The skip path now reports/audits the existing service_id, not the freshly generated one.
  • Tests reworked: duplicate_managed_launch_detected_across_distinct_service_ids writes an old dead + new live manifest with different service_ids but the same engine+model and asserts the newest live one is detected; added live_service_for_other_model_does_not_block to guard against over-matching.

Note: the License-header CI failure is unrelated and pre-existing on main (6 files lack the SPDX header) — fixed separately in #52.

@volen-silo
volen-silo force-pushed the fix/duplicate-managed-service branch from 05e7180 to 59a4186 Compare June 24, 2026 13:02
@volen-silo
volen-silo force-pushed the fix/duplicate-managed-service branch from d890078 to 8ee0cc4 Compare June 25, 2026 11:18
@volen-silo
volen-silo enabled auto-merge June 25, 2026 11:18
@volen-silo
volen-silo disabled auto-merge June 25, 2026 13:32
@volen-silo
volen-silo enabled auto-merge June 25, 2026 13:32
start_managed_service spawned a new background process unconditionally,
so a second `serve --managed` for the same engine+model (e.g. the chat
assistant re-issuing the request) produced a duplicate process, port
conflict, and a doubled dashboard entry. The TUI job-bridge guard only
covers job state and clears once the first job detaches.

Guard at the single launch choke point: if a live manifest already
exists for the service_id, surface it and spawn nothing (idempotent
success). Stale/dead records refresh to "stopped" and relaunch normally.
generate_service_id embeds unix_time_millis(), so every launch mints a
unique service_id — the previous guard keyed on it would never match an
existing service and the duplicate still spawned. Scan existing managed
services by (engine, canonical_model_id) via load_managed_services
(which refreshes liveness, newest-first) and skip on the first live
match, reporting the existing service_id. Tests now use distinct
service_ids for the same engine+model.
@volen-silo
volen-silo force-pushed the fix/duplicate-managed-service branch from 8ee0cc4 to 125766a Compare June 25, 2026 13:35
@volen-silo
volen-silo added this pull request to the merge queue Jun 25, 2026
Merged via the queue into main with commit babfaa4 Jun 25, 2026
7 checks passed
@volen-silo
volen-silo deleted the fix/duplicate-managed-service branch June 25, 2026 14:06
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