use serde_json::Value;
fn fnv1a_feed(h: &mut u64, bytes: &[u8]) {
for &b in bytes {
*h ^= b as u64;
*h = h.wrapping_mul(0x100000001b3);
}
}
const FNV1A_SEED: u64 = 0xcbf29ce484222325;
fn fnv1a64(bytes: &[u8]) -> u64 {
let mut h = FNV1A_SEED;
fnv1a_feed(&mut h, bytes);
h
}
fn first_forwarded_value<'a>(mut get: impl FnMut(&str) -> Option<&'a str>) -> Option<&'a str> {
if let Some(xff) = get("x-forwarded-for")
&& let Some(first) = xff.split(',').next().map(str::trim)
&& !first.is_empty()
{
return Some(first);
}
get("x-real-ip").map(str::trim).filter(|v| !v.is_empty())
}
pub fn rollout_identity<'a>(metadata: &'a Value, sticky_header: &str) -> Option<&'a str> {
let headers = metadata.get("headers")?.as_object()?;
if !sticky_header.is_empty()
&& let Some((_, v)) = headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(sticky_header))
&& let Some(v) = v.as_str()
&& !v.is_empty()
{
return Some(v);
}
first_forwarded_value(|name| headers.get(name).and_then(|v| v.as_str()))
}
pub fn serialize_task_trace_capped(
trace: Option<&dataflow_rs::ExecutionTrace>,
max_bytes: usize,
context: &str,
) -> Option<String> {
let json = serde_json::to_string(trace?).ok()?;
if max_bytes > 0 && json.len() > max_bytes {
crate::metrics::record_error("task_trace_size_exceeded");
tracing::warn!(
context = %context,
task_trace_bytes = json.len(),
limit_bytes = max_bytes,
"task_trace_json exceeds queue.max_result_size_bytes; dropping task detail"
);
return None;
}
Some(json)
}
pub fn rollout_bucket_for_identity(identity: Option<&str>) -> u8 {
match identity {
Some(id) if !id.is_empty() => (fnv1a64(id.as_bytes()) % 100) as u8,
_ => (rand::random::<u32>() % 100) as u8,
}
}
#[cfg(test)]
mod tests {
use super::*;
use dataflow_rs::Message;
use serde_json::json;
fn ingress_message(payload: Value, metadata: Value, identity: Option<&str>) -> Message {
Message::builder()
.payload_json(&payload)
.metadata_json(&metadata)
.routing_bucket(rollout_bucket_for_identity(identity))
.build()
}
#[test]
fn test_task_trace_cap_drops_oversized_detail() {
let trace = dataflow_rs::ExecutionTrace::new();
assert!(serialize_task_trace_capped(Some(&trace), 1024, "t").is_some());
assert!(serialize_task_trace_capped(Some(&trace), 1, "t").is_none());
assert!(serialize_task_trace_capped(Some(&trace), 0, "t").is_some());
assert!(serialize_task_trace_capped(None, 1024, "t").is_none());
}
#[test]
fn ingress_seeds_metadata_with_literal_keys() {
let msg = ingress_message(json!({}), json!({"source": "test", "a.b": 2}), None);
assert_eq!(
msg.metadata().get("source").and_then(|v| v.as_str()),
Some("test")
);
assert_eq!(msg.metadata().get("a.b").and_then(|v| v.as_i64()), Some(2));
assert!(
msg.metadata().get("a").is_none(),
"a dotted metadata key must not be re-read as a path"
);
}
#[test]
fn test_rollout_bucket_is_sticky_per_identity() {
let a1 = rollout_bucket_for_identity(Some("10.0.0.7"));
let a2 = rollout_bucket_for_identity(Some("10.0.0.7"));
assert_eq!(a1, a2, "same identity must map to the same bucket");
assert!(a1 < 100);
let buckets: std::collections::HashSet<u8> = (0..50)
.map(|i| rollout_bucket_for_identity(Some(&format!("user-{i}"))))
.collect();
assert!(buckets.len() > 10, "expected spread, got {buckets:?}");
}
#[test]
fn test_rollout_identity_prefers_sticky_header() {
let metadata = json!({
"method": "POST",
"headers": {
"x-user-id": "user-7",
"x-forwarded-for": "10.1.1.1, 10.1.1.2"
}
});
assert_eq!(rollout_identity(&metadata, "X-User-Id"), Some("user-7"));
assert_eq!(rollout_identity(&metadata, ""), Some("10.1.1.1"));
let metadata = json!({"headers": {"x-real-ip": "10.2.2.2"}});
assert_eq!(rollout_identity(&metadata, "x-user-id"), Some("10.2.2.2"));
assert_eq!(rollout_identity(&json!({}), "x-user-id"), None);
}
#[test]
fn test_rollout_bucket_empty_identity_falls_back_to_random() {
let buckets: std::collections::HashSet<u8> = (0..100)
.map(|_| rollout_bucket_for_identity(Some("")))
.collect();
assert!(buckets.len() > 1, "empty identity should randomize");
}
#[test]
fn the_routing_bucket_is_not_part_of_the_message_body() {
let msg = ingress_message(json!({"order_id": 7}), json!({}), Some("caller-1"));
assert!(
msg.routing_bucket().is_some(),
"precondition: the ingress set a bucket"
);
let body: Value = msg.data().into();
assert_eq!(body, json!({}), "routing must not write into `data`");
let serialized = serde_json::to_string(&msg).expect("message serializes");
assert!(
!serialized.contains("_rollout_bucket") && !serialized.contains("routing_bucket"),
"the bucket must not reach the persisted message: {serialized}"
);
}
}