Goten Controller Library
You should know about controller design from the developer guide. Here we give a small recap of the controller with tips about code paths.
The controller framework is part of the wider Goten framework. It has annotations + compiler parts, in:
- https://github.com/cloudwan/goten/blob/main/annotations/controller.proto
- https://github.com/cloudwan/goten/tree/main/compiler/controller
You can read more about Goten compiler. For now, in this place, we will talk just about generated controllers.
There are some runtime elements for all controller components (NodeManager,
Node, Processor, Syncer…) in runtime/controller
direction in Goten
repo: https://github.com/cloudwan/goten/tree/main/runtime/controller.
In the config.proto
, we have node registry access config and nodes manager
configs, which you should already know from controller/db-controller config
proto files.
A bit more interesting thing we have with Node managers. As it was said in
the Developer Guide, we scale horizontally by adding more nodes. To have
more nodes in a single pod, which increases the chance of fairer workload
distribution, we often have more than 1 Node instance per type. We organize
them with Node Managers. You should see a directory
runtime/controller/node_management/manager.go
.
Each Node must implement:
type Node interface {
Run(ctx context.Context) error
UpdateShardRange(ctx context.Context, newRange ShardRange)
}
Node Manager component creates on the startup as many Nodes as it has
in the config. Next, it runs all of them, but they don’t get yet any share
of shards. Therefore, they are idle. Managers register all nodes in
the registry, where all node IDs across all pods are collected.
The registry is responsible for returning the shard range assigned for
each node. Whenever a pod dies or a new one is deployed, the Node
registry will notify the manager about new shard ranges per Node. It then
notifies the relevant Node via the UpdateShardRange
call.
Registry for Redis uses periodic polling, therefore there may be a chance two controllers executing the same work in theory for a couple of seconds. It probably will be better to improve, but we design controllers around the observed/desired state, and duplicating the same request may bring some temporary warning errors, but they should be harmless. Still, it’s a field for improvement.
See the NodeRegistry
component (in file registry.go
, we use Redis).
Apart from the node managers directory in runtime/controller
, you can see
the processor package. We have there from more notable elements:
- Runner module, which is processor runner goroutine. It is the component for executing all events in a thread-safe manner, but developers must not do any IO.
- Syncer module, which is generic and based on interfaces, although we
generate type-safe wrappers in all controllers. It is quite large, it
consists of Desired/Observed state objects (file
syncer_states.go
), an updater that operates on its own goroutine (filesyncer_updater.go
), and finally central Syncer object, defined insyncer.go
. It compares the desired vs observed state and pushes updates to the syncer updater. - In
synchronizable
we have structures responsible for propagating sync/lostSync events across Processor modules, so ideally developers don’t need to handle them themselves.
Syncer is fairly complex, it needs to handle failures/recoveries, resets, and bursts of updates. Note that it does not use Go channels because:
- They have limited capacity (defined). This is not nice considering we have IO works there.
- Maps are best if there are multiple updates to a single resource because they will allow to merging of multiple events (overwrite previous ones). Channels would force at least to consume all items from the queue.