Skip to content

fix(worker): do not use an ephemeral container ID as the worker identity - #134

Merged
GeiserX merged 2 commits into
mainfrom
fix/worker-identity-hostname-drift
Jul 31, 2026
Merged

fix(worker): do not use an ephemeral container ID as the worker identity#134
GeiserX merged 2 commits into
mainfrom
fix/worker-identity-hostname-drift

Conversation

@GeiserX

@GeiserX GeiserX commented Jul 31, 2026

Copy link
Copy Markdown
Owner

The bug

A worker keys its UI row — and its per-worker fleet key — on a client_id. When none was persisted yet, an already-enrolled worker fell back to WORKER_NAME, which defaults to socket.gethostname(). Inside Docker that is the first 12 hex chars of the container ID, regenerated on every recreate.

So every image bump minted a new identity. The worker presented its still-valid per-worker key under a client_id the UI had never enrolled, the UI correctly refused it, and every heartbeat returned 401 Unauthorized.

The failure is silent. Service containers keep running and earning, so nothing looks broken — the fleet just loses the worker. Hit in production upgrading a live 3-worker fleet from 1.0.0 to 1.4.1:

POST http://cashpilot-ui:8080/api/workers/heartbeat "HTTP/1.1 401 Unauthorized"

The UI listed the worker as 159d39365879; the recreated container had invented 515ccbc46cd9.

The fix

Narrow deliberately. The migration is worth keeping — on bare metal or a VM the hostname is stable, and reusing it preserves the row. Only the Docker container-ID shape is rejected, and only when /.dockerenv confirms we are in a container, so existing non-container workers migrate exactly as before (their test still passes unchanged).

Also: after three consecutive 401s while holding our own key, log the concrete remediation — which client_id we are sending and the file to write the expected one into — instead of repeating a generic warning forever.

Verification

  • 1317 tests pass; ruff check + ruff format --check clean
  • New regression tests cover: container-ID rejected, real hostname still migrates, hex-shaped hostname outside Docker still migrates, persisted id always wins, first run persists for the next recreate
  • Fix applied by hand to the live fleet first: all 3 workers recovered to 200 OK with key_confirmed=1, and pre-seeding /data/.worker_id on the other two servers upgraded them with zero 401s

Docs: new Worker identity section in docs/fleet.md with the symptom and the recovery steps, and CASHPILOT_WORKER_NAME now flagged as one to set on Docker workers.

Summary by CodeRabbit

  • Bug Fixes

    • Improved worker identity handling in Docker environments to prevent unstable container names from causing authentication failures.
    • Added alerts after repeated authentication failures, including recovery guidance.
    • Successful heartbeats now clear authentication failure alerts.
  • Documentation

    • Expanded worker configuration guidance for Docker deployments.
    • Documented persistent worker identities, authentication errors caused by identity changes, and recovery steps.
  • Tests

    • Added coverage for stable identity persistence, migration scenarios, and ephemeral container-name detection.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@GeiserX, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d8a470d3-5882-4a19-a70a-0af91286d994

📥 Commits

Reviewing files that changed from the base of the PR and between 0951090 and 6433b04.

📒 Files selected for processing (3)
  • app/worker_api.py
  • docs/fleet.md
  • tests/test_worker_keys.py
📝 Walkthrough

Walkthrough

The worker API now avoids using ephemeral Docker container IDs as migrated client identities. It tracks consecutive authenticated heartbeat failures, resets the count after success, and reports recovery guidance after three 401 responses. Documentation and regression tests cover identity persistence and recovery.

Changes

Worker identity recovery

Layer / File(s) Summary
Worker identity migration
app/worker_api.py, tests/test_worker_keys.py, docs/fleet.md
Worker identity generation rejects ephemeral Docker container IDs, preserves stable names, prioritizes persisted IDs, and documents identity recovery. Tests cover these cases and the lowercase hexadecimal predicate.
Heartbeat authentication failure handling
app/worker_api.py
Heartbeat processing tracks consecutive 401 responses for per-worker keys, resets failures after successful heartbeats, and emits recovery guidance after three failures.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

🚥 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: preventing ephemeral Docker container IDs from becoming worker identities.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/worker-identity-hostname-drift

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.

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 93.88%. Comparing base (25e80a6) to head (6433b04).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
app/worker_api.py 94.11% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #134      +/-   ##
==========================================
- Coverage   93.88%   93.88%   -0.01%     
==========================================
  Files          35       35              
  Lines        4102     4118      +16     
