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