Ephemere Signale - Atome in ein Sensing Network verwandeln (Deutsch (German))

Ephemere Signale - Atome in ein Sensing Network verwandeln

Friday, 12 December 2025

//

22 minute read

Ein winziges Primitiv, das gleichzeitige Arbeit in ein koordiniertes, adaptives System verwandelt.

"Das ephemere Signalmuster"

Im Teil 1 Wir bauten ephemere Ausführung - begrenzt, private, selbstreinigende async Workflows. In Teil 2 Wir haben daraus eine wiederverwendbare Bibliothek mit Koordinatoren, Schlüsselleitungen und DI-Integration gemacht.

Dieser Artikel fügt ein kleines Feature hinzu, das alles verändert: Signale.

NUGET!!!

Dies ist jetzt in der meistlucid.ephemerals Nuget-Paket auch mehr als 20 meistlucid.ephemerale Muster und 'Atome'.

NuGet Lizenz

Quelldateien

Der vollständige Quellcode ist in der meistlucid.atoms GitHub-Repository

Die Signalinfrastruktur lebt in:

Datei Zweck

------ ---------
EphemeralOperation.cs Signal-Emission und -Retraktion von Operationen
EphemeralOptions.cs Signal-reaktive Konfiguration (CancelOnSignals, DeferOnSignals, OnSignal, OnSignalRetracted)
StringMusterMatcher.cs Das Muster im Glob-Stil passt sich für die Signalfilterung an.
SignalDispatcher.cs Das Async-Signal-Routing mit Muster-Matching (unterstützt *, ?, Kommalisten, deterministische Reihenfolge)
Beispiele/SignalingHttpClient.cs Proben feinkörnige Signalemission für HTTP-Aufrufe
Beispiele/AdaptiveTranslationService.cs Anpassende Geschwindigkeitsbegrenzung mit signalbasiertem Deferral
Beispiele/SignalBasedCircuitBreaker.cs Schaltungsbrecher, der ephemeres Signalfenster liest
Beispiele/TelemetrieSignalHandler.cs Die Verarbeitung von Async-Signalen mit Telemetrie-Integration

Das Problem: Isolierte Atome

Unsere ephemeren Koordinatoren sind sehr gut in der Verarbeitung von Arbeit, aber sie sind isoliert. Jeder Koordinator weiß über seine eigenen Operationen, aber hat kein Bewusstsein für das, was anderswo im System geschieht.

// Translation coordinator has no idea that...
await translationCoordinator.EnqueueAsync(request);

// ...the API just hit a rate limit
// ...another service is experiencing backpressure
// ...a downstream dependency is slow

Wir könnten explizite Abhängigkeiten verdrahten, aber das schafft Kopplung. Was wir wollen, ist Umgebungsbewusstsein - Koordinatoren, die ihre Umwelt spüren können, ohne direkt miteinander verbunden zu sein.

Signale lassen Ausführungsatome Spuren in ihrem ephemeren Fenster hinterlassen. Diese Spuren verhalten sich wie kurzlebige Fakten:

  • "Diese API nur rate-limited"
  • "Dieser Benutzer hat 3 mal versagt"
  • "Eine Geschwisteroperation wartet immer noch auf dem Gateway"

Koordinatoren können dann ihr Verhalten ändern, basierend auf den Signalen, die in ihrem Fenster sichtbar sind. Das ist Umgebungsbewusstsein ohne Abhängigkeiten.


Die Lösung: Signale für den Betrieb

Von Signale.cs:

public readonly record struct SignalEvent(
    string Signal,
    long OperationId,
    string? Key,
    DateTimeOffset Timestamp,
    SignalPropagation? Propagation = null)
{
    public int Depth => Propagation?.Depth ?? 0;
    public bool WouldCycle(string signal) => Propagation?.Contains(signal) == true;
    public bool Is(string name) => Signal == name;
    public bool StartsWith(string prefix) => Signal.StartsWith(prefix, StringComparison.Ordinal);
}

Eine Operation kann Signale während der Ausführung anheben. Diese Signale leben im ephemeren Fenster neben der Operation. Wenn die Operation aus altert, gehen die Signale mit ihm.

Kein Nachrichtenmakler, keine separate Infrastruktur, nur Strings, die an Operationen gebunden sind.

Weil Signale innerhalb des ephemeren Fensters leben, erben sie ihre Garantien: begrenzte Größe, automatisches Altern und Null-Lebenszyklus über Kopf.


Die ephemeren Signalgesetze

Gesetz 1 - Signale führen niemals unplizit die Ausführung aus

Ein Signal allein verursacht keine Hinrichtung. Es zeichnet nur eine Tatsache im ephemeren Fenster auf. Nichts läuft, weil ein Signal gesendet wurde.

Gesetz 2 - OnSignal ist explizit, lokal und optional

