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

com.hivemq.persistence.SingleWriterServiceImpl Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2019-present HiveMQ GmbH
 *
 * 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.hivemq.persistence;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.hivemq.bootstrap.ioc.lazysingleton.LazySingleton;
import com.hivemq.configuration.service.InternalConfigurations;
import com.hivemq.extension.sdk.api.annotations.NotNull;
import com.hivemq.persistence.local.xodus.bucket.BucketUtils;
import com.hivemq.util.Exceptions;
import com.hivemq.util.ThreadFactoryUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.util.SplittableRandom;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import static com.hivemq.configuration.service.InternalConfigurations.SINGLE_WRITER_INTERVAL_TO_CHECK_PENDING_TASKS_AND_SCHEDULE_MSEC;

/**
 * @author Lukas Brandl
 */
@LazySingleton
public class SingleWriterServiceImpl implements SingleWriterService {

    private static final @NotNull Logger log = LoggerFactory.getLogger(SingleWriterServiceImpl.class);

    private static final int AMOUNT_OF_PRODUCERS = 5;
    private static final int RETAINED_MESSAGE_QUEUE_INDEX = 0;
    private static final int CLIENT_SESSION_QUEUE_INDEX = 1;
    private static final int SUBSCRIPTION_QUEUE_INDEX = 2;
    private static final int QUEUED_MESSAGES_QUEUE_INDEX = 3;
    private static final int ATTRIBUTE_STORE_QUEUE_INDEX = 4;

    private final int persistenceBucketCount;
    private final int threadPoolSize;
    private final int creditsPerExecution;
    private final long shutdownGracePeriod;

    private final @NotNull AtomicLong nonemptyQueueCounter = new AtomicLong(0);
    private final @NotNull AtomicInteger runningThreadsCount = new AtomicInteger(0);
    private final @NotNull AtomicLong globalTaskCount = new AtomicLong(0);

    private final @NotNull ProducerQueuesImpl @NotNull [] producers = new ProducerQueuesImpl[AMOUNT_OF_PRODUCERS];

    @VisibleForTesting
    @NotNull ExecutorService singleWriterExecutor;

    @VisibleForTesting
    public final @NotNull ExecutorService @NotNull [] callbackExecutors;

    @VisibleForTesting
    final @NotNull ScheduledExecutorService checkScheduler;

    private final int amountOfQueues;

    @Inject
    public SingleWriterServiceImpl() {

        persistenceBucketCount = InternalConfigurations.PERSISTENCE_BUCKET_COUNT.get();
        threadPoolSize = InternalConfigurations.SINGLE_WRITER_THREAD_POOL_SIZE.get();
        creditsPerExecution = InternalConfigurations.SINGLE_WRITER_CREDITS_PER_EXECUTION.get();
        shutdownGracePeriod = InternalConfigurations.PERSISTENCE_SHUTDOWN_GRACE_PERIOD_MSEC.get();

        final ThreadFactory threadFactory = ThreadFactoryUtil.create("single-writer-%d");
        singleWriterExecutor = Executors.newFixedThreadPool(threadPoolSize, threadFactory);

        amountOfQueues = validAmountOfQueues(threadPoolSize, persistenceBucketCount);

        for (int i = 0; i < producers.length; i++) {
            producers[i] = new ProducerQueuesImpl(this, amountOfQueues);
        }

        callbackExecutors = new ExecutorService[amountOfQueues];
        for (int i = 0; i < amountOfQueues; i++) {
            final ThreadFactory callbackThreadFactory = ThreadFactoryUtil.create("single-writer-callback-" + i);
            final ExecutorService executorService = Executors.newSingleThreadScheduledExecutor(callbackThreadFactory);
            callbackExecutors[i] = executorService;
        }


        final ThreadFactory checkThreadFactory =
                new ThreadFactoryBuilder().setNameFormat("single-writer-scheduled-check-%d").build();
        checkScheduler = Executors.newSingleThreadScheduledExecutor(checkThreadFactory);
    }

