|
| 1 | +package com.skyflow.utils.logger; |
| 2 | + |
| 3 | +import java.util.concurrent.LinkedBlockingQueue; |
| 4 | +import java.util.logging.ErrorManager; |
| 5 | +import java.util.logging.Handler; |
| 6 | +import java.util.logging.LogRecord; |
| 7 | + |
| 8 | +/** |
| 9 | + * Wraps a delegate {@link Handler} (in practice a {@link java.util.logging.ConsoleHandler}) so that |
| 10 | + * {@link #publish(LogRecord)} never performs blocking I/O on the calling thread. |
| 11 | + * <p> |
| 12 | + * {@code ConsoleHandler.publish} writes to and flushes the underlying stream synchronously, and does |
| 13 | + * so under a lock shared by every thread using the logger. Since {@code LogUtil.printInfoLog}/etc. are |
| 14 | + * called on every SDK request (often several times per call), that turns console logging into a |
| 15 | + * per-request blocking-I/O + lock-contention point under concurrent load. This handler hands each |
| 16 | + * {@link LogRecord} off to a single background daemon thread instead, which performs the actual write; |
| 17 | + * the calling thread only enqueues. |
| 18 | + * <p> |
| 19 | + * The handoff never blocks or applies backpressure to the caller: if the queue is momentarily full |
| 20 | + * (a sustained logging flood, or the writer thread stalled) the record is dropped rather than slowing |
| 21 | + * down request-serving threads — logging must never become the bottleneck it was flagged for. |
| 22 | + */ |
| 23 | +final class AsyncConsoleHandler extends Handler { |
| 24 | + |
| 25 | + /** Bounds worst-case memory use if the writer thread falls behind; excess records are dropped. */ |
| 26 | + private static final int QUEUE_CAPACITY = 10_000; |
| 27 | + |
| 28 | + private final Handler delegate; |
| 29 | + private final LinkedBlockingQueue<LogRecord> queue = new LinkedBlockingQueue<>(QUEUE_CAPACITY); |
| 30 | + private final Thread writer; |
| 31 | + private volatile boolean closed = false; |
| 32 | + |
| 33 | + AsyncConsoleHandler(Handler delegate) { |
| 34 | + this.delegate = delegate; |
| 35 | + setLevel(delegate.getLevel()); |
| 36 | + this.writer = new Thread(this::drain, "skyflow-sdk-log-writer"); |
| 37 | + this.writer.setDaemon(true); |
| 38 | + this.writer.start(); |
| 39 | + } |
| 40 | + |
| 41 | + @Override |
| 42 | + public void publish(LogRecord record) { |
| 43 | + if (closed || !isLoggable(record)) { |
| 44 | + return; |
| 45 | + } |
| 46 | + // offer() never blocks: a full queue means "drop", never "wait". |
| 47 | + queue.offer(record); |
| 48 | + } |
| 49 | + |
| 50 | + private void drain() { |
| 51 | + try { |
| 52 | + while (true) { |
| 53 | + LogRecord record = queue.take(); |
| 54 | + try { |
| 55 | + delegate.publish(record); |
| 56 | + } catch (RuntimeException e) { |
| 57 | + reportError(null, e, ErrorManager.WRITE_FAILURE); |
| 58 | + } |
| 59 | + } |
| 60 | + } catch (InterruptedException e) { |
| 61 | + Thread.currentThread().interrupt(); |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + @Override |
| 66 | + public void flush() { |
| 67 | + // Best-effort: drain what's queued right now onto the delegate, then flush it. |
| 68 | + LogRecord record; |
| 69 | + while ((record = queue.poll()) != null) { |
| 70 | + delegate.publish(record); |
| 71 | + } |
| 72 | + delegate.flush(); |
| 73 | + } |
| 74 | + |
| 75 | + @Override |
| 76 | + public void close() { |
| 77 | + closed = true; |
| 78 | + writer.interrupt(); |
| 79 | + flush(); |
| 80 | + delegate.close(); |
| 81 | + } |
| 82 | +} |
0 commit comments