-
Notifications
You must be signed in to change notification settings - Fork 881
[V2][design doc] cloud events #7903
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
popojk
wants to merge
3
commits into
main
Choose a base branch
from
docs/cloudevents-egress
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| # CloudEvents egress — design | ||
|
|
||
| Status: proposal | ||
| Scope: `runs/` | ||
| Tracks: flyteorg/flyte#7829 | ||
|
|
||
| ## What is missing | ||
|
|
||
| Flyte 1 pushed every execution event to a message broker. Systems outside Flyte — lineage | ||
| catalogs, alerting, cost accounting, pipelines in other orchestrators — subscribed to a topic | ||
| and reacted. They never talked to Flyte, and Flyte never knew they existed. | ||
|
|
||
| Flyte 2 records more events than v1 did, in a queryable table, with a live stream on top. What | ||
| it does not do is push them anywhere. Every consumer must now hold a long-lived gRPC stream | ||
| against the control plane, which makes each of them a client Flyte has to serve, keep | ||
| connected, and survive restarts with. | ||
|
|
||
| ``` | ||
| v1 | ||
| +------------+ +--------------+ +---------------------+ | ||
| | flyteadmin |------>| broker |------>| lineage / catalog | | ||
| +------------+ push | | +---------------------+ | ||
| | durable | +---------------------+ | ||
| | replayable |------>| alerting | | ||
| | fans out | +---------------------+ | ||
| | | +---------------------+ | ||
| | |------>| downstream pipeline | | ||
| +--------------+ +---------------------+ | ||
| consumers are decoupled; | ||
| admin never knows they exist | ||
|
|
||
| v2 proposed [NEW] = added by this document; everything else exists today | ||
|
|
||
| +----------------------------------------------------------------------+ | ||
| | InsertEvents() runs/repository/impl/action.go | | ||
| | the only writer of action_events | | ||
| +-----------+--------------------+-------------------------+-----------+ | ||
| | | | | ||
| | 1. INSERT | 2. hand off [NEW] | 3. notifyActionUpdate() | ||
| | (commit) | events in hand | id only, per-action | ||
| v v v | ||
| +----------------------+ +--------------------+ +----------------------------+ | ||
| | action_events | | publisher [NEW] | | pg_notify action_updates | | ||
| | the durable record | | Flyte event | | -> actionSubscribers | | ||
| | no cursor column | | -> CloudEvent | | channel full: DROPPED | | ||
| +----------+-----------+ +---------+----------+ +-------------+--------------+ | ||
| ^ | | | ||
| | | v | ||
| | | +-------------------------+ | ||
| | | | watch streams | | ||
| | | | console / CLI | | ||
| | | | existing subscribers | | ||
| | | +-------------------------+ | ||
| | | | ||
| +-----------------------+ [NEW] on restart, publisher re-reads | ||
| | SELECT ... WHERE > cursor, to | ||
| | cover the crash window: rows | ||
| v committed but never published | ||
| +--------------------------------------+ | ||
| | sender [NEW] | | ||
| | one per transport | | ||
| +------------------+-------------------+ | ||
| v | ||
| +---------------------+ +---------------------+ | ||
| | broker [NEW] |---->| lineage / catalog | | ||
| | | +---------------------+ | ||
| | operator-run, | +---------------------+ | ||
| | not shipped by |---->| alerting | | ||
| | Flyte | +---------------------+ | ||
| | | +---------------------+ | ||
| | |---->| downstream pipeline | | ||
| +---------------------+ +---------------------+ | ||
| new consumers land here | ||
| ``` | ||
|
|
||
| ## Where the publisher attaches | ||
|
|
||
| `InsertEvents` (`runs/repository/impl/action.go:117`) is the only writer of `action_events`, | ||
| and it already has the `[]*models.ActionEvent` batch in hand. That is the hook: the publisher | ||
| takes the same slice the INSERT just committed. No re-read on the happy path. | ||
|
|
||
| The obvious alternative — subscribe to `action_updates` alongside the watch streams — is worse | ||
| on three counts: | ||
|
|
||
| - **The payload is an id.** `notifyActionUpdate` sends `project/domain/run/name` and nothing | ||
| else (`action.go:1120`). A subscriber has to read the table anyway. | ||
| - **It is deduped per action.** A batch of N events for one action produces one notification | ||
| (`action.go:160`). The signal does not tell you how many events to go fetch. | ||
| - **Most of it is not events.** Six of the seven `notifyActionUpdate` call sites write no | ||
| `action_events` row at all — `CreateAction`, `UpdateActionPhase`, `AbortAction`, `AbortRun`, | ||
| `UpdateActionState`, `NotifyStateUpdate`. `action_test.go:1008` asserts the phase update must | ||
| *not* synthesize one. A publisher on that channel would mostly wake up and find nothing. | ||
|
|
||
| **Hand off, do not send inline.** `InsertEvents` sits on the executor's write path; a broker | ||
| round trip must not extend a commit. The publisher takes the batch onto its own queue and the | ||
| caller returns. | ||
|
|
||
| ## The crash window, and what it costs | ||
|
|
||
| Handing off in-process means an event can be committed and never published: the queue is | ||
| memory, and a restart between the commit and the drain loses whatever it held. Closing that is | ||
| the only reason the publisher ever reads `action_events` back — not because the notification | ||
| lacks content, but because nothing else survives the restart. | ||
|
|
||
| That reconcile needs a cursor, and **the table cannot currently support one**: | ||
|
|
||
| ```sql | ||
| -- runs/migrations/sql/20260408110000_init_schema.sql:56 | ||
| PRIMARY KEY (project, domain, run_name, name, attempt, phase, version) | ||
| -- no serial column; created_at is unindexed | ||
| ``` | ||
|
|
||
| The composite key is not globally monotonic, so there is no "everything after X" to scan. | ||
| `created_at` cannot stand in: it defaults to `CURRENT_TIMESTAMP`, which is transaction start | ||
| time, so a long transaction commits rows *behind* a high-water mark already advanced past them | ||
| and the reconcile skips them silently. | ||
|
|
||
| So this design has a prerequisite: either a monotonic `BIGSERIAL` column on `action_events` | ||
| with an index, or a separate outbox table written in the same transaction as the INSERT — which | ||
| `InsertEvents` would have to become, since it commits per chunk today (`action.go:126`). The | ||
| outbox costs a write per event and a reaper; the serial column costs a migration on a hot table | ||
| and still leaves a small ordering gap between sequence assignment and commit visibility, which | ||
| a "re-scan the last N seconds" overlap covers. Pick one before implementing — the rest of the | ||
| publisher design does not depend on which. | ||
|
|
||
| ## What carries over from v1 | ||
|
|
||
| v1 split the feature along a seam worth keeping: a **publisher** that turns a Flyte event into | ||
| a CloudEvent, and a **sender** that puts a CloudEvent on a wire. The publisher knows the Flyte | ||
| domain and nothing about brokers; the sender knows brokers and nothing about Flyte. | ||
|
|
||
| ``` | ||
| +--------------------------------------+ | ||
| | PUBLISHER | | ||
| | knows Flyte events | | ||
| | decides what an event looks like | | ||
| | on the wire: type, id, time, data | | ||
| +------------------+-------------------+ | ||
| | | ||
| v | ||
| +--------------------------------------+ | ||
| | one narrow interface | <-- the seam | ||
| +------------------+-------------------+ | ||
| | | ||
| +------------+------------+------------+ | ||
| v v v v | ||
| +-----------+ +---------+ +-----------+ +---------+ | ||
| | Kafka | | NATS | | cloud | | no-op | | ||
| | sender | | sender | | pub/sub | | | | ||
| +-----+-----+ +----+----+ +-----+-----+ +---------+ | ||
| v v v | ||
| +-----------+ +---------+ +-----------+ | ||
| | operator's brokers -- not shipped or run by Flyte | | ||
| +---------------------------------------------------+ | ||
| ``` | ||
|
|
||
| **The sender half transfers almost unchanged.** Four transports, one interface, and the | ||
| envelope conventions that go with them — a stable id that doubles as the consumer's | ||
| deduplication key, a payload encoding that survives protobuf `oneof` fields, a schema | ||
| reference that lets a consumer validate without asking Flyte. None of that depends on the | ||
| Flyte data model, and it was working in production for years. | ||
|
|
||
| **The publisher half does not transfer.** v1's is written against workflow, node and task | ||
| executions. v2 has runs and actions. Mapping one onto the other is the substantive design work | ||
| here, and it is what #7829 asks to be documented. | ||
|
|
||
| ## Turning it on | ||
|
|
||
| Config only, resolved once at startup. No per-run parameter, no launch-time opt-in, nothing in | ||
| the SDK surface: an operator enables egress for a deployment, and every event flows. | ||
|
|
||
| This mirrors v1, and the shape is worth repeating for two reasons. Operators already know it. | ||
| And the alternative — letting individual runs choose — makes the event stream unreliable as a | ||
| source of truth, because a consumer can no longer assume that silence means nothing happened. | ||
|
|
||
| Two properties the config gate should preserve: | ||
|
|
||
| - **Off by default.** A deployment that says nothing about egress publishes nothing. | ||
| - **Filterable.** An operator who wants only terminal events should be able to say so without | ||
| filtering client-side, because the cost of the events they do not want is paid on the wire. | ||
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to add a new CloudEventActionExecution or CloudEventRunExecution proto?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And the new schema for the new event? https://github.com/flyteorg/flyteadmin/blob/a0ca4b07d3b1c3cccfe3830307df50bc73152ddb/pkg/async/cloudevent/implementations/cloudevent_publisher.go#L25-L28