1use crate::bindings::golem::agent::durable_streams as wire;
16use crate::schema::wit::wire::Secret;
17use serde::Serialize;
18use serde::de::DeserializeOwned;
19use serde_json::value::RawValue;
20use std::sync::Arc;
21
22#[cfg(test)]
23use tests::resources as calls;
24#[cfg(not(test))]
25use wire as calls;
26
27pub use wire::{
28 DurableStreamAppendReceipt as AppendReceipt, DurableStreamCheckpoint as Checkpoint,
29 DurableStreamErrorKind as ErrorKind, DurableStreamTransport as Transport,
30};
31
32#[derive(Clone, Debug)]
34pub struct Producer {
35 pub id: String,
36 pub epoch: u64,
37 pub sequence: u64,
38}
39
40#[derive(Debug)]
42pub struct Error {
43 pub kind: ErrorKind,
44 pub message: String,
45 pub retry_after_ms: Option<u64>,
46 pub producer_epoch: Option<u64>,
47 pub expected_sequence: Option<u64>,
48}
49
50impl Error {
51 fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
52 Self {
53 kind,
54 message: message.into(),
55 retry_after_ms: None,
56 producer_epoch: None,
57 expected_sequence: None,
58 }
59 }
60}
61
62impl From<wire::DurableStreamError> for Error {
63 fn from(value: wire::DurableStreamError) -> Self {
64 Self {
65 kind: value.kind,
66 message: value.message,
67 retry_after_ms: value.retry_after_ms,
68 producer_epoch: value.producer_epoch,
69 expected_sequence: value.expected_sequence,
70 }
71 }
72}
73
74impl std::fmt::Display for Error {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 write!(f, "{:?}: {}", self.kind, self.message)
77 }
78}
79impl std::error::Error for Error {}
80
81#[derive(Clone, Debug)]
83pub struct RetryOptions {
84 pub max_retries: u32,
85 pub initial_delay_ms: u64,
86 pub max_delay_ms: u64,
87}
88
89impl Default for RetryOptions {
90 fn default() -> Self {
91 Self {
92 max_retries: 8,
93 initial_delay_ms: 100,
94 max_delay_ms: 30_000,
95 }
96 }
97}
98
99impl RetryOptions {
100 fn delay(&self, error: &Error, failures: u32) -> Option<u64> {
101 if failures >= self.max_retries
102 || !matches!(
103 error.kind,
104 ErrorKind::Timeout
105 | ErrorKind::Transport
106 | ErrorKind::RateLimited
107 | ErrorKind::Unavailable
108 )
109 {
110 return None;
111 }
112 let backoff = self
113 .initial_delay_ms
114 .saturating_mul(1u64 << failures.min(63))
115 .min(self.max_delay_ms);
116 Some(backoff.max(error.retry_after_ms.unwrap_or(0)))
117 }
118}
119
120async fn wait(delay_ms: u64) {
121 #[cfg(not(test))]
122 crate::wasip3::clocks::monotonic_clock::wait_for(delay_ms.saturating_mul(1_000_000)).await;
123 #[cfg(test)]
124 tests::WAITS.with_borrow_mut(|waits| waits.push(delay_ms));
125}
126
127#[derive(Clone, Debug)]
128pub struct ReadOptions {
129 pub checkpoint: Checkpoint,
130 pub transport: Transport,
132 pub timeout_ms: u64,
133 pub idle_delay_ms: u64,
134 pub retry: RetryOptions,
135 pub auth: Option<Arc<Secret>>,
137}
138
139impl Default for ReadOptions {
140 fn default() -> Self {
141 Self {
142 checkpoint: Checkpoint {
143 offset: "-1".into(),
144 cursor: None,
145 },
146 transport: Transport::LongPoll,
147 timeout_ms: 30_000,
148 idle_delay_ms: 100,
149 retry: RetryOptions::default(),
150 auth: None,
151 }
152 }
153}
154
155enum Items {
156 Json(Vec<Box<RawValue>>),
157 Bytes(Vec<u8>),
158}
159
160impl Items {
161 fn len(&self) -> usize {
162 match self {
163 Self::Json(items) => items.len(),
164 Self::Bytes(items) => items.len(),
165 }
166 }
167}
168
169struct Batch {
170 items: Items,
171 index: usize,
172 next: Checkpoint,
173 up_to_date: bool,
174 closed: bool,
175}
176
177pub struct ExternalDurableStream<T> {
182 reader: calls::DurableStreamReader,
183 mode: wire::DurableStreamMode,
184 request: wire::DurableStreamReadRequest,
185 options: ReadOptions,
186 decode: fn(&Items, usize) -> Result<T, Error>,
187 pending: Option<Batch>,
188 closed: bool,
189 failures: u32,
190 delay_ms: Option<u64>,
191}
192
193impl<T: DeserializeOwned> ExternalDurableStream<T> {
194 pub fn json(url: impl Into<String>, options: ReadOptions) -> Self {
195 Self::new(
196 url.into(),
197 options,
198 wire::DurableStreamMode::Json,
199 |items, index| {
200 let Items::Json(items) = items else {
201 unreachable!()
202 };
203 serde_json::from_str(items[index].get()).map_err(|e| {
204 Error::new(
205 ErrorKind::ProtocolError,
206 format!("JSON item decode failed: {e}"),
207 )
208 })
209 },
210 )
211 }
212}
213
214impl ExternalDurableStream<u8> {
215 pub fn bytes(url: impl Into<String>, options: ReadOptions) -> Self {
217 Self::new(
218 url.into(),
219 options,
220 wire::DurableStreamMode::Bytes,
221 |items, index| {
222 let Items::Bytes(items) = items else {
223 unreachable!()
224 };
225 Ok(items[index])
226 },
227 )
228 }
229}
230
231impl<T> ExternalDurableStream<T> {
232 fn new(
233 url: String,
234 mut options: ReadOptions,
235 mode: wire::DurableStreamMode,
236 decode: fn(&Items, usize) -> Result<T, Error>,
237 ) -> Self {
238 let reader = calls::DurableStreamReader::new(
239 &wire::DurableStreamReaderOptions {
240 url,
241 mode,
242 timeout_ms: options.timeout_ms,
243 },
244 options.auth.as_deref(),
245 );
246 options.auth = None;
247 Self {
248 reader,
249 mode,
250 request: wire::DurableStreamReadRequest {
251 checkpoint: options.checkpoint.clone(),
252 transport: Transport::CatchUp,
253 content_type: None,
254 },
255 options,
256 decode,
257 pending: None,
258 closed: false,
259 failures: 0,
260 delay_ms: None,
261 }
262 }
263
264 pub fn checkpoint(&self) -> &Checkpoint {
266 &self.request.checkpoint
267 }
268
269 pub fn close(&mut self) {
271 self.closed = true;
272 self.pending = None;
273 }
274
275 fn install(&mut self, batch: wire::DurableStreamBatch) -> Result<(), Error> {
276 if batch.next.offset == "now" {
277 return Err(Error::new(
278 ErrorKind::ProtocolError,
279 "server did not resolve now",
280 ));
281 }
282 let items = match self.mode {
283 wire::DurableStreamMode::Json => Items::Json(if batch.payload.is_empty() {
284 Vec::new()
285 } else {
286 serde_json::from_slice(&batch.payload).map_err(|e| {
287 Error::new(
288 ErrorKind::ProtocolError,
289 format!("JSON batch decode failed: {e}"),
290 )
291 })?
292 }),
293 wire::DurableStreamMode::Bytes => Items::Bytes(batch.payload),
294 };
295 self.request.content_type.get_or_insert(batch.content_type);
297 self.pending = Some(Batch {
298 items,
299 index: 0,
300 next: batch.next,
301 up_to_date: batch.up_to_date,
302 closed: batch.closed,
303 });
304 self.failures = 0;
305 Ok(())
306 }
307
308 pub async fn next(&mut self) -> Result<Option<T>, Error> {
309 loop {
310 if self.closed {
311 return Ok(None);
312 }
313 if let Some(batch) = &mut self.pending {
314 if batch.index < batch.items.len() {
315 let item = (self.decode)(&batch.items, batch.index)?;
316 batch.index += 1;
317 return Ok(Some(item));
318 }
319 let batch = self.pending.take().unwrap();
320 self.request.checkpoint = batch.next;
321 self.closed = batch.closed;
322 self.request.transport = if batch.up_to_date {
323 self.options.transport
324 } else {
325 Transport::CatchUp
326 };
327 if batch.items.len() == 0 && batch.up_to_date && !batch.closed {
328 self.delay_ms = Some(self.options.idle_delay_ms);
329 }
330 continue;
331 }
332 if let Some(delay) = self.delay_ms {
333 wait(delay).await;
334 self.delay_ms = None;
335 }
336 match self.reader.read(self.request.clone()).await {
337 Ok(batch) => self.install(batch)?,
338 Err(error) => {
339 let error = Error::from(error);
340 let Some(delay) = self.options.retry.delay(&error, self.failures) else {
341 return Err(error);
342 };
343 self.failures += 1;
344 self.delay_ms = Some(delay);
345 }
346 }
347 }
348 }
349
350 pub async fn collect(mut self) -> Result<Vec<T>, Error> {
351 let mut items = Vec::new();
352 while let Some(item) = self.next().await? {
353 items.push(item);
354 }
355 Ok(items)
356 }
357}
358
359#[cfg(feature = "export_golem_agentic")]
360impl<T: crate::IntoSchema + crate::FromSchema + Send + 'static> ExternalDurableStream<T> {
361 pub fn into_agent_stream(self) -> crate::agentic::AgentStream<T> {
364 crate::agentic::AgentStream::from_schema_stream(
365 crate::schema::SchemaValueStream::from_source(self),
366 |value| T::from_value(&value).map_err(|e| e.to_string()),
367 )
368 }
369}
370
371#[cfg(feature = "export_golem_agentic")]
372impl<T: crate::IntoSchema + Send + 'static> crate::schema::stream::SchemaValueStreamSource
373 for ExternalDurableStream<T>
374{
375 fn next(
376 &mut self,
377 ) -> std::pin::Pin<
378 Box<
379 dyn std::future::Future<
380 Output = Result<Option<crate::schema::wit::wire::SchemaValueTree>, String>,
381 > + Send
382 + '_,
383 >,
384 > {
385 Box::pin(async move {
386 match ExternalDurableStream::next(self)
387 .await
388 .map_err(|e| e.to_string())?
389 {
390 Some(value) => crate::schema::wit::encode_value_async(&value.to_value())
391 .await
392 .map(Some)
393 .map_err(|e| e.to_string()),
394 None => Ok(None),
395 }
396 })
397 }
398}
399
400#[derive(Clone, Debug)]
401pub struct WriteOptions {
402 pub producer_id: Option<String>,
404 pub epoch: u64,
405 pub timeout_ms: u64,
406 pub retry: RetryOptions,
407 pub auth: Option<Arc<Secret>>,
408}
409
410impl Default for WriteOptions {
411 fn default() -> Self {
412 Self {
413 producer_id: None,
414 epoch: 0,
415 timeout_ms: 30_000,
416 retry: RetryOptions::default(),
417 auth: None,
418 }
419 }
420}
421
422struct PendingAppend {
423 request: wire::DurableStreamAppendRequest,
424 failures: u32,
425 delay_ms: Option<u64>,
426}
427
428pub struct DurableStreamWriter {
432 writer: calls::DurableStreamWriter,
433 producer: Producer,
434 retry: RetryOptions,
435 pending: Option<PendingAppend>,
436 closed: bool,
437}
438
439const MAX_PRODUCER_NUMBER: u64 = (1u64 << 53) - 1;
440
441impl DurableStreamWriter {
442 pub fn new(
443 url: impl Into<String>,
444 content_type: impl Into<String>,
445 options: WriteOptions,
446 ) -> Result<Self, Error> {
447 if options.epoch > MAX_PRODUCER_NUMBER {
448 return Err(Error::new(
449 ErrorKind::InvalidRequest,
450 "producer epoch exceeds 2^53-1",
451 ));
452 }
453 let id = options
454 .producer_id
455 .clone()
456 .unwrap_or_else(|| crate::generate_idempotency_key().to_string());
457 Ok(Self {
458 writer: calls::DurableStreamWriter::new(
459 &wire::DurableStreamWriterOptions {
460 url: url.into(),
461 content_type: content_type.into(),
462 producer_id: id.clone(),
463 producer_epoch: options.epoch,
464 timeout_ms: options.timeout_ms,
465 },
466 options.auth.as_deref(),
467 ),
468 producer: Producer {
469 id,
470 epoch: options.epoch,
471 sequence: 0,
472 },
473 retry: options.retry,
474 pending: None,
475 closed: false,
476 })
477 }
478
479 pub fn producer(&self) -> &Producer {
480 &self.producer
481 }
482 pub fn has_pending(&self) -> bool {
483 self.pending.is_some()
484 }
485
486 fn prepare(
487 &mut self,
488 payload: wire::DurableStreamAppendPayload,
489 close: bool,
490 ) -> Result<(), Error> {
491 if self.pending.is_some() {
492 return Err(Error::new(
493 ErrorKind::SequenceConflict,
494 "resolve the pending append before supplying new data",
495 ));
496 }
497 if self.closed {
498 return Err(Error::new(ErrorKind::Closed, "writer is closed"));
499 }
500 if self.producer.sequence > MAX_PRODUCER_NUMBER {
501 return Err(Error::new(
502 ErrorKind::InvalidRequest,
503 "producer sequence exhausted",
504 ));
505 }
506 let empty = match &payload {
507 wire::DurableStreamAppendPayload::Json(v) => v.is_empty(),
508 wire::DurableStreamAppendPayload::Bytes(v) => v.is_empty(),
509 };
510 if empty && !close {
511 return Err(Error::new(
512 ErrorKind::InvalidRequest,
513 "empty append requires close",
514 ));
515 }
516 self.pending = Some(PendingAppend {
517 request: wire::DurableStreamAppendRequest {
518 payload,
519 sequence: self.producer.sequence,
520 close,
521 },
522 failures: 0,
523 delay_ms: None,
524 });
525 Ok(())
526 }
527
528 pub async fn append_json<T: Serialize>(
530 &mut self,
531 values: &[T],
532 close: bool,
533 ) -> Result<AppendReceipt, Error> {
534 let encoded = values
535 .iter()
536 .map(serde_json::to_string)
537 .collect::<Result<Vec<_>, _>>()
538 .map_err(|e| {
539 Error::new(
540 ErrorKind::InvalidRequest,
541 format!("JSON encode failed: {e}"),
542 )
543 })?;
544 self.prepare(wire::DurableStreamAppendPayload::Json(encoded), close)?;
545 self.retry_pending().await
546 }
547
548 pub async fn append_bytes(
549 &mut self,
550 bytes: &[u8],
551 close: bool,
552 ) -> Result<AppendReceipt, Error> {
553 self.prepare(
554 wire::DurableStreamAppendPayload::Bytes(bytes.to_vec()),
555 close,
556 )?;
557 self.retry_pending().await
558 }
559
560 pub async fn close(&mut self) -> Result<AppendReceipt, Error> {
562 self.prepare(wire::DurableStreamAppendPayload::Bytes(Vec::new()), true)?;
563 self.retry_pending().await
564 }
565
566 fn acknowledge(&mut self, receipt: &AppendReceipt) -> Result<(), Error> {
567 let pending = self
568 .pending
569 .as_ref()
570 .expect("acknowledgement requires a pending request");
571 if receipt.epoch != self.producer.epoch || receipt.sequence < pending.request.sequence {
572 return Err(Error::new(
573 ErrorKind::ProtocolError,
574 "invalid producer acknowledgement",
575 ));
576 }
577 if receipt.sequence > pending.request.sequence {
578 return Err(Error::new(
579 ErrorKind::ProducerDiverged,
580 "peer acknowledged a later producer sequence",
581 ));
582 }
583 if pending.request.close && !receipt.closed {
584 return Err(Error::new(
585 ErrorKind::ProtocolError,
586 "peer did not acknowledge closure",
587 ));
588 }
589 self.producer.sequence = pending.request.sequence + 1;
590 self.closed = receipt.closed;
591 self.pending = None;
592 Ok(())
593 }
594
595 pub async fn retry_pending(&mut self) -> Result<AppendReceipt, Error> {
596 loop {
597 let pending = self
598 .pending
599 .as_mut()
600 .ok_or_else(|| Error::new(ErrorKind::InvalidRequest, "no pending append"))?;
601 if let Some(delay) = pending.delay_ms {
602 wait(delay).await;
603 pending.delay_ms = None;
604 }
605 match self.writer.append(pending.request.clone()).await {
606 Ok(receipt) => {
607 self.acknowledge(&receipt)?;
608 return Ok(receipt);
609 }
610 Err(error) => {
611 let error = Error::from(error);
612 let Some(delay) = self.retry.delay(&error, pending.failures) else {
613 return Err(error);
614 };
615 pending.failures += 1;
616 pending.delay_ms = Some(delay);
617 }
618 }
619 }
620 }
621}
622
623#[cfg(test)]
624mod tests;