Skip to content

Commit 5cc7aac

Browse files
authored
feat: Add option to enable compression of event payloads (#102)
1 parent c239b8a commit 5cc7aac

File tree

8 files changed

+65
-8
lines changed

8 files changed

+65
-8
lines changed

contract-tests/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ actix = "0.13.0"
1010
actix-web = "4.2.1"
1111
env_logger = "0.10.0"
1212
log = "0.4.14"
13-
launchdarkly-server-sdk = { path = "../launchdarkly-server-sdk/", default-features = false}
13+
launchdarkly-server-sdk = { path = "../launchdarkly-server-sdk/", default-features = false, features = ["event-compression"]}
1414
serde = { version = "1.0.132", features = ["derive"] }
1515
serde_json = "1.0.73"
1616
futures = "0.3.12"

contract-tests/src/client_entity.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ impl ClientEntity {
113113
processor_builder.capacity(capacity);
114114
}
115115
processor_builder.all_attributes_private(events.all_attributes_private);
116+
if let Some(e) = events.enable_gzip {
117+
processor_builder.compress_events(e);
118+
}
116119

117120
if let Some(interval) = events.flush_interval_ms {
118121
processor_builder.flush_interval(Duration::from_millis(interval));

contract-tests/src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub struct EventParameters {
4141
pub enable_diagnostics: bool,
4242
#[serde(default = "bool::default")]
4343
pub all_attributes_private: bool,
44+
pub enable_gzip: Option<bool>,
4445
pub global_private_attributes: Option<HashSet<Reference>>,
4546
pub flush_interval_ms: Option<u64>,
4647
#[serde(default = "bool::default")]
@@ -110,6 +111,8 @@ async fn status() -> impl Responder {
110111
"migrations".to_string(),
111112
"event-sampling".to_string(),
112113
"client-prereq-events".to_string(),
114+
"event-gzip".to_string(),
115+
"optional-event-gzip".to_string(),
113116
],
114117
})
115118
}

launchdarkly-server-sdk/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ uuid = {version = "1.2.2", features = ["v4"] }
3535
hyper = { version = "0.14.19", features = ["client", "http1", "http2", "tcp"] }
3636
hyper-rustls = { version = "0.24.1" , optional = true}
3737
rand = "0.8"
38+
flate2 = { version = "1.0.35", optional = true }
3839

3940
[dev-dependencies]
4041
maplit = "1.0.1"
@@ -50,6 +51,7 @@ reqwest = { version = "0.12.4", features = ["json"] }
5051
[features]
5152
default = ["rustls"]
5253
rustls = ["hyper-rustls/http1", "hyper-rustls/http2", "eventsource-client/rustls"]
54+
event-compression = ["flate2"]
5355

5456
[[example]]
5557
name = "print_flags"

launchdarkly-server-sdk/src/data_source_builders.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ impl DataSourceFactory for MockDataSourceBuilder {
345345
_sdk_key: &str,
346346
_tags: Option<String>,
347347
) -> Result<Arc<dyn DataSource>, BuildError> {
348-
return Ok(self.data_source.as_ref().unwrap().clone());
348+
Ok(self.data_source.as_ref().unwrap().clone())
349349
}
350350

351351
fn to_owned(&self) -> Box<dyn DataSourceFactory> {

launchdarkly-server-sdk/src/events/processor_builders.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ pub struct EventProcessorBuilder<C> {
8484
private_attributes: HashSet<Reference>,
8585
connector: Option<C>,
8686
omit_anonymous_contexts: bool,
87+
compress_events: bool,
8788
// diagnostic_recording_interval: Duration
8889
}
8990

@@ -109,6 +110,7 @@ where
109110
}
110111

111112
let event_sender_result: Result<Arc<dyn EventSender>, BuildError> =
113+
// NOTE: This would only be possible under unit testing conditions.
112114
if let Some(event_sender) = &self.event_sender {
113115
Ok(event_sender.clone())
114116
} else if let Some(connector) = &self.connector {
@@ -117,6 +119,7 @@ where
117119
hyper::Uri::from_str(url_string.as_str()).unwrap(),
118120
sdk_key,
119121
default_headers,
122+
self.compress_events,
120123
)))
121124
} else {
122125
#[cfg(feature = "rustls")]
@@ -133,6 +136,7 @@ where
133136
hyper::Uri::from_str(url_string.as_str()).unwrap(),
134137
sdk_key,
135138
default_headers,
139+
self.compress_events,
136140
)))
137141
}
138142
#[cfg(not(feature = "rustls"))]
@@ -178,6 +182,7 @@ impl<C> EventProcessorBuilder<C> {
178182
private_attributes: HashSet::new(),
179183
omit_anonymous_contexts: false,
180184
connector: None,
185+
compress_events: false,
181186
}
182187
}
183188

@@ -258,7 +263,20 @@ impl<C> EventProcessorBuilder<C> {
258263
self
259264
}
260265

