1use crate::{engine::GASPSyncResult, meter::SyncCost, sync_load::SyncFilter};
4use serde::{Deserialize, Serialize};
5
6pub const BACKFILL_REQUEST_MS: u64 = 180_000;
16pub const BACKFILL_OPERATIONS: u64 = 500;
17pub const BACKFILL_D1_QUERIES: u64 = 600;
18pub const BACKFILL_MAX_SLICES: u64 = 64;
19
20#[derive(Debug, Clone, Default, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct SyncRequest {
23 pub topics: Option<Vec<String>>,
24 pub peers: Option<Vec<String>>,
25 pub until: Option<Until>,
26 pub budget_ms: Option<u64>,
27 pub continuation: Option<String>,
28}
29
30#[derive(Debug, Clone, Copy, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum Until {
33 Tip,
34}
35
36impl SyncRequest {
37 pub fn parse(body: &str) -> Result<Self, String> {
38 let request = if body.trim().is_empty() {
39 Self::default()
40 } else {
41 serde_json::from_str(body).map_err(|e| e.to_string())?
42 };
43 request.validate()?;
44 Ok(request)
45 }
46
47 pub fn filter(&self) -> SyncFilter {
48 SyncFilter {
49 topics: self.topics.clone(),
50 peers: self.peers.clone(),
51 }
52 }
53
54 pub fn validate(&self) -> Result<(), String> {
55 if self.until.is_none() {
56 if self.budget_ms.is_some() || self.continuation.is_some() {
57 return Err("budget_ms and continuation require until: tip".into());
58 }
59 return Ok(());
60 }
61 let (topic, peer) = self.pair()?;
62 if self.budget_ms.is_none_or(|n| n == 0) {
63 return Err("until: tip requires a positive integer budget_ms".into());
64 }
65 if let Some(token) = &self.continuation {
66 let token = Continuation::decode(token)?;
67 if token.topic != topic || token.peer != peer {
68 return Err("continuation belongs to another topic or peer".into());
69 }
70 }
71 Ok(())
72 }
73
74 pub fn pair(&self) -> Result<(&str, &str), String> {
75 match (self.topics.as_deref(), self.peers.as_deref()) {
76 (Some([topic]), Some([peer]))
77 if !topic.is_empty()
78 && topic.len() <= 256
79 && !peer.is_empty()
80 && peer.len() <= 1024 =>
81 {
82 Ok((topic, peer))
83 }
84 _ => Err(
85 "until: tip requires one topic (1..256 bytes) and one peer (1..1024 bytes)".into(),
86 ),
87 }
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(deny_unknown_fields)]
93pub struct Continuation {
94 version: u8,
95 pub topic: String,
96 pub peer: String,
97 pub cursor: u64,
98}
99
100impl Continuation {
101 pub fn new(topic: &str, peer: &str, cursor: u64) -> Self {
102 Self {
103 version: 1,
104 topic: topic.into(),
105 peer: peer.into(),
106 cursor,
107 }
108 }
109
110 pub fn encode(&self) -> String {
111 serde_json::to_string(self).expect("continuation has only scalar fields")
112 }
113
114 pub fn decode(token: &str) -> Result<Self, String> {
115 if token.len() > 16_384 {
116 return Err("continuation exceeds 16384 bytes".into());
117 }
118 let token: Self = serde_json::from_str(token).map_err(|_| "invalid continuation")?;
119 if token.version != 1 {
120 return Err("unsupported continuation version".into());
121 }
122 Ok(token)
123 }
124}
125
126#[derive(Debug, Clone, Default, Serialize, Deserialize)]
127pub struct PeerSyncResult {
128 #[serde(flatten)]
129 pub cost: SyncCost,
130 pub slices: u64,
131 pub pages: u64,
132 pub reached_tip: bool,
133 pub cursor: u64,
134}
135
136#[derive(Debug, Clone, Serialize)]
137pub struct BackfillResult {
138 #[serde(flatten)]
139 pub sync: GASPSyncResult,
140 pub slices: u64,
141 pub reached_tip: bool,
142 pub stop_reason: String,
143 pub continuation: Option<String>,
144}
145
146#[derive(Clone)]
147pub(crate) struct Control {
148 pub meter: crate::meter::LoadMeter,
149 pub budget_ms: u64,
150}
151
152impl Control {
153 pub fn remaining_ms(&self) -> u64 {
154 self.budget_ms.saturating_sub(self.meter.snapshot().ms)
155 }
156
157 pub fn stop_reason(&self) -> Option<&'static str> {
158 let cost = self.meter.snapshot();
159 if cost.ms >= self.budget_ms {
160 Some("wall_budget")
161 } else if cost.d1_queries >= BACKFILL_D1_QUERIES
162 || cost
163 .calls_out
164 .saturating_add(cost.storage_reads)
165 .saturating_add(cost.storage_writes)
166 >= BACKFILL_OPERATIONS
167 {
168 Some("request_allowance")
169 } else {
170 None
171 }
172 }
173}
174
175type ExpiredMemo = std::rc::Rc<std::cell::RefCell<std::collections::HashSet<String>>>;
176pub(crate) struct Run {
177 pub fetcher: Option<std::rc::Rc<dyn crate::gasp::AncestorFetcher>>,
178 expired: std::cell::RefCell<std::collections::HashMap<String, ExpiredMemo>>,
179 single_only: std::cell::RefCell<std::collections::HashMap<String, SingleOnly>>,
183}
184type SingleOnly = std::rc::Rc<std::cell::Cell<bool>>;
185impl Run {
186 pub fn new(fetcher: Option<std::rc::Rc<dyn crate::gasp::AncestorFetcher>>) -> Self {
187 Self {
188 fetcher: fetcher
189 .map(|f| std::rc::Rc::new(crate::sync_fetcher::SyncFetcher::new(f)) as _),
190 expired: std::cell::RefCell::default(),
191 single_only: std::cell::RefCell::default(),
192 }
193 }
194 pub fn single_only(&self, peer: &str) -> SingleOnly {
195 self.single_only
196 .borrow_mut()
197 .entry(peer.to_string())
198 .or_default()
199 .clone()
200 }
201 pub fn expired(&self, topic: &str) -> ExpiredMemo {
202 self.expired
203 .borrow_mut()
204 .entry(topic.to_string())
205 .or_default()
206 .clone()
207 }
208}
209
210impl GASPSyncResult {
211 pub(crate) fn append_slice(&mut self, slice: Self) {
212 for topic in slice.topics_filtered {
213 if !self.topics_filtered.contains(&topic) {
214 self.topics_filtered.push(topic);
215 }
216 }
217 for (name, mut incoming) in slice.topics_synced {
218 let Some(topic) = self.topics_synced.get_mut(&name) else {
219 self.topics_synced.insert(name, incoming);
220 continue;
221 };
222 topic.cost.add(incoming.cost);
223 macro_rules! sum {
224 ($($field:ident),+ $(,)?) => { $(topic.$field = topic.$field.saturating_add(incoming.$field);)+ };
225 }
226 sum!(
227 roots_proven,
228 roots_unproven,
229 expired_refused,
230 pruned_inputs,
231 discarded_graphs,
232 anchor_walk_held_graphs,
233 finalized_graphs,
234 deadline_dropped_graphs,
235 deferred_graphs,
236 resumed_graphs,
237 converged_graphs,
238 stalled_graphs,
239 held_back_graphs,
240 backed_off_graphs,
241 waiting_graphs,
242 idle_counted,
243 unanchorable_graphs,
244 unanchorable_cleared,
245 absent_struck,
246 strikes_refused,
247 dead_graphs,
248 expired_swept,
249 memo_folded
250 );
251 topic.root_bounds.append(&mut incoming.root_bounds);
252 topic.peers_skipped.append(&mut incoming.peers_skipped);
253 topic.errors.append(&mut incoming.errors);
254 topic.route_faults.append(&mut incoming.route_faults);
255 topic.cursor_moves.append(&mut incoming.cursor_moves);
256 topic.dropped_graphs.append(&mut incoming.dropped_graphs);
257 topic.cursor_held_by = incoming.cursor_held_by;
258 for fault in incoming.faulted_roots {
259 if !topic
260 .faulted_roots
261 .iter()
262 .any(|f| f.peer == fault.peer && f.outpoint == fault.outpoint)
263 {
264 topic.faulted_roots.push(fault);
265 }
266 }
267 for (peer, result) in incoming.peer_results {
268 let held = topic.peer_results.entry(peer).or_default();
269 held.cost.add(result.cost);
270 held.slices += result.slices;
271 held.pages += result.pages;
272 held.reached_tip = result.reached_tip;
273 held.cursor = result.cursor;
274 }
275 }
276 }
277}