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

com.spotify.futures.ConcurrencyLimiter Maven / Gradle / Ivy

There is a newer version: 4.3.3
Show newest version
/*
 * Copyright (c) 2013-2018 Spotify AB
 *
 * Licensed 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 com.spotify.futures;

import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;

/**
 * A ConcurrencyLimiter can be used for efficiently queueing up
 * asynchronous work to only run up to a specific limit of work
 * concurrently.
 *
 * 

This is a threadsafe class. */ public final class ConcurrencyLimiter implements FutureJobInvoker { private final Executor executor; private final BlockingQueue> queue; private final Semaphore limit; private final int maxQueueSize; private final int maxConcurrency; private ConcurrencyLimiter(final Executor executor, int maxConcurrency, int maxQueueSize) { this.executor = executor; this.maxConcurrency = maxConcurrency; this.maxQueueSize = maxQueueSize; Preconditions.checkArgument(maxConcurrency > 0); Preconditions.checkArgument(maxQueueSize > 0); this.queue = new ArrayBlockingQueue<>(maxQueueSize); this.limit = new Semaphore(maxConcurrency); } /** * Create a {@code ConcurrencyLimiter} instance. * * @param executor the executor to run callables on. * @param maxConcurrency maximum number of futures in progress, * @param maxQueueSize maximum number of jobs in queue. This is a soft bound and may be * temporarily exceeded if add() is called concurrently. * @return a new concurrency limiter */ public static ConcurrencyLimiter create( final Executor executor, int maxConcurrency, int maxQueueSize) { return new ConcurrencyLimiter<>(executor, maxConcurrency, maxQueueSize); } /** * the callable function will run as soon as the currently active set of * futures is less than the maxConcurrency limit. * * @param callable - a function that creates a future. * @return a proxy future that completes with the future created by the * input function. * This future will be immediately failed with * {@link CapacityReachedException} if the soft queue size limit is exceeded. * @throws NullPointerException if callable is null */ @Override public ListenableFuture add(Callable> callable) { Preconditions.checkNotNull(callable); final SettableFuture response = SettableFuture.create(); final Job job = new Job<>(callable, response); if (!queue.offer(job)) { final String message = "Queue size has reached capacity: " + maxQueueSize; return Futures.immediateFailedFuture(new CapacityReachedException(message)); } executor.execute(this::pump); return response; } /** * Return the number of callables that are queued up and haven't started yet. * @return the number of callables that are queued up and haven't started yet. */ public int numQueued() { return queue.size(); } /** * Return the number of currently active futures that have not yet completed. * @return the number of currently active futures that have not yet completed. */ public int numActive() { return maxConcurrency - limit.availablePermits(); } /** * Return the number of additional callables that can be queued before failing. * @return the number of additional callables that can be queued before failing. */ public int remainingQueueCapacity() { return queue.remainingCapacity(); } /** * Return the number of additional callables that can be run without queueing. * @return the number of additional callables that can be run without queueing. */ public int remainingActiveCapacity() { return limit.availablePermits(); } /** * Return a {@code Job} with acquired permit, {@code null} otherwise. * *

Does one of two things: * 1) return a job and acquire a permit from the semaphore * 2) return null and does not acquire a permit from the semaphore */ private Job grabJob() { if (!limit.tryAcquire()) { return null; } final Job job = queue.poll(); if (job != null) { return job; } limit.release(); return null; } private void pump() { Job job; while ((job = grabJob()) != null) { final SettableFuture response = job.response; if (response.isCancelled()) { limit.release(); } else { invoke(response, job.callable); } } } private void invoke( final SettableFuture response, Callable> callable) { final ListenableFuture future; try { future = callable.call(); if (future == null) { limit.release(); response.setException(new NullPointerException()); return; } } catch (Throwable e) { limit.release(); response.setException(e); return; } Futures.addCallback(future, new FutureCallback() { @Override public void onSuccess(T result) { limit.release(); response.set(result); pump(); } @Override public void onFailure(Throwable t) { limit.release(); response.setException(t); pump(); } }, executor); } private static class Job { private final Callable> callable; private final SettableFuture response; Job(Callable> callable, SettableFuture response) { this.callable = callable; this.response = response; } } public static class CapacityReachedException extends RuntimeException { public CapacityReachedException(String errorMessage) { super(errorMessage); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy