blob: 32abe805f17fc14ec83fa3da5740cc1a74fe29c0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ratis.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* A utility to detect leaks from @{@link ReferenceCountedObject}.
*/
public final class ReferenceCountedLeakDetector {
private static final Logger LOG = LoggerFactory.getLogger(ReferenceCountedLeakDetector.class);
// Leak detection is turned off by default.
private static final AtomicReference<Mode> FACTORY = new AtomicReference<>(Mode.NONE);
private static final Supplier<LeakDetector> SUPPLIER
= MemoizedSupplier.valueOf(() -> new LeakDetector(FACTORY.get().name()).start());
static Factory getFactory() {
return FACTORY.get();
}
public static LeakDetector getLeakDetector() {
return SUPPLIER.get();
}
private ReferenceCountedLeakDetector() {
}
static synchronized void enable(boolean advanced) {
FACTORY.set(advanced ? Mode.ADVANCED : Mode.SIMPLE);
}
interface Factory {
<V> ReferenceCountedObject<V> create(V value, Runnable retainMethod, Consumer<Boolean> releaseMethod);
}
private enum Mode implements Factory {
/** Leak detector is not enable in production to avoid performance impacts. */
NONE {
@Override
public <V> ReferenceCountedObject<V> create(V value, Runnable retainMethod, Consumer<Boolean> releaseMethod) {
return new Impl<>(value, retainMethod, releaseMethod);
}
},
/** Leak detector is enabled to detect leaks. This is intended to use in every tests. */
SIMPLE {
@Override
public <V> ReferenceCountedObject<V> create(V value, Runnable retainMethod, Consumer<Boolean> releaseMethod) {
return new SimpleTracing<>(value, retainMethod, releaseMethod, getLeakDetector());
}
},
/**
* Leak detector is enabled to detect leaks and report object creation stacktrace as well as every retain and
* release stacktraces. This has severe impact in performance and only used to debug specific test cases.
*/
ADVANCED {
@Override
public <V> ReferenceCountedObject<V> create(V value, Runnable retainMethod, Consumer<Boolean> releaseMethod) {
return new AdvancedTracing<>(value, retainMethod, releaseMethod, getLeakDetector());
}
}
}
private static class Impl<V> implements ReferenceCountedObject<V> {
private final AtomicInteger count;
private final V value;
private final Runnable retainMethod;
private final Consumer<Boolean> releaseMethod;
Impl(V value, Runnable retainMethod, Consumer<Boolean> releaseMethod) {
this.value = value;
this.retainMethod = retainMethod;
this.releaseMethod = releaseMethod;
count = new AtomicInteger();
}
@Override
public V get() {
final int previous = count.get();
if (previous < 0) {
throw new IllegalStateException("Failed to get: object has already been completely released.");
} else if (previous == 0) {
throw new IllegalStateException("Failed to get: object has not yet been retained.");
}
return value;
}
@Override
public V retain() {
// n < 0: exception
// n >= 0: n++
if (count.getAndUpdate(n -> n < 0? n : n + 1) < 0) {
throw new IllegalStateException("Failed to retain: object has already been completely released.");
}
retainMethod.run();
return value;
}
@Override
public boolean release() {
// n <= 0: exception
// n > 1: n--
// n == 1: n = -1
final int previous = count.getAndUpdate(n -> n <= 1? -1: n - 1);
if (previous < 0) {
throw new IllegalStateException("Failed to release: object has already been completely released.");
} else if (previous == 0) {
throw new IllegalStateException("Failed to release: object has not yet been retained.");
}
final boolean completedReleased = previous == 1;
releaseMethod.accept(completedReleased);
return completedReleased;
}
}
private static class SimpleTracing<T> extends Impl<T> {
private final UncheckedAutoCloseable leakTracker;
SimpleTracing(T value, Runnable retainMethod, Consumer<Boolean> releaseMethod, LeakDetector leakDetector) {
super(value, retainMethod, releaseMethod);
final Class<?> clazz = value.getClass();
this.leakTracker = leakDetector.track(this,
() -> LOG.warn("LEAK: A {} is not released properly", clazz.getName()));
}
@Override
public boolean release() {
boolean released = super.release();
if (released) {
leakTracker.close();
}
return released;
}
}
private static class AdvancedTracing<T> extends Impl<T> {
private final UncheckedAutoCloseable leakTracker;
private final List<StackTraceElement[]> retainsTraces;
private final List<StackTraceElement[]> releaseTraces;
AdvancedTracing(T value, Runnable retainMethod, Consumer<Boolean> releaseMethod, LeakDetector leakDetector) {
super(value, retainMethod, releaseMethod);
StackTraceElement[] createStrace = Thread.currentThread().getStackTrace();
final Class<?> clazz = value.getClass();
final List<StackTraceElement[]> localRetainsTraces = new LinkedList<>();
final List<StackTraceElement[]> localReleaseTraces = new LinkedList<>();
this.leakTracker = leakDetector.track(this, () ->
LOG.warn("LEAK: A {} is not released properly.\nCreation trace:\n{}\n" +
"Retain traces({}):\n{}\nRelease traces({}):\n{}",
clazz.getName(), formatStackTrace(createStrace, 3),
localRetainsTraces.size(), formatStackTraces(localRetainsTraces, 2),
localReleaseTraces.size(), formatStackTraces(localReleaseTraces, 2)));
this.retainsTraces = localRetainsTraces;
this.releaseTraces = localReleaseTraces;
}
@Override
public T retain() {
T retain = super.retain();
retainsTraces.add(Thread.currentThread().getStackTrace());
return retain;
}
@Override
public boolean release() {
boolean released = super.release();
if (released) {
leakTracker.close();
}
releaseTraces.add(Thread.currentThread().getStackTrace());
return released;
}
}
private static String formatStackTrace(StackTraceElement[] stackTrace, int startIdx) {
final StringBuilder sb = new StringBuilder();
for (int line = startIdx; line < stackTrace.length; line++) {
sb.append(stackTrace[line]).append("\n");
}
return sb.toString();
}
private static String formatStackTraces(List<StackTraceElement[]> stackTraces, int startIdx) {
final StringBuilder sb = new StringBuilder();
stackTraces.forEach(stackTrace -> {
if (sb.length() > 0) {
sb.append("\n");
}
for (int line = startIdx; line < stackTrace.length; line++) {
sb.append(stackTrace[line]).append("\n");
}
});
return sb.toString();
}
}