Ephemere Signale - Atome in ein Sensing Network verwandeln
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'.
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 SignalnameOperationId- Die Operation, die sie zurückgezogen hatKey- Der Schlüssel der Operation (falls vorhanden)Timestamp- Wenn es zu einer Retraktion kamWasPatternMatch- Stimmt, wenn überRetractMatchingPattern- 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
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 -
OnSignalkehrt 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
CancelOnSignalsundDeferOnSignals
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
- Teil 1: Feuer und nicht Ganz und gar nicht Vergessen - Theorie und Muster
- Teil 2: Aufbau einer wiederverwendbaren Ephemeral Execution Library - die vollständige Umsetzung
- Stigmatisierung (Wikipedia) - indirekte Koordinierung durch Umweltänderung
- Leistungsschalter-Muster
- Reaktive Erweiterungen