    @PostConstruct
    public void postConstruct() {
        // Periodically check if there are pending tasks in the queues
        checkScheduler.scheduleAtFixedRate(() -> {
                    try {

                        if (runningThreadsCount.getAndIncrement() == 0 && !singleWriterExecutor.isShutdown()) {
                            singleWriterExecutor.submit(new SingleWriterTask(nonemptyQueueCounter,
                                    globalTaskCount,
                                    runningThreadsCount,
                                    producers));
                        } else {
                            runningThreadsCount.decrementAndGet();
                        }
                    } catch (final Exception e) {
                        log.error("Exception in single writer check task ", e);
                    }
                },
                SINGLE_WRITER_INTERVAL_TO_CHECK_PENDING_TASKS_AND_SCHEDULE_MSEC.get(),
                SINGLE_WRITER_INTERVAL_TO_CHECK_PENDING_TASKS_AND_SCHEDULE_MSEC.get(),
                TimeUnit.MILLISECONDS);
    }

    @VisibleForTesting
    int validAmountOfQueues(final int processorCount, final int bucketCount) {
        for (int i = processorCount; i < bucketCount; i++) {
            if (bucketCount % i == 0) {
                return i;
            }
        }
        return persistenceBucketCount;
    }

    void incrementNonemptyQueueCounter() {
        nonemptyQueueCounter.incrementAndGet();

        if (runningThreadsCount.getAndIncrement() < threadPoolSize) {
            singleWriterExecutor.submit(new SingleWriterTask(nonemptyQueueCounter,
                    globalTaskCount,
                    runningThreadsCount,
                    producers));
        } else {
            runningThreadsCount.decrementAndGet();
        }
    }


    @NotNull
    public ExecutorService callbackExecutor(@NotNull final String key) {
        final int bucketsPerQueue = persistenceBucketCount / amountOfQueues;
        final int bucketIndex = BucketUtils.getBucket(key, persistenceBucketCount);
        final int queueIndex = bucketIndex / bucketsPerQueue;
        return callbackExecutors[queueIndex];
    }

    public void decrementNonemptyQueueCounter() {
        nonemptyQueueCounter.decrementAndGet();
    }

    public @NotNull ProducerQueues getRetainedMessageQueue() {
        return producers[RETAINED_MESSAGE_QUEUE_INDEX];
    }

    public @NotNull ProducerQueues getClientSessionQueue() {
        return producers[CLIENT_SESSION_QUEUE_INDEX];
    }

    public @NotNull ProducerQueues getSubscriptionQueue() {
        return producers[SUBSCRIPTION_QUEUE_INDEX];
    }

    public @NotNull ProducerQueues getQueuedMessagesQueue() {
        return producers[QUEUED_MESSAGES_QUEUE_INDEX];
    }

    public @NotNull ProducerQueues getAttributeStoreQueue() {
        return producers[ATTRIBUTE_STORE_QUEUE_INDEX];
    }

    public int getPersistenceBucketCount() {
        return persistenceBucketCount;
    }

    public int getCreditsPerExecution() {
        return creditsPerExecution;
    }

    public long getShutdownGracePeriod() {
        return shutdownGracePeriod;
    }

    public int getThreadPoolSize() {
        return threadPoolSize;
    }

    public @NotNull AtomicLong getGlobalTaskCount() {
        return globalTaskCount;
    }

    public @NotNull AtomicLong getNonemptyQueueCounter() {
        return nonemptyQueueCounter;
    }

    public @NotNull AtomicInteger getRunningThreadsCount() {
        return runningThreadsCount;
    }

    public @NotNull ExecutorService @NotNull [] getCallbackExecutors() {
        return callbackExecutors;
    }

