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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
//! Scheduled capability trait and types for cron-style background work.
//!
//! Plugins declare a set of [`Schedule`]s at init time; the host-owned
//! scheduler ticks, decides which schedules are due, and calls
//! [`ScheduledPlugin::run`] for each one. The plugin returns a result and
//! execution continues on the next tick.
//!
//! Schedules are **declarative**, not dynamic. A plugin returns its schedule
//! list once from [`ScheduledPlugin::schedules`]; there is no runtime
//! `register_schedule` host function. This mirrors the existing built-in
//! scheduler in `bext-core::scheduler` (driven by `[[tasks]]` in
//! `bext.config.toml`) and keeps the shape introspectable at install time —
//! the registry resolver, the dev dashboard, and `cap_conformance` can all
//! enumerate a plugin's schedules without running it.
//!
//! Data types are plain POD with `String`/`u64`/enum fields so the trait
//! stays ABI-compatible with the WASM guest crate (matches the convention
//! used by [`crate::lifecycle::LifecyclePlugin`] — JSON strings, `Result<_,
//! String>`, no framework types). The `bext-core` side owns the rich
//! `scheduler::Schedule` enum and converts to/from this POD shape at the
//! host boundary.
/// How the scheduler should behave when it discovers a schedule that was
/// due to fire while the host was down, restarting, or otherwise not
/// ticking.
///
/// This is distinct from "a previous run of this schedule is still
/// executing" — in that case the scheduler always skips (see
/// `is_due` in `bext-core::scheduler`).
/// Hint to the scheduler about distributed-locking requirements.
///
/// Single-node deployments can ignore this field. Multi-node deployments
/// use it, in combination with a configured [`crate::locking::LockingPlugin`],
/// to decide which instance fires each schedule.
///
/// # How the host uses the hint
///
/// The Locking capability shipped in E2 as
/// [`crate::locking::LockingPlugin`]. The host-owned scheduler in
/// `bext-core::scheduler` consults this field at each tick:
///
/// - [`LockingHint::NodeLocal`] — skip locking entirely; every node
/// may fire its own copy.
/// - [`LockingHint::RequireGlobal`] — call
/// [`LockingPlugin::try_lock`](crate::locking::LockingPlugin::try_lock)
/// keyed on `schedule_id` before invoking
/// [`ScheduledPlugin::run`]. On `Ok(None)` another node holds the
/// lock; skip this tick. On `Err(..)` log and skip.
/// - [`LockingHint::PreferGlobal`] — try the lock as above, but if no
/// `LockingPlugin` is configured fall back to `NodeLocal` behavior
/// rather than failing the install.
///
/// Performing the acquisition lives in `bext-core::scheduler` and is
/// tracked separately from this trait-level doc. The shape, however, is
/// stable: plugins declare the hint once, the host does the rest.
/// A single declared schedule.
///
/// The `cron` field accepts the same syntax as `bext-core::scheduler::Schedule::parse`:
/// - simple intervals: `"30s"`, `"5m"`, `"2h"`
/// - cron `*/N` subset: `"*/5 * * * *"`, `"0 */3 * * *"`
/// - shortcuts: `"@hourly"`, `"@daily"`
///
/// Invalid expressions are rejected at plugin load time (the host parses
/// before registering), so a plugin that returns a bad expression fails
/// to start with a clear error.
/// Context supplied to a single invocation of [`ScheduledPlugin::run`].
///
/// Plain-data only — no framework types. The host populates this from its
/// own clock and scheduler state; the plugin reads it to decide how to
/// behave on catch-up runs, to emit tracing, etc.
/// A plugin that declares cron-style background work.
///
/// **Compile-time and WASM execution.** All methods receive and return
/// JSON-friendly or POD data for ABI compatibility with WASM guests.
/// Runs are fire-and-forget from the request path's point of view — the
/// scheduler ticks on a dedicated task, so [`run`] must not block any
/// foreground work and is allowed to take its time (subject to the fuel
/// budget in [`fuel::RUN`]).
///
/// Concurrency: the host guarantees at most one concurrent invocation of
/// `run()` per `schedule_id` per node. Across nodes, at-most-one is
/// provided by the [`LockingHint`] in conjunction with a configured
/// [`crate::locking::LockingPlugin`] — the E2 Locking capability. When
/// no `LockingPlugin` is installed, multi-node deployments may
/// double-fire `RequireGlobal` schedules and should be configured with a
/// single scheduler node; the scheduler logs a warning at start when it
/// detects this condition.
///
/// [`run`]: ScheduledPlugin::run
/// [`fuel::RUN`]: fuel::RUN
/// Fuel budgets for WASM scheduled plugin calls.
///
/// Matches the shape in [`crate::types::fuel`]. The `RUN` budget is
/// deliberately generous — scheduled jobs routinely do real work
/// (database sweeps, report generation) and the host gates them via the
/// scheduler's tick interval rather than per-call fuel.