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

eu.lucaventuri.fibry.BaseActor Maven / Gradle / Ivy

There is a newer version: 3.0.1
Show newest version
package eu.lucaventuri.fibry;

import eu.lucaventuri.common.Exceptions;
import eu.lucaventuri.common.Exitable;
import eu.lucaventuri.common.Stateful;
import eu.lucaventuri.common.SystemUtils;
import eu.lucaventuri.functional.Either3;

import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;

public abstract class BaseActor extends Exitable implements Function, PartialActor, SinkActor, MessageOnlyActor {
    protected final MiniQueue>, T, MessageWithAnswer>> queue;
    protected S state;
    protected final Consumer finalizer;
    protected final List closeOnExit = new Vector<>();
    protected volatile boolean drainMessagesOnExit = true;
    protected final int pollTimeoutMs;
    protected final ActorSystem.AutoHealingSettings autoHealing;
    protected final CreationStrategy strategy;

    BaseActor(MiniQueue>, T, MessageWithAnswer>> queue, Consumer finalizer, CloseStrategy closeStrategy, int pollTimeoutMs, ActorSystem.AutoHealingSettings autoHealing, CreationStrategy strategy) {
        this.queue = queue;
        this.finalizer = finalizer;
        this.sendPoisonPillWhenExiting = true;
        this.pollTimeoutMs = pollTimeoutMs;
        this.autoHealing = autoHealing;
        this.strategy = strategy;

        if (autoHealing != null && autoHealing.executionTimeoutSeconds > 0)
            HealRegistry.INSTANCE.startThread();

        if (closeStrategy != null)
            this.closeStrategy = closeStrategy;
    }

    /**
     * Asynchronously sends a message to the actor; the result can be accessed using the CompletableFuture
     */
    public CompletableFuture sendMessageReturn(T message) {
        if (isExiting())
            return getCompletableFutureWhenExiting();

        return ActorUtils.sendMessageReturn(queue, message);
    }

    /**
     * Asynchronously sends multiple messages to the actor; the results can be accessed using the CompletableFutures provided
     */
    public CompletableFuture[] sendMessagesReturn(T... messages) {
        List> listFutures = new ArrayList<>();

        for (int i = 0; i < messages.length && !isExiting(); i++)
            listFutures.add(ActorUtils.sendMessageReturn(queue, messages[i]));

        CompletableFuture ar[] = new CompletableFuture[listFutures.size()];

        for (int i = 0; i < listFutures.size(); i++)
            ar[i] = listFutures.get(i);

        return ar;
    }

    /**
     * Send a series of messages to an actor and collects the results (blocking until all the messages have been processed or the actor died)
     */
    public List sendAndCollectSilent(R valueOnError, T... messages) {
        CompletableFuture futures[] = sendMessagesReturn(messages);
        List results = new ArrayList<>();

        waitForFuturesCompletionOrActorExit(futures, 5);

        for (CompletableFuture future : futures)
            results.add(future.isCompletedExceptionally() || future.isCancelled() ? valueOnError : future.getNow(valueOnError));

        return results;
    }

    private void waitForFuturesCompletionOrActorExit(CompletableFuture[] originalFutures, int pollingSleepMs) {
        List> futuresNotCompleted = new ArrayList<>();

        for (CompletableFuture future : originalFutures)
            futuresNotCompleted.add(future);

        while (!futuresNotCompleted.isEmpty() && !isFinished()) {
            for (int i = futuresNotCompleted.size() - 1; i >= 0; i--) {
                if (futuresNotCompleted.get(i).isDone())
                    futuresNotCompleted.remove(i);
            }
            SystemUtils.sleep(pollingSleepMs);
        }
    }

    private CompletableFuture getCompletableFutureWhenExiting() {
        assert isExiting();
        CompletableFuture cf = new CompletableFuture();

        if (isFinished())
            cf.completeExceptionally(new IllegalStateException("Actor terminated"));
        else
            cf.completeExceptionally(new IllegalStateException("Actor exiting"));

        return cf;
    }

    /**
     * Synchronously sends a message and waits for the result. This can take some time because of the context switch and of possible messages in the queue
     */
    public R sendMessageReturnWait(T message, R valueOnError) {
        try {
            if (isExiting())
                return valueOnError;

            return ActorUtils.sendMessageReturn(queue, message).get();
        } catch (InterruptedException | ExecutionException e) {
            return valueOnError;
        }
    }

    @Override
    public R apply(T message) {
        try {
            return sendMessageReturn(message).get();
        } catch (InterruptedException | ExecutionException e) {
            return null;
        }
    }

    /**
     * Asynchronously executes some logic in the actor; the parameter supplied is the actor itself.
     */
    public void execAsync(Consumer> worker) {
        if (!isExiting())
            ActorUtils.execAsync(queue, worker);
    }

    public void execAsyncStateful(Consumer> worker) {
        execAsync(worker::accept);
    }

    /**
     * Synchronously executes some logic in the actor; the parameter supplied is the actor itself.
     */
    public void execAndWait(Consumer> worker) {
        if (!isExiting())
            ActorUtils.execAndWait(queue, worker);
    }

    public void execAsyncState(Consumer worker) {
        execAsync(actor -> worker.accept(actor.getState()));
    }

    public void execAndWaitState(Consumer worker) {
        execAndWait(actor -> worker.accept(actor.getState()));
    }

