statsig_rust/interned_values/
mmap_sync.rs1use serde::Deserialize;
2
3use crate::{
4 networking::ResponseData,
5 observability::ops_stats::OpsStatsForInstance,
6 specs_response::{proto_specs::deserialize_protobuf, spec_types::SpecsResponseFull},
7 StatsigErr,
8};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct MmapSyncCursor {
13 pub lcut: u64,
14 pub checksum: Option<String>,
15}
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19pub enum MmapWriteOutcome {
20 Published(MmapSyncCursor),
21 NoUpdate,
22}
23
24pub(super) struct MmapResolvedUpdate {
25 pub(super) specs: SpecsResponseFull,
26 pub(super) cursor: MmapSyncCursor,
27}
28
29pub(super) struct MmapConfigResponse<'a> {
30 data: &'a mut ResponseData,
31 headers: MmapResponseHeaders,
32 format: MmapResponseFormat,
33}
34
35#[derive(Clone, Copy)]
36enum MmapResponseFormat {
37 Json,
38 Protobuf,
39}
40
41#[derive(Deserialize)]
42struct MmapResponseMetadata {
43 has_updates: Option<bool>,
44 time: Option<u64>,
45 checksum: Option<String>,
46}
47
48struct MmapResponseHeaders {
49 lcut: Option<u64>,
50 checksum: ChecksumHeader,
51 cache_hit: bool,
52}
53
54enum ChecksumHeader {
55 Missing,
56 Present(Option<String>),
59}
60
61#[derive(Clone, Copy)]
62struct ChecksumIdentity<'a> {
63 value: Option<&'a str>,
64 is_known: bool,
65}
66
67enum MetadataResolution {
68 NoUpdate,
69 ParseFull(MmapSyncCursor),
70}
71
72impl<'a> MmapConfigResponse<'a> {
73 pub(super) fn new(data: &'a mut ResponseData) -> Result<Self, StatsigErr> {
74 let headers = MmapResponseHeaders::from_response(data)?;
75 let format = MmapResponseFormat::from_response(data);
76
77 Ok(Self {
78 data,
79 headers,
80 format,
81 })
82 }
83
84 pub(super) fn resolve(
85 mut self,
86 previous: Option<&MmapSyncCursor>,
87 ) -> Result<Option<MmapResolvedUpdate>, StatsigErr> {
88 if self.headers.cache_hit {
89 let previous = previous.ok_or_else(|| {
90 invalid_mmap_response("Received a cache-hit response before any mmap was published")
91 })?;
92 validate_no_update_identity(
93 previous,
94 self.headers.lcut,
95 self.headers.checksum.identity(),
96 )?;
97 return Ok(None);
98 }
99
100 if previous.is_some_and(|previous| self.headers.is_no_update_without_body(previous)) {
101 return Ok(None);
102 }
103
104 let metadata_cursor = match self.format {
105 MmapResponseFormat::Protobuf => {
106 if previous.is_some() && self.headers.lcut.is_none() {
107 return Err(invalid_mmap_response(
108 "A conditional protobuf response did not include x-since-time",
109 ));
110 }
111 None
112 }
113 MmapResponseFormat::Json => match self.resolve_json_metadata(previous)? {
114 MetadataResolution::NoUpdate => return Ok(None),
115 MetadataResolution::ParseFull(cursor) => Some(cursor),
116 },
117 };
118
119 let specs = parse_specs_response_data(&mut *self.data, self.format)?;
120 self.resolve_parsed(specs, metadata_cursor, previous)
121 }
122
123 fn resolve_json_metadata(
124 &mut self,
125 previous: Option<&MmapSyncCursor>,
126 ) -> Result<MetadataResolution, StatsigErr> {
127 let metadata = self.data.deserialize_into::<MmapResponseMetadata>()?;
128 if metadata.has_updates == Some(false) {
129 let previous = previous.ok_or_else(|| {
130 invalid_mmap_response("Received a no-update response before any mmap was published")
131 })?;
132 validate_json_no_update(&metadata, &self.headers, previous)?;
133 return Ok(MetadataResolution::NoUpdate);
134 }
135 if metadata.has_updates != Some(true) {
136 return Err(invalid_mmap_response(
137 "A config response did not include a valid has_updates value",
138 ));
139 }
140
141 let cursor = cursor_from_metadata(&metadata)?;
142 self.headers.validate_cursor(&cursor)?;
143 if previous.is_some_and(|previous| cursor_is_stale_or_exact(&cursor, previous)) {
144 return Ok(MetadataResolution::NoUpdate);
145 }
146
147 Ok(MetadataResolution::ParseFull(cursor))
148 }
149
150 fn resolve_parsed(
151 &self,
152 specs: SpecsResponseFull,
153 metadata_cursor: Option<MmapSyncCursor>,
154 previous: Option<&MmapSyncCursor>,
155 ) -> Result<Option<MmapResolvedUpdate>, StatsigErr> {
156 let cursor = cursor_from_specs_response(&specs)?;
157 self.headers.validate_cursor(&cursor)?;
158 if metadata_cursor
159 .as_ref()
160 .is_some_and(|metadata_cursor| metadata_cursor != &cursor)
161 {
162 return Err(invalid_mmap_response(
163 "The parsed config identity did not match its JSON metadata",
164 ));
165 }
166 if !specs.has_updates {
167 let previous = previous.ok_or_else(|| {
168 invalid_mmap_response("Received a no-update response before any mmap was published")
169 })?;
170 validate_no_update_identity(
171 previous,
172 Some(cursor.lcut),
173 ChecksumIdentity::known(cursor.checksum.as_deref()),
174 )?;
175 return Ok(None);
176 }
177 if previous.is_some_and(|previous| cursor_is_stale_or_exact(&cursor, previous)) {
178 return Ok(None);
179 }
180
181 Ok(Some(MmapResolvedUpdate { specs, cursor }))
182 }
183}
184
185impl MmapResponseFormat {
186 fn from_response(response_data: &ResponseData) -> Self {
187 let is_protobuf = response_data
188 .get_header_ref("content-type")
189 .is_some_and(|value| value.contains("application/octet-stream"))
190 && response_data
191 .get_header_ref("content-encoding")
192 .is_some_and(|value| value.contains("statsig-br"));
193
194 if is_protobuf {
195 Self::Protobuf
196 } else {
197 Self::Json
198 }
199 }
200}
201
202impl MmapResponseHeaders {
203 fn from_response(response_data: &ResponseData) -> Result<Self, StatsigErr> {
204 let lcut = response_data
205 .get_header_ref("x-since-time")
206 .map(|value| {
207 value.parse::<u64>().map_err(|_| {
208 invalid_mmap_response("The x-since-time response header was not a valid u64")
209 })
210 })
211 .transpose()?;
212 let checksum = match response_data.get_header_ref("x-checksum") {
213 Some(value) => ChecksumHeader::Present(normalize_checksum(Some(value.clone()))),
214 None => ChecksumHeader::Missing,
215 };
216
217 if checksum.identity().value.is_some() && lcut.is_none() {
218 return Err(invalid_mmap_response(
219 "The response included x-checksum without x-since-time",
220 ));
221 }
222
223 Ok(Self {
224 lcut,
225 checksum,
226 cache_hit: response_data
227 .get_header_ref("x-cache-hit")
228 .is_some_and(|value| value.eq_ignore_ascii_case("true")),
229 })
230 }
231
232 fn is_no_update_without_body(&self, previous: &MmapSyncCursor) -> bool {
233 let Some(lcut) = self.lcut else {
234 return false;
235 };
236
237 lcut < previous.lcut || (lcut == previous.lcut && self.checksum.matches(&previous.checksum))
238 }
239
240 fn validate_cursor(&self, cursor: &MmapSyncCursor) -> Result<(), StatsigErr> {
241 if self.lcut.is_some_and(|lcut| lcut != cursor.lcut) {
242 return Err(invalid_mmap_response(
243 "The x-since-time response header did not match the config response time",
244 ));
245 }
246 if self
247 .checksum
248 .identity()
249 .conflicts_with(ChecksumIdentity::known(cursor.checksum.as_deref()))
250 {
251 return Err(invalid_mmap_response(
252 "The x-checksum response header did not match the config response checksum",
253 ));
254 }
255
256 Ok(())
257 }
258}
259
260impl ChecksumHeader {
261 fn identity(&self) -> ChecksumIdentity<'_> {
262 match self {
263 Self::Missing => ChecksumIdentity::unknown(),
264 Self::Present(checksum) => ChecksumIdentity::known(checksum.as_deref()),
265 }
266 }
267
268 fn matches(&self, expected: &Option<String>) -> bool {
269 let identity = self.identity();
270 identity.is_known && identity.value == expected.as_deref()
271 }
272}
273
274impl<'a> ChecksumIdentity<'a> {
275 fn unknown() -> Self {
276 Self {
277 value: None,
278 is_known: false,
279 }
280 }
281
282 fn known(value: Option<&'a str>) -> Self {
283 Self {
284 value,
285 is_known: true,
286 }
287 }
288
289 fn prefer(self, fallback: Self) -> Self {
290 if self.is_known {
291 self
292 } else {
293 fallback
294 }
295 }
296
297 fn conflicts_with(self, other: Self) -> bool {
298 self.is_known && other.is_known && self.value != other.value
299 }
300}
301
302fn cursor_from_metadata(metadata: &MmapResponseMetadata) -> Result<MmapSyncCursor, StatsigErr> {
303 let lcut = metadata
304 .time
305 .filter(|lcut| *lcut > 0)
306 .ok_or_else(|| invalid_mmap_response("A config response did not include a valid time"))?;
307
308 Ok(MmapSyncCursor {
309 lcut,
310 checksum: normalize_checksum(metadata.checksum.clone()),
311 })
312}
313
314fn cursor_from_specs_response(
315 specs_response: &SpecsResponseFull,
316) -> Result<MmapSyncCursor, StatsigErr> {
317 if specs_response.time == 0 {
318 return Err(invalid_mmap_response(
319 "A config response did not include a valid time",
320 ));
321 }
322
323 Ok(MmapSyncCursor {
324 lcut: specs_response.time,
325 checksum: normalize_checksum(specs_response.checksum.clone()),
326 })
327}
328
329fn normalize_checksum(checksum: Option<String>) -> Option<String> {
330 checksum.filter(|checksum| !checksum.is_empty())
331}
332
333fn cursor_is_stale_or_exact(cursor: &MmapSyncCursor, previous: &MmapSyncCursor) -> bool {
334 cursor.lcut < previous.lcut
335 || (cursor.lcut == previous.lcut && cursor.checksum == previous.checksum)
336}
337
338fn validate_json_no_update(
339 metadata: &MmapResponseMetadata,
340 headers: &MmapResponseHeaders,
341 previous: &MmapSyncCursor,
342) -> Result<(), StatsigErr> {
343 let body_checksum = match metadata.checksum.as_deref() {
344 Some(checksum) => ChecksumIdentity::known((!checksum.is_empty()).then_some(checksum)),
345 None => ChecksumIdentity::unknown(),
346 };
347 if body_checksum.is_known && metadata.time.is_none() {
348 return Err(invalid_mmap_response(
349 "A no-update response included checksum without time",
350 ));
351 }
352 if let (Some(header_lcut), Some(body_lcut)) = (headers.lcut, metadata.time) {
353 if header_lcut != body_lcut {
354 return Err(invalid_mmap_response(
355 "The x-since-time response header did not match the JSON response time",
356 ));
357 }
358 }
359
360 let header_checksum = headers.checksum.identity();
361 if header_checksum.conflicts_with(body_checksum) {
362 return Err(invalid_mmap_response(
363 "The x-checksum response header did not match the JSON response checksum",
364 ));
365 }
366
367 let lcut = metadata.time.or(headers.lcut);
368 validate_no_update_identity(previous, lcut, body_checksum.prefer(header_checksum))
369}
370
371fn validate_no_update_identity(
372 previous: &MmapSyncCursor,
373 lcut: Option<u64>,
374 checksum: ChecksumIdentity<'_>,
375) -> Result<(), StatsigErr> {
376 let Some(lcut) = lcut else {
377 return Ok(());
378 };
379
380 if lcut > previous.lcut
381 || (lcut == previous.lcut
382 && checksum.is_known
383 && previous.checksum.as_deref() != checksum.value)
384 {
385 return Err(invalid_mmap_response(
386 "A no-update response advertised a changed config identity",
387 ));
388 }
389
390 Ok(())
391}
392
393fn parse_specs_response_data(
394 response_data: &mut ResponseData,
395 format: MmapResponseFormat,
396) -> Result<SpecsResponseFull, StatsigErr> {
397 if matches!(format, MmapResponseFormat::Protobuf) {
398 let current = SpecsResponseFull::default();
399 let mut next = SpecsResponseFull::default();
400 deserialize_protobuf(
401 &OpsStatsForInstance::new(),
402 ¤t,
403 &mut next,
404 response_data,
405 )?;
406 return Ok(next);
407 }
408
409 response_data.deserialize_into::<SpecsResponseFull>()
410}
411
412fn invalid_mmap_response(message: &str) -> StatsigErr {
413 StatsigErr::InvalidOperation(format!("Invalid mmap config response: {message}"))
414}