Wenn ein Koordinator einen OnSignal-Handler definiert, läuft er synchron, wenn ein Signal ausgesendet wird - aber nur, weil der Koordinator ihn angehängt hat. Emitter wissen es nicht oder kümmern sich nicht. Das Entfernen aller Handler lässt das Kernverhalten unverändert.

Gesetz 3 - Signale sind im Atom lokal

Ein Signal wird nur an das Atom/die Operation angeschlossen, die es emittiert hat. Kein Signal mutiert oder kommentiert ein anderes Atom. Es gibt keinen gemeinsamen beschreibbaren Bus.

Gesetz 4 - Signale sind nur Fakten

Das Senden eines Signals hängt eine Tatsache an die Geschichte des Atoms an. Signale werden nie aktualisiert oder überschrieben. Retraktionen entfernen nur die Signale des Emitters.

Gesetz 5 - Die Signaloberfläche ist gesperrt und zeitlich begrenzt

Signale existieren nur innerhalb des ephemeren Fensters des Koordinators. Sie verfallen automatisch, wenn das Fenster aus altert. Nichts ist beharrlich, es sei denn, man baut ausdrücklich Beharrlichkeit auf.

Gesetz 6 - Beobachter lesen, sie schreiben nicht

Wenn ein Koordinator die Signale auf Schlüssel K. prüft, scannt er:

die Atome in ihrem Fenster

und die Signale lokal zu diesen Atomen Beobachter ändern niemals den Signalzustand eines Atoms.

Gesetz 7 - Ereignisähnliches Verhalten ist eine Ebene, keine Primitive

Wenn Signale async-Workflows antreiben sollen, müssen Sie Folgendes verwenden:

SignalDispatcher

AssyncSignalProzessor

oder andere Adapter.

Dies sind optionale Schichten, die auf Signalen aufgebaut sind, nicht Teil ihrer Semantik.

Gesetz 8 - Keine Kreuzatommutation unter Umständen

Kein Atom darf den Snapshot, Zustand, Signale oder Metadaten eines anderen Atoms verändern. Die Koordinierung erfolgt durch:

  • Signale

  • Sensorik

  • Fenster

  • Politikbereiche

Nicht durch Briefe.

Gesetz 9 - Weltanschauungen werden abgelenkt, nie veränderbar

Eine SignalSink- oder Schwarmaggregierte Oberfläche kann eine kombinierte Ansicht von Signalen zeigen — aber es ist immer nur lesen, nie maßgeblich, nie beschreibbar.

Gesetz 10 - Entfernen von Handlern bringt das gleiche Kernsystem

Wenn alle Handler (OnSignal, Dispatcher, Prozessoren) gelöst sind, das System bleibt vollständig korrekt und vorhersehbar. Signale haben immer noch Bedeutung, weil sie Fakten sind, keine Auslöser.

Die beste Analogie: Fußabdrücke, keine Anweisungen

Veranstaltungen sind wie ein Anruf:

"Ich rufe dich an, geh ran und reagiere."

Signale sind wie Fußabdrücke im Schnee:

"Ich habe Fußabdrücke hinterlassen. Wenn du wissen willst, wo ich hingegangen bin - schau, wenn es dir egal ist - ignoriere es."

Aus diesem Grund Signale nie brechen, nie blockieren, und nie mit Steuerungsfluss interagieren, es sei denn, Sie wählen um sie zu befragen.

Was dies entfernt

Die Unterscheidung ist wichtig, weil Signale eliminieren:

Probleme, die sich ergeben, haben Signale, die es vermeiden |---------|:--------------:|:----------------:| Publisher → Abonnenten Timing-Abhängigkeiten. Sofortige Reaktion erforderlich. Ambient, Umfrage, wenn bereit. Feedback-Schleifen Handler können Handler auslösen Keine Schleifen, es sei denn, explizit gefragt Beauftragen von Semantiken Beauftragen Beauftragen Unrelevant Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Beauftragen Lieferung garantiert muss geliefert / behandelt werden Keine Lieferung, nur Existenz Fehler-Verbreitung; Fehler-Verbreitung; Fehler-Verbreitung; Fehler-Verbreitung; Fehler-Verbreitung; Fehler-Verbreitung; Fehler-Verbreitung; Gefahren für die Wiederentspannbarkeit Häufig Unmöglich Ein Handler versagt, Kette bricht Keine Kette zu brechen

Schneller Vergleich

Ereignis-Feature, Signale, |---------|--------|---------| Starke Koppelung (Veröffentlicher → Abonnenten) Timing Sofortiges Ambient Keine Lieferung, nur Existenz Reagieren erforderlich Fakultativ • Daten-Payload • Häufig schwer • Kleine String-Metadaten • Nicht-Speichern-Fenster im LRU-Stil Lebenslanges, sofortiges Abscheiden automatisch Fehler Modi Viele Fast keine

