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

io.airlift.concurrent.DynamicSizeBoundQueue Maven / Gradle / Ivy

There is a newer version: 284
Show newest version
/*
 * 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 io.airlift.concurrent;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ticker;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.errorprone.annotations.ThreadSafe;
import jakarta.annotation.Nullable;

import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.ToLongFunction;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Verify.verify;
import static java.util.Objects.requireNonNull;
import static java.util.Objects.requireNonNullElseGet;
import static java.util.concurrent.TimeUnit.NANOSECONDS;

/**
 * Size constrained queue that utilizes a dynamic element size function. To prevent
 * starvation for adding large elements, the queue will only block new elements if
 * the total size has already been reached or exceeded. This means in the normal
 * case, the queue should be no larger than the max size plus the size of one element.
 * Callers also have the additional option to force insert further elements without
 * regard for size constraints. In the current implementation, elements are required
 * to have positive sizes (they cannot have zero size). This implementation is designed
 * to closely mirror the method signatures of {@link java.util.concurrent.BlockingQueue}.
 */
@ThreadSafe
public class DynamicSizeBoundQueue
{
    private final AtomicLong size = new AtomicLong();
    private final Queue> queue = new ConcurrentLinkedQueue<>();
    private final AtomicReference> enqueueFuture = new AtomicReference<>();
    private final AtomicReference> dequeueFuture = new AtomicReference<>();

    private final long maxSize;
    private final ToLongFunction elementSizeFunction;
    private final Ticker ticker;

    public DynamicSizeBoundQueue(long maxSize, ToLongFunction elementSizeFunction)
    {
        this(maxSize, elementSizeFunction, Ticker.systemTicker());
    }

    public DynamicSizeBoundQueue(long maxSize, ToLongFunction elementSizeFunction, Ticker ticker)
    {
        checkArgument(maxSize > 0, "maxSize must be positive");
        this.maxSize = maxSize;
        this.elementSizeFunction = requireNonNull(elementSizeFunction, "elementSizeFunction is null");
        this.ticker = requireNonNull(ticker, "ticker is null");
    }

    public long getMaxSize()
    {
        return maxSize;
    }

    /**
     * Gets the current size of the queue. The size is guaranteed to be no larger than max size plus
     * the size of one element if {@link DynamicSizeBoundQueue#forcePut(Object)} is not used.
     */
    public long getSize()
    {
        return size.get();
    }

    public boolean offer(T element)
    {
        long elementSize = elementSizeFunction.applyAsLong(element);
        return offer(element, elementSize);
    }

    private boolean offer(T element, long elementSize)
    {
        requireNonNull(element, "element is null");
        checkArgument(elementSize > 0, "element size must be positive");
        if (!tryAcquireSizeReservation(elementSize)) {
            return false;
        }
        queue.add(new ElementAndSize<>(element, elementSize));
        notifyIfNecessary(enqueueFuture);
        return true;
    }

    private boolean tryAcquireSizeReservation(long elementSize)
    {
        // Add the element as long as there is any space available
        if (size.get() >= maxSize) {
            return false;
        }

        long newSize;
        try {
            newSize = getAndAddOverflowChecked(size, elementSize);
        }
        catch (ArithmeticException e) { // Numeric overflow
            // While numeric overflow is extremely unlikely given typical numerical sizes,
            // even the largest possible element of size Long.MAX_VALUE can eventually fit
            // without numeric overflow as long as the queue can be emptied.
            return false;
        }

        if (newSize >= maxSize) {
            verify(size.addAndGet(-elementSize) >= 0);
            return false;
        }
        return true;
    }

    /**
     * Version of {@link java.util.concurrent.atomic.AtomicLong#getAndAdd} that throws {@link ArithmeticException}
     * on numeric overflow. This is slightly less efficient than the normal getAndAdd (which often has intrinsic
     * support). If this ever becomes a performance bottleneck, it is possible to use the original getAndAdd if
     * the caller can guarantee no risk of numeric overflow.
     */
    private static long getAndAddOverflowChecked(AtomicLong atomicLong, long delta)
    {
        return atomicLong.getAndAccumulate(delta, Math::addExact);
    }

    public boolean offer(T element, long timeout, TimeUnit unit)
            throws InterruptedException
    {
        long elementSize = elementSizeFunction.applyAsLong(element);
        long remainingTimeoutNs = unit.toNanos(timeout);
        while (!offer(element, elementSize)) {
            ListenableFuture future = getOrCreateFuture(dequeueFuture);
            // Check again in case we already missed the relevant dequeue event
            if (offer(element, elementSize)) {
                break;
            }
            long startTimeNs = ticker.read();
            if (remainingTimeoutNs <= 0 || !awaitDequeueFuture(future, remainingTimeoutNs, NANOSECONDS)) {
                // Timed out
                return false;
            }
            remainingTimeoutNs -= ticker.read() - startTimeNs;
        }
        return true;
    }