266+
#[cfg(feature = "event-compression")]
267+
/// Should the event payload sent to LaunchDarkly use gzip compression. By
268+
/// default this is false to prevent backward breaking compatibility issues with
269+
/// older versions of the relay proxy.
270+
//
271+
/// Customers not using the relay proxy are strongly encouraged to enable this
272+
/// feature to reduce egress bandwidth cost.
273+
pub fn compress_events(&mut self, enabled: bool) -> &mut Self {
274+
self.compress_events = enabled;
275+
self
276+
}
277+
261278
#[cfg(test)]
279+
/// Test only functionality that allows us to override the event sender.
262280
pub fn event_sender(&mut self, event_sender: Arc<dyn EventSender>) -> &mut Self {
263281
self.event_sender = Some(event_sender);
264282
self

launchdarkly-server-sdk/src/events/sender.rs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@ use crate::{
22
reqwest::is_http_error_recoverable, LAUNCHDARKLY_EVENT_SCHEMA_HEADER,
33
LAUNCHDARKLY_PAYLOAD_ID_HEADER,
44
};
5-
use std::collections::HashMap;
6-
75
use chrono::DateTime;
86
use crossbeam_channel::Sender;
7+
use std::collections::HashMap;
8+
9+
#[cfg(feature = "event-compression")]
10+
use flate2::write::GzEncoder;
11+
#[cfg(feature = "event-compression")]
12+
use flate2::Compression;
13+
#[cfg(feature = "event-compression")]
14+
use std::io::Write;
15+
916
use futures::future::BoxFuture;
1017
use hyper::{client::connect::Connection, service::Service, Uri};
1118
use tokio::{
@@ -36,6 +43,10 @@ pub struct HyperEventSender<C> {
3643
sdk_key: String,
3744
http: hyper::Client<C>,
3845
default_headers: HashMap<&'static str, String>,
46+
47+
// used with event-compression feature
48+
#[allow(dead_code)]
49+
compress_events: bool,
3950
}
4051

4152
impl<C> HyperEventSender<C>
@@ -50,12 +61,14 @@ where
5061
url: hyper::Uri,
5162
sdk_key: &str,
5263
default_headers: HashMap<&'static str, String>,
64+
compress_events: bool,
5365
) -> Self {
5466
Self {
5567
url,
5668
sdk_key: sdk_key.to_owned(),
5769
http: hyper::Client::builder().build(connector),
5870
default_headers,
71+
compress_events,
5972
}
6073
}
6174

@@ -96,7 +109,9 @@ where
96109
serde_json::to_string_pretty(&events).unwrap_or_else(|e| e.to_string())
97110
);
98111

99-
let json = match serde_json::to_vec(&events) {
112+
// mut is needed for event-compression feature
113+
#[allow(unused_mut)]
114+
let mut payload = match serde_json::to_vec(&events) {
100115
Ok(json) => json,
101116
Err(e) => {
102117
error!(
@@ -107,6 +122,21 @@ where
107122
}
108123
};
109124

125+
// mut is needed for event-compression feature
126+
#[allow(unused_mut)]
127+
let mut additional_headers = self.default_headers.clone();
128+
129+
#[cfg(feature = "event-compression")]
130+
if self.compress_events {
131+
let mut e = GzEncoder::new(Vec::new(), Compression::default());
132+
if e.write_all(payload.as_slice()).is_ok() {
133+
if let Ok(compressed) = e.finish() {
134+
payload = compressed;
135+
additional_headers.insert("Content-Encoding", "gzip".into());
136+
}
137+
}
138+
}
139+
110140
for attempt in 1..=2 {
111141
if attempt == 2 {
112142
sleep(Duration::from_secs(1)).await;
@@ -124,11 +154,11 @@ where
124154
)
125155
.header(LAUNCHDARKLY_PAYLOAD_ID_HEADER, uuid.to_string());
126156

127-
for default_header in &self.default_headers {
157+
for default_header in &additional_headers {
128158
request_builder =
129159
request_builder.header(*default_header.0, default_header.1.as_str());
130160
}
131-
let request = request_builder.body(hyper::Body::from(json.clone()));
161+
let request = request_builder.body(hyper::Body::from(payload.clone()));
132162

133163
let result = self.http.request(request.unwrap()).await;
134164

@@ -334,6 +364,7 @@ mod tests {
334364
url,
335365
"sdk-key",
336366
HashMap::<&str, String>::new(),
367+
false,
337368
)
338369
}
339370
}

launchdarkly-server-sdk/src/test_common.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::Stage;
77
pub const FLOAT_TO_INT_MAX: i64 = 9007199254740991;
88

99
pub fn basic_flag(key: &str) -> Flag {
10-
return basic_flag_with_visibility(key, false);
10+
basic_flag_with_visibility(key, false)
1111
}
1212

1313
pub fn basic_flag_with_visibility(key: &str, visible_to_environment_id: bool) -> Flag {

0 commit comments

Comments
 (0)