Der Unterschied im Code

Veranstaltungskonzept (klassisch):

public event Action RateLimited;

try
{
    await CallApiAsync();
}
catch (RateLimitException)
{
    RateLimited?.Invoke(); // Makes someone else act right now
}

Probleme:

  • Wer reagiert?
  • In welcher Reihenfolge?
  • Was, wenn zwei Handler kollidieren?
  • Was, wenn ein Handler wirft?
  • Was, wenn es die falsche Zeit ist?
  • Was, wenn der Anrufer dieses Verhalten nicht will?

Signalanflug (ephemer):

try
{
    await CallApiAsync(ct);
}
catch (RateLimitException ex)
{
    op.Signal($"rate-limit:{ex.RetryAfterMs}ms");
    throw;
}

Hier passiert nichts. Später, irgendwo ganz anders:

if (translator.HasSignal("rate-limit"))
{
    await Task.Delay(1000);   // Act when *we* choose
}

Anmerkungen:

  • Keine direkte Verbindung
  • Keine Überraschung Anrufe
  • Keine Hinrichtungssprünge
  • Keine Rückruf-Hölle
  • Keine Kreuzgewinde-Wurzel
  • reagiert nur, wenn wir fragen zu suchen

Die "Aha!"-Linie

Ereignisübertragungskontrolle. Signalübertragungskontext.

Das ist das gesamte mentale Modell in einem Satz.

Was wirklich Signale sind

Je nach Hintergrund:

Begriffsbestimmung des Publikums |----------|------------| | Denker der Systeme Ein stigmatisiertes Substrat für die indirekte Koordination | Ingenieure Leichte Metadaten, die an Operationen in einem begrenzten Schiebefenster angehängt werden | PL/Konkurrenz-Nerds Eine implizite, zeitliche Tafel gepaart mit begrenzter Konkurrenzsemantik | Rahmennutzer Eine im Prozess befindliche, selbstreinigende Zustandsoberfläche, die Sie jederzeit abfragen können

Signale sind Spuren, die in einer gemeinsamen, begrenzten Speicheroberfläche hinterlassen werden. Jeder kann sie ansehen. Niemand ist verpflichtet zu reagieren. Stigmatisierung Modell der Koordination - die gleiche Ameisen verwenden, die gleiche Tafelsysteme in frühen KI verwendet, und die gleichen modernen CRDT Klatsch-Netzwerke weisen auf.


Wie dies mit anderen Ansätzen verglichen wird

Anwendungs-Insights / OpenTelemetrie

using var activity = source.StartActivity("ProcessOrder");
activity?.SetTag("order.id", orderId);
activity?.SetTag("rate.limited", true);

Das Beste für: Verteilte Rückverfolgung über Dienstleistungen, Langzeit-Telemetrie-Speicher, Korrelations-IDs.

Telemetrie verwenden, wenn: Sie müssen Anfragen über mehrere Dienste verfolgen, Metriken zur Analyse speichern oder mit Monitoring-Tools integrieren.

Verwende ephemere Signale, wenn: Sie brauchen In-Prozess-Ambient-Bewusstsein, reaktive Koordination, oder wollen keine Telemetrie-Infrastruktur.

Reaktive Erweiterungen (Rx)

var rateLimits = Observable.FromEventPattern<RateLimitEventArgs>(
    h => api.RateLimitHit += h,
    h => api.RateLimitHit -= h);

rateLimits
    .Throttle(TimeSpan.FromSeconds(1))
    .Subscribe(e => HandleRateLimit(e));

Das Beste für: Komplexe Ereignisverarbeitung, zeitbasierte Operationen, Kombination mehrerer Ereignisströme.

Rx verwenden, wenn: Sie benötigen komplexe zeitliche Abfragen (Fenstern, Debouncing, Kombinieren von Streams).

Verwende ephemere Signale, wenn: Sie wollen eine einfachere Umfrage-basierte Erfassung, automatische Reinigung oder Integration mit Operation Tracking.

MediatR-Benachrichtigungen

public class RateLimitNotification : INotification
{
    public int RetryAfterMs { get; init; }
}

await _mediator.Publish(new RateLimitNotification { RetryAfterMs = 5000 });

Das Beste für: Entkoppeltes In-Process-Event-Handling mit mehreren Handlern.

MediatR verwenden, wenn: Sie möchten, dass mehrere Handler synchron auf dasselbe Ereignis reagieren.

Verwende ephemere Signale, wenn: Sie wollen Umgebungserfassung ohne explizites Abonnement, Selbstreinigungsgeschichte oder Integration mit begrenzter Ausführung.

Polly Circuit Breaker

var circuitBreaker = Policy
    .Handle<HttpRequestException>()
    .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));

