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

org.eclipse.ditto.connectivity.service.messaging.amqp.MessageRateLimiter Maven / Gradle / Ivy

/*
 * Copyright (c) 2020 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.eclipse.ditto.connectivity.service.messaging.amqp;

import java.time.Duration;
import java.util.HashSet;
import java.util.Set;

import javax.annotation.concurrent.NotThreadSafe;

import org.eclipse.ditto.connectivity.service.config.Amqp10Config;
import org.eclipse.ditto.connectivity.service.config.ConnectionThrottlingConfig;

/**
 * Rate limiter for unacknowledged messages and total messages.
 * NOT thread-safe! Only use in actor's own thread! Do NOT use in futures!
 *
 * @param  type of message IDs
 */
@NotThreadSafe
final class MessageRateLimiter {

    private final boolean enabled;
    private final int maxPerPeriod;
    private final int maxInFlight;
    private final Duration redeliveryExpectationTimeout;

    /**
     * Whether consumers are open before the next throttling decision.
     */
    private boolean isConsumerOpen = true;

    /**
     * Set of message IDs for which we have requested redelivery which has not arrived.
     */
    private final Set pendingRedeliveries = new HashSet<>();

    /**
     * Counter for messages we have received and have not acknowledged.
     */
    private int inFlight = 0;

    /**
     * Counter for messages we have received in a given period.
     */
    private int consumedInPeriod = 0;

    private MessageRateLimiter(final ConnectionThrottlingConfig connectionThrottlingConfig,
            final Duration redeliveryExpectationTimeout) {
        this.enabled = connectionThrottlingConfig.isEnabled();
        this.maxPerPeriod = connectionThrottlingConfig.getLimit();
        this.maxInFlight = connectionThrottlingConfig.getMaxInFlight();
        this.redeliveryExpectationTimeout = maxDuration(redeliveryExpectationTimeout, Duration.ofSeconds(1L));
    }

    private MessageRateLimiter(final ConnectionThrottlingConfig connectionThrottlingConfig,
            final Duration redeliveryExpectationTimeout, final MessageRateLimiter existingLimiter) {
        this.enabled = connectionThrottlingConfig.isEnabled();
        this.maxPerPeriod = connectionThrottlingConfig.getLimit();
        this.maxInFlight = connectionThrottlingConfig.getMaxInFlight();
        this.redeliveryExpectationTimeout = maxDuration(redeliveryExpectationTimeout, Duration.ofSeconds(1L));
        this.isConsumerOpen = existingLimiter.isConsumerOpen;
        this.pendingRedeliveries.addAll(existingLimiter.pendingRedeliveries);
        this.inFlight = existingLimiter.inFlight;
        this.consumedInPeriod = existingLimiter.consumedInPeriod;
    }

    static  MessageRateLimiter of(final Amqp10Config config) {
        return new MessageRateLimiter<>(config.getConsumerConfig().getThrottlingConfig(),
                config.getConsumerConfig().getRedeliveryExpectationTimeout());
    }

    static  MessageRateLimiter of(final Amqp10Config config, final MessageRateLimiter existingLimiter) {
        return new MessageRateLimiter<>(config.getConsumerConfig().getThrottlingConfig(),
                config.getConsumerConfig().getRedeliveryExpectationTimeout(),
                existingLimiter);
    }

    /**
     * @return whether rate limiter is enabled.
     */
    boolean isEnabled() {
        return enabled;
    }

    /**
     * @return how many messages were consumed within this period.
     */
    int getConsumedInPeriod() {
        return consumedInPeriod;
    }

    /**
     * @return maximum number of messages we are allowed to consume within this period.
     */
    int getMaxPerPeriod() {
        return maxPerPeriod;
    }

    /**
     * @return current number of unacknowledged messages and messages expected to be redelivered
     */
    int getInFlight() {
        return inFlight;
    }

    /**
     * @return the number of messages expected to be redelivered
     */
    int getToBeRedelivered() {
        return pendingRedeliveries.size();
    }

    /**
     * @return maximum number of unacknowledged messages and messages expected to be redelivered
     */
    int getMaxInFlight() {
        return maxInFlight;
    }

    /**
     * @return after how long to forget about a pending unacknowledged message (could be consumed by someone else)
     */
    Duration getRedeliveryExpectationTimeout() {
        return redeliveryExpectationTimeout;
    }

    /**
     * Record an incoming message.
     *
     * @param messageId the ID of the incoming message.
     */
    void incoming(final S messageId) {
        inFlight++;
        consumedInPeriod++;
        pendingRedeliveries.remove(messageId);
    }

    /**
     * Record acknowledgement of an incoming message with no redelivery expected.
     *
     * @param messageId the ID of the incoming message.
     */
    void terminallyAcknowledged(final S messageId) {
        inFlight--;
    }

    /**
     * Record acknowledgement of an incoming message with redelivery expected.
     *
     * @param messageId the ID of the incoming message.
     */
    void redeliveryRequested(final S messageId) {
        inFlight--;
        pendingRedeliveries.add(messageId);
    }

    /**
     * Forget a message whose redelivery is expected.
     *
     * @param messageId the ID of the message whose redelivery is expected.
     */
    void forgetPendingRedelivery(final S messageId) {
        pendingRedeliveries.remove(messageId);
    }

    /**
     * @return whether the consumer is known to be open.
     */
    boolean isConsumerOpen() {
        return isConsumerOpen;
    }

    /**
     * Set whether the consumer is open.
     *
     * @param isConsumerOpen whether the consumer is open.
     */
    void setIsConsumerOpen(final boolean isConsumerOpen) {
        this.isConsumerOpen = isConsumerOpen;
    }

    /**
     * @return whether the number of messages consumed exceeded the maximum.
     */
    boolean isMaxPerPeriodExceeded() {
        return consumedInPeriod > maxPerPeriod;
    }

    /**
     * @return whether the number of unacknowledged/to-be-redelivered messages exceeded the maximum (e. g.,
     * when a subscriber of events generated by incoming commands is offline).
     */
    boolean isMaxInFlightExceeded() {
        return maxInFlight - inFlight - pendingRedeliveries.size() < 0;
    }

    /**
     * Called periodically to allocate "credit" to this consumer.
     * Setting "prefetch" policy does not work for JMS client unfortunately (checked version: 0.45.0).
     */
    void reduceConsumedInPeriod() {
        consumedInPeriod = Math.max(consumedInPeriod - maxPerPeriod, 0);
    }

    private static Duration maxDuration(final Duration d1, final Duration d2) {
        return d2.minus(d1).isNegative() ? d1 : d2;
    }

}