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}