From f64d64a7b540d5422a405c9b2984eae8e94d2681 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Wed, 26 Jan 2022 06:19:32 -0800 Subject: [PATCH] ADR 074: RPC Event Subscription Interface (#7677) Status: Proposed. --- docs/architecture/README.md | 1 + docs/architecture/adr-075-rpc-subscription.md | 683 ++++++++++++++++++ docs/architecture/img/adr-075-log-after.png | Bin 0 -> 22864 bytes docs/architecture/img/adr-075-log-before.png | Bin 0 -> 15057 bytes 4 files changed, 684 insertions(+) create mode 100644 docs/architecture/adr-075-rpc-subscription.md create mode 100644 docs/architecture/img/adr-075-log-after.png create mode 100644 docs/architecture/img/adr-075-log-before.png diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 1a97addfa..6349b9c3c 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -102,3 +102,4 @@ Note the context/background should be written in the present tense. - [ADR-069: Node Initialization](./adr-069-flexible-node-initialization.md) - [ADR-071: Proposer-Based Timestamps](adr-071-proposer-based-timestamps.md) - [ADR-074: Migrate Timeout Parameters to Consensus Parameters](./adr-074-timeout-params.md) +- [ADR-075: RPC Event Subscription Interface](./adr-075-rpc-subscription.md) diff --git a/docs/architecture/adr-075-rpc-subscription.md b/docs/architecture/adr-075-rpc-subscription.md new file mode 100644 index 000000000..405119e48 --- /dev/null +++ b/docs/architecture/adr-075-rpc-subscription.md @@ -0,0 +1,683 @@ +# ADR 075: RPC Event Subscription Interface + +## Changelog + +- 22-Jan-2022: Updated and expanded (@creachadair). +- 20-Nov-2021: Initial draft (@creachadair). + +--- +## Status + +Proposed + +--- +## Background & Context + +For context, see [RFC 006: Event Subscription][rfc006]. + +The [Tendermint RPC service][rpc-service] permits clients to subscribe to the +event stream generated by a consensus node. This allows clients to observe the +state of the consensus network, including details of the consensus algorithm +state machine, proposals, transaction delivery, and block completion. The +application may also attach custom key-value attributes to events to expose +application-specific details to clients. + +The event subscription API in the RPC service currently comprises three methods: + +1. `subscribe`: A request to subscribe to the events matching a specific + [query expression][query-grammar]. Events can be filtered by their key-value + attributes, including custom attributes provided by the application. + +2. `unsubscribe`: A request to cancel an existing subscription based on its + query expression. + +3. `unsubscribe_all`: A request to cancel all existing subscriptions belonging + to the client. + +There are some important technical and UX issues with the current RPC event +subscription API. The rest of this ADR outlines these problems in detail, and +proposes a new API scheme intended to address them. + +### Issue 1: Persistent connections + +To subscribe to a node's event stream, a client needs a persistent connection +to the node. Unlike the other methods of the service, for which each call is +serviced by a short-lived HTTP round trip, subscription delivers a continuous +stream of events to the client by hijacking the HTTP channel for a websocket. +The stream (and hence the HTTP request) persists until either the subscription +is explicitly cancelled, or the connection is closed. + +There are several problems with this API: + +1. **Expensive per-connection state**: The server must maintain a substantial + amount of state per subscriber client: + + - The current implementation uses a [WebSocket][ws] for each active + subscriber. The connection must be maintained even if there are no + matching events for a given client. + + The server can drop idle connections to save resources, but doing so + terminates all subscriptions on those connections and forces those clients + to re-connect, adding additional resource churn for the server. + + - In addition, the server maintains a separate buffer of undelivered events + for each client. This is to reduce the dual risks that a client will miss + events, and that a slow client could "push back" on the publisher, + impeding the progress of consensus. + + Because event traffic is quite bursty, queues can potentially take up a + lot of memory. Moreover, each subscriber may have a different filter + query, so the server winds up having to duplicate the same events among + multiple subscriber queues. Not only does this add memory pressure, but it + does so most at the worst possible time, i.e., when the server is already + under load from high event traffic. + +2. **Operational access control is difficult**: The server's websocket + interface exposes _all_ the RPC service endpoints, not only the subscription + methods. This includes methods that allow callers to inject arbitrary + transactions (`broadcast_tx_*`) and evidence (`broadcast_evidence`) into the + network, remove transactions (`remove_tx`), and request arbitrary amounts of + chain state. + + Filtering requests to the GET endpoint is straightforward: A reverse proxy + like [nginx][nginx] can easily filter methods by URL path. Filtering POST + requests takes a bit more work, but can be managed with a filter program + that speaks [FastCGI][fcgi] and parses JSON-RPC request bodies. + + Filtering the websocket interface requires a dedicated proxy implementation. + Although nginx can [reverse-proxy websockets][rp-ws], it does not support + filtering websocket traffic via FastCGI. The operator would need to either + implement a custom [nginx extension module][ng-xm] or build and run a + standalone proxy that implements websocket and filters each session. Apart + from the work, this also makes the system even more resource intensive, as + well as introducing yet another connection that could potentially time out + or stall on full buffers. + + Even for the simple case of restricting access to only event subscription, + there is no easy solution currently: Once a caller has access to the + websocket endpoint, it has complete access to the RPC service. + +### Issue 2: Inconvenient client API + +The subscription interface has some inconvenient features for the client as +well as the server. These include: + +1. **Non-standard protocol:** The RPC service is mostly [JSON-RPC 2.0][jsonrpc2], + but the subscription interface diverges from the standard. + + In a standard JSON-RPC 2.0 call, the client initiates a request to the + server with a unique ID, and the server concludes the call by sending a + reply for that ID. The `subscribe` implementation, however, sends multiple + responses to the client's request: + + - The client sends `subscribe` with some ID `x` and the desired query + + - The server responds with ID `x` and an empty confirmation response. + + - The server then (repeatedly) sends event result responses with ID `x`, one + for each item with a matching event. + + Standard JSON-RPC clients will reject the subsequent replies, as they + announce a request ID (`x`) that is already complete. This means a caller + has to implement Tendermint-specific handling for these responses. + + Moreover, the result format is different between the initial confirmation + and the subsequent responses. This means a caller has to implement special + logic for decoding the first response versus the subsequent ones. + +2. **No way to detect data loss:** The subscriber connection can be terminated + for many reasons. Even ignoring ordinary network issues (e.g., packet loss): + + - The server will drop messages and/or close the websocket if its write + buffer fills, or if the queue of undelivered matching events is not + drained fast enough. The client has no way to discover that messages were + dropped even if the connection remains open. + + - Either the client or the server may close the websocket if the websocket + PING and PONG exchanges are not handled correctly, or frequently enough. + Even if correctly implemented, this may fail if the system is under high + load and cannot service those control messages in a timely manner. + + When the connection is terminated, the server drops all the subscriptions + for that client (as if it had called `unsubscribe_all`). Even if the client + reconnects, any events that were published during the period between the + disconnect and re-connect and re-subscription will be silently lost, and the + client has no way to discover that it missed some relevant messages. + +3. **No way to replay old events:** Even if a client knew it had missed some + events (due to a disconnection, for example), the API provides no way for + the client to "play back" events it may have missed. + +4. **Large response sizes:** Some event data can be quite large, and there can + be substantial duplication across items. The API allows the client to select + _which_ events are reported, but has no way to control which parts of a + matching event it wishes to receive. + + This can be costly on the server (which has to marshal those data into + JSON), the network, and the client (which has to unmarshal the result and + then pick through for the components that are relevant to it). + + Besides being inefficient, this also contributes to some of the persistent + connection issues mentioned above, e.g., filling up the websocket write + buffer and forcing the server to queue potentially several copies of a large + value in memory. + +5. **Client identity is tied to network address:** The Tendermint event API + identifies each subscriber by a (Client ID, Query) pair. In the RPC service, + the query is provided by the client, but the client ID is set to the TCP + address of the client (typically "host:port" or "ip:port"). + + This means that even if the server did _not_ drop subscriptions immediately + when the websocket connection is closed, a client may not be able to + reattach to its existing subscription. Dialing a new connection is likely + to result in a different port (and, depending on their own proxy setup, + possibly a different public IP). + + In isolation, this problem would be easy to work around with a new + subscription parameter, but it would require several other changes to the + handling of event subscriptions for that workaround to become useful. + +--- +## Decision + +(pending) + +To address the described problems, we will: + +1. Introduce a new API for event subscription to the Tendermint RPC service. + The proposed API is described in [Detailed Design](#detailed-design) below. + +2. This new API will target the Tendermint v0.36 release, during which the + current ("streaming") API will remain available as-is, but deprecated. + +3. The streaming API will be entirely removed in release v0.37, which will + require all users of event subscription to switch to the new API. + +> **Point for discussion:** Given that ABCI++ and PBTS are the main priorities +> for v0.36, it would be fine to slip the first phase of this work to v0.37. +> Unless there is a time problem, however, the proposed design does not disrupt +> the work on ABCI++ or PBTS, and will not increase the scope of breaking +> changes. Therefore the plan is to begin in v0.36 and slip only if necessary. + +--- +## Detailed Design + +### Design Goals + +Specific goals of this design include: + +1. Remove the need for a persistent connection to each subscription client. + Subscribers should use the same HTTP request flow for event subscription + requests as for other RPC calls. + +2. The server retains minimal state (possibly none) per-subscriber. In + particular: + + - The server does not buffer unconsumed writes nor queue undelivered events + on a per-client basis. + - A client that stalls or goes idle does not cost the server any resources. + - Any event data that is buffered or stored is shared among _all_ + subscribers, and is not duplicated per client. + +3. Slow clients have no impact (or minimal impact) on the rate of progress of + the consensus algorithm, beyond the ambient overhead of servicing individual + RPC requests. + +4. Clients can tell when they have missed events matching their subscription, + within some reasonable (configurable) window of time, and can "replay" + events within that window to catch up. + +5. Nice to have: It should be easy to use the event subscription API from + existing standard tools and libraries, including command-line use for + testing and experimentation. + +### Definitions + +- The **event stream** of a node is a single, time-ordered, heterogeneous + stream of event items. + +- Each **event item** comprises an **event datum** (for example, block header + metadata for a new-block event), and zero or more optional **events**. + +- An **event** means the [ABCI `Event` data type][abci-event], which comprises + a string type and zero or more string key-value **event attributes**. + + The use of the new terms "event item" and "event datum" is to avert confusion + between the values that are published to the event bus (what we call here + "event items") and the ABCI `Event` data type. + +- The node assigns each event item a unique identifier string called a + **cursor**. A cursor must be unique among all events published by a single + node, but it is not required to be unique globally across nodes. + + Cursors are time-ordered so that given event items A and B, if A was + published before B, then cursor(A) < cursor(B) in lexicographic order. + + A minimum viable cursor implementation is a tuple consisting of a timestamp + and a sequence number (e.g., `16CCC798FB5F4670-0123`). However, it may also + be useful to append basic type information to a cursor, to allow efficient + filtering (e.g., `16CCC87E91869050-0091:BeginBlock`). + + The initial implementation will use the minimum viable format. + +### Discussion + +The node maintains an **event log**, a shared ordered record of the events +published to its event bus within an operator-configurable time window. The +initial implementation will store the event log in-memory, and the operator +will be given two per-node configuration settings. Note, these names are +provisional: + +- `[event-subscription] time-window`: A duration before present during which the + node will retain event items published. Setting this value to zero disables + event subscription. + +- `[event-subscription] max-items`: A maximum number of event items that the + node will retain within the time window. If the number of items exceeds this + value, the node discardes the oldest items in the window. Setting this value + to zero means that no limit is imposed on the number of items. + +The node will retain all events within the time window, provided they do not +exceed the maximum number. These config parameters allow the operator to +loosely regulate how much memory and storage the node allocates to the event +log. The client can use the server reply to tell whether the events it wants +are still available from the event log. + +The event log is shared among all subscribers to the node. + +> **Discussion point:** Should events persist across node restarts? +> +> The current event API does not persist events across restarts, so this new +> design does not either. Note, however, that we may "spill" older event data +> to disk as a way of controlling memory use. Such usage is ephemeral, however, +> and does not need to be tracked as node data (e.g., it could be temp files). + +### Query API + +To retrieve event data, the client will call the (new) RPC method `events`. +The parameters of this method will correspond to the following Go types: + +```go +type EventParams struct { + // Optional filter spec. If nil or empty, all items are eligible. + Filter *Filter `json:"filter"` + + // The maximum number of eligible results to return. + // If zero or negative, the server will report a default number. + MaxResults int `json:"max_results"` + + // Return only items after this cursor. If empty, the limit is just + // before the the beginning of the event log. + After string `json:"after_item"` + + // Return only items before this cursor. If empty, the limit is just + // after the head of the event log. + Before string `json:"before_item"` + + // Wait for up to this long for events to be available. + WaitTime time.Duration `json:"wait_time"` +} + +type Filter struct { + Query string `json:"query"` +} +``` + +> **Discussion point:** The initial implementation will not cache filter +> queries for the client. If this turns out to be a performance issue in +> production, the service can keep a small shared cache of compiled queries. +> Given the improvements from #7319 et seq., this should not be necessary. + +> **Discussion point:** For the initial implementation, the new API will use +> the existing query language as-is. Future work may extend the Filter message +> with a more structured and/or expressive query surface, but that is beyond +> the scope of this design. + +The semantics of the request are as follows: An item in the event log is +**eligible** for a query if: + +- It is newer than the `after_item` cursor (if set). +- It is older than the `before_item` cursor (if set). +- It matches the filter (if set). + +Among the eligible items in the log, the server returns up to `max_results` of +the newest items, in reverse order of cursor. If `max_results` is unset the +server chooses a number to return, and will cap `max_results` at a sensible +limit. + +The `wait_time` parameter is used to effect polling. If `before_item` is empty, +the server will wait for up to `wait_time` for additional items, if there are +fewer than `max_results` eligible results in the log. If `wait_time` is zero, +the server will return whatever eligible items are available immediately. + +If `before_item` non-empty, `wait_time` is ignored: new results are only added +to the head of the log, so there is no need to wait. This allows the client to +poll for new data, and "page" backward through matching event items. This is +discussed in more detail below. + +The server will set a sensible cap on the maximum `wait_time`, overriding +client-requested intervals longer than that. + +A successful reply from the `events` request corresponds to the following Go +types: + +```go +type EventReply struct { + // The items matching the request parameters, from newest + // to oldest, if any were available within the timeout. + Items []*EventItem `json:"items"` + + // This is true if there is at least one older matching item + // available in the log that was not returned. + More bool `json:"more"` + + // The cursor of the oldest item in the log at the time of this reply, + // or "" if the log is empty. + Oldest string `json:"oldest_item"` + + // The cursor of the newest item in the log at the time of this reply, + // or "" if the log is empty. + Newest string `json:"newest_item"` +} + +type EventItem struct { + // The cursor of this item. + Cursor string `json:"cursor"` + + // The encoded event data for this item. + // The type identifies the structure of the value. + Data struct { + Type string `json:"type"` + Value json.RawMessage `json:"value"` + } `json:"data"` +} +``` + +The `oldest_item` and `newest_item` fields of the reply report the cursors of +the oldest and newest items (of any kind) recorded in the event log at the time +of the reply, or are `""` if the log is empty. + +The `data` field contains the type-specific event datum, and `events` contain +the ABCI events, if any were defined. + +> **Discussion point**: Based on [issue #7273][i7273], I did not include a +> separate field in the response for the ABCI events, since it duplicates data +> already stored elsewhere in the event data. + +The semantics of the reply are as follows: + +- If `items` is non-empty: + + - Items are ordered from newest to oldest. + + - If `more` is true, there is at least one additional, older item in the + event log that was not returned (in excess of `max_results`). + + In this case the client can fetch the next page by setting `before_item` + in a new request, to the cursor of the oldest item fetched (i.e., the + last one in `items`). + + - Otherwise (if `more` is false), all the matching results have been + reported (pagination is complete). + + - The first element of `items` identifies the newest item considered. + Subsequent poll requests can set `after_item` to this cursor to skip + items that were already retrieved. + +- If `items` is empty: + + - If the `before_item` was set in the request, there are no further + eligible items for this query in the log (pagination is complete). + + This is just a safety case; the client can detect this without issuing + another call by consulting the `more` field of the previous reply. + + - If the `before_item` was empty in the request, no eligible items were + available before the `wait_time` expired. The client may poll again to + wait for more event items. + +A client can store cursor values to detect data loss and to recover from +crashes and connectivity issues: + +- After a crash, the client requests events after the newest cursor it has + seen. If the reply indicates that cursor is no longer in range, the client + may (conservatively) conclude some event data may have been lost. + +- On the other hand, if it _is_ in range, the client can then page back through + the results that it missed, and then resume polling. As long as its recovery + cursor does not age out before it finishes, the client can be sure it has all + the relevant results. + +### Other Notes + +- The new API supports two general "modes" of operation: + + 1. In ordinary operation, clients will **long-poll** the head of the event + log for new events matching their criteria (by setting a `wait_time` and + no `before_item`). + + 2. If there are more events than the client requested, or if the client needs + to to read older events to recover from a stall or crash , clients will + **page** backward through the event log (by setting `before_item` and + possibly `after_item`). + +- While the new API requires explicit polling by the client, it makes better + use of the node's existing HTTP infrastructure (e.g., connection pools). + Moreover, the direct implementation is easier to use from standard tools and + client libraries for HTTP and JSON-RPC. + + Explicit polling does shift the burden of timeliness to the client. That is + arguably preferable, however, given that the RPC service is ancillary to the + node's primary goal, viz., consensus. The details of polling can be easily + hidden from client applications with simple libraries. + +- The format of a cursor is considered opaque to the client. Clients must not + parse cursor values, but they may rely on their ordering properties. + +- To maintain the event log, the server must prune items outside the time + window and in excess of the item limit. + + The initial implementation will do this by checking the tail of the event log + after each new item is published. If the number of items in the log exceeds + the item limit, it will delete oldest items until the log is under the limit; + then discard any older than the time window before present. + + To minimize coordination interference between the publisher (the event bus) + and the subcribers (the `events` service handlers), the event log will be + stored as a persistent linear queue with shared structure (a cons list). A + single reader-writer mutex will guard the "head" of the queue where new + items are published: + + - **To publish a new item**, the publisher acquires the write lock, conses a + new item to the front of the existing queue, and replaces the head pointer + with the new item. + + - **To scan the queue**, a reader acquires the read lock, captures the head + pointer, and then releases the lock. The rest of its request can be served + without holding a lock, since the queue structure will not change. + + When a reader wants to wait, it will yield the lock and wait on a condition + that is signaled when the publisher swings the pointer. + + - **To prune the queue**, the publisher (who is the sole writer) will track + the queue length and the age of the oldest item separately. When the + length and or age exceed the configured bounds, it will construct a new + queue spine on the same items, discarding out-of-band values. + + Pruning can be done while the publisher already holds the write lock, or + could be done outside the lock entirely: Once the new queue is constructed, + the lock can be re-acquired to swing the pointer. This costs some extra + allocations for the cons cells, but avoids duplicating any event items. + The pruning step is a simple linear scan down the first (up to) max-items + elements of the queue, to find the breakpoint of age and length. + + Moreover, the publisher can amortize the cost of pruning by item count, if + necessary, by pruning length "more aggressively" than the configuration + requires (e.g., reducing to 3/4 of the maximum rather than 1/1). + + The state of the event log before the publisher acquires the lock: + ![Before publish and pruning](./img/adr-075-log-before.png) + + After the publisher has added a new item and pruned old ones: + ![After publish and pruning](./img/adr-075-log-after.png) + +### Migration Plan + +This design requires that clients eventually migrate to the new event +subscription API, but provides a full release cycle with both APIs in place to +make this burden more tractable. The migration strategy is broadly: + +**Phase 1**: Release v0.36. + +- Implement the new `events` endpoint, keeping the existing methods as they are. +- Update the Go clients to support the new `events` endpoint, and handle polling. +- Update the old endpoints to log annoyingly about their own deprecation. +- Write tutorials about how to migrate client usage. + +At or shortly after release, we should proactively update the Cosmos SDK to use +the new API, to remove a disincentive to upgrading. + +**Phase 2**: Release v0.37 + +- During development, we should actively seek out any existing users of the + streaming event subscription API and help them migrate. +- Possibly also: Spend some time writing clients for JS, Rust, et al. +- Release: Delete the old implementation and all the websocket support code. + +> **Discussion point**: Even though the plan is to keep the existing service, +> we might take the opportunity to restrict the websocket endpoint to _only_ +> the event streaming service, removing the other endpoints. To minimize the +> disruption for users in the v0.36 cycle, I have decided not to do this for +> the first phase. +> +> If we wind up pushing this design into v0.37, however, we should re-evaulate +> this partial turn-down of the websocket. + +### Future Work + +- This design does not immediately address the problem of allowing the client + to control which data are reported back for event items. That concern is + deferred to future work. However, it would be straightforward to extend the + filter and/or the request parameters to allow more control. + +- The node currently stores a subset of event data (specifically the block and + transaction events) for use in reindexing. While these data are redundant + with the event log described in this document, they are not sufficient to + cover event subscription, as they omit other event types. + + In the future we should investigate consolidating or removing event data from + the state store entirely. For now this issue is out of scope for purposes of + updating the RPC API. We may be able to piggyback on the database unification + plans (see [RFC 001][rfc001]) to store the event log separately, so its + pruning policy does not need to be tied to the block and state stores. + +- This design reuses the existing filter query language from the old API. In + the future we may want to use a more structured and/or expressive query. The + Filter object can be extended with more fields as needed to support this. + +- Some users have trouble communicating with the RPC service because of + configuration problems like improperly-set CORS policies. While this design + does not address those issues directly, we might want to revisit how we set + policies in the RPC service to make it less susceptible to confusing errors + caused by misconfiguration. + +--- +## Consequences + +- ✅ Reduces the number of transport options for RPC. Supports [RFC 002][rfc002]. +- ️✅ Removes the primary non-standard use of JSON-RPC. +- ⛔️ Forces clients to migrate to a different API (eventually). +- ↕️ API requires clients to poll, but this reduces client state on the server. +- ↕️ We have to maintain both implementations for a whole release, but this + gives clients time to migrate. + +--- +## Alternative Approaches + +The following alternative approaches were considered: + +1. **Leave it alone.** Since existing tools mostly already work with the API as + it stands today, we could leave it alone and do our best to improve its + performance and reliability. + + Based on many issues reported by users and node operators (e.g., + [#3380][i3380], [#6439][i6439], [#6729][i6729], [#7247][i7247]), the + problems described here affect even the existing use that works. Investing + further incremental effort in the existing API is unlikely to address these + issues. + +2. **Design a better streaming API.** Instead of polling, we might try to + design a better "streaming" API for event subscription. + + A significant advantage of switching away from streaming is to remove the + need for persistent connections between the node and subscribers. A new + streaming protocol design would lose that advantage, and would still need a + way to let clients recover and replay. + + This approach might look better if we decided to use a different protocol + for event subscription, say gRPC instead of JSON-RPC. That choice, however, + would be just as breaking for existing clients, for marginal benefit. + Moreover, this option increases both the complexity and the resource cost on + the node implementation. + + Given that resource consumption and complexity are important considerations, + this option was not chosen. + +3. **Defer to an external event broker.** We might remove the entire event + subscription infrastructure from the node, and define an optional interface + to allow the node to publish all its events to an external event broker, + such as Apache Kafka. + + This has the advantage of greatly simplifying the node, but at a great cost + to the node operator: To enable event subscription in this design, the + operator has to stand up and maintain a separate process in communion with + the node, and configuration changes would have to be coordinated across + both. + + Moreover, this approach would be highly disruptive to existing client use, + and migration would probably require switching to third-party libraries. + Despite the potential benefits for the node itself, the costs to operators + and clients seems too large for this to be the best option. + + Publishing to an external event broker might be a worthwhile future project, + if there is any demand for it. That decision is out of scope for this design, + as it interacts with the design of the indexer as well. + +--- +## References + +- [RFC 006: Event Subscription][rfc006] +- [Tendermint RPC service][rpc-service] +- [Event query grammar][query-grammar] +- [RFC 6455: The WebSocket protocol][ws] +- [JSON-RPC 2.0 Specification][jsonrpc2] +- [Nginx proxy server][nginx] + - [Proxying websockets][rp-ws] + - [Extension modules][ng-xm] +- [FastCGI][fcgi] +- [RFC 001: Storage Engines & Database Layer][rfc001] +- [RFC 002: Interprocess Communication in Tendermint][rfc002] +- Issues: + - [rpc/client: test that client resubscribes upon disconnect][i3380] (#3380) + - [Too high memory usage when creating many events subscriptions][i6439] (#6439) + - [Tendermint emits events faster than clients can pull them][i6729] (#6729) + - [indexer: unbuffered event subscription slow down the consensus][i7247] (#7247) + - [rpc: remove duplication of events when querying][i7273] (#7273) + +[rfc006]: https://github.com/tendermint/tendermint/blob/master/docs/rfc/rfc-006-event-subscription.md +[rpc-service]: https://docs.tendermint.com/master/rpc +[query-grammar]: https://pkg.go.dev/github.com/tendermint/tendermint@master/internal/pubsub/query/syntax +[ws]: https://datatracker.ietf.org/doc/html/rfc6455 +[jsonrpc2]: https://www.jsonrpc.org/specification +[nginx]: https://nginx.org/en/docs/ +[fcgi]: http://www.mit.edu/~yandros/doc/specs/fcgi-spec.html +[rp-ws]: https://nginx.org/en/docs/http/websocket.html +[ng-xm]: https://www.nginx.com/resources/wiki/extending/ +[abci-event]: https://pkg.go.dev/github.com/tendermint/tendermint/abci/types#Event +[rfc001]: https://github.com/tendermint/tendermint/blob/master/docs/rfc/rfc-001-storage-engine.rst +[rfc002]: https://github.com/tendermint/tendermint/blob/master/docs/rfc/rfc-002-ipc-ecosystem.md +[i3380]: https://github.com/tendermint/tendermint/issues/3380 +[i6439]: https://github.com/tendermint/tendermint/issues/6439 +[i6729]: https://github.com/tendermint/tendermint/issues/6729 +[i7247]: https://github.com/tendermint/tendermint/issues/7247 +[i7273]: https://github.com/tendermint/tendermint/issues/7273 diff --git a/docs/architecture/img/adr-075-log-after.png b/docs/architecture/img/adr-075-log-after.png new file mode 100644 index 0000000000000000000000000000000000000000..359f205e490477fde73a051528e58baf70e50afe GIT binary patch literal 22864 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 zʥ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 literal 0 HcmV?d00001 diff --git a/docs/architecture/img/adr-075-log-before.png b/docs/architecture/img/adr-075-log-before.png new file mode 100644 index 0000000000000000000000000000000000000000..813b9d257b5eadeaa30af68b0ec767bb25a6d256 GIT binary patch literal 15057 zcmb7rWmp|SvnKBDA-KD{yL*BY+#L??PH+ekBzTbE1P{SI!QBb&PH@>FA>X}wclXDh z=fLTiskZ8_>bKwOiBM6JMn=F#00RRSFhCw0<0TCar18O^|J(-y(*S?{pR_(W&40;< zTY*8n$e07a&mS4!56J!U3z-A{UnS;1{F8=A&4K)v1{;6QCi&ra2Ji##D5DJm14G1o z{sRX~|AYs02hr-SmW!660-uS49g~r%gRvQthn?edE-(QPJ|Jml=3+$RVP|U(;`0zB z`y;^zq@Qmylac%paj_93(^6C+5qEGlBjI9VVPYW@LLea_5pXs&=TnoAddUu436fd5 zxH$4LGrPOHGr6-fIXGJ|v-0xtGPAHTv#~J(5{w{Edlw@QMtczX-$MSCBVh(Iakg@F zv2w5{c`nz;*um9Bkc{lPqW^sUw$sJR{J(0l2fcI)=pgg+6J}N>7Uuty4P+H~zRRa( z26C`}+KQwD&oi5bH~p|9wx}oIjvc_h9EM%x6t$;Oq z_;E5xwa+3^*w?u98ghtO^r2)O?}Dk(My^5*8=A6gKQyeR0$OJ$=f7>JBRT6Za)~k13J-zo+~>P-!Gbte3+GJpeUpOp|2}Z?UC*l=Uz$PkET4`l zF8jByB2z%C)RzPp2wrp{1IT~e>QZ@WnhGcQ5&3`e=Qu;Cjv1jlIV?-G#ze)rgPebE z9PF-b6zFb+gt(-ea*6L#t%9D_SA>U6{^3uHEYSn3W5)%)oj;CEcz+}_5d7NU?l_z@ zF~;uD5$*7`kQpsIr=aOePhI1{(Py%cgtXAl`9#u5er>%v6}W$=wUfK39c4bG`O2cH z)zf$VrZg&UNsx^g6;a2}Y)* zk!4b8w=16t6xK0#=1Fzq1x94-ro(zh8>wgM6f(5zVN$v^*VIy7@CO18d}dT!MLn@b zsV+p#i_`vNaY-iOH*yrugRnq_hKu`fwmFeX1Iy3v{*Z3Xv3t0_Mz-AgJ|bcqwIJ>Q z7y>hCxxnWUGF*o`I3%3t>9OJ%J9VnQRj+GsmwoT#gZoIw#3Uu0B=i;YmofKTf*Q&5 zc+Cocds7b;X|s1zQ8eyoataAe_e}6!wkFoF<2LWEC#-}=>iHEW$vn51OQD^}?L5`S z3EnN;<&u@vVSR0Fydc<#+lfzr^j(ltengZv?#-54_d-#nzL)9mv#6N51|xjyVtni- ze4RkEKNGx^^w$nr0(akWbv5g%!-}M2k7QqWcWTLA`E_HHgEf<^ zMc$M#lZ0NOqN&LtK14Z5`^>{$wP(v{mnI9ymlJjg#dTwbs-COlj_B*4r&FkWBJ zTQknf?LQr-=q`wgiip`FXu^n*kgR|*+Y&%HHm7%5F(X=^hDb6AmKW?D&9c z4}r&+IB8ByPTuH)Q^)qv3RjiVArcjBNnmzQlQ5t>YpXZ|OlkvuboNwb<@zc3G7Qt} zu8ZR){7OPHYX@z4>K);08ba(QWDc^@KW^d&dOO;}@Nma)$5;pHG&thYIOU#;_nj6$ zKYz?pxVi&8yxZt+#|6st)ns~Yogvy+lCfdvUK&=(akRg~XyLPM-l+M>h;LElkb^LD z($Yc(hoCIQoY~Vd;`6I1;7?Q0m3@3Yl;3V#8{p-U!`H&V>|86`q}nTBJeZNhN0U8` z5=Hc)W4E59Nu5q=vw-9Zbhb*MQRZnf6xnb~dPkagV{F89ru-&{?muM&bQ)sZ9~c+$ z)aMvk@ud_|mw>cp*NrjP)`fgDFq5ytHGTMacqq8xR5eCSv6{*}Rq_7Vd%zT?Nab9| z*g%I6(>%0$?83sFXd*RF4+ReopP$NNgt&8bALlMX&RltG{j>_;S!^sRQxcwC&s%X4 zCkO4pyZ$Q&^3Hs+d={qgZ_^TzQqq!ZPc%jhu~$3@^m||^|)0P^-#z1^3u@I zGAX3=W{SN(owGbwxN;Pc+Z-%F;KOwPkmMyEO_8{04<5-z0r>-z%Z zM8fV+*rWzc|ZtIF*EqzPK;X?IdADFV-A! z9r&q+FMc4?$l z>f+89{M71UDP+lK)Ws3-a1;zpuwzcgh!g-m2CY6lk=VgME`I-R@wxfx#bZ90ik|Lz ze|;+Gbus(Rwe$em`|k3vM7>llnNB@VBI4$Jo1c#_OUPH}Qttx#18T8W=Rq{CI)|1d1K`JWzM|55fb`d9%y52{Ya*d!RNGIn%}XYRELg@i4k<(=wIuJ0`!f%iCjlVp`0z` zTe6W#kTy7=cYksMV#-q)H0m16EtgL!{=i-7aCwlu*o>P_n}{1`1?Igwo-H2s_Gb#C zLlh3=(MsDlPtZxebVH%i5)Lu3`{DfDEB4m@CGI;n!M^3BNm&tD_?Uhb3ujOmN7_i6%@2>Yo4B zpnWnucezeoK?j^w#~XObEK2zlOllePkkB-Q5Ld&ns?jRDlY8 zLC>LdS6R&0# zZ!7-2o$MtjtArSh)EPne+t^qFo;WVu+uQq_{Jyy3PU7iq(!*+#r@a}Sx_R-auD%KO zjHoExp2(P-p3|M3og_{hrVhB{{rzyG+Z@sCr~7k+ovpbVcV;a|JSSis|B>`XyG%>6 z(N%x+!e&P46_2Cn*5ERqsMVfo1UhekahAtujwm`hI$(gEAN2&^8!5H9?rS{W-vC-T zTuqowNZQ=|^-E>H3lX%D!R^o+0-OKjM6vT!rOnv_;iUtcb%L4KZiz8m? z9V+OHdqVY}&t=vZz#+=aOf<*l#BYv%RTDWTL9S7x6u7#|e0B7W5Ni@P*YoW9VjuVI z7c~>s!d6K7DRkueK;4E%bg^tnHYd&&#)g{nB1C8mfy@M;&Rh}smC57oA8kz(b@ za=}YPVbf{s7RJ@OP0pJXzwi{8&<(0dJP`{6I9?mtAFP||k+`!!8l;|H~W_C=fhm1J37N40B$qMa2Y77dF@FVaQi=Q
*sz84=1L-t-%C5upmp*7WMkaa7=dR}fn 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 BY{>us literal 0 HcmV?d00001