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

io.numaproj.numaflow.reducestreamer.SupervisorActor Maven / Gradle / Ivy

The newest version!
package io.numaproj.numaflow.reducestreamer;

import akka.actor.AbstractActor;
import akka.actor.ActorRef;
import akka.actor.ChildRestartStats;
import akka.actor.Props;
import akka.actor.SupervisorStrategy;
import akka.japi.pf.DeciderBuilder;
import akka.japi.pf.ReceiveBuilder;
import com.google.common.base.Preconditions;
import io.numaproj.numaflow.reduce.v1.ReduceOuterClass;
import io.numaproj.numaflow.reducestreamer.model.Metadata;
import io.numaproj.numaflow.reducestreamer.model.ReduceStreamer;
import io.numaproj.numaflow.reducestreamer.model.ReduceStreamerFactory;
import lombok.extern.slf4j.Slf4j;
import scala.PartialFunction;
import scala.collection.Iterable;

import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
 * Supervisor actor distributes the messages to other actors and handles failures.
 */
@Slf4j
class SupervisorActor extends AbstractActor {
    private final ReduceStreamerFactory reduceStreamerFactory;
    private final Metadata md;
    private final ActorRef shutdownActor;
    private final ActorRef outputActor;
    private final Map actorsMap = new HashMap<>();

    public SupervisorActor(
            ReduceStreamerFactory reduceStreamerFactory,
            Metadata md,
            ActorRef shutdownActor,
            ActorRef outputActor) {
        this.reduceStreamerFactory = reduceStreamerFactory;
        this.md = md;
        this.shutdownActor = shutdownActor;
        this.outputActor = outputActor;
    }

    public static Props props(
            ReduceStreamerFactory reduceStreamerFactory,
            Metadata md,
            ActorRef shutdownActor,
            ActorRef outputActor) {
        return Props.create(
                SupervisorActor.class,
                reduceStreamerFactory,
                md,
                shutdownActor,
                outputActor);
    }

    // if there is an uncaught exception stop in the supervisor actor, send a signal to shut down
    @Override
    public void preRestart(Throwable reason, Optional message) {
        log.debug("supervisor pre restart was executed");
        shutdownActor.tell(reason, ActorRef.noSender());
        Service.reduceActorSystem.stop(getSelf());
    }

    @Override
    public SupervisorStrategy supervisorStrategy() {
        return new ReduceSupervisorStrategy();
    }


    @Override
    public void postStop() {
        log.debug("post stop of supervisor executed - {}", getSelf().toString());
        shutdownActor.tell(Constants.SUCCESS, ActorRef.noSender());
    }

    @Override
    public Receive createReceive() {
        return ReceiveBuilder
                .create()
                .match(ActorRequest.class, this::invokeActor)
                .match(String.class, this::sendEOF)
                .match(ActorResponse.class, this::handleActorEOFResponse)
                .build();
    }

    /*
        based on the keys of the input message invoke the right reduce streamer actor
        if there is no actor for an incoming set of keys, create a new reduce streamer actor
        track all the child actors using actors map
     */
    private void invokeActor(ActorRequest actorRequest) {
        String[] keys = actorRequest.getKeySet();
        String uniqueId = actorRequest.getUniqueIdentifier();
        if (!actorsMap.containsKey(uniqueId)) {
            ReduceStreamer reduceStreamerHandler = reduceStreamerFactory.createReduceStreamer();
            ActorRef actorRef = getContext()
                    .actorOf(ReduceStreamerActor.props(
                            keys,
                            this.md,
                            reduceStreamerHandler,
                            this.outputActor));
            actorsMap.put(uniqueId, actorRef);
        }
        HandlerDatum handlerDatum = constructHandlerDatum(actorRequest.getRequest().getPayload());
        actorsMap.get(uniqueId).tell(handlerDatum, getSelf());
    }

    private void sendEOF(String EOF) {
        for (Map.Entry entry : actorsMap.entrySet()) {
            entry.getValue().tell(EOF, getSelf());
        }
    }

    private void handleActorEOFResponse(ActorResponse actorResponse) {
        // when the supervisor receives an actor response, it means the corresponding
        // reduce streamer actor has finished its job.
        // we remove the entry from the actors map.
        actorsMap.remove(actorResponse.getActorUniqueIdentifier());
        if (actorsMap.isEmpty()) {
            // since the actors map is empty, this particular actor response is the last response to forward to output gRPC stream.
            // for reduce streamer, we only send to output stream one single EOF response, which is the last one.
            // we don't care about per-key-set EOFs.
            this.outputActor.tell(actorResponse, getSelf());
        }
    }

    private HandlerDatum constructHandlerDatum(ReduceOuterClass.ReduceRequest.Payload payload) {
        return new HandlerDatum(
                payload.getValue().toByteArray(),
                Instant.ofEpochSecond(
                        payload.getWatermark().getSeconds(),
                        payload.getWatermark().getNanos()),
                Instant.ofEpochSecond(
                        payload.getEventTime().getSeconds(),
                        payload.getEventTime().getNanos()),
                payload.getHeadersMap()
        );
    }

    /*
        We need supervisor to handle failures, by default if there are any failures
        actors will be restarted, but we want to escalate the exception and terminate
        the system.
    */
    private final class ReduceSupervisorStrategy extends SupervisorStrategy {

        @Override
        public PartialFunction decider() {
            return DeciderBuilder.match(Exception.class, e -> SupervisorStrategy.stop()).build();
        }

        @Override
        public void handleChildTerminated(
                akka.actor.ActorContext context,
                ActorRef child,
                Iterable children) {

        }

        @Override
        public void processFailure(
                akka.actor.ActorContext context,
                boolean restart,
                ActorRef child,
                Throwable cause,
                ChildRestartStats stats,
                Iterable children) {

            Preconditions.checkArgument(
                    !restart,
                    "on failures, we will never restart our actors, we escalate");
            /*
                   tell the shutdown actor about the exception.
             */
            log.debug("process failure of supervisor strategy executed - {}", getSelf().toString());
            shutdownActor.tell(cause, context.parent());
        }
    }
}