The helloworld-rmi
example is a small project that comes with the FraSCAti distribution. During this tutorial we will use this example to explain the basics of specifying deployment artifacts with the Amelia DSL.
This example comprises two SCA components: a Server
, exposing a print service through RMI, and a Client
, consuming that service to print a message in the standard output. These components are configured by default to run in the same machine, but with litle effort we can change that, running each component in a separate node.
Generally, to run this example you only need to do two main tasks: compile the source code, and then run the components having into account the dependencies among them. The latter is generally time consuming and error prone in complex projects, because you have to carefully execute each component into its corresponding node respecting the dependencies order. Moreover, when you add several machines to the scenario, you need to perform another task: transport either the source code or the compiled artifacts to the corresponding computing nodes. This may also requires relocating local resources, so they are available for the components using them.
Amelia provides the syntax and semantics to abstract those repetitive deployment tasks into composable elements that will allow you to perform systematic executions of SCA systems.
In Amelia there are two compilation units (think of Java interfaces and classes): subsystems and deployments. The first ones contain the definition of hosts (computing nodes) and execution rules; the second ones allow to configure deployment strategies from subsystem definitions.
First, let's specify the host in which the Server
and Client
components will run, i.e., localhost.
Note: Amelia makes use of SSH to communicate with remote machines (including localhost), and FTP to transport resources. In this tutorial we use the default SSH & FTP ports 21 and 22.
var Host localhost = new Host("localhost", 21, 22, "user", "pass", "Ubuntu-16.04")
Notice that you can specify hosts using either the constructors in Host or the helper methods in Hosts.
Now, to specify deployment actions you only need to group command declarations as rules. An execution rule has three parts: target, dependencies (other targets), and commands. It's syntax looks like this:
target2: target0, target1, ...;
command1
command2
...
If a rule has no dependencies, don't use the trailing semi-colon.
Let's see how to specify the compilation and execution of the helloworld-rmi
example:
compilation:
cd "$FRASCATI_HOME/examples/helloworld-rmi"
compile "server/src" "server"
compile "client/src" "client"
execution: compilation;
cd "$FRASCATI_HOME/examples/helloworld-rmi"
run "helloworld-rmi-server" -libpath "server.jar"
run "helloworld-rmi-client" -libpath "client.jar" -s "r" -m "run"
Notice that $FRASCATI_HOME is not related to Amelia in any way, it's just an environment variable that gets resolved during the SSH session.
Of course, different set of rules can express the same, that's how programming works! let's see another way of specifying the deployment actions above:
server:
cd "$FRASCATI_HOME/examples/helloworld-rmi"
compile "server/src" "server"
run "helloworld-rmi-server" -libpath "server.jar"
client: server;
cd "$FRASCATI_HOME/examples/helloworld-rmi"
compile "client/src" "client"
run "helloworld-rmi-client" -libpath "client.jar" -s "r" -m "run"
In the first case the overall execution would be:
- Change the current directory to $FRASCATI_HOME/examples/helloworld-rmi
- Compile source code of the
Server
component - Compile source code of the
Client
component - Change the current directory to $FRASCATI_HOME/examples/helloworld-rmi
- Run the
Server
component - Run the
Client
component
As these commands are executed in the same SSH session, you can ommit step 4. Now it's your turn, what would be the overall execution for the second set of rules?
Now that we know how to specify hosts and execution rules, let's create a subsystem HelloworldRMI
:
package subsystems
import org.amelia.dsl.lib.descriptors.Host
subsystem HelloworldRMI {
var Host localhost = new Host("localhost", 21, 22, "user", "pass", "Ubuntu-16.04")
on localhost {
server:
cd "$FRASCATI_HOME/examples/helloworld-rmi"
compile "server/src" "server"
run "helloworld-rmi-server" -libpath "server.jar"
client: server;
cd "$FRASCATI_HOME/examples/helloworld-rmi"
compile "client/src" "client"
run "helloworld-rmi-client" -libpath "client.jar" -s "r" -m "run"
}
}
All rules without dependencies are executed concurrently*, while commands within the same rule are executed sequentially.
*: In fact, if they are executed using the same SSH session they will be executed sequentially. If concurrent execution is desired, you must create several instances of the same node, for example:
package ^package
import org.amelia.dsl.lib.descriptors.Host
subsystem Subsystem {
var Host localhost1 = new Host("localhost", 21, 22, "user", "pass", "local1")
var Host localhost2 = new Host("localhost", 21, 22, "user", "pass", "local2")
on localhost1 {
target1: ...
cmd "echo concurrent"
}
on localhost2 {
target2:
cmd "echo concurrent"
target3: target1;
cmd "echo after target1"
}
}
You can find a complete list of the supported commands here.
Subsystems are parameterizable. Parameters are useful for instantiating subsystems with different values. For example, the host may be a parameter, that way the components may be executed in several hosts abstracting the execution rules:
package subsystems
import org.amelia.dsl.lib.descriptors.Host
subsystem HelloworldRMI {
param Host host
on host {
server:
cd "$FRASCATI_HOME/examples/helloworld-rmi"
compile "server/src" "server"
run "helloworld-rmi-server" -libpath "server.jar"
client: server;
cd "$FRASCATI_HOME/examples/helloworld-rmi"
compile "client/src" "client"
run "helloworld-rmi-client" -libpath "client.jar" -s "r" -m "run"
}
}
In the next section, we will see how to pass a parameter value to the subsystem.
When executing the subsystem HelloworldRMI
you're getting the default deployment. That is, a single execution with no automatic shutdown. However, if a subsystem has dependencies or parameters, it is necessary to create a deployment strategy. Deployment strategies are also useful for automatically repeating the same deployment, or retrying on failure; these are custom behaviors regarding how the deployment is executed. For example:
package deployments
import org.amelia.dsl.lib.util.RetryOnFailure
includes subsystems.HelloworldRMI
deployment RetryOnFailure {
add(new HelloworldRMI) // deploy one instance of the subsystem
var helper = new RetryableDeployment()
helper.deploy([
start(true) // Deploy and stop executed components when finish
], 3) // In case of failure, retry 2 more times
}
package deployments
import org.amelia.dsl.lib.util.RetryOnFailure
includes subsystems.HelloworldRMI
deployment SequentialDeployments {
add(new HelloworldRMI)
for (i : 1..5) {
start(true) // Deploy and stop executed components when finish
}
}
If the subsystem expects a parameter, this is the way to go:
package deployments
import org.amelia.dsl.lib.util.RetryOnFailure
includes subsystems.HelloworldRMI
deployment SimpleDeployment {
var Host host = new Host("localhost", 21, 22, "user", "pass", "Ubuntu-16.04")
add(new HelloworldRMI(host))
start(true) // Deploy and stop executed components when finish
}
In case a subsystem expects several parameters, they must be passed in the same order they were defined.