Das Beste für: Resilienz um individuelle Anrufe mit automatischer Staatsführung.

Polly verwenden, wenn: Sie benötigen per-Call-Resilienz mit automatischen halboffenen/geschlossenen Übergängen.

Verwende ephemere Signale, wenn: Sie wollen Umgebungsbewusstsein über viele Operationen, benutzerdefinierte Schaltungslogik oder Integration mit Operation Tracking.

Kombinieren Sie sie: Verwenden Sie Polly in Ihrem Arbeitskörper, senden Sie Signale, wenn Schaltungen Reise.

Vergleichstabelle

Annäherung der Selbstreinigung Ambient Sensing Entkoppelt Benutzerdefinierte Logic-Integration |----------|:-------------:|:---------------:|:---------:|:------------:|:-----------:| Offene Telemetrie Externe Werkzeuge Reaktive Erweiterungen Komplex MediatR-Handbuch Polly Circuit Breaker (Pully Circuit Breaker) | Vergängliche Signale Eingebaut


Signale erheben und zurückziehen

Durchführung der Maßnahmen ISignalEmitter:

public interface ISignalEmitter
{
    // Emit signals
    void Emit(string signal);
    bool EmitCaused(string signal, SignalPropagation? cause);

    // Retract (remove) signals
    bool Retract(string signal);
    int RetractMatching(string pattern);
    bool HasSignal(string signal);

    long OperationId { get; }
    string? Key { get; }
}

Im Inneren Ihres Arbeitskörpers:

await coordinator.ProcessAsync(async (item, op, ct) =>
{
    try
    {
        var result = await CallExternalApiAsync(item, ct);

        if (result.WasCached)
            op.Signal("cache-hit");

        if (result.Duration > TimeSpan.FromSeconds(2))
            op.Signal("slow-response");
    }
    catch (RateLimitException ex)
    {
        op.Signal("rate-limit");
        op.Signal($"rate-limit:{ex.RetryAfterMs}ms");
        throw;
    }
    catch (TimeoutException)
    {
        op.Signal("timeout");
        throw;
    }
});

Signale sind nur Strings. Verwenden Sie einfache Namen ("rate-limit") oder strukturierte Namen ("rate-limit:5000ms"). Musterfilter verwenden Glob Semantik (*, ?) und Unterstützung Komma-Listen ("error.*,timeout"). Matching ist deterministisch und Allokation-Licht über StringPatternMatcher.

Feinkörniges Signal Beispiel: HTTP-Aufrufe

Für eine sehr detaillierte Beobachtbarkeit, können Sie Signale in jeder Phase einer Operation aussenden. Die Bibliothek enthält eine Probe SignalisierungHttpClient Das zeigt dieses Muster:

using Mostlylucid.Helpers.Ephemeral.Examples;

// Inside your work body where you have access to the operation's emitter:
await coordinator.ProcessAsync(async (request, op, ct) =>
{
    var data = await SignalingHttpClient.DownloadWithSignalsAsync(
        httpClient,
        new HttpRequestMessage(HttpMethod.Get, request.Url),
        op,  // ISignalEmitter
        ct);

    // Process the downloaded data...
});

Dies sendet Signale in jeder Phase aus:

Signal, wenn |--------|------| | stage.starting Vor Beginn der Anfrage | progress:0 Erstes Fortschrittszeichen | stage.request HTTP-Anfrage gesendet | stage.headers Ansprech-Header erhalten | stage.reading Beginnen, Körper zu lesen | progress:XX Progress-Prozentsatz (0-100) während des Downloads | stage.completed Download beendet

Sie können diese dann mit Muster-Matching abfragen:

// Find all stage transitions
var stages = coordinator.GetSignalsByPattern("stage.*");

// Check download progress
var progress = coordinator.GetSignalsByPattern("progress:*");

// Check if any download is still in progress
if (coordinator.HasSignalMatching("stage.reading") &&
    !coordinator.HasSignalMatching("stage.completed"))
{
    // Download in progress
}

Signale zurückziehen

Operationen können auch ihre eigenen Signale entfernen. Dies ist nützlich für temporäre Zustände:

await coordinator.ProcessAsync(async (item, op, ct) =>
{
    // Mark as processing
    op.Emit("processing");

    try
    {
        await ProcessItemAsync(item, ct);

        // Success - retract the processing signal
        op.Retract("processing");
        op.Emit("completed");
    }
    catch (RetryableException)
    {
        // Keep processing signal, add retry info
        op.Emit("retrying");
    }
    catch (Exception)
    {
        // Remove all temporary signals
        op.RetractMatching("processing*");
        op.Emit("failed");
        throw;
    }
});

Retraktionsereignisse

Genau wie die Signalemission können Retraktionen Rückrufe auslösen:

