Skip to content

fix(client): verify stale service markers - #55668

Open
tomopas wants to merge 5 commits into
anomalyco:devfrom
tomopas:port-recovery-owner-check
Open

tomopas wants to merge 5 commits into
anomalyco:devfrom
tomopas:port-recovery-owner-check

Conversation

@tomopas

@tomopas tomopas commented Oct 15, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #54267

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

A managed service can lose its registration while its process still owns the configured port. The port marker remains available for that recovery case, while marker-based termination now requires the recorded PID to be live and the recorded endpoint to still own a listening port. The marker regression test synchronizes with the fixture instead of relying on a fixed time window.

How did you verify your code works?

  • packages/client: bun run typecheck
  • packages/cli: bun run typecheck
  • Focused client marker recovery test
  • git diff --check

Screenshots / recordings

Not applicable.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #54181, #50672

tomopas and others added 5 commits October 14, 2026 23:35
Remove the port marker after the listener has closed and verify marker-owned services before termination. Replace lifecycle sleeps with polling.\n\nCo-authored-by: Copilot <[email protected]>
Require a live marker PID and occupied endpoint before terminating an unresponsive owner, and synchronize the marker regression test with the fixture.\n\nCo-authored-by: Copilot <[email protected]>
@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

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

With the bot's reproduction for #54267 (registration deleted, old service paused with SIGSTOP), the base fails after about 21 s with "Managed service port … already in use". With this PR, service start kills the unresponsive owner it finds through the .port marker and comes up on the same port in about 15 s. Client tests (26) pass, and typecheck passes in packages/client and packages/cli. The new client test fails on the base, so it does guard against the bug.

Public API: no exported signatures change. The only external addition is a new state file, <registration>.port (for example service.json.port), written and removed by the service. Nothing reads it except the client. Backwards compatible.

Worth fixing:

  • The new CLI lifecycle test passes unchanged on the base, so it does not cover the fix (see inline).
  • The extra registration read added on every poll in ensure is redundant (see inline).

Related: #54181, #50672

signal(original.pid, "SIGSTOP")
await fs.rm(home.registration)
const starting = cli(home, "service", "start")
await until(async () => {

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.

This test also passes on the base branch; I ran it there unchanged. until returns as soon as servers(home) is [original.pid], and that is already true before the client has done anything, so the wait checks nothing. After SIGCONT, the old service sees its registration is gone and shuts down by itself, and the base contender's 15 s bind retry then succeeds. Could you either make this fail without the fix (e.g. leave the old service paused and expect it to be replaced, which is the case from #54267), or drop it and rely on the client test, which does fail on the base?

const found = yield* Effect.gen(function* () {
const registration = yield* registered(options.file, timing.requestTimeout)
const registrationFile = options.file ?? fallback()
const registrationPresent = (yield* read(registrationFile)) !== undefined

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.

This adds a second read of the registration file on every poll (every 25 ms), and the result doesn't change anything: registered() only returns info when the registration or the marker was read, so registrationPresent || registration.markerOnly is always true when info !== undefined. The same applies in promise/service.ts (line 55). Dropping the extra read and the condition would keep the behaviour and make the loop simpler.

This branch has not been deployed

No deployments
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.

service: relaunch fails with "Managed service port already in use" when previous background service has not exited

1 participant