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
//! Which role an IOC thread takes on, and therefore which scheduling band it
//! sits at — the one place that choice is made.
//!
//! # Why this exists
//!
//! [`task::enter_ioc_thread`](super::task::enter_ioc_thread) is the single
//! owner of *applying* a band: every IOC thread passes through it, and a
//! source guard in `task.rs` fails the build if one does not. Nothing owned
//! *choosing* the band. The value was an ordinary argument at each thread's
//! creation point, so the serving paths grew named, C-cited, guarded
//! constants (`CAS_TCP_PRIORITY`, `PVA_SERVER_PRIORITY`, `periodic_priority`)
//! while the diagnostic paths — the ones nobody writes a parity note about —
//! took `ThreadPriority::Low` because `Low` is what a thread with no stated
//! opinion gets. C has the same default in the same place
//! (`EPICS_THREAD_OPTS_INIT` is `{epicsThreadPriorityLow, …}`,
//! `epicsThread.h:173`), and it is a default, not a decision.
//!
//! It was measured as a defect, not deduced. On the VxWorks bring-up rig,
//! ramping CA connections to the admission wall froze `UPTIME` for the whole
//! ramp and resumed it two seconds after the load stopped: the values were
//! correct and the publisher never ran, so `CA_REFUSED_CNT` read `0.0` at the
//! exact moment an operator needs it. The bring-up console reporter, which
//! shares no lock with the publisher but shares its band, emitted 4 ticks in
//! 444 s instead of ~44. Client service at `CaServerLow-4..CaServerLow`
//! (16..20) starves anything at 10.
//!
//! # The rule
//!
//! **A thread whose output is how the IOC is observed must be schedulable
//! while the IOC is at its wall — if its per-tick work is bounded.** The
//! operator surface sits strictly above `epicsThreadPriorityCAServerHigh`
//! (40), the ceiling of the band C gives `rsrv` (`caservertask.c:562-575`
//! builds its ladder down from `CAServerLow`), and at or below
//! `epicsThreadPriorityScanHigh` (70), so it can never delay record
//! processing more urgent than the slowest periodic scan.
//!
//! C reaches the same ordering by a different route, which is why the rule is
//! stated against C's constants rather than invented here: its status numbers
//! are *records*, processed by the periodic scan threads at
//! `epicsThreadPriorityScanLow + ind` (`dbScan.c:949`) — 60 and up, i.e. 40
//! levels above the CA server that would otherwise starve them. Ours was
//! inverted. The operator-facing report path C does have as a thread, `iocsh`,
//! sits higher still at 91 (`epicsThread.h:86`), so this table is conservative
//! relative to C in both directions.
//!
//! The bounded-work half of the rule is measured, not assumed, and it is why
//! this table has two answers rather than one. On the same rig, the same ramp
//! and the same guest, with the status pusher raised to `ScanLow`: 136 clients
//! admitted before the pool's capacity refusal, identical to the pre-fix run,
//! and `UPTIME` advancing 1 s/s throughout. With the bring-up console census
//! raised alongside it: 87, twice, the 88th client's handshake exceeding a 15 s
//! timeout. Its per-tick work is roughly a hundred lines onto a byte-serial
//! console, and above the serving threads that work *is* the load — an
//! instrument that changes what it measures. `epicsThreadPriorityIocsh` (91)
//! is not a counter-example: C's unbounded reports are typed by a human, once,
//! not emitted on a timer forever.
//!
//! # What is here and what is not
//!
//! Roles are added here, in view of the ordering test below, not at a thread's
//! creation point — that is the whole mechanism. The serving bands are *not*
//! here: they stay with their servers (`epics_ca_rs::server::blocking`,
//! `epics_pva_rs::server_native::blocking`, `server::scan`) because each is
//! derived from the C line that sets it and each has its own guard pinning it
//! to that derivation. This module is the owner for the roles that had no such
//! home, and it states the ordering against them using C's own constant rather
//! than by reaching across crates for theirs.
use io;
use JoinHandle;
use ;
/// A role an IOC thread takes on, as a name rather than a number.
/// The thread prologue for a role: name this thread and take the role's band.
///
/// For a thread whose creation the caller owns for another reason — the
/// embedded entry points build theirs with `thread::Builder` directly so the
/// pool's byte account can be charged inside the closure. Everything else
/// [`spawn_ioc_role`] does is done there.
/// Start a thread in `role`: the band comes from the role, so the creation
/// point never names one.
///
/// Delegates to [`spawn_dedicated_thread`], which charges the thread to the
/// worker pool's reservation account and runs the prologue.