Add the keygrabber collector: config, scheduling and the daemon - #33
Conversation
| pool = self._pool | ||
| if pool is None: | ||
| return | ||
| pool.submit(self._run_tick, collection) |
There was a problem hiding this comment.
What happens if submit fails? (basically the pool exists but already shut down) Should this be wrapped in try/except?
try:
pool.submit(self._run_tick, collection)
except RuntimeError:
with self._in_flight_lock:
self._in_flight.discard(name)
There was a problem hiding this comment.
Good catch, it can fail and the consequence is worse than a crash: the claim was never released, so that collection would look permanently busy and every later tick would be skipped. _dispatch now returns False for both a shut-down pool and a None one, and _submit gives the claim back. Added a regression test that shuts the pool down while leaving it non-None, which is the exact race.
| # Time allowed for the retry queue to drain during shutdown. Must stay well | ||
| # under the systemd unit's TimeoutStopSec so a wedged sink cannot turn a stop | ||
| # into a SIGKILL. | ||
| DRAIN_DEADLINE_S = 5.0 |
There was a problem hiding this comment.
Should the writer thread do its own draining? When it sees _halt, it stops taking new work, drains what's left, and exits. Then on_stop just waits for it and only calls close() if exited
There was a problem hiding this comment.
Yes, and that is better than what I had. Draining from on_stop could put two threads in one sink at once, since the join had a timeout and RetryingWriter is not thread-safe. The writer now drains at the end of its own loop, on_stop waits out DRAIN_DEADLINE_S plus a margin, and only calls close() if the thread actually exited; otherwise it logs and leaves the sink to process exit rather than closing underneath it.
Makes the keygrabber run. Builds on the sink layer from #32 with config parsing, the read path, and the daemon that drives them.
KeygrabberDaemon: a scheduler thread working a due-time heap, a bounded worker pool reading through oneClient, and a single writer thread that owns every sink call, so no reader thread touches the backendCollection, which resolves a peer's keywords from onekeys.listand reads them as onekeys.readper tick. Selection is local, so a collection with several patterns still costs one requestservicesfield rather than probing, and fall back to per-keyword reads for a peer on an older libby. The fallback abandons the rest of a tick after the first timeout, since payingtimeout_sper keyword would overrun the interval many times over[a-z0-9_]+so they can become control keyword prefixes,peermust be<group>.<daemon>, and cadences must be numbersinterval_sat or below1.5 x timeout_s. bamboo waitstimeout_sfor the ACK andtimeout_s / 2more for the reply, so one unanswered read can occupy a worker for one and a half timeoutsuptimeandlasterrorby default:uptimechanges every second and says nothing a timestamp does not, andlasterroris null most of the time. Naming either explicitly opts it back insink.token, requiringtoken_envinstead, and validate every sink field before importing the backend so a malformed config reports the same way whether or not the extra is installedkeygrabberconsole entry point, with exits confined tomain.pyso the daemon stays importabledocs/source/keygrabber.mdcovering running it, the config schema, how it reads and how it stores, wired into the toctree, the API reference and the README-Wdocs build verified locallyOne thing a reviewer may want to push on:
KeygrabberDaemoncarries twelve attributes and has atoo-many-instance-attributesdisable. Extracting the scheduler into its own class would fix that and would also let the due-time ordering and the skip rule be unit-tested against a fake clock, which today are only covered end to end. That extraction is in progress on the follow-up branch, sincereloadneeds the same boundary.The control keywords are not here:
enabled, the counters,reloadand the per-collectionintervalland next, so for now the daemon is observable only through its logs.