Öfterliga signaler - Att omvandla atomer till ett känselnätverk (Svenska (Swedish))

Öfterliga signaler - Att omvandla atomer till ett känselnätverk

Friday, 12 December 2025

//

21 minute read

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.

NuGet Licens

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 signalnamen
  • OperationId - Operationen som tog tillbaka den
  • Key - Operationen
  • Timestamp - När spänningen uppstod
  • WasPatternMatch - Verkligen om tillbakatagen via RetractMatching
  • Pattern - 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 CancelOnSignals och DeferOnSignals

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


Finding related posts...
logo

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