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
//! Which agent the pipeline is currently working on, so a panic inside a
//! schedule system can be blamed on the run that caused it.
//!
//! Every agent lives in one shared [`World`](bevy_ecs::world::World) driven by
//! one [`Schedule`](bevy_ecs::schedule::Schedule), so a caught panic carries no
//! hint of *whose* data tripped it. Without attribution the daemon can only log
//! "something panicked" and re-tick the same unchanged state on the next wake -
//! panicking again, forever, while every other agent stalls behind it.
//!
//! Each per-agent loop in the pipeline therefore calls [`enter`] before touching
//! an entity and [`clear`] once the loop is done. If a system panics mid-loop,
//! the slot still holds the offending entity when
//! [`PipelineWorld::tick`](crate::world::PipelineWorld::tick) catches the
//! unwind, which then fails that one agent and lets the world carry on.
//!
//! ## Why a thread-local, and why no RAII guard
//!
//! Thread-local rather than a global: parallel tests each drive their own world
//! on their own thread and would otherwise trample a shared slot. This is sound
//! because the schedule runs single-threaded (see
//! [`PipelineWorld::new`](crate::world::PipelineWorld::new)) - systems run on
//! the same thread that catches the panic.
//!
//! And plainly set/cleared rather than a `Drop` guard: a guard would clear the
//! slot *while unwinding*, destroying the very evidence the catch needs.
//!
//! ## Work that runs off the driver thread
//!
//! One system fans its per-agent work out over the compute task pool
//! ([`dispatch_inference`](crate::pipeline::dispatch_inference)'s `par_iter`).
//! A thread-local set inside that closure lives on a pool thread and is
//! invisible to the driver thread that catches the unwind, so the mechanism
//! above cannot see it. Those bodies run under [`run_agent_parallel`] instead,
//! which catches the panic where the entity *is* known and leaves a
//! [`PanickedInParallel`] marker for
//! [`PipelineWorld::tick`](crate::world::PipelineWorld::tick) to act on.
use Cell;
use Entity;
use Component;
use ParallelCommands;
thread_local!
/// Record `entity` as the agent being processed right now.
/// Forget the current agent - call this when a per-agent loop finishes, so a
/// later panic in agent-independent code isn't blamed on the last agent seen.
/// The agent recorded by the last [`enter`] that has not been [`clear`]ed.
/// Left on an agent whose per-agent work panicked on a compute-pool thread.
///
/// [`PipelineWorld::tick`](crate::world::PipelineWorld::tick) drains these after
/// the schedule returns and fails each marked agent, exactly as it would for a
/// panic caught on the driver thread.
/// Run one agent's share of a parallel system body, containing a panic to that
/// agent.
///
/// Catching *here* - inside the closure, where `entity` is in hand - is what
/// makes a compute-pool panic attributable at all: the thread-local scope can't
/// cross back to the driver thread, and letting the panic unwind out of the task
/// pool would take down the whole fan-out rather than one agent. The remaining
/// agents in the batch finish normally.
///
/// `body` is a `&mut dyn FnMut` rather than a generic so every caller shares one
/// instantiation - the workspace gates a hard 100%, and a generic would give
/// each call site its own panic arm to cover.