var coordinator = new EphemeralWorkCoordinator<Request>(
    body,
    new EphemeralOptions
    {
        // Sync retraction handler
        OnSignalRetracted = evt =>
        {
            _metrics.DecrementGauge(evt.Signal);
            Console.WriteLine($"Signal {evt.Signal} retracted from op {evt.OperationId}");

            if (evt.WasPatternMatch)
                Console.WriteLine($"  (matched pattern: {evt.Pattern})");
        },

        // Async retraction handler
        OnSignalRetractedAsync = async (evt, ct) =>
        {
            await _telemetry.TrackRetraction(evt.Signal, evt.OperationId, ct);
        }
    });

Das SignalRetractedEvent umfasst:

  • Signal - Der eingefahrene Signalname
  • OperationId - Die Operation, die sie zurückgezogen hat
  • Key - Der Schlüssel der Operation (falls vorhanden)
  • Timestamp - Wenn es zu einer Retraktion kam
  • WasPatternMatch - Stimmt, wenn über RetractMatching
  • Pattern - Das verwendete Muster (wenn Muster übereinstimmen)

Real-World Beispiel: Rate Limit Recovery

await coordinator.ProcessAsync(async (request, op, ct) =>
{
    // Check if we already have a rate limit signal
    if (op.HasSignal("rate-limited"))
    {
        // We're in recovery mode
        await Task.Delay(1000, ct);
    }

    try
    {
        var response = await _api.SendAsync(request, ct);

        // Success! Remove any rate limit signal
        if (op.Retract("rate-limited"))
        {
            op.Emit("rate-limit-cleared");
        }
    }
    catch (RateLimitException ex)
    {
        op.Emit("rate-limited");
        op.Emit($"rate-limit:{ex.RetryAfterMs}ms");
        throw;
    }
});

Signale sensibilisieren

Alle Koordinatoren bieten eine optimierte Signalabfrage:

// Check if any recent operation hit a rate limit
if (coordinator.HasSignal("rate-limit"))
{
    await Task.Delay(1000);
}

// Count slow responses in the window
var slowCount = coordinator.CountSignals("slow-response");
if (slowCount > 10)
{
    await ThrottleAsync();
}

// Get signals by pattern
var httpErrors = coordinator.GetSignalsByPattern("http.error.*");

// Get signals since a time
var recentSignals = coordinator.GetSignalsSince(DateTimeOffset.UtcNow.AddMinutes(-1));

// Get signals for a specific key
var userSignals = coordinator.GetSignalsByKey("user-123");

Signal-Reaktive Verarbeitung

Von EphemeralOptions.cs:

Koordinatoren können automatisch auf Signale reagieren:

var coordinator = new EphemeralWorkCoordinator<Request>(
    body,
    new EphemeralOptions
    {
        // Cancel new work if these signals are present
        CancelOnSignals = new HashSet<string> { "system-overload", "circuit-open" },

        // Defer new work while these signals are present
        DeferOnSignals = new HashSet<string> { "rate-limit" },
        MaxDeferAttempts = 10,
        DeferCheckInterval = TimeSpan.FromMilliseconds(100)
    });

Wenn ein Signal in CancelOnSignals wird erkannt, neue Elemente werden übersprungen (als fehlgeschlagen gezählt). Wenn ein Signal in DeferOnSignals wird erkannt, neue Elemente warten, bis das Signal löscht.


Real-World Beispiel: Adaptive Rate Limiting

Von AdaptiveTranslationService.cs:

public class AdaptiveTranslationService : IAsyncDisposable
{
    private readonly EphemeralWorkCoordinator<TranslationRequest> _coordinator;
    private readonly ITranslationApi _translationApi;

    public AdaptiveTranslationService(ITranslationApi translationApi)
    {
        _translationApi = translationApi;

        _coordinator = new EphemeralWorkCoordinator<TranslationRequest>(
            ProcessTranslationAsync,
            new EphemeralOptions
            {
                MaxConcurrency = 8,
                MaxTrackedOperations = 100,

                // New work is deferred while any "rate-limit" or "rate-limit:*" signal is present
                DeferOnSignals = new HashSet<string> { "rate-limit", "rate-limit:*" },
                MaxDeferAttempts = 10,
                DeferCheckInterval = TimeSpan.FromMilliseconds(100)
            });
    }

    public async Task TranslateAsync(TranslationRequest request)
    {
        // Optional: extra politeness based on most recent retry-after
        var rateLimitSignals = _coordinator.GetSignalsByPattern("rate-limit:*");
        if (rateLimitSignals.Count > 0)
        {
            var latest = rateLimitSignals
                .OrderByDescending(s => s.Timestamp)
                .First()
                .Signal; // "rate-limit:5000ms"

            if (TryParseRetryAfter(latest, out var delay))
            {
                await Task.Delay(delay);
            }
        }

        await _coordinator.EnqueueAsync(request);
    }

