|
2 | 2 | slug: 29
|
3 | 3 | title: |
|
4 | 4 | 29. EventServer abstraction for event persistence
|
5 |
| -authors: [@cardenaso11] |
| 5 | +authors: [@cardenaso11, @quantumplation] |
6 | 6 | tags: []
|
7 | 7 | ---
|
8 | 8 |
|
9 | 9 | ## Status
|
10 |
| -N/A |
| 10 | +Draft |
11 | 11 |
|
12 | 12 | ## Context
|
13 | 13 |
|
14 |
| -The current Hydra API is materialized as a firehose WebSocket connection: upon connecting, a client receives a deluge of information, such as all past transactions. Additionally, there is no way to pick and choose which messages to receive, and not all events are exposed via the API. |
| 14 | +The Hydra node represents a significant engineering asset, providing layer 1 monitoring, peer to peer consensus, durable persistence, and an isomorphic Cardano ledger. Because of this, it is being eyed as a key building block not just in Hydra based applications, but other protocols as well. |
15 | 15 |
|
16 |
| -This forces some awkward interactions when integrating with a Hydra node externally, such as when using it as a component in other protocols, or trying to operationalize the Hydra server for monitoring and persistence. Here's just a few of the things that prove to be awkward: |
17 |
| - - An application that only cared about certain Hydra resources (like say, the current UTxO set), would be unable to subscribe to just that resource. The application would be made to either parse the Hydra incremental persistence log directly (which may change), or subscribe to the full firehose of Websocket API events. |
18 |
| - - An application that wanted to write these hydra events to a message broker like RabbitMQ or Kinesis would need it's own custom integration layer |
19 |
| - - An application that wishes to build on Hydra would need to fork the Hydra project in order to change how file persistence worked. |
20 |
| - |
| 16 | +One remaining difficulty in integrating Hydra into a fully productionalized software stack is the persistence model. Currently, the Hydra node achieves durability by writing a sequence of "StateChanged" events to disk. If a node is interrupted, upon restarting, it can replay just these events, ignoring their corresponding effects, to recover the same internal state it had when it was interrupted. However, downstream consumers don't have the same luxury. |
21 | 17 |
|
22 |
| -Additionally, much of the changes that would need to happen to solve any of these would apply equally well to the on-disk persistence the Hydra node currently provides. |
23 |
| - |
24 |
| -------------------------------------- |
25 |
| - |
26 |
| -- Current client API is a stateful WebSocket connection. Client API has its own incrementally persisted state that must be maintained, and includes a monotonically increasing `Seq` ID, but this is based off the events a client observes, not the Hydra host's event log. |
27 |
| - |
28 |
| -- Client must be flexible and ready to handle many different events |
29 |
| - |
30 |
| -- There is no way to simply hand off transactions to the hyrda-node currently, a full connection must be initiated before observed transactions can be applied. |
31 |
| - |
32 |
| -- Using history=0 in the query string allows ignoring prior events, but it's not possible to ignore events going forward, only disabling UTxO on Snapshot events. |
33 |
| - |
34 |
| -- Many applications, like SundaeSwap's Gummiworm Protocol, would benefit from using the API in ways which are currently not supported |
35 |
| - |
36 |
| -- Custom event management is intended to be for a minority of Hydra users, that intend to integrate heavily with Hydra. It's a good fit for passing stuff off to a message queue (MQTT, Kafka, whatever) for further processing, probably from a dedicated process (see "Chain Server" below) |
37 |
| - - The event management is intended to modularize and unify the Websocket API, and (incremental, full) persistence |
38 |
| - - The default event management is intended to be transparent to most users |
39 |
| - - There exist "highly compatible" specifications like STOMP or MQTT, but supporting these directly as a substitute for API or persistence, would lock in a significant amount of implementation details |
40 |
| - |
41 |
| - |
42 |
| -<!-- The following section can be removed from the ADR, but mostly just recorded background, motivation, prior art --> |
43 |
| -- Previous mock chain using ZeroMQ was [removed][zmq] as part of [#119][#119], due to complexity and feeling of ZeroMQ being unmaintained (primary author no longer contributing, new tag release versions not being published) |
44 |
| - - This mock chain was used to mock the layer L1 chain, not the L2 ledger |
45 |
| -- Attempt in February 2023 to externalize chainsync server as part of [#230][#230] |
46 |
| - - Similar to [#119][#119], but the "Chain Server" component in charge of translating Hydra Websocket messages into direct chain, mock chain, or any hypothetical message queue, not necessarily just ZeroMQ |
47 |
| - - Deemed low priority due to ambiguous use-case at the time. SundaeSwap's Gummiworm Protocol would benefit from the additional control enabled by the Event Server |
48 |
| - |
49 |
| -- Offline mode intended to persist UTxO state to a file for simplified offline-mode persistence. As a standalone feature, the interface would be too ad-hoc. A less ad-hoc way to keep a single updated UTxO state file, would instead allow for keeping an updated file for any Hydra resource. |
| 18 | +We propose generalizing the persistence mechanism to open the door to a plugin based approach to extending the Hydra node. |
50 | 19 |
|
51 | 20 | # Decision
|
52 |
| -Each internal hydra event will have a durable, monotonically increasing event ID, ordering all the internal events in the persistence log. |
53 | 21 |
|
54 |
| -A new abstraction, the EventSink, will be introduced. The node's state will contain a non-empty list of EventSinks. Each internal hydra event will be sent to a non-empty list of event sinks, which will be responsible for persisting or serving that event in a specific manner. When multiple event sinks are specified, they run in order. Active event can change at runtime. Initially, we will implement the following: |
55 |
| - - MockEventSink, which discards the event. This is exclusive to offline mode, since this would change the semantics of the Hydra node in online mode. This is the only way to have an "empty" EventSink list |
56 |
| - - APIBroadcastEventSink, which broadcasts the publicly visible resource events over the websocket API. |
57 |
| - - Subsumes the existing Websocket API. |
58 |
| - - Can be created via CLI subcommand if Websocket client IP is known. Can be created at runtime by the top-level API (APIServerEventSource) |
59 |
| - - Two modes: |
60 |
| - - Full event log mode. Similar to existing Websocket API, broadcasts all events. |
61 |
| - - Single-resource event log mode. Broadcasts the state changes of a single resource. |
62 |
| - - One APIBroadcastEventSink per listener. A Hydra node running with no one listening would have 0 APIBroadcastEventSink's in the EventSink list |
63 |
| - - Establishing a websocket connection will add a new event sink to handle broadcasting messages |
64 |
| - - Resources should all support JSON content type. UTXO resource, Tx resource, should support CBOR. |
65 |
| - - APIBroadcastResourceSink, which broadcasts the latest resource after a StateChanged event |
66 |
| - - Runs in single-resource event log mode, broadcasting the current state of a single resource. |
67 |
| - - EventFileSink, which updates a file with the state changed by a StateChanged event |
68 |
| - - Two modes: |
69 |
| - - Full event log mode. Encapsulates the existing incremental file persistence. Appends all server events incrementally to a file. |
70 |
| - - One of these in the EventSink list is required in Online mode, for proper Hydra semantics |
71 |
| - - Single-resource event log mode. Incrementally appends an event log file for a single resource |
72 |
| - - Persists StateChanged changes |
73 |
| - - ResourceFileSink, which updates a file with the latest resource after a StateChanged event |
74 |
| - - Two modes: |
75 |
| - - Full event log mode. Encapsulates the existing non-incremental full file persistence mechanism. Appends all server events incrementally to a file. |
76 |
| - - Single-resource event log mode. Maintains an up-to-date file for a single resource |
77 |
| - - Consuming an up-to-date single resource will no longer be coupled with overall Hydra state format, only the encoding schema of that particular resource |
78 |
| - - Generalizes the UTxO persistence mechanism previously discussed in [offline mode][offline-mode] |
79 |
| - - May be configured to only persist resource state: |
80 |
| - - Periodically, if the last write was more than a certain interval ago |
81 |
| - - On graceful shutdown/SIGTERM |
82 |
| - - Allows for performant in-memory Hydra usage, for offline mode usecases where the transactions ingested are already persisted elsewhere, and only certain resources are considered important |
| 22 | +We propose adding a "persistence combinator", which can combine one or more "persistenceIncremental" instances. |
83 | 23 |
|
84 |
| - - One configuration which we expect will be common and useful, is the usage of a ResourceFileSink on the UTxO resource in tandem with a MockEventServer in offline mode. |
| 24 | +When appending to the combinator, it will forward the append to each persistence mechanism. |
85 | 25 |
|
86 |
| -The event server will be configured via a new subcommand ("initial-sinks"), which takes an unbounded list of positional arguments. Each positional argument adds a sink to the initial event sink list. There is one argument constructor per EventSink type. Arguments are added in-order to the initial EventSink list. The default parameters configure an EventFileSink for full incremental persistence. |
| 26 | +As the node starts up, as part of recovering the node state, it will also ensure "at least once" semantics for each persistence mechanism. It will maintain a notion of a "primary" instance, from which events are loaded, and "secondary instances", which are given the opportunity to re-observe each event. |
87 | 27 |
|
88 |
| -The top-level API will change to implement the API changes described in [ADR 25][adr-25] |
89 |
| - - Top level Websocket subscription adds a Full event log EventFileSink |
90 |
| - - API on /vN/ (for some N) will feature endpoints different resources |
91 |
| - - POST verbs emit state change events to modify the resource in question |
92 |
| - - Websocket upgrades on GET emit state change events for the EventSink list (itself a resource) to establish new ongoing Websocket client subscriptions |
93 |
| - - This will expose the new single-resource APIBroadcastEventSink and APIBroadcastResourceSink |
| 28 | +Each persistence mechanism will be responsible for it's own durability; for example, it may maintain its own checkpoint, and only re-broadcast the event if its after the checkpoint. |
94 | 29 |
|
95 |
| -<!-- Full EventSource implementation probably should be in its own ADR. This is included here, for now, to give an idea of what the bigger picture is--> |
96 |
| -A new abstraction, the EventSource, will be introduced. |
97 |
| - - APIServerEventSource |
98 |
| - - Top level API |
99 |
| - - Top level Websocket API adds a Full event log EventFileSink |
100 |
| - - Handles non-websocket-upgraded single-shot HTTP API verbs [ADR 25][adr-25] |
101 |
| - - POST verbs emit state change events to modify the resource in question |
102 |
| - - Websocket upgraded verbs modify the EventSink list (itself a resource) to establish new ongoing subscribers |
| 30 | +The exact implementation details of the above are left unspecified, to allow some flexibility and experimentation on the exact mechanism to realize these goals. |
103 | 31 |
|
104 | 32 | ## Consequences
|
105 | 33 |
|
106 |
| -The primary consequence of this is to enable deeper integration and better operationalization of the Hydra node. For example: |
107 |
| -- Users may now use the new sinks to implement custom integrations with existing ecosystem tools |
108 |
| -- Users may use the file sinks to reduce overhead significantly in Offline mode |
109 |
| -- Developers may more easily maintain downstream forks with custom implementations that aren't appropriate for community-wide adoption, such as the Gummiworm Protocol |
110 |
| -- Logging, metrics, and durability can be improved or tailored to the application through such integrations |
111 |
| - |
112 |
| -Note that while a future goal of this work is to improve the websocket API, making it more stateless and "subscription" based, this ADR does not seek to make those changes, only make them easier to implement in the future. |
113 |
| - |
114 |
| -[adr-25]: https://hydra.family/head-protocol/adr/25/ |
115 |
| -[offline-mode]: 2023-10-16_028_offline_adr.md |
116 |
| -[#119]: https://github.com/input-output-hk/hydra/pull/119 |
117 |
| -[zmq]: https://github.com/input-output-hk/hydra/blob/41598800a9e0396c562a946780909732e5332245/CHANGELOG.md?plain=1#L710- |
118 |
| -[#230]: https://github.com/input-output-hk/hydra/pull/230 |
| 34 | +Here are the consequences we forsee from this change: |
| 35 | +- The default operation of the node remains unchanged |
| 36 | +- Projects forking the hydra node have a natively supported mechanism to extend node persistence |
| 37 | +- These extensions can preserve robust "at least once" semantics for each hydra event |
| 38 | +- Sundae Labs will build a "Save transaction batches to S3" proof of concept extension |
| 39 | +- Sundae Labs will build a "Scrolls source" proof of concept integration |
| 40 | +- This may also enable a future ADRs for dynamically loaded plugins without having to fork the Hydra node at all |
0 commit comments