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
annotations:
tinysystems.io/signal-nonce: "unique-id-12345"
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 │
└────────────────────────────────────────────────────────────────────────┘
│
▼
2. CONTROLLER DETECTION
┌────────────────────────────────────────────────────────────────────────┐
│ TinySignalReconciler watches for new signals │
│ - Only LEADER pod processes signals (avoid duplicates) │
│ - Checks nonce annotation for deduplication │
└────────────────────────────────────────────────────────────────────────┘
│
▼
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, │
│ }) │
└────────────────────────────────────────────────────────────────────────┘
│
▼
4. COMPONENT EXECUTION
┌────────────────────────────────────────────────────────────────────────┐
│ Scheduler routes to Runner │
│ Runner calls Component.Handle() │
│ Flow execution continues from there │
└────────────────────────────────────────────────────────────────────────┘
│
▼
5. SIGNAL CLEANUP
┌────────────────────────────────────────────────────────────────────────┐
│ Controller deletes TinySignal CR after processing │
│ (Signals are ephemeral, not persisted) │
└────────────────────────────────────────────────────────────────────────┘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
nodeName := signal.Spec.Node
if !r.Scheduler.HasInstance(nodeName) {
// Not our signal, ignore
return ctrl.Result{}, nil
}
// Check nonce for deduplication
nonce := signal.Annotations["tinysystems.io/signal-nonce"]
if r.hasProcessedNonce(nonce) {
// Already processed, delete and skip
r.Delete(ctx, signal)
return ctrl.Result{}, nil
}
// Route to scheduler
err := r.Scheduler.Handle(ctx, runner.Msg{
To: fmt.Sprintf("%s.%s", signal.Spec.Node, signal.Spec.Port),
From: "signal",
Data: signal.Spec.Data,
})
// Mark nonce as processed
r.markNonceProcessed(nonce)
// Delete signal after processing
r.Delete(ctx, signal)
return ctrl.Result{}, err
}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,
},
Annotations: map[string]string{
"tinysystems.io/signal-nonce": uuid.New().String(),
},
},
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,
},
},
}Signal Deduplication
The nonce annotation prevents duplicate processing:
yaml
metadata:
annotations:
tinysystems.io/signal-nonce: "unique-id-12345"go
type TinySignalReconciler struct {
processedNonces sync.Map
}
func (r *TinySignalReconciler) hasProcessedNonce(nonce string) bool {
_, exists := r.processedNonces.Load(nonce)
return exists
}
func (r *TinySignalReconciler) markNonceProcessed(nonce string) {
r.processedNonces.Store(nonce, time.Now())
// Clean old nonces (optional, prevent memory leak)
go r.cleanOldNonces()
}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 ──▶ Reconciled ──▶ Processed ──▶ Deleted
Typical lifetime: < 1 secondSignals are ephemeral - they 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. Always Use Nonces
go
signal.Annotations["tinysystems.io/signal-nonce"] = uuid.New().String()2. Include Node Label
go
signal.Labels["tinysystems.io/node-name"] = nodeName3. Set Owner References
go
signal.OwnerReferences = []metav1.OwnerReference{
*metav1.NewControllerRef(node, v1alpha1.GroupVersion.WithKind("TinyNode")),
}4. 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