    public static bool TryParseRetryAfter(string signal, out TimeSpan delay)
    {
        delay = default;
        var parts = signal.Split(':', 2);
        if (parts.Length != 2) return false;

        var payload = parts[1].Trim();
        if (!payload.EndsWith("ms", StringComparison.OrdinalIgnoreCase)) return false;

        var numPart = payload[..^2];
        if (!int.TryParse(numPart, out var ms) || ms < 0) return false;

        delay = TimeSpan.FromMilliseconds(ms);
        return true;
    }
}

Jede Instanz dieses Dienstes schaltet sich automatisch ab, wenn die Geschwindigkeitsbegrenzungen getroffen werden. Kein geteilter Zustand. Keine Nachricht. Einfach das ephemere Fenster lesen.


Real-World Beispiel: Cross-Coordinator-Bewusstsein

Mehrere Koordinatoren können sich durch eine gemeinsame SignalSink:

public class OrderProcessingSystem
{
    private readonly SignalSink _sharedSignals = new(maxCapacity: 1000);

    private readonly EphemeralWorkCoordinator<Order> _orderProcessor;
    private readonly EphemeralWorkCoordinator<PaymentRequest> _paymentProcessor;

    public OrderProcessingSystem()
    {
        var options = new EphemeralOptions { Signals = _sharedSignals };

        _orderProcessor = new EphemeralWorkCoordinator<Order>(
            ProcessOrderAsync, options);

        _paymentProcessor = new EphemeralWorkCoordinator<PaymentRequest>(
            ProcessPaymentAsync, options);
    }

    public async Task ProcessOrderAsync(Order order)
    {
        // Check shared signals for payment gateway issues
        if (_sharedSignals.Detect("gateway-error"))
        {
            await _retryQueue.EnqueueAsync(order);
            return;
        }

        await _orderProcessor.EnqueueAsync(order);
    }
}

Real-World Beispiel: Gesundheitsüberwachung

[HttpGet("/health/detailed")]
public IActionResult GetDetailedHealth()
{
    return Ok(new
    {
        translation = new
        {
            pending = _translationCoordinator.PendingCount,
            active = _translationCoordinator.ActiveCount,
            recentRateLimits = _translationCoordinator.CountSignals("rate-limit"),
            recentTimeouts = _translationCoordinator.CountSignals("timeout"),
            recentSuccess = _translationCoordinator.CountSignals("success"),
            hasErrors = _translationCoordinator.HasSignalMatching("error.*")
        },
        payment = new
        {
            pending = _paymentCoordinator.PendingCount,
            gatewayErrors = _paymentCoordinator.CountSignals("gateway-error"),
            declines = _paymentCoordinator.CountSignals("declined"),
            approvals = _paymentCoordinator.CountSignals("approved")
        }
    });
}

Keine Metriken-Bibliothek erforderlich. Abfragen Sie einfach das ephemere Fenster.


Real-World Beispiel: Signalbasierte Schaltung bricht

Von SignalbasiertCircuitBreaker.cs:

public class SignalBasedCircuitBreaker
{
    private readonly string _failureSignal;
    private readonly int _threshold;
    private readonly TimeSpan _windowSize;

    public SignalBasedCircuitBreaker(
        string failureSignal = "failure",
        int threshold = 5,
        TimeSpan? windowSize = null)
    {
        _failureSignal = failureSignal;
        _threshold = threshold;
        _windowSize = windowSize ?? TimeSpan.FromSeconds(30);
    }

    public bool IsOpen<T>(EphemeralWorkCoordinator<T> coordinator)
    {
        var recentFailures = coordinator.GetSignalsSince(
            DateTimeOffset.UtcNow - _windowSize);

        return recentFailures.Count(s => s.Signal == _failureSignal) >= _threshold;
    }

    public int GetFailureCount<T>(EphemeralWorkCoordinator<T> coordinator)
    {
        var recentFailures = coordinator.GetSignalsSince(
            DateTimeOffset.UtcNow - _windowSize);

        return recentFailures.Count(s => s.Signal == _failureSignal);
    }
}

// Usage
var circuitBreaker = new SignalBasedCircuitBreaker("api-error", threshold: 3);

if (circuitBreaker.IsOpen(_coordinator))
{
    throw new CircuitOpenException("Too many recent API errors");
}

await _coordinator.EnqueueAsync(request);

Der Leistungsschalter hat keinen eigenen Zustand - er liest nur das ephemere Fenster.


Signalbeschränkungen: Unendliche Schleifen verhindern

Von Signale.cs:

Wenn Signale andere Signale verursachen können, riskieren Sie unendliche Schleifen. SignalConstraints verhindert dies:

