feat: add opt-in resource leak detector to sdk-core#151
Conversation
Add a log-only detector that warns when a closeable resource (e.g. a response body) becomes phantom-reachable without having been closed. The detector is off by default and never changes program behavior: it only observes. Auto-closing a caller-owned resource from a GC callback is unsafe, so detection is strictly diagnostic. - LeakDetector tracks resources via a PhantomReference per resource plus a ReferenceQueue drained by a single daemon reaper thread. This works on every JDK from 8 up and needs no java.lang.ref.Cleaner (9+). - A tracked resource shares an AtomicBoolean with its LeakTracker; LeakTracker.closed() flips the flag and de-registers the phantom, so a cleanly-closed resource is never reported. - trackCloseable wraps an AutoCloseable so close() auto-marks the tracker, the by-hand integration point a response-body factory uses today. - systemDefault reads dexpace.sdk.leakDetection (and .captureStack); a Builder lets applications enable it explicitly and supply a custom LeakListener (default logs each leak at WARN via SLF4J). - drainManually() processes already-enqueued leaks synchronously, giving tests a race-free trigger after a GC-poll loop. Closes #45
|
This adds an opt-in, log-only resource leak detector under a new Issues
|
A caller who forgets to close a response body or stream gets no signal until something downstream breaks. This adds an opt-in, log-only detector that warns when a closeable resource becomes phantom-reachable without having been closed.
What it does
LeakDetector.track(resource, description)registers a resource and returns aLeakTracker; the resource callsLeakTracker.closed()from itsclose()to mark a clean shutdown.LeakDetector.trackCloseable(closeable, description)wraps anAutoCloseablesoclose()auto-marks the tracker. This is the by-hand integration point a response-body or stream factory would use today.LeakReport— by default a singleWARNlog line via SLF4J, optionally carrying the creation stack.What it deliberately does NOT do
It never closes anything. Auto-closing a caller-owned resource from a GC callback is unsafe (arbitrary thread, arbitrary time, transport state the caller may still own), so detection is strictly observational. Program behavior is identical whether the detector is on or off.
Off by default
LeakDetector.systemDefaultreadsdexpace.sdk.leakDetectionand is disabled unless set totrue(creation-stack capture is gated separately bydexpace.sdk.leakDetection.captureStack). ABuilderlets an application enable it explicitly and plug in a customLeakListener.Mechanism (Java 8 compatible)
Detection uses one
PhantomReferenceper tracked resource plus aReferenceQueuedrained by a single daemon reaper thread. This works identically on every JDK from 8 up and needs nojava.lang.ref.Cleaner(which is 9+). A tracked resource shares anAtomicBooleanwith its tracker, so a closed resource is never reported.Testing
drainManually()processes already-enqueued leaks synchronously, so tests drive detection with the reaper thread disabled and assert race-free after a bounded GC-poll loop (pollsSystem.gc()against aWeakReferencesentinel, fails fast rather than flaking if the collector never runs). Coverage includes: unclosed resource reported, closed resource not reported, idempotent close, disabled-path no-op, creation-stack on/off, a throwing listener not stalling the drain, thetrackCloseablewrapper paths, andsystemDefaultbeing off without the property.Gated build commands run (all green)
Closes #45