TinySignal CRD
TinySignal is the Custom Resource used to trigger node execution. It's how external events enter the TinySystems flow execution system.
Full Specification
yaml
apiVersion: operator.tinysystems.io/v1alpha1
kind: TinySignal
metadata:
name: signal-abc123
namespace: tinysystems
labels:
tinysystems.io/node-name: my-node
spec:
# Target node name
node: my-router-abc123
# Target port on the node
port: input
# Payload data
data:
message: "Hello World"
userId: "user-123"
timestamp: "2024-01-15T10:30:00Z"How Signals Work
+-----------------------------------------------------------------------------+
| SIGNAL EXECUTION FLOW |
+-----------------------------------------------------------------------------+
1. SIGNAL CREATION
+------------------------------------------------------------------------+
| External trigger creates TinySignal CR: |
| - HTTP request received by http-module |
| - User clicks button in UI (control port) |
| - Timer fires in ticker component |
| - Component calls output() to next module |
+------------------------------------------------------------------------+
|
v
2. CONTROLLER DETECTION
+------------------------------------------------------------------------+
| TinySignalReconciler watches for new signals |
| - Only LEADER pod processes signals (avoid duplicates) |
+------------------------------------------------------------------------+
|
v
3. ROUTING TO SCHEDULER
+------------------------------------------------------------------------+
| Controller extracts target and sends to scheduler: |
| |
| scheduler.Handle(ctx, runner.Msg{ |
| To: signal.Spec.Node + "." + signal.Spec.Port, |
| From: "signal", |
| Data: signal.Spec.Data, |
| }) |
+------------------------------------------------------------------------+
|
v
4. COMPONENT EXECUTION
+------------------------------------------------------------------------+
| Scheduler routes to Runner |
| Runner calls Component.Handle() |
| Flow execution continues from there |
+------------------------------------------------------------------------+
|
v
5. SIGNAL CLEANUP
+------------------------------------------------------------------------+
| Controller deletes TinySignal CR before delivery (fire-and-forget) |
| Signals are one-off and ephemeral |
+------------------------------------------------------------------------+TinySignal Controller
go
// tinysignal_controller.go
func (r *TinySignalReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// Only leader processes signals to avoid duplicate execution
if !r.IsLeader.Load() {
return ctrl.Result{}, nil
}
signal := &v1alpha1.TinySignal{}
if err := r.Get(ctx, req.NamespacedName, signal); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Check if signal belongs to this module
if !r.Scheduler.HasInstance(signal.Spec.Node) {
return ctrl.Result{}, nil
}
// Delete signal first (fire-and-forget)
r.Delete(ctx, signal)
// Route to scheduler
r.Scheduler.Handle(ctx, runner.Msg{
To: fmt.Sprintf("%s.%s", signal.Spec.Node, signal.Spec.Port),
From: "signal",
Data: signal.Spec.Data,
})
return ctrl.Result{}, nil
}Creating Signals
From Component Output (Cross-Module)
When a component sends to a node in a different module:
go
// resource/manager.go
func (m *Manager) CreateSignal(ctx context.Context, req CreateSignalRequest) error {
signal := &v1alpha1.TinySignal{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("signal-%s", uuid.New().String()),
Namespace: m.namespace,
Labels: map[string]string{
"tinysystems.io/node-name": req.NodeName,
},
},
Spec: v1alpha1.TinySignalSpec{
Node: req.NodeName,
Port: req.Port,
Data: req.Data,
},
}
return m.client.Create(ctx, signal)
}From HTTP Server
The http-module creates signals for incoming requests:
go
func (h *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Parse request
body, _ := io.ReadAll(r.Body)
// Create signal to trigger the flow
h.resourceManager.CreateSignal(ctx, CreateSignalRequest{
NodeName: h.nodeName,
Port: "request",
Data: map[string]interface{}{
"method": r.Method,
"path": r.URL.Path,
"headers": r.Header,
"body": body,
},
})
// Wait for response...
}From UI Control Port
When user clicks a button:
go
// Platform creates signal for control port action
signal := &v1alpha1.TinySignal{
Spec: v1alpha1.TinySignalSpec{
Node: "ticker-abc123",
Port: "_control",
Data: map[string]interface{}{
"start": true,
},
},
}Owner References
Signals can be owned by nodes for automatic cleanup:
yaml
metadata:
ownerReferences:
- apiVersion: operator.tinysystems.io/v1alpha1
kind: TinyNode
name: my-node-abc123
uid: "..."When the TinyNode is deleted, associated signals are garbage collected.
Signal Lifecycle
Created --> Deleted --> Delivered
(fire-and-forget)
Typical lifetime: < 1 secondSignals are one-off and ephemeral - they are deleted before delivery and exist only long enough to trigger execution.
Watching Signals
Controllers filter signals by labels:
go
func (r *TinySignalReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.TinySignal{}).
WithEventFilter(predicate.Funcs{
CreateFunc: func(e event.CreateEvent) bool {
// Only process signals for nodes we manage
signal := e.Object.(*v1alpha1.TinySignal)
return r.Scheduler.HasInstance(signal.Spec.Node)
},
}).
Complete(r)
}Best Practices
1. Include Node Label
go
signal.Labels["tinysystems.io/node-name"] = nodeName2. Set Owner References
go
signal.OwnerReferences = []metav1.OwnerReference{
*metav1.NewControllerRef(node, v1alpha1.GroupVersion.WithKind("TinyNode")),
}3. Handle Timeouts
Signals should complete quickly. For long operations, use async patterns:
go
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) any {
// Quick acknowledgment
go func() {
// Long-running work
result := longOperation()
output(context.Background(), "result", result)
}()
return nil // Return quickly
}Next Steps
- Controller Reconciliation - Reconciliation patterns
- Message Flow - How messages travel
- Cross-Module Communication - gRPC routing