    /**
     * Asynchronously executes some logic in the actor; the parameter supplied is the actor itself. The returned Future can be used to check if the task has been completed.
     */
    public CompletableFuture execFuture(Consumer> worker) {
        if (isExiting())
            return getCompletableFutureWhenExiting();

        return ActorUtils.execFuture(queue, worker);
    }

    /**
     * Asynchronously executes some logic in the actor.
     */
    public void execAsync(Runnable worker) {
        if (!isExiting())
            ActorUtils.execAsync(queue, state -> worker.run());
    }

    /**
     * Asynchronously executes some logic in the actor. This method is useful for bounded queues, when the caller should wait instead of getting an exception (e.g. Excutors).
     */
    public boolean execAsyncTimeout(Runnable worker, int timeoutMs) {
        if (!isExiting())
            return ActorUtils.execAsyncTimeout(queue, state -> worker.run(), timeoutMs);

        return false;
    }

    /**
     * Synchronously executes some logic in the actor.
     */
    public void execAndWait(Runnable worker) {
        if (!isExiting())
            ActorUtils.execAndWait(queue, state -> worker.run());
    }

    /**
     * Asynchronously executes some logic in the actor. The returned Future can be used to check if the task has been completed.
     */
    public CompletableFuture execFuture(Runnable worker) {
        if (isExiting())
            return getCompletableFutureWhenExiting();

        return ActorUtils.execFuture(queue, state -> worker.run());
    }

    @Override
    public Exitable setExitSendsPoisonPill(boolean send) {
        return super.setExitSendsPoisonPill(send);
    }

    @Override
    public void askExit() {
        super.askExit();
    }

    /**
     * Queue a request to exit, that will be processed after all the messages currently in the queue
     *
     * @return
     */
    @Override
    public boolean sendPoisonPill() {
        execAsyncTimeout(() -> askExit(), Integer.MAX_VALUE);

        return true;
    }

    /**
     * Queue a request to exit, that will be processed after all the messages currently in the queue
     *
     * @return a completable future that can be used to check when the pill ahs been sent
     */
    public CompletableFuture sendPoisonPillFuture() {
        try {
            CompletableFuture future = new CompletableFuture<>();

            execAsync(state -> {
                askExit();
                future.complete(true);
            });

            return future;
        } catch (IllegalStateException state) {
            return CompletableFuture.completedFuture(false);
        }
    }

    /**
     * @return the state of the actor
     */
    public S getState() {
        return state;
    }

    /**
     * @return the length of the queue
     */
    public long getQueueLength() {
        return queue.size();
    }

    public final void processMessages() {
        if (autoHealing == null || autoHealing.executionTimeoutSeconds <= 0) {  // No executio timeout
            if (pollTimeoutMs == Integer.MAX_VALUE) {
                while (!isExiting())
                    Exceptions.log(this::takeAndProcessSingleMessage);
            } else {
                while (!isExiting())
                    Exceptions.log(this::takeAndProcessSingleMessageTimeout);
            }
        } else { // Execution timeout
            Thread curThread = Thread.currentThread();
            AtomicBoolean threadShouldDie = new AtomicBoolean();

            while (!isExiting()) {
                try {
                    if (pollTimeoutMs == Integer.MAX_VALUE)
                        takeAndProcessSingleMessage();
                    else
                        takeAndProcessSingleMessageTimeout();
                } catch (Exception e) {
                    if (autoHealing.onInterruption != null && (Thread.interrupted() ||
                            e.getClass() == InterruptedException.class || e.getCause().getClass() == InterruptedException.class)) {
                        autoHealing.onInterruption.run();
                    } else {
                        System.err.println(e);
                    }
                }
                HealRegistry.INSTANCE.remove(this, curThread, threadShouldDie);
                if (threadShouldDie.get()) {  // Notification done in HealRegistry, earlier
                    HealRegistry.INSTANCE.addThread(this);
                    return;  // Skips finalization, or the messages will be removed
                }
            }
        }

        finalOperations();
    }

    protected abstract void takeAndProcessSingleMessage() throws InterruptedException;

    protected abstract void takeAndProcessSingleMessageTimeout() throws InterruptedException;

    public BaseActor closeOnExit(AutoCloseable... closeables) {
        for (AutoCloseable closeable : closeables)
            closeOnExit.add(closeable);

        return this;
    }

    public BaseActor sendPoisonPillOnExit(Exitable... exitables) {
        for (Exitable exitable : exitables)
            closeOnExit.add(exitable::sendPoisonPill);

        return this;
    }

    protected void finalOperations() {
        if (finalizer != null)
            finalizer.accept(state);

        if (drainMessagesOnExit)
            queue.clear();

        for (AutoCloseable closeable : closeOnExit) {
            SystemUtils.close(closeable);
        }

        notifyFinished();
    }

    @Override
    // Just return a more specific type
    public BaseActor sendMessage(T message) {
        if (!isExiting())
            ActorUtils.sendMessage(queue, message);
        return this;
    }

    @Override
    public void setState(S state) {
        this.state = state;
    }

    public BaseActor setDrainMessagesOnExit(boolean drainMessagesOnExit) {
        this.drainMessagesOnExit = drainMessagesOnExit;

        return this;
    }

    // Create a new Thread reusing the same actor, for auto healing
    protected abstract BaseActor recreate();
}