Skip to content

Withdraw disabled capabilities from every session in one publication - #108

Open
codegeek wants to merge 2 commits into
mainfrom
014-capability-lifecycle
Open

codegeek wants to merge 2 commits into
mainfrom
014-capability-lifecycle

Conversation

@codegeek

@codegeek codegeek commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Why

Disabling a capability at runtime (EXTENSION disable, REHASH or SIGHUP) left it active for clients that had already negotiated it (#97). Negotiated sets were never pruned, and CAP validation and apply were separate steps, so a CAP REQ that raced a disable could bring the name back. This is PR 3 of spec 014 (tasks T042–T054), after #104 and #106.

Changes

  • Atomic publication. A transition publishes a new immutable RuntimeSnapshot and prunes every live session in one step under a short mutex in CapabilityRuntime. CAP DEL/NEW are queued after publication. Work that captured the old snapshot may still finish, which the spec allows.
  • Two-level lock. ExtensionRegistry's lifecycle lock serializes whole transitions. The runtime mutex covers publish+prune, CAP LS/LIST/REQ, adding/removing/attaching live sessions and the batch group choice. Lock order is lifecycle → runtime mutex → one session writer lock. Provider start/stop never run under the mutex. Code outside the runtime gets a CapabilityControl handle that has no publish methods.
  • CAP. REQ validates and applies in one step, with zero mutation on NAK/417 and an exact echo. Negative REQ of held or already-pruned names is accepted. Core cap-notify is always advertised. LS-version tracking is sticky and overflow-safe, LIST distinguishes explicit and implicit, and NEW/DEL/LS/LIST lines are packed within bounds.
  • REHASH. The whole candidate graph is validated before anything changes: cycles, missing or disabled prerequisites, duplicate offers/slots/commands and reply budgets. An invalid file changes nothing. Providers start dependency-first outside the mutex, one publication swaps all changes together with the new limits, and withdrawn providers then stop dependents-first. A rollback runs in reverse start order.
  • Provider commands are part of the snapshot. Dispatch checks the core table first, then resolves provider commands from one snapshot read. A command moving between providers within one reload is never briefly unknown, and a server extension can't register or shadow a provider's command.
  • NICK. The nick change and the client's own NICK line are serialized with publication, so CAP DEL never targets a stale nick.
  • Admin. EXTENSION and REHASH go through the same lifecycle and report the actual outcome.
  • Docs. A dated amendment in plan.md/research.md records the two-level lock. The specs/001 contracts, the data model and the README are updated. Validation records are in specs/014-capability-plugin-spi/validation/pr-97/.

Testing

Open points (recorded in review.md)

fixes #97

🤖 Generated with Claude Code

Summary by Sourcery

Make runtime capability lifecycle changes atomic across snapshots, negotiated sessions, notifications, provider commands, and configuration reloads.

New Features:

  • Advertise core cap-notify and support atomic CAP negotiation with sticky version handling, bounded replies, and capability notifications.
  • Withdraw disabled capabilities and their dependents from all live sessions in a single publication, sending CAP DEL/NEW notifications to eligible clients.
  • Apply complete REHASH transitions atomically with dependency validation, ordered provider lifecycle handling, failure reporting, and coherent configuration updates.
  • Resolve provider-owned commands from immutable runtime snapshots so commands can move between providers without transient gaps.
  • Preserve coherent output-group behavior and nickname/notification ordering across capability changes.

Bug Fixes:

  • Prevent CAP REQ races from restoring capabilities that have just been disabled.
  • Ensure negotiated capability state is pruned for already-connected sessions when providers are withdrawn.
  • Prevent invalid reloads, partial command handovers, stale notification targets, and inconsistent lifecycle outcomes.

Enhancements:

  • Introduce coordinated lifecycle and capability-runtime locking while keeping provider callbacks outside the short runtime mutex.
  • Make negotiated capability sets immutable and update capability protocol grammar for deterministic, size-bounded LS, LIST, NEW, and DEL responses.

Documentation:

  • Update README, protocol contracts, data model, and capability lifecycle design documentation for runtime withdrawal, notification behavior, immutable negotiation, and snapshot-based commands.
  • Record validation, performance measurements, and review results for the capability lifecycle implementation.

Tests:

  • Add unit, socket, integration, concurrency, reload failure-path, command handover, notification ordering, and publication timing coverage for the new lifecycle behavior.
  • Validate the full build, coverage thresholds, static analysis, and 1,000-session publication performance.

codegeek and others added 2 commits October 24, 2026 21:04
The 014 design assumed an existing administrative mutex; there wasn't one.
Record the decision: ExtensionRegistry's lifecycle lock serializes whole
transitions and a short CapabilityRuntime mutex covers publish+prune, CAP
LS/LIST/REQ and session admission. Update the contracts for snapshot
capture, single publication per reload, CAP DEL/NEW, negative REQ, core
cap-notify in LS and snapshot-resolved provider commands.

Refs #97

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Disabling a capability left it active for clients that had negotiated
it, because the negotiated sets were never pruned and CAP validation and
apply were separate steps. Publication now swaps an immutable snapshot
and prunes every live session under one mutex, then sends CAP DEL/NEW.
CAP REQ validates and applies in one step, so a REQ racing a disable
cannot restore a pruned name.

A REHASH validates the whole candidate graph (cycles, missing
prerequisites, conflicts, reply budgets) before changing anything, starts
providers outside the mutex, publishes once, and stops withdrawn
providers dependents-first. Provider commands are part of the snapshot,
so a command moving between providers is never briefly unknown, and a
server extension can't shadow a provider's command. NICK changes and
self-notification are serialized with publication so CAP DEL never
targets a stale nick.

Publication with 1,000 sessions takes under 4 ms including DEL/NEW
(budget 100 ms).

fixes #97

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 25, 2026

Copy link
Copy Markdown

Sorry @codegeek, this account has used its review budget of 2,500,000 diff characters for the last 7 days.

You can request another review in 13 hours and 19 minutes by commenting @sourcery-ai review.

@sourcery-ai

sourcery-ai Bot commented Oct 25, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR closes the runtime capability lifecycle gap by atomically publishing immutable capability snapshots, pruning withdrawn capabilities from every live session, and synchronizing CAP negotiation, notifications, reloads, command dispatch, and related session behavior around a two-level locking model, with extensive protocol, concurrency, failure-path, integration, and load-test coverage plus corresponding contract and design documentation.

Sequence diagram for atomic capability withdrawal

sequenceDiagram
    participant Admin as Admin or REHASH
    participant Registry as ExtensionRegistry
    participant Runtime as CapabilityRuntime
    participant Sessions as Live sessions
    participant Writers as Session writers
    participant Provider as Provider lifecycle

    Admin->>Registry: disableLocked(id) or applyDesiredState(...)
    Registry->>Registry: acquire lifecycle lock
    Registry->>Runtime: withdraw(id) / publish(change)
    Runtime->>Runtime: acquire administrative mutex
    Runtime->>Runtime: publish immutable RuntimeSnapshot
    Runtime->>Sessions: prune withdrawn capabilities
    Runtime->>Writers: queue CAP DEL and CAP NEW
    Runtime-->>Registry: publication complete
    Registry->>Provider: stop() outside runtime mutex
    Registry-->>Admin: actual lifecycle outcome
Loading

Sequence diagram for race-safe CAP REQ and publication

sequenceDiagram
    participant Client as Client
    participant CAP as CapCommandHandler
    participant Control as CapabilityControl
    participant Runtime as CapabilityRuntime
    participant Session as ClientSession

    Client->>CAP: CAP REQ capabilities
    CAP->>Control: runUnderControl(...)
    Control->>Runtime: acquire administrative mutex
    CAP->>Session: candidate(session, requested)
    alt request accepted
        CAP->>Session: replaceNegotiatedCapabilities(candidate)
        CAP-->>Client: CAP ACK original request
    else request refused
        CAP-->>Client: CAP NAK original request
    end
    Runtime-->>Control: release mutex

    Note over Runtime,Session: A concurrent publication either prunes first or CAP REQ commits first, and a withdrawn name cannot be restored
Loading

Sequence diagram for provider command handover

sequenceDiagram
    participant Client as Client
    participant Handler as ConnectionHandler
    participant Registry as ExtensionRegistry
    participant Runtime as CapabilityRuntime
    participant Snapshot as RuntimeSnapshot
    participant Provider as Provider command handler

    Client->>Handler: command
    Handler->>Handler: resolve core command table
    alt no core handler
        Handler->>Registry: capabilityCommand(command)
        Registry->>Runtime: snapshot()
        Runtime-->>Registry: one immutable snapshot read
        Registry->>Snapshot: command(command)
        Snapshot-->>Registry: published provider handler
        Registry->>Provider: guardedCommand(registration)
        Provider-->>Client: handle command
    else core handler exists
        Handler-->>Client: dispatch core handler
    end

    Note over Runtime,Snapshot: Reload publication swaps provider commands with hooks, so a command moves atomically between providers
Loading

Flow diagram for atomic configuration reload

flowchart TD
    Reload[ConfigurationReloader.reloadWithOutcome] --> Validate[Validate complete candidate graph and limits]
    Validate -->|invalid| Unchanged[Leave snapshot and sessions unchanged]
    Validate -->|valid| Start[Start providers dependency-first]
    Start --> Publish[Publish snapshot and configuration together]
    Publish --> Prune[Prune withdrawn capabilities from live sessions]
    Prune --> Notify[Queue CAP DEL and CAP NEW]
    Notify --> Stop[Stop withdrawn providers dependents-first]
    Start -->|start failure| Failed[Record FAILED provider and dependent outcomes]
    Failed --> Publish
Loading

File-Level Changes

Change Details Files
Made capability lifecycle publication atomic across runtime state and all live sessions.
  • Added immutable snapshot publication with all-session negotiation pruning and ordered CAP DEL/NEW notifications.
  • Introduced the lifecycle-lock plus short runtime-mutex model, including synchronized session admission, writer attachment, captures, NICK handling, and group admission.
  • Kept provider callbacks outside the runtime mutex and added dependency-aware withdrawal and cleanup ordering.
jircd-core/src/main/java/net/jircd/core/extension/capability/CapabilityRuntime.java
jircd-core/src/main/java/net/jircd/core/extension/capability/CapabilityControl.java
jircd-core/src/main/java/net/jircd/core/extension/capability/RuntimeSnapshot.java
jircd-core/src/main/java/net/jircd/core/extension/ExtensionRegistry.java
jircd-core/src/main/java/net/jircd/core/session/ClientSession.java
jircd-core/src/main/java/net/jircd/core/session/ConnectionHandler.java
jircd-core/src/main/java/net/jircd/core/session/command/NickCommandHandler.java
Reworked CAP negotiation and protocol framing for atomic, race-safe runtime behavior.
  • Advertised core cap-notify and implemented sticky, overflow-safe LS version tracking with explicit versus implicit LIST state.
  • Combined REQ validation, size checking, mutation, and reply under the runtime mutex with exact ACK/NAK echoes and zero mutation on rejection.
  • Added bounded deterministic packing for LS/LIST/NEW/DEL and accepted held or known-pruned negative requests according to the updated contract.
jircd-core/src/main/java/net/jircd/core/capability/CapabilityNegotiator.java
jircd-core/src/main/java/net/jircd/core/session/command/CapCommandHandler.java
jircd-protocol/src/main/java/net/jircd/protocol/CapabilityName.java
jircd-protocol/src/main/java/net/jircd/protocol/CapabilityNegotiationGrammar.java
Made reloads and extension toggles transactional across the candidate provider graph and lifecycle state.
  • Validated dependency cycles, prerequisites, duplicate ownership, reply budgets, and candidate configuration before changing state.
  • Started providers dependency-first, published all capability/configuration changes together, then stopped withdrawn providers dependents-first with rollback and failure reporting.
  • Moved provider-owned commands into RuntimeSnapshot dispatch so command handovers are atomic and cannot temporarily become unknown or be shadowed by server extensions.
jircd-core/src/main/java/net/jircd/core/config/ConfigurationReloader.java
jircd-core/src/main/java/net/jircd/core/extension/ExtensionRegistry.java
jircd-core/src/main/java/net/jircd/core/extension/capability/RuntimeSnapshot.java
jircd-core/src/main/java/net/jircd/core/session/ConnectionHandler.java
jircd-capabilities/message-tags/src/test/java/net/jircd/capabilities/messagetags/TagmsgCommandHandlerTest.java
Added comprehensive unit, integration, lifecycle, concurrency, and load coverage for the new semantics.
  • Covered CAP grammar and negotiation edge cases, publication/prune races, dependency cascades, command handover, NICK ordering, group fallback, failure paths, and notification eligibility.
  • Added real-socket EXTENSION/REHASH lifecycle matrix tests and a 1,000-session publication timing test.
  • Migrated mutable negotiated-capability fixtures to immutable test setup helpers.
jircd-core/src/test/java/net/jircd/core/capability/CapabilityLifecycleNegotiationTest.java
jircd-core/src/test/java/net/jircd/core/extension/capability/CapabilityRuntimeTest.java
jircd-core/src/test/java/net/jircd/core/config/ConfigurationReloaderLifecycleTest.java
jircd-core/src/test/java/net/jircd/core/config/ConfigurationReloaderFailurePathTest.java
jircd-core/src/test/java/net/jircd/core/session/CommandHandoverDispatchTest.java
jircd-core/src/test/java/net/jircd/core/session/ConnectionHandlerCommandTableTest.java
jircd-core/src/test/java/net/jircd/core/session/command/NickCapabilityNotificationOrderTest.java
jircd-protocol/src/test/java/net/jircd/protocol/CapabilityNegotiationGrammarTest.java
jircd-integration-tests/src/test/java/net/jircd/integration/CapabilityLifecycleTest.java
jircd-integration-tests/src/test/java/net/jircd/integration/CapabilityGroupLifecycleTest.java
jircd-integration-tests/src/test/java/net/jircd/integration/CapabilityPublicationTimingLoadTest.java
jircd-integration-tests/src/test/java/net/jircd/integration/CapabilityProviderAbsenceTest.java
jircd-core/src/test/java/net/jircd/core/session/TestNegotiation.java
Updated public contracts, README guidance, design amendments, task tracking, and validation records.
  • Documented cap-notify, runtime pruning, notification rules, reload atomicity, immutable negotiation, command snapshot ownership, and accepted captured-output behavior.
  • Recorded the two-level lock design, completed PR tasks T042–T054, and added build, coverage, performance, and review evidence.
README.md
specs/001-ircv3-server/contracts/irc-protocol-commands.md
specs/001-ircv3-server/contracts/server-configuration.md
specs/001-ircv3-server/data-model.md
specs/014-capability-plugin-spi/plan.md
specs/014-capability-plugin-spi/research.md
specs/014-capability-plugin-spi/tasks.md
specs/014-capability-plugin-spi/validation/pr-97/results.md
specs/014-capability-plugin-spi/validation/pr-97/review.md
Updated administrator-facing lifecycle outcome reporting.
  • Route REHASH through ReloadOutcome and report provider-specific failures while applying the remainder of a valid reload.
  • Report the actual post-toggle extension state instead of assuming the requested state.
jircd-server-extensions/admin/src/main/java/net/jircd/serverextensions/admin/ExtensionCommandHandler.java
jircd-server-extensions/admin/src/main/java/net/jircd/serverextensions/admin/RehashCommandHandler.java
jircd-server-extensions/admin/src/test/java/net/jircd/serverextensions/admin/LifecycleOutcomeReportingTest.java

Assessment against linked issues

Issue Objective Addressed Explanation
#97 On runtime disable, atomically withdraw the capability and prune it from every live session’s negotiated set so subsequent capability-gated behavior and CAP LIST reflect the change; do not restore prior negotiations on re-enable. ✅
#97 Notify eligible clients with CAP DEL and CAP NEW, advertise cap-notify, and accept CAP REQ removals for held or previously pruned known capabilities even when no longer offered. ✅
#97 Document the runtime lifecycle and cover disable/re-enable through EXTENSION and REHASH, including client-visible behavior and post-disable negative CAP REQ, with integration tests. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sonarqubecloud

Copy link
Copy Markdown

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Disabling a capability at runtime leaves it active for clients that negotiated it

1 participant