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

org.kurator.akka.KuratorActor Maven / Gradle / Ivy

package org.kurator.akka;

import java.io.InputStream;
import java.io.PrintStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.kurator.akka.data.WorkflowArtifact;
import org.kurator.akka.data.WorkflowProduct;
import org.kurator.akka.messages.ControlMessage;
import org.kurator.akka.messages.EndOfStream;
import org.kurator.akka.messages.ExceptionMessage;
import org.kurator.akka.messages.Failure;
import org.kurator.akka.messages.FutureComplete;
import org.kurator.akka.messages.Initialize;
import org.kurator.akka.messages.ProductPublication;
import org.kurator.akka.messages.Success;
import org.kurator.akka.messages.Start;
import org.kurator.akka.messages.WrappedMessage;
import org.kurator.akka.metadata.BroadcastEventCountChecker;
import org.kurator.akka.metadata.BroadcastEventCounter;
import org.kurator.akka.metadata.MetadataWriter;
import org.kurator.akka.metadata.MetadataReader;
import org.kurator.log.Log;
import org.kurator.log.Logger;
import org.kurator.log.SilentLogger;

import akka.actor.ActorRef;
import akka.actor.UntypedActor;

/** 
 * Base class for all actors that can run within the Kurator-Akka workflow framework.
 * 
 * 

This class standardizes the actor lifecycle and maintains the list of listeners for each actor. * It also provides the option of automatically stopping an actor when an * {@link org.kurator.akka.messages.EndOfStream EndOfStream} * message is received, and for automatically propagating the * {@link org.kurator.akka.messages.EndOfStream EndOfStream} message to listeners. * The class thus supports clean shutdown of Akka workflows with each actor terminating itself when no * further messages from upstream can be expected.

*/ public abstract class KuratorActor extends UntypedActor { /** Shorthand for platform-specific end-of-line character sequence. */ public static final String EOL = System.getProperty("line.separator"); private static Integer nextActorId = 1; /** Determines if this actor automatically terminates when it receives an * {@link org.kurator.akka.messages.EndOfStream EndOfStream} message. * Defaults to true. */ public boolean endOnEos = true; /** Determines if this actor automatically sends an * {@link org.kurator.akka.messages.EndOfStream EndOfStream} message to * all of its listeners just before it stops executing. * Defaults to true. */ public boolean sendEosOnEnd = true; public boolean needsTrigger = false; /** Stream used by actor instead of reading from System.in directly. * Defaults to System.in. *

Non-default value assigned can be assigned via the {@link #inputStream inputStream()} method.

*/ protected volatile InputStream inStream = System.in; /** Stream used by actor instead of writing to System.out directly. * Defaults to System.out. *

Non-default value assigned can be assigned via the {@link #outputStream outputStream()} method.

*/ protected volatile PrintStream outStream = System.out; /** Stream used by actor instead of writing to System.err directly. * Defaults to System.err. *

Non-default value can be assigned via the {@link #errorStream errorStream()} method.

*/ protected volatile PrintStream errStream = System.err; // private fields public final int id; private List listenerConfigs = new LinkedList(); private Set listeners = new HashSet(); private WorkflowRunner runner; /** * Mapping between input field keys and output field keys. */ protected Map inputs = new HashMap(); protected String name; protected Map settings; protected Map configuration; protected ActorFSM state = ActorFSM.CONSTRUCTED; private List metadataWriters = null; private List metadataReaders = null; protected Logger logger = new SilentLogger(); protected ActorRef workflowRef; private WrappedMessage receivedWrappedMessage; private enum ActorFSM { CONSTRUCTED, BUILT, INITIALIZED, STARTED, ENDED } public KuratorActor() { synchronized(nextActorId) { this.id = nextActorId++; } this.metadataReaders = new LinkedList(); this.metadataReaders.add(new BroadcastEventCountChecker()); this.metadataWriters = new LinkedList(); this.metadataWriters.add(new BroadcastEventCounter()); } /** * Specifies the output stream to be used by an actor that needs * to write to stderr. The value is stored in {@link #errStream}. * *

Child classes should write error messages to {@link #errStream} instead of * writing to System.err directly.

* * @param errStream The PrintStream to use for writing to stderr. * @return this AkkaActor */ public synchronized KuratorActor errorStream(PrintStream errStream) { this.errStream = errStream; return this; } /** * Specifies the input stream to be used by an actor that needs * to read from stdin. The value is stored in {@link #inStream}. * *

Child classes should read input from {@link #inStream} instead of * reading from System.in directly.

* * @param inStream The InputStream to use for reading from stdin. * @return this AkkaActor */ public synchronized KuratorActor inputStream(InputStream inStream) { this.inStream = inStream; return this; } /** * Specifies the output stream to be used by an actor that needs * to write to stdout. The value is stored in {@link #outStream}. * *

Child classes should send output to {@link #outStream} instead of * writing to System.out directly.

* * @param outStream The PrintStream to use for writing to stdout. * @return this AkkaActor */ public synchronized KuratorActor outputStream(PrintStream outStream) { this.outStream = outStream; return this; } /** * Specifies the list of listeners for this actor in the current workflow. * *

The input parameter is given in terms of {@link org.kurator.akka.ActorConfig ActorConfig} * instances (rather than {@link akka.actor.ActorRef ActorRef} instances) * because the actors may not have been constructed yet. The ActorRef corresponding * to each ActorConfig is looked up and the list of listeners in terms of ActorRef * instances composed by {@link #onReceive(Object) onReceive()} when the * {@link org.kurator.akka.messages.Initialize Initialize} message is received.

* * @param listenerConfigs The list of actor configurations corresponding to this actor's listeners. * @return this AkkaActor */ public synchronized KuratorActor listeners(List listenerConfigs) { Contract.requires(state, ActorFSM.CONSTRUCTED); if (listenerConfigs != null) { this.listenerConfigs = listenerConfigs; } return this; } /** * Specifies the {@link org.kurator.akka.WorkflowRunner WorkflowRunner} for the current workflow. * *

The workflow runner is used for accessing the mapping of listener {@link org.kurator.akka.ActorConfig ActorConfig} * instances to {@link akka.actor.ActorRef ActorRef} instances, and for reporting exceptions to the runner.

* * @param runner The {@link org.kurator.akka.WorkflowRunner WorkflowRunner} that built and is currently * executing the workflow containing this actor. * @return this AkkaActor */ public synchronized KuratorActor runner(WorkflowRunner runner) { Contract.requires(state, ActorFSM.CONSTRUCTED); this.runner = runner; return this; } public synchronized void settings(Map settings) { Contract.requires(state, ActorFSM.CONSTRUCTED); this.settings = settings; } public synchronized KuratorActor inputs(Map inputs) { this.inputs = inputs; return this; } public synchronized KuratorActor setNeedsTrigger(boolean needsTrigger) { Contract.requires(state, ActorFSM.CONSTRUCTED); this.needsTrigger = needsTrigger; return this; } public synchronized KuratorActor metadataWriters(List metadataWriters) { if (this.metadataWriters == null) { this.metadataWriters = metadataWriters; } else { this.metadataWriters.addAll(metadataWriters); } return this; } public synchronized KuratorActor metadataReaders(List metadataReaders) { if (this.metadataReaders == null) { this.metadataReaders = metadataReaders; } else { this.metadataReaders.addAll(metadataReaders); } return this; } public synchronized KuratorActor configuration(Map configuration) { Contract.requires(state, ActorFSM.CONSTRUCTED); this.configuration = configuration; return this; } /** * Initial handler for all messages received by this actor via the Akka framework. * *

This method may not be overridden by child classes. Non-default responses to * messages can provided by overriding one or more of {@link #onInitialize()}, * {@link #onStart()}, {@link #onEndOfStream(EndOfStream) handleEndOfStream()}, * {@link #onEnd()}, and {@link #onData(Object) handleDataMessage()}.

* *

This method is responsible calling the more specific message and event handlers, and for * initializing the list of listeners using the listener configurations assigned * via the {@link #listeners(List) listeners()} method.

* *

This method catches exceptions thrown by overridden message and event handlers. If such * an exception is caught, the method reports the exception to the parent workflow via * {@link #reportException(Exception) reportException()}, * then stops the actor with a call to {@link #endStreamAndStop()} .

* * @param message The received message. * @throws Exception if any of the other message and event handlers throw one. */ @Override public synchronized final void onReceive(Object message) throws Exception { Contract.disallows(state, ActorFSM.ENDED); logger.trace("Received message of type " + message.getClass() + " from " + getSender()); logger.value("Received message: ", message.toString()); try { if (message instanceof WrappedMessage) { receivedWrappedMessage = (WrappedMessage)message; message = unwrapMessage(receivedWrappedMessage); } else { receivedWrappedMessage = null; } // handle control messages (subclasses of ControlMessage) if (message instanceof ControlMessage) { if (message instanceof Initialize) { name = (String) configuration.get("name"); this.logger.setSource(Log.ACTOR(name)); logger.comm("Received INITIALIZE message from WORKFLOW"); // workflowRef = getContext().parent(); Contract.requires(state, ActorFSM.CONSTRUCTED); // compose the list of listeners from the configured list of listener configurations for (ActorConfig listenerConfig : listenerConfigs) { ActorRef listener = runner.getActorForConfig(listenerConfig); listeners.add(listener); } // invoke the Initialize event handler try { logger.trace("Invoking ON_INITIALIZE_EVENT handler"); onInitialize(); } catch(Exception initializationException) { Object exceptionMessage = initializationException.getMessage(); if (exceptionMessage == null || ((String)exceptionMessage).isEmpty()) { exceptionMessage = initializationException.toString(); } List failures = new LinkedList(); failures.add(new Failure("Error initializing actor '" + name + "'")); failures.add(new Failure(exceptionMessage.toString())); getSender().tell(new Failure(failures), getSelf()); getContext().stop(getSelf()); return; } state = ActorFSM.INITIALIZED; // report success logger.comm("Sending INITIALIZE response to WORKFLOW"); getSender().tell(new Success(), getSelf()); } else if (message instanceof Start) { logger.comm("Received START message from WORKFLOW"); workflowRef = getSender(); Contract.requires(state, ActorFSM.INITIALIZED, ActorFSM.STARTED); if (state == ActorFSM.INITIALIZED) { state = ActorFSM.STARTED; logger.trace("Invoking ON_START_EVENT handler"); handleOnStart(); } } else if (message instanceof EndOfStream) { logger.comm("Received END_OF_STREAM message from " + Log.ACTOR(runner.name(getSender()))); Contract.requires(state, ActorFSM.INITIALIZED, ActorFSM.STARTED); logger.trace("Invoking ON_END_OF_STREAM_EVENT handler"); onEndOfStream((EndOfStream)message); } else if (message instanceof FutureComplete) { Contract.requires(state, ActorFSM.INITIALIZED, ActorFSM.STARTED); onFutureComplete((FutureComplete)message); } // all other messages are assumed to be data } else { logger.comm("Received DATA from " + Log.ACTOR(runner.name(getSender()))); logger.value("Received DATA", message); Contract.requires(state, ActorFSM.STARTED, ActorFSM.INITIALIZED); if (state == ActorFSM.INITIALIZED) { logger.trace("Invoking ON_START_EVENT handler"); state = ActorFSM.STARTED; handleOnStart(); } logger.trace("Invoking ON_DATA_EVENT handler"); onData(message); } } catch (Exception e) { reportException(e); errStream.println(e.getMessage()); endStreamAndStop(); } } private void handleOnStart() throws Exception { onStart(); if (this.needsTrigger) { logger.trace("Invoking ON_TRIGGER_EVENT handler"); onTrigger(); } } /** * Empty default handler for Initialize event. * Called when the actor receives a {@link org.kurator.akka.messages.Initialize Initialize} message. * *

This method can be overridden by child classes to perform any tasks that must occur before * the workflow begins to execute. A workflow begins executing (and a {@link org.kurator.akka.messages.Start Start} message * is sent to each actor) only after all actors in the workflow receive the * {@link org.kurator.akka.messages.Initialize Initialize} message and return from this handler.

* * @throws Exception If the actor implementation of onInitialize() method throws an exception. */ protected void onInitialize() throws Exception { logger.trace("Executing default ON_INITIALIZE_EVENT handler"); } /** * Empty default handler for Start event. * Called when the actor receives a {@link org.kurator.akka.messages.Start Start} message. * *

Can be overridden by children classes to perform any tasks that must occur once at the beginning * of a workflow run but after all actors have been initialized. Actors that handle the * {@link org.kurator.akka.messages.Start Start} * message can bootstrap the execution of a workflow by peforming computations and sending one or more * messages before receiving messages from other actors in the workflow.

* *

Note that if an actor is a listener of another actor in the workflow then it is not guaranteed * to receive the {@link org.kurator.akka.messages.Start Start} message before receiving messages from other actors. * This method thus is most useful when (a) an actor is not a listener, or (b) when the actor occurs * in a workflow cycle such that the messages it receives are produced in response to the messages that it sends.

* *

Note also that if an actor is not a listener of any other actor then it may delay returning from * this method until the actor has performed all of its activity for the workflow run. Thus, an actor serving * as a data source for a workflow may in some cases perform all of its work in an override of this method. * The {@link org.kurator.akka.actors.OneShot OneShot} actor provides a {@link org.kurator.akka.actors.OneShot#onStart * onStart()} implementation that formalizes this approach. *

* * @throws Exception If the actor implementation of onStart() method throws an exception. */ protected void onStart() throws Exception { logger.trace("Executing default ON_START_EVENT handler"); } protected void onTrigger() throws Exception { logger.trace("Executing default ON_TRIGGER_EVENT handler"); } /** * Default handler for {@link org.kurator.akka.messages.EndOfStream EndOfStream} message. * If the {@link #endOnEos} property is true, this method calls {@link #endStreamAndStop(EndOfStream)}. * *

This method can be overridden by child classes to provide an alternative response to receiving * an {@link org.kurator.akka.messages.EndOfStream EndOfStream} message.

* *

If {@link #endOnEos} is true and an actor simply needs to perform tasks before the * {@link org.kurator.akka.messages.EndOfStream EndOfStream} message is forwarded to listeners, * the {@link #onEnd onEnd()} method should be overridden instead.

* * @param eos The received {@link org.kurator.akka.messages.EndOfStream EndOfStream} message. * @throws Exception If the actor implementation of onTrigger() method throws and exception. */ protected void onEndOfStream(EndOfStream eos) throws Exception { logger.trace("Executing default ON_END_OF_STREAM_EVENT handler"); if (endOnEos) { endStreamAndStop(eos); } } /** * Empty default handler for End event. Called when the actor has stopped sending * messages to receivers and before the actor fully stops. * * @throws Exception If the actor implementation of onEnd() method throws an exception. */ protected void onEnd() throws Exception { logger.trace("Executing default ON_END_EVENT handler"); } protected void onFutureComplete(FutureComplete message) throws Exception { } /** * Empty default handler for incoming data messages. Called when the actor receives a message * that is not derived from {@link org.kurator.akka.messages.ControlMessage ControlMessage}. * *

Most actors will override this method to receive incoming data from other actors.

* * @param value The received data value. * @throws Exception If the actor implementation of onEnd() method throws an exception. */ protected void onData(Object value) throws Exception { logger.trace("Executing default ON_DATA_EVENT handler"); } private Object wrapMessage(Object message) { if (metadataWriters == null) { return message; } else { WrappedMessage wrappedMessage = new WrappedMessage(message); for (MetadataWriter mw : metadataWriters) { mw.writeMetadata(this, wrappedMessage); } return wrappedMessage; } } private Object unwrapMessage(WrappedMessage wrappedMessage) throws Exception { if (metadataReaders != null) { for (MetadataReader mr : metadataReaders) { mr.readMetadata(this, wrappedMessage); } } return wrappedMessage.unwrap(); } public synchronized WrappedMessage getReceivedWrappedMessage() { return receivedWrappedMessage; } /** * Sends a message to all of the the actor's listeners. * * @param message The message to send. */ protected synchronized final void broadcast(Object message) { Contract.requires(state, ActorFSM.INITIALIZED, ActorFSM.STARTED); Object wrappedMessage = wrapMessage(message); for (ActorRef listener : listeners) { if (listener != null) { logger.comm("Sending DATA to " + Log.ACTOR(runner.name(listener))); listener.tell(wrappedMessage, this.getSelf()); logger.value("Sent DATA", message); } } } /** * Stops the actor after (optionally) broadcasting the provided {@link org.kurator.akka.messages.EndOfStream EndOfStream} * message to listeners. It is called by {@link #onEndOfStream(EndOfStream) handleEndOfStream()} * on arrival of an {@link org.kurator.akka.messages.EndOfStream EndOfStream} message if * the {@link #endOnEos} property is true. * *

This method broadcasts the received {@link org.kurator.akka.messages.EndOfStream EndOfStream} * message (a new {@link org.kurator.akka.messages.EndOfStream EndOfStream} instance is created * if eos is null) to the actor's listeners if the {@link #sendEosOnEnd} * property is true. * The method then calls {@link #onEnd onEnd()} and terminates the actor. * * @param eos The {@link org.kurator.akka.messages.EndOfStream EndOfStream} message to broadcast to listeners. * Can be null (see above). * @throws Exception if {@link #onEnd onEnd()} throws an exception. */ protected final void endStreamAndStop(EndOfStream eos) throws Exception { // optionally send an EndOfStream message to listeners if (sendEosOnEnd) { broadcast(eos != null ? eos : new EndOfStream()); } // call the End event handler onEnd(); // stop the actor getContext().stop(getSelf()); state = ActorFSM.ENDED; } /** * Stops the actor after sending a new {@link org.kurator.akka.messages.EndOfStream EndOfStream} message to listeners. * *

Calling this method is the primary means of shutting down an actor if {@link #endOnEos} is false. *

* * @throws Exception if {@link #onEnd onEnd()} throws an exception. */ protected final void endStreamAndStop() throws Exception { endStreamAndStop(null); } /** * Used to report exceptions that are caught by this actor. * *

Child classes are expected to catch exceptions that occur while performing * computations in response to incoming messages. Exceptions that cannot be handled * silently should be reported via this method.

* *

Exceptions that are not caught (or are thrown) by overridden message and event * handlers are caught by {@link #onReceive(Object) onReceive()} which also reports the * exception to the parent workflow via this method. However, any exception caught by * {@link #onReceive(Object) onReceive()} also causes the actor to stop.

* * @param exception The reported exception. */ protected final void reportException(Exception exception) { logger.error(exception.getMessage()); ActorRef workflowRef = runner.getWorkflowRef(); ExceptionMessage em = new ExceptionMessage(exception); workflowRef.tell(em, this.getSelf()); } public KuratorActor logger(Logger logger) { this.logger = logger; return this; } protected void publishProducts(Map products) { if (products != null ) { logger.debug("Publishing " + products.size() + " products."); for(Map.Entry entry: products.entrySet()) { String label = (String) entry.getKey(); Object product = entry.getValue(); publishProduct(label, product); } logger.trace("Done publishing products"); } } protected void publishProduct(String label, Object product) { publishProduct(label, product, product.getClass().getName()); } protected void publishProduct(String label, Object product, String type) { WorkflowProduct ap = new WorkflowProduct(this.name, type, label, product); logger.value("Published product:", label, product); ProductPublication message = new ProductPublication(ap); logger.trace("Sending product to " + workflowRef); logger.comm("Sending value PUBLICATION_REQUEST message to WORKFLOW"); workflowRef.tell(message, getSelf()); } protected void publishArtifacts(Map artifacts) { if (artifacts != null ) { logger.debug("Publishing " + artifacts.size() + " artifacts."); for(Map.Entry entry: artifacts.entrySet()) { String label = (String) entry.getKey(); String artifact = (String)entry.getValue(); publishArtifact(label, artifact); } logger.trace("Done publishing artifacts"); } } protected void publishArtifact(String label, String pathToArtifact) { publishProduct(label, pathToArtifact, "File"); } protected void publishArtifact(String label, String pathToArtifact, String type) { WorkflowArtifact ap = new WorkflowArtifact(this.name, type, label, pathToArtifact); logger.value("Published artifact:", label, pathToArtifact); ProductPublication message = new ProductPublication(ap); logger.comm("Sending artifact PUBLICATION_REQUEST message to WORKFLOW"); workflowRef.tell(message, getSelf()); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy