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}