All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.pulsar.common.util.RateLimiter Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.pulsar.common.util;

import static org.apache.pulsar.shade.com.google.common.base.Preconditions.checkArgument;
import static org.apache.pulsar.common.util.Runnables.catchingAndLoggingThrowables;
import org.apache.pulsar.shade.com.google.common.base.MoreObjects;
import org.apache.pulsar.shade.io.netty.util.concurrent.DefaultThreadFactory;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;

/**
 * A Rate Limiter that distributes permits at a configurable rate. Each {@link #acquire()} blocks if necessary until a
 * permit is available, and then takes it. Each {@link #tryAcquire()} tries to acquire permits from available permits,
 * it returns true if it succeed else returns false. Rate limiter release configured permits at every configured rate
 * time, so, on next ticket new fresh permits will be available.
 *
 * 

For example: if RateLimiter is configured to release 10 permits at every 1 second then RateLimiter will allow to * acquire 10 permits at any time with in that 1 second. * *

Comparison with other RateLimiter such as {@link org.apache.pulsar.shade.com.google.common.util.concurrent.RateLimiter} *

    *
  • Per second rate-limiting: Per second rate-limiting not satisfied by Guava-RateLimiter
  • *
  • Guava RateLimiter: For X permits: it releases X/1000 permits every msec. therefore, * for permits=2/sec => it release 1st permit on first 500msec and 2nd permit on next 500ms. therefore, * if 2 request comes with in 500msec duration then 2nd request fails to acquire permit * though we have configured 2 permits/second.
  • *
  • RateLimiter: it releases X permits every second. so, in above usecase: * if 2 requests comes at the same time then both will acquire the permit.
  • *
  • Faster: RateLimiter is light-weight and faster than Guava-RateLimiter
  • *
*/ @Slf4j public class RateLimiter implements AutoCloseable{ private final ScheduledExecutorService executorService; private long rateTime; private TimeUnit timeUnit; private final boolean externalExecutor; private ScheduledFuture renewTask; private volatile long permits; private volatile long acquiredPermits; private boolean isClosed; // permitUpdate helps to update permit-rate at runtime private Supplier permitUpdater; private RateLimitFunction rateLimitFunction; private boolean isDispatchOrPrecisePublishRateLimiter; @Builder RateLimiter(final ScheduledExecutorService scheduledExecutorService, final long permits, final long rateTime, final TimeUnit timeUnit, Supplier permitUpdater, boolean isDispatchOrPrecisePublishRateLimiter, RateLimitFunction rateLimitFunction) { checkArgument(permits > 0, "rate must be > 0"); checkArgument(rateTime > 0, "Renew permit time must be > 0"); this.rateTime = rateTime; this.timeUnit = timeUnit; this.permits = permits; this.permitUpdater = permitUpdater; this.isDispatchOrPrecisePublishRateLimiter = isDispatchOrPrecisePublishRateLimiter; if (scheduledExecutorService != null) { this.executorService = scheduledExecutorService; this.externalExecutor = true; } else { final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, new DefaultThreadFactory("pulsar-rate-limiter")); executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); this.executorService = executor; this.externalExecutor = false; } this.rateLimitFunction = rateLimitFunction; } // default values for Lombok generated builder class public static class RateLimiterBuilder { private long rateTime = 1; private TimeUnit timeUnit = TimeUnit.SECONDS; } @Override public synchronized void close() { if (!isClosed) { if (!externalExecutor) { executorService.shutdownNow(); } if (renewTask != null) { renewTask.cancel(false); } isClosed = true; // If there is a ratelimit function registered, invoke it to unblock. if (rateLimitFunction != null) { rateLimitFunction.apply(); } } } public synchronized boolean isClosed() { return isClosed; } /** * Acquires the given number of permits from this {@code RateLimiter}, blocking until the request be granted. * *

This method is equivalent to {@code acquire(1)}. */ public synchronized void acquire() throws InterruptedException { acquire(1); } /** * Acquires the given number of permits from this {@code RateLimiter}, blocking until the request be granted. * * @param acquirePermit * the number of permits to acquire */ public synchronized void acquire(long acquirePermit) throws InterruptedException { checkArgument(!isClosed(), "Rate limiter is already shutdown"); checkArgument(acquirePermit <= this.permits, "acquiring permits must be less or equal than initialized rate =" + this.permits); // lazy init and start task only once application start using it if (renewTask == null) { renewTask = createTask(); } boolean canAcquire = false; do { canAcquire = acquirePermit < 0 || acquiredPermits < this.permits; if (!canAcquire) { wait(); } else { acquiredPermits += acquirePermit; } } while (!canAcquire); } /** * Acquires permits from this {@link RateLimiter} if it can be acquired immediately without delay. * *

This method is equivalent to {@code tryAcquire(1)}. * * @return {@code true} if the permits were acquired, {@code false} otherwise */ public synchronized boolean tryAcquire() { return tryAcquire(1); } /** * Acquires permits from this {@link RateLimiter} if it can be acquired immediately without delay. * * @param acquirePermit * the number of permits to acquire * @return {@code true} if the permits were acquired, {@code false} otherwise */ public synchronized boolean tryAcquire(long acquirePermit) { if (isClosed()) { log.info("The current rate limiter is already shutdown, acquire permits directly."); return true; } // lazy init and start task only once application start using it if (renewTask == null) { renewTask = createTask(); } boolean canAcquire = acquirePermit < 0 || acquiredPermits < this.permits; if (isDispatchOrPrecisePublishRateLimiter) { // for dispatch rate limiter just add acquirePermit acquiredPermits += acquirePermit; // we want to back-pressure from the current state of the rateLimiter therefore we should check if there // are any available premits again canAcquire = acquirePermit < 0 || acquiredPermits < this.permits; } else { // acquired-permits can't be larger than the rate if (acquirePermit + acquiredPermits > this.permits) { return false; } if (canAcquire) { acquiredPermits += acquirePermit; } } return canAcquire; } /** * Return available permits for this {@link RateLimiter}. * * @return returns 0 if permits is not available */ public long getAvailablePermits() { return Math.max(0, this.permits - this.acquiredPermits); } /** * Resets new rate by configuring new value for permits per configured rate-period. * * @param permits */ public synchronized void setRate(long permits) { this.permits = permits; } /** * Resets new rate with new permits and rate-time. * * @param permits * @param rateTime * @param timeUnit * @param permitUpdaterByte */ public synchronized void setRate(long permits, long rateTime, TimeUnit timeUnit, Supplier permitUpdaterByte) { if (renewTask != null) { renewTask.cancel(false); } this.permits = permits; this.rateTime = rateTime; this.timeUnit = timeUnit; this.permitUpdater = permitUpdaterByte; this.renewTask = createTask(); } /** * Returns configured permit rate per pre-configured rate-period. * * @return rate */ public synchronized long getRate() { return this.permits; } public synchronized long getRateTime() { return this.rateTime; } public synchronized TimeUnit getRateTimeUnit() { return this.timeUnit; } protected ScheduledFuture createTask() { return executorService.scheduleAtFixedRate(catchingAndLoggingThrowables(this::renew), this.rateTime, this.rateTime, this.timeUnit); } synchronized void renew() { acquiredPermits = isDispatchOrPrecisePublishRateLimiter ? Math.max(0, acquiredPermits - permits) : 0; if (permitUpdater != null) { long newPermitRate = permitUpdater.get(); if (newPermitRate > 0) { setRate(newPermitRate); } } // release the back-pressure by applying the rateLimitFunction only when there are available permits if (rateLimitFunction != null && this.getAvailablePermits() > 0) { rateLimitFunction.apply(); } notifyAll(); } @Override public String toString() { return MoreObjects.toStringHelper(this).add("rateTime", rateTime).add("permits", permits) .add("acquiredPermits", acquiredPermits).toString(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy