github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/docs/architecture/adr-075-rpc-subscription.md (about)

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