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
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
//! Turning the log firehose into Discord messages.
//!
//! Three stages: [`buffer`] coalesces raw lines into groups under a bounded
//! queue, [`pack`] turns a group into the chunks and messages Discord's own
//! limits allow, and this module is the loop that joins them to a live bus
//! subscription. [`state::State`] is the middle of that loop, and it is
//! deliberately plain data plus synchronous methods: [`state::State::on_event`]
//! takes a [`shep_client::shep_core::protocol::BusEvent`] and returns
//! nothing, so a test drives it with no client, no socket, and no running
//! bot behind it, the same reason `buffer` and `pack` are testable that way.
//! It lives in its own file, [`state`], for the same reason [`discord`]
//! does: it is a whole pipeline stage, worth reading and testing apart from
//! the trait and the loop that drive it. [`run`] is the one place that
//! touches a live [`Live`] session and a real [`Sink`], built by
//! [`state::State::from_config`] and driven by `tokio::select!`.
//!
//! [`run`]'s bus subscription is also where the live monitor hears about a
//! sheep changing state, which is the one place this module reaches into
//! [`crate::bot`]: this loop already has the only `process.*` subscription
//! in the process, and opening a second one for the monitor's sake would
//! double the bus traffic to learn the same facts twice. Nothing else here
//! touches the bot half, and nothing in the bot half opens a stream.
//!
//! # Why ctrl-c is the only stop this loop honours
//!
//! There is no `SIGTERM` handler here, and [`run`] never drains
//! [`state::State`]'s buffers before returning. The shepherd owns this
//! process's signals (see
//! [`crate::stop`]): an ordinary stop is the shepherd killing this process
//! outright, at its default disposition, and whatever is still queued in a
//! [`buffer::Buffer`] is lost with it.
//!
//! That loss costs an operator nothing they cannot already read. Every line
//! this dog ever sends to Discord arrived here as a `log.out` or `log.err`
//! bus event, and the shepherd only emits those as it writes the same bytes
//! to the sheep's own `out_file`/`err_file` on disk. Discord is a mirror of
//! that file, never the system of record, so a mirror's tail going stale
//! across a restart is recoverable with `shep logs` and not worth a single
//! byte of extra risk.
//!
//! The risk a drain-on-stop would add is real, not theoretical. Flushing
//! the buffers on the way out means an HTTP round trip to Discord inside a
//! supervised stop, and that round trip can hang on the network for as long
//! as Discord lets it. shep's own kill ladder is already counting down
//! underneath a stop it is waiting on, so a hung drain trades a loss that
//! costs nothing for a stop that can stall until SIGKILL does the shepherd's
//! job for it. ctrl-c is the one exception, and it stays one: it is the
//! clean-exit path for somebody running this binary by hand in a terminal,
//! not a substitute for the shepherd's own kill ladder.
use fmt;
use Arc;
use ;
use crate::;
/// Why a [`Sink::send`] failed.
///
/// One variant only, deliberately. A rate limit is not this crate's problem
/// to solve: [`discord::DiscordSink`] sends through `serenity::http::Http`,
/// and that client's own `Ratelimiter` already sleeps on a `retry-after`
/// header and re-sends the request before `send` ever returns, on the
/// default path where `ratelimiter_disabled` is left `false`
/// (`http/client.rs:71`, `http/ratelimiting.rs:236` and `:377` in serenity
/// 0.12.5's vendored source). A second sleep-and-retry here would be a rate
/// limit handled twice, once inside the call this crate makes and once
/// around it, so this type must never grow a variant for one again.
/// Where a packed batch of chunks actually goes.
///
/// Narrow on purpose, the same reasoning shep-log-rotate's own `Daemon`
/// trait gives for existing at all: with the network behind one method,
/// [`state::State`] is testable against a recording fake with no client, no
/// socket, and no running bot. [`discord::DiscordSink`] is the one real
/// implementation, and [`state`]'s own tests use a recording fake.
///
/// `async fn` in a trait rather than a boxed future: this is used through a
/// generic bound only, never behind `dyn`, so the auto-trait bound an
/// `async fn` in a trait cannot spell is never missed by a caller. This
/// binary has no caller that needs one. The `async_fn_in_trait` lint stays
/// quiet here for the same reason it does in shep-log-rotate's `tick`
/// module: a binary's own trait is not a public surface for anyone to hand
/// a `dyn` object to.
/// Subscribe to this dog's bus topics and drive [`state::State`], and the
/// live monitor, until `stop` resolves or the subscription itself ends.
///
/// `own_id` is `Some` with the numeric id the shepherd assigned this dog
/// when the caller resolved one against the muster roll's own dog rows and
/// `None` when nobody adopted this process, so there is no id of its own
/// that could ever appear on the bus. See [`state::State::on_event`] for why
/// a number and not that name is what a `LogOut`/`LogErr` is filtered
/// against, and `crate::session::stream_once` for why a caller never
/// reaches this function at all while an adopted dog's own id has not
/// resolved yet: streaming unfiltered against a bus that does carry this
/// dog's own lines is the bug this project exists to fix. `names` is the
/// cache the caller's own first [`Live::flock`] already populated, so this
/// loop's first flush renders under real names rather than every id's
/// placeholder.
///
/// Subscribes to `log.out` and `log.err` for the lines themselves, and to
/// six `process.*` topics. Two of them, `process.start` and
/// `process.delete`, keep the name cache tracking what the flock holds;
/// all six are handed to `monitor`, so a sheep that stops, comes back
/// online, exits or is restarted is redrawn the moment it happens rather
/// than up to a whole refresh interval later. A `Lagged` item ends
/// nothing, matching [`shep_client::EventStream`]'s own contract.
///
/// `monitor` is `Some` only when `dogs.toml` names a `monitor_channel`,
/// and even then [`watch::Wired::on_process_event`] draws nothing while
/// the refresh task is off: that is the gate the old code kept at
/// `ready.ts:28`, and it is what stops a bus event writing to a channel an
/// operator has not turned the monitor on for.
///
/// The redraw is spawned rather than awaited here, and the reason is a
/// burst rather than a single event. A `shep restart` across a fold of
/// thirty sheep puts thirty `process.*` events on the bus at once, each
/// one a Discord edit of a few hundred milliseconds plus whatever the
/// rate limiter adds. Awaiting them in turn takes this loop off the bus
/// for the whole run, and the upstream channel holds 64 items, so the log
/// lines those same restarting sheep are emitting overflow it: the
/// operator gets a `Lagged` notice and a hole in the log channel at the
/// moment they are most likely to be reading it. Spawning keeps
/// `events.next()` and the flush ticker responsive while the edits go
/// out, and [`watch::Wired`] is behind an [`Arc`] so a spawned redraw
/// borrows nothing from this frame.
///
/// Nothing the monitor's locking guarantees is given up by that:
/// [`crate::bot::monitor::Monitor`] takes a per-sheep guard before it
/// reads its cache, so two redraws for one sheep still serialise and the
/// second still finds the message id the first posted. What does change
/// is ORDER between them. Two events for one sheep no longer necessarily
/// reach Discord in the order the bus emitted them, so a `stop` arriving
/// a moment after an `online` can leave the older state drawn. That is
/// bounded and self-correcting: [`crate::bot::monitor::refresh`] redraws
/// every sheep from one fresh muster roll on its interval, which is the
/// same mechanism that already covers the four event kinds
/// [`watch`] deliberately ignores.
///
/// The muster-roll read beside it is still awaited, because the name
/// cache the next event renders against has to be current before that
/// event is handled.
///
/// The `select!` is `biased`, stop first, the same shape shep-log-rotate's
/// own `wait` uses: a stop already requested wins over a flush tick or a
/// bus event that is also ready, rather than the coin toss an unbiased
/// select would make of it. See this module's own doc for why a stop never
/// drains what `state` is still holding.
///
/// # How a shepherd handover is survived
///
/// An [`shep_client::EventStream`] belongs to one connection generation
/// and is not re-armed across a reconnect, so the subscription simply
/// ending is how this dog learns the shepherd handed over. This function
/// used to return there, which left `main`'s own loop to notice on its
/// next pass: a flat [`RESUBSCRIBE_BUDGET`]'s worth of silence in the log
/// channel with nothing printed to say why, and twice that if the
/// reconnect outlasted one cycle. shep does not replay the bus, so every
/// line emitted in that window was gone for good.
///
/// It now waits on [`Live::connected_within`] and subscribes again on the
/// fresh generation. Two things fall out of staying in this function
/// rather than returning: [`state::State`] keeps its buffers, so lines
/// queued when the connection went are flushed after it comes back rather
/// than dropped, and the reconnect is announced on stderr instead of
/// being silent. Giving up after the budget returns `Ok`, handing the
/// retry back to `main`'s loop, which also rereads `dogs.toml` on its way
/// round.
///
/// # Errors
/// [`Error`] if a subscription request fails, the first or a later one.
/// Nothing else is fatal: a failed flush drops its own batch per
/// [`state::State::flush`], and a failed muster-roll read on a `process.*`
/// event is printed and simply leaves the name cache as it was until the
/// next one succeeds.
pub async
/// The bus topics this dog subscribes to, named once because a handover
/// subscribes again with exactly the same list.
/// How long [`run`] waits for a successor shepherd before handing the
/// retry back to `main`'s own loop.
///
/// The same length as that loop's own recheck interval, so giving up here
/// costs no more than the cycle it hands back to, and waiting here rather
/// than there is what turns a silent gap into a resubscribe the moment the
/// successor is up.
const RESUBSCRIBE_BUDGET: Duration = from_secs;
/// What waiting for a successor shepherd came to.
/// Wait for the client's supervisor to be on a connection again, or for a
/// stop, whichever comes first.
///
/// Biased on the stop, the same shape as every other wait in this dog: an
/// operator pressing ctrl-c during a handover should not sit through the
/// rest of the budget.
async
/// What is printed when the bus subscription ends.
///
/// A function rather than an inline `eprintln!`, the same reason every
/// other person-facing string in this crate is one: it lets the dash check
/// reach the text. Printed at all because the silence was half the defect:
/// an operator watching a log channel go quiet had nothing anywhere saying
/// the shepherd had handed over.
/// What is printed when no successor arrived inside the budget.