1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Craton Software Company
//! Ticker-down spawn refusal regression test.
//!
//! Pre-fix the executor logged a one-shot `tracing::error!` and proceeded
//! with the spawn when [`TensorWasmEngine::is_epoch_ticker_running`]
//! returned `false`. That meant a runaway guest — start function or
//! per-call invocation — could wedge the worker thread forever: with
//! the epoch counter frozen, neither the per-call deadline nor
//! [`tensor_wasm_exec::executor::MAX_START_FN_DURATION`] can fire.
//!
//! The fix refuses the spawn with
//! [`tensor_wasm_exec::executor::ExecError::EpochTickerNotRunning`] when
//! the ticker is down and a deadline-class bound would otherwise apply.
//! This test stands up an engine, stops its ticker, and confirms the
//! refusal.
use std::sync::Arc;
use std::time::Duration;
use tensor_wasm_core::types::TenantId;
use tensor_wasm_exec::engine::TensorWasmEngine;
use tensor_wasm_exec::executor::{ExecError, SpawnConfig, TensorWasmExecutor};
fn trivial_wasm() -> Vec<u8> {
wat::parse_str(r#"(module (func (export "noop")))"#).expect("valid wat")
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn spawn_refused_when_ticker_down_with_deadline() {
// `TensorWasmEngine::new` auto-spawns the ticker when constructed
// inside a Tokio runtime. To exercise the ticker-down path we
// stop it explicitly.
let mut engine = TensorWasmEngine::new().expect("engine");
engine.stop_epoch_ticker();
assert!(
!engine.is_epoch_ticker_running(),
"ticker must be stopped before the test runs",
);
let engine = Arc::new(engine);
let exec = TensorWasmExecutor::new(engine);
// Configure a per-call deadline so the contract is unambiguous:
// without the ticker the deadline cannot fire, so the spawn must
// be refused.
let cfg = SpawnConfig::for_tenant(TenantId(1)).with_deadline(Duration::from_millis(100));
let err = exec
.spawn_instance(cfg, &trivial_wasm())
.await
.expect_err("spawn must be refused when ticker is down and deadline is configured");
assert!(
matches!(err, ExecError::EpochTickerNotRunning),
"expected EpochTickerNotRunning, got {err:?}",
);
// The refused spawn must not have leaked an admission slot.
assert_eq!(
exec.instances_len(),
0,
"refused spawn must roll back its admission slot",
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn spawn_refused_when_ticker_down_without_explicit_deadline() {
// Even without a per-call deadline, `MAX_START_FN_DURATION` always
// applies inside `Instance::new_async`, so the ticker-required
// contract holds. Pin that behaviour: a `SpawnConfig` with no
// deadline must still be refused when the ticker is down.
let mut engine = TensorWasmEngine::new().expect("engine");
engine.stop_epoch_ticker();
let engine = Arc::new(engine);
let exec = TensorWasmExecutor::new(engine);
let err = exec
.spawn_instance(SpawnConfig::for_tenant(TenantId(1)), &trivial_wasm())
.await
.expect_err("spawn must be refused when ticker is down");
assert!(
matches!(err, ExecError::EpochTickerNotRunning),
"expected EpochTickerNotRunning, got {err:?}",
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn spawn_succeeds_when_ticker_running() {
// Sanity counterpart: the same setup with the ticker running must
// succeed. Catches a regression that would over-tighten the gate
// and refuse every spawn.
let engine = Arc::new(TensorWasmEngine::new().expect("engine"));
assert!(
engine.is_epoch_ticker_running(),
"ticker should be auto-spawned by the engine constructor inside a tokio runtime",
);
let exec = TensorWasmExecutor::new(engine);
let id = exec
.spawn_instance(
SpawnConfig::for_tenant(TenantId(1)).with_deadline(Duration::from_millis(100)),
&trivial_wasm(),
)
.await
.expect("spawn must succeed when the ticker is running");
exec.terminate(id).await.expect("terminate");
}