//
//  ========================================================================
//  Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.util;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

/**
 * A facility to detect improper usage of resource pools.
 * <p>
 * Resource pools usually have a method to acquire a pooled resource and a method to released it back to the pool.
 * <p>
 * To detect if client code acquires a resource but never releases it, the resource pool can be modified to use a
 * {@link LeakDetector}. The modified resource pool should call {@link #acquired(Object)} every time the method to
 * acquire a resource is called, and {@link #released(Object)} every time the method to release the resource is called.
 * {@link LeakDetector} keeps track of these resources and invokes method
 * {@link #leaked(org.eclipse.jetty.util.LeakDetector.LeakInfo)} when it detects that a resource has been leaked (that
 * is, acquired but never released).
 * <p>
 * To detect whether client code releases a resource without having acquired it, the resource pool can be modified to
 * check the return value of {@link #released(Object)}: if false, it means that the resource was not acquired.
 * <p>
 * IMPLEMENTATION NOTES
 * <p>
 * This class relies on {@link System#identityHashCode(Object)} to create a unique id for each resource passed to
 * {@link #acquired(Object)} and {@link #released(Object)}. {@link System#identityHashCode(Object)} does not guarantee
 * that it will not generate the same number for different objects, but in practice the chance of collision is rare.
 * <p>
 * {@link LeakDetector} uses {@link PhantomReference}s to detect leaks. {@link PhantomReference}s are enqueued in their
 * {@link ReferenceQueue} <em>after</em> they have been garbage collected (differently from {@link WeakReference}s that
 * are enqueued <em>before</em>). Since the resource is now garbage collected, {@link LeakDetector} checks whether it
 * has been released and if not, it reports a leak. Using {@link PhantomReference}s is better than overriding
 * {@link #finalize()} and works also in those cases where {@link #finalize()} is not overridable.
 *
 * @param <T> the resource type.
 */
public class LeakDetector<T> extends AbstractLifeCycle implements Runnable
{
    private static final Logger LOG = Log.getLogger(LeakDetector.class);

    private final ReferenceQueue<T> queue = new ReferenceQueue<>();
    private final ConcurrentMap<String, LeakInfo> resources = new ConcurrentHashMap<>();
    private Thread thread;

    /**
     * Tracks the resource as been acquired.
     *
     * @param resource the resource that has been acquired
     * @return true whether the resource has been acquired normally, false if the resource has detected a leak (meaning
     * that another acquire occurred before a release of the same resource)
     * @see #released(Object)
     */
    public boolean acquired(T resource)
    {
        String id = id(resource);
        LeakInfo info = resources.putIfAbsent(id, new LeakInfo(resource, id));
        // Leak detected, prior acquire exists (not released) or id clash.
        return info == null; // Normal behavior.
    }

    /**
     * Tracks the resource as been released.
     *
     * @param resource the resource that has been released
     * @return true whether the resource has been released normally (based on a previous acquire). false if the resource
     * has been released without a prior acquire (such as a double release scenario)
     * @see #acquired(Object)
     */
    public boolean released(T resource)
    {
        String id = id(resource);
        LeakInfo info = resources.remove(id);
        // Normal behavior.
        return info != null;

        // Leak detected (released without acquire).
    }

    /**
     * Generates a unique ID for the given resource.
     *
     * @param resource the resource to generate the unique ID for
     * @return the unique ID of the given resource
     */
    public String id(T resource)
    {
        return String.valueOf(System.identityHashCode(resource));
    }

    @Override
    protected void doStart() throws Exception
    {
        super.doStart();
        thread = new Thread(this, getClass().getSimpleName());
        thread.setDaemon(true);
        thread.start();
    }

    @Override
    protected void doStop() throws Exception
    {
        super.doStop();
        thread.interrupt();
    }

    @Override
    public void run()
    {
        try
        {
            while (isRunning())
            {
                @SuppressWarnings("unchecked")
                LeakInfo leakInfo = (LeakInfo)queue.remove();
                if (LOG.isDebugEnabled())
                    LOG.debug("Resource GC'ed: {}", leakInfo);
                if (resources.remove(leakInfo.id) != null)
                    leaked(leakInfo);
            }
        }
        catch (InterruptedException x)
        {
            // Exit
        }
    }

    /**
     * Callback method invoked by {@link LeakDetector} when it detects that a resource has been leaked.
     *
     * @param leakInfo the information about the leak
     */
    protected void leaked(LeakInfo leakInfo)
    {
        LOG.warn("Resource leaked: " + leakInfo.description, leakInfo.stackFrames);
    }

    /**
     * Information about the leak of a resource.
     */
    public class LeakInfo extends PhantomReference<T>
    {
        private final String id;
        private final String description;
        private final Throwable stackFrames;

        private LeakInfo(T referent, String id)
        {
            super(referent, queue);
            this.id = id;
            this.description = referent.toString();
            this.stackFrames = new Throwable();
        }

        /**
         * @return the resource description as provided by the resource's {@link Object#toString()} method.
         */
        public String getResourceDescription()
        {
            return description;
        }

        /**
         * @return a Throwable instance that contains the stack frames at the time of resource acquisition.
         */
        public Throwable getStackFrames()
        {
            return stackFrames;
        }

        @Override
        public String toString()
        {
            return description;
        }
    }
}