    public void put(T element)
            throws InterruptedException
    {
        long elementSize = elementSizeFunction.applyAsLong(element);
        while (!offer(element, elementSize)) {
            ListenableFuture future = getOrCreateFuture(dequeueFuture);
            // Check again in case we already missed the relevant dequeue event
            if (offer(element, elementSize)) {
                break;
            }
            awaitDequeueFuture(future);
        }
    }

    /**
     * Enqueue the element if there is space, otherwise returns a ListenableFuture that will complete
     * when space becomes available for the element. If a future is returned, the element was not inserted.
     */
    public Optional> offerWithBackoff(T element)
    {
        long elementSize = elementSizeFunction.applyAsLong(element);
        if (offer(element, elementSize)) {
            return Optional.empty();
        }
        ListenableFuture future = getOrCreateFuture(dequeueFuture);
        // Check again in case we already missed the relevant dequeue event
        if (offer(element, elementSize)) {
            return Optional.empty();
        }
        return Optional.of(Futures.nonCancellationPropagating(future));
    }

    /**
     * Insert without regard to the max size (potentially exceeding the max limit). This can throw an
     * {@link IllegalStateException} if the forced element triggers a numeric overflow, in which case
     * the element is not inserted.
     */
    public void forcePut(T element)
    {
        long elementSize = elementSizeFunction.applyAsLong(element);
        checkArgument(elementSize > 0, "element size must be positive");
        try {
            getAndAddOverflowChecked(size, elementSize);
        }
        catch (ArithmeticException e) { // Numeric overflow
            throw new IllegalStateException("Forced element triggered queue size numeric overflow");
        }
        queue.add(new ElementAndSize<>(element, elementSize));
        notifyIfNecessary(enqueueFuture);
    }

    @Nullable
    public T poll()
    {
        ElementAndSize elementAndSize = queue.poll();
        if (elementAndSize == null) {
            return null;
        }

        verify(size.addAndGet(-elementAndSize.size()) >= 0);
        notifyIfNecessary(dequeueFuture);
        return elementAndSize.element();
    }

    public T poll(long timeout, TimeUnit unit)
            throws InterruptedException
    {
        long remainingTimeoutNs = unit.toNanos(timeout);
        while (true) {
            T element = poll();
            if (element != null) {
                return element;
            }

            ListenableFuture future = getOrCreateFuture(enqueueFuture);
            // Check again in case we already missed the relevant enqueue event
            element = poll();
            if (element != null) {
                return element;
            }

            long startTimeNs = ticker.read();
            if (remainingTimeoutNs <= 0 || !awaitEnqueueFuture(future, remainingTimeoutNs, NANOSECONDS)) {
                // Timed out
                return null;
            }
            remainingTimeoutNs -= ticker.read() - startTimeNs;
        }
    }

    public T take()
            throws InterruptedException
    {
        while (true) {
            T element = poll();
            if (element != null) {
                return element;
            }

            ListenableFuture future = getOrCreateFuture(enqueueFuture);
            // Check again in case we already missed the relevant enqueue event
            element = poll();
            if (element != null) {
                return element;
            }

            awaitEnqueueFuture(future);
        }
    }

    private static ListenableFuture getOrCreateFuture(AtomicReference> reference)
    {
        return reference.updateAndGet(current -> requireNonNullElseGet(current, SettableFuture::create));
    }

    private static void notifyIfNecessary(AtomicReference> reference)
    {
        SettableFuture future = reference.getAndSet(null);
        if (future != null) {
            future.set(null);
        }
    }

    @VisibleForTesting
    void preEnqueueAwaitHook() {}

    @VisibleForTesting
    void preDequeueAwaitHook() {}

    private void awaitDequeueFuture(Future future)
            throws InterruptedException
    {
        preDequeueAwaitHook();
        awaitFutureUnchecked(future);
    }

    private boolean awaitDequeueFuture(Future future, long timeout, TimeUnit timeUnit)
            throws InterruptedException
    {
        preDequeueAwaitHook();
        return awaitFutureUnchecked(future, timeout, timeUnit);
    }

    private void awaitEnqueueFuture(Future future)
            throws InterruptedException
    {
        preEnqueueAwaitHook();
        awaitFutureUnchecked(future);
    }

    private boolean awaitEnqueueFuture(Future future, long timeout, TimeUnit timeUnit)
            throws InterruptedException
    {
        preEnqueueAwaitHook();
        return awaitFutureUnchecked(future, timeout, timeUnit);
    }

    private static void awaitFutureUnchecked(Future future)
            throws InterruptedException
    {
        try {
            future.get();
        }
        catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    private static boolean awaitFutureUnchecked(Future future, long timeout, TimeUnit timeUnit)
            throws InterruptedException
    {
        try {
            future.get(timeout, timeUnit);
            return true;
        }
        catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
        catch (TimeoutException e) {
            return false;
        }
    }

    private record ElementAndSize(T element, long size)
    {
        private ElementAndSize
        {
            requireNonNull(element, "element is null");
            checkArgument(size > 0, "size must be positive");
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy