001/*
002 * Copyright 2008-2020 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2008-2020 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2008-2020 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk;
037
038
039
040import java.io.Serializable;
041import java.util.concurrent.ArrayBlockingQueue;
042import java.util.concurrent.Future;
043import java.util.concurrent.TimeoutException;
044import java.util.concurrent.TimeUnit;
045import java.util.concurrent.atomic.AtomicBoolean;
046import java.util.concurrent.atomic.AtomicReference;
047
048import com.unboundid.util.Debug;
049import com.unboundid.util.NotMutable;
050import com.unboundid.util.NotNull;
051import com.unboundid.util.Nullable;
052import com.unboundid.util.StaticUtils;
053import com.unboundid.util.ThreadSafety;
054import com.unboundid.util.ThreadSafetyLevel;
055
056import static com.unboundid.ldap.sdk.LDAPMessages.*;
057
058
059
060/**
061 * This class defines an object that provides information about a request that
062 * was initiated asynchronously.  It may be used to abandon or cancel the
063 * associated request.  This class also implements the
064 * {@code java.util.concurrent.Future} interface, so it may be used in that
065 * manner.
066 * <BR><BR>
067 * <H2>Example</H2>
068 * The following example initiates an asynchronous modify operation and then
069 * attempts to abandon it:
070 * <PRE>
071 * Modification mod = new Modification(ModificationType.REPLACE,
072 *      "description", "This is the new description.");
073 * ModifyRequest modifyRequest =
074 *      new ModifyRequest("dc=example,dc=com", mod);
075 *
076 * AsyncRequestID asyncRequestID =
077 *      connection.asyncModify(modifyRequest, myAsyncResultListener);
078 *
079 * // Assume that we've waited a reasonable amount of time but the modify
080 * // hasn't completed yet so we'll try to abandon it.
081 *
082 * connection.abandon(asyncRequestID);
083 * </PRE>
084 */
085@NotMutable()
086@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
087public final class AsyncRequestID
088       implements Serializable, Future<LDAPResult>
089{
090  /**
091   * The serial version UID for this serializable class.
092   */
093  private static final long serialVersionUID = 8244005138437962030L;
094
095
096
097  // The queue used to receive the result for the associated operation.
098  @NotNull private final ArrayBlockingQueue<LDAPResult> resultQueue;
099
100  // A flag indicating whether a request has been made to cancel the operation.
101  @NotNull private final AtomicBoolean cancelRequested;
102
103  // The result for the associated operation.
104  @NotNull private final AtomicReference<LDAPResult> result;
105
106  // The message ID for the request message.
107  private final int messageID;
108
109  // The connection used to process the asynchronous operation.
110  @NotNull private final LDAPConnection connection;
111
112  // The timer task that will allow the associated request to be cancelled.
113  @Nullable private volatile AsyncTimeoutTimerTask timerTask;
114
115
116
117  /**
118   * Creates a new async request ID with the provided message ID.
119   *
120   * @param  messageID   The message ID for the associated request.
121   * @param  connection  The connection used to process the asynchronous
122   *                     operation.
123   */
124  AsyncRequestID(final int messageID, @NotNull final LDAPConnection connection)
125  {
126    this.messageID  = messageID;
127    this.connection = connection;
128
129    resultQueue     = new ArrayBlockingQueue<>(1);
130    cancelRequested = new AtomicBoolean(false);
131    result          = new AtomicReference<>();
132    timerTask       = null;
133  }
134
135
136
137  /**
138   * Retrieves the message ID for the associated request.
139   *
140   * @return  The message ID for the associated request.
141   */
142  public int getMessageID()
143  {
144    return messageID;
145  }
146
147
148
149  /**
150   * Attempts to cancel the associated asynchronous operation operation.  This
151   * will cause an abandon request to be sent to the server for the associated
152   * request, but because there is no response to an abandon operation then
153   * there is no way that we can determine whether the operation was actually
154   * abandoned.
155   *
156   * @param  mayInterruptIfRunning  Indicates whether to interrupt the thread
157   *                                running the associated task.  This will be
158   *                                ignored.
159   *
160   * @return  {@code true} if an abandon request was sent to cancel the
161   *          associated operation, or {@code false} if it was not possible to
162   *          send an abandon request because the operation has already
163   *          completed, because an abandon request has already been sent, or
164   *          because an error occurred while trying to send the cancel request.
165   */
166  @Override()
167  public boolean cancel(final boolean mayInterruptIfRunning)
168  {
169    // If the operation has already completed, then we can't cancel it.
170    if (isDone())
171    {
172      return false;
173    }
174
175    // Try to send a request to cancel the operation.
176    try
177    {
178      cancelRequested.set(true);
179      result.compareAndSet(null,
180           new LDAPResult(messageID, ResultCode.USER_CANCELED,
181                INFO_ASYNC_REQUEST_USER_CANCELED.get(), null,
182                StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS));
183
184      connection.abandon(this);
185    }
186    catch (final Exception e)
187    {
188      Debug.debugException(e);
189    }
190
191    return true;
192  }
193
194
195
196  /**
197   * Indicates whether an attempt has been made to cancel the associated
198   * operation before it completed.
199   *
200   * @return  {@code true} if an attempt was made to cancel the operation, or
201   *          {@code false} if no cancel attempt was made, or if the operation
202   *          completed before it could be canceled.
203   */
204  @Override()
205  public boolean isCancelled()
206  {
207    return cancelRequested.get();
208  }
209
210
211
212  /**
213   * Indicates whether the associated operation has completed, regardless of
214   * whether it completed normally, completed with an error, or was canceled
215   * before starting.
216   *
217   * @return  {@code true} if the associated operation has completed, or if an
218   *          attempt has been made to cancel it, or {@code false} if the
219   *          operation has not yet completed and no cancel attempt has been
220   *          made.
221   */
222  @Override()
223  public boolean isDone()
224  {
225    if (cancelRequested.get())
226    {
227      return true;
228    }
229
230    if (result.get() != null)
231    {
232      return true;
233    }
234
235    final LDAPResult newResult = resultQueue.poll();
236    if (newResult != null)
237    {
238      result.set(newResult);
239      return true;
240    }
241
242    return false;
243  }
244
245
246
247  /**
248   * Attempts to get the result for the associated operation, waiting if
249   * necessary for it to complete.  Note that this method will differ from the
250   * behavior defined in the {@code java.util.concurrent.Future} API in that it
251   * will not wait forever.  Rather, it will wait for no more than the length of
252   * time specified as the maximum response time defined in the connection
253   * options for the connection used to send the asynchronous request.  This is
254   * necessary because the operation may have been abandoned or otherwise
255   * interrupted, or the associated connection may have become invalidated, in
256   * a way that the LDAP SDK cannot detect.
257   *
258   * @return  The result for the associated operation.  If the operation has
259   *          been canceled, or if no result has been received within the
260   *          response timeout period, then a generated response will be
261   *          returned.
262   *
263   * @throws  InterruptedException  If the thread calling this method was
264   *                                interrupted before a result was received.
265   */
266  @Override()
267  @NotNull()
268  public LDAPResult get()
269         throws InterruptedException
270  {
271    final long maxWaitTime =
272         connection.getConnectionOptions().getResponseTimeoutMillis();
273
274    try
275    {
276      return get(maxWaitTime, TimeUnit.MILLISECONDS);
277    }
278    catch (final TimeoutException te)
279    {
280      Debug.debugException(te);
281      return new LDAPResult(messageID, ResultCode.TIMEOUT, te.getMessage(),
282           null, StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS);
283    }
284  }
285
286
287
288  /**
289   * Attempts to get the result for the associated operation, waiting if
290   * necessary for up to the specified length of time for the operation to
291   * complete.
292   *
293   * @param  timeout   The maximum length of time to wait for the response.
294   * @param  timeUnit  The time unit for the provided {@code timeout} value.
295   *
296   * @return  The result for the associated operation.  If the operation has
297   *          been canceled, then a generated response will be returned.
298   *
299   * @throws  InterruptedException  If the thread calling this method was
300   *                                interrupted before a result was received.
301   *
302   * @throws  TimeoutException  If a timeout was encountered before the result
303   *                            could be obtained.
304   */
305  @Override()
306  @NotNull()
307  public LDAPResult get(final long timeout, @NotNull final TimeUnit timeUnit)
308         throws InterruptedException, TimeoutException
309  {
310    final LDAPResult newResult = resultQueue.poll();
311    if (newResult != null)
312    {
313      result.set(newResult);
314      return newResult;
315    }
316
317    final LDAPResult previousResult = result.get();
318    if (previousResult != null)
319    {
320      return previousResult;
321    }
322
323    final LDAPResult resultAfterWaiting = resultQueue.poll(timeout, timeUnit);
324    if (resultAfterWaiting == null)
325    {
326      final long timeoutMillis = timeUnit.toMillis(timeout);
327      throw new TimeoutException(
328           WARN_ASYNC_REQUEST_GET_TIMEOUT.get(timeoutMillis));
329    }
330    else
331    {
332      result.set(resultAfterWaiting);
333      return resultAfterWaiting;
334    }
335  }
336
337
338
339  /**
340   * Sets the timer task that may be used to cancel this result after a period
341   * of time.
342   *
343   * @param  timerTask  The timer task that may be used to cancel this result
344   *                    after a period of time.  It may be {@code null} if no
345   *                    timer task should be used.
346   */
347  void setTimerTask(@Nullable final AsyncTimeoutTimerTask timerTask)
348  {
349    this.timerTask = timerTask;
350  }
351
352
353
354  /**
355   * Sets the result for the associated operation.
356   *
357   * @param  result  The result for the associated operation.  It must not be
358   *                 {@code null}.
359   */
360  void setResult(@NotNull final LDAPResult result)
361  {
362    resultQueue.offer(result);
363
364    final AsyncTimeoutTimerTask t = timerTask;
365    if (t != null)
366    {
367      t.cancel();
368      connection.getTimer().purge();
369      timerTask = null;
370    }
371  }
372
373
374
375  /**
376   * Retrieves a hash code for this async request ID.
377   *
378   * @return  A hash code for this async request ID.
379   */
380  @Override()
381  public int hashCode()
382  {
383    return messageID;
384  }
385
386
387
388  /**
389   * Indicates whether the provided object is equal to this async request ID.
390   *
391   * @param  o  The object for which to make the determination.
392   *
393   * @return  {@code true} if the provided object is equal to this async request
394   *          ID, or {@code false} if not.
395   */
396  @Override()
397  public boolean equals(@Nullable final Object o)
398  {
399    if (o == null)
400    {
401      return false;
402    }
403
404    if (o == this)
405    {
406      return true;
407    }
408
409    if (o instanceof AsyncRequestID)
410    {
411      return (((AsyncRequestID) o).messageID == messageID);
412    }
413    else
414    {
415      return false;
416    }
417  }
418
419
420
421  /**
422   * Retrieves a string representation of this async request ID.
423   *
424   * @return  A string representation of this async request ID.
425   */
426  @Override()
427  @NotNull()
428  public String toString()
429  {
430    return "AsyncRequestID(messageID=" + messageID + ')';
431  }
432}