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