appcore_sync/sync/
client.rs1use crate::sync::error::{SyncError, SyncResult};
14use crate::sync::outbox::{
15 FileSyncOutbox, InMemorySyncOutbox, SyncOutbox, SyncOutboxReceipt, SyncOutboxStats,
16 MAX_OUTBOX_PAGE_BYTES,
17};
18use crate::sync::retry::{SyncPushMetrics, SyncRetryPolicy};
19use crate::sync::transport::HttpSyncTransport;
20use crate::sync::types::SyncMessage;
21use parking_lot::Mutex;
22use std::path::PathBuf;
23use std::sync::Arc;
24use std::time::{Duration, SystemTime, UNIX_EPOCH};
25
26#[derive(Clone)]
28pub struct FollowerSyncClient {
29 transport: HttpSyncTransport,
30 retry_policy: SyncRetryPolicy,
31 outbox: Arc<dyn SyncOutbox>,
32 flush_lock: Arc<Mutex<()>>,
33 metrics: Arc<Mutex<SyncPushMetrics>>,
34}
35
36impl std::fmt::Debug for FollowerSyncClient {
37 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 formatter
39 .debug_struct("FollowerSyncClient")
40 .field("retry_policy", &self.retry_policy)
41 .field("pending_len", &self.pending_len())
42 .field("metrics", &self.metrics())
43 .finish()
44 }
45}
46
47impl FollowerSyncClient {
48 pub fn new(transport: HttpSyncTransport) -> Self {
50 Self {
51 transport,
52 retry_policy: SyncRetryPolicy::default(),
53 outbox: Arc::new(InMemorySyncOutbox::new()),
54 flush_lock: Arc::new(Mutex::new(())),
55 metrics: Arc::new(Mutex::new(SyncPushMetrics::default())),
56 }
57 }
58
59 pub fn with_retry_policy(mut self, retry_policy: SyncRetryPolicy) -> Self {
61 self.retry_policy = retry_policy;
62 self
63 }
64
65 pub fn retry_policy(&self) -> SyncRetryPolicy {
67 self.retry_policy
68 }
69
70 pub fn with_outbox(mut self, outbox: Arc<dyn SyncOutbox>) -> Self {
72 self.outbox = outbox;
73 self
74 }
75
76 pub fn with_file_outbox(self, file_path: impl Into<PathBuf>) -> SyncResult<Self> {
78 Ok(self.with_outbox(Arc::new(FileSyncOutbox::new(file_path)?)))
79 }
80
81 pub fn metrics(&self) -> SyncPushMetrics {
83 *self.metrics.lock()
84 }
85
86 pub fn pending_len(&self) -> usize {
88 self.outbox.len().unwrap_or(0)
89 }
90
91 pub fn pending_messages(&self) -> SyncResult<Vec<SyncMessage>> {
96 self.outbox.messages()
97 }
98
99 pub fn pending_page(&self, limit: usize, max_bytes: usize) -> SyncResult<Vec<SyncMessage>> {
101 self.outbox.peek(limit, max_bytes)
102 }
103
104 pub fn outbox_stats(&self) -> SyncResult<SyncOutboxStats> {
106 self.outbox.stats()
107 }
108
109 pub fn cancel(&self) {
111 self.transport.cancel();
112 }
113
114 pub fn is_cancelled(&self) -> bool {
116 self.transport.is_cancelled()
117 }
118
119 pub fn flush_pending(&self) -> SyncResult<()> {
121 self.flush_queue().map(|_| ())
122 }
123
124 pub fn flush_pending_with_progress(&self) -> SyncResult<Option<SyncMessage>> {
126 self.flush_queue()
127 }
128
129 pub fn push_events(&self, message: &SyncMessage) -> SyncResult<()> {
131 if !self
132 .outbox
133 .try_enqueue(message.clone(), self.retry_policy.max_queue_len)?
134 {
135 let mut metrics = self.metrics.lock();
136 metrics.push_dropped += 1;
137 return Err(SyncError::TransportFailed("sync queue full".to_string()));
138 }
139 self.flush_queue().map(|_| ())
140 }
141
142 fn flush_queue(&self) -> SyncResult<Option<SyncMessage>> {
143 let _flush_guard = self.flush_lock.lock();
144 let mut last_acknowledged = None;
145 loop {
146 let now_ms = unix_time_ms();
147 let message = match self
148 .outbox
149 .next_ready(now_ms, 1, MAX_OUTBOX_PAGE_BYTES)?
150 .into_iter()
151 .next()
152 {
153 Some(message) => message,
154 None if self.outbox.is_empty()? => return Ok(last_acknowledged),
155 None => {
156 return Err(SyncError::TransportFailed(
157 "sync push retry deferred".to_string(),
158 ));
159 }
160 };
161 if let Err(error) = self.try_send_with_retry(&message) {
162 let mut metrics = self.metrics.lock();
163 metrics.push_failed += 1;
164 return Err(error);
165 }
166 let receipt = SyncOutboxReceipt::new(vec![message.batch_id.clone()])?;
167 self.outbox.acknowledge_receipt(&receipt)?;
168 let mut metrics = self.metrics.lock();
169 metrics.push_success += 1;
170 last_acknowledged = Some(message);
171 }
172 }
173
174 fn try_send_with_retry(&self, message: &SyncMessage) -> SyncResult<()> {
175 let max_attempts = self.retry_policy.max_attempts.max(1);
176 for attempt in 1..=max_attempts {
177 if self.transport.is_cancelled() {
178 return Err(SyncError::TransportFailed(
179 "sync push cancelled".to_string(),
180 ));
181 }
182 let mut metrics = self.metrics.lock();
183 metrics.push_attempt += 1;
184 drop(metrics);
185 if self.transport.post_sync_events(message).is_ok() {
186 return Ok(());
187 }
188 let next_ready_at_ms = unix_time_ms().saturating_add(self.retry_policy.backoff_ms);
189 match self
190 .outbox
191 .mark_attempt(&message.batch_id, next_ready_at_ms)
192 {
193 Ok(_) | Err(SyncError::OutboxOperationUnsupported(_)) => {}
194 Err(error) => return Err(error),
195 }
196 if attempt < max_attempts
197 && self.retry_policy.backoff_ms > 0
198 && self
199 .transport
200 .cancellation_token()
201 .wait_timeout(Duration::from_millis(self.retry_policy.backoff_ms))
202 {
203 return Err(SyncError::TransportFailed(
204 "sync push cancelled".to_string(),
205 ));
206 }
207 }
208 Err(SyncError::TransportFailed(
209 "sync push retry exhausted".to_string(),
210 ))
211 }
212}
213
214fn unix_time_ms() -> u64 {
215 SystemTime::now()
216 .duration_since(UNIX_EPOCH)
217 .map(|duration| u64::try_from(duration.as_millis()).unwrap_or(u64::MAX))
218 .unwrap_or(0)
219}