Skip to content

fix(swarm): close lifecycle admission on shutdown#776

Open
PierrunoYT wants to merge 1 commit into
Gitlawb:mainfrom
PierrunoYT:fix/issue-771-swarm-shutdown-lifecycle
Open

fix(swarm): close lifecycle admission on shutdown#776
PierrunoYT wants to merge 1 commit into
Gitlawb:mainfrom
PierrunoYT:fix/issue-771-swarm-shutdown-lifecycle

Conversation

@PierrunoYT

@PierrunoYT PierrunoYT commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • close swarm lifecycle admission atomically before cancellation
  • reject spawn, handoff, adoption, retry, and queue-drain work after shutdown begins
  • fail queued tasks and wait for all owned member watchers before Close returns
  • make concurrent swarm and scheduler close calls completion barriers
  • add lifecycle shutdown, retry, queue, and concurrent-close regression coverage

Validation

  • go test ./internal/swarm -count=1
  • go test -race ./internal/swarm -count=1 (WSL/Linux)
  • go vet ./internal/swarm
  • go test ./...
  • go run ./cmd/zero-release build
  • go run ./cmd/zero-release smoke
  • git diff --check

Closes #771

Summary by CodeRabbit

  • New Features

    • Added coordinated swarm shutdown handling.
    • Queued tasks are rejected with a clear shutdown error when the swarm closes.
    • Shutdown now waits for active member watchers and scheduled work to finish.
    • Repeated shutdown requests are handled safely and consistently.
  • Bug Fixes

    • Prevented new members from launching after shutdown begins.
    • Prevented queued members and temporary-member retries from restarting during shutdown.
    • Improved lifecycle handling for adopted and handed-off members.

Copilot AI review requested due to automatic review settings July 20, 2026 15:04

Copilot AI 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.

Pull request overview

This PR addresses issue #771 (AUD-010) by making Swarm.Close a true shutdown barrier: it atomically closes lifecycle admission before cancelation, prevents post-shutdown lifecycle work (including retry and queue-drain paths), and waits for owned watcher goroutines to exit before returning.

Changes:

  • Add swarm-level lifecycle admission gating (closed + lifecycleMu) and watcher ownership tracking (watchers), and update Close to fail queued work and wait for watchers.
  • Make lifecycle entry points (Spawn, Handoff, AdoptOrphans) and retry/queue-drain logic reject work after shutdown begins.
  • Make Scheduler.Close idempotent and concurrency-safe via sync.Once, and add regression tests covering shutdown admission, queue, retry, and concurrent Close.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
internal/swarm/team.go Adds swarm closed state + admission lock + watcher tracking; updates Close to fail queued work and wait for watchers; ensures scheduler access respects shutdown.
internal/swarm/scheduler.go Makes Scheduler.Close a completion barrier via closeOnce and simplifies idempotent close behavior.
internal/swarm/lifecycle.go Gates lifecycle entry points and retry/queue-drain behavior behind lifecycle admission; tracks watcher goroutines in a WaitGroup.
internal/swarm/lifecycle_test.go Adds regression coverage for admission rejection, queued members not launching, retry prevention, and Close waiting for watchers/concurrent callers.

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

Comment on lines +35 to +42
func (s *Swarm) beginLifecycleAdmission() error {
s.lifecycleMu.RLock()
if s.closed {
s.lifecycleMu.RUnlock()
return ErrSwarmClosed
}
return nil
}
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Swarm shutdown now closes lifecycle admission, cancels execution, fails queued tasks, prevents retries and queue draining, and waits for member watchers. Scheduler close operations are synchronized, and lifecycle tests cover admission rejection, queued work, retries, and concurrent shutdown.

Changes

Swarm shutdown lifecycle

Layer / File(s) Summary
Shutdown coordination
internal/swarm/team.go, internal/swarm/scheduler.go
Swarm and scheduler shutdown paths are serialized, queued specs are failed with ErrSwarmClosed, and watcher completion is awaited.
Admission and supervision
internal/swarm/lifecycle.go
Spawn, Handoff, and AdoptOrphans require lifecycle admission; dispatch, exit handling, and retry supervision use admission-aware paths.
Shutdown behavior validation
internal/swarm/lifecycle_test.go
Tests cover post-close rejection, queued-member suppression, retry prevention, and concurrent close synchronization.

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

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Swarm
  participant Scheduler
  participant Watchers
  Caller->>Swarm: Close()
  Swarm->>Swarm: Mark closed and cancel context
  Swarm->>Scheduler: Close()
  Swarm->>Swarm: Fail queued specs
  Swarm->>Watchers: Wait for completion
  Watchers-->>Swarm: Exit
  Swarm-->>Caller: Return
Loading

Possibly related PRs

  • Gitlawb/zero#207: Introduced related multi-agent swarm lifecycle flows refined by this change.

Suggested reviewers: copilot, gnanam1990

🚥 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 is concise and directly describes the main shutdown/lifecycle admission change.
Linked Issues check ✅ Passed The changes implement the linked shutdown requirements: reject post-close admission, stop retries/queue draining, and wait for watcher completion.
Out of Scope Changes check ✅ Passed The scheduler and lifecycle updates appear to support the shutdown-barrier objective and do not introduce unrelated scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
internal/swarm/lifecycle.go (1)

35-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the lock-transfer contract on beginLifecycleAdmission. On success this returns while still holding lifecycleMu.RLock(); on failure it releases and returns ErrSwarmClosed. Every caller silently depends on that asymmetry (defer s.lifecycleMu.RUnlock() only after a nil error). A doc comment stating "returns with the read lock held on success; released on error" prevents a future caller from adding its own RUnlock or forgetting the deferred one.

📝 Suggested doc
+// beginLifecycleAdmission gates a lifecycle entry point against shutdown. On
+// success it returns nil WITH lifecycleMu held for reading; the caller must
+// release it (e.g. via defer). On shutdown it releases the lock and returns
+// ErrSwarmClosed.
 func (s *Swarm) beginLifecycleAdmission() error {
🤖 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 `@internal/swarm/lifecycle.go` around lines 35 - 42, Add a doc comment to
Swarm.beginLifecycleAdmission documenting its lock-transfer contract: successful
calls return with lifecycleMu's read lock held, while the closed/error path
releases the lock before returning ErrSwarmClosed. Keep the existing locking
behavior unchanged.
internal/swarm/team.go (1)

177-191: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The close-after-create handoff is race-free — worth a one-liner so it stays that way. Because a concurrent Close() can only take lifecycleMu.Lock() either fully before or fully after this method's read-lock section, either Close observes the freshly stored s.scheduler and closes it, or this method observes closed == true and closes it itself. No leak, no double-work window. A short comment on why the closed snapshot + post-unlock sched.Close() is safe would protect this from a future well-meaning refactor.

🤖 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 `@internal/swarm/team.go` around lines 177 - 191, Add a concise comment in
Swarm.Scheduler explaining that the closed snapshot is synchronized by
lifecycleMu: Close either observes and closes the newly stored scheduler, or
Scheduler sees closed and closes sched after releasing the locks. Preserve the
existing locking and post-unlock sched.Close() behavior.
🤖 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.

Nitpick comments:
In `@internal/swarm/lifecycle.go`:
- Around line 35-42: Add a doc comment to Swarm.beginLifecycleAdmission
documenting its lock-transfer contract: successful calls return with
lifecycleMu's read lock held, while the closed/error path releases the lock
before returning ErrSwarmClosed. Keep the existing locking behavior unchanged.

In `@internal/swarm/team.go`:
- Around line 177-191: Add a concise comment in Swarm.Scheduler explaining that
the closed snapshot is synchronized by lifecycleMu: Close either observes and
closes the newly stored scheduler, or Scheduler sees closed and closes sched
after releasing the locks. Preserve the existing locking and post-unlock
sched.Close() behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 76fca25e-776c-47e6-a683-cf49151ddb4f

📥 Commits

Reviewing files that changed from the base of the PR and between da9fb50 and dd54ec8.

📒 Files selected for processing (4)
  • internal/swarm/lifecycle.go
  • internal/swarm/lifecycle_test.go
  • internal/swarm/scheduler.go
  • internal/swarm/team.go

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Do not hold lifecycle admission while starting a member
    internal/swarm/lifecycle.go:59
    Spawn (and the retry and queue-drain paths) keeps lifecycleMu.RLock() while invoking the injected MemberLauncher.Launch. Close needs the write lock before it calls s.cancel(), so a launcher that blocks during startup waiting for its supplied context can never be cancelled: launch holds the read lock, while Close waits for that lock before issuing the cancellation. This leaves both the spawn and every close caller hung. Release the admission lock before calling external launcher code, while retaining an ordering mechanism that prevents a launch from being admitted once shutdown starts; add a regression launcher whose Launch waits on ctx.Done().

  • [P3] Document the admission helper's lock-transfer contract
    internal/swarm/lifecycle.go:35
    beginLifecycleAdmission returns with lifecycleMu read-locked on success but unlocks it before returning ErrSwarmClosed. Callers must know that asymmetric contract to avoid leaked read locks or an extra unlock, yet the helper has no comment describing it. This remains the open Copilot/CodeRabbit review request; document the ownership rule (or return an explicit unlock function).

  • [P3] Explain the post-close scheduler handoff
    internal/swarm/team.go:177
    Scheduler snapshots closed under lifecycleMu, installs/reads s.scheduler, then closes the scheduler after releasing both locks. The correctness relies on Close either seeing and closing that stored scheduler or Scheduler seeing the closed snapshot and closing it itself. This is the unresolved CodeRabbit request; add a concise comment preserving that synchronization rationale.

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.

fix(swarm): shutdown does not close lifecycle admission

3 participants