001/*
002 * Copyright 2016-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2016-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) 2016-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.listener;
037
038
039
040import java.util.List;
041import java.util.concurrent.Semaphore;
042import java.util.concurrent.TimeUnit;
043
044import com.unboundid.ldap.protocol.AbandonRequestProtocolOp;
045import com.unboundid.ldap.protocol.AddRequestProtocolOp;
046import com.unboundid.ldap.protocol.AddResponseProtocolOp;
047import com.unboundid.ldap.protocol.BindRequestProtocolOp;
048import com.unboundid.ldap.protocol.BindResponseProtocolOp;
049import com.unboundid.ldap.protocol.CompareRequestProtocolOp;
050import com.unboundid.ldap.protocol.CompareResponseProtocolOp;
051import com.unboundid.ldap.protocol.DeleteRequestProtocolOp;
052import com.unboundid.ldap.protocol.DeleteResponseProtocolOp;
053import com.unboundid.ldap.protocol.ExtendedRequestProtocolOp;
054import com.unboundid.ldap.protocol.ExtendedResponseProtocolOp;
055import com.unboundid.ldap.protocol.LDAPMessage;
056import com.unboundid.ldap.protocol.ModifyRequestProtocolOp;
057import com.unboundid.ldap.protocol.ModifyResponseProtocolOp;
058import com.unboundid.ldap.protocol.ModifyDNRequestProtocolOp;
059import com.unboundid.ldap.protocol.ModifyDNResponseProtocolOp;
060import com.unboundid.ldap.protocol.SearchRequestProtocolOp;
061import com.unboundid.ldap.protocol.SearchResultDoneProtocolOp;
062import com.unboundid.ldap.sdk.Control;
063import com.unboundid.ldap.sdk.LDAPException;
064import com.unboundid.ldap.sdk.OperationType;
065import com.unboundid.ldap.sdk.ResultCode;
066import com.unboundid.util.Debug;
067import com.unboundid.util.NotMutable;
068import com.unboundid.util.NotNull;
069import com.unboundid.util.Nullable;
070import com.unboundid.util.StaticUtils;
071import com.unboundid.util.ThreadSafety;
072import com.unboundid.util.ThreadSafetyLevel;
073import com.unboundid.util.Validator;
074
075import static com.unboundid.ldap.listener.ListenerMessages.*;
076
077
078
079/**
080 * This class provides an implementation of an LDAP listener request handler
081 * that can be used to limit the number of requests that may be processed
082 * concurrently.  It uses one or more {@link Semaphore} instances to limit the
083 * number of requests that may be processed at any time, and provides the
084 * ability to impose limiting on a per-operation-type basis.
085 */
086@NotMutable()
087@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
088public final class ConcurrentRequestLimiterRequestHandler
089       extends LDAPListenerRequestHandler
090{
091  // The downstream request handler that will be used to process the requests
092  // after any appropriate concurrent request limiting has been performed.
093  @NotNull private final LDAPListenerRequestHandler downstreamRequestHandler;
094
095  // A timeout value (expressed in milliseconds) that will cause the operation
096  // to be rejected rather than processed if the associated semaphore cannot be
097  // acquired in this length of time.
098  private final long rejectTimeoutMillis;
099
100  // The semaphores that will be used for each type of operation.
101  @Nullable private final          Semaphore abandonSemaphore;
102  @Nullable private final Semaphore addSemaphore;
103  @Nullable private final Semaphore bindSemaphore;
104  @Nullable private final Semaphore compareSemaphore;
105  @Nullable private final Semaphore deleteSemaphore;
106  @Nullable private final Semaphore extendedSemaphore;
107  @Nullable private final Semaphore modifySemaphore;
108  @Nullable private final Semaphore modifyDNSemaphore;
109  @Nullable private final Semaphore searchSemaphore;
110
111
112
113  /**
114   * Creates a new concurrent request limiter request handler that will impose
115   * the specified limit on the number of operations that may be in progress at
116   * any time.  The limit will be enforced for all types of operations except
117   * abandon and unbind operations, which will not be limited.
118   *
119   * @param  downstreamRequestHandler  The downstream request handler that will
120   *                                   be used to actually process the requests
121   *                                   after any appropriate limiting has been
122   *                                   performed.
123   * @param  maxConcurrentRequests     The maximum number of requests that may
124   *                                   be processed at any given time.  This
125   *                                   limit will be enforced for all operation
126   *                                   types except abandon and unbind, which
127   *                                   will not be limited.
128   * @param  rejectTimeoutMillis       A timeout value (expressed in
129   *                                   milliseconds) that will cause a requested
130   *                                   operation to be rejected rather than
131   *                                   processed if the associate semaphore
132   *                                   cannot be acquired in this length of
133   *                                   time.  A value of zero indicates that the
134   *                                   operation should be rejected immediately
135   *                                   if the maximum number of concurrent
136   *                                   requests are already in progress.  A
137   *                                   value that is less than zero indicates
138   *                                   that no timeout should be imposed and
139   *                                   that requests should be forced to wait as
140   *                                   long as necessary until they can be
141   *                                   processed.
142   */
143  public ConcurrentRequestLimiterRequestHandler(
144       @NotNull final LDAPListenerRequestHandler downstreamRequestHandler,
145       final int maxConcurrentRequests, final long rejectTimeoutMillis)
146  {
147    this(downstreamRequestHandler, new Semaphore(maxConcurrentRequests),
148         rejectTimeoutMillis);
149  }
150
151
152
153  /**
154   * Creates a new concurrent request limiter request handler that will use the
155   * provided semaphore to limit on the number of operations that may be in
156   * progress at any time.  The limit will be enforced for all types of
157   * operations except abandon and unbind operations, which will not be limited.
158   *
159   * @param  downstreamRequestHandler  The downstream request handler that will
160   *                                   be used to actually process the requests
161   *                                   after any appropriate limiting has been
162   *                                   performed.
163   * @param  semaphore                 The semaphore that will be used to limit
164   *                                   the number of concurrent operations in
165   *                                   progress, for all operation types except
166   *                                   abandon and unbind.
167   * @param  rejectTimeoutMillis       A timeout value (expressed in
168   *                                   milliseconds) that will cause a requested
169   *                                   operation to be rejected rather than
170   *                                   processed if the associate semaphore
171   *                                   cannot be acquired in this length of
172   *                                   time.  A value of zero indicates that the
173   *                                   operation should be rejected immediately
174   *                                   if the maximum number of concurrent
175   *                                   requests are already in progress.  A
176   *                                   value that is less than zero indicates
177   *                                   that no timeout should be imposed and
178   *                                   that requests should be forced to wait as
179   *                                   long as necessary until they can be
180   *                                   processed.
181   */
182  public ConcurrentRequestLimiterRequestHandler(
183       @NotNull final LDAPListenerRequestHandler downstreamRequestHandler,
184       @NotNull final Semaphore semaphore, final long rejectTimeoutMillis)
185  {
186    this(downstreamRequestHandler, null, semaphore, semaphore, semaphore,
187         semaphore, semaphore, semaphore, semaphore, semaphore,
188         rejectTimeoutMillis);
189  }
190
191
192
193  /**
194   * Creates a new concurrent request limiter request handler that can use the
195   * provided semaphore instances to limit the number of operations in progress
196   * concurrently for each type of operation.  The same semaphore instance can
197   * be provided for multiple operation types if performance for those
198   * operations should be limited in aggregate rather than individually (e.g.,
199   * if you don't want the total combined number of search and modify operations
200   * in progress at any time to exceed a given threshold, then you could provide
201   * the same semaphore instance for the {@code modifySemaphore} and
202   * {@code searchSemaphore} arguments).
203   *
204   * @param  downstreamRequestHandler  The downstream request handler that will
205   *                                   be used to actually process the requests
206   *                                   after any appropriate rate limiting has
207   *                                   been performed.  It must not be
208   *                                   {@code null}.
209   * @param  abandonSemaphore          The semaphore to use when processing
210   *                                   abandon operations.  It may be
211   *                                   {@code null} if no concurrent request
212   *                                   limiting should be performed for abandon
213   *                                   operations.
214   * @param  addSemaphore              The semaphore to use when processing add
215   *                                   operations.  It may be {@code null} if no
216   *                                   concurrent request limiting should be
217   *                                   performed for add operations.
218   * @param  bindSemaphore             The semaphore to use when processing
219   *                                   bind operations.  It may be
220   *                                   {@code null} if no concurrent request
221   *                                   limiting should be performed for bind
222   *                                   operations.
223   * @param  compareSemaphore          The semaphore to use when processing
224   *                                   compare operations.  It may be
225   *                                   {@code null} if no concurrent request
226   *                                   limiting should be performed for compare
227   *                                   operations.
228   * @param  deleteSemaphore           The semaphore to use when processing
229   *                                   delete operations.  It may be
230   *                                   {@code null} if no concurrent request
231   *                                   limiting should be performed for delete
232   *                                   operations.
233   * @param  extendedSemaphore         The semaphore to use when processing
234   *                                   extended operations.  It may be
235   *                                   {@code null} if no concurrent request
236   *                                   limiting should be performed for extended
237   *                                   operations.
238   * @param  modifySemaphore           The semaphore to use when processing
239   *                                   modify operations.  It may be
240   *                                   {@code null} if no concurrent request
241   *                                   limiting should be performed for modify
242   *                                   operations.
243   * @param  modifyDNSemaphore         The semaphore to use when processing
244   *                                   modify DN operations.  It may be
245   *                                   {@code null} if no concurrent request
246   *                                   limiting should be performed for modify
247   *                                   DN operations.
248   * @param  searchSemaphore           The semaphore to use when processing
249   *                                   search operations.  It may be
250   *                                   {@code null} if no concurrent request
251   *                                   limiting should be performed for search
252   *                                   operations.
253   * @param  rejectTimeoutMillis       A timeout value (expressed in
254   *                                   milliseconds) that will cause a requested
255   *                                   operation to be rejected rather than
256   *                                   processed if the associate semaphore
257   *                                   cannot be acquired in this length of
258   *                                   time.  A value of zero indicates that the
259   *                                   operation should be rejected immediately
260   *                                   if the maximum number of concurrent
261   *                                   requests are already in progress.  A
262   *                                   value that is less than zero indicates
263   *                                   that no timeout should be imposed and
264   *                                   that requests should be forced to wait as
265   *                                   long as necessary until they can be
266   *                                   processed.
267   */
268  public ConcurrentRequestLimiterRequestHandler(
269       @NotNull final LDAPListenerRequestHandler downstreamRequestHandler,
270       @Nullable final Semaphore abandonSemaphore,
271       @Nullable final Semaphore addSemaphore,
272       @Nullable final Semaphore bindSemaphore,
273       @Nullable final Semaphore compareSemaphore,
274       @Nullable final Semaphore deleteSemaphore,
275       @Nullable final Semaphore extendedSemaphore,
276       @Nullable final Semaphore modifySemaphore,
277       @Nullable final Semaphore modifyDNSemaphore,
278       @Nullable final Semaphore searchSemaphore,
279       final long rejectTimeoutMillis)
280  {
281    Validator.ensureNotNull(downstreamRequestHandler);
282
283    this.downstreamRequestHandler = downstreamRequestHandler;
284    this.abandonSemaphore         = abandonSemaphore;
285    this.addSemaphore             = addSemaphore;
286    this.bindSemaphore            = bindSemaphore;
287    this.compareSemaphore         = compareSemaphore;
288    this.deleteSemaphore          = deleteSemaphore;
289    this.extendedSemaphore        = extendedSemaphore;
290    this.modifySemaphore          = modifySemaphore;
291    this.modifyDNSemaphore        = modifyDNSemaphore;
292    this.searchSemaphore          = searchSemaphore;
293
294    if (rejectTimeoutMillis >= 0L)
295    {
296      this.rejectTimeoutMillis = rejectTimeoutMillis;
297    }
298    else
299    {
300      this.rejectTimeoutMillis = (long) Integer.MAX_VALUE;
301    }
302  }
303
304
305
306  /**
307   * {@inheritDoc}
308   */
309  @Override()
310  @NotNull()
311  public ConcurrentRequestLimiterRequestHandler newInstance(
312              @NotNull final LDAPListenerClientConnection connection)
313         throws LDAPException
314  {
315    return new ConcurrentRequestLimiterRequestHandler(
316         downstreamRequestHandler.newInstance(connection), abandonSemaphore,
317         addSemaphore, bindSemaphore, compareSemaphore, deleteSemaphore,
318         extendedSemaphore, modifySemaphore, modifyDNSemaphore,
319         searchSemaphore, rejectTimeoutMillis);
320  }
321
322
323
324  /**
325   * {@inheritDoc}
326   */
327  @Override()
328  public void processAbandonRequest(final int messageID,
329                   @NotNull final AbandonRequestProtocolOp request,
330                   @NotNull final List<Control> controls)
331  {
332    try
333    {
334      acquirePermit(abandonSemaphore, OperationType.ABANDON);
335    }
336    catch (final LDAPException le)
337    {
338      Debug.debugException(le);
339      return;
340    }
341
342    try
343    {
344      downstreamRequestHandler.processAbandonRequest(messageID, request,
345           controls);
346    }
347    finally
348    {
349      releasePermit(abandonSemaphore);
350    }
351  }
352
353
354
355  /**
356   * {@inheritDoc}
357   */
358  @Override()
359  @NotNull()
360  public LDAPMessage processAddRequest(final int messageID,
361                          @NotNull final AddRequestProtocolOp request,
362                          @NotNull final List<Control> controls)
363  {
364    try
365    {
366      acquirePermit(addSemaphore, OperationType.ADD);
367    }
368    catch (final LDAPException le)
369    {
370      Debug.debugException(le);
371      return new LDAPMessage(messageID,
372           new AddResponseProtocolOp(le.toLDAPResult()));
373    }
374
375    try
376    {
377      return downstreamRequestHandler.processAddRequest(messageID, request,
378           controls);
379    }
380    finally
381    {
382      releasePermit(addSemaphore);
383    }
384  }
385
386
387
388  /**
389   * {@inheritDoc}
390   */
391  @Override()
392  @NotNull()
393  public LDAPMessage processBindRequest(final int messageID,
394                          @NotNull final BindRequestProtocolOp request,
395                          @NotNull final List<Control> controls)
396  {
397    try
398    {
399      acquirePermit(bindSemaphore, OperationType.BIND);
400    }
401    catch (final LDAPException le)
402    {
403      Debug.debugException(le);
404      return new LDAPMessage(messageID,
405           new BindResponseProtocolOp(le.toLDAPResult()));
406    }
407
408    try
409    {
410      return downstreamRequestHandler.processBindRequest(messageID, request,
411           controls);
412    }
413    finally
414    {
415      releasePermit(bindSemaphore);
416    }
417  }
418
419
420
421  /**
422   * {@inheritDoc}
423   */
424  @Override()
425  @NotNull()
426  public LDAPMessage processCompareRequest(final int messageID,
427                          @NotNull final CompareRequestProtocolOp request,
428                          @NotNull final List<Control> controls)
429  {
430    try
431    {
432      acquirePermit(compareSemaphore, OperationType.COMPARE);
433    }
434    catch (final LDAPException le)
435    {
436      Debug.debugException(le);
437      return new LDAPMessage(messageID,
438           new CompareResponseProtocolOp(le.toLDAPResult()));
439    }
440
441    try
442    {
443      return downstreamRequestHandler.processCompareRequest(messageID, request,
444           controls);
445    }
446    finally
447    {
448      releasePermit(compareSemaphore);
449    }
450  }
451
452
453
454  /**
455   * {@inheritDoc}
456   */
457  @Override()
458  @NotNull()
459  public LDAPMessage processDeleteRequest(final int messageID,
460                          @NotNull final DeleteRequestProtocolOp request,
461                          @NotNull final List<Control> controls)
462  {
463    try
464    {
465      acquirePermit(deleteSemaphore, OperationType.DELETE);
466    }
467    catch (final LDAPException le)
468    {
469      Debug.debugException(le);
470      return new LDAPMessage(messageID,
471           new DeleteResponseProtocolOp(le.toLDAPResult()));
472    }
473
474    try
475    {
476      return downstreamRequestHandler.processDeleteRequest(messageID, request,
477           controls);
478    }
479    finally
480    {
481      releasePermit(deleteSemaphore);
482    }
483  }
484
485
486
487  /**
488   * {@inheritDoc}
489   */
490  @Override()
491  @NotNull()
492  public LDAPMessage processExtendedRequest(final int messageID,
493                          @NotNull final ExtendedRequestProtocolOp request,
494                          @NotNull final List<Control> controls)
495  {
496    try
497    {
498      acquirePermit(extendedSemaphore, OperationType.EXTENDED);
499    }
500    catch (final LDAPException le)
501    {
502      Debug.debugException(le);
503      return new LDAPMessage(messageID,
504           new ExtendedResponseProtocolOp(le.toLDAPResult()));
505    }
506
507    try
508    {
509      return downstreamRequestHandler.processExtendedRequest(messageID, request,
510           controls);
511    }
512    finally
513    {
514      releasePermit(extendedSemaphore);
515    }
516  }
517
518
519
520  /**
521   * {@inheritDoc}
522   */
523  @Override()
524  @NotNull()
525  public LDAPMessage processModifyRequest(final int messageID,
526                          @NotNull final ModifyRequestProtocolOp request,
527                          @NotNull final List<Control> controls)
528  {
529    try
530    {
531      acquirePermit(modifySemaphore, OperationType.MODIFY);
532    }
533    catch (final LDAPException le)
534    {
535      Debug.debugException(le);
536      return new LDAPMessage(messageID,
537           new ModifyResponseProtocolOp(le.toLDAPResult()));
538    }
539
540    try
541    {
542      return downstreamRequestHandler.processModifyRequest(messageID, request,
543           controls);
544    }
545    finally
546    {
547      releasePermit(modifySemaphore);
548    }
549  }
550
551
552
553  /**
554   * {@inheritDoc}
555   */
556  @Override()
557  @NotNull()
558  public LDAPMessage processModifyDNRequest(final int messageID,
559                          @NotNull final ModifyDNRequestProtocolOp request,
560                          @NotNull final List<Control> controls)
561  {
562    try
563    {
564      acquirePermit(modifyDNSemaphore, OperationType.MODIFY_DN);
565    }
566    catch (final LDAPException le)
567    {
568      Debug.debugException(le);
569      return new LDAPMessage(messageID,
570           new ModifyDNResponseProtocolOp(le.toLDAPResult()));
571    }
572
573    try
574    {
575      return downstreamRequestHandler.processModifyDNRequest(messageID, request,
576           controls);
577    }
578    finally
579    {
580      releasePermit(modifyDNSemaphore);
581    }
582  }
583
584
585
586  /**
587   * {@inheritDoc}
588   */
589  @Override()
590  @NotNull()
591  public LDAPMessage processSearchRequest(final int messageID,
592                          @NotNull final SearchRequestProtocolOp request,
593                          @NotNull final List<Control> controls)
594  {
595    try
596    {
597      acquirePermit(searchSemaphore, OperationType.SEARCH);
598    }
599    catch (final LDAPException le)
600    {
601      Debug.debugException(le);
602      return new LDAPMessage(messageID,
603           new SearchResultDoneProtocolOp(le.toLDAPResult()));
604    }
605
606    try
607    {
608      return downstreamRequestHandler.processSearchRequest(messageID, request,
609           controls);
610    }
611    finally
612    {
613      releasePermit(searchSemaphore);
614    }
615  }
616
617
618
619  /**
620   * Acquires a permit from the provided semaphore.
621   *
622   * @param  semaphore      The semaphore from which to acquire a permit.  It
623   *                        may be {@code null} if no semaphore is needed for
624   *                        the associated operation type.
625   * @param  operationType  The type of operation
626   *
627   * @throws  LDAPException  If it was not possible to acquire a permit from the
628   *                         provided semaphore.
629   */
630  private void acquirePermit(@NotNull final Semaphore semaphore,
631                             @NotNull final OperationType operationType)
632          throws LDAPException
633  {
634    if (semaphore == null)
635    {
636      return;
637    }
638
639    try
640    {
641      if (rejectTimeoutMillis == 0L)
642      {
643        if (! semaphore.tryAcquire())
644        {
645          throw new LDAPException(ResultCode.BUSY,
646               ERR_CONCURRENT_LIMITER_REQUEST_HANDLER_NO_TIMEOUT.get(
647                    operationType.name()));
648        }
649      }
650      else
651      {
652        if (! semaphore.tryAcquire(rejectTimeoutMillis, TimeUnit.MILLISECONDS))
653        {
654          throw new LDAPException(ResultCode.BUSY,
655               ERR_CONCURRENT_LIMITER_REQUEST_HANDLER_TIMEOUT.get(
656                    operationType.name(), rejectTimeoutMillis));
657        }
658      }
659    }
660    catch (final LDAPException le)
661    {
662      throw le;
663    }
664    catch (final Exception e)
665    {
666      Debug.debugException(e);
667      throw new LDAPException(ResultCode.OTHER,
668           ERR_CONCURRENT_LIMITER_REQUEST_HANDLER_SEMAPHORE_EXCEPTION.get(
669                operationType.name(), StaticUtils.getExceptionMessage(e)),
670           e);
671    }
672  }
673
674
675
676  /**
677   * Releases a permit back to the provided semaphore.
678   *
679   * @param  semaphore  The semaphore to which the permit should be released.
680   *                    It may be {@code null} if no semaphore is needed for the
681   *                    associated operation type.
682   */
683  private static void releasePermit(@NotNull final Semaphore semaphore)
684  {
685    if (semaphore != null)
686    {
687      semaphore.release();
688    }
689  }
690}