Under the hood
Flow-based programming was invented in the early 70s, and it perfectly fits into an event-driven system we so like to build. Each node of a graph follows the Unix philosophy when it does the certain job only and does it well.
Glossary
- Node - instance of the certain component on the flow.
- Flow - a set of nodes connected with each other.
- Edge - connection between nodes.
- Component - piece of code that solves a single purpose. Component has input and output ports.
- Module - a program which contains sets of components which can be deployed to the cluster.
- Project - set of the flows. Project is connected to a single cluster.
- Cluster - Kubernetes cluster.
Here is the interface written with go which each component needs to implement.
// Component interface
type Component interface {
// GetInfo provides basic information about the component: its name, description, info and tags
GetInfo() ComponentInfo
// Handle handles incoming messages; emits downstream via output and returns a Result
Handle(ctx context.Context, output Handler, port string, message any) Result
// Ports gets list of ports
Ports() []Port
// Instance creates new instance with default settings
Instance() Component
}
Where Port is:
type Port struct {
// Source is true when this port is a source of data (an output port)
Source bool
// Position: which side of the node this port is displayed on (Top, Right, Bottom, Left)
Position Position
// Name of the port, e.g. "req"
Name string
// Human-readable name of the port, e.g. "Request"
Label string
// Instance of a custom struct responsible for the data exchange
Configuration interface{}
// Response struct for blocking request/response ports
ResponseConfiguration interface{}
// Optional raw JSON schema override for runtime-shaped forms
Schema json.RawMessage
}
Each instance of the component (node) is represented in the cluster as a Custom Resource of a tinynode CRD. We use operator pattern to reconcile tinynodes in a cluster.
// TinyNode CRD specifications
type TinyNodeSpec struct {
// Module name - container image repo + tag
// +kubebuilder:validation:Required
Module string `json:"module"`
// Component name within a module
// +kubebuilder:validation:Required
Component string `json:"component"`
// Port configurations
// +kubebuilder:validation:Optional
Ports []TinyNodePortConfig `json:"ports"`
// Edges to send message next
// +kubebuilder:validation:Optional
Edges []TinyNodeEdge `json:"edges"`
}
By creating a new node on the flow - we create a new custom resource in a cluster which starts a new instance (goroutine) inside the corresponding module's container, so single container may host hundreds of the nodes.
Instances of the components within the same module communicate with each other through direct function calls in the same process.
Instances of components from different modules communicate over NATS when the runtime is started with TINY_NATS_URL set, falling back to gRPC otherwise.
We use Helm to install modules. With help of TinySystems Operator we can deploy any kind of module.