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
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
//! Durable operator rename (#211): record a display name.
//!
//! A display name is a LABEL over UUID identity, never an address — nothing
//! resolves a workflow by name. A rename is a RECORDED
//! [`Event::SearchAttributesUpdated`] carrying the `aion.display_name`
//! attribute, so history keeps every name that has been worn and the current
//! name is the last-write-wins fold ([`aion_core::display_name`]). Renaming
//! appends; it never rewrites the past.
//!
//! # A run is what you address; a workflow is what wears the name
//!
//! The write side takes a `(workflow, run)` pair and refuses a run whose name
//! could not land where the caller means it to (see the guards below). The READ
//! side is per-WORKFLOW: [`aion_core::display_name`] folds the attribute over
//! the whole history, so a continue-as-new successor INHERITS its predecessor's
//! name rather than starting unnamed. That asymmetry is deliberate and is why
//! `SearchAttributesUpdated` carrying no run id is safe to record only at the
//! workflow's current head — but it does mean "the run's display name" is a
//! loose way to speak about it, and the read-side docs say per-workflow.
//!
//! # Single-writer discipline
//!
//! Following pause (#204, `pause.rs`): a run with a REGISTERED handle appends
//! through that handle's own recorder — the single writer — under its lock, so
//! the append is serialised against the exit monitor and every other writer
//! through the same recorder. Only a run whose DURABLE STATUS says no live
//! recorder can exist — terminal, or `Paused` and therefore excluded from
//! respawn by design — builds a one-shot [`Recorder::resume_at`] at the durable
//! head, exactly the non-resident path pause's resume and the paused-signal
//! record use. A merely-unregistered run is NOT that: an unregistered `Running`
//! run is mid-birth or mid-recovery and its live recorder owns the head, so a
//! rename there is refused rather than raced.
//!
//! One thing rename must check that pause does not: the recorder and the
//! store's expected-sequence check are per-WORKFLOW, while
//! [`Event::SearchAttributesUpdated`] carries no run id and
//! [`aion_core::run_segment`] is positional — so a recorded name always lands
//! in the workflow's LAST run. A superseded run therefore cannot be renamed at
//! all; see the guard in [`rename`]. Never two writers; never
//! `EventStore::append` directly.
//!
//! # The continue-as-new window
//!
//! Supersession is not established the instant a run continues as new, and
//! which window that leaves depends on WHICH DOOR the continuation came
//! through.
//!
//! The OPERATOR door has no window any more. Since aion#213
//! (`lifecycle/continuation.rs`) `Engine::continue_as_new` records the
//! predecessor's terminal and the successor's `WorkflowStarted` as ONE batch
//! through ONE recorder, and re-keys the registry onto the successor before
//! releasing the lock — so a rename that acquires that lock finds either
//! neither half or both.
//!
//! The WORKFLOW-CODE door still has one, and always did. The `continue_as_new`
//! NIF (`runtime/nif_continue_as_new.rs`) records `WorkflowContinuedAsNew`
//! through the run's own recorder and ends the process; the successor is opened
//! afterwards, by the exit monitor. In that window the predecessor is still
//! registered, still owns the head, and is not yet superseded, so the
//! positional guard above admits it: renaming it there records the operator's
//! name at the head the successor's boundary is about to claim, landing it in
//! the SUCCESSOR's segment while reporting success — and, if it wins the race
//! for that sequence, hard-failing the boundary itself.
//!
//! What closes that window is the run's OWN `WorkflowContinuedAsNew`
//! ([`has_continued_as_new`]), checked twice: once before the lock over the
//! history read there, and again INSIDE the registered handle's recorder lock
//! over everything appended SINCE that read. The two checks partition the
//! history at the pre-lock head, so between them they cover all of it. The
//! in-lock check is the load-bearing one — the predecessor's
//! `WorkflowContinuedAsNew` is appended through that same recorder, so while
//! this rename holds the lock the event has either already landed (and the
//! delta re-read sees it) or cannot land until this rename is done, and a
//! continue-as-new that starts afterwards reads a head that already includes
//! this append. The pre-lock check cannot stand in for it: the pre-lock read
//! can precede the predecessor's own `WorkflowContinuedAsNew` append entirely.
use HashMap;
use Arc;
use ;
use EventStore;
use VisibilityStore;
use Utc;
use crateEngineError;
use crateRecorder;
use crateRegistry;
/// Dependencies required to rename a workflow run.
///
/// Narrower than [`super::pause::PauseWorkflowContext`] because a rename never
/// respawns anything: it only appends one recorded event and refreshes
/// visibility, so no catalog, runtime, or supervision tree is involved.
/// Records `display_name` as the workflow's current display name, addressed
/// by run (#211).
///
/// Addressed by run because which run is live decides whether the append is
/// safe; RECORDED per workflow because the event carries no run id and readers
/// fold it over the whole history (see the module docs).
///
/// Validates BEFORE any append: the trimmed name must be non-empty and the
/// `(id, run)` pair must have recorded history. There is no STATUS
/// precondition on the label itself — a completed run's name is as legitimate
/// as a running one's, and the recorded event is an ordinary
/// [`Event::SearchAttributesUpdated`] that status projections ignore. Status is
/// consulted only to choose the append path safely (see the module docs).
///
/// Returns the name exactly as recorded (trimmed).
///
/// # Errors
///
/// Returns [`EngineError::InvalidState`] when the trimmed name is empty (the
/// transport layers reject this earlier; this is the engine boundary's own
/// check); when the run has been superseded by a later run of the same
/// workflow, or has recorded its own `WorkflowContinuedAsNew` and so is about
/// to be (see the module docs) — either way the name would land on the
/// successor; or when the run is non-terminal, not `Paused`, and not resident
/// on this node — renaming it then would append behind its live writer, so it
/// is refused for the caller to retry. Returns [`EngineError::WorkflowNotFound`]
/// when no history exists for `(id, run)`, and [`EngineError::Durability`] when
/// the schema refuses the attribute or the store rejects the append. A
/// rejection appends nothing.
pub async
/// Whether `run` has recorded its own [`Event::WorkflowContinuedAsNew`].
///
/// Matched on the event's `parent_run_id` — the run that continued — rather
/// than on a [`run_segment`] slice, so the answer does not depend on the
/// positional slicing that the successor's not-yet-appended `WorkflowStarted`
/// is exactly what would fix.
/// What both continued-as-new refusals tell the operator to do about it. Shared
/// so the two checks cannot drift apart in their guidance.
const CONTINUED_AS_NEW_GUIDANCE: &str = "its successor run owns (or is about to own) the workflow head, so a recorded display name \
would land behind the successor's own writer — rename the successor run instead";
/// The pre-lock refusal: the run had ALREADY continued as new when the rename
/// was asked for.
/// The in-lock refusal: the run continued as new BETWEEN this rename's own
/// validation read and its append — the race the recorder lock exists to make
/// visible. Worded distinctly from [`continued_as_new_refusal`] because the two
/// are different facts about timing for whoever reads the error, and because a
/// refusal that cannot be attributed to the check that produced it cannot be
/// tested.