Handlers in Docker

We want to make it really easy to run handler services in Docker, toword that version 0.0.4 introduces a packager that can create containers on your behalf.

Preparing Handlers

Go Based

The idea is that you would create a Handler per Go package, the packager will then pull in all the configured handlers into a small microservice.

package handler

import (
	aj "github.com/choria-io/asyncjobs"
)

func AsyncJobHandler(ctx context.Context, log aj.Logger, task *aj.Task) (any, error) {
	// process your email
}

Place this in any package you like, for example git.example.com/example/email/new. You can have many handlers, as long as they are in packages like here.

Other Languages

Other languages are supported using NATS Request-Reply, implement them according to the protocol describe in Remote Request-Reply Handlers.

Packaging

In a empty directory create a file asyncjobs.yaml with the following content:

# The NATS Context to connect with.
#
# Same as NatsContext() client option
nats: AJ_EMAIL

# The Work Queue to consume.
#
# Same as BindWorkQueue() client option
queue: EMAIL

# The package name to generate
name: git.example.com/example

# The version of github.com/choria-io/asyncjobs to use,
# something go get would accept. Defeaults to the same
# as the CLI version
asyncjobs: latest

# Use the RetryLinearTenMinutes retry policy,
#
# Equivalent to client RetryBackoffPolicyName() option
retry: 10m

# Discard tasks that reach complete state.
#
# Same as DiscardTaskStates() client option
discard:
  - completed

# List of Task handlers
tasks:
  - type: email:new
    package: git.example.com/example/email/new
    version: v0.2.0
  - type: audit:log
    remote: true
  - type: webhook:call
    command: webhook/call.sh

We set up a remote: true Task handler for audit:log Tasks, this will delegate to external processes, see Remote Request Reply Handlers.

The webhook:call Task handler is a shell script that should exist in commands/webhook/call.sh, it will be copied into the container. It’s most likely you will need dependencies not in the default container, I suggest using the generated one as a FROM container to derive one with your dependencies met via the alpine package system.

Next we create our package:

$ ajc package docker
╭────────────────────────────────────────────────────────────────╮
│                 Handler Microservice Settings                  │
├────────────────────────────────┬───────────────────────────────┤
│ Package Name                   │ git.example.com/example       │
│ NATS Context Name              │ AJ_EMAIL                      │
│ Work Queue                     │ EMAIL                         │
│ Task Handlers                  │ 2                             │
│ github.com/choria-io/asyncjobs │ latest                        │
╰────────────────────────────────┴───────────────────────────────╯

╭───────────────────────────────────────────────────────────────────────────────────────────────╮
│                              Handler Configuration and Packages                               │
├──────────────┬───────────────────────┬────────────────────────────────────────────────────────┤
│ Task Type    │ Handler Kind          │ Detail                                                 │
├──────────────┼───────────────────────┼────────────────────────────────────────────────────────┤
│ email:new    │ Go Package            │ git.example.com/example/email/new@v0.2.0               │
│ webhook:call │ External Command      │ webhook/call.sh                                        │
│ audit:log    │ Request-Reply Service │ CHORIA_AJ.H.T.audit:log                                │
╰──────────────┴───────────────────────┴────────────────────────────────────────────────────────╯

Build your container using 'docker build'

$ ls -l
total 12
-rw-rw-r-- 1 rip rip  166 Feb  8 17:48 asyncjobs.yaml
-rw-r--r-- 1 rip rip  713 Feb  8 20:01 Dockerfile
-rw-r--r-- 1 rip rip 2540 Feb  8 20:01 main.go
drwxrwxr-x 3 rip rip   19 Feb  8 17:48 commands

You see we have a main.go that will be built into a container:

$ docker build . --tag example/email:latest
$ docker push example/email:latest

Running

The container will rely on a NATS Context for connectivity options, lets create one in the same directory:

$ pwd 
/home/myname/work/email_service
$ XDG_CONFIG_HOME=`pwd` nats context add AJ_EMAIL --server nats://nats.example.net:4222
NATS Configuration Context "AJ_EMAIL"

      Server URLs: nats.example.net:4222
             Path: /home/myname/work/email_service/nats/context/AJ_EMAIL.json

We can now run it:

$ docker run -ti -v "/home/myname/work/email_service/nats:/handler/config/nats" -p 8080:8080 --rm example/email:latest
INFO[19:07:39] Connecting using Context AJ_EMAIL consuming work queue EMAIL with concurrency 4
WARN[19:07:39] Exposing Prometheus metrics on port 8080

Note we mount the nats configuration directory into /handler/config/nats which is where the container will look for the context configuration. Should you need other supporting files like credentials you can place them in the container and reference them at their in-container paths.

Environment Configuration

A few environment variables can be set to influence the container:

VariableDescriptionYAML Item
AJ_WORK_QUEUEThe name of the Queue to connect toqueue
AJ_NATS_CONTEXTThe NATS context to use for connectivitynats
XDG_CONFIG_HOMEThe prefix for NATS Context configuration, defaults to /handler/config
AJ_CONCURRENCYThe number of workers to run, defaults to runtime.NumCPU()
AJ_DEBUGSet to 1 to enable debug logging
AJ_RETRY_POLICYSets the Retry Backoff Policy, one of 10m, 1h, 1m, defaultretry