var options = new EphemeralOptions
{
    SignalConstraints = new SignalConstraints
    {
        // Max propagation depth before blocking
        MaxDepth = 10,

        // Prevent A → B → A cycles
        BlockCycles = true,

        // Signals that end propagation chains
        TerminalSignals = new HashSet<string> { "completed", "failed", "resolved" },

        // Signals that emit but don't propagate
        LeafSignals = new HashSet<string> { "logged", "metric" },

        // Callback when a signal is blocked
        OnBlocked = (signal, reason) =>
        {
            _logger.LogWarning("Signal {Signal} blocked: {Reason}",
                signal.Signal, reason);
        }
    }
};

Signalverbreitung

Verfolgen Sie die Kausalität mit EmitCaused:

public void HandleSignal(SignalEvent evt, ISignalEmitter emitter)
{
    if (evt.Is("order-placed"))
    {
        // This signal carries the propagation chain
        // Will be blocked if it would create a cycle
        emitter.EmitCaused("inventory-reserved", evt.Propagation);
    }
}

Die Ausbreitungskette verfolgt den Weg: order-placed → inventory-reserved → ...

Wenn inventory-reserved versucht zu emittieren order-placed, es würde blockiert werden (Zyklus erkannt).


Der SignalSink: Globaler Signalraum

Von Signale.cs:

Für Signale, die über die Koordinatoren sichtbar sein müssen:

public sealed class SignalSink
{
    private readonly ConcurrentQueue<SignalEvent> _window;
    private readonly int _maxCapacity;
    private readonly TimeSpan _maxAge;

    public SignalSink(int maxCapacity = 1000, TimeSpan? maxAge = null);

    // Raise signals
    public void Raise(SignalEvent signal);
    public void Raise(string signal, string? key = null);

    // Sense signals
    public IReadOnlyList<SignalEvent> Sense();
    public IReadOnlyList<SignalEvent> Sense(Func<SignalEvent, bool> predicate);
    public bool Detect(string signalName);
    public bool Detect(Func<SignalEvent, bool> predicate);

    public int Count { get; }
}

Verwendung:

// Create a shared sink
var sink = new SignalSink(maxCapacity: 1000, maxAge: TimeSpan.FromMinutes(2));

// Configure coordinators to use it
var options = new EphemeralOptions { Signals = sink };

// Or raise signals directly
sink.Raise("system-maintenance");

// Sense from anywhere
if (sink.Detect("system-maintenance"))
{
    await DeferWorkAsync();
}

Muster passend zu StringMusterMatcher

Von StringMusterMatcher.cs:

Glob-Style-Matching für die Signalfilterung:

// Exact match
coordinator.HasSignal("rate-limit");

// Wildcard patterns
coordinator.HasSignalMatching("http.*");           // http.timeout, http.error
coordinator.HasSignalMatching("error.*.critical"); // error.payment.critical
coordinator.HasSignalMatching("user-???-failed");  // user-123-failed

// Comma-separated patterns in CancelOnSignals/DeferOnSignals
new EphemeralOptions
{
    CancelOnSignals = new HashSet<string>
    {
        "system-overload, circuit-open",  // Either pattern
        "error.*"                          // Any error signal
    }
}

Signal-Nennkonventionen

Halten Sie Signale einfach und konsistent:

// Good - simple, categorical
op.Signal("success");
op.Signal("failure");
op.Signal("rate-limit");
op.Signal("timeout");
op.Signal("cache-hit");

// Good - structured for parsing
op.Signal("rate-limit:5000ms");
op.Signal("retry:attempt-3");
op.Signal("slow:2500ms");
op.Signal("http.error:429");

// Good - hierarchical for pattern matching
op.Signal("payment.declined");
op.Signal("payment.approved");
op.Signal("payment.gateway-error");

// Avoid - entity identification belongs in Key, not signals
op.Signal("user-123-rate-limited");  // Bad

// Instead
op.Key = "user-123";
op.Signal("rate-limit");

Bearbeitung von Async-Signalen

Synchrone Signalabnehmer (OnSignal) laufen auf dem Thread der Operation – halten Sie sie schnell. Um I/O-gebundene Arbeit zu tun, signalisiert Lüfter in einen async-Pfad mit SignalDispatcher (Muster-Matching, deterministische Reihenfolge) oder AsyncSignalProcessor.

SignalDispatcher-Fan-out

await using var dispatcher = new SignalDispatcher(new EphemeralOptions
{
    MaxConcurrency = Environment.ProcessorCount,
    MaxConcurrencyPerKey = 1  // sequential per signal name by default
});

dispatcher.Register("error.*", evt => _alerts.SendAsync(evt.Signal));
dispatcher.Register("progress:*", evt => _metrics.Record(evt.Signal));

