Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(event-streaming): API-driven subscription management #2172

Merged
merged 139 commits into from
Feb 6, 2025
Merged

Conversation

mariocynicys
Copy link
Collaborator

@mariocynicys mariocynicys commented Jul 22, 2024

This PR aims to support event streaming in some of MM2's API endpoints.

The work in this PR till now is only a refactor of the event streaming code after multiple iterations of discussing how this should end up looking like.

The flow goes as follows:

  • A client initially opens an SSE connection with MM2 to receive events on. For wasm, since it can't support SSEs, we use a worker to deliver the events to the client (let's consider it as SSE too).
  • If a client wants to receive some events (balance change, network topology change, new gas estimations, etc...), they need to subscribe to these events (enable these streamers) from the API.
  • A streamer is a background task that is responsible for firing some events. Initially MM2 doesn't have any streamers running. When a client enables a streamer (= subscribes to a particular event), they start receiving events from that streamer. Until they unsubscribe to them.
  • If no client is listening to some active streamer, it will shutdown. A new streamer will start again if a client re-subscribes.
  • (little irrelevant now) Multiple clients subscribing to the same event on MM2 doesn't mean we will have a new streamer for every client. Only one streamer is booted up for the first subscribed client and new subscribing clients will be sharing the same streamer.

The role of the StreamingManager:

  • Manage different clients (each client has an id cid when it first connects through SSE).
  • Add a new (cid, sid) pair (translates to: client with id cid wants to listen to the streamer with id sid). If such a streamer with sid is already running, it is just instructed that a new client is gonna be listening along, otherwise, it will be spawned in the background.
  • Stop listening (cid, sid) pair (= client with cid no longer wants to receive events from streamer with id sid, if no more clients are listening, the streamer will die).
  • Manage different streamers.
  • Manage the connections between the streamers (the background thread) and MM2. Streamers either work in a periodic manner (do some job and possibly fire some events then sleep for a while and repeat) or they need to receive a notification from MM2 (e.g. UTXO balance event streaming rely on electrum subscriptions: the electrum server sends a notification to MM2 when an address balance has changed and MM2 in turn sends this notification to the streamer responsible for firing balance events). To send some data to a streamer, StreamingManager::send(streamer_id, arbitrary_data) is used.

Breaking Changes:

  • No more event_stream_configuration in MM2 json config. Requested streams are initialized and configured dynamically through the API.
  • Thus event_stream_configuration.access_control_allow_origin config was moved to access_control_allow_origin (one scope out) b9d1218.
  • And event_stream_configuration.worker_path was changed to event_stream_worker_path (one scope out) ee32fd1.
  • No more filters are required in the initial SSE setup with MM2. (e.g. mm2.com/event-stream?filter=NETWORK,BALANCE,DATA_NEEDED:datatype -> mm2.com/event-stream?id=1), but a client id is now required (a random u64 identifier for [& generated by] the client) (e.g. mm2.com/event-stream?id=799384531)
  • How to filter the wanted events then (client gets no events by default)? The client needs to activate specific event streamers through RPC (e.g. stream::enable::balance).
  • Contrary to the note in the previous point (client gets no events by default), some special events are now delivered to the client even if they never requested/filtered them (this happens using broadcast_all). example of these events are DATA_NEEDED:datatype, and HEARTBEAT should be similar later as well.

(so for keplr integration, only ?id=<RANDOM_U64> must be added to the SSE init request and everything should work. ?filter= shall be removed)
(the <RANDOM_U64> should be stored by the client since it's required in any event streamer activation/deactivation. If for keplr only though, this isn't necessary for now since DATA_NEEDED:datatype event streamer is a special event that's broadcasted to all clients and doesn't require any sort of activation/subscription before hand)

it was confusing why a utxo standard coin was being used instead of the coin we are building, this makes a bit more sense
There was no point of the option wrapping. Simplifying stuff.
also does some housekeeping, file splitting and renaming
remove the intermediate bridge task and replace it with `filter_map` call.
also properly implement the shutdown handle
also wraps the streamermanager mutable data inside Arc<RwLock< so to be used in concurrent code.
RwLock was chosen here since we should be sending streaming data way more than editing the streamers
along with some cleanups and simplifications
Also use the `data_rx` for utxo balance events.
A cleanup for the scripthash notification channels/logic will follow
this is very messy though (because of the current state of electurm code) and will be reverted, just recording this in the history for now
made more sense to have it inside the manager. also now the manager provides the controller as a parameter in the handle method so different streamers don't have to keep a copy of ctx just to broadcast on the controller.
this is still not hooked up though. no data will be received on this rx channel till we do
`event_stream_manager.send('BALANCE:ZCoin') (or whatever the coin name is)` somewhere.
#[derive(Default)]
pub struct Event {
/// The type of the event (balance, network, swap, etc...).
event_type: String,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event_id is a String although it has a structure like "TASK:{task_id}" or "FEE_ESTIMATION:{ticker}".
But all event id constructions are scattered over the code - so it's easy to overlook which events we support,
also the format is not checked - easy to make a mistake.
Maybe we could make event_id as a enum with the deser feature? It would be easier to maintain all event and document them for the GUI

@dimxy
Copy link
Collaborator

dimxy commented Nov 22, 2024

I tried to use this PR and it appears to work okay. This is great work.
I am about to approve it, to move forward and let GUI play with it.
There are few notes about I'd like to see resolved before (mandatory HEARBEATs)

I'd like to leave a note though, how IMO the event streaming could be simplified:

