-
Notifications
You must be signed in to change notification settings - Fork 169
Introduce NATS JetStream as event bus #4427
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
Changes from 15 commits
daa6499
ea1e32f
2dd6067
ceede24
878c869
cddb3b5
3a76b18
7d42424
19abcf6
cffd2bf
0f2270f
a27c820
830e78c
6c09378
51b89da
85c67da
9271d4f
17de493
2b9e78e
68d7a1a
990c58c
672ad51
cc990e9
0dc756b
afa8a59
e1b007e
5d5a5a5
246380e
9fc9085
1f4d298
0b56fca
36cf438
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -96,6 +96,9 @@ pub struct SharedConfig { | |
| /// By default, volume fees are NOT applied to same-token trades. | ||
| #[serde(default)] | ||
| pub enable_sell_equals_buy_volume_fee: bool, | ||
|
|
||
| /// Enables publishing events to a global events bus. | ||
| pub event_bus: Option<EventBusConfig>, | ||
| } | ||
|
|
||
| impl Default for SharedConfig { | ||
|
|
@@ -112,6 +115,7 @@ impl Default for SharedConfig { | |
| contracts: Default::default(), | ||
| volume_fee_bucket_overrides: Vec::new(), | ||
| enable_sell_equals_buy_volume_fee: false, | ||
| event_bus: None, | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -311,6 +315,17 @@ where | |
| serializer.serialize_str(level.as_str()) | ||
| } | ||
|
|
||
| /// OpenTelemetry tracing configuration. | ||
| #[derive(Debug, Deserialize)] | ||
| #[serde(rename_all = "kebab-case")] | ||
| #[derive(serde::Serialize)] | ||
| pub struct EventBusConfig { | ||
| /// Url of the event bus service. | ||
| pub url: Url, | ||
| /// Name of the channel to post events to. | ||
| pub channel: String, | ||
| } | ||
|
|
||
| /// Gas price estimation strategy. | ||
| #[derive(Debug, Clone, Deserialize)] | ||
| #[serde(tag = "type")] | ||
|
|
@@ -403,6 +418,10 @@ mod tests { | |
| "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", | ||
| "0x6B175474E89094C44Da98b954EedeAC495271d0F", | ||
| ] | ||
|
|
||
| [event-bus] | ||
| url = "localhost:4222" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| channel = "main" | ||
| "#; | ||
|
|
||
| let config: SharedConfig = toml::from_str(toml).unwrap(); | ||
|
|
@@ -430,5 +449,10 @@ mod tests { | |
| assert!(config.enable_sell_equals_buy_volume_fee); | ||
| assert_eq!(config.volume_fee_bucket_overrides.len(), 1); | ||
| assert_eq!(config.volume_fee_bucket_overrides[0].tokens.len(), 2); | ||
| assert_eq!( | ||
| config.event_bus.as_ref().unwrap().url, | ||
| "localhost:4222".parse().unwrap() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| ); | ||
| assert_eq!(config.event_bus.as_ref().unwrap().channel, "main"); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| //! Implements a simple globally available way to publish events to an event | ||
| //! bus. Under the hood it's using NATS. To support publishing events from | ||
| //! synchronous contexts we use an unbounded channel as an in-memory buffer. | ||
| //! Whenever a message gets posted to this channel a background task wakes | ||
| //! up and forwards it to the NATS service running in a different process. | ||
| //! Messages always get serialized as JSON so you can publish anything that | ||
| //! can be serialized to JSON as well. | ||
| use { | ||
| crate::config::EventBusConfig, | ||
| async_nats::jetstream::Context as JetstreamClient, | ||
| bytes::Bytes, | ||
| chrono::Utc, | ||
| serde::Serialize, | ||
| serde_json::json, | ||
| tokio::sync::{ | ||
| OnceCell, | ||
| mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}, | ||
|
MartinquaXD marked this conversation as resolved.
Outdated
|
||
| }, | ||
| }; | ||
|
|
||
| struct EventBusConnector { | ||
| /// Unbounded channel to allow emitting events from synchrounous | ||
| /// contexts. | ||
| message_queue: UnboundedSender<Message>, | ||
|
MartinquaXD marked this conversation as resolved.
Outdated
|
||
| /// Subject prefix to disambiguate messages in globally shared event bus | ||
| /// service. | ||
| subject_prefix: String, | ||
| } | ||
|
|
||
| struct Message { | ||
| subject: String, | ||
| data: Bytes, | ||
| } | ||
|
|
||
| /// Singleton event bus connection to allow publishing events | ||
| /// conventiently from everywhere. | ||
|
jmg-duarte marked this conversation as resolved.
Outdated
|
||
| static BUS: OnceCell<EventBusConnector> = OnceCell::const_new(); | ||
|
|
||
| /// Initializes the event bus and panics if it fails. | ||
| pub async fn init(config: EventBusConfig) { | ||
| BUS.get_or_init(|| async move { | ||
| let client = async_nats::connect(config.url.as_str()) | ||
| .await | ||
| .expect("failed to connect to NATS service"); | ||
| let jetstream = async_nats::jetstream::new(client); | ||
| let mut stream = jetstream | ||
| .get_stream(&config.channel) | ||
| .await | ||
| .expect("could not connect to jetstream"); | ||
| let info = stream.info().await.expect("failed to fetch stream info"); | ||
| tracing::debug!(?info, "connected to jetstream"); | ||
|
|
||
| let (sender, receiver) = unbounded_channel(); | ||
|
MartinquaXD marked this conversation as resolved.
Outdated
|
||
| tokio::task::spawn(forward_messages_to_event_bus_client(receiver, jetstream)); | ||
| EventBusConnector { | ||
| message_queue: sender, | ||
| // we prefix every subject with `event` to allow consumers to easily | ||
| // subscribe to all events without also seeing NATS internal events | ||
| subject_prefix: format!("event.{}.", config.chain_id), | ||
|
MartinquaXD marked this conversation as resolved.
Outdated
|
||
| } | ||
| }) | ||
| .await; | ||
| } | ||
|
|
||
| /// Monitors a message queue and forwards all messages to the event bus | ||
| /// service. | ||
| async fn forward_messages_to_event_bus_client( | ||
| mut receiver: UnboundedReceiver<Message>, | ||
|
MartinquaXD marked this conversation as resolved.
Outdated
|
||
| client: JetstreamClient, | ||
| ) { | ||
| while let Some(message) = receiver.recv().await { | ||
| match client.publish(message.subject, message.data).await { | ||
| Err(err) => { | ||
| tracing::debug!(?err, "failed to publish event"); | ||
| } | ||
| Ok(_fut) => { | ||
| // let's assume the message arrived for now | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Enqueues the event to be sent to the event bus in a background task. | ||
| pub fn publish(subject: &str, data: impl Serialize) { | ||
| let Some(bus) = BUS.get() else { | ||
| return; | ||
| }; | ||
|
Comment on lines
+188
to
+193
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I get that the event bus should be initialized by now, but should we be more defensive here? Maybe log a warning, or call |
||
|
|
||
| let mut message = json!({ | ||
| "version": "v1", | ||
| "timestamp": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true), | ||
| "body": data, | ||
| }); | ||
| if let Some(id) = crate::tracing::distributed::request_id::from_current_span() { | ||
| message["requestId"] = id.into(); | ||
| } | ||
| let body = match serde_json::to_vec(&message) { | ||
| Ok(body) => body, | ||
| Err(err) => { | ||
| tracing::error!(?err, "failed to serialize event"); | ||
| return; | ||
| } | ||
| }; | ||
|
|
||
| let message = Message { | ||
| subject: format!("{}{}", bus.subject_prefix, subject), | ||
| data: body.into(), | ||
| }; | ||
|
|
||
| if let Err(err) = bus.message_queue.send(message) { | ||
| tracing::error!(?err, "failed to enqueue message"); | ||
| } | ||
|
MartinquaXD marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use {super::*, serde_json::json}; | ||
|
|
||
| #[ignore] | ||
| #[tokio::test] | ||
| async fn send_messages() { | ||
| crate::tracing::init::initialize(&crate::Config { | ||
| env_filter: "warn,observe=debug".to_string(), | ||
| stderr_threshold: None, | ||
| use_json_format: false, | ||
| tracing: None, | ||
| }); | ||
| init(EventBusConfig { | ||
| url: "localhost:4222".parse().unwrap(), | ||
| channel: "main".to_string(), | ||
| chain_id: 1, | ||
| }) | ||
| .await; | ||
|
|
||
| for _ in 0..1000 { | ||
| publish( | ||
| "name", | ||
| json!({ | ||
| "estimator": "baseline", | ||
| "outAmount": 1234, | ||
| }), | ||
| ); | ||
| } | ||
| tokio::time::sleep(std::time::Duration::from_millis(100)).await; | ||
|
jmg-duarte marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.