1use std::{future::Future, sync::Arc, time::Duration};
6
7use api::v2::client::{MessageReader, MessageWriter};
8use heddle_object_model::object::ContentHash;
9use prost::Message;
10#[cfg(feature = "native")]
11use repo::thread_replication::ThreadReplica;
12use tokio::{
13 sync::{Mutex, Notify, mpsc, oneshot, watch},
14 task::{AbortHandle, JoinHandle},
15};
16use tracing::{Instrument, instrument::WithSubscriber};
17
18use crate::{
19 contract::*,
20 replication::{Frame, InputUnit, Outbound, Session, store::ReplicaStore},
21 transport,
22};
23
24#[derive(Debug, thiserror::Error)]
25pub enum Error<E: std::error::Error + 'static> {
26 #[error(transparent)]
27 Transport(#[from] transport::Error),
28 #[error("replica store: {0}")]
29 Store(#[source] E),
30 #[error(transparent)]
31 Protocol(#[from] crate::replication::Error),
32 #[error("replication worker: {0}")]
33 Worker(String),
34 #[error("replication response budget exhausted; reopen from durable state")]
35 Backpressure,
36 #[error("Thread change feed stopped")]
37 FeedClosed,
38 #[error("Thread policy changed; reconnect from durable receipts")]
39 PolicyChanged,
40}
41pub type Result<T, E> = std::result::Result<T, Error<E>>;
42
43impl<E: std::error::Error + 'static> From<crate::replication::StoreError<E>> for Error<E> {
44 fn from(error: crate::replication::StoreError<E>) -> Self {
45 match error {
46 crate::replication::StoreError::Store(error) => Self::Store(error),
47 crate::replication::StoreError::Protocol(error) => Self::Protocol(error),
48 }
49 }
50}
51
52struct AbortOnDrop(AbortHandle);
53impl Drop for AbortOnDrop {
54 fn drop(&mut self) {
55 self.0.abort();
56 }
57}
58
59#[derive(Clone)]
62pub struct Feed {
63 thread: ContentHash,
64 changes: watch::Receiver<Option<i64>>,
65 _task: Option<Arc<AbortOnDrop>>,
66}
67impl Feed {
68 pub fn from_changes(thread: ContentHash, changes: watch::Receiver<Option<i64>>) -> Self {
72 Self {
73 thread,
74 changes,
75 _task: None,
76 }
77 }
78
79 #[cfg(feature = "native")]
80 pub async fn new(
81 replica: ThreadReplica,
82 ) -> std::result::Result<Self, crate::replication::native::Error> {
83 let thread = replica.thread_id();
84 let initial = replica.clone();
85 let generation = tokio::task::spawn_blocking(move || initial.generation())
86 .await
87 .map_err(crate::replication::native::Error::from)??;
88 let (sender, changes) = watch::channel(Some(generation));
89 let task = tokio::spawn(async move {
90 let mut interval = tokio::time::interval(Duration::from_millis(200));
91 loop {
92 interval.tick().await;
93 if sender.is_closed() {
94 break;
95 }
96 let replica = replica.clone();
97 let result = tokio::task::spawn_blocking(move || replica.generation()).await;
98 match result {
99 Ok(Ok(generation)) => {
100 sender.send_if_modified(|old| {
101 if *old == Some(generation) {
102 false
103 } else {
104 *old = Some(generation);
105 true
106 }
107 });
108 }
109 _ => {
110 let _ = sender.send(None);
111 break;
112 }
113 }
114 }
115 });
116 Ok(Self {
117 thread,
118 changes,
119 _task: Some(Arc::new(AbortOnDrop(task.abort_handle()))),
120 })
121 }
122}
123
124#[derive(Clone, Copy)]
125pub enum Side {
126 Initiator,
127 Acceptor,
128}
129impl Side {
130 fn decode<E: std::error::Error + 'static>(self, bytes: &[u8]) -> Result<Frame, E> {
131 input::reservation(bytes, 64)?;
132 Ok(match self {
133 Self::Initiator => Frame::from_response(
134 ReplicateThreadResponse::decode(bytes).map_err(transport::Error::from)?,
135 )?,
136 Self::Acceptor => Frame::from_request(
137 ReplicateThreadRequest::decode(bytes).map_err(transport::Error::from)?,
138 )?,
139 })
140 }
141 fn encode(self, frame: Frame) -> Vec<u8> {
142 match self {
143 Self::Initiator => frame.request().encode_to_vec(),
144 Self::Acceptor => frame.response().encode_to_vec(),
145 }
146 }
147}
148
149#[allow(clippy::large_enum_variant)] enum Event {
151 Incoming(Frame),
152 Announce,
153 Maintain,
154}
155
156struct Queued {
157 item: Outbound,
158 immediate_receipt: bool,
159 generation: u64,
160 delivered: Option<oneshot::Sender<()>>,
161}
162async fn completion<E: std::error::Error + 'static>(
163 queue: &mpsc::Sender<Queued>,
164 item: Outbound,
165 generation: u64,
166) -> Result<(), E> {
167 let (sent, received) = oneshot::channel();
168 queue
169 .send(Queued {
170 item,
171 immediate_receipt: true,
172 generation,
173 delivered: Some(sent),
174 })
175 .await
176 .map_err(|_| Error::Backpressure)?;
177 received
178 .await
179 .map_err(|_| Error::Worker("replication sender stopped before receipt flush".into()))
180}
181fn requires_disclosure_fence<E: std::error::Error + 'static>(unit: &InputUnit) -> Result<bool, E> {
182 use heddle_object_model::object::thread_replication::{
183 ThreadOperation, ThreadOperationBody,
184 metadata::{Control, ThreadControl},
185 };
186 let InputUnit::Operation(received) = unit else {
187 return Ok(false);
188 };
189 let operation = ThreadOperation::decode(&received.original.canonical)
190 .map_err(crate::replication::Error::from)?;
191 let ThreadOperationBody::Metadata(bytes) = &operation.body else {
194 return Ok(false);
195 };
196 let control = ThreadControl::decode(bytes).map_err(crate::replication::Error::from)?;
197 Ok(matches!(
198 control.control,
199 Control::Audience(_) | Control::Retention(_) | Control::Sharing(_)
200 ))
201}
202#[path = "live_replication_input.rs"]
203pub mod input;
204fn unit_acceptance(
205 unit: &InputUnit,
206) -> Option<&Arc<crypto::original_boundary_acceptance::SignedBoundaryAcceptance>> {
207 let InputUnit::Operation(received) = unit else {
208 return None;
209 };
210 received
211 .authority_admission
212 .as_ref()?
213 .boundary_acceptance
214 .as_ref()
215}
216fn retained_unit_bytes(units: &[InputUnit]) -> usize {
220 units
221 .iter()
222 .enumerate()
223 .map(|(index, unit)| {
224 let InputUnit::Operation(received) = unit else {
225 return 0;
226 };
227 let mut bytes =
228 received.original.canonical.capacity() + received.original.signature.capacity();
229 if let Some(receipt) = &received.authority_admission {
230 bytes += receipt.canonical.capacity() + receipt.signature.capacity();
231 }
232 if let Some(acceptance) = unit_acceptance(unit)
233 && !units[..index]
234 .iter()
235 .filter_map(unit_acceptance)
236 .any(|prior| Arc::ptr_eq(prior, acceptance))
237 {
238 bytes += std::mem::size_of_val(acceptance.as_ref())
239 + 2 * std::mem::size_of::<usize>()
240 + acceptance.canonical.capacity()
241 + acceptance.signature.capacity();
242 }
243 bytes
244 })
245 .sum()
246}
247
248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
251pub enum Activity {
252 Idle,
256 InputConsumed {
259 remaining_bytes: usize,
260 },
261 Check,
262 Receive,
266 Work,
268 ReceiptWork,
270 ReceiptCheck,
272 Bookkeeping,
274}
275
276pub trait ActivityGuard: Send {
280 type Retained: Send;
281 fn finish(self, encoded_bytes: usize) -> std::result::Result<Self::Retained, transport::Error>;
282}
283impl ActivityGuard for () {
284 type Retained = ();
285 fn finish(self, _: usize) -> std::result::Result<(), transport::Error> {
286 Ok(())
287 }
288}
289
290pub async fn run<B, R, W, G, F, A>(
295 session: Session<B>,
296 reader: R,
297 writer: W,
298 side: Side,
299 feed: &Feed,
300 authorize: G,
301) -> Result<(), B::Error>
302where
303 B: ReplicaStore,
304 R: MessageReader<Error = transport::Error>,
305 W: MessageWriter<Error = transport::Error> + 'static,
306 G: Fn(Activity) -> F + Clone + Send + Sync + 'static,
307 F: Future<Output = std::result::Result<A, transport::Error>> + Send,
308 A: ActivityGuard,
309{
310 run_with_idle_clock(
311 session,
312 reader,
313 writer,
314 side,
315 feed,
316 authorize,
317 tokio::time::interval(Duration::from_secs(1)),
318 )
319 .await
320}
321
322async fn run_with_idle_clock<B, R, W, G, F, A>(
325 mut session: Session<B>,
326 mut reader: R,
327 mut writer: W,
328 side: Side,
329 feed: &Feed,
330 authorize: G,
331 mut heartbeat: tokio::time::Interval,
332) -> Result<(), B::Error>
333where
334 B: ReplicaStore,
335 R: MessageReader<Error = transport::Error>,
336 W: MessageWriter<Error = transport::Error> + 'static,
337 G: Fn(Activity) -> F + Clone + Send + Sync + 'static,
338 F: Future<Output = std::result::Result<A, transport::Error>> + Send,
339 A: ActivityGuard,
340{
341 if feed.thread != session.replica.thread_id() {
342 return Err(transport::Error::Protocol("change feed belongs to another Thread").into());
343 }
344 drop(authorize(Activity::Check).await?);
345 let mut changes = feed.changes.clone();
346 let (queue, mut outgoing) = mpsc::channel::<Queued>(256);
347 let progress = Arc::new(Notify::new());
348 let sender_progress = progress.clone();
349 let (completions, mut incoming_completions) = mpsc::channel::<Queued>(1);
350 let sender_session = session.clone();
351 let sender_authorize = authorize.clone();
352 let delivery = Arc::new(Mutex::new(()));
353 let sender_delivery = delivery.clone();
354 let (disclosures, mut disclosure_changes) = watch::channel(0u64);
355 let mut disclosure_generation = 0u64;
356 let mut sender: JoinHandle<Result<(), B::Error>> = tokio::spawn(
357 (async move {
358 let mut deferred = None;
359 let mut priority = None;
360 let mut priority_closed = false;
361 loop {
362 let next = if let Some(item) = priority.take() { Some(item) }
363 else if let Ok(item) = incoming_completions.try_recv() { Some(item) }
364 else if let Some(item) = deferred.take() { Some(item) }
365 else { tokio::select! {
366 biased;
367 item = incoming_completions.recv(), if !priority_closed => match item { Some(item) => Some(item), None => { priority_closed = true; continue; } },
368 item = outgoing.recv() => item,
369 }};
370 let Some(queued) = next else { break; };
371 sender_progress.notify_one();
372 let generation = queued.generation;
373 let immediate_receipt = queued.immediate_receipt;
374 if generation != *disclosure_changes.borrow_and_update() {
375 continue;
376 }
377 let session = sender_session.clone();
378 let gate = sender_authorize.clone();
379 let activity = if immediate_receipt { gate(Activity::ReceiptWork).await? } else {
383 tokio::select! {
384 biased;
385 changed = disclosure_changes.changed() => {
386 changed.map_err(|_| Error::FeedClosed)?;
387 continue;
388 }
389 received = incoming_completions.recv(), if !priority_closed => {
390 if let Some(received) = received {
391 deferred = Some(queued);
392 priority = Some(received);
393 continue;
394 }
395 priority_closed = true;
396 deferred = Some(queued);
397 continue;
398 }
399 activity = gate(Activity::Work) => activity?,
400 }
401 };
402 let Queued { item, delivered, .. } = queued;
403 let prepared = async {
404 let frame = match item {
405 Outbound::Operation(id) => session.export_operation(id).await?,
406 Outbound::Frame(frame) => {
407 if let Frame::Have(have) = &frame {
408 let allowed = session.export_facets().await?;
409 for frontier in &have.frontiers {
410 if !allowed.contains(&crate::replication::native_facet(
411 frontier.facet,
412 )?) {
413 return Err(transport::Error::Protocol(
414 "sharing policy changed before disclosure",
415 )
416 .into());
417 }
418 }
419 }
420 frame
421 }
422 };
423 let encoded = side.encode(frame);
424 let retained = activity.finish(encoded.len())?;
425 Ok::<_, Error<B::Error>>((encoded, retained))
426 }.await;
427 if generation != *disclosure_changes.borrow_and_update() { continue; }
428 let (encoded, retained) = prepared?;
429 let delivery_guard = sender_delivery.lock().await;
430 if generation != *disclosure_changes.borrow_and_update() {
431 continue;
432 }
433 drop(gate(if immediate_receipt { Activity::ReceiptCheck } else { Activity::Check }).await?);
436 writer.send(encoded).await?;
437 drop(delivery_guard);
438 drop(retained);
439 if let Some(delivered) = delivered { let _ = delivered.send(()); }
440 }
441 writer.finish().await?;
442 Ok(())
443 })
444 .in_current_span()
445 .with_current_subscriber(),
446 );
447 let _sender_guard = AbortOnDrop(sender.abort_handle());
450 heartbeat.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
451 let mut announce = true;
452 let mut maintain = true;
453 loop {
454 let event = tokio::select! {
455 result = &mut sender => return result.map_err(worker)?,
456 result = reader.next() => match result? {
457 Some(bytes) => Event::Incoming(side.decode(&bytes)?),
458 None => break,
459 },
460 result = changes.changed() => {
461 result.map_err(|_| Error::FeedClosed)?;
462 if changes.borrow_and_update().is_none() { return Err(Error::FeedClosed); }
463 announce = true;
464 maintain = true;
465 continue
466 },
467 _ = std::future::ready(()), if announce && queue.capacity() > 128 => Event::Announce,
468 _ = std::future::ready(()), if maintain && queue.capacity() > 128 => Event::Maintain,
469 _ = progress.notified() => continue,
470 _ = heartbeat.tick() => {
471 drop(authorize(Activity::Idle).await?);
472 continue
473 }
474 };
475 let event = match event {
476 Event::Incoming(frame) => {
477 maintain = true;
478 let preflight = authorize(Activity::Receive).await?;
479 let units = session.input_units(frame)?;
480 drop(preflight);
481 let container_bytes = units.capacity() * std::mem::size_of::<InputUnit>();
482 let mut units = units.into_iter();
483 while let Some(unit) = units.next() {
484 let immediate_receipt = matches!(&unit, InputUnit::Operation(_));
485 let policy = requires_disclosure_fence(&unit)?;
486 let activity = authorize(Activity::Receive).await?;
487 let delivery_guard = if policy {
488 let guard = delivery.lock().await;
489 disclosure_generation = disclosure_generation
490 .checked_add(1)
491 .ok_or(Error::Backpressure)?;
492 disclosures.send_replace(disclosure_generation);
493 Some(guard)
494 } else {
495 None
496 };
497 let output = session.handle_unit(unit).await?;
498 drop(activity);
499 drop(delivery_guard);
500 let remaining_bytes = if units.len() == 0 {
501 0
502 } else {
503 container_bytes + retained_unit_bytes(units.as_slice())
504 };
505 if units.len() == 0 {
508 drop(units);
509 units = Vec::new().into_iter();
510 }
511 drop(authorize(Activity::InputConsumed { remaining_bytes }).await?);
512 for item in output {
513 if immediate_receipt && matches!(&item, Outbound::Frame(Frame::Receipt(_)))
514 {
515 completion(&completions, item, disclosure_generation).await?;
516 } else {
517 queue
518 .try_send(Queued {
519 item,
520 immediate_receipt: false,
521 generation: disclosure_generation,
522 delivered: None,
523 })
524 .map_err(|_| Error::Backpressure)?;
525 }
526 }
527 if session.has_input_bookkeeping() {
528 let bookkeeping = authorize(Activity::Bookkeeping).await?;
529 session.finish_input_bookkeeping().await?;
530 drop(bookkeeping);
531 }
532 if policy {
533 return Err(Error::PolicyChanged);
534 }
535 }
536 continue;
537 }
538 other => other,
539 };
540 let activity = authorize(Activity::Receive).await?;
541 let output = match event {
542 Event::Incoming(frame) => {
543 maintain = true;
544 session.handle_input(frame).await?
545 }
546 Event::Announce => {
547 let frame = session.announcement().await?;
548 announce = frame.is_some();
549 frame.into_iter().map(Outbound::Frame).collect()
550 }
551 Event::Maintain => {
552 let frame = session.control().await?;
553 maintain = frame.is_some();
554 frame.into_iter().map(Outbound::Frame).collect()
555 }
556 };
557 drop(activity);
558 for item in output {
559 queue
560 .try_send(Queued {
561 item,
562 immediate_receipt: false,
563 generation: disclosure_generation,
564 delivered: None,
565 })
566 .map_err(|_| Error::Backpressure)?;
567 }
568 }
569 drop(queue);
570 drop(completions);
571 sender.await.map_err(worker)?
572}
573
574fn worker<E: std::error::Error + 'static>(error: tokio::task::JoinError) -> Error<E> {
575 Error::Worker(error.to_string())
576}
577
578#[cfg(all(test, feature = "native"))]
579#[path = "live_replication_tests.rs"]
580mod tests;
581
582#[cfg(all(test, feature = "native"))]
583#[path = "live_replication_schedule_tests.rs"]
584mod schedule_tests;