You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

684 lines
31 KiB

  1. # ADR 075: RPC Event Subscription Interface
  2. ## Changelog
  3. - 01-Mar-2022: Update long-polling interface (@creachadair).
  4. - 10-Feb-2022: Updates to reflect implementation.
  5. - 26-Jan-2022: Marked accepted.
  6. - 22-Jan-2022: Updated and expanded (@creachadair).
  7. - 20-Nov-2021: Initial draft (@creachadair).
  8. ---
  9. ## Status
  10. Accepted
  11. ---
  12. ## Background & Context
  13. For context, see [RFC 006: Event Subscription][rfc006].
  14. The [Tendermint RPC service][rpc-service] permits clients to subscribe to the
  15. event stream generated by a consensus node. This allows clients to observe the
  16. state of the consensus network, including details of the consensus algorithm
  17. state machine, proposals, transaction delivery, and block completion. The
  18. application may also attach custom key-value attributes to events to expose
  19. application-specific details to clients.
  20. The event subscription API in the RPC service currently comprises three methods:
  21. 1. `subscribe`: A request to subscribe to the events matching a specific
  22. [query expression][query-grammar]. Events can be filtered by their key-value
  23. attributes, including custom attributes provided by the application.
  24. 2. `unsubscribe`: A request to cancel an existing subscription based on its
  25. query expression.
  26. 3. `unsubscribe_all`: A request to cancel all existing subscriptions belonging
  27. to the client.
  28. There are some important technical and UX issues with the current RPC event
  29. subscription API. The rest of this ADR outlines these problems in detail, and
  30. proposes a new API scheme intended to address them.
  31. ### Issue 1: Persistent connections
  32. To subscribe to a node's event stream, a client needs a persistent connection
  33. to the node. Unlike the other methods of the service, for which each call is
  34. serviced by a short-lived HTTP round trip, subscription delivers a continuous
  35. stream of events to the client by hijacking the HTTP channel for a websocket.
  36. The stream (and hence the HTTP request) persists until either the subscription
  37. is explicitly cancelled, or the connection is closed.
  38. There are several problems with this API:
  39. 1. **Expensive per-connection state**: The server must maintain a substantial
  40. amount of state per subscriber client:
  41. - The current implementation uses a [WebSocket][ws] for each active
  42. subscriber. The connection must be maintained even if there are no
  43. matching events for a given client.
  44. The server can drop idle connections to save resources, but doing so
  45. terminates all subscriptions on those connections and forces those clients
  46. to re-connect, adding additional resource churn for the server.
  47. - In addition, the server maintains a separate buffer of undelivered events
  48. for each client. This is to reduce the dual risks that a client will miss
  49. events, and that a slow client could "push back" on the publisher,
  50. impeding the progress of consensus.
  51. Because event traffic is quite bursty, queues can potentially take up a
  52. lot of memory. Moreover, each subscriber may have a different filter
  53. query, so the server winds up having to duplicate the same events among
  54. multiple subscriber queues. Not only does this add memory pressure, but it
  55. does so most at the worst possible time, i.e., when the server is already
  56. under load from high event traffic.
  57. 2. **Operational access control is difficult**: The server's websocket
  58. interface exposes _all_ the RPC service endpoints, not only the subscription
  59. methods. This includes methods that allow callers to inject arbitrary
  60. transactions (`broadcast_tx_*`) and evidence (`broadcast_evidence`) into the
  61. network, remove transactions (`remove_tx`), and request arbitrary amounts of
  62. chain state.
  63. Filtering requests to the GET endpoint is straightforward: A reverse proxy
  64. like [nginx][nginx] can easily filter methods by URL path. Filtering POST
  65. requests takes a bit more work, but can be managed with a filter program
  66. that speaks [FastCGI][fcgi] and parses JSON-RPC request bodies.
  67. Filtering the websocket interface requires a dedicated proxy implementation.
  68. Although nginx can [reverse-proxy websockets][rp-ws], it does not support
  69. filtering websocket traffic via FastCGI. The operator would need to either
  70. implement a custom [nginx extension module][ng-xm] or build and run a
  71. standalone proxy that implements websocket and filters each session. Apart
  72. from the work, this also makes the system even more resource intensive, as
  73. well as introducing yet another connection that could potentially time out
  74. or stall on full buffers.
  75. Even for the simple case of restricting access to only event subscription,
  76. there is no easy solution currently: Once a caller has access to the
  77. websocket endpoint, it has complete access to the RPC service.
  78. ### Issue 2: Inconvenient client API
  79. The subscription interface has some inconvenient features for the client as
  80. well as the server. These include:
  81. 1. **Non-standard protocol:** The RPC service is mostly [JSON-RPC 2.0][jsonrpc2],
  82. but the subscription interface diverges from the standard.
  83. In a standard JSON-RPC 2.0 call, the client initiates a request to the
  84. server with a unique ID, and the server concludes the call by sending a
  85. reply for that ID. The `subscribe` implementation, however, sends multiple
  86. responses to the client's request:
  87. - The client sends `subscribe` with some ID `x` and the desired query
  88. - The server responds with ID `x` and an empty confirmation response.
  89. - The server then (repeatedly) sends event result responses with ID `x`, one
  90. for each item with a matching event.
  91. Standard JSON-RPC clients will reject the subsequent replies, as they
  92. announce a request ID (`x`) that is already complete. This means a caller
  93. has to implement Tendermint-specific handling for these responses.
  94. Moreover, the result format is different between the initial confirmation
  95. and the subsequent responses. This means a caller has to implement special
  96. logic for decoding the first response versus the subsequent ones.
  97. 2. **No way to detect data loss:** The subscriber connection can be terminated
  98. for many reasons. Even ignoring ordinary network issues (e.g., packet loss):
  99. - The server will drop messages and/or close the websocket if its write
  100. buffer fills, or if the queue of undelivered matching events is not
  101. drained fast enough. The client has no way to discover that messages were
  102. dropped even if the connection remains open.
  103. - Either the client or the server may close the websocket if the websocket
  104. PING and PONG exchanges are not handled correctly, or frequently enough.
  105. Even if correctly implemented, this may fail if the system is under high
  106. load and cannot service those control messages in a timely manner.
  107. When the connection is terminated, the server drops all the subscriptions
  108. for that client (as if it had called `unsubscribe_all`). Even if the client
  109. reconnects, any events that were published during the period between the
  110. disconnect and re-connect and re-subscription will be silently lost, and the
  111. client has no way to discover that it missed some relevant messages.
  112. 3. **No way to replay old events:** Even if a client knew it had missed some
  113. events (due to a disconnection, for example), the API provides no way for
  114. the client to "play back" events it may have missed.
  115. 4. **Large response sizes:** Some event data can be quite large, and there can
  116. be substantial duplication across items. The API allows the client to select
  117. _which_ events are reported, but has no way to control which parts of a
  118. matching event it wishes to receive.
  119. This can be costly on the server (which has to marshal those data into
  120. JSON), the network, and the client (which has to unmarshal the result and
  121. then pick through for the components that are relevant to it).
  122. Besides being inefficient, this also contributes to some of the persistent
  123. connection issues mentioned above, e.g., filling up the websocket write
  124. buffer and forcing the server to queue potentially several copies of a large
  125. value in memory.
  126. 5. **Client identity is tied to network address:** The Tendermint event API
  127. identifies each subscriber by a (Client ID, Query) pair. In the RPC service,
  128. the query is provided by the client, but the client ID is set to the TCP
  129. address of the client (typically "host:port" or "ip:port").
  130. This means that even if the server did _not_ drop subscriptions immediately
  131. when the websocket connection is closed, a client may not be able to
  132. reattach to its existing subscription. Dialing a new connection is likely
  133. to result in a different port (and, depending on their own proxy setup,
  134. possibly a different public IP).
  135. In isolation, this problem would be easy to work around with a new
  136. subscription parameter, but it would require several other changes to the
  137. handling of event subscriptions for that workaround to become useful.
  138. ---
  139. ## Decision
  140. To address the described problems, we will:
  141. 1. Introduce a new API for event subscription to the Tendermint RPC service.
  142. The proposed API is described in [Detailed Design](#detailed-design) below.
  143. 2. This new API will target the Tendermint v0.36 release, during which the
  144. current ("streaming") API will remain available as-is, but deprecated.
  145. 3. The streaming API will be entirely removed in release v0.37, which will
  146. require all users of event subscription to switch to the new API.
  147. > **Point for discussion:** Given that ABCI++ and PBTS are the main priorities
  148. > for v0.36, it would be fine to slip the first phase of this work to v0.37.
  149. > Unless there is a time problem, however, the proposed design does not disrupt
  150. > the work on ABCI++ or PBTS, and will not increase the scope of breaking
  151. > changes. Therefore the plan is to begin in v0.36 and slip only if necessary.
  152. ---
  153. ## Detailed Design
  154. ### Design Goals
  155. Specific goals of this design include:
  156. 1. Remove the need for a persistent connection to each subscription client.
  157. Subscribers should use the same HTTP request flow for event subscription
  158. requests as for other RPC calls.
  159. 2. The server retains minimal state (possibly none) per-subscriber. In
  160. particular:
  161. - The server does not buffer unconsumed writes nor queue undelivered events
  162. on a per-client basis.
  163. - A client that stalls or goes idle does not cost the server any resources.
  164. - Any event data that is buffered or stored is shared among _all_
  165. subscribers, and is not duplicated per client.
  166. 3. Slow clients have no impact (or minimal impact) on the rate of progress of
  167. the consensus algorithm, beyond the ambient overhead of servicing individual
  168. RPC requests.
  169. 4. Clients can tell when they have missed events matching their subscription,
  170. within some reasonable (configurable) window of time, and can "replay"
  171. events within that window to catch up.
  172. 5. Nice to have: It should be easy to use the event subscription API from
  173. existing standard tools and libraries, including command-line use for
  174. testing and experimentation.
  175. ### Definitions
  176. - The **event stream** of a node is a single, time-ordered, heterogeneous
  177. stream of event items.
  178. - Each **event item** comprises an **event datum** (for example, block header
  179. metadata for a new-block event), and zero or more optional **events**.
  180. - An **event** means the [ABCI `Event` data type][abci-event], which comprises
  181. a string type and zero or more string key-value **event attributes**.
  182. The use of the new terms "event item" and "event datum" is to avert confusion
  183. between the values that are published to the event bus (what we call here
  184. "event items") and the ABCI `Event` data type.
  185. - The node assigns each event item a unique identifier string called a
  186. **cursor**. A cursor must be unique among all events published by a single
  187. node, but it is not required to be unique globally across nodes.
  188. Cursors are time-ordered so that given event items A and B, if A was
  189. published before B, then cursor(A) < cursor(B) in lexicographic order.
  190. A minimum viable cursor implementation is a tuple consisting of a timestamp
  191. and a sequence number (e.g., `16CCC798FB5F4670-0123`). However, it may also
  192. be useful to append basic type information to a cursor, to allow efficient
  193. filtering (e.g., `16CCC87E91869050-0091:BeginBlock`).
  194. The initial implementation will use the minimum viable format.
  195. ### Discussion
  196. The node maintains an **event log**, a shared ordered record of the events
  197. published to its event bus within an operator-configurable time window. The
  198. initial implementation will store the event log in-memory, and the operator
  199. will be given two per-node configuration settings. Note, these names are
  200. provisional:
  201. - `[rpc] event-log-window-size`: A duration before the latest published event,
  202. during which the node will retain event items published. Setting this value
  203. to zero disables event subscription.
  204. - `[rpc] event-log-max-items`: A maximum number of event items that the node
  205. will retain within the time window. If the number of items exceeds this
  206. value, the node discardes the oldest items in the window. Setting this value
  207. to zero means that no limit is imposed on the number of items.
  208. The node will retain all events within the time window, provided they do not
  209. exceed the maximum number. These config parameters allow the operator to
  210. loosely regulate how much memory and storage the node allocates to the event
  211. log. The client can use the server reply to tell whether the events it wants
  212. are still available from the event log.
  213. The event log is shared among all subscribers to the node.
  214. > **Discussion point:** Should events persist across node restarts?
  215. >
  216. > The current event API does not persist events across restarts, so this new
  217. > design does not either. Note, however, that we may "spill" older event data
  218. > to disk as a way of controlling memory use. Such usage is ephemeral, however,
  219. > and does not need to be tracked as node data (e.g., it could be temp files).
  220. ### Query API
  221. To retrieve event data, the client will call the (new) RPC method `events`.
  222. The parameters of this method will correspond to the following Go types:
  223. ```go
  224. type EventParams struct {
  225. // Optional filter spec. If nil or empty, all items are eligible.
  226. Filter *Filter `json:"filter"`
  227. // The maximum number of eligible results to return.
  228. // If zero or negative, the server will report a default number.
  229. MaxResults int `json:"max_results"`
  230. // Return only items after this cursor. If empty, the limit is just
  231. // before the the beginning of the event log.
  232. After string `json:"after"`
  233. // Return only items before this cursor. If empty, the limit is just
  234. // after the head of the event log.
  235. Before string `json:"before"`
  236. // Wait for up to this long for events to be available.
  237. WaitTime time.Duration `json:"wait_time"`
  238. }
  239. type Filter struct {
  240. Query string `json:"query"`
  241. }
  242. ```
  243. > **Discussion point:** The initial implementation will not cache filter
  244. > queries for the client. If this turns out to be a performance issue in
  245. > production, the service can keep a small shared cache of compiled queries.
  246. > Given the improvements from #7319 et seq., this should not be necessary.
  247. > **Discussion point:** For the initial implementation, the new API will use
  248. > the existing query language as-is. Future work may extend the Filter message
  249. > with a more structured and/or expressive query surface, but that is beyond
  250. > the scope of this design.
  251. The semantics of the request are as follows: An item in the event log is
  252. **eligible** for a query if:
  253. - It is newer than the `after` cursor (if set).
  254. - It is older than the `before` cursor (if set).
  255. - It matches the filter (if set).
  256. Among the eligible items in the log, the server returns up to `max_results` of
  257. the newest items, in reverse order of cursor. If `max_results` is unset the
  258. server chooses a number to return, and will cap `max_results` at a sensible
  259. limit.
  260. The `wait_time` parameter is used to effect polling. If `before` is empty and
  261. no items are available, the server will wait for up to `wait_time` for matching
  262. items to arrive at the head of the log. If `wait_time` is zero or negative, the
  263. server will wait for a default (positive) interval.
  264. If `before` non-empty, `wait_time` is ignored: new results are only added to
  265. the head of the log, so there is no need to wait. This allows the client to
  266. poll for new data, and "page" backward through matching event items. This is
  267. discussed in more detail below.
  268. The server will set a sensible cap on the maximum `wait_time`, overriding
  269. client-requested intervals longer than that.
  270. A successful reply from the `events` request corresponds to the following Go
  271. types:
  272. ```go
  273. type EventReply struct {
  274. // The items matching the request parameters, from newest
  275. // to oldest, if any were available within the timeout.
  276. Items []*EventItem `json:"items"`
  277. // This is true if there is at least one older matching item
  278. // available in the log that was not returned.
  279. More bool `json:"more"`
  280. // The cursor of the oldest item in the log at the time of this reply,
  281. // or "" if the log is empty.
  282. Oldest string `json:"oldest"`
  283. // The cursor of the newest item in the log at the time of this reply,
  284. // or "" if the log is empty.
  285. Newest string `json:"newest"`
  286. }
  287. type EventItem struct {
  288. // The cursor of this item.
  289. Cursor string `json:"cursor"`
  290. // The encoded event data for this item.
  291. // The type identifies the structure of the value.
  292. Data struct {
  293. Type string `json:"type"`
  294. Value json.RawMessage `json:"value"`
  295. } `json:"data"`
  296. }
  297. ```
  298. The `oldest` and `newest` fields of the reply report the cursors of the oldest
  299. and newest items (of any kind) recorded in the event log at the time of the
  300. reply, or are `""` if the log is empty.
  301. The `data` field contains the type-specific event datum. The datum carries any
  302. ABCI events that may have been defined.
  303. > **Discussion point**: Based on [issue #7273][i7273], I did not include a
  304. > separate field in the response for the ABCI events, since it duplicates data
  305. > already stored elsewhere in the event data.
  306. The semantics of the reply are as follows:
  307. - If `items` is non-empty:
  308. - Items are ordered from newest to oldest.
  309. - If `more` is true, there is at least one additional, older item in the
  310. event log that was not returned (in excess of `max_results`).
  311. In this case the client can fetch the next page by setting `before` in a
  312. new request, to the cursor of the oldest item fetched (i.e., the last one
  313. in `items`).
  314. - Otherwise (if `more` is false), all the matching results have been
  315. reported (pagination is complete).
  316. - The first element of `items` identifies the newest item considered.
  317. Subsequent poll requests can set `after` to this cursor to skip items
  318. that were already retrieved.
  319. - If `items` is empty:
  320. - If the `before` was set in the request, there are no further eligible
  321. items for this query in the log (pagination is complete).
  322. This is just a safety case; the client can detect this without issuing
  323. another call by consulting the `more` field of the previous reply.
  324. - If the `before` was empty in the request, no eligible items were
  325. available before the `wait_time` expired. The client may poll again to
  326. wait for more event items.
  327. A client can store cursor values to detect data loss and to recover from
  328. crashes and connectivity issues:
  329. - After a crash, the client requests events after the newest cursor it has
  330. seen. If the reply indicates that cursor is no longer in range, the client
  331. may (conservatively) conclude some event data may have been lost.
  332. - On the other hand, if it _is_ in range, the client can then page back through
  333. the results that it missed, and then resume polling. As long as its recovery
  334. cursor does not age out before it finishes, the client can be sure it has all
  335. the relevant results.
  336. ### Other Notes
  337. - The new API supports two general "modes" of operation:
  338. 1. In ordinary operation, clients will **long-poll** the head of the event
  339. log for new events matching their criteria (by setting a `wait_time` and
  340. no `before`).
  341. 2. If there are more events than the client requested, or if the client needs
  342. to to read older events to recover from a stall or crash, clients will
  343. **page** backward through the event log (by setting `before` and `after`).
  344. - While the new API requires explicit polling by the client, it makes better
  345. use of the node's existing HTTP infrastructure (e.g., connection pools).
  346. Moreover, the direct implementation is easier to use from standard tools and
  347. client libraries for HTTP and JSON-RPC.
  348. Explicit polling does shift the burden of timeliness to the client. That is
  349. arguably preferable, however, given that the RPC service is ancillary to the
  350. node's primary goal, viz., consensus. The details of polling can be easily
  351. hidden from client applications with simple libraries.
  352. - The format of a cursor is considered opaque to the client. Clients must not
  353. parse cursor values, but they may rely on their ordering properties.
  354. - To maintain the event log, the server must prune items outside the time
  355. window and in excess of the item limit.
  356. The initial implementation will do this by checking the tail of the event log
  357. after each new item is published. If the number of items in the log exceeds
  358. the item limit, it will delete oldest items until the log is under the limit;
  359. then discard any older than the time window before the latest.
  360. To minimize coordination interference between the publisher (the event bus)
  361. and the subcribers (the `events` service handlers), the event log will be
  362. stored as a persistent linear queue with shared structure (a cons list). A
  363. single reader-writer mutex will guard the "head" of the queue where new
  364. items are published:
  365. - **To publish a new item**, the publisher acquires the write lock, conses a
  366. new item to the front of the existing queue, and replaces the head pointer
  367. with the new item.
  368. - **To scan the queue**, a reader acquires the read lock, captures the head
  369. pointer, and then releases the lock. The rest of its request can be served
  370. without holding a lock, since the queue structure will not change.
  371. When a reader wants to wait, it will yield the lock and wait on a condition
  372. that is signaled when the publisher swings the pointer.
  373. - **To prune the queue**, the publisher (who is the sole writer) will track
  374. the queue length and the age of the oldest item separately. When the
  375. length and or age exceed the configured bounds, it will construct a new
  376. queue spine on the same items, discarding out-of-band values.
  377. Pruning can be done while the publisher already holds the write lock, or
  378. could be done outside the lock entirely: Once the new queue is constructed,
  379. the lock can be re-acquired to swing the pointer. This costs some extra
  380. allocations for the cons cells, but avoids duplicating any event items.
  381. The pruning step is a simple linear scan down the first (up to) max-items
  382. elements of the queue, to find the breakpoint of age and length.
  383. Moreover, the publisher can amortize the cost of pruning by item count, if
  384. necessary, by pruning length "more aggressively" than the configuration
  385. requires (e.g., reducing to 3/4 of the maximum rather than 1/1).
  386. The state of the event log before the publisher acquires the lock:
  387. ![Before publish and pruning](./img/adr-075-log-before.png)
  388. After the publisher has added a new item and pruned old ones:
  389. ![After publish and pruning](./img/adr-075-log-after.png)
  390. ### Migration Plan
  391. This design requires that clients eventually migrate to the new event
  392. subscription API, but provides a full release cycle with both APIs in place to
  393. make this burden more tractable. The migration strategy is broadly:
  394. **Phase 1**: Release v0.36.
  395. - Implement the new `events` endpoint, keeping the existing methods as they are.
  396. - Update the Go clients to support the new `events` endpoint, and handle polling.
  397. - Update the old endpoints to log annoyingly about their own deprecation.
  398. - Write tutorials about how to migrate client usage.
  399. At or shortly after release, we should proactively update the Cosmos SDK to use
  400. the new API, to remove a disincentive to upgrading.
  401. **Phase 2**: Release v0.37
  402. - During development, we should actively seek out any existing users of the
  403. streaming event subscription API and help them migrate.
  404. - Possibly also: Spend some time writing clients for JS, Rust, et al.
  405. - Release: Delete the old implementation and all the websocket support code.
  406. > **Discussion point**: Even though the plan is to keep the existing service,
  407. > we might take the opportunity to restrict the websocket endpoint to _only_
  408. > the event streaming service, removing the other endpoints. To minimize the
  409. > disruption for users in the v0.36 cycle, I have decided not to do this for
  410. > the first phase.
  411. >
  412. > If we wind up pushing this design into v0.37, however, we should re-evaulate
  413. > this partial turn-down of the websocket.
  414. ### Future Work
  415. - This design does not immediately address the problem of allowing the client
  416. to control which data are reported back for event items. That concern is
  417. deferred to future work. However, it would be straightforward to extend the
  418. filter and/or the request parameters to allow more control.
  419. - The node currently stores a subset of event data (specifically the block and
  420. transaction events) for use in reindexing. While these data are redundant
  421. with the event log described in this document, they are not sufficient to
  422. cover event subscription, as they omit other event types.
  423. In the future we should investigate consolidating or removing event data from
  424. the state store entirely. For now this issue is out of scope for purposes of
  425. updating the RPC API. We may be able to piggyback on the database unification
  426. plans (see [RFC 001][rfc001]) to store the event log separately, so its
  427. pruning policy does not need to be tied to the block and state stores.
  428. - This design reuses the existing filter query language from the old API. In
  429. the future we may want to use a more structured and/or expressive query. The
  430. Filter object can be extended with more fields as needed to support this.
  431. - Some users have trouble communicating with the RPC service because of
  432. configuration problems like improperly-set CORS policies. While this design
  433. does not address those issues directly, we might want to revisit how we set
  434. policies in the RPC service to make it less susceptible to confusing errors
  435. caused by misconfiguration.
  436. ---
  437. ## Consequences
  438. - ✅ Reduces the number of transport options for RPC. Supports [RFC 002][rfc002].
  439. - ️✅ Removes the primary non-standard use of JSON-RPC.
  440. - ⛔️ Forces clients to migrate to a different API (eventually).
  441. - ↕️ API requires clients to poll, but this reduces client state on the server.
  442. - ↕️ We have to maintain both implementations for a whole release, but this
  443. gives clients time to migrate.
  444. ---
  445. ## Alternative Approaches
  446. The following alternative approaches were considered:
  447. 1. **Leave it alone.** Since existing tools mostly already work with the API as
  448. it stands today, we could leave it alone and do our best to improve its
  449. performance and reliability.
  450. Based on many issues reported by users and node operators (e.g.,
  451. [#3380][i3380], [#6439][i6439], [#6729][i6729], [#7247][i7247]), the
  452. problems described here affect even the existing use that works. Investing
  453. further incremental effort in the existing API is unlikely to address these
  454. issues.
  455. 2. **Design a better streaming API.** Instead of polling, we might try to
  456. design a better "streaming" API for event subscription.
  457. A significant advantage of switching away from streaming is to remove the
  458. need for persistent connections between the node and subscribers. A new
  459. streaming protocol design would lose that advantage, and would still need a
  460. way to let clients recover and replay.
  461. This approach might look better if we decided to use a different protocol
  462. for event subscription, say gRPC instead of JSON-RPC. That choice, however,
  463. would be just as breaking for existing clients, for marginal benefit.
  464. Moreover, this option increases both the complexity and the resource cost on
  465. the node implementation.
  466. Given that resource consumption and complexity are important considerations,
  467. this option was not chosen.
  468. 3. **Defer to an external event broker.** We might remove the entire event
  469. subscription infrastructure from the node, and define an optional interface
  470. to allow the node to publish all its events to an external event broker,
  471. such as Apache Kafka.
  472. This has the advantage of greatly simplifying the node, but at a great cost
  473. to the node operator: To enable event subscription in this design, the
  474. operator has to stand up and maintain a separate process in communion with
  475. the node, and configuration changes would have to be coordinated across
  476. both.
  477. Moreover, this approach would be highly disruptive to existing client use,
  478. and migration would probably require switching to third-party libraries.
  479. Despite the potential benefits for the node itself, the costs to operators
  480. and clients seems too large for this to be the best option.
  481. Publishing to an external event broker might be a worthwhile future project,
  482. if there is any demand for it. That decision is out of scope for this design,
  483. as it interacts with the design of the indexer as well.
  484. ---
  485. ## References
  486. - [RFC 006: Event Subscription][rfc006]
  487. - [Tendermint RPC service][rpc-service]
  488. - [Event query grammar][query-grammar]
  489. - [RFC 6455: The WebSocket protocol][ws]
  490. - [JSON-RPC 2.0 Specification][jsonrpc2]
  491. - [Nginx proxy server][nginx]
  492. - [Proxying websockets][rp-ws]
  493. - [Extension modules][ng-xm]
  494. - [FastCGI][fcgi]
  495. - [RFC 001: Storage Engines & Database Layer][rfc001]
  496. - [RFC 002: Interprocess Communication in Tendermint][rfc002]
  497. - Issues:
  498. - [rpc/client: test that client resubscribes upon disconnect][i3380] (#3380)
  499. - [Too high memory usage when creating many events subscriptions][i6439] (#6439)
  500. - [Tendermint emits events faster than clients can pull them][i6729] (#6729)
  501. - [indexer: unbuffered event subscription slow down the consensus][i7247] (#7247)
  502. - [rpc: remove duplication of events when querying][i7273] (#7273)
  503. [rfc006]: https://github.com/tendermint/tendermint/blob/master/docs/rfc/rfc-006-event-subscription.md
  504. [rpc-service]: https://github.com/tendermint/tendermint/blob/master/rpc/openapi/openapi.yaml
  505. [query-grammar]: https://pkg.go.dev/github.com/tendermint/tendermint@master/internal/pubsub/query/syntax
  506. [ws]: https://datatracker.ietf.org/doc/html/rfc6455
  507. [jsonrpc2]: https://www.jsonrpc.org/specification
  508. [nginx]: https://nginx.org/en/docs/
  509. [fcgi]: http://www.mit.edu/~yandros/doc/specs/fcgi-spec.html
  510. [rp-ws]: https://nginx.org/en/docs/http/websocket.html
  511. <!-- markdown-link-check-disable-next-line -->
  512. [ng-xm]: https://www.nginx.com/resources/wiki/extending/
  513. [abci-event]: https://pkg.go.dev/github.com/tendermint/tendermint/abci/types#Event
  514. [rfc001]: https://github.com/tendermint/tendermint/blob/master/docs/rfc/rfc-001-storage-engine.rst
  515. [rfc002]: https://github.com/tendermint/tendermint/blob/master/docs/rfc/rfc-002-ipc-ecosystem.md
  516. [i3380]: https://github.com/tendermint/tendermint/issues/3380
  517. [i6439]: https://github.com/tendermint/tendermint/issues/6439
  518. [i6729]: https://github.com/tendermint/tendermint/issues/6729
  519. [i7247]: https://github.com/tendermint/tendermint/issues/7247
  520. [i7273]: https://github.com/tendermint/tendermint/issues/7273