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 2009-2010 Sun Microsystems, Inc. 025 * Portions Copyright 2011-2015 ForgeRock AS. 026 */ 027 028package org.forgerock.opendj.ldap; 029 030import static com.forgerock.opendj.ldap.CoreMessages.HBCF_CONNECTION_CLOSED_BY_CLIENT; 031import static com.forgerock.opendj.ldap.CoreMessages.HBCF_HEARTBEAT_FAILED; 032import static com.forgerock.opendj.ldap.CoreMessages.HBCF_HEARTBEAT_TIMEOUT; 033import static com.forgerock.opendj.ldap.CoreMessages.LDAP_CONNECTION_CONNECT_TIMEOUT; 034import static com.forgerock.opendj.util.StaticUtils.DEFAULT_SCHEDULER; 035import static java.util.concurrent.TimeUnit.SECONDS; 036import static org.forgerock.opendj.ldap.LdapException.newLdapException; 037import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest; 038import static org.forgerock.opendj.ldap.requests.Requests.newStartTLSExtendedRequest; 039import static org.forgerock.opendj.ldap.requests.Requests.unmodifiableSearchRequest; 040import static org.forgerock.opendj.ldap.responses.Responses.newBindResult; 041import static org.forgerock.opendj.ldap.responses.Responses.newGenericExtendedResult; 042import static org.forgerock.opendj.ldap.responses.Responses.newResult; 043import static org.forgerock.opendj.ldap.spi.LdapPromiseImpl.newLdapPromiseImpl; 044import static org.forgerock.opendj.ldap.spi.LdapPromises.newFailedLdapPromise; 045import static org.forgerock.util.Utils.closeSilently; 046import static org.forgerock.util.promise.Promises.newResultPromise; 047 048import java.util.Collections; 049import java.util.LinkedList; 050import java.util.List; 051import java.util.Queue; 052import java.util.concurrent.ConcurrentLinkedQueue; 053import java.util.concurrent.ScheduledExecutorService; 054import java.util.concurrent.ScheduledFuture; 055import java.util.concurrent.TimeUnit; 056import java.util.concurrent.atomic.AtomicBoolean; 057import java.util.concurrent.atomic.AtomicInteger; 058import java.util.concurrent.atomic.AtomicReference; 059import java.util.concurrent.locks.AbstractQueuedSynchronizer; 060 061import javax.net.ssl.SSLContext; 062 063import org.forgerock.i18n.LocalizableMessage; 064import org.forgerock.i18n.slf4j.LocalizedLogger; 065import org.forgerock.opendj.ldap.requests.AbandonRequest; 066import org.forgerock.opendj.ldap.requests.AddRequest; 067import org.forgerock.opendj.ldap.requests.BindRequest; 068import org.forgerock.opendj.ldap.requests.CompareRequest; 069import org.forgerock.opendj.ldap.requests.DeleteRequest; 070import org.forgerock.opendj.ldap.requests.ExtendedRequest; 071import org.forgerock.opendj.ldap.requests.ModifyDNRequest; 072import org.forgerock.opendj.ldap.requests.ModifyRequest; 073import org.forgerock.opendj.ldap.requests.SearchRequest; 074import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest; 075import org.forgerock.opendj.ldap.requests.UnbindRequest; 076import org.forgerock.opendj.ldap.responses.BindResult; 077import org.forgerock.opendj.ldap.responses.CompareResult; 078import org.forgerock.opendj.ldap.responses.ExtendedResult; 079import org.forgerock.opendj.ldap.responses.Result; 080import org.forgerock.opendj.ldap.responses.SearchResultEntry; 081import org.forgerock.opendj.ldap.responses.SearchResultReference; 082import org.forgerock.opendj.ldap.spi.ConnectionState; 083import org.forgerock.opendj.ldap.spi.LDAPConnectionFactoryImpl; 084import org.forgerock.opendj.ldap.spi.LDAPConnectionImpl; 085import org.forgerock.opendj.ldap.spi.LdapPromiseImpl; 086import org.forgerock.opendj.ldap.spi.TransportProvider; 087import org.forgerock.util.AsyncFunction; 088import org.forgerock.util.Function; 089import org.forgerock.util.Option; 090import org.forgerock.util.Options; 091import org.forgerock.util.Reject; 092import org.forgerock.util.promise.ExceptionHandler; 093import org.forgerock.util.promise.Promise; 094import org.forgerock.util.promise.PromiseImpl; 095import org.forgerock.util.promise.ResultHandler; 096import org.forgerock.util.time.Duration; 097import org.forgerock.util.time.TimeService; 098 099import com.forgerock.opendj.util.ReferenceCountedObject; 100 101/** 102 * A factory class which can be used to obtain connections to an LDAP Directory Server. A connection attempt comprises 103 * of the following steps: 104 * <ul> 105 * <li>first of all a TCP connection to the remote LDAP server is obtained. The attempt will fail if a connection is 106 * not obtained within the configured {@link #CONNECT_TIMEOUT connect timeout} 107 * <li>if LDAPS (not StartTLS) is requested then an SSL handshake is performed. LDAPS is enabled by specifying the 108 * {@link #SSL_CONTEXT} option along with {@link #SSL_USE_STARTTLS} set to {@code false} 109 * <li>if StartTLS is requested then a StartTLS request is sent and then an SSL handshake performed once the response 110 * has been received. StartTLS is enabled by specifying the {@link #SSL_CONTEXT} option along with 111 * {@link #SSL_USE_STARTTLS} set to {@code true} 112 * <li>an initial authentication request is sent if the {@link #AUTHN_BIND_REQUEST} option is specified 113 * <li>if heart-beat support is enabled via the {@link #HEARTBEAT_ENABLED} option, and none of steps 2-4 were performed, 114 * then an initial heart-beat is sent in order to determine whether the directory service is available. 115 * <li>the connect attempt will fail if it does not complete within the configured connection timeout. If the SSL 116 * handshake, StartTLS request, initial bind request, or initial heart-beat fail for any reason then the connection 117 * attempt will be deemed to have failed and an appropriate error returned. 118 * </ul> 119 * Once a connection has been established heart-beats will be sent periodically on the connection based on the 120 * configured heart-beat interval. If the heart-beat times out then the server is assumed to be down and an appropriate 121 * {@link ConnectionException} generated and published to any registered {@link ConnectionEventListener}s. Note 122 * however, that heart-beats will only be sent when the connection is determined to be reasonably idle: there is no 123 * point in sending heart-beats if the connection has recently received a response. A connection is deemed to be idle 124 * if no response has been received during a period equivalent to half the heart-beat interval. 125 * <p> 126 * The LDAP protocol specifically precludes clients from performing operations while bind or startTLS requests are being 127 * performed. Likewise, a bind or startTLS request will cause active operations to be aborted. This factory coordinates 128 * heart-beats with bind or startTLS requests, ensuring that they are not performed concurrently. Specifically, bind and 129 * startTLS requests are queued up while a heart-beat is pending, and heart-beats are not sent at all while there are 130 * pending bind or startTLS requests. 131 */ 132public final class LDAPConnectionFactory extends CommonLDAPOptions implements ConnectionFactory { 133 /** 134 * Configures the connection factory to return pre-authenticated connections using the specified {@link 135 * BindRequest}. The connections returned by the connection factory will support all operations with the exception 136 * of Bind requests. Attempts to perform a Bind will result in an {@code UnsupportedOperationException}. 137 * <p> 138 * If the Bind request fails for some reason (e.g. invalid credentials), then the connection attempt will fail and 139 * an {@link LdapException} will be thrown. 140 */ 141 public static final Option<BindRequest> AUTHN_BIND_REQUEST = Option.of(BindRequest.class, null); 142 143 /** 144 * Specifies the connect timeout spcified. If a connection is not established within the timeout period (incl. SSL 145 * negotiation, initial bind request, and/or heart-beat), then a {@link TimeoutResultException} error result will be 146 * returned. 147 * <p> 148 * The default operation timeout is 10 seconds and may be configured using the {@code 149 * org.forgerock.opendj.io.connectTimeout} property. A timeout setting of 0 causes the OS connect timeout to be 150 * used. 151 */ 152 public static final Option<Duration> CONNECT_TIMEOUT = 153 Option.withDefault(new Duration((long) getIntProperty("org.forgerock.opendj.io.connectTimeout", 10000), 154 TimeUnit.MILLISECONDS)); 155 156 /** 157 * Configures the connection factory to periodically send "heart-beat" or "keep-alive" requests to the Directory 158 * Server. This feature allows client applications to proactively detect network problems or unresponsive 159 * servers. In addition, frequent heartbeat requests may also prevent load-balancers or Directory Servers from 160 * closing otherwise idle connections. 161 * <p> 162 * Before returning new connections to the application the factory will first send an initial heart-beat request in 163 * order to determine that the remote server is responsive. If the heart-beat request fails or is too slow to 164 * respond then the connection is closed immediately and an error returned to the client. 165 * <p> 166 * Once a connection has been established successfully (including the initial heart-beat request), the connection 167 * factory will periodically send heart-beat requests on the connection based on the configured heart-beat interval. 168 * If the Directory Server is too slow to respond to the heart-beat then the server is assumed to be down and an 169 * appropriate {@link ConnectionException} generated and published to any registered 170 * {@link ConnectionEventListener}s. Note however, that heart-beat requests will only be sent when the connection 171 * is determined to be reasonably idle: there is no point in sending heart-beats if the connection has recently 172 * received a response. A connection is deemed to be idle if no response has been received during a period 173 * equivalent to half the heart-beat interval. 174 * <p> 175 * The LDAP protocol specifically precludes clients from performing operations while bind or startTLS requests are 176 * being performed. Likewise, a bind or startTLS request will cause active operations to be aborted. The LDAP 177 * connection factory coordinates heart-beats with bind or startTLS requests, ensuring that they are not performed 178 * concurrently. Specifically, bind and startTLS requests are queued up while a heart-beat is pending, and 179 * heart-beats are not sent at all while there are pending bind or startTLS requests. 180 */ 181 public static final Option<Boolean> HEARTBEAT_ENABLED = Option.withDefault(false); 182 183 /** 184 * Specifies the time between successive heart-beat requests (default interval is 10 seconds). Heart-beats will only 185 * be sent if {@link #HEARTBEAT_ENABLED} is set to {@code true}. 186 * 187 * @see #HEARTBEAT_ENABLED 188 */ 189 public static final Option<Duration> HEARTBEAT_INTERVAL = Option.withDefault(new Duration(10L, SECONDS)); 190 191 /** 192 * Specifies the scheduler which will be used for periodically sending heart-beat requests. A system-wide scheduler 193 * will be used by default. Heart-beats will only be sent if {@link #HEARTBEAT_ENABLED} is set to {@code true}. 194 * 195 * @see #HEARTBEAT_ENABLED 196 */ 197 public static final Option<ScheduledExecutorService> HEARTBEAT_SCHEDULER = 198 Option.of(ScheduledExecutorService.class, null); 199 200 /** 201 * Specifies the timeout for heart-beat requests, after which the remote Directory Server will be deemed to be 202 * unavailable (default timeout is 3 seconds). Heart-beats will only be sent if {@link #HEARTBEAT_ENABLED} is set to 203 * {@code true}. If a {@link #REQUEST_TIMEOUT request timeout} is also set then the lower of the two will be used 204 * for sending heart-beats. 205 * 206 * @see #HEARTBEAT_ENABLED 207 */ 208 public static final Option<Duration> HEARTBEAT_TIMEOUT = Option.withDefault(new Duration(3L, SECONDS)); 209 210 /** 211 * Specifies the operation timeout. If a response is not received from the Directory Server within the timeout 212 * period, then the operation will be abandoned and a {@link TimeoutResultException} error result returned. A 213 * timeout setting of 0 disables operation timeout limits. 214 * <p> 215 * The default operation timeout is 0 (no timeout) and may be configured using the {@code 216 * org.forgerock.opendj.io.requestTimeout} property or the deprecated {@code org.forgerock.opendj.io.timeout} 217 * property. 218 */ 219 public static final Option<Duration> REQUEST_TIMEOUT = 220 Option.withDefault(new Duration((long) getIntProperty("org.forgerock.opendj.io.requestTimeout", 221 getIntProperty("org.forgerock.opendj.io.timeout", 0)), 222 TimeUnit.MILLISECONDS)); 223 224 /** 225 * Specifies the SSL context which will be used when initiating connections with the Directory Server. 226 * <p> 227 * By default no SSL context will be used, indicating that connections will not be secured. If an SSL context is set 228 * then connections will be secured using either SSL or StartTLS depending on {@link #SSL_USE_STARTTLS}. 229 */ 230 public static final Option<SSLContext> SSL_CONTEXT = Option.of(SSLContext.class, null); 231 232 /** 233 * Specifies the cipher suites enabled for secure connections with the Directory Server. 234 * <p> 235 * The suites must be supported by the SSLContext specified by option {@link #SSL_CONTEXT}. Only the suites listed 236 * in the parameter are enabled for use. 237 */ 238 @SuppressWarnings({ "unchecked", "rawtypes" }) 239 public static final Option<List<String>> SSL_ENABLED_CIPHER_SUITES = 240 (Option) Option.of(List.class, Collections.<String>emptyList()); 241 242 /** 243 * Specifies the protocol versions enabled for secure connections with the Directory Server. 244 * <p> 245 * The protocols must be supported by the SSLContext specified by option {@link #SSL_CONTEXT}. Only the protocols 246 * listed in the parameter are enabled for use. 247 */ 248 @SuppressWarnings({ "unchecked", "rawtypes" }) 249 public static final Option<List<String>> SSL_ENABLED_PROTOCOLS = 250 (Option) Option.of(List.class, Collections.<String>emptyList()); 251 252 /** 253 * Specifies whether SSL or StartTLS should be used for securing connections when an SSL context is specified. 254 * <p> 255 * By default SSL will be used in preference to StartTLS. 256 */ 257 public static final Option<Boolean> SSL_USE_STARTTLS = Option.withDefault(false); 258 259 /** Default heart-beat which will target the root DSE but not return any results. */ 260 private static final SearchRequest DEFAULT_HEARTBEAT = 261 unmodifiableSearchRequest(newSearchRequest("", SearchScope.BASE_OBJECT, "(objectClass=*)", "1.1")); 262 263 /** 264 * Specifies the parameters of the search request that will be used for heart-beats. The default heart-beat search 265 * request is a base object search against the root DSE requesting no attributes. Heart-beats will only be sent if 266 * {@link #HEARTBEAT_ENABLED} is set to {@code true}. 267 * 268 * @see #HEARTBEAT_ENABLED 269 */ 270 public static final Option<SearchRequest> HEARTBEAT_SEARCH_REQUEST = 271 Option.of(SearchRequest.class, DEFAULT_HEARTBEAT); 272 273 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 274 275 /** The overall timeout to use when establishing connections, including SSL, bind, and heart-beat. */ 276 private final long connectTimeoutMS; 277 278 /** 279 * The minimum amount of time the connection should remain idle (no responses) before starting to send heartbeats. 280 */ 281 private final long heartBeatDelayMS; 282 283 /** Indicates whether heartbeats should be performed. */ 284 private final Boolean heartBeatEnabled; 285 286 /** The heartbeat search request. */ 287 private final SearchRequest heartBeatRequest; 288 289 /** 290 * The heartbeat timeout in milli-seconds. The connection will be marked as failed if no heartbeat response is 291 * received within the timeout. 292 */ 293 private final long heartBeatTimeoutMS; 294 295 /** The interval between successive heartbeats. */ 296 private final long heartBeatintervalMS; 297 298 /** The factory responsible for handling the low-level network communication with the Directory Server. */ 299 private final LDAPConnectionFactoryImpl impl; 300 301 /** The optional bind request which will be used as the initial heartbeat if specified. */ 302 private final BindRequest initialBindRequest; 303 304 /** Flag which indicates whether this factory has been closed. */ 305 private final AtomicBoolean isClosed = new AtomicBoolean(); 306 307 /** A copy of the original options. This is only useful for debugging. */ 308 private final Options options; 309 310 /** Transport provider that provides the implementation of this factory. */ 311 private final TransportProvider provider; 312 313 /** 314 * Prevents the scheduler being released when there are remaining references (this factory or any connections). It 315 * is initially set to 1 because this factory has a reference. 316 */ 317 private final AtomicInteger referenceCount = new AtomicInteger(1); 318 319 /** The heartbeat scheduler. */ 320 private final ReferenceCountedObject<ScheduledExecutorService>.Reference scheduler; 321 322 /** Non-null if SSL or StartTLS should be used when creating new connections. */ 323 private final SSLContext sslContext; 324 325 /** The list of permitted SSL ciphers for SSL negotiation. */ 326 private final List<String> sslEnabledCipherSuites; 327 328 /** The list of permitted SSL protocols for SSL negotiation. */ 329 private final List<String> sslEnabledProtocols; 330 331 /** Indicates whether a StartTLS request should be sent immediately after connecting. */ 332 private final boolean sslUseStartTLS; 333 334 /** List of valid connections to which heartbeats will be sent. */ 335 private final List<ConnectionImpl> validConnections = new LinkedList<>(); 336 337 /** This is package private in order to allow unit tests to inject fake time stamps. */ 338 TimeService timeService = TimeService.SYSTEM; 339 340 /** Scheduled task which sends heart beats for all valid idle connections. */ 341 private final Runnable sendHeartBeatRunnable = new Runnable() { 342 @Override 343 public void run() { 344 boolean heartBeatSent = false; 345 for (final ConnectionImpl connection : getValidConnections()) { 346 heartBeatSent |= connection.sendHeartBeat(); 347 } 348 if (heartBeatSent) { 349 scheduler.get().schedule(checkHeartBeatRunnable, heartBeatTimeoutMS, TimeUnit.MILLISECONDS); 350 } 351 } 352 }; 353 354 /** Scheduled task which checks that all heart beats have been received within the timeout period. */ 355 private final Runnable checkHeartBeatRunnable = new Runnable() { 356 @Override 357 public void run() { 358 for (final ConnectionImpl connection : getValidConnections()) { 359 connection.checkForHeartBeat(); 360 } 361 } 362 }; 363 364 /** The heartbeat scheduled future - which may be null if heartbeats are not being sent (no valid connections). */ 365 private ScheduledFuture<?> heartBeatFuture; 366 367 /** 368 * Creates a new LDAP connection factory which can be used to create LDAP connections to the Directory Server at the 369 * provided host and port number. 370 * 371 * @param host 372 * The host name. 373 * @param port 374 * The port number. 375 * @throws NullPointerException 376 * If {@code host} was {@code null}. 377 * @throws ProviderNotFoundException 378 * if no provider is available or if the provider requested using options is not found. 379 */ 380 public LDAPConnectionFactory(final String host, final int port) { 381 this(host, port, Options.defaultOptions()); 382 } 383 384 /** 385 * Creates a new LDAP connection factory which can be used to create LDAP connections to the Directory Server at the 386 * provided host and port number. 387 * 388 * @param host 389 * The host name. 390 * @param port 391 * The port number. 392 * @param options 393 * The LDAP options to use when creating connections. 394 * @throws NullPointerException 395 * If {@code host} or {@code options} was {@code null}. 396 * @throws ProviderNotFoundException 397 * if no provider is available or if the provider requested using options is not found. 398 */ 399 public LDAPConnectionFactory(final String host, final int port, final Options options) { 400 Reject.ifNull(host, options); 401 402 this.connectTimeoutMS = options.get(CONNECT_TIMEOUT).to(TimeUnit.MILLISECONDS); 403 Reject.ifTrue(connectTimeoutMS < 0, "connect timeout must be >= 0"); 404 Reject.ifTrue(options.get(REQUEST_TIMEOUT).getValue() < 0, "request timeout must be >= 0"); 405 406 this.heartBeatEnabled = options.get(HEARTBEAT_ENABLED); 407 this.heartBeatintervalMS = options.get(HEARTBEAT_INTERVAL).to(TimeUnit.MILLISECONDS); 408 this.heartBeatTimeoutMS = options.get(HEARTBEAT_TIMEOUT).to(TimeUnit.MILLISECONDS); 409 this.heartBeatDelayMS = heartBeatintervalMS / 2; 410 this.heartBeatRequest = options.get(HEARTBEAT_SEARCH_REQUEST); 411 if (heartBeatEnabled) { 412 Reject.ifTrue(heartBeatintervalMS <= 0, "heart-beat interval must be positive"); 413 Reject.ifTrue(heartBeatTimeoutMS <= 0, "heart-beat timeout must be positive"); 414 } 415 416 this.provider = getTransportProvider(options); 417 this.scheduler = DEFAULT_SCHEDULER.acquireIfNull(options.get(HEARTBEAT_SCHEDULER)); 418 this.impl = provider.getLDAPConnectionFactory(host, port, options); 419 this.initialBindRequest = options.get(AUTHN_BIND_REQUEST); 420 this.sslContext = options.get(SSL_CONTEXT); 421 this.sslUseStartTLS = options.get(SSL_USE_STARTTLS); 422 this.sslEnabledProtocols = options.get(SSL_ENABLED_PROTOCOLS); 423 this.sslEnabledCipherSuites = options.get(SSL_ENABLED_CIPHER_SUITES); 424 425 this.options = Options.copyOf(options); 426 } 427 428 @Override 429 public void close() { 430 if (isClosed.compareAndSet(false, true)) { 431 synchronized (validConnections) { 432 if (!validConnections.isEmpty()) { 433 logger.debug(LocalizableMessage.raw( 434 "HeartbeatConnectionFactory '%s' is closing while %d active connections remain", 435 this, 436 validConnections.size())); 437 } 438 } 439 releaseScheduler(); 440 impl.close(); 441 } 442 } 443 444 @Override 445 public Connection getConnection() throws LdapException { 446 return getConnectionAsync().getOrThrowUninterruptibly(); 447 } 448 449 @Override 450 public Promise<Connection, LdapException> getConnectionAsync() { 451 acquireScheduler(); // Protect scheduler. 452 453 // Register the connect timeout timer. 454 final PromiseImpl<Connection, LdapException> promise = PromiseImpl.create(); 455 final AtomicReference<LDAPConnectionImpl> connectionHolder = new AtomicReference<>(); 456 final ScheduledFuture<?> timeoutFuture; 457 if (connectTimeoutMS > 0) { 458 timeoutFuture = scheduler.get().schedule(new Runnable() { 459 @Override 460 public void run() { 461 if (promise.tryHandleException(newConnectTimeoutError())) { 462 closeSilently(connectionHolder.get()); 463 releaseScheduler(); 464 } 465 } 466 }, connectTimeoutMS, TimeUnit.MILLISECONDS); 467 } else { 468 timeoutFuture = null; 469 } 470 471 // Now connect, negotiate SSL, etc. 472 impl.getConnectionAsync() 473 // Save the connection. 474 .then(new Function<LDAPConnectionImpl, LDAPConnectionImpl, LdapException>() { 475 @Override 476 public LDAPConnectionImpl apply(final LDAPConnectionImpl connection) throws LdapException { 477 connectionHolder.set(connection); 478 return connection; 479 } 480 }) 481 .thenAsync(performStartTLSIfNeeded()) 482 .thenAsync(performSSLHandShakeIfNeeded(connectionHolder)) 483 .thenAsync(performInitialBindIfNeeded(connectionHolder)) 484 .thenAsync(performInitialHeartBeatIfNeeded(connectionHolder)) 485 .thenOnResult(new ResultHandler<Result>() { 486 @Override 487 public void handleResult(Result result) { 488 if (timeoutFuture != null) { 489 timeoutFuture.cancel(false); 490 } 491 final LDAPConnectionImpl connection = connectionHolder.get(); 492 final ConnectionImpl connectionImpl = new ConnectionImpl(connection); 493 if (!promise.tryHandleResult(registerConnection(connectionImpl))) { 494 connectionImpl.close(); 495 } 496 } 497 }) 498 .thenOnException(new ExceptionHandler<LdapException>() { 499 @Override 500 public void handleException(final LdapException e) { 501 if (timeoutFuture != null) { 502 timeoutFuture.cancel(false); 503 } 504 final LdapException connectException; 505 if (e instanceof ConnectionException || e instanceof AuthenticationException) { 506 connectException = e; 507 } else if (e instanceof TimeoutResultException) { 508 connectException = newHeartBeatTimeoutError(); 509 } else { 510 connectException = newLdapException(ResultCode.CLIENT_SIDE_SERVER_DOWN, 511 HBCF_HEARTBEAT_FAILED.get(), 512 e); 513 } 514 if (promise.tryHandleException(connectException)) { 515 closeSilently(connectionHolder.get()); 516 releaseScheduler(); 517 } 518 } 519 }); 520 521 return promise; 522 } 523 524 /** 525 * Returns the host name of the Directory Server. The returned host name is the same host name that was provided 526 * during construction and may be an IP address. More specifically, this method will not perform a reverse DNS 527 * lookup. 528 * 529 * @return The host name of the Directory Server. 530 */ 531 public String getHostName() { 532 return impl.getHostName(); 533 } 534 535 /** 536 * Returns the port of the Directory Server. 537 * 538 * @return The port of the Directory Server. 539 */ 540 public int getPort() { 541 return impl.getPort(); 542 } 543 544 /** 545 * Returns the name of the transport provider, which provides the implementation of this factory. 546 * 547 * @return The name of actual transport provider. 548 */ 549 public String getProviderName() { 550 return provider.getName(); 551 } 552 553 @Override 554 public String toString() { 555 return "LDAPConnectionFactory(provider=`" + getProviderName() + ", host='" + getHostName() + "', port=" 556 + getPort() + ", options=" + options + ")"; 557 } 558 559 private void acquireScheduler() { 560 /* 561 * If the factory is not closed then we need to prevent the scheduler from being released while the 562 * connection attempt is in progress. 563 */ 564 referenceCount.incrementAndGet(); 565 if (isClosed.get()) { 566 releaseScheduler(); 567 throw new IllegalStateException("Attempted to get a connection on closed factory"); 568 } 569 } 570 571 private ConnectionImpl[] getValidConnections() { 572 synchronized (validConnections) { 573 return validConnections.toArray(new ConnectionImpl[validConnections.size()]); 574 } 575 } 576 577 private LdapException newConnectTimeoutError() { 578 final LocalizableMessage msg = LDAP_CONNECTION_CONNECT_TIMEOUT.get(impl.getSocketAddress(), connectTimeoutMS); 579 return newLdapException(ResultCode.CLIENT_SIDE_CONNECT_ERROR, msg.toString()); 580 } 581 582 private LdapException newHeartBeatTimeoutError() { 583 return newLdapException(ResultCode.CLIENT_SIDE_SERVER_DOWN, HBCF_HEARTBEAT_TIMEOUT.get(heartBeatTimeoutMS)); 584 } 585 586 private AsyncFunction<Void, BindResult, LdapException> performInitialBindIfNeeded( 587 final AtomicReference<LDAPConnectionImpl> connectionHolder) { 588 return new AsyncFunction<Void, BindResult, LdapException>() { 589 @Override 590 public Promise<BindResult, LdapException> apply(final Void ignored) throws LdapException { 591 if (initialBindRequest != null) { 592 return connectionHolder.get().bindAsync(initialBindRequest, null); 593 } else { 594 return newResultPromise(newBindResult(ResultCode.SUCCESS)); 595 } 596 } 597 }; 598 } 599 600 private AsyncFunction<BindResult, Result, LdapException> performInitialHeartBeatIfNeeded( 601 final AtomicReference<LDAPConnectionImpl> connectionHolder) { 602 return new AsyncFunction<BindResult, Result, LdapException>() { 603 @Override 604 public Promise<Result, LdapException> apply(final BindResult ignored) throws LdapException { 605 // Only send an initial heartbeat if we haven't already interacted with the server. 606 if (heartBeatEnabled && sslContext == null && initialBindRequest == null) { 607 return connectionHolder.get().searchAsync(heartBeatRequest, null, null); 608 } else { 609 return newResultPromise(newResult(ResultCode.SUCCESS)); 610 } 611 } 612 }; 613 } 614 615 private AsyncFunction<ExtendedResult, Void, LdapException> performSSLHandShakeIfNeeded( 616 final AtomicReference<LDAPConnectionImpl> connectionHolder) { 617 return new AsyncFunction<ExtendedResult, Void, LdapException>() { 618 @Override 619 public Promise<Void, LdapException> apply(final ExtendedResult extendedResult) throws LdapException { 620 if (sslContext != null && !sslUseStartTLS) { 621 return connectionHolder.get().enableTLS(sslContext, sslEnabledProtocols, sslEnabledCipherSuites); 622 } else { 623 return newResultPromise(null); 624 } 625 } 626 }; 627 } 628 629 private AsyncFunction<LDAPConnectionImpl, ExtendedResult, LdapException> performStartTLSIfNeeded() { 630 return new AsyncFunction<LDAPConnectionImpl, ExtendedResult, LdapException>() { 631 @Override 632 public Promise<ExtendedResult, LdapException> apply(final LDAPConnectionImpl connection) 633 throws LdapException { 634 if (sslContext != null && sslUseStartTLS) { 635 final StartTLSExtendedRequest startTLS = newStartTLSExtendedRequest(sslContext) 636 .addEnabledCipherSuite(sslEnabledCipherSuites) 637 .addEnabledProtocol(sslEnabledProtocols); 638 return connection.extendedRequestAsync(startTLS, null); 639 } else { 640 return newResultPromise((ExtendedResult) newGenericExtendedResult(ResultCode.SUCCESS)); 641 } 642 } 643 }; 644 } 645 646 private Connection registerConnection(final ConnectionImpl heartBeatConnection) { 647 synchronized (validConnections) { 648 if (heartBeatEnabled && validConnections.isEmpty()) { 649 // This is the first active connection, so start the heart beat. 650 heartBeatFuture = scheduler.get() 651 .scheduleWithFixedDelay(sendHeartBeatRunnable, 652 0, 653 heartBeatintervalMS, 654 TimeUnit.MILLISECONDS); 655 } 656 validConnections.add(heartBeatConnection); 657 } 658 return heartBeatConnection; 659 } 660 661 private void releaseScheduler() { 662 if (referenceCount.decrementAndGet() == 0) { 663 scheduler.release(); 664 } 665 } 666 667 /** 668 * This synchronizer prevents Bind or StartTLS operations from being processed concurrently with heart-beats. This 669 * is required because the LDAP protocol specifically states that servers receiving a Bind operation should either 670 * wait for existing operations to complete or abandon them. The same presumably applies to StartTLS operations. 671 * Note that concurrent bind/StartTLS operations are not permitted. 672 * <p> 673 * This connection factory only coordinates Bind and StartTLS requests with heart-beats. It does not attempt to 674 * prevent or control attempts to send multiple concurrent Bind or StartTLS operations, etc. 675 * <p> 676 * This synchronizer can be thought of as cross between a read-write lock and a semaphore. Unlike a read-write lock 677 * there is no requirement that a thread releasing a lock must hold it. In addition, this synchronizer does not 678 * support reentrancy. A thread attempting to acquire exclusively more than once will deadlock, and a thread 679 * attempting to acquire shared more than once will succeed and be required to release an equivalent number of 680 * times. 681 * <p> 682 * The synchronizer has three states: 683 * <ul> 684 * <li> UNLOCKED(0) - the synchronizer may be acquired shared or exclusively 685 * <li> LOCKED_EXCLUSIVELY(-1) - the synchronizer is held exclusively and cannot be acquired shared or 686 * exclusively. An exclusive lock is held while a heart beat is in progress 687 * <li> LOCKED_SHARED(>0) - the synchronizer is held shared and cannot be acquired exclusively. N shared locks 688 * are held while N Bind or StartTLS operations are in progress. 689 * </ul> 690 */ 691 private static final class Sync extends AbstractQueuedSynchronizer { 692 /** Lock states. Positive values indicate that the shared lock is taken. */ 693 private static final int LOCKED_EXCLUSIVELY = -1; 694 private static final int UNLOCKED = 0; // initial state 695 696 /** Keep compiler quiet. */ 697 private static final long serialVersionUID = -3590428415442668336L; 698 699 boolean isHeld() { 700 return getState() != 0; 701 } 702 703 void lockShared() { 704 acquireShared(1); 705 } 706 707 boolean tryLockExclusively() { 708 return tryAcquire(0 /* unused */); 709 } 710 711 boolean tryLockShared() { 712 return tryAcquireShared(1) > 0; 713 } 714 715 void unlockExclusively() { 716 release(0 /* unused */); 717 } 718 719 void unlockShared() { 720 releaseShared(0 /* unused */); 721 } 722 723 @Override 724 protected boolean isHeldExclusively() { 725 return getState() == LOCKED_EXCLUSIVELY; 726 } 727 728 @Override 729 protected boolean tryAcquire(final int ignored) { 730 if (compareAndSetState(UNLOCKED, LOCKED_EXCLUSIVELY)) { 731 setExclusiveOwnerThread(Thread.currentThread()); 732 return true; 733 } 734 return false; 735 } 736 737 @Override 738 protected int tryAcquireShared(final int readers) { 739 for (;;) { 740 final int state = getState(); 741 if (state == LOCKED_EXCLUSIVELY) { 742 return LOCKED_EXCLUSIVELY; // failed 743 } 744 final int newState = state + readers; 745 if (compareAndSetState(state, newState)) { 746 return newState; // succeeded + more readers allowed 747 } 748 } 749 } 750 751 @Override 752 protected boolean tryRelease(final int ignored) { 753 if (getState() != LOCKED_EXCLUSIVELY) { 754 throw new IllegalMonitorStateException(); 755 } 756 setExclusiveOwnerThread(null); 757 setState(UNLOCKED); 758 return true; 759 } 760 761 @Override 762 protected boolean tryReleaseShared(final int ignored) { 763 for (;;) { 764 final int state = getState(); 765 if (state == UNLOCKED || state == LOCKED_EXCLUSIVELY) { 766 throw new IllegalMonitorStateException(); 767 } 768 final int newState = state - 1; 769 if (compareAndSetState(state, newState)) { 770 /* 771 * We could always return true here, but since there cannot be waiting readers we can specialize 772 * for waiting writers. 773 */ 774 return newState == UNLOCKED; 775 } 776 } 777 } 778 779 } 780 781 /** A connection that sends heart beats and supports all operations. */ 782 private final class ConnectionImpl extends AbstractAsynchronousConnection implements ConnectionEventListener { 783 /** The wrapped connection. */ 784 private final LDAPConnectionImpl connectionImpl; 785 786 /** List of pending Bind or StartTLS requests which must be invoked once the current heart beat completes. */ 787 private final Queue<Runnable> pendingBindOrStartTLSRequests = new ConcurrentLinkedQueue<>(); 788 789 /** 790 * List of pending responses for all active operations. These will be signaled if no heart beat is detected 791 * within the permitted timeout period. 792 */ 793 private final Queue<LdapResultHandler<?>> pendingResults = new ConcurrentLinkedQueue<>(); 794 795 /** Internal connection state. */ 796 private final ConnectionState state = new ConnectionState(); 797 798 /** Coordinates heart-beats with Bind and StartTLS requests. */ 799 private final Sync sync = new Sync(); 800 801 /** Timestamp of last response received (any response, not just heart beats). */ 802 private volatile long lastResponseTimestamp = timeService.now(); 803 804 private ConnectionImpl(final LDAPConnectionImpl connectionImpl) { 805 this.connectionImpl = connectionImpl; 806 connectionImpl.addConnectionEventListener(this); 807 } 808 809 @Override 810 public LdapPromise<Void> abandonAsync(final AbandonRequest request) { 811 return connectionImpl.abandonAsync(request); 812 } 813 814 @Override 815 public LdapPromise<Result> addAsync( 816 final AddRequest request, final IntermediateResponseHandler intermediateResponseHandler) { 817 if (hasConnectionErrorOccurred()) { 818 return newConnectionErrorPromise(); 819 } 820 return timestampPromise(connectionImpl.addAsync(request, intermediateResponseHandler)); 821 } 822 823 @Override 824 public void addConnectionEventListener(final ConnectionEventListener listener) { 825 state.addConnectionEventListener(listener); 826 } 827 828 @Override 829 public LdapPromise<BindResult> bindAsync( 830 final BindRequest request, final IntermediateResponseHandler intermediateResponseHandler) { 831 if (hasConnectionErrorOccurred()) { 832 return newConnectionErrorPromise(); 833 } 834 if (sync.tryLockShared()) { 835 // Fast path 836 return timestampBindOrStartTLSPromise(connectionImpl.bindAsync(request, intermediateResponseHandler)); 837 } 838 return enqueueBindOrStartTLSPromise(new AsyncFunction<Void, BindResult, LdapException>() { 839 @Override 840 public Promise<BindResult, LdapException> apply(Void value) throws LdapException { 841 return timestampBindOrStartTLSPromise(connectionImpl.bindAsync(request, 842 intermediateResponseHandler)); 843 } 844 }); 845 } 846 847 @Override 848 public void close() { 849 handleConnectionClosed(); 850 connectionImpl.close(); 851 } 852 853 @Override 854 public void close(final UnbindRequest request, final String reason) { 855 handleConnectionClosed(); 856 connectionImpl.close(request, reason); 857 } 858 859 @Override 860 public LdapPromise<CompareResult> compareAsync( 861 final CompareRequest request, final IntermediateResponseHandler intermediateResponseHandler) { 862 if (hasConnectionErrorOccurred()) { 863 return newConnectionErrorPromise(); 864 } 865 return timestampPromise(connectionImpl.compareAsync(request, intermediateResponseHandler)); 866 } 867 868 @Override 869 public LdapPromise<Result> deleteAsync( 870 final DeleteRequest request, final IntermediateResponseHandler intermediateResponseHandler) { 871 if (hasConnectionErrorOccurred()) { 872 return newConnectionErrorPromise(); 873 } 874 return timestampPromise(connectionImpl.deleteAsync(request, intermediateResponseHandler)); 875 } 876 877 @Override 878 public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync( 879 final ExtendedRequest<R> request, final IntermediateResponseHandler intermediateResponseHandler) { 880 if (hasConnectionErrorOccurred()) { 881 return newConnectionErrorPromise(); 882 } 883 if (!isStartTLSRequest(request)) { 884 return timestampPromise(connectionImpl.extendedRequestAsync(request, intermediateResponseHandler)); 885 } 886 if (sync.tryLockShared()) { 887 // Fast path 888 return timestampBindOrStartTLSPromise( 889 connectionImpl.extendedRequestAsync(request, intermediateResponseHandler)); 890 } 891 return enqueueBindOrStartTLSPromise(new AsyncFunction<Void, R, LdapException>() { 892 @Override 893 public Promise<R, LdapException> apply(Void value) throws LdapException { 894 return timestampBindOrStartTLSPromise( 895 connectionImpl.extendedRequestAsync(request, intermediateResponseHandler)); 896 } 897 }); 898 } 899 900 @Override 901 public void handleConnectionClosed() { 902 if (state.notifyConnectionClosed()) { 903 failPendingResults(newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED, 904 HBCF_CONNECTION_CLOSED_BY_CLIENT.get())); 905 synchronized (validConnections) { 906 connectionImpl.removeConnectionEventListener(this); 907 validConnections.remove(this); 908 if (heartBeatEnabled && validConnections.isEmpty()) { 909 // This is the last active connection, so stop the heartbeat. 910 heartBeatFuture.cancel(false); 911 } 912 } 913 releaseScheduler(); 914 } 915 } 916 917 @Override 918 public void handleConnectionError(final boolean isDisconnectNotification, final LdapException error) { 919 if (state.notifyConnectionError(isDisconnectNotification, error)) { 920 failPendingResults(error); 921 } 922 } 923 924 @Override 925 public void handleUnsolicitedNotification(final ExtendedResult notification) { 926 timestamp(notification); 927 state.notifyUnsolicitedNotification(notification); 928 } 929 930 @Override 931 public boolean isClosed() { 932 return state.isClosed(); 933 } 934 935 @Override 936 public boolean isValid() { 937 return state.isValid() && connectionImpl.isValid(); 938 } 939 940 @Override 941 public LdapPromise<Result> modifyAsync( 942 final ModifyRequest request, final IntermediateResponseHandler intermediateResponseHandler) { 943 if (hasConnectionErrorOccurred()) { 944 return newConnectionErrorPromise(); 945 } 946 return timestampPromise(connectionImpl.modifyAsync(request, intermediateResponseHandler)); 947 } 948 949 @Override 950 public LdapPromise<Result> modifyDNAsync( 951 final ModifyDNRequest request, final IntermediateResponseHandler intermediateResponseHandler) { 952 if (hasConnectionErrorOccurred()) { 953 return newConnectionErrorPromise(); 954 } 955 return timestampPromise(connectionImpl.modifyDNAsync(request, intermediateResponseHandler)); 956 } 957 958 @Override 959 public void removeConnectionEventListener(final ConnectionEventListener listener) { 960 state.removeConnectionEventListener(listener); 961 } 962 963 @Override 964 public LdapPromise<Result> searchAsync( 965 final SearchRequest request, 966 final IntermediateResponseHandler intermediateResponseHandler, 967 final SearchResultHandler searchHandler) { 968 if (hasConnectionErrorOccurred()) { 969 return newConnectionErrorPromise(); 970 } 971 972 final AtomicBoolean searchDone = new AtomicBoolean(); 973 final SearchResultHandler entryHandler = new SearchResultHandler() { 974 @Override 975 public synchronized boolean handleEntry(SearchResultEntry entry) { 976 if (!searchDone.get()) { 977 timestamp(entry); 978 if (searchHandler != null) { 979 searchHandler.handleEntry(entry); 980 } 981 } 982 return true; 983 } 984 985 @Override 986 public synchronized boolean handleReference(SearchResultReference reference) { 987 if (!searchDone.get()) { 988 timestamp(reference); 989 if (searchHandler != null) { 990 searchHandler.handleReference(reference); 991 } 992 } 993 return true; 994 } 995 }; 996 return timestampPromise(connectionImpl.searchAsync(request, intermediateResponseHandler, entryHandler) 997 .thenOnResultOrException(new Runnable() { 998 @Override 999 public void run() { 1000 searchDone.getAndSet(true); 1001 } 1002 })); 1003 } 1004 1005 @Override 1006 public String toString() { 1007 return connectionImpl.toString(); 1008 } 1009 1010 private void checkForHeartBeat() { 1011 if (sync.isHeld()) { 1012 /* 1013 * A heart beat or bind/startTLS is still in progress, but it should have completed by now. Let's 1014 * avoid aggressively terminating the connection, because the heart beat may simply have been delayed 1015 * by a sudden surge of activity. Therefore, only flag the connection as failed if no activity has been 1016 * seen on the connection since the heart beat was sent. 1017 */ 1018 final long currentTimeMillis = timeService.now(); 1019 if (lastResponseTimestamp < (currentTimeMillis - heartBeatTimeoutMS)) { 1020 logger.warn(LocalizableMessage.raw("No heartbeat detected for connection '%s'", connectionImpl)); 1021 handleConnectionError(false, newHeartBeatTimeoutError()); 1022 } 1023 } 1024 } 1025 1026 private boolean hasConnectionErrorOccurred() { 1027 return state.getConnectionError() != null; 1028 } 1029 1030 private <R extends Result> LdapPromise<R> enqueueBindOrStartTLSPromise( 1031 AsyncFunction<Void, R, LdapException> doRequest) { 1032 /* 1033 * A heart beat must be in progress so create a runnable task which will be executed when the heart beat 1034 * completes. 1035 */ 1036 final LdapPromiseImpl<Void> promise = newLdapPromiseImpl(); 1037 final LdapPromise<R> result = promise.thenAsync(doRequest); 1038 1039 // Enqueue and flush if the heart beat has completed in the mean time. 1040 pendingBindOrStartTLSRequests.offer(new Runnable() { 1041 @Override 1042 public void run() { 1043 // FIXME: Handle cancel chaining. 1044 if (!result.isCancelled()) { 1045 sync.lockShared(); // Will not block. 1046 promise.handleResult(null); 1047 } 1048 } 1049 }); 1050 flushPendingBindOrStartTLSRequests(); 1051 return result; 1052 } 1053 1054 private void failPendingResults(final LdapException error) { 1055 // Peek instead of pool because notification is responsible for removing the element from the queue. 1056 LdapResultHandler<?> pendingResult; 1057 while ((pendingResult = pendingResults.peek()) != null) { 1058 pendingResult.handleException(error); 1059 } 1060 } 1061 1062 private void flushPendingBindOrStartTLSRequests() { 1063 if (!pendingBindOrStartTLSRequests.isEmpty()) { 1064 /* 1065 * The pending requests will acquire the shared lock, but we take it here anyway to ensure that 1066 * pending requests do not get blocked. 1067 */ 1068 if (sync.tryLockShared()) { 1069 try { 1070 Runnable pendingRequest; 1071 while ((pendingRequest = pendingBindOrStartTLSRequests.poll()) != null) { 1072 // Dispatch the waiting request. This will not block. 1073 pendingRequest.run(); 1074 } 1075 } finally { 1076 sync.unlockShared(); 1077 } 1078 } 1079 } 1080 } 1081 1082 private boolean isStartTLSRequest(final ExtendedRequest<?> request) { 1083 return request.getOID().equals(StartTLSExtendedRequest.OID); 1084 } 1085 1086 private <R> LdapPromise<R> newConnectionErrorPromise() { 1087 return newFailedLdapPromise(state.getConnectionError()); 1088 } 1089 1090 private void releaseBindOrStartTLSLock() { 1091 sync.unlockShared(); 1092 } 1093 1094 private void releaseHeartBeatLock() { 1095 sync.unlockExclusively(); 1096 flushPendingBindOrStartTLSRequests(); 1097 } 1098 1099 /** 1100 * Sends a heart beat on this connection if required to do so. 1101 * 1102 * @return {@code true} if a heart beat was sent, otherwise {@code false}. 1103 */ 1104 private boolean sendHeartBeat() { 1105 // Don't attempt to send a heart beat if the connection has already failed. 1106 if (!state.isValid()) { 1107 return false; 1108 } 1109 1110 // Only send the heart beat if the connection has been idle for some time. 1111 final long currentTimeMillis = timeService.now(); 1112 if (currentTimeMillis < (lastResponseTimestamp + heartBeatDelayMS)) { 1113 return false; 1114 } 1115 1116 /* Don't send a heart beat if there is already a heart beat, bind, or startTLS in progress. Note that the 1117 * bind/startTLS response will update the lastResponseTimestamp as if it were a heart beat. 1118 */ 1119 if (sync.tryLockExclusively()) { 1120 try { 1121 connectionImpl.searchAsync(heartBeatRequest, null, new SearchResultHandler() { 1122 @Override 1123 public boolean handleEntry(final SearchResultEntry entry) { 1124 timestamp(entry); 1125 return true; 1126 } 1127 1128 @Override 1129 public boolean handleReference(final SearchResultReference reference) { 1130 timestamp(reference); 1131 return true; 1132 } 1133 }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Result>() { 1134 @Override 1135 public void handleResult(Result result) { 1136 timestamp(result); 1137 releaseHeartBeatLock(); 1138 } 1139 }).thenOnException(new ExceptionHandler<LdapException>() { 1140 @Override 1141 public void handleException(LdapException exception) { 1142 /* 1143 * Connection failure will be handled by connection event listener. Ignore cancellation 1144 * errors since these indicate that the heart beat was aborted by a client-side close. 1145 */ 1146 if (!(exception instanceof CancelledResultException)) { 1147 /* 1148 * Log at debug level to avoid polluting the logs with benign password policy related 1149 * errors. See OPENDJ-1168 and OPENDJ-1167. 1150 */ 1151 logger.debug(LocalizableMessage.raw("Heartbeat failed for connection factory '%s'", 1152 LDAPConnectionFactory.this, 1153 exception)); 1154 timestamp(exception); 1155 } 1156 releaseHeartBeatLock(); 1157 } 1158 }); 1159 } catch (final IllegalStateException e) { 1160 /* 1161 * This may happen when we attempt to send the heart beat just after the connection is closed but 1162 * before we are notified. Release the lock because we're never going to get a response. 1163 */ 1164 releaseHeartBeatLock(); 1165 } 1166 } 1167 /* 1168 * Indicate that a the heartbeat should be checked even if a bind/startTLS is in progress, since these 1169 * operations will effectively act as the heartbeat. 1170 */ 1171 return true; 1172 } 1173 1174 private <R> R timestamp(final R response) { 1175 if (!(response instanceof ConnectionException)) { 1176 lastResponseTimestamp = timeService.now(); 1177 } 1178 return response; 1179 } 1180 1181 private <R extends Result> LdapPromise<R> timestampBindOrStartTLSPromise(LdapPromise<R> wrappedPromise) { 1182 return timestampPromise(wrappedPromise).thenOnResultOrException(new Runnable() { 1183 @Override 1184 public void run() { 1185 releaseBindOrStartTLSLock(); 1186 } 1187 }); 1188 } 1189 1190 private <R extends Result> LdapPromise<R> timestampPromise(LdapPromise<R> wrappedPromise) { 1191 final LdapPromiseImpl<R> outerPromise = new LdapPromiseImplWrapper<>(wrappedPromise); 1192 pendingResults.add(outerPromise); 1193 wrappedPromise.thenOnResult(new ResultHandler<R>() { 1194 @Override 1195 public void handleResult(R result) { 1196 outerPromise.handleResult(result); 1197 timestamp(result); 1198 } 1199 }).thenOnException(new ExceptionHandler<LdapException>() { 1200 @Override 1201 public void handleException(LdapException exception) { 1202 outerPromise.handleException(exception); 1203 timestamp(exception); 1204 } 1205 }); 1206 outerPromise.thenOnResultOrException(new Runnable() { 1207 @Override 1208 public void run() { 1209 pendingResults.remove(outerPromise); 1210 } 1211 }); 1212 if (hasConnectionErrorOccurred()) { 1213 outerPromise.handleException(state.getConnectionError()); 1214 } 1215 return outerPromise; 1216 } 1217 1218 private class LdapPromiseImplWrapper<R> extends LdapPromiseImpl<R> { 1219 protected LdapPromiseImplWrapper(final LdapPromise<R> wrappedPromise) { 1220 super(new PromiseImpl<R, LdapException>() { 1221 @Override 1222 protected LdapException tryCancel(boolean mayInterruptIfRunning) { 1223 /* 1224 * FIXME: if the inner cancel succeeds then this promise will be completed and we can never 1225 * indicate that this cancel request has succeeded. 1226 */ 1227 wrappedPromise.cancel(mayInterruptIfRunning); 1228 return null; 1229 } 1230 }, wrappedPromise.getRequestID()); 1231 } 1232 } 1233 } 1234}