001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2010 Sun Microsystems, Inc.
015 * Portions copyright 2013-2014 ForgeRock AS.
016 */
017package org.forgerock.opendj.ldap;
018
019import static java.util.Collections.newSetFromMap;
020
021import java.util.Set;
022import java.util.concurrent.ConcurrentHashMap;
023
024import org.forgerock.i18n.LocalizableMessage;
025import org.forgerock.i18n.slf4j.LocalizedLogger;
026
027import com.forgerock.opendj.util.ReferenceCountedObject;
028
029/**
030 * Checks {@code TimeoutEventListener listeners} for events that have timed out.
031 * <p>
032 * All listeners registered with the {@code #addListener()} method are called
033 * back with {@code TimeoutEventListener#handleTimeout()} to be able to handle
034 * the timeout.
035 */
036public final class TimeoutChecker {
037    /**
038     * Global reference on the timeout checker.
039     */
040    public static final ReferenceCountedObject<TimeoutChecker> TIMEOUT_CHECKER =
041            new ReferenceCountedObject<TimeoutChecker>() {
042                @Override
043                protected void destroyInstance(final TimeoutChecker instance) {
044                    instance.shutdown();
045                }
046
047                @Override
048                protected TimeoutChecker newInstance() {
049                    return new TimeoutChecker();
050                }
051            };
052
053    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
054
055    /**
056     * Condition variable used for coordinating the timeout thread.
057     */
058    private final Object stateLock = new Object();
059
060    /**
061     * The listener set must be safe from CMEs. For example, if the listener is
062     * a connection, expiring requests can cause the connection to be closed.
063     */
064    private final Set<TimeoutEventListener> listeners =
065            newSetFromMap(new ConcurrentHashMap<TimeoutEventListener, Boolean>());
066
067    /**
068     * Used to signal thread shutdown.
069     */
070    private volatile boolean shutdownRequested;
071
072    /**
073     * Contains the minimum delay for listeners which were added while the
074     * timeout check was not sleeping (i.e. while it was processing listeners).
075     */
076    private volatile long pendingListenerMinDelay = Long.MAX_VALUE;
077
078    private TimeoutChecker() {
079        final Thread checkerThread = new Thread("OpenDJ LDAP SDK Timeout Checker") {
080            @Override
081            public void run() {
082                logger.debug(LocalizableMessage.raw("Timeout Checker Starting"));
083                while (!shutdownRequested) {
084                    /*
085                     * New listeners may be added during iteration and may not
086                     * be included in the computation of the new delay. This
087                     * could potentially result in the timeout checker waiting
088                     * longer than it should, or even forever (e.g. if the new
089                     * listener is the first).
090                     */
091                    final long currentTime = System.currentTimeMillis();
092                    long delay = Long.MAX_VALUE;
093                    pendingListenerMinDelay = Long.MAX_VALUE;
094                    for (final TimeoutEventListener listener : listeners) {
095                        logger.trace(LocalizableMessage.raw("Checking connection %s delay = %d", listener, delay));
096
097                        // May update the connections set.
098                        final long newDelay = listener.handleTimeout(currentTime);
099                        if (newDelay > 0) {
100                            delay = Math.min(newDelay, delay);
101                        }
102                    }
103
104                    try {
105                        synchronized (stateLock) {
106                            // Include any pending listener delays.
107                            delay = Math.min(pendingListenerMinDelay, delay);
108                            if (shutdownRequested) {
109                                // Stop immediately.
110                                break;
111                            } else if (delay <= 0) {
112                                /*
113                                 * If there is at least one connection then the
114                                 * delay should be > 0.
115                                 */
116                                stateLock.wait();
117                            } else {
118                                stateLock.wait(delay);
119                            }
120                        }
121                    } catch (final InterruptedException e) {
122                        shutdownRequested = true;
123                    }
124                }
125            }
126        };
127
128        checkerThread.setDaemon(true);
129        checkerThread.start();
130    }
131
132    /**
133     * Registers a timeout event listener for timeout notification.
134     *
135     * @param listener
136     *            The timeout event listener.
137     */
138    public void addListener(final TimeoutEventListener listener) {
139        /*
140         * Only add the listener if it has a non-zero timeout. This assumes that
141         * the timeout is fixed.
142         */
143        final long timeout = listener.getTimeout();
144        if (timeout > 0) {
145            listeners.add(listener);
146            synchronized (stateLock) {
147                pendingListenerMinDelay = Math.min(pendingListenerMinDelay, timeout);
148                stateLock.notifyAll();
149            }
150        }
151    }
152
153    /**
154     * Deregisters a timeout event listener for timeout notification.
155     *
156     * @param listener
157     *            The timeout event listener.
158     */
159    public void removeListener(final TimeoutEventListener listener) {
160        listeners.remove(listener);
161        // No need to signal.
162    }
163
164    private void shutdown() {
165        synchronized (stateLock) {
166            shutdownRequested = true;
167            stateLock.notifyAll();
168        }
169    }
170}