-
Notifications
You must be signed in to change notification settings - Fork 2
Writing a Spawnpoint Service in Go
This guide will show you how to write a simple program that uses the Golang Bosswave bindings and how to deploy this program so that it runs on a Spawnpoint instance.
An example Go driver is available here. It increments a counter and publishes a Bosswave message once per second until the counter hits a user-specified limit. Adapted examples from this code are discussed below.
import (
"github.com/immesys/spawnpoint/spawnable"
bw2 "gopkg.in/immesys/bw2bind.v5"
)
These two import statements are very common in Go programs hosted on Spawnpoint.
-
Spawnpoint offers a collection of utility Go functions in the
spawnable
library. We will use this library to access driver-specific execution parameters stored in a file within a Spawnpoint container. -
The
bw2bind
library allows Go programs to send and receive messages on Bosswave's message bus. Most Spawnpoint drivers will only use the publish and subscribe operations.
Driver code commonly uses external files to define parameters that are specific to individual instances of that driver, such as IP addresses or polling intervals. The counter driver reads a YAML file to produce a dictionary of parameters specifying the URI to publish to and the message to publish upon incrementing the internal counter.
parameters := spawnable.GetParamsOrExit()
message := parameters.MustString("msg")
destUri := parameters.MustString("to")
Spawnpoint makes it easy to include parameter files in your containers, as discussed below.
bwClient := bw2.ConnectOrExit("")
bwClient.SetEntityFromEnvironOrExit()
Here, the first line initializes a Bosswave client backed by a connection to a Bosswave agent. By passing an empty string as an argument, we direct the client to connect to the network address specified by the BW2_AGENT
environment variable. This variable is automatically set in all Spawnpoint containers. However, you may override this default by passing the network address as a string argument to the ConnectOrExit
function.
The second line directs the Bosswave client to set its entity (which will be used to identify the client in all future Bosswave operations) from the BW2_DEFAULT_ENTITY
environment variable. In Spawnpoint, the BW2_DEFAULT_ENTITY
variable automatically refers to the entity file you have specified in a service's configuration file (described below), so you typically want to set the client's entity from the environment as was done here.
for i := 0; i < repetitions; i++ {
output := fmt.Sprintf("%v: %s", i, message)
po := bw2.CreateStringPayloadObject(output)
bwClient.PublishOrExit(&bw2.PublishParams{
URI: parameters.MustString("to"),
AutoChain: true,
PayloadObjects: []bw2.PayloadObject{po},
})
fmt.Printf("Publishing %d\n", i)
time.Sleep(1 * time.Second)
}
This loop repeatedly publishes messages to the Bosswave message bus. Here are the steps it takes:
- Create a string
output
that will form the main contents of the next message. - All Bosswave messages are comprised of a sequence of payload objects. Thus, we convert the string to an instance of
bw2.PayloadObject
so we can add it to the outgoing message. - Call the Bosswave client's
PublishOrExit
function to send an outgoing message. If an error occurs, this function will terminate the program. There are variants of this function that allow the caller to explicitly handle errors if desired. The arguments to this function (collected into a singlestruct
) are:
-
URI
: The destination topic of the message. Here, that destination is specified in an external parameter file. -
AutoChain
: Set this totrue
. -
PayloadObjects
: A Golang slice ofbw2.PayloadObject
instances. Here, we only have one PO, created from theoutput
string.
- Print out an informative message and sleep for 1 second before the next message is published.
Spawnpoint containers are described in simple YAML files that contain a sequence of key/value configuration pairs. Here is a YAML file that could be used to execute this example, assuming you have a compiled Go program as the file counter
in the current directory.
bw2Entity: counter.ent
cpuShares: 256
memory: 256
includedFiles: [counter, params.yml]
run: [./counter, 50]
-
bw2Entity
is the Bosswave entity that will be used within the deployed container for all Bosswave operations. It is automatically set up as theBW2_DEFAULT_ENTITY
environment variable within the container. -
cpuShares
specifies an allocation of host CPU resources for the container. 1024 shares correlate to one CPU core. A Spawnpoint daemon will not accept the new container for deployment if it has fewer unallocated CPU shares than are requested here. -
memory
specifies an allocation of the host's memory for the container in MiB. As with CPU shares, new containers are not deployed if the allocation cannot be satisfied. -
includedFiles
is a list of files on the deploying machine that will be included in the container. These are made available in the current working directory of the container's entry point command. Here, we include both the compiled Go program as well as a YAML file for the parameters. -
run
specifies the command that will be executed upon container startup. Here, we invoke thecounter
binary, which was copied from the host to the container due to the previousincludedFiles
directive, and give it a single argument,50
, to specify the number of messages to publish.
Spawnpoint makes it easy to deploy code that is under version control and hosted on GitHub. A service configuration file may contain a source
parameter specifying a repository URL. This repository will then be cloned when the container is initialized. All files and directories within the repository are placed in the container's working directory. If you need to check out a specific branch or commit, this will need to be part of the commands specified in the build
field.
As an example, to deploy a container running the counter example using code from the demosvc repository:
bw2Entity: counter.ent
source: git+https://github.com/jhkolb/demosvc
build: [go get -d, go build -o counter]
run: [./counter, 50]
memAlloc: 256
cpuShares: 512
includedFiles: [params.yml]
The spawnctl
command line tool allows you to scan for spawnpoints and deploy containers. Pre-built spawnctl
releases are available for the following platforms:
As an example, say we want to deploy our counter driver on a spawnpoint running at the Bosswave URI oski/spawnpoint/beta
.
As a first step, we may want to perform a scan to verify that the spawnpoint is running and healthy.
$ spawnctl scan -u oski
[beta] seen 17 Mar 18 17:44 PDT (20.2s) ago at oski/spawnpoint/beta
Available CPU Shares: 1536/2048
Available Memory: 1536/2048
1 Running Service(s)
• [thermostat-driver] seen 17 Mar 18 17:44 PDT (18.58s) ago.
CPU: ~1.02/512 Shares. Memory: 3.86/512 MiB
Next, deploy your code to the spawnpoint using spawnctl
's deploy
command. The deploy operation involves two mandatory parameters:
- The Bosswave URI of the spawnpoint to deploy to
- The YAML configuration file for the container
You may specify the name of the new service in its configuration file or on the command line via the -n
flag. If the name is specified in both ways, the command line value overrides the configuration's value.
No two containers running on the same spawnpoint may share a name.
The counter driver is deployed on the Spawnpoint based at oski/spawnpoint/beta
like so:
$ spawnctl deploy -u oski/spawnpoint/beta -c config.yml -n counter
This assumes the following files are present in the current working directory:
-
config.yml
: A YAML configuration file for the container. -
params.yml
: This defines instance-specific parameters for the driver and is also referenced in theincludedFiles
configuration parameter, meaning it will be copied into the container.
Once the deploy process is started, spawnctl
will tail the logs for the new service on the target spawnpoint until the user types <CTRL>-c
. For example, the output might look like the following:
Tailing service logs. Press CTRL-c to exit...
...
[SUCCESS] Service container has started
...
To check if the counter driver is running properly, we can subscribe to the Bosswave messages it emits. The easiest way to do this is using the bw2
command line tool. Assuming the counter is publishing messages on scratch.ns/counter/out
, we can subscribe as follows:
$ bw2 s scratch.ns/counter/out
Message from <snip>/counter/out:
PO 64.0.1.0 len 17 (human readable) contents:
(0) Hello, World!
Message from <snip>/counter/out:
PO 64.0.1.0 len 17 (human readable) contents:
(1) Hello, World!
Message from <snip>/counter/out:
PO 64.0.1.0 len 17 (human readable) contents:
(2) Hello, World!
Message from <snip>/counter/out:
PO 64.0.1.0 len 17 (human readable) contents:
(3) Hello, World!
Message from <snip>/counter/out:
PO 64.0.1.0 len 17 (human readable) contents:
(4) Hello, World!
Message from <snip>/counter/out:
PO 64.0.1.0 len 17 (human readable) contents:
(5) Hello, World!
...
This output indicates that the driver is successfully running on Spawnpoint and publishing Bosswave messages.
package main
import (
"fmt"
"os"
"strconv"
"time"
"github.com/immesys/spawnpoint/spawnable"
bw2 "gopkg.in/immesys/bw2bind.v5"
)
func main() {
if len(os.Args) < 2 {
fmt.Printf("Usage: %s [message] <num_repetitions>\n", os.Args[0])
os.Exit(1)
}
var repetitions int
var message string
var err error
parameters := spawnable.GetParamsOrExit()
if len(os.Args) >= 3 {
message = os.Args[1]
repetitions, err = strconv.Atoi(os.Args[2])
} else {
repetitions, err = strconv.Atoi(os.Args[1])
message = parameters.MustString("msg")
}
if err != nil {
fmt.Println("Invalid repetitions argument:", err)
os.Exit(1)
}
bwClient := bw2.ConnectOrExit("")
bwClient.SetEntityFromEnvironOrExit()
for i := 0; i < repetitions; i++ {
output := fmt.Sprintf("%v: %s", i, message)
po := bw2.CreateStringPayloadObject(output)
bwClient.PublishOrExit(&bw2.PublishParams{
URI: parameters.MustString("to"),
AutoChain: true,
PayloadObjects: []bw2.PayloadObject{po},
})
fmt.Printf("Publishing %d\n", i)
time.Sleep(1 * time.Second)
}
fmt.Printf("Sent %v messages. Terminating.\n", repetitions)
}
msg: "Hello, World!"
to: scratch.ns/counter.out