Creating a Component
This walk-through helps in creating a CSW component in Scala/Java. CSW components depend on the csw-framework
package, which can be found here. This section discusses constructing an HCD, but the principles apply to an Assembly as well. We will be constructing the Assembly in the next section Working with Multiple Components.
Tutorial: Developing an HCD
This tutorial shows code written in Scala and Java, based on code generated by the giter8 templates with the default values.
Most of CSW is written using asynchronous programming. Therefore, in the examples, you may find constructs that deal with Futures and other asynchronous code in various ways. The following constructs are commonly used throughout this manual:
-
Scala: The Scala async package is used extensively.
async
marks a block of asynchronous code and allows toawait
the computation until the Future is complete. For more info, please refer to: https://github.com/scala/scala-async.Sometimes, an example may use the
Await.result
construct. While this is often used in tests, it is a blocking call and typically should not be used in production code. -
Java non-blocking example: The code snippets use
CompletableFuture
and itsthenAsync
,thenApply
methods. This style allows to compose multiple Futures and not block the calling thread until Futures are complete. -
Java blocking example: The code snippets use
CompletableFuture
usingget
blocking call. This style also blocks the calling thread until the Future is complete, and it should only be prudently used.
Anatomy of Component
A component consists of a Supervisor actor, a Top Level Actor (TLA) that provides component handlers, and one or more worker actors. The csw-framework
provides the Supervisor actor, the Top Level Actor and an abstract class of handlers. Component developers are expected to implement these handlers, which collectively act as the gateway from the framework to the developer’s component code.
Supervisor
A Supervisor actor is the actor first started for any component. The main responsibilities that the Supervisor performs is as follows:
- Creation of the TLA when the component starts up
- Implement and manage the component lifecycle for the TLA and for the component (see Lifecycle below).
- Register the component with the Location Service.
- Provide an administrative interface to the component to the rest of the system. For instance, the Container can perform some administrative communication with the Supervisor such as restart or shutdown of the component.
- Allow components outside of the Supervisor and TLA to monitor the lifecycle state of the TLA. This is particularly useful for testing, when the test needs to know that the component is ready before performing its test actions.
- Supports the locking functionality for the component (see Locking)
- Receives external commands and passes them to the correct component handlers.
The source code of the Supervisor actor can be found here
Top Level Actor
While the Supervisor works as the external interface for the component and the manager of its lifecycle, the functional implementation of a component is implemented in a Top Level Actor (TLA), spawned by the Supervisor actor for each component. However, the developer is not expected to implement a TLA code entirely. Instead, the component-specific functionality of the TLA is added by implementing the ComponentHandlers
abstract class, consisting of a set of methods, or hooks
, called by the TLA during specific lifecycle and command events (see Handlers). The ComponentHandlers
implementation is specified during construction using a factory (see Constructing The Component)
The source code of the Top Level Actor can be found here.
Handlers
The following hooks may be overridden in your ComponentHandlers implementation class:
initialize
: called when the component is starting up, prior to be put into the Running state.validateCommand
: called when the component receives a command to determine if the command is valid and can be executed. (see Validation)onSubmit
: called on Submit command if validateCommand returnsAccepted
.onOneway
: called on Oneway command if validateCommand returnsAccepted
.onGoOffline
: called when the component receives an external message from an administrative client to go offline.onGoOnline
: called when the component receives an external message from an administrative client to go online.onDiagnosticMode
: called when the component receives an external message from a client to enter a specified diagnostic behavior.onOperationsMode
: called when the component receives an external message from a client to exit any diagnostic behavior and return to normal, operations behavior.onLocationTrackingEvent
: called when a tracked dependency changes location state. (see Tracking Dependencies)onShutdown
: called when the component is shutting down.
The source code of ComponentHandlers
can be found here.
More details about handler significance and invocation can be found here
If the component developer wishes to write the component handler implementation in Java, they need to implement the Java version of ComponentHandlers
called JComponentHandlers
. The source code of JComponentHandlers
can be found here. Any further reference to ComponentHandlers
should be inferred as also applying to JComponentHandlers
.
Tutorial: Developing an HCD
As seen in the Getting Started page, if you are using the giter8 template, component handler classes for both the HCD and Assembly are written for you, with implementations stubbed out. We will walk-through filling them in below.
Constructing the Component
After writing the component handlers, a developer needs to wire it up with the framework. In order to do this, the developer needs to specify handlers full class path in a ComponentInfo configuration file for the component (see example below). The csw-framework
picks up the full class path of the ComponentHandlers
from the file and the Supervisor spawns the component using these handlers in the process of booting the component. The factory is instantiated using Java reflection.
Additional sample code to implement the ComponentHandlers
can be found here
Tutorial: Developing an HCD
As seen in the Getting Started page, if using the template, this factory class will be implemented for you.
Component Configuration (ComponentInfo)
The component configuration, called the ComponentInfo file, contains details needed to create a component. This configuration defines a few parameters needed for a particular component. The template creates one for our sample HCD as follows:
- Scala
-
source
prefix = "csw.samplehcd" componentType = hcd componentHandlerClassName= "org.tmt.csw.samplehcd.SampleHcdHandlers" locationServiceUsage = RegisterOnly
- Java
-
source
prefix = "csw.samplehcd" componentType = hcd componentHandlerClassName = "org.tmt.csw.samplehcd.JSampleHcdHandlers" locationServiceUsage = RegisterOnly
componentHandlerClassName
refers to class name of the a concrete implementation of ComponentHandlers
, which is SampleHcdHandlers
for Scala in above example, JSampleHcdHandlers
for Java.
The prefix
and componentType
are used to create the ComponentId
identifier, which must be unique within the control system and the Location Service. The prefix
must begin with a valid TMT subsystem, which establishes a scope for the component name. The list of valid subsystems is here.
The locationServiceUsage
is used by the Supervisor actor to decide whether to only register a component with the Location Service or to register and track other components. It is also possible to choose not register the component with the Location Service.
The ComponentInfo file is parsed to a ComponentInfo
object and injected in the Supervisor actor. It is then injected in ComponentHandlers
while spawning a component and the contents is available for the developer to access.
ComponentInfo
includes the Prefix
for the component. Developers should try to use this prefix value rather than defining a new one to reduce errors.
The ComponentInfo file can also contain a list of components and services it wishes to track as dependencies. See Tracking Dependencies.
More details about ComponentInfo
can be found here.
An additional sample configuration file can be found here.
Lifecycle
The Supervisor of a component manages its lifecycle state, which can be one of the following:
- Idle
- Running
- RunningOffline
- Restart
- Shutdown
- Lock
The state the component is in dictates the actions it can take when it receives a message or command, and whether those actions are carried out.
Idle
The component initializes in the idle state. The Top Level Actor calls the initialize
hook of ComponentHandlers
as the first thing on boot-up. Component developers write their initialization logic in this hook. The logic can also do things like accessing the Configuration Service to fetch information such as hardware configurations to set the hardware to default positions.
After initialization, if the component would have configured RegisterAndTrack
for locationServiceUsage
, then the Top Level Actor will start tracking the connections
configured for that component. This use case is mostly applicable for Sequencers and Assemblies. HCDs should have RegisterOnly
configured for locationServiceUsage
in most all cases.
If initialize
is successful, the Supervisor will register the component with the Location Service. Registering with the Location Service will notify other components tracking this component with a LocationUpdated
event containing a Location
with a reference to the Supervisor of the component.
After successful registration, the component will transition to the Running
state.
Running
When the Supervisor actor receives Initialized
message from the Top Level Actor after successful initialization, it registers itself with the Location Service and transitions the component to the Running
state. Running state signifies that the component is accessible via the Location Service, which allows other entities to communicate with it by sending commands via messages. Any commands received by the Supervisor actor will be forwarded to the Top Level Actor for processing once the component is in the Running
state.
A component should be ready for operation after successfully leaving the initialize
handler and entering Running
. A component should ready to process any command after initialization and must not require specific commands to be issued by users in order to become ready.
RunningOffline
When the Supervisor actor receives a GoOffline
message, it transitions the component to the RunningOffline
state and forwards it to the Top Level Actor. The Top Level Actor then calls the onGoOffline
hook of ComponentHandlers
.
If a GoOnline
message is received by the Supervisor actor, it transitions the component back to the Running
state and forwards it to the Top Level Actor. The Top Level Actor then calls the onGoOnline
hook of ComponentHandlers
.
In the RunningOffline
state, if any command is received, it is forwarded to the underlying component hook through the Top Level Actor. It is then the responsibility of the component developer to check the isOnline
flag provided by csw-framework
and process the command according to whether the command is appropriate for the command when offline.
Restart
When the Supervisor actor receives a Restart
message, it will transition the component to the Restart
state. Then, it will unregister itself from the Location Service so that other components tracking this component will be notified and no commands are received while restart is in progress.
Then, the Top Level Actor is stopped and the postStop
hook of the Top Level Actor will call the onShutdown
hook of ComponentHandlers
. Component developers are expected to write any cleanup of resources or other logic that should be executed for the graceful shutdown of the component.
After successful shutdown of component, the Supervisor actor will re-create the Top Level Actor again from scratch. This will cause the initialize
hook of ComponentHandlers
to be called again. After successful initialization of the component, the Supervisor actor will register itself with the Location Service once more.
Shutdown
When the Supervisor actor receives a Shutdown
message, it transitions the component to the Shutdown
state. Any commands received while a shutdown is in progress will be ignored. Then, it will stop the Top Level Actor. The postStop
hook of the Top Level Actor will call the onShutdown
hook of ComponentHandlers
. Component developers are expected to write any cleanup of resources or other logic that should be executed for the graceful shutdown of component. The component, including the Supervisor, exits after onShutdown
completes.
Lock
When the Supervisor actor receives a Lock
message, it transitions the component to the Lock
state. When locked, the Supervisor will only accept commands received from the component that originally locked the component and ignore commands from all others.
In the Lock
state, messages like Shutdown
and Restart
will also be ignored. A component must first be unlocked to accept these commands.
Lock
messages are constructed with a duration value specified. When this duration expires, the component will automatically be unlocked. In order to retain the Lock on the component, sender of the orginal Lock must resend the Lock
message.
There are two ways component can be unlocked:
- Sending
Unlock
message (Note: This message should be sent by the same component that locked the component.) - Sending
Unlock
message with an administrative Prefix.
CSW Services Injection
Common Software provides a set of CSW Services provided through the TLA. They are injected into the ComponentHandlers
class in the constructor in a CswContext
object. This object provides the following services through their respective APIs:
- Location Service
- Event Service
- Alarm Service (Client API)
- Time Service (Scheduler)
- Configuration Service (Client API)
- Logging Service (Logger Factory)
And the following information and support utilities:
- Component Configuration (ComponentInfo)
- Command Service Command Response Manager
- Current State Publisher Actor (intended for HCDs)
Logging
csw-framework
provides a LoggerFactory
in the CswContext
injected in the constructor of ComponentHandlers
. The LoggerFactory
will have the component’s Prefix predefined so long messages have a clear source. The component developer is expected to and must use this factory to log messages that work with the centralized logging facility.
Logging works much like other popular loggers such as log4j. However, with the development of log management tools such as logstash, the emphasis on log message formatting has been to write structured logging messages in JSON format, so that they can easily be ingested, stored, and searched. Plain text writing to stdout is also supported.
More details on how to use logging can be found here.
Tutorial: Developing an HCD
Let’s use logging to flesh out some of our command handlers. The template will instantiate a logger for you to use by constructing one from the LoggerFactory
from in the CswContext
passed in the constructor, instantiated as a log
object.
Add some simple log messages in the initialize
and onShutdown
hooks, and to the onLocationTrackingEvent
hook as well, although we won’t be using it for this HCD:
- Scala
-
source
var maybePublishingGenerator: Option[Cancellable] = None override def initialize(): Unit = { log.info("In HCD initialize") maybePublishingGenerator = Some(publishCounter()) } override def onLocationTrackingEvent(trackingEvent: TrackingEvent): Unit = { log.debug(s"TrackingEvent received: ${trackingEvent.connection.name}") } override def onShutdown(): Unit = { log.info("HCD is shutting down") }
- Java
-
source
private Optional<Cancellable> maybePublishingGenerator = Optional.empty(); @Override public void initialize() { log.info("In HCD initialize"); maybePublishingGenerator = Optional.of(publishCounter()); } @Override public void onLocationTrackingEvent(TrackingEvent trackingEvent) { log.debug(() -> "TrackingEvent received: " + trackingEvent.connection().name()); } @Override public void onShutdown() { log.info("HCD is shutting down"); }
In the example code, you’ll notice we have added some functionality to start publishing events. We will cover the Event Service later. You can leave that code out for now.
Next we’ll discuss handling commands.
Receiving Commands
A command is something that carries some metadata and a set of parameters. A component sends commands
to other components to execute actions. CSW defines three kinds of commands as follows:
- Setup : Contains goal, command, or demand information to be used to configure the target OMOA component.
- Observe: Contains goal or demand information to be used by a detector. system. Properties and their value types will be standardized by the ESW subsystem.
- Wait: Sequencer only. Instructs a sequencer to pause until told to continue.
A Sequencer receives a Sequence, which is a list of the above commands that are executed sequentially.
More details about creating commands can be found here.
Whenever a command is sent to a component, it is sent using a Command Service method. There are two general ways to send a command:
- submit: A command is sent using submit when a completion result is expected from the destination component. There are two options:
submit
andsubmitAndWait
. - oneway: A command is sent using oneway when the completion of command is not expected from the destination component.
Validation
When any command is received by a component using submit
or oneway
, the Top Level Actor will first call the validateCommand
hook of ComponentHandlers
. Component developers are expected to perform appropriate validation of a command to determine if it is valid to execute the requested actions and then return a ValidateCommandResponse
. The ValidateCommandResponse
returned from this handler will be returned to the sender directly by csw-framework
if the command fails validation.
The component developer should return either an Accepted
response or an or Invalid
response specifying whether the command is valid to be executed or not. CSW defines a set of CommandIssues
for use in validation here that should be used within Invalid
responses.
If the handler is being called as part of a submit
or oneway
call, the command will automatically be passed on to the onSubmit
or onOneway
handlers (see Command Response) only if the validation handler returns a ValidationCommandResponse
of Accepted
. Otherwise, the Invalid
response is returned to the caller immediately.
Different types of command responses and their significance can be found here.
Tutorial: Developing an HCD
Let’s add some command validation to our HCD. For our sample HCD, we will only handle one command, sleep
, in which we will cause the HCD to sleep for the time specified in a parameter of the command. This will simulate a long-running command.
Add some code to ensure the command we receive is the sleep
command, and return an Invalid
response if not. You could imagine much more checking could be added, such as checking the types and values of the parameters of our sleep
command, but we will keep it simple for our demonstration.
- Scala
-
source
override def validateCommand(runId: Id, controlCommand: ControlCommand): ValidateCommandResponse = { log.info(s"Validating command: ${controlCommand.commandName.name}") controlCommand.commandName.name match { case "sleep" => Accepted(runId) case x => Invalid(runId, CommandIssue.UnsupportedCommandIssue(s"Command $x. not supported.")) } }
- Java
-
source
@Override public CommandResponse.ValidateCommandResponse validateCommand(Id runId, ControlCommand controlCommand) { String commandName = controlCommand.commandName().name(); log.info(() -> "Validating command: " + commandName); if (commandName.equals("sleep")) { return new CommandResponse.Accepted(runId); } return new CommandResponse.Invalid(runId, new CommandIssue.UnsupportedCommandIssue("Command " + commandName + ". not supported.")); }
Command Response
The response returned from validateCommand
handler of ComponentHandlers
will be received by the Supervisor. If the response returned was Accepted
, then it either calls the onSubmit
hook or the onOneway
hook of ComponentHandlers
depending on the whether submit, submitAndWait, or oneway was used to send the command.
If sent with submit
or submitAndWait
and the validation response is Accepted
, the framework calls the onSubmit
hook of ComponentHandlers
. The return value of onSubmit
is then returned to the sender, which can be a Completed
for commands that return quickly, or Started
for long running commands. If the response from validation was Invalid
, this is returned to the sender of the command without calling the onSubmit
or onOneway
handler.
If onSubmit
returns Started
, the component’s CommandResponseManager
keeps track of the status of long-running submit commands. The sender of a Started
command (and any component, really) can query a Started
command’s status or wait for the final response using queryFinal
of the CommandService
API.
If a command is sent using oneway
, the validation handler if called first. The onOneway
handler of ComponentHandlers
is also called when the validation result is Accepted
.
The validation response is always sent back to the sender as the response for a oneway
command. There is no final response from a ownway
command and no way to wait for it using query
or queryFinal
.
The CommandService
class provides convenient API methods for communicating with other components, and should be the primary means of sending commands to other components. This will be described in the next tutorial section, Sending Commands.
When the onSubmit
hook is called and Started
is returned, it is the responsibility of the component developer to update the sender with the final status of the Started
command when the actions complete using the CommandResponseManager
API. An instance of CommandResponseManager
is provided in the CswContext
object in ComponentHandlers
and should be injected in any worker actor or other actor/class created for the component that needs it.
More details on the methods available in CommandResponseManager
can be found here.
Tutorial: Developing an HCD
We will implement command handling in the onSubmit
hook. Note that this hook actually receives a ControlCommand
as an argument, which can be either a Setup
or an Observe
. We will use pattern matching to handle the command if it is a Setup
and forward to an onSetup
handling method. Observe
commands will be ignored and returned with as Invalid.
- Scala
-
source
override def onSubmit(runId: Id, controlCommand: ControlCommand): SubmitResponse = { log.info(s"Handling command: ${controlCommand.commandName}") controlCommand match { case setupCommand: Setup => onSetup(runId, setupCommand) case observeCommand: Observe => // implement (or not) Error(runId, "Observe not supported") } } def onSetup(runId: Id, setup: Setup): SubmitResponse = { val sleepTimeKey: Key[Long] = KeyType.LongKey.make("SleepTime") // get param from the Parameter Set in the Setup val sleepTimeParam: Parameter[Long] = setup(sleepTimeKey) // values of parameters are arrays. Get the first one (the only one in our case) using `head` method available as a convenience method on `Parameter`. val sleepTimeInMillis: Long = sleepTimeParam.head log.info(s"command payload: ${sleepTimeParam.keyName} = $sleepTimeInMillis") workerActor ! Sleep(runId, sleepTimeInMillis) Started(runId) }
- Java
-
source
@Override public CommandResponse.SubmitResponse onSubmit(Id runId, ControlCommand controlCommand) { log.info(() -> "Handling command: " + controlCommand.commandName()); if (controlCommand instanceof Setup) { onSetup(runId, (Setup) controlCommand); return new CommandResponse.Started(runId); } else if (controlCommand instanceof Observe) { // implement (or not) } return new CommandResponse.Error(runId, "Observe command not supported"); } private void onSetup(Id runId, Setup setup) { Key<Long> sleepTimeKey = JKeyType.LongKey().make("SleepTime", JUnits.millisecond); // get param from the Parameter Set in the Setup Optional<Parameter<Long>> sleepTimeParamOption = setup.jGet(sleepTimeKey); // values of parameters are arrays. Get the first one (the only one in our case) using `head` method available as a convenience method on `Parameter`. if (sleepTimeParamOption.isPresent()) { Parameter<Long> sleepTimeParam = sleepTimeParamOption.orElseThrow(); long sleepTimeInMillis = sleepTimeParam.head(); log.info(() -> "command payload: " + sleepTimeParam.keyName() + " = " + sleepTimeInMillis); workerActor.tell(new Sleep(runId, sleepTimeInMillis)); } }
In our example, the sleep
command has one parameter called SleepTime
. We retrieve this parameter from the Setup
by creating a Key
to this parameter using the name and type, and then calling an apply
method on the Setup
(the setup(sleepKey)
shorthand) which finds the matching Parameter
in the Setup
’s ParameterSet
(use the Setup.jget()
method in Java). By doing this, the Parameter
is returned with the proper typing, and so the values retrieved from the Parameter
are typed as well. Note, all values are stored as an array, so we get our single value for sleepTime
by using the head
method available as a convenience method on ParameterSet
.
At this point, to prevent our HCD from blocking and simulate a long-running command, we pass the sleep function off to a worker actor, which we will specify elsewhere in this class.
The worker actor can be defined in a separate class, but writing it as an internal class allows us to use the logging facility and CommandResponseManager
without having to inject them into a new actor class. Additional versions of the tutorial code show a separate, enhanced worker actor class.
Note that our onSetup
command handling logic returns a Started
response. This indicates that the command is a long-running command and will be finished after some time, and that the final result will be posted to the CommandResponseManager
. The submitAndWait
command in the CommandService is implemented such that when it receives a Started
response, it automatically issues a queryFinal
to the commanded component to await the final completion response. When the command is updated using the CommandResponseManager
, the Future returned by submitAndWait
command is completed with this value. Note that in this code, there is a chance that there is no sleepTime parameter. Good validation code would ensure this so onSetup
does not need to worry. Enhanced versions of the tutorial code show this.
- Scala
-
source
sealed trait WorkerCommand case class Sleep(runId: Id, timeInMillis: Long) extends WorkerCommand private val workerActor = ctx.spawn( Behaviors.receiveMessage[WorkerCommand](msg => { msg match { case sleep: Sleep => log.trace(s"WorkerActor received sleep command with time of ${sleep.timeInMillis} ms") // simulate long running command val when: UTCTime = UTCTime.after(FiniteDuration(sleep.timeInMillis, MILLISECONDS)) timeServiceScheduler.scheduleOnce(when) { commandResponseManager.updateCommand(CommandResponse.Completed(sleep.runId)) } case _ => log.error("Unsupported message type") } Behaviors.same }), "WorkerActor" )
- Java
-
source
private interface WorkerCommand { } private static final class Sleep implements WorkerCommand { private final Id runId; private final long timeInMillis; private Sleep(Id runId, long timeInMillis) { this.runId = runId; this.timeInMillis = timeInMillis; } } private ActorRef<WorkerCommand> createWorkerActor() { return actorContext.spawn( Behaviors.receiveMessage(msg -> { if (msg instanceof Sleep) { Sleep sleep = (Sleep) msg; log.trace(() -> "WorkerActor received sleep command with time of " + sleep.timeInMillis + " ms"); UTCTime when = UTCTime.after(new FiniteDuration(sleep.timeInMillis, MILLISECONDS)); Runnable task = () -> cswCtx.commandResponseManager().updateCommand(new CommandResponse.Completed(sleep.runId)); // simulate long running command that updates CRM when completed cswCtx.timeServiceScheduler().scheduleOnce(when, task); } else { log.error("Unsupported message type"); } return Behaviors.same(); }), "WorkerActor" ); }
This worker actor takes the time passed in the message and sleeps that amount using the TimeService scheduling API. When the time is expired, the worker updates the CommandResponseManager
that the command is Completed
.
Events
CSW Events
have a similar structure to commands in that along with a name and a prefix (used to represent the source of the event), they include data represented in the Event
in a set of parameters. More details about events can be found here.
Access to the Event Service is in the CswContext
object passed in to the handlers class in the constructor. The Event Service provides a factory method to create a “default” publisher and subscriber, which can be accessed in various parts of your code to reuse a single connection to the service. In most cases, reusing this connection will provide the performance needed.
But if you prefer to create new connections, custom publishers and subscribers can be constructed. See the manual on the Event Service for more information.
Publishers have an API that allows the publishing of a single event, a stream of events, or periodic events created by an EventGenerator
, which is simply a function that returns an Event
.
Tutorial: Developing an HCD
Let’s add a publisher to our component. We will use the default publisher that will periodically publish events generated by an EventGenerator
.
- Scala
-
source
import scala.concurrent.duration._ private def publishCounter(): Cancellable = { var counter = 0 def incrementCounterEvent() = Option { counter += 1 val param: Parameter[Int] = KeyType.IntKey.make("counter").set(counter) SystemEvent(componentInfo.prefix, EventName("HcdCounter")).add(param) } log.info("Starting publish stream.") eventService.defaultPublisher.publish(incrementCounterEvent(), 2.second, err => log.error(err.getMessage, ex = err)) } private def stopPublishingGenerator(): Unit = { log.info("Stopping publish stream") maybePublishingGenerator.foreach(_.cancel()) }
- Java
-
source
private int counter = 0; private Optional<Event> incrementCounterEvent() { counter += 1; Parameter<Integer> param = JKeyType.IntKey().make("counter", JUnits.NoUnits).set(counter); return Optional.of(new SystemEvent(cswCtx.componentInfo().prefix(), new EventName("HcdCounter")).add(param)); } private Cancellable publishCounter() { log.info("Starting publish stream."); return cswCtx.eventService().defaultPublisher().publish(this::incrementCounterEvent, java.time.Duration.ofSeconds(2)); } private void stopPublishingGenerator() { log.info("Stopping publish stream"); maybePublishingGenerator.ifPresent(Cancellable::cancel); }
We encapsulate the starting of the publishing in our method publishCounter
. Our EventGenerator
is the incrementCounterEvent
method which increments our integer variable counter
and stores it in the ParameterSet
of a new SystemEvent
and returns it. Once our defaultPublisher
is resolved, we pass in a reference to incrementCounterEvent
and specify a period of 2 seconds. We log a message when publishing the event so that it can be observed when running the component.
The publish
method returns a Cancellable
type in a future. When the publishing is set up, the Cancellable
can be used to stop the event generator. We demonstrate its usage in the stopPublishingGenerator
method, although this method is not called in our tutorial.
We will start this publishing when our component initializes, so we return to our initialize
method and add a call to our publishCounter
method. We save a reference to the Cancellable
object for future use in our stopPublishingGenerator
method.
- Scala
-
source
var maybePublishingGenerator: Option[Cancellable] = None override def initialize(): Unit = { log.info("In HCD initialize") maybePublishingGenerator = Some(publishCounter()) } override def onLocationTrackingEvent(trackingEvent: TrackingEvent): Unit = { log.debug(s"TrackingEvent received: ${trackingEvent.connection.name}") } override def onShutdown(): Unit = { log.info("HCD is shutting down") }
- Java
-
source
private Optional<Cancellable> maybePublishingGenerator = Optional.empty(); @Override public void initialize() { log.info("In HCD initialize"); maybePublishingGenerator = Optional.of(publishCounter()); } @Override public void onLocationTrackingEvent(TrackingEvent trackingEvent) { log.debug(() -> "TrackingEvent received: " + trackingEvent.connection().name()); } @Override public void onShutdown() { log.info("HCD is shutting down"); }
Starting CSW Services
Before we run our application, we must first start the Location Service and the Event Service. csw-services
application has been provided to simplify the starting and stopping of CSW services. Refer here to install csw-services
and understand commands for starting the required services.
There are several environment variables that are used by the CSW framework and services. The environment variables used by CSW services are specified here.
Tutorial: Developing an HCD
Let’s go ahead and start our CSW Services using the csw-services
application.
csw-services start
Building and Running component in standalone mode
Once the component is ready, it is started using the ContainerCmd
object in a standalone mode. The details for starting the ContainerCmd
in a standalone mode can be found here.
There are various ways to build and run the project. A simple way during development is to use sbt to run it. The sbt command runMain
can be used to specify an application with a main method and run it with arguments specified at the command line. When this command is executed, sbt will take care of any downloading of dependencies, compiling, or building necessary to run your application.
Our template includes a wrapper application around ContainerCmd that we can use in the deployment module. To run our HCD in a standalone mode, go to the project root directory and type sbt "<deploy-module>/runMain <mainClass> --local <path-to-standalone-config-file>"
, where
<deploy-module>
is the name of the deployment module created by the template (sample-deploy
if using defaults)<mainClass>
is the full class name of our ContainerCmd application, which the template names<package>.<name>deploy.<Name>ContainerCmdApp
. If you accept the defaults for the template, it will beorg.tmt.csw.sampledeploy.SampleContainerCmdApp
. If you are having problems determining the class name, usesbt <deploy-module>/run
and it will prompt you the possibilities.<path-to-standalone-config-file>
is the filename, which can be an absolute path or relative to the directory of the deployment module. If using defaults, this would besrc/main/resources/SampleHcdStandalone.conf
for Scala, andsrc/main/resources/JSampleHcdStandalone.conf
for Java.
So if using the template defaults, the full command would be
- Scala
-
sbt "sample-deploy/runMain org.tmt.csw.sampledeploy.SampleContainerCmdApp --local src/main/resources/SampleHcdStandalone.conf"
- Java
-
sbt "sample-deploy/runMain org.tmt.csw.sampledeploy.SampleContainerCmdApp --local src/main/resources/JSampleHcdStandalone.conf"