Component Model
Components are the building blocks of TinySystems modules. Understanding the component model is essential for building effective modules.
What is a Component?
A component is a reusable unit of logic that:
- Receives messages on input ports
- Processes data according to its logic
- Sends results to output ports
- Maintains optional internal state
┌────────────────────────────────────┐
│ COMPONENT │
│ │
Input Port ────▶│ ┌─────────────────────────┐ │────▶ Output Port
│ │ Handle() │ │
Input Port ────▶│ │ │ │────▶ Output Port
│ │ - Process message │ │
Settings Port ──▶│ │ - Apply logic │ │
│ │ - Call output() │ │
Control Port ◀──▶│ └─────────────────────────┘ │
│ │
│ Component State │
│ (settings, etc.) │
└────────────────────────────────────┘Component Interface
Every component implements this interface:
type Component interface {
// Metadata about the component
GetInfo() ComponentInfo
// Process incoming messages
Handle(ctx context.Context, output Handler, port string, message any) any
// Define available ports
Ports() []Port
// Factory to create new instances
Instance() Component
}GetInfo()
Returns component metadata displayed in the UI:
func (c *MyComponent) GetInfo() module.ComponentInfo {
return module.ComponentInfo{
Name: "my-component", // Unique identifier
Description: "Brief description", // Short summary
Info: "Detailed info...", // Extended documentation
Tags: []string{"utility", "transform"},
}
}Handle()
The core processing function. Called for every message:
func (c *MyComponent) Handle(
ctx context.Context, // Request context (deadlines, tracing)
output module.Handler, // Callback to send output
port string, // Which port received the message
message any, // The message data (typed)
) any { // Return nil for success, error for failure
// Process based on port
switch port {
case "input":
result := process(message)
return output(ctx, "output", result)
case v1alpha1.SettingsPort:
c.settings = message.(Settings)
return nil
}
return nil
}Ports()
Defines the component's interface:
func (c *MyComponent) Ports() []module.Port {
return []module.Port{
{
Name: "input",
Label: "Input",
Position: module.Left,
Source: false, // Input port
Configuration: InputType{},
},
{
Name: "output",
Label: "Output",
Position: module.Right,
Source: true, // Output port
Configuration: OutputType{},
},
}
}Instance()
Factory method creating new component instances:
func (c *MyComponent) Instance() module.Component {
return &MyComponent{
settings: Settings{
DefaultValue: "default",
},
}
}Important: Always return a new instance with default values. Don't return c itself.
Component Lifecycle
1. REGISTRATION (at module startup)
┌─────────────────────────────────────────┐
│ registry.Register(&MyComponent{}) │
│ Component added to global registry │
└─────────────────────────────────────────┘
│
▼
2. DISCOVERY (TinyNode created)
┌─────────────────────────────────────────┐
│ TinyNode CRD references component │
│ Scheduler calls Instance() │
│ New component instance created │
└─────────────────────────────────────────┘
│
▼
3. INITIALIZATION (Settings port)
┌─────────────────────────────────────────┐
│ Handle(ctx, out, "_settings", config) │
│ Component stores configuration │
└─────────────────────────────────────────┘
│
▼
4. RUNNING (Message processing)
┌─────────────────────────────────────────┐
│ Handle(ctx, out, "input", message) │
│ Process message, call output() │
│ Repeat for each incoming message │
└─────────────────────────────────────────┘
│
▼
5. RECONCILIATION (Periodic)
┌─────────────────────────────────────────┐
│ Handle(ctx, out, "_reconcile", node) │
│ Refresh state, cleanup resources │
│ Every 5 minutes by default │
└─────────────────────────────────────────┘
│
▼
6. DESTRUCTION (TinyNode deleted)
┌─────────────────────────────────────────┐
│ Context cancelled │
│ Component instance garbage collected │
└─────────────────────────────────────────┘Stateless vs Stateful Components
Stateless Components
Most components should be stateless - they only use:
- Incoming message data
- Settings (from settings port)
type StatelessComponent struct {
settings Settings // Only store settings
}
func (c *StatelessComponent) Handle(ctx context.Context, output module.Handler, port string, msg any) any {
// All data comes from msg or c.settings
input := msg.(Input)
result := transform(input, c.settings)
return output(ctx, "output", result)
}Stateful Components
Some components need to maintain state:
- Accumulators, counters
- Connection pools
- Running timers
type StatefulComponent struct {
settings Settings
counter int64
cancelFunc context.CancelFunc
mu sync.Mutex // Protect concurrent access
}
func (c *StatefulComponent) Handle(ctx context.Context, output module.Handler, port string, msg any) any {
c.mu.Lock()
defer c.mu.Unlock()
c.counter++
// Use c.counter in processing
return nil
}Important: Stateful components require careful handling for:
- Thread safety (use mutexes)
- Resource cleanup (respect context cancellation)
- Multi-replica scenarios (see Scalability)
Component State Sharing
For multi-replica scenarios, share state via TinyNode metadata:
func (c *MyComponent) Handle(ctx context.Context, output module.Handler, port string, msg any) any {
if port == v1alpha1.ReconcilePort {
// Receive TinyNode with shared metadata
node := msg.(v1alpha1.TinyNode)
sharedValue := node.Status.Metadata["my-key"]
// Use shared value
}
// Update shared state (leader only)
if utils.IsLeader(ctx) {
output(ctx, v1alpha1.ReconcilePort, func(node *v1alpha1.TinyNode) {
node.Status.Metadata["my-key"] = "new-value"
})
}
}See CR-Based State Propagation for details.
Best Practices
1. Keep Components Focused
Each component should do one thing well:
// Good: Single responsibility
type JSONParser struct{} // Only parses JSON
type HTTPClient struct{} // Only makes HTTP requests
// Bad: Multiple responsibilities
type DoEverything struct{} // Parses, requests, transforms, logs...2. Use Typed Messages
Always define typed structs for ports:
// Good: Typed messages
type Input struct {
UserID string `json:"userId"`
Action string `json:"action"`
}
// Bad: Untyped
func (c *Component) Handle(..., msg any) any {
data := msg.(map[string]interface{}) // Fragile
}3. Handle All Ports
Don't ignore unknown ports:
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) any {
switch port {
case "input":
// Handle input
case v1alpha1.SettingsPort:
// Handle settings
case v1alpha1.ReconcilePort:
// Handle reconcile
default:
return fmt.Errorf("unknown port: %s", port)
}
return nil
}4. Respect Context
Always check context for cancellation:
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) any {
for _, item := range items {
select {
case <-ctx.Done():
return ctx.Err() // Stop processing
default:
process(item)
}
}
return nil
}Next Steps
- Ports and Messages - Deep dive into port system
- Message Flow - Understanding execution flow
- Component Patterns - Common implementation patterns