fix: forkserver child processes' logs never reach ccb.log (#319)#326
fix: forkserver child processes' logs never reach ccb.log (#319)#326bygadd wants to merge 1 commit into
Conversation
setup_logging() runs in main.py under `if __name__ == '__main__'` before the forkserver start method is set, so forkserver children re-import the package without going through __main__ and their `ccb.*` loggers lose all handlers. Per-request embed/ingest records are then silently dropped; the stdconn pipe only relays the child's raw stdout/stderr, not logging records. Add a single QueueListener bound to the parent's real `ccb` handlers (one writer, so no RotatingFileHandler rotation race) and install a QueueHandler on each child's `ccb` logger from exception_wrap. Scoped to `ccb.*`. Fixes nextcloud#319 Signed-off-by: Yoan Bozhilov <bygadd@gmail.com> Assisted-by: Claude (Anthropic)
kyteinsky
left a comment
There was a problem hiding this comment.
thanks for the PR!
there are a few issue which should be addressed though
how about adding the queue listener handler to the main app's logging config in setup_logging() so we have to do that once and it remains in the main app and does not go into the child process as intended.
the logger.py file can contain the _ensure_log_listener() and _setup_child_logging() functions so all logging stuff is there.
not related to the change but maybe we can replace the ccb logger's handers with a queue handler too so IO delays are offset but probably the pickel time is more than the IO wait?
something like: ccb_logger.handlers = [logging.handlers.QueueHandler(_log_queue)]
| _log_queue = None | ||
| _log_listener = None | ||
|
|
||
|
|
||
| def _ensure_log_listener(): | ||
| """Lazily start a QueueListener bound to the parent's real `ccb` handlers. Returns the queue | ||
| (or None if setup_logging hasn't configured any handlers yet).""" |
There was a problem hiding this comment.
there can be a race condition with the global _log_queue and _log_listener vars when multiple subprocess are started at the same time. It would be nicer to do it once at init time and cleaner to do it in logger.py file with the other logger related configuration.
| qh = logging.handlers.QueueHandler(queue) | ||
| ccb = logging.getLogger('ccb') | ||
| ccb.handlers = [qh] | ||
| ccb.setLevel(logging.DEBUG) |
There was a problem hiding this comment.
let's not assume DEBUG level. If the logger config in the main app does not accept it (has info/warning level), the data sent across the processes is wasted. It would be better to pass in the parent's log level in this function and use that.
| _log_queue = mp.Queue() | ||
| _log_listener = logging.handlers.QueueListener(_log_queue, *handlers, respect_handler_level=True) | ||
| _log_listener.start() | ||
| return _log_queue |
There was a problem hiding this comment.
the logs queue is not flushed when the program exits so we might lose some of the entries, maybe atexit can be used here.
atexit.register(_log_listener.stop)
| global _log_queue, _log_listener | ||
| if _log_listener is not None: | ||
| return _log_queue | ||
| ccb_logger = logging.getLogger('ccb') |
There was a problem hiding this comment.
the log level of this logger can be stored in a global var too (in logger.py) so the value can be fetched quickly from the child process to setup the logger at the level.
Follow-up to #319.
setup_logging()runs inmain.pyunderif __name__ == '__main__', and the forkserver start method is set right after it, so the forkserver children re-import the package without going through__main__and never run the logging setup — theirccb.*loggers end up with no handlers. Thestdconnpipe inexception_wraprelays the child's raw stdout/stderr (and the faulthandler dump on a crash), but not logging records, so the per-request embed/ingest logs from the child processes get silently dropped. On our instanceccb.logonly ever showed the main pid.This does what you were happy with in the issue: one
multiprocessing.Queueplus aQueueListenerin the parent bound to the realccbhandlers (single writer, so noRotatingFileHandlerrotation race), and aQueueHandlerinstalled on the child'sccblogger from insideexception_wrap. The queue is passed in as a kwarg fromexec_in_proc, and it's scoped toccb.*so it doesn't start relaying unrelated library logs.Running this on our 5.4.0 instance now — the child records (
ccb.injest,ccb.vectordb,ccb.nextwork_em, …) show up inccb.logwith the child pid, where before we only saw the main pid.ccb.log with the patch (pid 44 is the main worker, the rest are forkserver children)
Developed with AI assistance and verified on a live 5.4.0 deployment.