Skip to content

Writing a Spawnpoint Service in Go

jhkolb edited this page Mar 23, 2018 · 4 revisions

Using Go to Write a Spawnpoint Service

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.

Writing a Simple Counter Driver

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.

Imports

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.

  1. 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.

  2. 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.

Parameters

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.

Bosswave Client Initialization

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.

Publishing Messages

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:

  1. Create a string output that will form the main contents of the next message.
  2. 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.
  3. 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 single struct) are:
  • URI: The destination topic of the message. Here, that destination is specified in an external parameter file.
  • AutoChain: Set this to true.
  • PayloadObjects: A Golang slice of bw2.PayloadObject instances. Here, we only have one PO, created from the output string.
  1. Print out an informative message and sleep for 1 second before the next message is published.

Writing a Configuration File

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 the BW2_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 the counter binary, which was copied from the host to the container due to the previous includedFiles directive, and give it a single argument, 50, to specify the number of messages to publish.

Cloning a Repository

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]

Deploying on a Spawnpoint

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 the includedFiles 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
...

Verifying Functionality

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.

Appendix 1: The Complete Driver Program

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)
}

Appendix 2: The Corresponding Parameters File

msg: "Hello, World!"
to: scratch.ns/counter.out