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
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 second

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

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

Build flow-based applications on Kubernetes