Öfterliga signaler - Att omvandla atomer till ett känselnätverk
Ett litet primitiv som omvandlar samarbets till ett koordinerat adaptiv system
"Efenomen av tidiga signaler
I Del 1 vi byggde efenerala överdrag - begränsade , privata M SK2 själva - städa asyncka arbetsflöden MSC4 I Del 2 vi omvandlade det till en återanvändbar bibliotek med koordinatorer
Denna artikel lägger till en liten egenskap som ändrar allt signaler.
NUGET!!!
Detta är nu även i det mest uppenbara packaget Nuget mer än 20.
källdates
Den fulla källkoden finns i mostlylucid.atoms GitHubs arkiv
Signalinfrastrukturen bor i:
| Dossier | syfte | |||
|---|---|---|---|---|
| Signaler.cs | SignalEvent, SignalRetractedEvent, SignalPropagation, SignalConstraints, SignalSink, ISignalEmitter, AsyncSignalProcessor |
|||
| EphemeralOperation | Signalutsläpp och dämpning från operationer | |||
| ÄphemeralaOptioner.cs | SignalCancelOnSignals, DeferOnSignals, OnSignal, OnSignalRetracted) |
|||
| StringPatternMatcher.cs | Glob | |||
| SignalDispatcher.cs | Asynkt signalruttning med mönster matchande ( stöder *, ?, kommalister , deterministisk ordning |
|||
| exempel/Attecknande Httpklient.cs | Sammafinning - integrerad signalutsläpp för HTTP-sändningar | |||
| exempel/AdaptiveTranslationService.cs | Adaptiv hastighetsgränsning med signal | |||
| exempel/SignalBasedCircuitBreaker.cs | Omkretsbrytare läser tidiga signalfenster | |||
| exempel/TelemetrySignalHandler.cs | Asynkt signalbehandling med telemetriintegrering |
Problemet : Isolerade atomer
Våra tidiga koordinatorer är bra på att processa arbetet, men de är isolerade. Varje koordinator vet om sina egna aktiviteter, men har ingen aning om vad som händer på andra platser i systemet.
// 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
Vi skulle kunna koppla upp explicita beroenden, men det skapar kuppling. omgivningsmedvetenhet - koordinatorer som kan känna sin omgivning utan att vara direkt anslutna
Signaler låter utövningsatomer lämna spår i sina efemeriska fönster.
- "Det här API-värdet är rätt
- " Den här användaren har misslyckats
- "En syskonoperation väntar fortfarande på dörren
Koordinatorerna kan sedan ändra sitt beteende baserat på de signaler som är synliga i deras fönster.
Lösningen: Signaler på operationer
Från Signaler.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);
}
En operation kan lyfta signaler under uitvoeringen. De signalerna lever i den efenerala venstern bredvid operationen . När operationen åldras ut, går signalerna med
Det är det.
Eftersom signaler lever inuti den efemeriska fönstret, härvt de sina garantier, :, begränsad storlek,,, automatisk åldrande, , och noll livscykel överhuvudtaget.
De tidiga signallagarna
lag 1 - Signaler Never Implicitly Trigger Execution
Ett signal, i sig självt , orsakar inte utdrag Den registrerar bara en fakta i den efenerella venstern. Inget går igång eftersom en signal skickats ut.
lag 2 - Upptecknen är uppenbar , Lokal , och optionell
Om en koordinator definierar en OnSignal-handlerare, så kör den samtidigt när en signal släpps ut - men bara för att koordinatorn valde att ansluta den utsändare vet inte eller bryr sig Att ta bort alla handler lämnar kärnans beteende oföränderligt.
lag 3 - Signaler är lokala för atomen
Ett signal är kopplat endast till atomen som utstrålade det. Ingen signal muterar eller summerar någon annan atom Det finns inga delade skrivbara bussarna.
Rättsväsendet 4 - Signaler är anhängande-Som fakta
Att skicka en signal lägger till ett faktum i atomens historia Signaler är aldrig uppdaterade eller överskrivna. Retraktioner tar bort bara utsändarens egen signaler
lag 5 - Signalens yta är begränsad och tid
Signaler existerar bara i koordinatorn De försvinner automatiskt när vensteret åldras. Inget blir upprätt förrän du explicitt bygger upp upprätthållighet.
Rättsväsendet 6 - Observatörer läser , De skriver inte
När en koordinator “ser signaler för nyckeln K”, scannar den
atomerna i sitt fönster
och signalerna lokalt till atomerna Bekymmerna modifierar aldrig en atom
lag 7 - händelsen- liknande beteende är en ytaM SK3 inte ett primitivt
Om du vill att signaler driver async arbetsflöden, måste du använda::
Signaldispatcher
AsyncSignalProcessor
eller andra adapter.
Dessa är optionella lager byggda ovanpå signaler, inte en del av deras semantik.
lag 8 - Ingen korsningM SK2 Atommutation under alla omständigheter
Ingen atom kan förändra syntavlan, ,, tillståndet, ,, signalerna,,, eller metadata från en annan atom. Koordinering sker genom:
signaler
känsel
fönster
policyr
Inte genom skrivar.
Rättsväsendet 9 - Globala synvinklar är avkomma
En SignalSink eller svamm -samlagd yta kan visa en kombinerad syn på signaler — men den är alltid läsbar
lag 10 - Att ta bort handledare ger samma grundsystem
Om alla hållare (OnSignal, utsändare , processorerM SK3 är avkopplade systemet är fullt korrekt och förutsägbart. Signaler har fortfarande betydelse eftersom de är fakta.
Den bästa analogin: Yttermärken , Inte Instruktioner
Erfarelser är som ett telefonsamtal
" jag ' jag ska ringa dig just nu . plocka upp och reagera
Signaler är som fotavtryck i snön
" Jag lämnade fotavtryck
Detta är anledningen till att signaler aldrig bryter, aldrig blockerar och aldrig interagerar med kontrollflödet om du inte välja att svara på dem
Vad detta tar bort
Skillnaden spelar roll eftersom signaler eliminerar
| Problemet | Erfarenheterna har det ♫ ♫ | ♫ Signaler hindrar det ♫ | |||||||
|---|---|---|---|---|---|---|---|---|---|
| Koppling ♫ ♫ | ♫ Publisher ♫ | ||||||||
| Timingsdependenser | Omedelbart reagerad | Omgivningen | МSK3 | undersökning när den är redo | |||||
| Feedback-loop | Handler kan aktivera handler | МSK2 | Inga loop om inte explicitt frågats | ||||||
| Ordering semantics | Order matters ♫ ♫ | ♫ Irrelevant ♫ | |||||||
| Tillförselgarantier | måste levereras | / | skött | МSK3 | ingen tillförsel | , | bara existering | ||
| Bredsling av fel | Handler fel sprider | Isolerad | |||||||
| Rädslor för återkomst | Vanligt ♫ ♫ | ♫ Otänkbart ♫ | |||||||
| Förluster i kaskad | En handledare misslyckas | , | kedjan går sönder | МSK3 | Ingen kedja som går sönder |
Vinnig jämförelse
| Funktion | Erfarenheter | Signaler | ||||||
|---|---|---|---|---|---|---|---|---|
| Koppling | Stabilt | Belag | МSK3 | Abonnärer | ||||
| Timing | Omedelbart | Omgivningen | ||||||
| Tillförsel | Gwaranterad | / | försökt | ingen tillförsel | ||||
| Reaktion | Erforderlig | Optionell | МSK3 | |||||
| Data-payload | ofta tunga | МSK2 | små metadata | |||||
| lagring | Ingen | Sliding LRU | LRU 3 | Stilfenster | ||||
| livstid | Omedelbart | Förrutsnar automatiskt | ||||||
| Misslyckande režimer | Många ♫ ♫ | ♫ Nästan ingen ♫ |
Skillnaden i kod
Erfarenhetssätt (klassisktM SK1
public event Action RateLimited;
try
{
await CallApiAsync();
}
catch (RateLimitException)
{
RateLimited?.Invoke(); // Makes someone else act right now
}
Problemet:
- Vem reagerar?
- I vilken ordning?
- Tänk om två användare konflikterar?
- Tänk om en handledare kastar?
- Tänk om det är fel tid
- Tänk om telefonören inte vill ha det här beteendet
Signalmetod (ephemeral):
try
{
await CallApiAsync(ct);
}
catch (RateLimitException ex)
{
op.Signal($"rate-limit:{ex.RetryAfterMs}ms");
throw;
}
Inget händer här.
if (translator.HasSignal("rate-limit"))
{
await Task.Delay(1000); // Act when *we* choose
}
Märkter:
- Ingen direkt anslutning
- Inga överraskande samtal
- Inga uträttelsesspringar
- Ingen återkoppling i helvetet
- Inget kors-störningstråd
- Bara när vi fråga att titta
Linsen "Aha!"
Event-överföring-kontroll. Signalöverföringskontext
Det är hela den mentala modellen i en mening.
Vad signaler egentligen är
beroende på din bakgrund:
| Publiken | Definitionen | |||
|---|---|---|---|---|
| Systemtänkande | Ett stigmergiskt substans för indirekt koordinering | |||
| Ingenieurer | Lightweight metadata som fästes vid operationer i ett begränsat rörande fönster | |||
| PL/Konkurentskördor | En impliserad, temporal svarta tavla, kombinerad med begränsad samtida semantik. | |||
| Framework-användare | En in-process, -, ,, själv--, renande yta som du kan bläddra vid varje tid. |
Signaler är spår som lämnas i ett gemensamt minne. stigmergisk modell av koordinering - samma mönster som använder , samma svarta bordssystem som användes i tidiga AI
Hur det ser ut jämfört med andra metoder
Application Insights / OpenTelemetry
using var activity = source.StartActivity("ProcessOrder");
activity?.SetTag("order.id", orderId);
activity?.SetTag("rate.limited", true);
Den bästa för: Distribuerad spårning över hela tjänsterna , långvarig M SK2 termisk telemetrins lagring , korrelationsidentifikationer
Använd telemetri när: Du behöver spåra begäran över flera tjänster , spara mätinstrument för analysen , eller integrera det med övervakningsverktyg
Använd tidiga signaler när: Du behöver i processmässigt omgivningsmedvetenhet
Reaktiva utlängningar (Rx)
var rateLimits = Observable.FromEventPattern<RateLimitEventArgs>(
h => api.RateLimitHit += h,
h => api.RateLimitHit -= h);
rateLimits
.Throttle(TimeSpan.FromSeconds(1))
.Subscribe(e => HandleRateLimit(e));
Den bästa för: komplext händelsefördelning , tid, - operationer baserade på , kombinerade flera händelseflöden
Använd Rx när: Du behöver komplexa temporära sökningar ( vingande , avbrytande M SK3 kombinerade flöden MSC4
Använd tidiga signaler när: Du vill ha en enklare rösting -baserade känseln M SK2 automatisk renning , eller integrering med operationsföljning
MediatR-avsikter
public class RateLimitNotification : INotification
{
public int RetryAfterMs { get; init; }
}
await _mediator.Publish(new RateLimitNotification { RetryAfterMs = 5000 });
Den bästa för: Avkopplad i-process handling av händelser med flera skötare
Använd MediatR när: Du vill att flera sköterare ska reagera på samma händelse samtidigt
Använd tidiga signaler när: Du vill ha omgivningskänning utan explicit subskryption
Polly-kretsarbrytare
var circuitBreaker = Policy
.Handle<HttpRequestException>()
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
Den bästa för: Robusthet kring individuella samtal med automatiskt tillståndskontroll
Använd Polly när: Du behöver per-uppkopplingsförmåga med automatisk halvans -öppen | / | instängda förvandlar |. |
Använd tidiga signaler när: Du vill ha omgivningsmedvetenhet över många operationer , skräddarsydda kretslogik
Kombinera dem: Använd Polly inuti din arbetskropp , släpp ut signaler när kretsar triggas
Vergleichstabellen
| Närhållningssätt | Själva | - | rengörning ♫ | Ambient Sensing ♫ | ||
|---|---|---|---|---|---|---|
| OpenTelemetry | ||||||
| Reaktiva utlängningar | ❌ ♫ ♫ | ♫ | ||||
| MediatR ♫ ♫ | ♫ | |||||
| Polly-kretsbrytare | ||||||
| Äphemerala signaler | ✅ |
Att lyfta och dra tillbaka signaler
Operationer implementerar 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; }
}
Inuti din arbetskropp:
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;
}
});
Signaler är bara strängar. Använd enkla namn ("rate-limit") eller strukturerade namn"rate-limit:5000ms").
Pattern-filter använder glob-semantik (*, ?) och stödja kommalister ("error.*,timeout"). matchning är deterministisk och alokation StringPatternMatcher.
Fint- Ett färgat signalexempla : HTTP sändningar
För mycket detaljerad observabilitet, kan du skicka ut signaler i varje steg av en operation. Biblioteket består av ett exemplar SignaliseringHttpClient som visar detta mönster:
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...
});
Detta släpper ut signaler på varje steg:
| Signal | |
|---|---|
stage.starting |
Innan begäran börjar |
progress:0 |
Tillgångspunkt |
stage.request |
HTTP-frågan skickad |
stage.headers |
Response naglarna fick |
stage.reading |
Att börja läsa kroppen |
progress:XX |
Förcentage av framsteg |
stage.completed |
ladda ner färdigt |
Du kan sedan fråga dessa med mönster matchande:
// 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
}
Att dra tillbaka signaler
Operationer kan också ta bort sina egna signaler.
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;
}
});
Motsägelseer
Precis som signalutsläppen
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);
}
});
Den SignalRetractedEvent inkluderar:
Signal- Den återvunna signalnamenOperationId- Operationen som tog tillbaka denKey- OperationenTimestamp- När spänningen uppstodWasPatternMatch- Verkligen om tillbakatagen viaRetractMatchingPattern- Pattern som användes om mönstret matchar
verkligt -World Example: 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;
}
});
Sensorsignaler
Alla koordinatorer tillhandahåller optimerade signalsökningar:
// 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-Reaktiv bearbetning
Från ÄphemeralaOptioner.cs:
Koordinatorer kan automatiskt reagera på signaler:
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)
});
När en signal kommer in CancelOnSignals är upptäckt, nya punkter är hoppade över ( räknats som fel
När en signal kommer in DeferOnSignals är upptäckt, nya punkter vänta tills signalen klarar sig.
Verklig-World Example: Adaptiv hastighetsbegränsning
Från AdaptivTranslationService.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;
}
}
Varje instans av den här tjänsten avbryter sig automatiskt när hastighetsgränserna är träffade. Inget delat tillståndet . Inga meddelanden går igenom. Bara att läsa den efemerala vensternM SK3
Verklig-World Example: CrossM SK2 Koordinatormedvetenhet
Flera koordinatorer kan känna varandra genom en gemensam 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);
}
}
verkligt -World Example: hälsoövervakning
[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")
}
});
}
Ingen mätライブryre behövs. Bara fråga i den efemerala venstern.
Verklig -World Example: Signal-Based Circuit Breaking
Från SignalbasedCircuitBreaker.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);
Den kretsbrytare har ingen egen tillstånd. - den läser bara det efemerala oknet.
Signalbegränsningar: Att förhindra oändliga kretsar
Från Signaler.cs:
När signaler kan orsaka andra signaler SignalConstraints förhindrar detta:
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);
}
}
};
Signalförökning
spåra orsakssambandet med 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);
}
}
Förökningskedjan följer banan: order-placed → inventory-reserved → ...
Om inventory-reserved försökte utsläppa order-placed, det skulle bli blockerat ( cykel upptäckt
SignalSink: Global Signal Space
Från Signaler.cs:
För signaler som behöver vara synliga över koordinatorer:
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; }
}
Användning:
// 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();
}
Pattern matchar med StringPatternMatcher
Från StringPatternMatcher.cs:
Glob-stilmatchning för signalfilterning:
// 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 Naming Konventioner
Håll signalerna enkla och konsekventa:
// 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");
Async Signalbehandling
Synkroniserade signalbehandlingar (OnSignal) köra på operationen ' threaden, — sköta dem snabbt, . Att göra I/ OM SK5 gränslig arbete, , driva ventilatorsignaler in i en async bana med SignalDispatcher (möjlig matchning av mönster AsyncSignalProcessor.
SignalDispatcher ventilator
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 cheap and enqueue
var coordinator = new EphemeralWorkCoordinator<Request>(
body,
new EphemeralOptions
{
OnSignal = dispatcher.Dispatch
});
Patterns stöd *, ?, och kommalister ("error.*,timeout"). Alla matchande handler körs i registrationsordning på en koordinator med bakgrundsklyter ; utsläppen är fortfarande sinkroniserad
AsyncSignalProcessor
För standalone async-processing:
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));
TelemetrySignalHandler exempel
Ett komplett exempel på att kombinera async-signalbehandling med telemetriintegration:
källa: TelemetrisignalHandler.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();
}
koppla upp det till din koordinator:
await using var telemetryHandler = new TelemetrySignalHandler(telemetryClient);
await using var coordinator = new EphemeralWorkCoordinator<Request>(
ProcessAsync,
new EphemeralOptions
{
OnSignal = signal => telemetryHandler.OnSignal(signal)
});
Handlaren:
- Aldrig blockerar operationstråden -
OnSignalåtervänder omedelbart - Kategoriserar signaler med prefix för olika typer av telemetri
- Exposerar mätetal (
- Avtryck i minnet - släpper ned de äldsta signalerna om köen fyller upp
Varför det här fungerar
Signaler är kraftfulla eftersom de efemeral:
| egendom | Belöning | |||
|---|---|---|---|---|
| Avgränsad | KanM SK1 inte växa obegränsad - gamla signaler åldras | |||
| Självt städning | Ingen städkod behövs | |||
| Avkopplade | utsändare vet inte ' vet inte om lyssnare | |||
| Observerbara | Alla koder kan känna den nuvarande tillståndet | |||
| Privat | Inga användardata - bara signalernas namn | |||
| Vinnig | OM SK1 detektion med korta apparater |
Det efemerala vensteret finns redan för att lösa problem.
Slutsatsen
Signaler omvandlar isolerade utövningsatomer till känselnätverk. Varje koordinator kan
- Emit signaler om vad den upplevde
- Förnuft signaler från sin egen historia eller ett gemensamt vattenkoll
- reagera automatiskt via
CancelOnSignalsochDeferOnSignals
Inga meddelandesmakare . Inga gemensamt tillstånd. Inga koordineringsprotokoll. Bara operationer med metadata som helt naturligt bryts ut
Atomerna pratar inte med varandra direkt, de lämnar bara spår i ett efemeriskt fönster som andra kan observera.
Brand ... Signal... Bemärkelse... FörgettnaM SK3
Links
- Del 1: Brand och Don Ganska Glöm - teorin och mönstret
- Del 2: Att bygga en återanvändbar ephemeral Execution Library - fulla implementering
- Stigmergy (Wikipedia) - indirekt koordinering genom att modifiera miljön
- Sirkelbrytaremönster
- Reaktiva Erweiterunger