    public void stop() {
        final long start = System.currentTimeMillis();
        if (log.isTraceEnabled()) {
            log.trace("Shutting down single writer");
        }

        singleWriterExecutor.shutdown();

        try {
            singleWriterExecutor.awaitTermination(shutdownGracePeriod, TimeUnit.SECONDS);
            if (log.isTraceEnabled()) {
                log.trace("Finished single writer shutdown in {} ms", (System.currentTimeMillis() - start));
            }
        } catch (final InterruptedException e) {
            //ignore
        }
        singleWriterExecutor.shutdownNow();
        for (final ExecutorService callbackExecutor : callbackExecutors) {
            callbackExecutor.shutdownNow();
        }
        checkScheduler.shutdownNow();
    }

    private static class SingleWriterTask implements Runnable {

        private final @NotNull AtomicLong nonemptyQueueCounter;
        private final @NotNull AtomicLong globalTaskCount;
        private final @NotNull AtomicInteger runningThreadsCount;
        private final ProducerQueuesImpl @NotNull [] producers;
        final int @NotNull [] probabilities;

        private static final int MIN_PROBABILITY_IN_PERCENT = 5;

        private static final @NotNull SplittableRandom RANDOM = new SplittableRandom();

        public SingleWriterTask(
                final @NotNull AtomicLong nonemptyQueueCounter,
                final @NotNull AtomicLong globalTaskCount,
                final @NotNull AtomicInteger runningThreadsCount,
                final ProducerQueuesImpl @NotNull [] producers) {

            this.nonemptyQueueCounter = nonemptyQueueCounter;
            this.globalTaskCount = globalTaskCount;
            this.runningThreadsCount = runningThreadsCount;
            this.producers = producers;
            probabilities = new int[producers.length];
        }

        public void run() {
            try {

                final SplittableRandom random = RANDOM.split();

                // It is possible that all tasks stop running while there are still non-empty queues.
                // We have yet to determine if there is a lock free way to avoid this.
                outerLoop:
                while (nonemptyQueueCounter.get() >= runningThreadsCount.getAndDecrement()) {
                    runningThreadsCount.incrementAndGet();
                    final long countSnapShot = globalTaskCount.get();
                    if (countSnapShot == 0) {
                        continue;
                    }

                    // Calculate the percentage portion of total tasks per persistence.
                    for (int i = 0; i < producers.length; i++) {
                        probabilities[i] = (int) ((producers[i].getTaskCount().get() * 100) / countSnapShot);
                    }

                    int sumWithoutMins = 0;
                    // Set to min probability if necessary
                    for (int i = 0; i < probabilities.length; i++) {
                        if (probabilities[i] < MIN_PROBABILITY_IN_PERCENT) {
                            probabilities[i] = MIN_PROBABILITY_IN_PERCENT;
                        } else {
                            sumWithoutMins += probabilities[i];
                        }
                    }

                    int surplus = 0;
                    for (int i = 0; i < probabilities.length; i++) {
                        surplus += probabilities[i];
                    }
                    surplus -= 100;

                    if (surplus > 0) { // Normalize to a 100% sum
                        // We reduce the probability of all persistences that are not at the minimum, be a portion of the overhead.
                        // The portion is based on there portion of the sum of all probabilities, ignoring those with minimum probability.
                        for (int i = 0; i < probabilities.length; i++) {
                            if (probabilities[i] > MIN_PROBABILITY_IN_PERCENT) {
                                probabilities[i] -= surplus / (sumWithoutMins / probabilities[i]);
                            }
                        }
                    }

                    final int randomInt = random.nextInt(100);
                    int offset = 0;

                    for (int i = 0; i < probabilities.length; i++) {
                        if (randomInt <= probabilities[i] + offset) {
                            producers[i].execute(random);
                            continue outerLoop;
                        }
                        offset += probabilities[i];
                    }
                }

            } catch (final Throwable t) {
                // Exceptions in the executed tasks, are passed to there result future.
                // So we only end up here if there is an exception in the probability calculation.
                // We decrement the running thread count so that a new thread will start running, as soon as a new task is added to any queue.
                runningThreadsCount.decrementAndGet();
                Exceptions.rethrowError("Exception in single writer executor. ", t);
            }
        }
    }


}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy