On Fri, Jul 31, 2026 at 08:07:28PM +0900, ygkat wrote: > Hi Willy, > > Thanks, this settles the shape for me: a time-based grace, no state > detection, and actconn out of the picture entirely. It also removes the > question I had about a flush hook on the exit path. If the sink holds a > job, the process simply has not reached its exit condition yet and the > flush happens in the normal poll loop, so nothing special is needed > there. > > Let me write out what I understood so you can correct it before I write > any code.
It's really painful that instead of participating to the discussion point by point you always repost a new wall of text. Each time it takes me 20mn to read your prose and I feel like I'm reading the output of an LLM. I'll try to get back to this early next week now, I still have other reviews waiting. thanks, Willy > > 1. Semantics > ============ > > Per forwarding sink, two durations, both only meaningful once stopping > is set: > > grace <ms> how long the sink may keep the worker alive > empty <ms> how long it waits for a new message while the ring is > empty > > and during that window the sink behaves exactly as it does outside of > soft-stop today: it dispatches, it waits on the ring, and it reconnects. > > when the sink enters its grace: > deadline_grace = now + grace > deadline_empty = unset > > on each wakeup during the grace: > if the ring is not empty, or something was forwarded since the > last wakeup: > deadline_empty = unset > else if deadline_empty is unset: > deadline_empty = now + empty > > if deadline_empty or deadline_grace has expired: > close the forwarders, release the job > > So "the ring stayed empty for <empty> ms" is the normal way out, and > <grace> is the hard cap that covers a downstream which is slow or gone. > A message arriving at any point puts us back in the first case. > > I wrote <empty> as a sliding window rather than a single deadline > computed once at stopping, because a one-shot deadline would close the > sink right after a message that arrived and was drained just before it > fired, which contradicts the paragraph above. Tell me if you meant it > one-shot. > > Two definitions to pin down, since both are ambiguous as soon as you > look at the code: > > - with several servers on the ring, each target keeps its own read > position, so "the ring is empty" has to be read per target: every > target has forwarded everything written to the ring. A target still > holding backlog, for instance one that is reconnecting, keeps the > empty window unarmed and we run to the grace cap. I believe that is > what you meant by wanting to wait longer when there is data. > > - "when the sink enters its grace" is when its forward_task gets the > stopping broadcast, not when the stopping flag is set. With a global > "grace" configured those are different instants: soft_stop() sets > stopping immediately and do_soft_stop_now() only runs after the > global delay. Keying the sink off its own armed state rather than > off the global flag keeps the two from interacting. > > One consequence worth stating plainly, since I over-claimed in the > opposite direction last time: a sliding <empty> closes on the first > quiet gap longer than <empty>. A straggler whose log lands after such a > gap, a long poll on a low-traffic instance for example, is still lost no > matter how large <grace> is. That is the trade-off you described, and I > think it is the right one; I just do not want to pretend the design > guarantees more than it does. > > 2. Where the job is taken > ========================= > > Agreed on incrementing jobs, and on taking it when the sink starts > rather than when the stopping signal arrives. > > I had drafted the peers variant first, taking it on the signal like the > dont_stop latch in __process_stopping_peer_sync(), before noticing that > it reintroduces the same family of race you mentioned for peers: > do_soft_stop_now() releases the listeners' jobs in protocol_stop_now() > before broadcasting signal 0, so on an otherwise idle process jobs can > reach zero and the exit condition can be met while the sink's > forward_task is still queued and has not taken its job yet. Taking it at > startup has no such window. > > A sink with no grace configured takes no job at all, so its behaviour > stays exactly today's. The only visible effect of holding one from > startup is the "Jobs" counter of "show info" being one higher per > forwarding sink, since jobs is otherwise only compared in the exit > condition. > > The deadlines and the periodic re-check still belong on the sink's > forward_task, which is already woken by the stopping broadcast as it is > registered on signal 0. That is also the fix for the busy-loop I > reported: the re-check has to drive forward_task->expire, which > process_sink_forward() already manages, and never appctx->t->expire. > > One job per sink rather than one per forwarding target, released when > one of the two deadlines fires. > > One interaction worth mentioning: hard_stop() shuts all streams down, > ours included, sets killed and re-arms itself one second later; its > second pass logs "Some tasks resisted to hard-stop", sets killed to 2, > and the poll loop exits on that. So we never hang. But in between, with > the grace still armed, the forward_task would cheerfully reconnect the > forwarders hard_stop has just killed, which is both noisy and > self-defeating. Ending the grace as soon as killed is set avoids it. > > 3. Reconnecting during grace > ============================ > > Agreed that no-reconnect is wrong. There is a detail in the way though: > process_sink_forward() allows at most one connection attempt per second > per target (sft->last_conn + 1s, last_conn being set when the session is > created). With "grace 1000", a target that loses its connection during > the window gets its next attempt up to a second after the previous one, > so at the very end of the window at best, and past it, therefore never, > at worst. The guarantee you are asking for would not actually hold at > small grace values. > > Skipping the tempo for the first attempt after a disconnection during > the grace looks like the smallest change, and it keeps the one-per- > second pace for repeated failures. I would do that unless you prefer > clamping the tempo instead. > > 4. Naming and placement > ======================= > > "grace" is still taken at the global level, and it means the other end > of the same operation: global "grace <time>" delays the soft-stop itself > after SIGUSR1 (global.grace_delay). A ring-level "grace" would mean > "keep going for this long once the soft-stop has started". Having both > in one configuration file seems likely to confuse. > > It is your call and I do not mind either way; I just did not want us to > discover it after the fact. If you would rather avoid it, something in > the timeout family reads naturally next to the "timeout connect" and > "timeout server" a ring section already accepts. > > You also mentioned the setting could live on a TCP log line. I would > start with the ring section only, give the implicit sinks created from > "log" lines the same default as everyone else, and add a per-log-line > override later if it turns out to be wanted. Those implicit sinks are > the common case, so if you would rather have the override from the > start, say so and I will do it in the same patch. > > 5. Defaults > =========== > > One thing left to decide: whether grace defaults to zero. > > Zero keeps today's behaviour for everyone and makes the whole thing > opt-in. A non-zero default fixes the common case at a fairly modest > cost: streams hold jobs anyway, so the worker already lives until its > last stream finishes, which means the sink typically only adds about > <empty> ms after the last message, and the full <grace> is only reached > when the downstream is stuck or slow. > > I would still start at zero so that a backport changes nothing, and > revisit the default separately once the mechanism is in. Tell me if you > would rather have it non-zero from the start. > > Separately, and independent of all this: an explicit "ring" section gets > no default "timeout connect" or "timeout server" at all, both stay at > TICK_ETERNITY unless set explicitly, while sink_new_from_logger() gives > the implicit rings 1s/5s. Giving the explicit ones the same defaults > looks like a small standalone fix, useful outside soft-stop too. > > 6. On the point I got wrong > =========================== > > You are right about the TOCTOU and I withdraw the claim. actconn == 0 > does not mean no more logs, since a connection sitting in the accept > queue can be accepted right after the read. As the design no longer > looks at actconn at all, the log-forward exclusion, the drain sampling > and the ring-lock ordering argument all disappear with it. Good > riddance. > > 7. Where that leaves the series > =============================== > > 1. MINOR: sink: give an explicit "ring" section the default connect > and server timeouts. Independent of the rest. > 2. MEDIUM: sink: the grace mechanism above. Takes a job when the sink > starts, keeps dispatching and reconnecting during the grace, > releases on whichever deadline fires first. Fixes both (a) and (b). > 3. (yours) the frontend option. > > That is one patch for the sink side rather than the three I proposed > last time. Splitting "flush the backlog" from "wait for the stragglers" > would only have produced an intermediate behaviour nobody wants, and > your design covers both with one mechanism anyway. > > On resuming a file-backed ring in the new process: I agree it looks > possible, and I agree about the missing read position. I would rather > leave it out of this series. > > So, to summarise what I need from you: confirmation that section 1 is > what you meant, and your preference on the naming. The rest I have > proposed an answer for and will follow unless you say otherwise. > > Thanks, > ygkat

