feat(pelican): stream file events over SSE at GET /pelican/subscribe - #266
Open
rbardaji wants to merge 2 commits into
Open
feat(pelican): stream file events over SSE at GET /pelican/subscribe#266rbardaji wants to merge 2 commits into
rbardaji wants to merge 2 commits into
Conversation
The event server notifies subscribers when an object appears in a namespace, but that stream existed only in the client library, so an Endpoint could not offer subscriptions to its own callers. The Endpoint holds one upstream STOMP subscription per event source and fans it out to every SSE listener. The event server requires a unique client-id per subscriber, so a subscription per caller would split the stream between them or leave an orphaned id registered upstream on every connection. The upstream opens on the first listener and closes on the last, so an idle Endpoint holds no connection. The wire format is implemented here rather than taken from ndp-ep, to avoid the API depending on its own client SDK and on pelicanfs. Adds websockets as a dependency. Closes #262.
The event server identity and credentials were the Endpoint's alone, so every caller subscribed as the Endpoint. They are now accepted per request as client_id, username and password, falling back to the Endpoint's configuration for callers without an account of their own. They are taken from headers as well as query parameters, and the headers win: uvicorn and nginx both write the query string to their access logs, which would put a password on disk in plain text. Credentials are taken as a pair, so supplying only a username cannot borrow the Endpoint's password and sign the caller in as the Endpoint under another name. The upstream is now keyed by client id as well as event source. Callers sharing an identity still share one connection and each receive every event; a caller with its own gets its own, since two identities cannot be served over one authenticated session. Part of #262.
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.
Closes #262, alongside
/pelican/read(#265).Why
The event server notifies subscribers whenever an object appears in a Pelican namespace, but that stream existed only in the
ndp-epclient library. The API had no way to expose it, so an Endpoint could not offer subscriptions to its own callers.The routes
Each event's
urlgoes straight to/pelican/readfor the contents, or/pelican/downloadfor the file. That pipeline — subscribe, then read what arrived — is the reason the route exists.Reports the upstream subscriptions the Endpoint currently holds: connection state, listener count, and how many events were dropped for slow listeners.
Design decisions worth reviewing
One upstream subscription per event source, fanned out to every listener. The event server requires a unique
client-idper subscriber: two connections sharing one compete for the same events instead of both receiving them. A subscription per SSE caller would therefore either split the stream between callers or leave an orphaned client id registered upstream on every connection. Instead the Endpoint subscribes once per event source under its own stable id and distributes each event to all its listeners. The upstream opens when the first listener arrives and closes when the last leaves, so an idle Endpoint holds no connection to the event server.The STOMP wire format is implemented here rather than taken from the client library. Depending on
ndp-epwould make the API depend on its own client SDK and pull inpelicanfs, which pins Python 3.11. The cost, stated in the CHANGELOG: the protocol now lives in two repositories and has to be kept in step by hand.Per-listener buffers are bounded, unlike the client library's deliberately unbounded queue. A caller that stops reading must not be able to grow the Endpoint's memory without limit, so a full buffer drops its oldest event and counts it in
/pelican/subscriptions.The idle timeout lives inside the generator, not around it. Cancelling an
__anext__from outside would unwind the generator and tear the subscription down on every quiet interval, solisten()times its own wait out and yieldsNonefor the route to turn into a: keepalive. There is a test pinning this.Also:
X-Accel-Buffering: no, or nginx buffers the stream and holds events back until the buffer fills, defeating the point.Verification
59 new cases in
tests/test_pelican_events.py: STOMP header escaping (including a round trip over all four escapes), frame encode/parse, event parsing and its rejection cases, redelivery identity, the environment resolution and every one of its fallbacks, the fan-out itself (every listener gets every event; a slow listener drops its oldest; a duplicate is suppressed but still acknowledged, since an unacknowledged message is redelivered forever), and the routes.Against the real app: no token gives 401, an unconfigured Endpoint gives 503 naming the missing setting,
/pelican/subscriptionsanswers 200, and both routes appear in the OpenAPI schema.Local gate:
blackandflake8clean, 1349 tests pass. The three failures intests/repositories/test_catalog_settings.pyandtests/test_publi_env.pypredate this branch — they reproduce on a cleanmainand come from a local.envbeing read by the settings tests, which does not exist in CI.Not verified: the live connection to the event server. There are no credentials to hand, so the STOMP path is exercised against simulated frames, not against
stomp-server.chtcdev.chtc.io. Reviewing that end of it needs an account on the event server.Backwards compatibility
Purely additive. Both routes sit behind the authorization added in #261 and are only mounted when
PELICAN_ENABLEDis set.Subscriptions need
PELICAN_EVENT_CLIENT_ID, or anAFFINITIES_EP_UUIDto derive it from; without either,/pelican/subscribeanswers 503 with the reason, so an Endpoint that never configures it is unaffected. The remaining settings (PELICAN_EVENT_SERVER_URL,PELICAN_EVENT_USERNAME,PELICAN_EVENT_PASSWORD,PELICAN_EVENT_VIRTUAL_HOST,PELICAN_EVENT_HEARTBEAT_MS) are optional and documented inexample.envanddocs/configuration.md.websocketsis a new dependency, required by the event server's transport.