TheBrig.java

package com.renomad.minum.security;

import com.renomad.minum.state.Constants;
import com.renomad.minum.state.Context;
import com.renomad.minum.database.Db;
import com.renomad.minum.logging.ILogger;
import com.renomad.minum.utils.SearchUtils;
import com.renomad.minum.utils.ThrowingRunnable;
import com.renomad.minum.utils.TimeUtils;

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;

/**
 * See {@link ITheBrig}
 */
public final class TheBrig implements ITheBrig {
    private final ExecutorService es;
    private final Db<Inmate> inmatesDb;
    private final ILogger logger;
    private final Constants constants;
    private final ReentrantLock lock = new ReentrantLock();
    private Thread myThread;

    /**
     * How long our inner thread will sleep before waking up to scan
     * for old keys
     */
    private final int sleepTime;

    public TheBrig(int sleepTime, Context context) {
        this.es = context.getExecutorService();
        this.constants = context.getConstants();
        this.logger = context.getLogger();
        this.inmatesDb = context.getDb("the_brig", Inmate.EMPTY);
        this.sleepTime = sleepTime;
    }

    /**
     * In this class we create a thread that runs throughout the lifetime
     * of the application, in an infinite loop removing keys from the list
     * under consideration.
     */
    public TheBrig(Context context) {
        this(10 * 1000, context);
    }

    // Regarding the BusyWait - indeed, we expect that the while loop
    // below is an infinite loop unless there's an exception thrown, that's what it is.
    @Override
    public ITheBrig initialize() {
        logger.logDebug(() -> "Initializing TheBrig main loop");
        ThrowingRunnable innerLoopThread = () -> {
            Thread.currentThread().setName("TheBrigThread");
            myThread = Thread.currentThread();
            while (true) {
                try {
                    reviewCurrentInmates();
                } catch (InterruptedException ex) {

                    /*
                    this is what we expect to happen.
                    once this happens, we just continue on.
                    this only gets called when we are trying to shut everything
                    down cleanly
                     */

                    logger.logDebug(() -> String.format("%s TheBrig is stopped.%n", TimeUtils.getTimestampIsoInstant()));
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        };
        es.submit(ThrowingRunnable.throwingRunnableWrapper(innerLoopThread, logger));
        return this;
    }

    private void reviewCurrentInmates() throws InterruptedException {
        Collection<Inmate> values = inmatesDb.values();
        if (! values.isEmpty()) {
            logger.logTrace(() -> "TheBrig reviewing current inmates. Count: " + values.size());
        }
        var now = System.currentTimeMillis();

        processInmateList(now, values, logger, inmatesDb);
        Thread.sleep(sleepTime);
    }

    /**
     * figure out which clients have paid their dues
     * @param now the current time, in milliseconds past the epoch
     * @param inmatesDb the database of all inmates
     */
    static void processInmateList(long now, Collection<Inmate> inmates, ILogger logger, Db<Inmate> inmatesDb) {
        List<String> keysToRemove = new ArrayList<>();
        for (Inmate clientKeyAndDuration : inmates) {
            reviewForParole(now, keysToRemove, clientKeyAndDuration, logger);
        }
        for (var k : keysToRemove) {
            logger.logTrace(() -> "TheBrig: removing " + k + " from jail");
            Inmate inmateToRemove = SearchUtils.findExactlyOne(inmates.stream(), x -> x.getClientId().equals(k));
            inmatesDb.delete(inmateToRemove);
        }
    }

    private static void reviewForParole(
            long now,
            List<String> keysToRemove,
            Inmate inmate,
            ILogger logger) {
        // if the release time is in the past (that is, the release time is
        // before / less-than now), add them to the list to be released.
        if (inmate.getReleaseTime() < now) {
            logger.logTrace(() -> "UnderInvestigation: " + inmate.getClientId() + " has paid its dues as of " + inmate.getReleaseTime() + " and is getting released. Current time: " + now);
            keysToRemove.add(inmate.getClientId());
        }
    }

    @Override
    public void stop() {
        logger.logDebug(() -> "TheBrig has been told to stop");
        if (myThread != null) {
            logger.logDebug(() -> "TheBrig: Sending interrupt to thread");
            myThread.interrupt();
            this.inmatesDb.stop();
        } else {
            throw new MinumSecurityException("TheBrig was told to stop, but it was uninitialized");
        }
    }


    @Override
    public boolean sendToJail(String clientIdentifier, long sentenceDuration) {
        if (!constants.isTheBrigEnabled) {
            return false;
        }
        lock.lock(); // block threads here if multiple are trying to get in - only one gets in at a time
        try {
            long now = System.currentTimeMillis();

            Inmate existingInmate = SearchUtils.findExactlyOne(inmatesDb.values().stream(), x -> x.getClientId().equals(clientIdentifier));
            if (existingInmate == null) {
                // if this is a new inmate, add them
                long releaseTime = now + sentenceDuration;
                logger.logDebug(() -> "TheBrig: Putting away " + clientIdentifier + " for " + sentenceDuration + " milliseconds. Release time: " + releaseTime + ". Current time: " + now);
                Inmate newInmate = new Inmate(0L, clientIdentifier, releaseTime);
                inmatesDb.write(newInmate);
            } else {
                // if this is an existing inmate continuing to attack us, just update their duration
                long releaseTime = existingInmate.getReleaseTime() + sentenceDuration;
                logger.logDebug(() -> "TheBrig: Putting away " + clientIdentifier + " for " + sentenceDuration + " milliseconds. Release time: " + releaseTime + ". Current time: " + now);
                inmatesDb.write(new Inmate(existingInmate.getIndex(), existingInmate.getClientId(), releaseTime));
            }
        } finally {
            lock.unlock();
        }
        return true;

    }

    @Override
    public boolean isInJail(String clientIdentifier) {
        if (!constants.isTheBrigEnabled) {
            return false;
        }
        lock.lock();
        try {
            return inmatesDb.values().stream().anyMatch(x -> x.getClientId().equals(clientIdentifier));
        } finally {
            lock.unlock();
        }
    }

    @Override
    public List<Inmate> getInmates() {
        lock.lock();
        try {
            return inmatesDb.values().stream().toList();
        } finally {
            lock.unlock();
        }
    }

}