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