// In coordinator options, keep OnSignal chor options, keep OnSignal cheap and enqueue
var coordinator = new EphemeralWorkCoordinator<Request>(
    body,
    new EphemeralOptions
    {
        OnSignal = dispatcher.Dispatch
    });

Unterstützung von Mustern *, ?, und Kommalisten ("error.*,timeout"). Alle passenden Handler laufen in Registrierungsreihenfolge auf einem hintergrundgeschlüsselten Koordinator; emittieren bleibt synchron.

AssyncSignalProzessor

Zur eigenständigen async-Verarbeitung:

await using var processor = new AsyncSignalProcessor(
    async (signal, ct) =>
    {
        await _externalService.LogAsync(signal, ct);
    },
    maxConcurrency: 4,
    maxQueueSize: 1000);

// Enqueue signals (returns immediately)
processor.Enqueue(new SignalEvent(
    "rate-limit",
    operationId,
    key,
    DateTimeOffset.UtcNow));

TelemetrieSignalHandler Beispiel

Ein komplettes Beispiel für die Kombination von async Signalverarbeitung und Telemetrieintegration:

Quelle: TelemetrieSignalHandler.cs

public class TelemetrySignalHandler : IAsyncDisposable
{
    private readonly AsyncSignalProcessor _processor;
    private readonly ITelemetryClient _telemetry;

    public TelemetrySignalHandler(ITelemetryClient telemetry)
    {
        _telemetry = telemetry;
        _processor = new AsyncSignalProcessor(
            HandleSignalAsync,
            maxConcurrency: 8,
            maxQueueSize: 5000);
    }

    // Synchronous entry point - returns immediately
    public bool OnSignal(SignalEvent signal) => _processor.Enqueue(signal);

    private async Task HandleSignalAsync(SignalEvent signal, CancellationToken ct)
    {
        var properties = new Dictionary<string, string>
        {
            ["signal"] = signal.Signal,
            ["operationId"] = signal.OperationId.ToString(),
            ["key"] = signal.Key ?? "none"
        };

        await _telemetry.TrackEventAsync("EphemeralSignal", properties, ct);

        // Categorized tracking based on signal prefix
        if (signal.StartsWith("error"))
            await _telemetry.TrackExceptionAsync(signal.Signal, properties, ct);
        else if (signal.StartsWith("perf"))
            await _telemetry.TrackMetricAsync(signal.Signal, 1, ct);
    }

    // Expose stats for monitoring
    public int QueuedCount => _processor.QueuedCount;
    public long ProcessedCount => _processor.ProcessedCount;
    public long DroppedCount => _processor.DroppedCount;

    public async ValueTask DisposeAsync() => await _processor.DisposeAsync();
}

Überweisen Sie es an Ihren Koordinator:

await using var telemetryHandler = new TelemetrySignalHandler(telemetryClient);

await using var coordinator = new EphemeralWorkCoordinator<Request>(
    ProcessAsync,
    new EphemeralOptions
    {
        OnSignal = signal => telemetryHandler.OnSignal(signal)
    });

Der Handler:

  • Nie blockiert das Betriebsgewinde - OnSignal kehrt sofort zurück
  • Kategorisiert Signale durch Präfix für verschiedene Telemetrietypen
  • Ergibt Metriken (gequetscht, verarbeitet, fallengelassen) zur Überwachung der Gesundheit
  • Grenzen Speicher - Die ältesten Signale fallen, wenn sich die Warteschlange füllt

Warum das funktioniert

Signale sind mächtig, weil sie ephemer:

Vermögenswert |----------|---------| | Gebunden Das Alter kann nicht unbeschränkt wachsen | Selbstreinigung Kein Bereinigungscode erforderlich | Entkoppelt Emitter wissen nichts über Hörer | Beobachtbar Jeder Code kann den aktuellen Zustand spüren | Privat Keine Benutzerdaten - nur Signalnamen | Schnell Detektion von O(1) mit Kurzschluss

Das ephemere Fenster ist schon zum Debuggen da. Signale geben ihm nur semantische Bedeutung.


Schlußfolgerung

Signale verwandeln isolierte Ausführungsatome in eine Erfassungsnetz. Jeder Koordinator kann

  • emittieren Signale über das, was es erlebt hat
  • Sinn Signale aus der eigenen Geschichte oder einem gemeinsamen Waschbecken
  • Reagieren automatisch über CancelOnSignals und DeferOnSignals

Kein Nachrichten-Broker, kein geteilter Zustand, kein Koordinationsprotokoll, nur Operationen mit Metadaten, die natürlich zerfallen.

Die Atome sprechen nicht direkt miteinander - sie hinterlassen nur Spuren in dem ephemeren Fenster, das andere beobachten können. Es ist Stigmatisierung für async Systeme.

Feuer, Signal, Sinn, Vergessen.


Verknüpfungen

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.