Skip to content

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 second

Signals 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"] = nodeName

3. 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

Build flow-based applications on Kubernetes