ControllerBus is a framework for communicating control loops:
- Configurable: flexible self-documenting config with Protobuf and YAML.
- Cross-platform: supports web browsers, servers, desktop, mobile, ...
- Hot-loadable: plugins and IPC dynamically add controllers at runtime.
- Modular: easily combine together application components w/o glue code.
- Declarative: de-duplicated declarative requests between controllers.
The primary concepts are:
- Config: configuration for a controller or process.
- Controller: goroutine which can create & handle Directives.
- Directive: a cross-controller request or declaration of target state.
- Bus: communication channel between running Controllers.
- Factory: constructor for controller and configuration objects.
Controller Bus provides a pattern for structuring modular Go projects.
Basic demo of the controllerbus daemon and ConfigSet format:
cd ./cmd/controllerbus
go build -v
./controllerbus daemonThis will load controllerbus_daemon.yaml and execute the boilerplate demo:
added directive                               directive="LoadControllerWithConfig<config-id=controllerbus/configset>"
added directive                               directive="ExecController<config-id=controllerbus/configset"
added directive                               directive="LoadConfigConstructorByID<config-id=controllerbus/example/boilerplate>"
starting controller                           controller=controllerbus/configset
added directive                               directive="ApplyConfigSet<controller-keys=boilerplate-example-0@1>"
added directive                               directive="LoadControllerWithConfig<config-id=controllerbus/bus/api>"
removed directive                             directive="LoadConfigConstructorByID<config-id=controllerbus/example/boilerplate>"
added directive                               directive="ExecController<config-id=controllerbus/bus/api>"
executing controller                          config-key=boilerplate-example-0 controller=controllerbus/configset
starting controller                           controller=controllerbus/bus/api
grpc api listening on: :5110                 
added directive                               directive="LoadControllerWithConfig<config-id=controllerbus/example/boilerplate>"
added directive                               directive="ExecController<config-id=controllerbus/example/boilerplate>"
starting controller                           controller=controllerbus/example/boilerplate
hello from boilerplate controller 1: hello world  controller=controllerbus/example/boilerplate
controller exited normally                    controller=controllerbus/example/boilerplate exec-dur="31.053µs"
ConfigSet is a key/value set of controller configurations to load.
The following is an example ConfigSet in YAML format for a program:
example-1:
  # configuration object
  config:
    exampleField: "Hello world!"
  # ID of the configuration type
  id: controllerbus/example/boilerplate
  # rev # for overriding previous configs
  rev: 1In this case, example-1 is the ID of the controller. If multiple ConfigSet are
applied with the same ID, the latest rev wins. The ConfigSet controller
will automatically start and stop controllers as ConfigSets are changed.
Controllers are executed by attaching them to a Bus. When attaching to a Bus, all ongoing Directives are passed to the new Controller. The Controllers can return Resolver objects to resolve result objects for Directives.
There are multiple ways to start a Controller:
- AddController: with the Go API: construct & add the controller.
- AddDirective->- ExecControllerWithConfig: with a directive.
- yaml/json: resolving human-readable configuration to Config objects.
Config objects are Protobuf messages with attached validation functions. They can be hand written in YAML and parsed to Protobuf or be created as Go objects.
The boilerplate example has the following configuration proto:
// Config is the boilerplate configuration.
message Config {
  // ExampleField is an example configuration field.
  string example_field = 1;
}This is an example YAML configuration for this controller:
exampleField: "Hello world!"With the Go API, we can use the LoadControllerWithConfig directive to execute the controller with a configuration object:
	bus.ExecOneOff(
		ctx,
		cb,
		resolver.NewLoadControllerWithConfig(&boilerplate_controller.Config{
			ExampleField: "Hello World!",
		}),
		nil,
	)The example daemon has an associated client and CLI for the Bus API, for example:
$ controllerbus client exec -f controllerbus_daemon.yaml  {
    "controllerInfo": {
      "version": "0.0.1",
      "id": "controllerbus/example/boilerplate"
    },
    "status": "ControllerStatus_RUNNING",
    "id": "boilerplate-example-0"
  }The bus service has the following API:
// ControllerBusService is a generic controller bus lookup api.
service ControllerBusService {
  // GetBusInfo requests information about the controller bus.
  rpc GetBusInfo(GetBusInfoRequest) returns (GetBusInfoResponse) {}
  // ExecController executes a controller configuration on the bus.
  rpc ExecController(controller.exec.ExecControllerRequest) returns (stream controller.exec.ExecControllerResponse) {}
}The RPC API is itself implemented as a controller, which can be configured:
grpc-api:
  config:
    listenAddr: ":5000"
    busApiConfig:
      enableExecController: true
  id: controllerbus/bus/api
  rev: 1For security, the default value of enableExecController is false to disallow
executing controllers via the API.
The structure under cmd/controllerbus and example/boilerplate are examples
which are intended to be copied to other projects, which reference the core
controllerbus controllers. A minimal program is as follows:
	ctx := context.Background()
	log := logrus.New()
	log.SetLevel(logrus.DebugLevel)
	le := logrus.NewEntry(log)
	b, sr, err := core.NewCoreBus(ctx, le)
	if err != nil {
		t.Fatal(err.Error())
	}
	sr.AddFactory(NewFactory(b))
	execDir := resolver.NewLoadControllerWithConfig(&Config{
		ExampleField: "testing",
	})
	_, ctrlRef, err := bus.ExecOneOff(ctx, b, execDir, nil)
	if err != nil {
		t.Fatal(err.Error())
	}
	defer ctrlRef.Release()This provides logging, context cancelation. A single Factory is attached which provides support for the Config type, (see the boilerplate example).
Plugins can be bundled together with a set of root configurations into a CLI. This can be used to bundle modules into a daemon and/or client for an application - similar to the controllerbus cli.
An in-memory Bus can be created for testing, an example is provided in the boilerplate package.
⚠ Plugins are experimental and not yet feature-complete.
The plugin system and compiler scans a set of Go packages for ControllerBus factories and bundles them together into a hashed Plugin bundle. The compiler CLI can watch code files for changes and re-build automatically. Multiple plugin loaders and binary formats are supported.
USAGE:
   controllerbus hot compile - compile packages specified as arguments once
OPTIONS:
   --build-prefix value           prefix to prepend to import paths, generated on default [$CONTROLLER_BUS_PLUGIN_BUILD_PREFIX]
   --codegen-dir value            path to directory to create/use for codegen, if empty uses tmpdir [$CONTROLLER_BUS_CODEGEN_DIR]
   --output PATH, -o PATH         write the output plugin to PATH - accepts {buildHash} [$CONTROLLER_BUS_OUTPUT]
   --plugin-binary-id value       binary id for the output plugin [$CONTROLLER_BUS_PLUGIN_BINARY_ID]
   --plugin-binary-version value  binary version for the output plugin, accepts {buildHash} [$CONTROLLER_BUS_PLUGIN_BINARY_VERSION]
   --no-cleanup                   disable cleaning up the codegen dirs [$CONTROLLER_BUS_NO_CLEANUP]
   --help, -h                     show help
The CLI will analyze a list of Go package paths, discover all Factories available in the packages, generate a Go module for importing all of the factories into a single Plugin, and compile that package to a .so library.
List of projects known to use Controller Bus:
- Bifrost: networking and p2p library + daemon
Open a PR to add your project to this list!
Please open a GitHub issue with any questions / issues.
... or feel free to reach out on Matrix Chat or Discord.
MIT