  • there are several dedicated rpcs for each event type to enable it - these rpcs could be converted to single or even eliminated,
  • GUI needs to generate unique client-id to receive events, this is not GUI friendly and need to sync between several GUI processes - client-id could be KDF created or even eliminated,
  • task manager rpcs become too heavily integrated with event streaming: GUI needs to send client-id in each task init rpc and the task manager tackles it - I think client-id could be eliminated from the task API.

I think , these problems come from use of one-directional Server-Side Events (SSE) protocol. SSE is a web protocol but we use it for native configuration.
We have an example of event delivering as eth_subscribe call. It works over web sockets which, as two-directional, can send both control and data over same connection (no need for dedicated rpcs and client-ids).
I think we could do this similarly, to simplify rpcs and internal architecture:
GUI opens a ws connection and sends a json to subscribe to events (using known names like 'TASK', 'BALANCE' etc.).
We don't need client-id anymore: API already knowns which connection expects which events.
In any time client (GUI) can subscribe to new events or unsubscribe from others.
We need a streaming manager serving as an event bus, which:

  • receives events from all event sources in code (task manager, polling loops)
  • maintains client subscriptions: when a client subscribes on some events the streaming manager keeps those events in a list for this client and filters and sends only the subscribed events to the client (over the same channel that used now).

@shamardy shamardy added 2.3.0-beta and removed P2 labels Nov 25, 2024
@shamardy shamardy requested a review from dimxy November 25, 2024 10:49
@laruh
Copy link
Member

laruh commented Nov 26, 2024

@mariocynicys could you fix conflicts please

@shamardy shamardy requested review from shamardy and removed request for shamardy February 3, 2025 09:54
shamardy
shamardy previously approved these changes Feb 4, 2025
Copy link
Collaborator

@shamardy shamardy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huge Work! All comments are for discussion and non blockers.

A few items to consider:

  • Similar to how we test RPCs we need to add some integration tests for each streamer type and streaming in general, we may need to implement a SSE reader / EventSource for these tests
  • Please resolve conflicts
  • test_zcoin_tx_streaming is failing in wasm
  • Please open docs issue
  • Once this PR is merged, we need to check open PRs for streaming coverage like the WalletConnect PR as an example

mm2src/mm2_event_stream/src/streamer.rs Show resolved Hide resolved
Comment on lines +37 to +40
// TODO: If the the streamer was already running, it is probably running with different configuration.
// We might want to inform the client that the configuration they asked for wasn't applied and return
// the active configuration instead?
// pub config: Json,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would still be strange for the first client to subscribe to set the config. I see that you postponed this as multi client support completion is not a high priority currently, we can think about better solutions for this in the future.

Comment on lines +53 to +65
MmCoinEnum::EthCoin(_) => (),
MmCoinEnum::ZCoin(_)
| MmCoinEnum::UtxoCoin(_)
| MmCoinEnum::Bch(_)
| MmCoinEnum::QtumCoin(_)
| MmCoinEnum::Tendermint(_) => {
if req.config.is_some() {
Err(BalanceStreamingRequestError::EnableError(
"Invalid config provided. No config needed".to_string(),
))?
}
},
_ => Err(BalanceStreamingRequestError::CoinNotSupported)?,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we will need this and other streamers for SIA in the future for GUIs to fully support balance streaming c.c. @Alrighttt

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added to the streaming tracking list

let streamer = ZCoinTxHistoryEventStreamer::new(coin.clone());
ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await
},
_ => Err(TxHistoryStreamingRequestError::CoinNotSupported)?,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SIA will need to be added here as well c.c. @Alrighttt

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added to the streaming tracking list

.gitignore Outdated Show resolved Hide resolved
mm2src/coins/z_coin.rs Show resolved Hide resolved
@shamardy shamardy changed the title feat(event-streaming): new streams (orders, swaps, eth_fee_estimation) feat(event-streaming): API-driven subscription management Feb 6, 2025
@shamardy shamardy merged commit 927f84b into dev Feb 6, 2025
19 of 25 checks passed
@shamardy shamardy deleted the event-streaming branch February 6, 2025 10:34
dimxy added a commit that referenced this pull request Feb 6, 2025
* dev:
  feat(event-streaming): API-driven subscription management (#2172)
shamardy pushed a commit that referenced this pull request Feb 7, 2025
Refactor event streaming to support dynamic client subscriptions over RPC using a unified StreamingManager. The StreamingManager now orchestrates background streamers by initializing a streamer when a client activates it via the RPC API and shutting it down when no longer needed. Legacy fee estimator endpoints have been replaced with streaming RPC methods, and new task manager RPCs for BCH and Tendermint have been added. Additionally, event stream configuration has been migrated from static JSON settings to runtime API initialization for improved flexibility.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

return an error if transaction history futures initialization fails
6 participants