==========================================
+ Hits         3851     3866      +15     
- Misses        251      252       +1     
Files with missing lines Coverage Δ
app/worker_api.py 87.14% <94.11%> (+0.31%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Actionable comments posted: 3

🤖 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 `@app/worker_api.py`:
- Around line 164-173: Update the recovery warning in the worker identity
handling around _active_key() to remove the claim that the worker will enroll as
a new worker. State instead that heartbeats will be rejected until the persisted
client_id is restored, while preserving the existing remediation instructions
and placeholders.
- Line 218: Update the heartbeat handling function around the global
_consecutive_auth_failures state so both the non-401 HTTPStatusError branch and
the general exception branch reset the counter to zero before handling the
outcome. Preserve the existing increment behavior for 401 responses and reset on
successful heartbeats, and add sequence tests covering 401 failures interrupted
by timeout, other HTTP errors, or general exceptions.

In `@docs/fleet.md`:
- Line 155: Update the CASHPILOT_WORKER_NAME entry in the environment-variable
table to describe it strictly as a display-name setting, stating that
configuring it keeps the worker’s display name stable. Remove the claim that
leaving it unset makes identity fall back to the Docker container hostname, and
retain the legacy recovery behavior details in the Worker identity section.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 46112d17-2fd5-4064-8867-ccd31c69038f

📥 Commits

Reviewing files that changed from the base of the PR and between 321face and 0951090.

📒 Files selected for processing (3)
  • app/worker_api.py
  • docs/fleet.md
  • tests/test_worker_keys.py

Comment thread app/worker_api.py Outdated
Comment thread app/worker_api.py
Comment thread docs/fleet.md Outdated
@GeiserX

GeiserX commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

All three addressed — two of them caught my own text being factually wrong:

  1. "It will enroll as a NEW worker" — you're right, and production proved it. With a per-worker key present, _active_key() sends that key, the UI refuses it under an id it never enrolled, and the worker just 401-loops; no new row is ever created. That is exactly what I observed on watchtower. Reworded to say heartbeats will be rejected until the id is restored, and raised to error level.

  2. Counter reset — fixed. Any non-401 outcome now resets it, in both the other HTTPStatusError path and the general exception path. Added sequence tests: 401,401,401 reaches the alarm; 401,timeout,401,500,401 stops at 1; 401,401,503,401 stops at 1; 401,401,200 resets to 0.

  3. Docs wording — correct, that line described behaviour this PR removes. It now says what the variable actually does: keeps the display name stable across recreates, and gives a pre-existing worker a durable identity to migrate on. The recovery details stay in the Worker identity section.

1321 tests green, ruff clean.

GeiserX added 2 commits July 31, 2026 23:07
A worker keys its UI row and its per-worker fleet key on a client_id. When no
client_id was persisted yet, an already-enrolled worker reused WORKER_NAME,
which defaults to socket.gethostname() — inside Docker that is the first 12 hex
characters of the container ID, regenerated on every recreate.

So every image bump minted a new identity. The worker then presented its still
valid per-worker key under a client_id the UI had never enrolled, the UI
correctly refused it, and every heartbeat returned 401 while the service
containers kept running and earning. Nothing else surfaced the problem — the
fleet just silently lost the worker. Hit in production upgrading 1.0.0 -> 1.4.1.

The migration itself is worth keeping: on bare metal or a VM the hostname is
stable and reusing it preserves the row. Only the Docker container-ID shape is
rejected, and only when actually running inside a container, so existing
non-container workers migrate exactly as before.

Also: after three consecutive 401s while holding our own key, log the concrete
remediation (which client_id we are sending, and the file to write the expected
one into) instead of repeating a generic warning forever.
…secutive

Three review points:

- The warning claimed the worker would 'enroll as a NEW worker'. It cannot: it
  still authenticates with its existing per-worker key, which the UI refuses
  under an id it never enrolled, so heartbeats are simply rejected until the id
  is restored by hand. Says that now, at error level.
- Only a successful heartbeat reset the 401 counter, so 401 -> timeout -> 401 ->
  500 -> 401 tripped the alarm without three consecutive rejections. Any
  non-401 outcome now breaks the run.
- The env-var table described identity falling back to the container hostname,
  which this change removed. It now says what the variable actually does: keeps
  the display name stable, and gives pre-existing workers something to migrate on.
@GeiserX
GeiserX force-pushed the fix/worker-identity-hostname-drift branch from 4c639b8 to 6433b04 Compare July 31, 2026 21:07
@GeiserX
GeiserX merged commit 060d5f3 into main Jul 31, 2026
5 checks passed
@GeiserX
GeiserX deleted the fix/worker-identity-hostname-drift branch July 31, 2026 21:08
GeiserX added a commit that referenced this pull request Aug 5, 2026
…ity (#134)

* fix(worker): do not use an ephemeral container ID as the worker identity

A worker keys its UI row and its per-worker fleet key on a client_id. When no
client_id was persisted yet, an already-enrolled worker reused WORKER_NAME,
which defaults to socket.gethostname() — inside Docker that is the first 12 hex
characters of the container ID, regenerated on every recreate.

So every image bump minted a new identity. The worker then presented its still
valid per-worker key under a client_id the UI had never enrolled, the UI
correctly refused it, and every heartbeat returned 401 while the service
containers kept running and earning. Nothing else surfaced the problem — the
fleet just silently lost the worker. Hit in production upgrading 1.0.0 -> 1.4.1.

The migration itself is worth keeping: on bare metal or a VM the hostname is
stable and reusing it preserves the row. Only the Docker container-ID shape is
rejected, and only when actually running inside a container, so existing
non-container workers migrate exactly as before.

Also: after three consecutive 401s while holding our own key, log the concrete
remediation (which client_id we are sending, and the file to write the expected
one into) instead of repeating a generic warning forever.

* fix(worker): correct the lockout warning and make the alarm truly consecutive

Three review points:

- The warning claimed the worker would 'enroll as a NEW worker'. It cannot: it
  still authenticates with its existing per-worker key, which the UI refuses
  under an id it never enrolled, so heartbeats are simply rejected until the id
  is restored by hand. Says that now, at error level.
- Only a successful heartbeat reset the 401 counter, so 401 -> timeout -> 401 ->
  500 -> 401 tripped the alarm without three consecutive rejections. Any
  non-401 outcome now breaks the run.
- The env-var table described identity falling back to the container hostname,
  which this change removed. It now says what the variable actually does: keeps
  the display name stable, and gives pre-existing workers something to migrate on.
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.

1 participant