launchdarkly_server_sdk/fdv2/
source.rs1use std::time::Duration;
2
3use rand::Rng;
4
5use crate::stores::change_set::ChangeSet;
6
7use super::model::Selector;
8
9const FALLBACK_HEADER: &str = "X-LD-FD-Fallback";
10const FALLBACK_TTL_HEADER: &str = "X-LD-FD-Fallback-TTL";
11const DEFAULT_FALLBACK_TTL: Duration = Duration::from_secs(60 * 60);
12const MAX_FALLBACK_TTL: Duration = Duration::from_secs(60 * 60);
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum ErrorKind {
18 Unknown,
20 NetworkError,
22 ErrorResponse {
24 status_code: u16,
26 },
27 InvalidData,
29}
30
31#[derive(Debug, Clone)]
33pub struct ErrorInfo {
34 pub kind: ErrorKind,
36 pub message: String,
38}
39
40#[derive(Debug, Clone)]
42pub struct FDv1FallbackDirective {
43 pub ttl: Duration,
45}
46
47impl FDv1FallbackDirective {
48 pub(super) fn from_ttl(ttl: Option<Duration>) -> Self {
52 let ttl = match ttl {
53 Some(t) if t > Duration::ZERO && t <= MAX_FALLBACK_TTL => t,
54 _ => jittered_default_ttl(),
55 };
56 Self { ttl }
57 }
58}
59
60fn jittered_default_ttl() -> Duration {
64 DEFAULT_FALLBACK_TTL.mul_f64(rand::rng().random_range(0.5..=1.0))
65}
66
67pub(super) fn read_fallback_directive<'a>(
68 lookup: impl Fn(&str) -> Option<&'a str>,
69) -> Option<FDv1FallbackDirective> {
70 let flag = lookup(FALLBACK_HEADER)?;
71 if !flag.eq_ignore_ascii_case("true") {
72 return None;
73 }
74 let ttl = lookup(FALLBACK_TTL_HEADER)
75 .and_then(|s| s.parse::<u64>().ok())
76 .map(Duration::from_secs);
77 Some(FDv1FallbackDirective::from_ttl(ttl))
78}
79
80#[derive(Debug)]
82pub enum FDv2SourceResult {
83 ChangeSet(ChangeSet),
85 Interrupted(ErrorInfo),
87 TerminalError(ErrorInfo),
89 Goodbye,
91}
92
93#[derive(Debug)]
95pub struct FDv2SourceEvent {
96 pub result: FDv2SourceResult,
98 pub fdv1_fallback: Option<FDv1FallbackDirective>,
100}
101
102pub type FDv2SourceEventFuture<'a> =
104 std::pin::Pin<Box<dyn std::future::Future<Output = FDv2SourceEvent> + Send + 'a>>;
105
106pub trait Initializer: Send {
108 fn run(&mut self) -> FDv2SourceEventFuture<'_>;
110 fn name(&self) -> &str;
112}
113
114pub trait Synchronizer: Send {
116 fn next(&mut self, selector: Selector) -> FDv2SourceEventFuture<'_>;
118 fn name(&self) -> &str;
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use std::collections::HashMap;
126
127 fn headers<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<&'a str> + 'a {
128 let map: HashMap<&'a str, &'a str> = pairs.iter().copied().collect();
129 move |k| map.get(k).copied()
130 }
131
132 fn assert_is_jittered_default(ttl: Duration) {
134 assert!(ttl <= DEFAULT_FALLBACK_TTL && ttl >= DEFAULT_FALLBACK_TTL / 2);
135 }
136
137 #[test]
138 fn fallback_absent_header_returns_none() {
139 assert!(read_fallback_directive(headers(&[])).is_none());
140 }
141
142 #[test]
143 fn fallback_header_value_other_than_true_returns_none() {
144 assert!(read_fallback_directive(headers(&[("X-LD-FD-Fallback", "false")])).is_none());
145 }
146
147 #[test]
148 fn fallback_header_uppercase_true_uses_default_ttl() {
149 let d =
150 read_fallback_directive(headers(&[("X-LD-FD-Fallback", "TRUE")])).expect("directive");
151 assert_is_jittered_default(d.ttl);
152 }
153
154 #[test]
155 fn fallback_ttl_header_is_parsed() {
156 let d = read_fallback_directive(headers(&[
157 ("X-LD-FD-Fallback", "true"),
158 ("X-LD-FD-Fallback-TTL", "60"),
159 ]))
160 .expect("directive");
161 assert_eq!(d.ttl, Duration::from_secs(60));
162 }
163
164 #[test]
165 fn fallback_ttl_header_malformed_uses_default() {
166 let d = read_fallback_directive(headers(&[
167 ("X-LD-FD-Fallback", "true"),
168 ("X-LD-FD-Fallback-TTL", "not-a-number"),
169 ]))
170 .expect("directive");
171 assert_is_jittered_default(d.ttl);
172 }
173
174 #[test]
175 fn fallback_ttl_zero_uses_default() {
176 let d = read_fallback_directive(headers(&[
177 ("X-LD-FD-Fallback", "true"),
178 ("X-LD-FD-Fallback-TTL", "0"),
179 ]))
180 .expect("directive");
181 assert_is_jittered_default(d.ttl);
182 }
183
184 #[test]
185 fn fallback_ttl_longer_than_an_hour_uses_default() {
186 let d = read_fallback_directive(headers(&[
187 ("X-LD-FD-Fallback", "true"),
188 ("X-LD-FD-Fallback-TTL", "3601"),
189 ]))
190 .expect("directive");
191 assert_is_jittered_default(d.ttl);
192 }
193}