feat(worker): self-update install, restart, health-check, and rollback (#890 C3) - #1910
feat(worker): self-update install, restart, health-check, and rollback (#890 C3)#1910hognek wants to merge 9 commits into
Conversation
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds worker self-update orchestration with checkpointing, dependency updates, draining, restart, health verification, rollback, and controller outcome reporting. Worker heartbeats expose drain completion, and update triggers run during heartbeat cycles. ChangesWorker self-update lifecycle
Estimated code review effort: 4 (Complex) | ~60 minutes Mergeability Score: 🔵 Low · up to The worker self-update flow uses detached restart mechanisms where available but retains a last-resort service-manager fallback that may race with cgroup teardown; the change is otherwise mergeable, but the cgroup-safety claim should be qualified or the fallback addressed before merge. Sequence Diagram(s)sequenceDiagram
participant WorkerAgent
participant ClusterController
participant DeployHelper
WorkerAgent->>ClusterController: report update availability
ClusterController-->>WorkerAgent: return drain_complete heartbeat state
WorkerAgent->>DeployHelper: create checkpoint and restart worker
DeployHelper-->>WorkerAgent: return health-check result
WorkerAgent->>ClusterController: report success or rollback outcome
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
| "output": f"git fetch origin {target_ref} failed", | ||
| "exit_code": rc, | ||
| } | ||
| branch = target_ref |
There was a problem hiding this comment.
WARNING: For a plain branch name (e.g. master), the new remote commits are fetched into origin/master but the worker then checks out the local master branch, which the fetch does NOT advance. The worker will therefore stay on the old commit and the update effectively no-ops. Also, for origin/<branch> refs the code checks out the detached remote-tracking ref rather than a local branch. Resolve by checking out FETCH_HEAD (or remote/branch) after fetch, e.g. git checkout --quiet FETCH_HEAD, or fast-forward the local branch with git checkout --quiet <branch> && git merge --ff-only <remote>/<branch>.
| logger.warning("self-update: drain heartbeat failed — continuing") | ||
|
|
||
| logger.info("self-update: drain wait complete (%.0fs elapsed)", elapsed) | ||
| return True |
There was a problem hiding this comment.
WARNING: _wait_for_drain unconditionally return True regardless of whether drain actually completed or heartbeats failed. As a result results["drain_wait"]["ok"] never reflects a real problem, and the if not drain_ok warning branch / the comment claiming "proceed anyway" is dead. The loop also never inspects the controller-reported drain/lease state (the args agent is only used to send heartbeats). Either detect completion (e.g. a heartbeat returning the worker's status/pending_leases or a controller endpoint) and return that, or drop the misleading ok semantics.
| import json | ||
| import logging | ||
| import os | ||
| import platform |
There was a problem hiding this comment.
SUGGESTION: import platform is unused (no other references in the module). Remove it, and also drop the unused from typing import Optional (line 19) and the unused import datetime (line 446) to keep imports honest.
| run health-check and signal the outcome.""" | ||
| marker = { | ||
| "checkpoint_tag": checkpoint_tag, | ||
| "from_sha": from_sha, |
There was a problem hiding this comment.
SUGGESTION: The marker writes "started_at": None with a comment "filled by caller with ISO timestamp", but no caller ever sets it —run_full_update calls _write_update_marker(...) without a timestamp. This is dead/misleading state. Either populate it (e.g. datetime.datetime.now().isoformat()) or remove the field and the comment.
| if outcome == "success": | ||
| # Worker is already back online via re-registration. | ||
| # Nothing to do — the heartbeat loop handles it. | ||
| logger.info("worker '%s' self-update SUCCESS", name) |
There was a problem hiding this comment.
SUGGESTION: On outcome == "success" the endpoint does nothing beyond logging — the worker is expected to re-register via heartbeat. That's acceptable, but the worker's state on the controller is never explicitly reconciled/cleared here, and there is no record that the update succeeded (only informational logs + a rollback notification on failure). Consider recording the outcome on the worker object (or emitting a worker.update-success notification) so operators have an auditable trail and the controller can detect a stuck "update-available"/"draining" worker that never reports back.
| } | ||
|
|
||
| cmd_health_check() { | ||
| local timeout="${1:-10}" |
There was a problem hiding this comment.
SUGGESTION: cmd_health_check accepts a timeout argument (${1:-10}) but never uses it — no wait/retry/poll loop honors it. The caller run_health_check() passes POST_RESTART_HEALTH_TIMEOUT (30s), implying a timeout is expected, yet the shell check is instantaneous. Either implement a bounded retry/poll against the port/health endpoint using timeout, or remove the unused parameter to avoid implying a timeout is enforced.
Code Review SummaryStatus: No New Issues Found | Recommendation: The prior WARNING at Overview
Issue Details (click to expand)Carry-over from prior review (unchanged code in this diff)
Re-verification of this increment (106664b → ef12411)The diff is no longer empty: 378 insertions / 23 deletions across 5 files. The PR addresses three BLOCKERS (deploy route re-gated to operator session,
No new findings to add. Files Reviewed (incremental diff, 5 files)
Previous Review Summaries (11 snapshots, latest commit 106664b)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 106664b)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Re-verification on this incrementThe diff between the previous review SHA ( The only differences in PR-scope files are unrelated Files Reviewed (incremental diff, 7 PR-scope files)
Fix these issues in Kilo Cloud Previous review (commit 6f605ff)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Previously-reported findings — re-verified on this incrementAll findings from the prior review cycle (commit
Files Reviewed (7 files)
Fix these issues in Kilo Cloud Previous review (commit 618ebc6)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Previously-reported findings — re-verified on this incrementAll findings from the prior review cycle (commit
Files Reviewed (7 files)
Fix these issues in Kilo Cloud Previous review (commit a6e6abf)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Previously-reported findings — re-verified on this incrementAll findings from the prior review cycle (commit
Files Reviewed (incremental commit e6a751e)
Fix these issues in Kilo Cloud Previous review (commit 2bff5d0)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Previously-reported findings — re-verified on this incrementAll findings from the prior review cycle (commit
Files Reviewed (incremental commit e6a751e)
Fix these issues in Kilo Cloud Previous review (commit f967846)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Previously-reported findings — re-verified on this incrementAll findings from the prior review cycle (commit
Files Reviewed (incremental commit e6a751e)
Fix these issues in Kilo Cloud Previous review (commit e6a751e)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Previously-reported findings — re-verified on this incrementAll findings from the prior review cycle (commit
Files Reviewed (incremental commit e6a751e)
Fix these issues in Kilo Cloud Previous review (commit 1ba39cf)Status: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Previously-reported findings (re-verified)The two prior-cycle WARNINGs on
Files Reviewed (7 files, full PR diff vs merge-base)
Fix these issues in Kilo Cloud Previous review (commit b2433d3)Status: No new issues in incremental diff | 2 previous WARNINGs resolved | Recommendation: Merge incremental changes Overview
Resolved since last review (click to expand)WARNING (resolved)
Carried-forward suggestions (unchanged lines, not re-reported)
These remain open but are outside the lines changed in this incremental diff and were not duplicated. Files Reviewed (incremental diff, 2 files)
Previous review (commit a089e02)Status: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Incremental changes reviewed since 6b583bc
Files Reviewed (3 files in incremental diff)
Fix these issues in Kilo Cloud Previous review (commit 6b583bc)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Previously Reported Issues — Resolved in this update
[Snapshot truncated.] Additional previous summary content was truncated to keep this comment within platform limits. Reviewed by minimax-m3:free · Input: 66.6K · Output: 12.2K · Cached: 2M |
| log "worker service restarted" | ||
| } | ||
|
|
||
| cmd_health_check() { |
There was a problem hiding this comment.
SUGGESTION: Removing local timeout="${1:-10}" here leaves the run_health_check(timeout=...) caller's positional arg (_run_helper(["health-check", str(timeout)]) in self_update.py:341) silently ignored.
The timeout parameter is now dead: the shell function no longer reads $1, and the value is never used. The actual subprocess timeout is still enforced by _run_helper's default, so there is no functional break — but the Python-side contract that "the helper applies the health-check timeout" is now misleading. Either drop the timeout parameter from run_health_check, or make the shell actually honor it (e.g. bound the systemctl is-active/port checks with a deadline).
| "output": f"git fetch origin {target_ref} failed", | ||
| "exit_code": rc, | ||
| } | ||
| branch = f"origin/{target_ref}" |
There was a problem hiding this comment.
SUGGESTION: Checking out origin/<branch> fixes the stale-local-branch no-op, but it leaves the working tree in a detached HEAD state. The local <branch> ref is never fast-forwarded, so a subsequent plain-branch update will re-fetch but still check out the remote ref, and any later operation that assumes an attached branch (e.g. tag creation on the branch, future auto-pulls) may behave unexpectedly.
Consider updating the local branch ref instead, e.g. git fetch origin <branch>:<branch> (ff-only) followed by a non-detached checkout, or git branch -f <branch> origin/<branch> after checkout, so the update lands on the branch rather than a detached commit.
|
Two blockers plus the merge conflict (rebase onto post-#1903 dev — this is stacked on #1903 and duplicates its commit). BLOCKER 1 — restart bricks the worker. BLOCKER 2 — the orchestrator is dead code. Lower: |
Review-blocker resolution (6b583bc)BLOCKER 1 — restart bricks the worker ✓
BLOCKER 2 — orchestrator is dead code ✓
Fix — pull_update detached HEAD ✓
Fix — run_health_check unused timeout arg ✓
Rebase — onto current Tests: 68/68 pass ( |
| # after a direct origin/<ref> checkout. | ||
| if "/" in target_ref: | ||
| local_branch = target_ref.split("/", 1)[1] | ||
| await _run_git( |
There was a problem hiding this comment.
SUGGESTION: The git branch -f return code is ignored — await _run_git(...) discards both (rc, out). If the fast-forward fails (e.g. the local branch name collides with a tag or the remote-tracking ref isn't resolvable), the function still falls through to rev-parse HEAD and returns {"ok": True, ...}, silently reporting a successful checkout while the repo may remain in a detached/undesired state. Capture and log the rc (or fail the update) so a fast-forward failure isn't masked as success.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| path = "/api/cluster/heartbeat" | ||
| payload = _json.dumps({ | ||
| "name": name, | ||
| "load": 0.0, |
There was a problem hiding this comment.
WARNING: This drain-complete check fires a second, full heartbeat POST on every loop iteration, right after agent.heartbeat(...) already sent one. The first heartbeat's response body — which now carries drain_complete — is thrown away (because heartbeat() only returns the status code), so each 5s tick doubles heartbeat traffic to the controller and re-signs/re-sends a redundant request.
Additionally, this lightweight payload hardcodes "load": 0.0. The controller's ClusterManager.heartbeat() assigns worker.load = load unconditionally (no None guard, unlike backends/capabilities/vram), so every drain-window tick overwrites the worker's real load metric with 0.0, corrupting operator/routing views for the duration of the drain.
Prefer reading drain_complete from the response of the single heartbeat that is already being sent (e.g. have heartbeat() return the parsed body or expose the last response) instead of issuing this duplicate POST. If a separate request is unavoidable, omit load so the cached value is preserved.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
…ranch -f - Replace redundant dual-POST heartbeat pattern in _wait_for_drain with a single POST that sends real CPU load (via psutil) and reads drain_complete from the response body. Eliminates heartbeat traffic doubling and load=0.0 clobber (Kilo WARNING). - Check git branch -f return code in pull_update and fail the update early when the fast-forward fails instead of silently masking the error (Kilo SUGGESTION). - Update test_pull_success mock to handle the branch git subcommand. Refs: jaylfc#1910
…ranch -f - Replace redundant dual-POST heartbeat pattern in _wait_for_drain with a single POST that sends real CPU load (via psutil) and reads drain_complete from the response body. Eliminates heartbeat traffic doubling and load=0.0 clobber (Kilo WARNING). - Check git branch -f return code in pull_update and fail the update early when the fast-forward fails instead of silently masking the error (Kilo SUGGESTION). - Update test_pull_success mock to handle the branch git subcommand. Refs: jaylfc#1910
b2433d3 to
1ba39cf
Compare
| # Check for pending self-update trigger on each heartbeat | ||
| # cycle. The check is cheap (a file stat) and the trigger | ||
| # file is removed atomically before the update runs. | ||
| await self._check_update_trigger() |
There was a problem hiding this comment.
WARNING: The full self-update lifecycle runs inline in the single heartbeat loop, blocking heartbeat sends during long phases.
_check_update_trigger() awaits run_full_update(), whose phases run sequentially in this while self._running loop. The dependency-install phase (update_dependencies) can take well over the controller's HEARTBEAT_TIMEOUT = 30s (see tinyagentos/cluster/manager.py:15), and the drain wait is up to 120s. While these phases await subprocesses, the loop cannot send the next heartbeat, so the controller marks the worker offline mid-update (manager.py:767). A worker marked offline may be de-registered, and the post-restart hook depends on successful re-registration — so an eviction can break the whole self-update/rollback flow.
Consider moving the update into a detached asyncio.create_task(...) (fire-and-forget) instead of awaiting it inside the heartbeat loop, and/or continuing to send periodic heartbeats (status updating) while the update runs so the controller never sees a 30s gap.
Note also: the trigger is only checked after a successful heartbeat while _registered is True (line 886 is reached before the status == 404 -> _registered = False branch at line 887+). If the controller forgot the worker (404), the trigger will not fire until re-registration succeeds.
| worker = cluster._workers.get(body.name) | ||
| drain_complete = False | ||
| if worker is not None and worker.status == "draining" and hasattr(cluster, "_leases"): | ||
| active = [ |
There was a problem hiding this comment.
SUGGESTION: drain_complete counts every lease in cluster._leases, including expired ones not yet swept by _sweep_expired_leases.
The active-lease filter at lines 542-547 iterates cluster._leases.items() directly, but elsewhere the codebase treats expired leases as gone — e.g. get_leases() (manager.py:529-532) and find_existing_lease() (manager.py:432-438) both filter on lease.expires_at > now. If a lease has expired but has not yet been swept by the monitor loop, it is still counted here, so drain_complete stays False and the worker waits the full 120s _wait_for_drain timeout instead of proceeding early — defeating the purpose of the drain_complete optimization.
Filter by expiry to match the rest of the code:
| active = [ | |
| if worker is not None and worker.status == "draining" and hasattr(cluster, "_leases"): | |
| now = time.time() | |
| active = [ | |
| lid for lid, lease in cluster._leases.items() | |
| if getattr(lease, "expires_at", 0) > now | |
| and (parsed := cluster._parse_resource_id(lease.resource_id)) | |
| and parsed[0] == body.name | |
| ] | |
| drain_complete = len(active) == 0 |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
scripts/taos-deploy-helper.sh (1)
348-348: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueBrace the extras suffix to avoid array-subscript ambiguity (SC1087).
"$repo_dir[worker]"parses correctly in Bash today (variable expansion stops at[), but shellcheck flags it because[worker]reads like an array subscript. Use${repo_dir}to make the pip extras path unambiguous.♻️ Proposed tweak
- "$venv/bin/pip" install -q -e "$repo_dir[worker]" 2>/dev/null || \ + "$venv/bin/pip" install -q -e "${repo_dir}[worker]" 2>/dev/null || \🤖 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 `@scripts/taos-deploy-helper.sh` at line 348, Update the pip install command around "$venv/bin/pip" to brace the repo_dir variable before appending the [worker] extras suffix, making the editable-install path unambiguous without changing its behavior.Source: Linters/SAST 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 `@tinyagentos/routes/cluster.py`:
- Around line 1491-1500: Update the handler containing require_worker_hmac and
the cluster.get_worker(name) lookup to compare request.state.hmac_worker_name
with the requested worker name before performing any lookup or outcome action.
Return the same 403 response used by worker_heartbeat for mismatched identities,
while preserving the existing HMAC failure and valid-worker behavior.
In `@tinyagentos/worker/self_update.py`:
- Around line 533-551: Reset the worker lifecycle status before returning from
both failure paths in the self-update flow: the `if not pull["ok"]` branch after
`pull_update(target_ref)` and the `if not deps["ok"]` branch after
`update_dependencies()`. Clear `_lifecycle_status` or send an `online` heartbeat
before `notify_drain_complete()` so the worker is routable again after aborting.
---
Nitpick comments:
In `@scripts/taos-deploy-helper.sh`:
- Line 348: Update the pip install command around "$venv/bin/pip" to brace the
repo_dir variable before appending the [worker] extras suffix, making the
editable-install path unambiguous without changing its 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e71ddb3c-73fa-415a-bd54-b0f49c73ecfa
📒 Files selected for processing (7)
scripts/taos-deploy-helper.shtests/test_cluster.pytests/test_worker_self_update.pytinyagentos/routes/cluster.pytinyagentos/worker/agent.pytinyagentos/worker/deploy.pytinyagentos/worker/self_update.py
1ba39cf to
a089e02
Compare
…fc#1910) - agent.py: move self-update to detached asyncio.create_task so the heartbeat loop is never blocked during long update phases; add _update_in_progress guard to prevent duplicate launches (Kilo WARNING: heartbeat-loop blocking) - cluster.py (routes): filter drain_complete by lease expiry to match get_leases()/find_existing_lease() behavior, avoiding the full 120s timeout when expired leases haven't been swept yet (Kilo SUGGESTION: expired leases) - cluster.py (routes): add HMAC worker-name cross-check in report_update_outcome so a valid worker can't spoof another worker's outcome; matches existing pattern in worker_heartbeat (CodeRabbit Major: cross-worker spoofing guard) - self_update.py: reset _lifecycle_status to None and send a regular heartbeat instead of notify_drain_complete() in all three abort paths (pull failure, deps failure, restart failure) so the worker is routable again instead of stuck in 'updating' (CodeRabbit Major: lifecycle reset on abort) - deploy-helper.sh: brace $repo_dir variable to avoid SC1087 ambiguity (CodeRabbit Nitpick: shell variable bracing) Tests: 68/68 pass (test_worker_self_update, test_cluster, test_register_all_routers)
a089e02 to
e6a751e
Compare
| logger.error("self-update: pull failed — aborting update") | ||
| # Don't rollback yet — we haven't installed anything. | ||
| # Reset lifecycle so the worker is routable again instead of | ||
| # stuck in "updating" (CodeRabbit: cross-worker spoofing guard). |
There was a problem hiding this comment.
SUGGESTION: Misleading copy-paste comment.
This block resets the worker's lifecycle so it is routable again after an abort (the change referenced in the commit body as "lifecycle reset on abort"). The trailing (CodeRabbit: cross-worker spoofing guard) note is wrong here — the cross-worker spoofing guard is the HMAC name cross-check added in routes/cluster.py, unrelated to this abort path. Fix the comment so future maintainers are not misled about what this code does.
| # stuck in "updating" (CodeRabbit: cross-worker spoofing guard). | |
| # stuck in "updating" (CodeRabbit: lifecycle reset on abort). |
|
@hognek Rebase needed - this has conflicts against current dev (GitHub reports Everything else on it looks fine from my side; it is purely staleness. Rebase onto current dev and I will take another look. |
e6a751e to
90326d7
Compare
|
Flagging this from the periodic repo sweep: this PR is red on both required checks, and it is a real failure rather than infra.
Every one gets 403 where the test expects the endpoint to have been reached at all, including the two negative cases that expect 404 and 400. So the update-outcome endpoint is refusing the request before any of its own logic runs. That reads as an auth or permission gate applying to a route the tests exercise unauthenticated, rather than four separate bugs. Worth checking whether the tests need to authenticate now or the gate is wider than intended. The Not touching it since it is yours. |
|
Fixed the 4 failing Root cause: The tests patched Fix: Each test's All 42 cluster tests pass (including the 4 fixed). |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
tinyagentos/worker/agent.py (2)
733-743: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAborted update leaves the worker permanently
draining.
notify_drain_complete()sends one status-less heartbeat, butself._lifecycle_status/_lifecycle_reasonare still"draining"frominitiate_self_drain(). The run loop at Line 892 re-sendsstatus=self._lifecycle_statuson every subsequent tick, so after an aborted update (pull failed/dependency update failedinself_update.run_full_update) the controller keeps the worker out of routing forever. Clear the fields here.🔧 Proposed fix
logger.info("worker '%s': drain complete, ready for update", self.name) + self._lifecycle_status = None + self._lifecycle_reason = None return await self.heartbeat()🤖 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 `@tinyagentos/worker/agent.py` around lines 733 - 743, Clear the worker’s lifecycle state in notify_drain_complete() before sending the final heartbeat, resetting _lifecycle_status and _lifecycle_reason from the draining state so subsequent run-loop heartbeats no longer advertise the worker as draining after an aborted update.
117-119: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
WorkerUpdateServiceis undefined for the annotation (Ruff F821).The quoted annotation is runtime-safe, but Ruff flags the unresolved name and CI lint will fail. Add a
TYPE_CHECKINGimport.🔧 Proposed fix
+ # (add near the top of the module) + # if TYPE_CHECKING: + # from tinyagentos.worker.update_check import WorkerUpdateService self._update_service: "WorkerUpdateService | None" = None🤖 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 `@tinyagentos/worker/agent.py` around lines 117 - 119, Add a TYPE_CHECKING-only import for WorkerUpdateService so the annotation on Worker._update_service resolves for Ruff without introducing a runtime import; leave the existing lazy initialization and run() startup behavior unchanged.Source: Linters/SAST tools
🧹 Nitpick comments (3)
tests/test_worker_self_update.py (2)
438-439: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMock signature drifts from
run_health_check().
run_health_check()takes no parameters (self_update.pyLine 353), but these mocks declaretimeout=30. It passes today only because the default absorbs the missing argument, which means the tests won't catch future signature drift. Drop the parameter.♻️ Proposed tweak
- async def mock_health_check(timeout=30): + async def mock_health_check(): return {"ok": True, "output": "healthy", "exit_code": 0}Also applies to: 474-475
🤖 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 `@tests/test_worker_self_update.py` around lines 438 - 439, Update the mock_health_check definitions in both referenced test locations to accept no parameters, matching the run_health_check signature while preserving their existing healthy response.
266-400: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNo coverage for
_wait_for_drain(graceful=True) orsignal_update_outcome.Both tests use
graceful=Falseor abort before drain, so the drain-polling loop and the HMAC-signed outcome POST — the two most failure-prone paths in this module — are untested. Worth adding a case that drives_wait_for_drainwith a mocked controller returningdrain_complete: true.🤖 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 `@tests/test_worker_self_update.py` around lines 266 - 400, The TestRunFullUpdate coverage should exercise the graceful drain path and signed update outcome reporting. Add a success test that calls run_full_update with graceful=True, mocks the controller interactions used by _wait_for_drain to return drain_complete=true, and verifies signal_update_outcome is invoked with the successful result while preserving the existing phase assertions.tinyagentos/worker/agent.py (1)
930-933: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
stop()reaches intoWorkerUpdateService._stop_event.Private-attribute access across a module boundary; the service already owns an async
stop(). Consider exposing a synchronousrequest_stop()onWorkerUpdateServiceand calling that.🤖 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 `@tinyagentos/worker/agent.py` around lines 930 - 933, Update Worker.stop to avoid directly accessing WorkerUpdateService._stop_event across the module boundary. Add a synchronous request_stop() method to WorkerUpdateService that signals its existing stop event, then call self._update_service.request_stop() from stop() while preserving the current None guard.
🤖 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 `@scripts/taos-deploy-helper.sh`:
- Around line 320-330: Update the systemd-run dispatch branches in the worker
restart helper to use the default transient service mode instead of --scope,
while retaining asynchronous --no-block behavior. Ensure the dispatched
systemctl restart is independent of the worker service cgroup so it can outlive
the cgroup teardown, preserving the existing service fallback order and success
logging.
- Around line 272-281: Bound the suffix retry loop in the tag creation logic
within cmd_checkpoint so it makes only a finite number of attempts. Preserve the
existing counter-suffix behavior for tag collisions, but exit with a clear
failure when repeated git tag operations fail for other reasons instead of
allowing indefinite retries.
In `@tinyagentos/worker/agent.py`:
- Around line 851-873: The heartbeat loop must continue while the post-restart
hook and full update run. Update the startup hook around post_update_startup and
the _check_update_trigger flow so these long-running operations execute as
detached/background tasks, or otherwise emit an updating heartbeat throughout
their await periods, while preserving outcome logging and error handling.
In `@tinyagentos/worker/deploy.py`:
- Around line 33-37: Update the deploy helper contract between
rollback_to_checkpoint() and run_deploy() so checkpoint_tag values are handled
consistently: either allow and parse the tagged “rollback <tag>” form in
run_deploy() for controller-invoked rollbacks, or keep the allowlist restricted
to bare “rollback” and route explicit manual tags through scripts/rollback.sh.
Preserve bare self-update rollback behavior and ensure unsupported command forms
remain rejected.
In `@tinyagentos/worker/self_update.py`:
- Around line 243-255: The plain-branch update path must prevent git option
injection from target_ref. In tinyagentos/worker/self_update.py lines 243-255,
add an argument terminator before target_ref in git fetch and terminate the git
checkout arguments; in tinyagentos/worker/agent.py lines 768-793, validate
target_ref against a conservative ref pattern that rejects leading hyphens
before run_full_update, and delete the update trigger when validation fails.
- Around line 617-644: Update the self-update drain polling flow around
agent.heartbeat() to reuse the existing heartbeat response/body and read its
parsed drain_complete value, removing the second HTTP POST and duplicate request
construction. Ensure the heartbeat payload continues reporting the worker’s real
load rather than overwriting it with 0.0, while preserving the existing
successful drain-confirmation behavior.
- Around line 262-272: Consolidate the target_ref handling in the update flow
around _run_git: derive the branch name by removing the remote prefix when
target_ref contains "/", otherwise use target_ref directly, then perform one
branch -f operation. Capture and validate its return result, propagating failure
so pull_update does not return ok: True while HEAD remains detached.
---
Outside diff comments:
In `@tinyagentos/worker/agent.py`:
- Around line 733-743: Clear the worker’s lifecycle state in
notify_drain_complete() before sending the final heartbeat, resetting
_lifecycle_status and _lifecycle_reason from the draining state so subsequent
run-loop heartbeats no longer advertise the worker as draining after an aborted
update.
- Around line 117-119: Add a TYPE_CHECKING-only import for WorkerUpdateService
so the annotation on Worker._update_service resolves for Ruff without
introducing a runtime import; leave the existing lazy initialization and run()
startup behavior unchanged.
---
Nitpick comments:
In `@tests/test_worker_self_update.py`:
- Around line 438-439: Update the mock_health_check definitions in both
referenced test locations to accept no parameters, matching the run_health_check
signature while preserving their existing healthy response.
- Around line 266-400: The TestRunFullUpdate coverage should exercise the
graceful drain path and signed update outcome reporting. Add a success test that
calls run_full_update with graceful=True, mocks the controller interactions used
by _wait_for_drain to return drain_complete=true, and verifies
signal_update_outcome is invoked with the successful result while preserving the
existing phase assertions.
In `@tinyagentos/worker/agent.py`:
- Around line 930-933: Update Worker.stop to avoid directly accessing
WorkerUpdateService._stop_event across the module boundary. Add a synchronous
request_stop() method to WorkerUpdateService that signals its existing stop
event, then call self._update_service.request_stop() from stop() while
preserving the current None guard.
🪄 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: 010ad996-20ef-4d81-9c30-c5d244ab0bfd
📒 Files selected for processing (7)
scripts/taos-deploy-helper.shtests/test_cluster.pytests/test_worker_self_update.pytinyagentos/routes/cluster.pytinyagentos/worker/agent.pytinyagentos/worker/deploy.pytinyagentos/worker/self_update.py
🚧 Files skipped from review as they are similar to previous changes (2)
- tests/test_cluster.py
- tinyagentos/routes/cluster.py
| # Tag the current HEAD so it survives a checkout (detached or branch) | ||
| local tag="taos-worker-pre-update-$(date -u +%Y%m%d-%H%M%S)" | ||
| git -C "$repo_dir" tag "$tag" HEAD 2>/dev/null || { | ||
| # Tag already exists — add a counter suffix | ||
| local suffix=1 | ||
| while ! git -C "$repo_dir" tag "${tag}-${suffix}" HEAD 2>/dev/null; do | ||
| ((suffix++)) | ||
| done | ||
| tag="${tag}-${suffix}" | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Unbounded retry loop can hang forever.
If git tag keeps failing for a reason other than "tag already exists" (disk full, index.lock present, permissions), the while loop spins indefinitely incrementing suffix, hanging cmd_checkpoint — and therefore the entire self-update flow that depends on it completing.
🔒 Bound the retry loop
git -C "$repo_dir" tag "$tag" HEAD 2>/dev/null || {
# Tag already exists — add a counter suffix
local suffix=1
- while ! git -C "$repo_dir" tag "${tag}-${suffix}" HEAD 2>/dev/null; do
+ while ! git -C "$repo_dir" tag "${tag}-${suffix}" HEAD 2>/dev/null; do
+ if (( suffix >= 20 )); then
+ die "failed to create checkpoint tag after ${suffix} attempts"
+ fi
((suffix++))
done
tag="${tag}-${suffix}"
}📝 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.
| # Tag the current HEAD so it survives a checkout (detached or branch) | |
| local tag="taos-worker-pre-update-$(date -u +%Y%m%d-%H%M%S)" | |
| git -C "$repo_dir" tag "$tag" HEAD 2>/dev/null || { | |
| # Tag already exists — add a counter suffix | |
| local suffix=1 | |
| while ! git -C "$repo_dir" tag "${tag}-${suffix}" HEAD 2>/dev/null; do | |
| ((suffix++)) | |
| done | |
| tag="${tag}-${suffix}" | |
| } | |
| # Tag the current HEAD so it survives a checkout (detached or branch) | |
| local tag="taos-worker-pre-update-$(date -u +%Y%m%d-%H%M%S)" | |
| git -C "$repo_dir" tag "$tag" HEAD 2>/dev/null || { | |
| # Tag already exists — add a counter suffix | |
| local suffix=1 | |
| while ! git -C "$repo_dir" tag "${tag}-${suffix}" HEAD 2>/dev/null; do | |
| if (( suffix >= 20 )); then | |
| die "failed to create checkpoint tag after ${suffix} attempts" | |
| fi | |
| ((suffix++)) | |
| done | |
| tag="${tag}-${suffix}" | |
| } |
🧰 Tools
🪛 Shellcheck (0.11.0)
[warning] 273-273: Declare and assign separately to avoid masking return values.
(SC2155)
🤖 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 `@scripts/taos-deploy-helper.sh` around lines 272 - 281, Bound the suffix retry
loop in the tag creation logic within cmd_checkpoint so it makes only a finite
number of attempts. Preserve the existing counter-suffix behavior for tag
collisions, but exit with a clear failure when repeated git tag operations fail
for other reasons instead of allowing indefinite retries.
| # 2 — systemd-run --scope: runs outside the service cgroup. | ||
| if command -v systemd-run >/dev/null 2>&1; then | ||
| if systemd-run --scope --no-block systemctl restart tinyagentos-worker.service 2>/dev/null; then | ||
| log "worker restart dispatched via systemd-run" | ||
| return 0 | ||
| fi | ||
| if systemd-run --scope --no-block systemctl restart taos-worker.service 2>/dev/null; then | ||
| log "worker restart dispatched via systemd-run (taos-worker)" | ||
| return 0 | ||
| fi | ||
| fi |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Does systemd-run --scope --no-block actually run in the background, or does --scope force synchronous execution regardless of --no-block?
💡 Result:
The short answer is yes, when you use systemd-run --scope, the command execution is inherently synchronous, meaning it will block the caller and return only when the command finishes, regardless of whether you include --no-block [1][2][3]. In the context of systemd-run, the --scope option fundamentally changes how the command is executed compared to the default transient service unit [1][2]. 1. Understanding --scope: When you run a command with --scope, the systemd-run process itself acts as the parent of the command [1][3]. The command inherits the execution environment of the caller and runs synchronously; systemd-run waits for the command to terminate [1][2][4]. This is different from the default service mode, where systemd acts as the parent and manages the process asynchronously in the background [1][2]. 2. The role of --no-block: The --no-block flag is designed to control whether systemd-run waits for the unit's start-up operation to be enqueued and completed [1][2]. While it is effective for default service units, it does not transform the synchronous nature of a scope unit into a background process [1][2]. 3. Why it behaves this way: The developers have noted that --scope is intended as a direct wrapper for the command execution within a transient control group [5]. Because the caller remains the parent process in this mode, it naturally remains attached to the lifecycle of the command [1][3]. If you need a process to run in the background while using --scope, you must handle the backgrounding yourself, for instance, by appending an ampersand (&) to your command string (e.g., systemd-run --scope my-command &) to place the entire systemd-run process in the background [5]. Top results for further reading: [1] https://man.archlinux.org/man/systemd-run.1.en [2] https://freedesktop.org/software/systemd/man/latest/systemd-run.html [5] https://lists.freedesktop.org/archives/systemd-devel/2018-February/040351.html [3] https://manpages.debian.org/trixie/systemd/systemd-run.1.en.html
Citations:
- 1: https://man.archlinux.org/man/systemd-run.1.en
- 2: https://freedesktop.org/software/systemd/man/latest/systemd-run.html
- 3: https://manpages.debian.org/trixie/systemd/systemd-run.1.en.html
- 4: https://docs.redhat.com/fr/documentation/red_hat_enterprise_linux/7/html/resource_management_guide/chap-using_control_groups
- 5: https://lists.freedesktop.org/archives/systemd-devel/2018-February/040351.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file and relevant section =="
if [ -f scripts/taos-deploy-helper.sh ]; then
nl -ba scripts/taos-deploy-helper.sh | sed -n '280,355p'
else
echo "scripts/taos-deploy-helper.sh not found"
fi
echo
echo "== check systemd version availability if installed =="
if command -v systemd-run >/dev/null 2>&1; then
systemd-run --version | head -n 8 || true
else
echo "systemd-run not available in sandbox"
fi
echo
echo "== search for related docs/comments in repo =="
rg -n "systemd-run|--scope|--no-block|taos-worker|tinyagentos-worker" . --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' || trueRepository: jaylfc/taOS
Length of output: 228
🌐 Web query:
Red Hat systemd-run --scope synchronous no-block does not run in background systemd man page
💡 Result:
When you use systemd-run with the --scope option, the command is executed synchronously by design [1][2][3]. In this mode, systemd-run acts as the parent process and waits for the command to finish before it returns [1][4][3]. Because scope units are inherently tied to the lifecycle of the process that created them, they are not designed to be "backgrounded" in the way that transient service units (created without --scope) can be [1][4]. The --no-block option, which is intended to prevent systemd-run from waiting for the unit's initial start-up confirmation, does not override the fundamental synchronous nature of --scope [1][5][3]. While you may attempt to pass both, the manual page indicates that the synchronous execution behavior of --scope is a core aspect of how it manages the command as a child process of the caller [1][2][3]. If your goal is to run a command in the background, you should use the default transient service unit mode (by omitting --scope), which starts the service asynchronously by default [1][6][7].
Citations:
- 1: https://freedesktop.org/software/systemd/man/latest/systemd-run.html
- 2: https://manpages.debian.org/trixie/systemd/systemd-run.1.en.html
- 3: https://man.archlinux.org/man/systemd-run.1
- 4: https://deepwiki.com/systemd/systemd/9.2-systemd-run-and-transient-units
- 5: https://man7.org/linux/man-pages/man1/systemd-run.1.html
- 6: https://www.freedesktop.org/software/systemd/man/systemd-run
- 7: https://manpages.ubuntu.com/manpages/jammy/man1/systemd-run.1.html
Avoid relying on systemd-run --scope --no-block to stay prompt.
With --scope, the command is synchronous and --no-block does not make it backgrounded, so this dispatch path can still block the deploy script through the whole systemctl restart. Use the default transient service mode for async dispatch and ensure the restart can outlive the worker cgroup teardown.
🤖 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 `@scripts/taos-deploy-helper.sh` around lines 320 - 330, Update the systemd-run
dispatch branches in the worker restart helper to use the default transient
service mode instead of --scope, while retaining asynchronous --no-block
behavior. Ensure the dispatched systemctl restart is independent of the worker
service cgroup so it can outlive the cgroup teardown, preserving the existing
service fallback order and success logging.
| # Worker self-update subcommands (taOS #890 C3). | ||
| "checkpoint", | ||
| "rollback", | ||
| "restart-self", | ||
| "health-check", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check whether any caller passes a rollback tag through run_deploy().
rg -n 'run_deploy\(' -A2 tinyagentos | rg -n 'rollback'Repository: jaylfc/taOS
Length of output: 149
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate deploy.py and related helper files =="
fd -a 'deploy\.py$|self_update\.py$|.*deploy.*\.(py|sh)$' tinyagentos 2>/dev/null | sed 's#^\./##'
echo
echo "== tinyagentos/worker/deploy.py relevant lines =="
wc -l tinyagentos/worker/deploy.py
cat -n tinyagentos/worker/deploy.py | sed -n '1,120p'
echo
echo "== run_deploy call sites =="
rg -n 'run_deploy\(' tinyagentos -C 2 || true
echo
echo "== cmd_rollback definitions/usages =="
rg -n 'cmd_rollback|rollback|ALLOWED_COMMANDS' tinyagentos -C 2Repository: jaylfc/taOS
Length of output: 26964
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== helper rollback implementation =="
fd -a 'taos-deploy-helper|deploy.*helper|deploy-helper|deploy\.sh' /usr/local/bin /usr/local /tmp /home 2>/dev/null | sed 's#^\./##' || true
fd -a 'deploy-helper|deploy.*helper|deploy\.sh'| sed 's#^\./##' | head -50 || true
echo
echo "== local deployment scripts likely containing rollback argument parsing =="
for f in $(fd -a '.*deploy.*' tinyagentos tinyagentos | sed 's#^\./##'); do
if rg -q 'rollback|UPDATE_IN_PROGRESS|checkpoint|restart-self' "$f" 2>/dev/null; then
echo "--- $f"
wc -l "$f"
sed -n '1,240p' "$f"
fi
done
echo
echo "== read-only semantic probe of run_deploy allowlist and split behavior =="
python3 - <<'PY'
ALLOWED_COMMANDS = {
"install-ollama", "install-exo", "install-llama-cpp",
"install-llama-cpp --cuda", "install-vllm", "install-rknpu",
"update-worker", "status", "checkpoint", "rollback",
"restart-self", "health-check",
}
commands = ["rollback", "rollback abc", "rollback abc def", "install-llama-cpp --cuda"]
for command in commands:
print(command, "-- accepted:", command in ALLOWED_COMMANDS, "args:", command.split())
PYRepository: jaylfc/taOS
Length of output: 7645
Move rollback allowance and tag argument handling behind the deploy helper contract.
rollback_to_checkpoint() can pass checkpoint_tag, but run_deploy() only allows the exact string "rollback" and will reject "rollback <tag>". If run_deploy() is intended for controller-invoked rollbacks with a specific tag, add the tag argument path into the helper path; otherwise ensure the allowlist is stable for bare self-update rollbacks and explicit manual tags use the dedicated scripts/rollback.sh.
🤖 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 `@tinyagentos/worker/deploy.py` around lines 33 - 37, Update the deploy helper
contract between rollback_to_checkpoint() and run_deploy() so checkpoint_tag
values are handled consistently: either allow and parse the tagged “rollback
<tag>” form in run_deploy() for controller-invoked rollbacks, or keep the
allowlist restricted to bare “rollback” and route explicit manual tags through
scripts/rollback.sh. Preserve bare self-update rollback behavior and ensure
unsupported command forms remain rejected.
| # Check the heartbeat response body for drain_complete. | ||
| # We re-post a lightweight request so we can read the response | ||
| # payload (heartbeat() only returns the status code). | ||
| try: | ||
| path = "/api/cluster/heartbeat" | ||
| payload = _json.dumps({ | ||
| "name": name, | ||
| "load": 0.0, | ||
| "status": "draining", | ||
| "drain_reason": "update", | ||
| }).encode() | ||
| headers = sign_request_headers(key, name, "POST", path, payload) if key else {} | ||
| headers["content-type"] = "application/json" | ||
|
|
||
| async with httpx.AsyncClient(timeout=5) as client: | ||
| resp = await client.post( | ||
| f"{controller.rstrip('/')}{path}", | ||
| content=payload, | ||
| headers=headers, | ||
| ) | ||
| if resp.status_code == 200: | ||
| body = resp.json() | ||
| if body.get("drain_complete"): | ||
| logger.info( | ||
| "self-update: drain confirmed complete by controller " | ||
| "(%.0fs elapsed)", elapsed, | ||
| ) | ||
| return True |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Second heartbeat POST per tick, and it zeroes the reported load.
agent.heartbeat() at Line 603 already sent a full heartbeat whose body carries drain_complete; this re-post doubles controller traffic every 5s and hardcodes "load": 0.0, which ClusterManager.heartbeat() assigns unconditionally — overwriting the worker's real load for the whole drain window. Have heartbeat() surface the parsed body (or the last response) and read drain_complete from it; at minimum omit load here.
🤖 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 `@tinyagentos/worker/self_update.py` around lines 617 - 644, Update the
self-update drain polling flow around agent.heartbeat() to reuse the existing
heartbeat response/body and read its parsed drain_complete value, removing the
second HTTP POST and duplicate request construction. Ensure the heartbeat
payload continues reporting the worker’s real load rather than overwriting it
with 0.0, while preserving the existing successful drain-confirmation behavior.
|
Confirming your fix resolved it, and that my earlier read was right rather than a flake: Not merging it on the sweep, and the reason is scope rather than anything wrong with it. This is +1815/-7 across 7 files implementing worker self-update, restart, health-check and rollback. No bot has commented on the current head, so there is no review covering this tree, and green CI is not a review. Self-update and rollback logic is close enough to "can take a worker down" that I want it read properly rather than merged by a 4-hourly automated pass. So it is blocked on review, not on CI. Nothing for you to do unless you want to flag anything specific you would like looked at first. |
43886e6 to
f967846
Compare
…ploy_backend 1. Worker-name authorization: deploy_backend now calls require_worker_hmac and verifies the authenticated worker matches the path name. Previously the endpoint had no auth at all - any caller could trigger deploys on any worker. Now only the paired worker can deploy on itself (CodeRabbit, PR jaylfc#1910). 2. Concurrency guard: added per-worker asyncio.Lock (_worker_deploy_locks) to both deploy_backend and _do_single_worker_update. Prevents concurrent API calls from double-installing or double-draining the same worker.
| "output": output.strip(), | ||
| "exit_code": proc.returncode or 0, | ||
| } | ||
| except asyncio.TimeoutError: |
There was a problem hiding this comment.
WARNING: _run_helper leaks the child process on timeout
asyncio.wait_for cancels the communicate() wait on timeout but does not terminate the child. Unlike _run_git (line 132) and update_dependencies (line 306), this handler returns without proc.kill(), leaving an orphaned sudo/taos-deploy-helper process running after the timeout.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
618ebc6 to
34b0f08
Compare
…ploy_backend 1. Worker-name authorization: deploy_backend now calls require_worker_hmac and verifies the authenticated worker matches the path name. Previously the endpoint had no auth at all - any caller could trigger deploys on any worker. Now only the paired worker can deploy on itself (CodeRabbit, PR jaylfc#1910). 2. Concurrency guard: added per-worker asyncio.Lock (_worker_deploy_locks) to both deploy_backend and _do_single_worker_update. Prevents concurrent API calls from double-installing or double-draining the same worker.
BLOCKER 1 — restart bricks worker: - Add _restart_service_by_name() helper that tries systemctl --user scope (via su -l $SUDO_USER) when system-level restart fails. - at(1) script now includes --user fallback commands. - systemd-run --scope tries --user scope when system scope fails. - Verify atd daemon is active before trusting at(1) (CodeRabbit). BLOCKER 2 — unstack from jaylfc#1903: - Rebase onto clean origin/dev (ad7e4fe). CodeRabbit findings (Jul 31): - Subprocess timeouts: catch asyncio.TimeoutError in _run_git and update_dependencies, kill the process, return structured error. - HMAC error: return exc.response instead of flat 403 in report_update_outcome, matching worker_heartbeat pattern. - post_update_startup: schedule via asyncio.ensure_future in background task instead of awaiting inline, so the heartbeat loop is never blocked by the grace period + health-check delay.
|
Rebased onto current origin/dev (7 commits, clean — no conflicts) and pushed as
Changelog fragment Remaining gate is the hardware proof you asked for: a real update-and-rollback on a systemd worker (successful self-update + a deliberately-failed rollback, with logs showing which |
…ploy_backend 1. Worker-name authorization: deploy_backend now calls require_worker_hmac and verifies the authenticated worker matches the path name. Previously the endpoint had no auth at all - any caller could trigger deploys on any worker. Now only the paired worker can deploy on itself (CodeRabbit, PR jaylfc#1910). 2. Concurrency guard: added per-worker asyncio.Lock (_worker_deploy_locks) to both deploy_backend and _do_single_worker_update. Prevents concurrent API calls from double-installing or double-draining the same worker.
BLOCKER 1 — restart bricks worker: - Add _restart_service_by_name() helper that tries systemctl --user scope (via su -l $SUDO_USER) when system-level restart fails. - at(1) script now includes --user fallback commands. - systemd-run --scope tries --user scope when system scope fails. - Verify atd daemon is active before trusting at(1) (CodeRabbit). BLOCKER 2 — unstack from jaylfc#1903: - Rebase onto clean origin/dev (ad7e4fe). CodeRabbit findings (Jul 31): - Subprocess timeouts: catch asyncio.TimeoutError in _run_git and update_dependencies, kill the process, return structured error. - HMAC error: return exc.response instead of flat 403 in report_update_outcome, matching worker_heartbeat pattern. - post_update_startup: schedule via asyncio.ensure_future in background task instead of awaiting inline, so the heartbeat loop is never blocked by the grace period + health-check delay.
34b0f08 to
6f605ff
Compare
|
Rebased onto current
The current |
|
Rebase verified — merge-base equals current dev HEAD, nothing replayed, all checks green. Three things before this merges:
Worth batching into the same push: trigger unlinked before the durable marker (agent.py:814), untracked |
|
@hognek — see #2070 (comment) for the full note covering all four of your open PRs. This is the one that will eventually need something from you: it has gone conflicting against dev while it waited, so it cannot merge as-is. A rebase should be all it needs. It has not been rejected or superseded and the work is still wanted. No rush, and deliberately so. #2070, #2048 and #2043 are green and in review now, and I would rather get those moving than ask you to do rebase work while three finished PRs of yours are still sitting. Whenever you get to it is fine. |
Rebase onto origin/dev (2026-08-27)Rebased Conflicts resolved1 file: Why: Upstream dev added a Resolution: Merged both fields into one response: return {"status": "ok", "generation": cluster.generation, "drain_complete": drain_complete}The upstream Test results
Remaining 7 commits applied cleanly (no further conflicts)Commit history preserved in original order; only the first commit (c30d59e) needed conflict resolution. |
…orchestrator (jaylfc#890 C3) BLOCKER 1: _detached_restart_worker() uses at(1) → systemd-run --scope → --no-block fallback chain so the restart survives systemd KillMode=control-group teardown. Previously the deploy-helper (child of the worker's cgroup) was SIGTERM'd mid-restart, permanently bricking the worker. BLOCKER 2: Wire _check_update_trigger() into the agent heartbeat loop (reads update-trigger.json), and post_update_startup() into the post-registration path (reads update-in-progress.json marker). The orchestrator now has real callers. Also: drain_complete in heartbeat response, update-outcome HMAC endpoint, C3 deploy-helper subcommands (checkpoint/rollback/ restart-self/health-check), and deploy.py allowlist update. Tests: 42 pass (22 self_update + 20 cluster unit). App-fixture endpoint tests deferred to CI (pre-existing conftest hang).
The HMAC gate only proves the caller is a paired worker, not which worker. Without this check, worker A could spoof an outcome report for worker B. Now matches the pattern used by incus-enroll and heartbeat endpoints. QA-Impacted: TestUpdateOutcomeEndpoint tests may need hmac_worker_name set on request.state — CI will catch.
The route-level name cross-check (report_update_outcome line 1500)
requires request.state.hmac_worker_name to match the path {name}
parameter. The previous side_effect=lambda r: None left it unset,
causing all 4 TestUpdateOutcomeEndpoint tests to return 403 instead
of their expected status codes (200/404/400).
…ploy_backend 1. Worker-name authorization: deploy_backend now calls require_worker_hmac and verifies the authenticated worker matches the path name. Previously the endpoint had no auth at all - any caller could trigger deploys on any worker. Now only the paired worker can deploy on itself (CodeRabbit, PR jaylfc#1910). 2. Concurrency guard: added per-worker asyncio.Lock (_worker_deploy_locks) to both deploy_backend and _do_single_worker_update. Prevents concurrent API calls from double-installing or double-draining the same worker.
BLOCKER 1 — restart bricks worker: - Add _restart_service_by_name() helper that tries systemctl --user scope (via su -l $SUDO_USER) when system-level restart fails. - at(1) script now includes --user fallback commands. - systemd-run --scope tries --user scope when system scope fails. - Verify atd daemon is active before trusting at(1) (CodeRabbit). BLOCKER 2 — unstack from jaylfc#1903: - Rebase onto clean origin/dev (ad7e4fe). CodeRabbit findings (Jul 31): - Subprocess timeouts: catch asyncio.TimeoutError in _run_git and update_dependencies, kill the process, return structured error. - HMAC error: return exc.response instead of flat 403 in report_update_outcome, matching worker_heartbeat pattern. - post_update_startup: schedule via asyncio.ensure_future in background task instead of awaiting inline, so the heartbeat loop is never blocked by the grace period + health-check delay.
Docs-Reviewed: worker self-update adds internal worker subcommands (install/restart/health-check/rollback) and HMAC-authenticated deploy endpoints; no agent-facing API surface or contributor workflow change. Subcommand usage is documented in scripts/taos-deploy-helper.sh --help; worker/README.md covers build/install and the deploy runbook will document the ops subcommands.
6f605ff to
106664b
Compare
|
Rebased onto origin/dev (c575fc2 → 106664b) Conflict: Workflow diff checks: 0 lines vs origin/dev (no deletions); upstream additions (evil-merge-gate.yml, actions v7→v9) came in clean via rebase. All 8 commits replayed cleanly with only that one rerere-resolved conflict. |
Lead review — BLOCKED. Three blockers, two of them proven by running the real callers.Reviewed at The design is sound and the phase decomposition is good. What is wrong is that none of the three new network paths are exercised by the credentials their real callers actually hold, and two of them are provably broken as a result. The PR's own tests pass because they reach the routes with the 🔴 BLOCKER 1 — this PR locks everyone out of
|
…nt injection
BLOCKER 1 — drop the worker HMAC gate on POST /api/cluster/workers/{name}/deploy.
It is a session-gated operator action; the HMAC+name cross-check locked out
both the operator (session, no HMAC) and the worker (HMAC, no session).
BLOCKER 2 — add /update-outcome to the auth middleware session-exempt list
alongside /incus-enroll so the worker's HMAC-only caller (no session cookie)
reaches the route-level HMAC gate instead of dying at AuthMiddleware.
BLOCKER 3 — roll back on a stale update marker in post_update_startup. A
healthy update clears the marker within ~1-2 min; a marker far older than
STALE_UPDATE_MARKER_SECONDS means the new code never booted. Staleness alone
triggers rollback, because the health check can only pass when the worker is
already running.
Security (non-blocking) — validate target_ref in pull_update against
^[A-Za-z0-9._/-]+$ and reject a leading '-'; keep the '--' separator in both
fetch branches to block option/remote-protocol injection.
|
Addressed the lead review (head
Targeted tests green: |
Summary
Implements the worker-side self-install, restart, re-register, and rollback flow for taOS Issue #890.
Builds on C2 (PR #1903, feat/worker-self-drain) drain protocol.
Changes
New module:
tinyagentos/worker/self_update.py(747 lines)post_update_startup()— reads in-progress marker, waits grace period, runs health-check, signals success or triggers rollbackExtended:
scripts/taos-deploy-helper.sh(+237 lines)checkpoint,rollback [<tag>],restart-self,health-check_detached_restart_worker— cgroup-safe restart via at(1) → systemd-run --scope → --no-block fallback chain, survives systemd KillMode=control-group teardownExtended:
tinyagentos/worker/agent.py(+96 lines)_check_update_trigger()— readsupdate-trigger.jsonon each heartbeat cycle, executesrun_full_update()post_update_startup()— called post-registration whenupdate-in-progress.jsonmarker is found, runs health-check and signals success or rollback to controllerExtended:
tinyagentos/worker/deploy.py(+5 lines)ALLOWED_COMMANDSfor security allowlistNew endpoint:
routes/cluster.py(+114 lines)POST /api/cluster/workers/{name}/update-outcome— worker reports success or rollbackworker.update-rollbacknotification on failureTests (26 new, all pass)
tests/test_worker_self_update.py:tests/test_cluster.py:Depends on
Test results
All existing cluster + router tests also pass.
Bot review status (2026-07-18)
Kilo (
kilo-code-bot): ✅ All findings resolved across 5 review cycles.git branch -frc ignored (6b583bc)CodeRabbit: Rate-limited — no review produced.
Gitar: Working (pending).
Qodo: Paused for this user.
Summary by CodeRabbit
New Features
Reliability