Skip to content

Commit 928f003

Browse files
feat(lazer): add resilient client in rust (#2859)
* feat(lazer): add resilient client in rust * configurable backoff * add backoff reset * impl dedup * fix * use Url * asset non empty endponts in constructor * configurable channel capacity * use single channel for connections * add expo backoff builder wrapper * add timeout * add pyth lazer client builder * fix backoff reset logic
1 parent 264c790 commit 928f003

File tree

8 files changed

+674
-151
lines changed

8 files changed

+674
-151
lines changed

Cargo.lock

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lazer/sdk/rust/client/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pyth-lazer-client"
3-
version = "0.1.3"
3+
version = "1.0.0"
44
edition = "2021"
55
description = "A Rust client for Pyth Lazer"
66
license = "Apache-2.0"
@@ -17,6 +17,9 @@ anyhow = "1.0"
1717
tracing = "0.1"
1818
url = "2.4"
1919
derive_more = { version = "1.0.0", features = ["from"] }
20+
backoff = { version = "0.4.0", features = ["futures", "tokio"] }
21+
ttl_cache = "0.5.1"
22+
2023

2124
[dev-dependencies]
2225
bincode = "1.3.3"
@@ -25,3 +28,4 @@ hex = "0.4.3"
2528
libsecp256k1 = "0.7.1"
2629
bs58 = "0.5.1"
2730
alloy-primitives = "0.8.19"
31+
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] }

lazer/sdk/rust/client/examples/subscribe_price_feeds.rs

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
use std::time::Duration;
2+
13
use base64::Engine;
2-
use futures_util::StreamExt;
3-
use pyth_lazer_client::{AnyResponse, LazerClient};
4+
use pyth_lazer_client::backoff::PythLazerExponentialBackoffBuilder;
5+
use pyth_lazer_client::client::PythLazerClientBuilder;
6+
use pyth_lazer_client::ws_connection::AnyResponse;
47
use pyth_lazer_protocol::message::{
58
EvmMessage, LeEcdsaMessage, LeUnsignedMessage, Message, SolanaMessage,
69
};
@@ -9,8 +12,10 @@ use pyth_lazer_protocol::router::{
912
Channel, DeliveryFormat, FixedRate, Format, JsonBinaryEncoding, PriceFeedId, PriceFeedProperty,
1013
SubscriptionParams, SubscriptionParamsRepr,
1114
};
12-
use pyth_lazer_protocol::subscription::{Request, Response, SubscribeRequest, SubscriptionId};
15+
use pyth_lazer_protocol::subscription::{Response, SubscribeRequest, SubscriptionId};
1316
use tokio::pin;
17+
use tracing::level_filters::LevelFilter;
18+
use tracing_subscriber::EnvFilter;
1419

