rovenue/attributes/
dispatcher.rs1use std::sync::Arc;
2use std::time::Duration;
3
4use crate::attributes::buffer::AttributeBuffer;
5use crate::error::RovenueResult;
6use crate::polling::scheduler::PollingScheduler;
7use crate::transport::http_client::HttpClient;
8
9const BATCH_LIMIT: usize = 200;
11
12pub struct AttributeDispatcher {
13 buffer: Arc<AttributeBuffer>,
14 http: Arc<HttpClient>,
15}
16
17impl AttributeDispatcher {
18 pub fn new(buffer: Arc<AttributeBuffer>, http: Arc<HttpClient>) -> Self {
19 Self { buffer, http }
20 }
21
22 pub fn flush_once(&self) -> RovenueResult<usize> {
29 let mut synced = 0;
30 loop {
31 let rows = self.buffer.list(BATCH_LIMIT)?;
32 let Some(first) = rows.first() else {
33 return Ok(synced);
34 };
35 let sub_id = first.rovenue_id.clone();
36 let run: Vec<_> = rows
37 .into_iter()
38 .take_while(|r| r.rovenue_id == sub_id)
39 .collect();
40 let ids: Vec<i64> = run.iter().map(|r| r.id).collect();
41 if sub_id.is_empty() {
44 self.buffer.delete(&ids)?;
45 continue;
46 }
47 let mut map = serde_json::Map::new();
49 for r in &run {
50 let v = match &r.value {
51 Some(s) => serde_json::Value::String(s.clone()),
52 None => serde_json::Value::Null,
53 };
54 map.insert(r.key.clone(), v);
55 }
56 if let Err(err) = self.http.post_attributes(&sub_id, &map) {
64 if !err.retryable
65 && err.kind != crate::error::ErrorKind::SubscriberUnresolvable
66 && matches!(err.http_status, Some(s) if (400..500).contains(&s))
67 {
68 self.buffer.delete(&ids)?;
69 }
70 return Err(err);
71 }
72 self.buffer.delete(&ids)?;
73 synced += map.len();
75 }
76 }
77
78 pub fn start(self: Arc<Self>, scheduler: &PollingScheduler) {
81 let me = Arc::clone(&self);
82 scheduler.register("attributes", Duration::from_secs(30), move || {
83 let _ = me.flush_once();
84 });
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::attributes::buffer::AttributeBuffer;
92 use crate::cache::store::CacheStore;
93 use std::sync::Arc;
94 use std::time::Duration;
95
96 fn http_unreachable() -> Arc<HttpClient> {
100 Arc::new(
101 HttpClient::new("http://127.0.0.1:1".to_string(), "pk_test_abc".into())
102 .with_max_attempts(1)
103 .with_min_backoff(Duration::from_millis(1))
104 .with_request_timeout(Duration::from_millis(200)),
105 )
106 }
107
108 #[test]
109 fn flush_drops_unattributable_rows_without_sending() {
110 let store = Arc::new(CacheStore::open_in_memory().unwrap());
111 let buf = Arc::new(AttributeBuffer::new(Arc::clone(&store)));
112 buf.set("", "$email", Some("a@b.com")).unwrap();
113 let dispatcher = AttributeDispatcher::new(Arc::clone(&buf), http_unreachable());
115 assert_eq!(dispatcher.flush_once().unwrap(), 0);
116 assert_eq!(buf.list(100).unwrap().len(), 0);
117 }
118
119 #[test]
120 fn flush_deletes_only_on_success() {
121 let mut server = mockito::Server::new();
122 let m = server
123 .mock("POST", "/v1/me/attributes")
124 .match_header("x-rovenue-app-user-id", "rov_x")
125 .match_body(mockito::Matcher::JsonString(
126 r#"{"attributes":{"$email":"a@b.com","country":null}}"#.into(),
127 ))
128 .with_status(200)
129 .with_body(r#"{"data":{"ok":true}}"#)
130 .create();
131
132 let store = Arc::new(CacheStore::open_in_memory().unwrap());
133 let buf = Arc::new(AttributeBuffer::new(Arc::clone(&store)));
134 buf.set("rov_x", "$email", Some("a@b.com")).unwrap();
135 buf.set("rov_x", "country", None).unwrap();
136 let http = Arc::new(
137 HttpClient::new(server.url(), "pk_test_abc".into())
138 .with_max_attempts(1)
139 .with_request_timeout(Duration::from_millis(500)),
140 );
141 let dispatcher = AttributeDispatcher::new(Arc::clone(&buf), http);
142 assert_eq!(dispatcher.flush_once().unwrap(), 2);
143 assert_eq!(buf.list(100).unwrap().len(), 0);
144 m.assert();
145 }
146
147 #[test]
151 fn flush_discards_queue_on_permanent_4xx() {
152 let mut server = mockito::Server::new();
153 let _m = server
154 .mock("POST", "/v1/me/attributes")
155 .with_status(400)
156 .with_body(r#"{"error":{"code":"VALIDATION_ERROR"}}"#)
157 .create();
158
159 let store = Arc::new(CacheStore::open_in_memory().unwrap());
160 let buf = Arc::new(AttributeBuffer::new(Arc::clone(&store)));
161 buf.set("rov_x", "$email", Some("a@b.com")).unwrap();
162 let http = Arc::new(
163 HttpClient::new(server.url(), "pk_test_abc".into())
164 .with_max_attempts(1)
165 .with_request_timeout(Duration::from_millis(500)),
166 );
167 let dispatcher = AttributeDispatcher::new(Arc::clone(&buf), http);
168 assert!(dispatcher.flush_once().is_err(), "a 400 must still surface");
169 assert_eq!(
170 buf.list(100).unwrap().len(),
171 0,
172 "a 4xx-rejected queue must be dropped, not retried forever"
173 );
174 }
175
176 #[test]
177 fn flush_keeps_queue_on_network_error() {
178 let store = Arc::new(CacheStore::open_in_memory().unwrap());
179 let buf = Arc::new(AttributeBuffer::new(Arc::clone(&store)));
180 buf.set("rov_x", "$email", Some("a@b.com")).unwrap();
181 let dispatcher = AttributeDispatcher::new(Arc::clone(&buf), http_unreachable());
182 assert!(dispatcher.flush_once().is_err());
183 assert_eq!(buf.list(100).unwrap().len(), 1);
185 }
186}