feat(api): wire ContainerEvent + events() streaming API (CHAOS-1323)#14
Open
chrisgeo wants to merge 1 commit into
Open
feat(api): wire ContainerEvent + events() streaming API (CHAOS-1323)#14chrisgeo wants to merge 1 commit into
chrisgeo wants to merge 1 commit into
Conversation
Wires the placeholder 'XPCRoute.containerEvent' and 'XPCKeys.containerEvent'
that already existed in main into a working lifecycle-event stream:
the daemon now records create / start / stop / die / destroy events in
a bounded ring buffer, and 'ContainerClient.events()' returns the
buffered events to the client.
Motivation
----------
External orchestrators that drive the API server (the canonical use
case is a Compose-spec orchestrator implementing 'compose events')
today have no daemon-side event signal — they have to poll
'ContainerClient.list' on a 1-second cadence and diff snapshots to
synthesize lifecycle events. That has three problems:
1. 1s latency floor on event delivery.
2. Events that happen between polls (a quick start->exit->restart)
are lost.
3. Polling cost grows linearly with project size.
Wiring real events at the daemon side replaces the polling-fallback
with first-party signal.
What this PR changes
--------------------
- Sources/ContainerResource/Container/ContainerEvent.swift (new):
ContainerEvent { containerId, action: { create, start, stop, die,
destroy }, timestamp } as a Codable Sendable Equatable struct.
- Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift:
a private 'eventBuffer: [ContainerEvent]' ring (capped at 1000) and
a private 'recordEvent(_:action:)' helper. Lifecycle methods
(handleCreate, handleStart, handleStop, handleDelete) record the
matching action. A new public 'recentEvents(since:)' method returns
the buffered events to the harness.
- Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift:
new 'events(_:)' XPC handler that JSON-encodes recentEvents() into
the reply via the existing 'XPCKeys.containerEvent' payload key.
- Sources/Services/ContainerAPIService/Client/ContainerClient.swift:
new 'events()' method on ContainerClient — sends the
'XPCRoute.containerEvent' message and decodes
'[ContainerEvent]' from the response.
- Sources/APIServer/APIServer+Start.swift: register the harness'
events handler for 'XPCRoute.containerEvent' (the route case
already existed in main as an unwired placeholder).
Note: 'XPCRoute.containerEvent' and 'XPCKeys.containerEvent' both
already existed in main as reserved placeholders — no enum changes
needed in this PR. This is purely the implementation that fills them
in.
Wire compatibility
------------------
Pure additive at the API surface. Older clients ignore the new
events() method. Older servers receiving a containerEvent route
respond with whatever the unwired placeholder did before (typically
an empty reply, which decodes as 'no events').
Known limitations (intentional follow-ups)
------------------------------------------
- Snapshot-style API, not push. Clients call events() and get the
current buffer; long-lived subscribers still have to poll. A
push-style AsyncStream is a natural follow-up.
- No per-client cursor. The buffer is global; clients deduplicate
via 'recentEvents(since:)' on the daemon side.
- No persistence across daemon restarts. The buffer is in-memory.
- Buffer cap is hardcoded at 1000 events. Sufficient for typical
Compose project sizes; large projects under chatty orchestration
will see rollover. A configurable cap is a follow-up.
Verification
------------
Full 'swift build' clean on macOS 26 / Apple silicon (release config,
all targets including downstream consumers of ContainerClient).
0fdd2e0 to
13495fe
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Staging branch — fork-internal review before any apple/container upstream filing.
Linear: CHAOS-1323
Type of Change
Motivation and Context
External orchestrators that drive the API server (the canonical use case is a Compose-spec orchestrator implementing
compose events) today have no daemon-side event signal — they have to pollContainerClient.liston a 1-second cadence and diff snapshots to synthesize lifecycle events. That has three problems:start → exit → restart) are lost.Wiring real events at the daemon side replaces the polling-fallback with first-party signal.
What this PR changes
Sources/ContainerResource/Container/ContainerEvent.swift(new):ContainerEvent { containerId, action: { create, start, stop, die, destroy }, timestamp }as aCodable + Sendable + Equatablestruct.Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift: a privateeventBuffer: [ContainerEvent]ring (capped at 1000) and a privaterecordEvent(_:action:)helper. Lifecycle methods (handleCreate, handleStart, handleStop, handleDelete-running, handleDelete-default) record the matching action. A new publicrecentEvents(since:)returns the buffered events to the harness.Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift: newevents(_:)XPC handler that JSON-encodesrecentEvents()into the reply via the existingXPCKeys.containerEventpayload key.Sources/Services/ContainerAPIService/Client/ContainerClient.swift: newevents()method — sends theXPCRoute.containerEventmessage and decodes[ContainerEvent]from the response.Sources/APIServer/APIServer+Start.swift: register the harness'eventshandler forXPCRoute.containerEvent.Total: 5 files (1 new + 4 modified), +99/−0.
Note on placeholder reuse
XPCRoute.containerEventANDXPCKeys.containerEventboth already exist inmainas reserved placeholders (no implementation behind them). This PR is purely the implementation that fills them in — no enum changes needed.Wire compatibility
Pure additive at the API surface. Older clients ignore the new
events()method. Older servers receiving acontainerEventroute respond with whatever the unwired placeholder did before (typically an empty reply, which decodes as "no events").Known limitations (intentional follow-ups)
events()and get the current buffer; long-lived subscribers still have to poll. A push-styleAsyncStream<ContainerEvent>is a natural follow-up.recentEvents(since:)on the daemon side.Testing
swift buildclean on macOS 26 / Apple silicon, all targets including downstream consumers ofContainerClient).events()method (including the ring-buffer / no-persistence caveats).Status
Draft, fork-staged. Routing to
full-chaos/container:mainfirst so we can review the surface internally before opening the apple/container companion issue + upstream PR (same pattern as CHAOS-1319 / CHAOS-1320 / CHAOS-1321 / CHAOS-1322 / CHAOS-1324).