1520
fn get_lazer_access_token() -> String {
1621
// Place your access token in your env at LAZER_ACCESS_TOKEN or set it here
@@ -20,11 +25,32 @@ fn get_lazer_access_token() -> String {
2025

2126
#[tokio::main]
2227
async fn main() -> anyhow::Result<()> {
28+
tracing_subscriber::fmt()
29+
.with_env_filter(
30+
EnvFilter::builder()
31+
.with_default_directive(LevelFilter::INFO.into())
32+
.from_env()?,
33+
)
34+
.json()
35+
.init();
36+
2337
// Create and start the client
24-
let mut client = LazerClient::new(
25-
"wss://pyth-lazer.dourolabs.app/v1/stream",
26-
&get_lazer_access_token(),
27-
)?;
38+
let mut client = PythLazerClientBuilder::new(get_lazer_access_token())
39+
// Optionally override the default endpoints
40+
.with_endpoints(vec![
41+
"wss://pyth-lazer-0.dourolabs.app/v1/stream".parse()?,
42+
"wss://pyth-lazer-1.dourolabs.app/v1/stream".parse()?,
43+
])
44+
// Optionally set the number of connections
45+
.with_num_connections(4)
46+
// Optionally set the backoff strategy
47+
.with_backoff(PythLazerExponentialBackoffBuilder::default().build())
48+
// Optionally set the timeout for each connection
49+
.with_timeout(Duration::from_secs(5))
50+
// Optionally set the channel capacity for responses
51+
.with_channel_capacity(1000)
52+
.build()?;
53+
2854
let stream = client.start().await?;
2955
pin!(stream);
3056

@@ -72,16 +98,16 @@ async fn main() -> anyhow::Result<()> {
7298
];
7399

74100
for req in subscription_requests {
75-
client.subscribe(Request::Subscribe(req)).await?;
101+
client.subscribe(req).await?;
76102
}
77103

78104
println!("Subscribed to price feeds. Waiting for updates...");
79105

80106
// Process the first few updates
81107
let mut count = 0;
82-
while let Some(msg) = stream.next().await {
108+
while let Some(msg) = stream.recv().await {
83109
// The stream gives us base64-encoded binary messages. We need to decode, parse, and verify them.
84-
match msg? {
110+
match msg {
85111
AnyResponse::Json(msg) => match msg {
86112
Response::StreamUpdated(update) => {
87113
println!("Received a JSON update for {:?}", update.subscription_id);
@@ -189,8 +215,6 @@ async fn main() -> anyhow::Result<()> {
189215
println!("Unsubscribed from {sub_id:?}");
190216
}
191217

192-
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
193-
client.close().await?;
194218
Ok(())
195219
}
196220

lazer/sdk/rust/client/src/backoff.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use std::time::Duration;
2+
3+
use backoff::{
4+
default::{INITIAL_INTERVAL_MILLIS, MAX_INTERVAL_MILLIS, MULTIPLIER, RANDOMIZATION_FACTOR},
5+
ExponentialBackoff, ExponentialBackoffBuilder,
6+
};
7+
8+
#[derive(Debug)]
9+
pub struct PythLazerExponentialBackoffBuilder {
10+
initial_interval: Duration,
11+
randomization_factor: f64,
12+
multiplier: f64,
13+
max_interval: Duration,
14+
}
15+
16+
impl Default for PythLazerExponentialBackoffBuilder {
17+
fn default() -> Self {
18+
Self {
19+
initial_interval: Duration::from_millis(INITIAL_INTERVAL_MILLIS),
20+
randomization_factor: RANDOMIZATION_FACTOR,
21+
multiplier: MULTIPLIER,
22+
max_interval: Duration::from_millis(MAX_INTERVAL_MILLIS),
23+
}
24+
}
25+
}
26+
27+
impl PythLazerExponentialBackoffBuilder {
28+
pub fn new() -> Self {
29+
Default::default()
30+
}
31+
32+
/// The initial retry interval.
33+
pub fn with_initial_interval(&mut self, initial_interval: Duration) -> &mut Self {
34+
self.initial_interval = initial_interval;
35+
self
36+
}
37+
38+
/// The randomization factor to use for creating a range around the retry interval.
39+
///
40+
/// A randomization factor of 0.5 results in a random period ranging between 50% below and 50%
41+
/// above the retry interval.
42+
pub fn with_randomization_factor(&mut self, randomization_factor: f64) -> &mut Self {
43+
self.randomization_factor = randomization_factor;
44+
self
45+
}
46+
47+
/// The value to multiply the current interval with for each retry attempt.
48+
pub fn with_multiplier(&mut self, multiplier: f64) -> &mut Self {
49+
self.multiplier = multiplier;
50+
self
51+
}
52+
53+
/// The maximum value of the back off period. Once the retry interval reaches this
54+
/// value it stops increasing.
55+
pub fn with_max_interval(&mut self, max_interval: Duration) -> &mut Self {
56+
self.max_interval = max_interval;
57+
self
58+
}
59+
60+
pub fn build(&self) -> ExponentialBackoff {
61+
ExponentialBackoffBuilder::default()
62+
.with_initial_interval(self.initial_interval)
63+
.with_randomization_factor(self.randomization_factor)
64+
.with_multiplier(self.multiplier)
65+
.with_max_interval(self.max_interval)
66+
.with_max_elapsed_time(None)
67+
.build()
68+
}
69+
}

0 commit comments

Comments
 (0)