Sequencer Technical Documentation
The Sequencer is OMOA component which has the responsibility of executing a sequence of Steps. In an observation, Sequencers will form a hierarchy with a top level ESW Sequencer sending Sequences to downstream Sequencers and downstream Sequencers sending commands to Assemblies/HCDs.
The Sequencer implementation has two main parts:
- Sequencer Framework
- Scripting Support
The Sequencer Framework uses an Akka Actor at its core and is responsible for executing the received Sequence and calling handlers in the Script. Sequencer Scripting Support defines the behaviour of the Sequencer while executing a Sequence. Scripts are written using a Domain Specific Language provided as a part of Framework.
Modules
-
esw-ocs-api - This is a cross-compiled module, which is compiled into JVM as well as JavaScript code (Note: The generated JavaScript code is currently not used anywhere). This module includes
SequencerApi
which defines an interface for Sequencer. This module also consists of core models, actor client, JVM and JavaScript client for Sequencer. -
esw-ocs-impl - This module consists of the core implementation of a Sequencer, the actor, which is
SequencerBehaviour
(Sequencer Actor), Engine and SequencerData. -
esw-ocs-app - This module consists of wiring as well as the command line application to start Sequencer. The wiring integrates a Sequencer into the rest of the ESW/CSW environment.
-
esw-ocs-dsl - This module consists of the Scala implementation supporting the Script DSL.
-
esw-ocs-dsl-kt - This module consists of the Kotlin counterpart of the Script DSL.
-
esw-ocs-handler - This handler module is responsible for providing HTTP routes for the Sequencer HTTP server. Sequencer provides an HTTP and Akka interface. The HTTP routes are defined and implemented here.
Sequence execution process
Starting a Sequencer
When we load a script from a sequence component, it creates a Sequencer Wiring. Sequencer Wiring passes the Kotlin script’s class name as string parameter to Script Loader, which uses Java reflection APIs to dynamically load the script class with the given name and create its instance. The loaded script is then passed to an execution Engine, which is responsible for processing each step. After initialization, the Sequencer’s Akka and HTTP connection is registered with the Location Service.
Loading and Running a sequence in Sequencer
- During initialization of a Sequencer, it is set to the IDLE state and initialized with empty data in Sequence Data, with an empty Step List,etc.
- Once initialized, a user can either Load and start a sequence or Submit a sequence. In both cases list of commands will be converted to the richer model of list of Steps.
- Load and Start a Sequence: Using
loadSequence
api,SequencerData
(described below) will be initialized with Sequence Steps. A user can then use thestartSequence
api to start the execution of steps. - Submit a Sequence: Using
submit
/submitAndWait
api,SequencerData
will be initialized with Sequence Steps, and start the sequence execution.
- Load and Start a Sequence: Using
SequencerData
has different fields as follows:
stepList
- This will store steps of the SequencerunId
- This is runId of the SequencesequenceResponseSubscribers
- This is a list of Subscribers who are interested in a response, either by submitting a sequence or by querying a response.- When a subscriber has submitted a sequence using the
submitAndWait
API, it will get a response once the Sequence is completed with Success or Failure - Other subscribers (who have not submitted the sequence), can also get the same response using the
queryFinal
API. For this they need to provide the runId of the Sequence.
- When a subscriber has submitted a sequence using the
Mapping between steps and script handlers
A Kotlin script file contains multiple command names and associated handler blocks.
onSetup("commandName") {
...handler block of command
}
When a Script is loaded, ScriptDsl stores all command names with respective handler code blocks. You can think of it as a map with command name as key and handler code block as its value.
The sequence submitted by a client to a sequencer contains a list of commands that need to be executed. These commands are stored as a richer model of steps. When a step is picked for execution by Engine, its corresponding code block is picked from a mapping in ScriptDsl and that block is executed.
Engine is a continuous running loop: It pulls the next step once the current step is finished.
Completion of a Sequence
Once every step is executed, it is marked as Finished with Success or Failure. If any of step is Failed, the Sequence is terminated and an Error response is sent to all Subscribers. If all steps are completed with Success, then a Success response is sent to all Subscribers.
Implementation Details
The Sequencer framework uses Akka Actor as the core implementation (Sequencer Actor). The following figure explains the architecture of the Sequencer framework. A Sequencer is registered with Location Service. The future SOSS Planning Tool or ESW.HCMS Script Monitoring Tool will use the Location of the top-level Sequencer returned by Sequence Manager to resolve the top-level Sequencer and will send the Observation’s Sequence to the top-level Sequencer.
Engine and Sequencer Actor are core parts of Framework. The framework part is the same for every Sequencer, but the Script can vary. The Script defines the behaviour of the Sequencer for each step within a Sequence.
The following sections explain the core components of a Sequencer:
- Sequencer Lifecycle
- Scripting Support
Sequencer Lifecycle
The Sequencer lifecycle is implemented as a fairly complicated finite state machine as shown in the figure below. This Section explains the different states and messages accepted in each respective state of Sequencer. At any given time a Sequencer is in exactly one of these states. The state of the Sequencer is tied to whether or not it has received a Sequence and whether the Sequence has started executing. Sequencer supports a set of commands/messages, and on receiving those commands, it takes an action and transitions to other states.
Following are the states supported by the Sequencer:
-
Idle/Online: This is the default state of the Sequencer. A Sequencer is idle when it is starts up. It has a Script since it has been loaded/created by the Sequence Component, but there is no Sequence under execution. A Sequencer can come to the idle state from the following situations:
-
when the Sequencer starts up for the first time with a Script loaded
- when the Sequencer has finished execution of a Sequence
- when the Sequencer was offline, and a goOnline command is received
In this state, the Sequencer can only receive a Sequence, goOffline
, or shutdown
, in which the Sequencer transitions to the Loaded
, Offline
, and Killed
states, respectively.
-
Loaded: A Sequencer is in loaded state when a Sequence is received and ready for execution, but execution of the Sequence hasn’t started. A separate
start
command is expected to start execution of the Sequence. All sequence editor actions (for e.g. add, remove, reset) are accepted in this state. From this state, the Sequencer can go to theRunning
state on receiving astart
command, or it could go to theOffline
state ifgoOffline
command is sent. On receiving areset
command, which discards all the pending steps, the Sequencer will go toIdle
state. -
InProgress/Running: The Sequencer is in the
Running
state only when it is executing a Sequence. All sequence editor actions (for e.g. add, remove, reset) are accepted in this state. From theRunning
state, the Sequencer can go toIdle
state on completion of a Sequence, or it can beshutdown
. A Sequencer cannot goOffline
from this state; the Sequencer must first to go to theIdle
state and thenOffline
. -
Offline: The Sequencer goes to the
Offline
state only on receiving agoOffline
command, which can either come from an upstream Sequencer, or from a user through the admin dashboard. In this state, only a few commands are excepted (for eg. goOnline, shutdown, status etc). -
Killed: This is the final state of the Sequencer, achieved when receiving a
shutdown
command. The shutdown command can be sent in any state, hence a Sequencer can transition to this state from any other state. However, a Sequencer doesn’t stay in this state long; when a Sequencer is killed, it is removed from the Location Service, and ActorSystem is shutdown, effectively destroying the Sequencer.
Scripting Support
Sequencer Scripts are the most important part of the Sequencer architecture. The scripting environment has following core requirements:
Domain Specific Language (DSL) constructs for writing Scripts. For example, par
to execute commands in parallel, onSetup
like constructs where the script writer will define logic to be executed when Setup
steps are processed. Kotlin has been used to create the DSL for writing a Script. Kotlin has excellent language support for writing an embedded DSL. Kotlin also has excellent support for asynchronous processing/tasks that allows a more script-like syntax for the kinds of things ESW Scripts need to do.
Mutable State within the script To handle mutable state in thread safe manner, Script is implemented using the Active Object Pattern. Every operation in a Script needs to be asynchronous and non-blocking in nature, and each operation will be scheduled on Single Threaded Execution Context (StrandEC). This ensures that state inside the Script can be accessed/modified at any place inside with the guarantee of thread safety. If there is need to have CPU intensive or blocking operations in Script, patterns supporting these needs to be followed which uses another Execution Context so that Script StrandEC is not blocked. The scripting DSL provides special constructs for background processing.
Scripting Support is implemented using Kotlin. onSetup
and onObserve
handlers are provided which will be used by script writers to write behaviour when Setup
and Observe
commands are received. Specific onSetup
handler will be picked based on command name specified in handler. For more details about scripting please refer here
Sequencer Interfaces
Sequencer exposes its interface in three ways:
- Akka interface - Sequencer is registered as an Akka-based component. One can resolve Sequencer and use the Akka client to interact with Sequencer.
- HTTP direct interface - Each Sequencer also exposes an HTTP-based interface as an embedded Sequencer Server (direct and unprotected usage). This access provides routes that allow user to directly control the Sequencer without any auth protection. UI applications are supposed to use Gateway interface described below to interact with Sequencer as Gateway provided auth protection layer.
- HTTP Gateway interface - It is also possible to interact with Sequencer using the UI Application Gateway (as outside network interface). Being outside network interface, this access requires user to be authenticated and authorized. The Gateway hosts the Sequencer API, which communicates with the Sequencer via the Akka interface. Please refer to the Gateway documentation for more information.
Following snippet shows instantiating Akka Interface to interact with Sequencer:
- Scala
-
source
private implicit val actorSystem: ActorSystem[SpawnProtocol.Command] = ActorSystemFactory.remote(SpawnProtocol(), "example") private val sequencerAkkaConnection: AkkaConnection = AkkaConnection(ComponentId(Prefix(ESW, "IRIS_DARKNIGHT"), Sequencer)) private val locationService: LocationService = HttpLocationServiceFactory.makeLocalClient(actorSystem) private val sequencerAkkaLocation: AkkaLocation = Await.result(locationService.resolve(sequencerAkkaConnection, 10.seconds), 10.seconds).get private val sequencer: SequencerApi = SequencerApiFactory.make(sequencerAkkaLocation)
Following snippet shows instantiating HTTP direct Interface to interact with Sequencer:
- Scala
-
source
private val sequencerHttpConnection: HttpConnection = HttpConnection(ComponentId(Prefix(ESW, "IRIS_DARKNIGHT"), Sequencer)) private val sequencerHttpLocation: HttpLocation = Await.result(locationService.resolve(sequencerHttpConnection, 10.seconds), 10.seconds).get private val sequencerHttpClient: SequencerApi = SequencerApiFactory.make(sequencerHttpLocation) sequencerHttpClient.getSequence
For interacting using HTTP Gateway interface, please refer here
Sequencer Http direct interface is not supposed to be used from anywhere as it is unprotected and also bind to inside network ip.
Interacting with Sequencer
One can use Akka Interface or HTTP Gateway interface to interact with Sequencer. APIs to interact with Sequencer are broadly categorised as following.
- Sequencer Command Service - Provided as a part of CSW. Provides way to submit sequence and receive response.
- Sequence Editor APIs - Provided as a part of ESW. Provided way to edit sequence submitted to Sequencer.
- Sequencer Lifecycle APIs - Provided as a part of ESW. Provided way to send lifecycle commands to Sequencer.
- Other APIs - Provided as a part of ESW.
Sequencer Command Service
Commands can be sent to Sequencer to submit sequence and response is received in return.
Sequencer Interface exposes APIs on top of Sequencer Command Service. Sequencer Command Service provides way to submit sequence to Sequencer and receive started or final response. Sequencer Command Service is provided as a part of CSW and details about using Sequencer Command Service can be found here.
Sequence Editor APIs
Sequence Editor APIs allow actions to edit sequence such as add more steps, delete/replace existing steps, Add/remove breakpoint in sequence. For using Sequence Editor actions, sequencer must be running a sequence. If Sequencer is not running any sequence then, Sequencer will return Unhandled
response.
- add
This API allows to add more steps to sequence. Steps will be added in the end of sequence.
- Scala
-
source
val stepsToAdd: List[SequenceCommand] = List( Setup(Prefix(ESW, "filter.wheel"), CommandName("setup-iris")), Setup(Prefix(ESW, "filter.wheel"), CommandName("setup-tcs")) ) sequencer.add(stepsToAdd)
- prepend
This API allows to add more steps to sequence. Steps will be added after currently running step of sequence.
- Scala
-
source
val stepsToPrepend: List[SequenceCommand] = List( Setup(Prefix(ESW, "filter.wheel"), CommandName("setup-iris")), Setup(Prefix(ESW, "filter.wheel"), CommandName("setup-tcs")) ) sequencer.prepend(stepsToPrepend)
- getSequence
This API allows returns Sequence running in Sequencer if any.
- Scala
-
source
private val stepList: StepList = Await.result(sequencer.getSequence, 1.seconds).get
- replace
This API allows to replace particular step in the sequence with more steps.
- Scala
-
source
val stepsToReplace: List[SequenceCommand] = List( Setup(Prefix(ESW, "filter.wheel"), CommandName("setup-iris")), Setup(Prefix(ESW, "filter.wheel"), CommandName("setup-tcs")) ) sequencer.replace(stepList.steps(4).id, stepsToReplace)
- insertAfter
This API allows to insert more steps after particular step in the sequence.
- Scala
-
source
val stepsToInsertAfter: List[SequenceCommand] = List( Setup(Prefix(ESW, "filter.wheel"), CommandName("setup-iris")), Setup(Prefix(ESW, "filter.wheel"), CommandName("setup-tcs")) ) sequencer.insertAfter(stepList.steps(4).id, stepsToInsertAfter)
- delete
This API allows to delete particular step in the sequence.
- Scala
-
source
private val stepToDelete: Id = stepList.steps(4).id sequencer.delete(stepToDelete)
- add and remove breakpoint
These APIs allows to add and remove breakpoint for particular step in the sequence.
- Scala
-
source
private val breakpointStep: Id = stepList.steps(4).id sequencer.addBreakpoint(breakpointStep) sequencer.removeBreakpoint(breakpointStep)
- reset
These APIs allows to discard all pending steps in the sequence.
- Scala
-
source
sequencer.reset()
- pause and resume sequence
These APIs allows to pause and resume sequence. This essentially adds/removes breakpoint at first pending step in sequence
- Scala
-
source
sequencer.pause sequencer.resume
Sequencer Lifecycle APIs
Sequencer Lifecycle APIs allow to send lifecycle commands to Sequencer such as goOnline, abortSequence etc.
Certain commands are restricted depending on state of Sequencer. For example, goOnline command is handled only when Sequencer is in Offline state. If goOnline is sent otherwise it will return Unhandled
response with error msg. For details refer Sequencer Lifecycle Section
- isAvailable
This API allows to check if Sequencer is in Idle state or not. It returns true if Sequencer is in Idle state.
- Scala
-
source
sequencer.isAvailable
- online/offline
These APIs allow to send goOnline/goOffline mode commands to Sequencer. isOnline
command returns true if Sequencer is online.
- Scala
-
source
sequencer.isOnline sequencer.goOnline() sequencer.goOffline()
- abortSequence
This API allow to abort running sequence. This essentially discards pending steps from sequence and also call onAbortSequence
handler written in script.
- Scala
-
source
sequencer.abortSequence()
- Stop
This API allow to stop sequence. This essentially discards pending steps from sequence and also call onStop
handler written in script.
- Scala
-
source
sequencer.stop()
- getSequencerState
Sequencer is implememted as state machine. It accepts/discards msgs based on Sequencer State. This API allow returns current sequencer state.
- Scala
-
source
sequencer.getSequencerState
- diagnosticMode
This API allow to send diagnosticMode command to Sequencer. This calls onDiagnosticMode
handler written in script
- Scala
-
source
sequencer.diagnosticMode(UTCTime.now(), "diagnostic-mode")
- operationsMode
This API allow to send operationsMode command to Sequencer. This calls onOperationsMode
handler written in script
- Scala
-
source
sequencer.operationsMode()
- subscribeSequencerState
This API allows subscribing to state of Sequencer. It returns a Source of SequencerStateResponse
which contains current SequencerState
and StepList
. The Subscription
can be used to unsubscribe.
- Scala
-
source
val sequencerStateSource: Source[SequencerStateResponse, Subscription] = sequencer.subscribeSequencerState()
Other APIs
- loadSequence
This API allows to load sequence in Sequencer. Loaded Sequence does not start execution unless StartSequence
Command is received. One can replace already loaded sequence by firing another loadSequence
command.
- Scala
-
source
val sequence: Sequence = Sequence( Setup(Prefix(ESW, "filter.wheel"), CommandName("setup-iris")), Setup(Prefix(ESW, "filter.wheel"), CommandName("setup-tcs")) ) sequencer.loadSequence(sequence)
- startSequence
This API allows to start execution of previously loaded sequence in Sequencer. This return SubmitResponse
which is Started
in case of success.
- Scala
-
source
sequencer.startSequence()
- getSequenceComponent
This API allows to get location of Sequence Component running the Sequencer.
- Scala
-
source
sequencer.getSequenceComponent sequencer.getSequenceComponent
Running Sequencer
For running Sequencer Script, please refer this.