1use std::collections::{BTreeMap, HashMap};
18use std::sync::{Arc, Mutex};
19use tokio::sync::{Semaphore, SemaphorePermit, mpsc, oneshot};
21use crate::Frame;
23use harmos_domain::Message;
24use harmos_domain::operations::instance::Id;
25use harmos_sdk::operations::{CHILD_METHOD, OperationDeclaration, WireChildAction, WireChildControl};
26
27const PENDING_CALLS: usize = 256;
30const PUBLICATION_BATCH_BYTES: usize = 1024 * 1024;
32
33struct PendingCalls {
40 state: Mutex<PendingState>,
41 capacity: Semaphore,
42}
43
44struct PendingState {
45 closed: bool,
46 next_id: u64,
47 waiters: HashMap<u64, oneshot::Sender<Result<crate::FramePayload, CallError>>>,
48}
49
50struct PendingCall<'a> {
51 pending: &'a PendingCalls,
52 id: u64,
53 _permit: SemaphorePermit<'a>,
54 cancel: Option<mpsc::Permit<'a, Outgoing>>,
55}
56
57#[derive(Clone)]
58pub struct Supervisor {
59 outgoing: mpsc::Sender<Outgoing>,
60 sequences: Arc<Mutex<BTreeMap<&'static str, u64>>>,
61 pending: Arc<PendingCalls>,
62 operation_context: Option<[u8; 32]>,
63 generation_context: Option<[u8; 32]>,
64 assignments: harmos_sdk::operations::OperationAssignments,
65 call: Arc<harmos_domain::operations::CallContext>,
66 clock: fn() -> Option<i64>,
67}
68
69pub(crate) enum Outgoing {
72 Frame(Frame),
73 Started(u64),
74 Finished(u64),
75 Exiting(String),
76 Abandon,
77}
78
79#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
80pub enum CallError {
81 #[error("`{method}` requires process-local request ownership")]
82 LocalResources { method: &'static str },
83
84 #[error("caller may not invoke operation `{operation}` without a matching host assignment")]
85 Unassigned { operation: &'static str },
86
87 #[error("{}", refused_line(*.code, .failure))]
90 Refused {
91 code: i64,
92
93 #[source]
94 failure: harmos_domain::Failure,
95 },
96
97 #[error("the host connection is unavailable")]
98 Unavailable,
99
100 #[error("the host returned a malformed reply for `{method}`: {message}")]
101 Malformed { method: &'static str, message: String },
102
103 #[error("the request for `{method}` requires {encoded} bytes; the frame limit is {limit}")]
104 Oversized { method: &'static str, encoded: usize, limit: usize },
105
106 #[error(transparent)]
107 Publication(#[from] PublicationError),
108}
109
110#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
111pub enum PublicationError {
112 #[error("publication sequence exhausted for stream `{stream}`")]
113 SequenceExhausted { stream: &'static str },
114
115 #[error("the observer did not declare stream `{stream}` in streams(...)")]
116 Undeclared { stream: &'static str },
117
118 #[error("the publication requires {encoded} bytes; the frame limit is {limit}")]
119 Oversized { encoded: usize, limit: usize },
120}
121
122impl From<CallError> for harmos_domain::Refusal {
129 fn from(error: CallError) -> Self {
130 match error {
131 CallError::Refused { code, failure } => Self::failed(code, failure),
132 error => Self::of(error),
133 }
134 }
135}
136
137impl harmos_sdk::lifecycle::Context for Supervisor {
138 type Configuration = harmos_sdk::__private::WireTable;
139}
140
141impl harmos_sdk::plugin::Context for Supervisor {
142 fn assign_operations(&self, bytes: &[u8]) -> Result<(), harmos_sdk::operations::ServingError> {
143 self.assignments.install(bytes)
144 }
145
146 fn with_generation(&self, token: [u8; 32]) -> Self {
147 let mut host = self.clone();
148 host.generation_context = Some(token);
149 host
150 }
151}
152
153impl harmos_sdk::operations::Caller for Supervisor {
154 fn call(
155 &self, target: harmos_sdk::operations::Target, request: Box<dyn std::any::Any + Send>,
156 ) -> harmos_sdk::operations::CallFuture<'_> {
157 Box::pin(async move {
158 let result = async move {
159 if self.operation_context.is_none() && self.generation_context.is_none() {
160 return Err(CallError::Unavailable);
161 }
162 let assigned = self.assignments.resolve_target(&target).map_err(|_| CallError::Unassigned { operation: target.id })?;
163 let payload = (target.encode)(request.as_ref()).ok_or(CallError::Unavailable)?;
164 let values = self.call.portable();
165 let encoded = Frame::child_payload_encoded_len(
166 &assigned.operation,
167 assigned.generation,
168 payload.len(),
169 self.generation_context.is_some(),
170 &values,
171 );
172 if encoded > Frame::LIMIT {
173 return Err(CallError::Oversized {
174 method: target.id,
175 encoded,
176 limit: Frame::LIMIT,
177 });
178 }
179 let command = harmos_sdk::operations::WireChildCall {
180 context: self.operation_context.unwrap_or([0; 32]),
181 generation_context: self.generation_context,
182 values,
183 operation: assigned.operation.clone(),
184 fingerprint: assigned.fingerprint,
185 generation: assigned.generation,
186 payload,
187 submit: false,
188 };
189 let params = command.encode().map_err(|error| CallError::Malformed {
190 method: target.id,
191 message: error.to_string(),
192 })?;
193 let bytes = self
194 .exchange_with(|id, call| {
195 let frame = Self::request_frame(id, CHILD_METHOD, params)?;
196 call.send(Outgoing::Frame(frame));
197 Ok::<(), CallError>(())
198 })
199 .await?;
200 (target.decode)(bytes.as_slice()).map_err(|error| CallError::Malformed {
201 method: target.id,
202 message: error.to_string(),
203 })
204 }
205 .await;
206 result.map_err(|error| harmos_sdk::operations::InvocationError::failed(target.id, error))
207 })
208 }
209}
210
211impl PendingCalls {
212 fn new() -> Self {
213 Self {
214 state: Mutex::new(PendingState {
215 closed: false,
216 next_id: 1,
217 waiters: HashMap::new(),
218 }),
219 capacity: Semaphore::new(PENDING_CALLS),
220 }
221 }
222
223 fn register(&self, waiter: oneshot::Sender<Result<crate::FramePayload, CallError>>) -> Result<u64, CallError> {
224 let mut state = self.state.lock().expect("the pending-call lock is healthy");
225 if state.closed {
226 return Err(CallError::Unavailable);
227 }
228
229 let id = state.next_id;
230 state.next_id = state.next_id.checked_add(1).ok_or(CallError::Unavailable)?;
231 state.waiters.insert(id, waiter);
232
233 Ok(id)
234 }
235
236 fn answer(&self, id: u64, result: Result<crate::FramePayload, CallError>) {
237 let waiter = self.state.lock().expect("the pending-call lock is healthy").waiters.remove(&id);
238 if let Some(waiter) = waiter {
239 let _ = waiter.send(result);
240 }
241 }
242
243 fn close(&self) {
244 let waiters = {
245 let mut state = self.state.lock().expect("the pending-call lock is healthy");
246 state.closed = true;
247 std::mem::take(&mut state.waiters)
248 };
249 self.capacity.close();
250 for (_, waiter) in waiters {
251 let _ = waiter.send(Err(CallError::Unavailable));
252 }
253 }
254}
255
256impl Drop for PendingCall<'_> {
257 fn drop(&mut self) {
258 let unanswered = self.pending.state.lock().expect("the pending-call lock is healthy").waiters.remove(&self.id).is_some();
259 if unanswered && let Some(cancel) = self.cancel.take() {
260 cancel.send(Outgoing::Frame(Frame::Cancel { id: self.id }));
261 }
262 }
263}
264
265impl Supervisor {
266 pub(crate) fn new(outgoing: mpsc::Sender<Outgoing>, clock: fn() -> Option<i64>) -> Self {
267 Self {
268 outgoing,
269 sequences: Arc::new(Mutex::new(BTreeMap::new())),
270 pending: Arc::new(PendingCalls::new()),
271 operation_context: None,
272 generation_context: None,
273 assignments: Default::default(),
274 call: Arc::default(),
275 clock,
276 }
277 }
278
279 pub(crate) fn for_operation(&self, context: [u8; 32]) -> Self {
280 let mut host = self.clone();
281 host.operation_context = Some(context);
282 host
283 }
284
285 pub(crate) fn for_attempt(
286 &self, context: [u8; 32], values: Vec<harmos_domain::operations::CallValue>,
287 ) -> Result<Self, harmos_domain::operations::ContextError> {
288 let mut host = self.for_operation(context);
289 host.call = Arc::new(harmos_domain::operations::CallContext::from_portable(values)?);
290 Ok(host)
291 }
292
293 pub fn context(&self) -> &harmos_domain::operations::CallContext {
294 &self.call
295 }
296
297 pub fn with_value<T: harmos_domain::operations::ContextValue>(
298 &self, value: T,
299 ) -> Result<Self, harmos_domain::operations::ContextError> {
300 let mut child = self.clone();
301 child.call = Arc::new(self.call.with_value(value)?);
302 Ok(child)
303 }
304
305 pub(super) async fn child_call<O>(&self, request: &O::Request, submit: bool) -> Result<crate::FramePayload, CallError>
308 where
309 O: OperationDeclaration,
310 {
311 if self.operation_context.is_none() && self.generation_context.is_none() {
312 return Err(CallError::Unavailable);
313 }
314 self.exchange_with(|id, call| {
315 let params = self.child::<O>(request, submit)?;
316 let frame = Self::request_frame(id, CHILD_METHOD, params)?;
317 call.send(Outgoing::Frame(frame));
318 Ok::<(), CallError>(())
319 })
320 .await
321 }
322
323 pub(super) async fn child_control(&self, instance: Id, action: WireChildAction) -> Result<crate::FramePayload, CallError> {
326 let params = self.control_params(instance, action)?;
327 self.exchange_with(|id, call| {
328 let frame = Self::request_frame(id, CHILD_METHOD, params)?;
329 call.send(Outgoing::Frame(frame));
330 Ok::<(), CallError>(())
331 })
332 .await
333 }
334
335 pub(super) fn child_control_detached(&self, instance: Id, action: WireChildAction) {
337 let Ok(params) = self.control_params(instance, action) else {
338 return;
339 };
340 let (waiter, _) = oneshot::channel();
341 let Ok(id) = self.pending.register(waiter) else {
342 return;
343 };
344 let Ok(frame) = Self::request_frame(id, CHILD_METHOD, params) else {
345 return;
346 };
347 let frame = match self.outgoing.try_send(Outgoing::Frame(frame)) {
348 Ok(()) => return,
349 Err(mpsc::error::TrySendError::Closed(_)) => return,
350 Err(mpsc::error::TrySendError::Full(frame)) => frame,
351 };
352 if let Ok(runtime) = tokio::runtime::Handle::try_current() {
354 let outgoing = self.outgoing.clone();
355 runtime.spawn(async move {
356 let _ = outgoing.send(frame).await;
357 });
358 }
359 }
360
361 fn control_params(&self, instance: Id, action: WireChildAction) -> Result<Vec<u8>, CallError> {
362 let context = self.operation_context.ok_or(CallError::Unavailable)?;
363 let control = WireChildControl {
364 context,
365 instance: instance.value(),
366 action,
367 };
368 control.encode().map_err(|error| CallError::Malformed {
369 method: CHILD_METHOD,
370 message: error.to_string(),
371 })
372 }
373
374 fn child<O: OperationDeclaration>(&self, request: &O::Request, submit: bool) -> Result<Vec<u8>, CallError> {
375 if self.operation_context.is_none() && self.generation_context.is_none() {
376 return Err(CallError::Unavailable);
377 }
378 let values = self.call.portable();
379 self.child_with_values::<O>(request, submit, values)
380 }
381
382 fn child_with_values<O: OperationDeclaration>(
383 &self, request: &O::Request, submit: bool, values: Vec<harmos_domain::operations::CallValue>,
384 ) -> Result<Vec<u8>, CallError> {
385 let context = self.operation_context.unwrap_or([0; 32]);
386 let target = self.assignments.resolve::<O>().map_err(|_| CallError::Unassigned { operation: O::ID })?;
387 let payload_bytes = harmos_domain::Message::encoded_size(request);
388 let encoded = Frame::child_payload_encoded_len(
389 &target.operation,
390 target.generation,
391 payload_bytes,
392 self.generation_context.is_some(),
393 &values,
394 );
395 if encoded > Frame::LIMIT {
396 return Err(CallError::Oversized {
397 method: O::ID,
398 encoded,
399 limit: Frame::LIMIT,
400 });
401 }
402 let payload = harmos_domain::Message::encode(request);
403 let command = harmos_sdk::operations::WireChildCall {
404 context,
405 generation_context: self.generation_context,
406 values,
407 operation: target.operation.clone(),
408 fingerprint: target.fingerprint,
409 generation: target.generation,
410 payload,
411 submit,
412 };
413 command.encode().map_err(|error| CallError::Malformed {
414 method: O::ID,
415 message: error.to_string(),
416 })
417 }
418
419 fn request_frame(id: u64, method: &'static str, params: Vec<u8>) -> Result<Frame, CallError> {
420 let mut frame = Frame::Call {
421 id: u64::MAX,
422 method: method.to_owned(),
423 params: params.into(),
424 };
425 let encoded = frame.encoded_len();
426 if encoded > Frame::LIMIT {
427 return Err(CallError::Oversized { method, encoded, limit: Frame::LIMIT });
428 }
429 let Frame::Call { id: original, .. } = &mut frame else { unreachable!() };
430 *original = id;
431 Ok(frame)
432 }
433
434 async fn exchange_with<'a, E: From<CallError> + Send>(
435 &'a self, prepare: impl FnOnce(u64, mpsc::Permit<'a, Outgoing>) -> Result<(), E> + Send,
436 ) -> Result<crate::FramePayload, E> {
437 let permit = self.pending.capacity.acquire().await.map_err(|_| CallError::Unavailable)?;
438 let mut frames = self.outgoing.reserve_many(2).await.map_err(|_| CallError::Unavailable)?;
442 let call = frames.next().expect("two reserved frame slots");
443 let cancel = frames.next().expect("two reserved frame slots");
444 let (waiter, response) = oneshot::channel();
445 let id = self.pending.register(waiter)?;
446 let mut pending = PendingCall {
447 pending: &self.pending,
448 id,
449 _permit: permit,
450 cancel: Some(cancel),
451 };
452
453 if let Err(source) = prepare(id, call) {
454 pending.cancel.take();
456 return Err(source);
457 }
458 response.await.map_err(|_| CallError::Unavailable)?.map_err(E::from)
459 }
460
461 pub(crate) fn receive_answer(&self, id: u64, result: crate::FramePayload) {
462 self.pending.answer(id, Ok(result));
463 }
464
465 pub(crate) fn receive_refusal(&self, id: u64, refusal: harmos_domain::Refusal) {
466 let code = refusal.code();
467 let failure = refusal.into_failure();
468 self.pending.answer(id, Err(CallError::Refused { code, failure }));
469 }
470
471 pub(crate) fn disconnected(&self) {
472 self.pending.close();
473 }
474
475 pub(super) fn clock(&self) -> fn() -> Option<i64> {
477 self.clock
478 }
479
480 pub(super) fn now(&self) -> Option<i64> {
481 (self.clock)()
482 }
483
484 pub(super) async fn publication<S: crate::StreamSpec>(
488 &self, rows: &[Vec<u8>], stamps: &[Option<i64>], context: Option<&str>,
489 ) -> Result<(), super::PublishError> {
490 debug_assert_eq!(rows.len(), stamps.len());
491 if rows.is_empty() {
492 return Ok(());
493 }
494 if self.operation_context.is_none() && self.generation_context.is_none() {
495 return Err(CallError::Unavailable.into());
496 }
497 let target = self.assignments.resolve::<harmos_operations::streams::Publish>().map_err(|_| CallError::Unassigned {
498 operation: harmos_operations::streams::Publish::ID,
499 })?;
500 let mut rest = (rows, stamps);
501 while !rest.0.is_empty() {
502 let mut sent = 0;
503 let bytes = self
504 .exchange_with(|id, call| {
505 let values = self.call.portable();
506 let envelope = harmos_sdk::operations::WireChildCall::payload_encoded_len(
507 &target.operation,
508 target.generation,
509 0,
510 self.generation_context.is_some(),
511 &values,
512 )
513 .map_err(|error| CallError::Malformed {
514 method: harmos_operations::streams::Publish::ID,
515 message: error.to_string(),
516 })?;
517 sent = self.with_publication::<S, super::PublishError>(rest.0, rest.1, context, envelope, |request| {
518 let params = self.child_with_values::<harmos_operations::streams::Publish>(&request, false, values)?;
519 let frame = Self::request_frame(id, CHILD_METHOD, params)?;
520 call.send(Outgoing::Frame(frame));
521 Ok(())
522 })?;
523 Ok::<(), super::PublishError>(())
524 })
525 .await?;
526 <() as Message>::decode(bytes.as_slice()).map_err(|source| CallError::Malformed {
527 method: harmos_operations::streams::Publish::ID,
528 message: source.to_string(),
529 })?;
530 rest = (&rest.0[sent..], &rest.1[sent..]);
531 }
532 Ok(())
533 }
534
535 pub(super) fn with_publication<S: crate::StreamSpec, E: From<PublicationError>>(
543 &self, rows: &[Vec<u8>], stamps: &[Option<i64>], context: Option<&str>, empty_envelope: usize,
544 send: impl FnOnce(harmos_operations::streams::Publish) -> Result<(), E>,
545 ) -> Result<usize, E> {
546 let fits = |body: usize| Frame::child_encoded_from_empty(empty_envelope, body) <= Frame::LIMIT;
547 let mut sequences = self.sequences.lock().expect("the publication sequence lock is healthy");
548 let next = sequences.entry(S::ID).or_insert(0);
549 let first = next.checked_add(1).ok_or(PublicationError::SequenceExhausted { stream: S::ID })?;
550 *next = first;
551 let published_unix_micros = stamps[0];
552 let head = crate::wire::publication_encoded_len(S::ID, S::VERSION, first, rows[0].len(), published_unix_micros, context);
553 let encoded = |count: usize, following: usize| head.saturating_add(crate::wire::varint_len(count as u64)).saturating_add(following);
554 if !fits(encoded(0, 0)) {
555 return Err(PublicationError::Oversized {
556 encoded: encoded(0, 0),
557 limit: Frame::LIMIT,
558 }
559 .into());
560 }
561 let mut following = 0_usize;
562 let mut deltas = Vec::with_capacity(rows.len() - 1);
563 for (row, pair) in rows[1..].iter().zip(stamps.windows(2)) {
564 if deltas.len() + 1 >= S::WINDOW {
565 break;
566 }
567 let delta = match (pair[0], pair[1]) {
568 (_, None) => None,
569 (Some(previous), Some(stamp)) => match stamp.checked_sub(previous) {
570 Some(delta) => Some(delta),
571 None => break,
572 },
573 (None, Some(_)) => break,
574 };
575 let grown = following.saturating_add(crate::wire::following_encoded_len(row.len(), delta));
576 let total = encoded(deltas.len() + 1, grown);
577 if total > PUBLICATION_BATCH_BYTES || !fits(total) || *next == u64::MAX {
578 break;
579 }
580 following = grown;
581 deltas.push(delta);
582 *next += 1;
583 }
584 let taken = deltas.len() + 1;
585 send(harmos_operations::streams::Publish {
586 stream: S::ID.to_owned(),
587 schema: S::VERSION,
588 sequence: Some(first),
589 published_unix_micros,
590 context: context.map(str::to_owned),
591 payload: rows[0].clone(),
592 following: rows[1..taken]
593 .iter()
594 .zip(deltas)
595 .map(|(row, published_delta_micros)| harmos_operations::streams::FollowingRow {
596 payload: row.clone(),
597 published_delta_micros,
598 })
599 .collect(),
600 })?;
601 Ok(taken)
602 }
603
604 pub fn exiting(&self, reason: impl Into<String>) -> bool {
605 self.outgoing.try_send(Outgoing::Exiting(reason.into())).is_ok()
606 }
607
608 pub fn abandon(&self) -> bool {
609 self.outgoing.try_send(Outgoing::Abandon).is_ok()
610 }
611}
612
613fn refused_line(code: i64, failure: &harmos_domain::Failure) -> String {
618 match failure.context().operation() {
619 Some(operation) => format!("operation `{operation}` failed"),
620 None => format!("the host refused the call ({code})"),
621 }
622}
623
624pub(super) fn published_at() -> Option<i64> {
625 match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
626 Ok(elapsed) => i64::try_from(elapsed.as_micros()).ok(),
627 Err(error) => i64::try_from(error.duration().as_micros()).ok().and_then(i64::checked_neg),
628 }
629}
630
631#[cfg(test)]
634mod tests {
635 use super::*;
636 use harmos_domain::model::storage::files::{ByteRange, FileBatch, FileSource};
637 use harmos_domain::operations::CallValue;
638 use harmos_operations::{storage::ReadFile, streams::Publish};
639 use harmos_sdk::operations::WireChildCall;
640
641 struct Samples;
642
643 impl crate::StreamSpec for Samples {
644 type Row = FileBatch;
645 const ID: &'static str = "context.samples";
646 const VERSION: u32 = 1;
647 const WINDOW: usize = 8;
648 }
649
650 fn host(values: Vec<CallValue>) -> Supervisor {
651 let (outgoing, _) = mpsc::channel(1);
652 let host = Supervisor::new(outgoing, || None).for_attempt([7; 32], values).unwrap();
653 let assignments = [ReadFile::definition(), Publish::definition()]
654 .into_iter()
655 .map(|definition| harmos_sdk::operations::AssignedOperation {
656 operation: format!("host.{}", definition.id),
657 fingerprint: definition.fingerprint.0,
658 generation: u64::MAX,
659 })
660 .collect::<Vec<_>>();
661 host.assignments.install(&harmos_sdk::operations::AssignedOperation::encode(&assignments).unwrap()).unwrap();
662 host
663 }
664
665 #[test]
666 fn child_context_admitted() {
667 let values = vec![CallValue {
668 key: "context".into(),
669 payload: vec![3; 65536 - 7],
670 }];
671 let host = host(values.clone());
672 let request = ReadFile {
673 source: FileSource::Local("data.bin".into()),
674 range: ByteRange { offset: 0, limit: None },
675 };
676 let encoded = host.child::<ReadFile>(&request, false).unwrap();
677 let decoded = WireChildCall::decode(&encoded).unwrap();
678 assert_eq!(decoded.values, values);
679 assert_eq!(decoded.payload, request.encode());
680
681 let request = ReadFile {
682 source: FileSource::Local("a".repeat(Frame::LIMIT - 1024).into()),
683 range: request.range,
684 };
685 assert!(Frame::request_fits::<ReadFile>(&request));
686 assert!(matches!(host.child::<ReadFile>(&request, false), Err(CallError::Oversized { .. })));
687 }
688
689 #[test]
690 fn publication_context_boundary() {
691 let values = vec![CallValue {
692 key: "context".into(),
693 payload: vec![3; 65536 - 7],
694 }];
695 let host = host(values.clone());
696 let empty = WireChildCall::payload_encoded_len(&format!("host.{}", Publish::ID), u64::MAX, 0, false, &values).unwrap();
697 let fits = |bytes: usize| {
698 let row = FileBatch { offset: 0, bytes: vec![0; bytes] };
699 let body = crate::wire::publication_encoded_len("context.samples", 1, 1, row.encoded_size(), None, None) + 1;
700 Frame::child_encoded_from_empty(empty, body) <= Frame::LIMIT
701 };
702 let mut low = 0;
703 let mut high = Frame::LIMIT;
704 while low + 1 < high {
705 let middle = low + (high - low) / 2;
706 if fits(middle) {
707 low = middle;
708 } else {
709 high = middle;
710 }
711 }
712 let rows = [FileBatch { offset: 0, bytes: vec![0; low] }.encode()];
713 let taken = host
714 .with_publication::<Samples, CallError>(&rows, &[None], None, empty, |request| {
715 let envelope = host.child_with_values::<Publish>(&request, true, values.clone())?;
716 let frame = Frame::Call {
717 id: u64::MAX,
718 method: CHILD_METHOD.into(),
719 params: envelope.into(),
720 };
721 assert_eq!(frame.encoded_len(), Frame::LIMIT);
722 Ok(())
723 })
724 .unwrap();
725 assert_eq!(taken, 1);
726 let rows = [FileBatch { offset: 0, bytes: vec![0; low + 1] }.encode()];
727 let result =
728 host.with_publication::<Samples, CallError>(&rows, &[None], None, empty, |_| panic!("an oversized publication must not send"));
729 assert!(matches!(result, Err(CallError::Publication(PublicationError::Oversized { .. }))));
730 assert_eq!(host.sequences.lock().unwrap()["context.samples"], 2);
731 }
732}