Signales éphémères - Transformation d'atomes en réseau de détection (Français (French))

Signales éphémères - Transformation d'atomes en réseau de détection

Friday, 12 December 2025

//

26 minute read

Un petit primitive qui transforme le travail simultané en un système d'adaptation coordonné, .

"Le patron des signaux éphémèresM SK1

À l'intérieur Partie 1 Nous avons construit une exécution éphémère : Partie 2 Nous avons transformé cette bibliothèque en une bibliothèque réutilisable avec des coordonnateurs, pipelines clés , et l’intégration de DI.

Cet article ajoute une petite fonctionnalité qui change tout: signals.

NUGET!!!

Cela est maintenant dans le mostlylucid.ephemerals Nuget paquet aussi plus que 20 les patrons éphémères de la plupart du temps clairs et 'atoms.

NuGet Licence

Les fichiers de source

Le code source complet se trouve dans le mostlylucid.atoms GitHub repository

L’infrastructure de signalisation vit dans:

Dossier But
Signals.cs SignalEvent, SignalRetractedEvent, SignalPropagation, SignalConstraints, SignalSink, ISignalEmitter, AsyncSignalProcessor
Opération éphémère.cs Émission de signaux et retrait des opérations
Options éphémères.cs Configuration réactive (CancelOnSignals, DeferOnSignals, OnSignal, OnSignalRetracted)
StringPatternMatcher.cs GlobM SK1manifestation du modèle pour le filtrage des signaux
SignalDispatcher.cs Routage de signaux async avec correspondance du modèle (supports *, ?, listes de commasM SK1 ordre déterministe)
Exemples/SignalingHttpClient.cs Frais d'échantillonnage -émission de signaux formés pour les appels HTTP
Exemples/AdaptiveTranslationService.cs Limitation du taux d'adaptation avec déferralisation basée sur le signal
Exemples/SignalBasedCircuitBreaker.cs Modèle de lecture des signaux éphémères par l'interrupteur de circuits
Exemples/TelemetrySignalHandler.cs Traitement de signaux asynchrones avec intégration télémétrique

Le problème: Atomes isolés

Nos coordonnateurs éphémères sont excellents dans le travail de traitement, mais ils sont isolés.

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

Nous pourrions connecter des dépendances explicites, mais cela crée un couplage. sensibilisation à l'environnement - Coordonnateurs capables de percevoir leur environnement sans être directement reliés à celui-ci

Les signaux permettent aux atomes d'exécution de laisser des traces dans leur fenêtre éphémère.

  • "Ce coefficient d'utilisation de l'API est juste-limitéM SK2
  • " Cet utilisateur a échoué
  • "A sibling operation is still waiting on the gateway "

Les coordonnateurs peuvent alors changer leur comportement en fonction des signaux visibles dans leur fenêtre.


La solution: Signals sur les opérations

À partir de Signals.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);
}

Une opération peut susciter des signaux pendant l’exécution. Ces signaux vivent dans la fenêtre éphémère côte à côté de l’opération. Lorsque l'opération vieillit, les signaux passent avec elle.

Il n’y a pas de courtier en messages, il n’existe pas d’infrastructure distincte, c’est-à-dire seulement des chaînes attachées aux opérations.

Comme les signaux vivent à l’intérieur de la fenêtre éphémère, ils héritent des garanties suivantes :


Les lois sur les signaux éphémères

Loi 1 - Les signaux ne freinent jamais implicitement l'exécution

Un signal, par lui-mêmeM SK1 n'entraîne pas l'exécution. Il n’enregistre qu’un fait dans la fenêtre éphémère. Rien ne fonctionne parce qu'un signal a été émis.

Droit 2 - OnSignal Is ExplicitM SK2 Local, and Optional

Si un coordonnateur définit un gestionnaire OnSignal, il fonctionne en synchronité lorsqu'un signal est émis - mais seulement parce que le coordonneur a choisi d'y attacher . Les émetteurs ne savent pas ou s’inquiètent. Le retrait de tous les gestionnaires laisse le comportement du noyau inchangé.

Loi 3 - Les signaux sont locaux pour l'atome

Un signal est attaché uniquement à l'atome qui l'a émis. Aucun signal ne mutait jamais ou n'annotait un autre atome. Il n'y a pas de bus commun à écrire.

Loi 4 - Les signaux sont appliquésM SK2 Seulement les faits

L'émission d'un signal ajoute un fait à l'histoire de l'atome Les signaux ne sont jamais mis à jour ou superscrits. Les retraits enlèvent uniquement les signaux propres de l’émetteur’.

Loi 5 - La surface du signal est délimitée et le temps de la transmission est limité

Les signaux ne sont présents que dans la fenêtre éphémère du coordonnateur’ Ils expirent automatiquement à mesure que la fenêtre vieillit. Rien n’est persisté à moins que vous ne créez explicitement la persistance.

Droit 6 - Les observateurs lisent, Ils ne disent pas

Lorsqu’un coordonnateur “ vérifie les signaux de la clé KM SK1 il scanne:

les atomes dans sa fenêtre

et les signaux locaux à ces atomes Les observateurs ne modifient jamais l'état du signal d'un atome’

Droit 7 - Événement- Le comportement similaire est une coucheM SK3 Pas un élément primaire

Si vous voulez que les signaux conduisent des flux de travail async,, vous devez utiliser:

Système de diffusion des signaux

AsyncSignalProcessor

ou d'autres adapters.

Il s’agit de couches facultatives construites sur les signaux, qui ne font pas partie de leur sémantique

Droit 8 - Pas de croisementM SK2 Mutation atomique sous toutes circonstances

Aucun atome ne peut modifier les signaux de l'état snapshot,, , ou des métadonnées d'un autre atome.. La coordination se fait par le biais de:

  • signals

  • de l’inspection

  • Les fenêtres

  • Politiques

Non par écrit.

Droit 9 - Les points de vue mondiaux sont dérivés, Néanmoins inchangés

Une surface agrégée de signalSink ou d’éclosion- peut présenter une vue combinée des signaux M SK1 mais il est toujours libellé-seulement,ne s'agit jamais d'un texte authentique ,ne peut jamais être écritM SK3

Loi 10 - Le retrait des manutentionnaires donne le même système de base

Si tous les gestionnaires (OnSignal,dispatchersM SK2processeurs ) sont détachésMSC4 Le système demeure entièrement correct et prévisible. Les signaux ont toujours un sens parce qu’ils sont des faits.

La meilleure analogie: empreintes de pied, Pas d'instruction

Événements sont comme un appel de téléphone:

"I'Je vous appele maintenantM SK2 Prenez la parole et réagissez ."

Signales sont comme les empreintes de pied dans la neige:

"J’ai laissé des empreintes de pied . Si vous voulez savoir où j’étais allée - regardez. Si ce n’est pas le cas

C’est la raison pour laquelle les signaux ne brisent jamais, ne bloquent jamais , et n’interagissent jamais avec le flux de commande à moins que vous Choisissez pour les interroger.

Qu'est-ce que cela retient

La distinction est importante parce que les signaux éliminent

Problème Événements l'ont-ils survenu MSC2 Signales évitent de le faire MSC3
Coupling Éditeur MSC2 Abonnés MSC3 Aucune SMC4
Dépendances en matière de calendrier Réaction immédiate requise MSC2 Milieu ambiant MSC3 sondage lorsqu'il est prêt SMC4
Loops de rétroaction Les gestionnaires peuvent déclencher les gestionnaires Aucune boucle à moins que cela ne soit explicitement demandé
Sémantique de l'ordonnance Questions relatives à l'ordre Irrelevant M
Garanties de livraison Doit être livré/ manipulé Aucune livraisonM SK4 seulement l'existence ≥
Propagation de l'erreur Les erreurs du gestionnaire se propagent Isolée M
Risques liés à la réintroduction courants
Défaillances causées par la cascade Défaut d'un gestionnaire MSC2 rupture de chaîne MSC3 Aucune chaîne à briser SMC4

Comparaison rapide

Caractéristique Événements MSC2 Signaux MSC3
Coupling Strong MSC2publisher MSC3 subscribers MSc4 USC5 Aucune USC6
Échéancier Immédiat
Livraison Garantie/essayée Aucune livraisonM SK4 juste existence
Réaction Exigé MSC2 Optionnel MSC3
Charge de données Souvent lourde MSC2 Méthodes métadonnées à chaînes minces MSC3
Entreposage Aucune Sliding LRUM SK3style window M
Durée de vie Immédiate MSC2 Décompose automatiquement MSC3
Modes de défaillance Bon nombre CMS2 Presque aucun CMS3

La différence dans le code

Approche des événements (classiqueM SK1

public event Action RateLimited;

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

Problèmes:

  • Qui réagit?
  • Dans quel ordre?
  • Qu'arrive-t-il si deux opérateurs sont en conflit?
  • Que se passera-t-il si un gestionnaire lance?
  • Qu’arrive-t-il s’il est le mauvais moment?
  • Que faire si l’appelant ne veut pas ce comportement'

Approche de signalisation (éphémèreM SK1

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

Aucune chose ne se passe ici. Plus tard, ailleurs complètement différentM SK2

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

Notes:

  • Aucun couplage direct
  • Aucun appel de surprise
  • Il n'y a pas de sauts d'exécution
  • Il n'y a pas de retour d'appel dans l'enfer
  • No cross-thread weirdness
  • Il ne réagit que lorsque nous demander d'examiner

Ligne "Aha!"

Contrôle du transfert d’événements. Contexte de transfert des signaux.

Il s’agit de l’ensemble du modèle mental dans une seule phrase.

Quels sont en réalité les signaux

Selon votre contexte:

Audience Définition
Les penseurs des systèmes Un substrat stigmère pour la coordination indirecte
Les ingénieurs Données métadonnées légères attachées aux opérations dans une fenêtre de glissage délimitée
PL/Nards de compétition Un tableau temporel implicite , parallèle à la sémantique de coïncidence limitée
Utilisateurs du cadre Une surface en état de nettoyage dans un procédé, à l'intérieur d'un processus, d'une surface autonome, d’un processus2 et d’une surface propre que vous pouvez demander à tout moment.

Les signaux sont des traces laissées dans une surface de mémoire partagée. SIGMERGIQUE Modèle de coordination - le même système que les souris utilisent , le same système que l’AI précoce utilisait


Comment cela se compare à d'autres approches

Application Insights / OpenTelemetry

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

Le meilleur pour: Traçage réparti entre les services, longsM SK2 stockage de télémétrie à durée déterminée , ID de corrélationMSC4

Utiliser la télémétrie lorsque: Vous devez tracer les demandes dans plusieurs services , Mettre à disposition des métriques de stockage pour l’analyse M SK2 ou les intégrer aux outils de surveillance .

Utiliser des signaux éphémères lorsque: Vous avez besoin d’une coordination réactive dans le processus de la prise en considération de l’environnement , ou vous ne voulez pas d’infrastructures de télémétrie

Extensions réactives (Rx)

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

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

Le meilleur pour: Traitement complexe des événements, tempsM SK2 opérations fondées sur , combinaison de plusieurs flux d'événements

Utiliser Rx lorsque: Vous avez besoin de réponses temporelles complexes.

Utiliser des signaux éphémères lorsque: Vous voulez un sondage plus simple - Sensage basé sur les données, Nettoyage automatiqueM SK3 ou intégration avec le suivi de l’exploitationMSC4

Notifications MediatR

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

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

Le meilleur pour: Découplé dans la manipulation des événements de traitement avec plusieurs gestionnaires .

Utiliser MediatR lorsque: Vous voulez que plusieurs gestionnaires réagissent à un même événement de façon synchrone.

Utiliser des signaux éphémères lorsque: Vous voulez une perception ambiante sans abonnement expliciteM SK1 vous-même - histoire de nettoyage, ou intégration avec l'exécution limitéeMSC4

Polly Circuit Breaker

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

Le meilleur pour: Résilience à l'égard des appels individuels avec gestion automatique de l'état

Utiliser Polly lorsque: Vous avez besoin d’une résilience à l’appel par -, avec demi-automatisme.

Utiliser des signaux éphémères lorsque: Vous voulez une sensibilisation à l'environnement dans de nombreuses opérations , Logique des circuits personnalisés M SK2 ou intégration avec le suivi de l'opération .

Combiner les deux: Utilisez Polly à l'intérieur de votre corps de travail , émettez des signaux lorsque les circuits se déplacent

Tableau de comparaison

Approche Automne - Nettoyage Sensation de l'environnement SMK4 Découplé SMK5 Logique personnalisée MVK6 Intégration SVK7
OpenTelemetry ❌ ≥ ≤❌ =✅ ± ~❌ ─ Outils externes
Extensions réactives ❌ 3 4 5 6 7 8 9
MediatR ❌ SMK4 SMK5 FS6 FS7 SFS8 BS9 Manuel PS10
Disparateur de circuits Polly
Signales éphémères ✅ ≥ ≤✅ ✅ MSC6 MSC7 SMC8 Construit SMC9 dans SMC10

Relever et retracer les signaux

Mise en œuvre des opérations 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; }
}

À l’intérieur de votre corps de travail:

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;
    }
});

Les signaux sont simplement des chaînes. Utiliser des noms simples ("rate-limit") ou noms structurels ("rate-limit:5000ms"). Les filtres à modèles utilisent la sémantique du globe (*, ?) et listes de commas d'appui ("error.*,timeout"). Le parallèle est déterministe et l’allocation StringPatternMatcher.

Fine-Exemple de signal granulé:Calls HTTP

Pour une observabilité très détaillée, vous pouvez émettre des signaux à chaque étape d'une opération . La bibliothèque comprend un échantillon Signalisation du client Http qui démontre ce modèle:

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...
});

Il émet des signaux à chaque étape:

Signale Quand MSC2
stage.starting Avant le début de la demande
progress:0 Cote de progrès initiale
stage.request Requête HTTP envoyée
stage.headers Les en-têtes de réponse ont été reçus
stage.reading Commençant à lire le corps
progress:XX Pourcentage d’avancement
stage.completed Déchargement terminé

Vous pouvez ensuite les interroger avec le paramètre correspondant:

// 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
}

Retracter les signaux

Les opérations peuvent aussi retirer leurs propres signaux. Ceci est utile pour les états temporaires:

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;
    }
});

Événements de rétraction

Tout comme l’émission de signaux, les rétractions peuvent déclencher des appels de rappelM SK1

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);
        }
    });

L'article 5 de la Loi sur l'immigration et le statut des réfugiés SignalRetractedEvent inclut:

  • Signal - Nom du signal retiré
  • OperationId - L’opération qui l’a retirée
  • Key - La clé de l’opérationM SK1 ( s’il y a une,)
  • Timestamp - Lorsque la rétraction a eu lieu
  • WasPatternMatch - Oui si retraité par RetractMatching
  • Pattern - Le modèle utilisé si le modèle correspond à celui de l’autre

Réel-Exemple mondiale:Récupération des limites de vitesse

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;
    }
});

Systèmes de détection

Tous les coordonnateurs fournissent une demande de signalisation optimisée:

// 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-Procédure réactive

À partir de Options éphémères.cs:

Les coordonnateurs peuvent réagir automatiquement aux signaux:

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)
    });

Lorsqu'un signal est entré CancelOnSignals est détecté, de nouveaux articles sont passés à l'écart M SK1 comptabilisé comme ayant échoué ). Lorsqu'un signal est entré DeferOnSignals est détecté, nouveaux articles attendant jusqu’à ce que le signal se clarifieM SK1


Réel-Exemple mondiale: Limitation du taux d'adaptation

À partir de 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;
    }
}

Toutes les instances de ce service s'activent automatiquement lorsque les limites du taux sont atteintes. Aucun état partagéM SK1 Aucun message passible. Je n'ai que lu la fenêtre éphémère .


Réel-Exemple mondiale:CrossM SK2Connaissance du coordonnateur

Plusieurs coordonnateurs peuvent se sentir mutuellement grâce à un 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);
    }
}

Surveillance de la santé

[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")
        }
    });
}

Aucune bibliothèque de métrique n'est nécessaire. Demander simplement la fenêtre éphémère.


Réel-Exemple mondialeM SK1Signal-Breakout de circuit basé

À partir de 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);

L'interrupteur de circuit n'a pas son propre état - il lit simplement la fenêtre éphémèreM SK1


Constraints de signalisation: Prévention des boucles infinies

À partir de Signals.cs:

Lorsque les signaux peuvent causer d’autres signaux, vous risquez des boucles infiniesM SK1 SignalConstraints Prévient ce

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);
        }
    }
};

Propagation de signaux

Suivre la causalité avec 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);
    }
}

La chaîne de propagation suit le chemin: order-placed → inventory-reserved → ...

Si inventory-reserved a tenté d'émettre order-placed, il serait bloqué (cycle detectedM SK2


L’espace de signal global SignalSink:

À partir de Signals.cs:

Pour les signaux qui doivent être visibles à travers les coordonnateurs:

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; }
}

Usage:

// 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 Matching with StringPatternMatcher

À partir de StringPatternMatcher.cs:

Glob-combinaison de style pour le filtrage des signauxM SK1

// 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
    }
}

Conventions sur la désignation des signaux

Maintenir les signaux simples et cohérents:

// 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");

Traitement du signal async

Traiteurs de signaux synchrones (OnSignal) fonctionner sur l'opération's threadM SK2les maintenir rapidementMSC3 Pour faire IMSSK4O -travail lié~, les signaux de ventilateurs vers un chemin async avec SignalDispatcher (compatibilité du modèleM SK1ordre déterministe ) ou 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 cheap and enqueue
var coordinator = new EphemeralWorkCoordinator<Request>(
    body,
    new EphemeralOptions
    {
        OnSignal = dispatcher.Dispatch
    });

Soutien des paramètres *, ?, et listes de commas ("error.*,timeout"). Tous les gestionnaires de correspondance fonctionnent dans l'ordre d'enregistrement sur un coordonnateur à clés de fond.

AsyncSignalProcessor

Pour le traitement async autonome:

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));

Exemple d'un gestionnaire de signatures de télémétrie

Un exemple complet de la combinaison du traitement des signaux async avec l’intégration télémétrique:

Source: TelemetrySignalHandler.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();
}

Connectez-le à votre coordonnateur:

await using var telemetryHandler = new TelemetrySignalHandler(telemetryClient);

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

Le gestionnaire:

  • Ne bloque jamais le thread d'opération - OnSignal retourne immédiatement
  • Catégories des signaux par préfixe pour différents types de télémétrie
  • Exposer les métriques (queuedM SK1 processed , dropped) pour surveillance de la santé
  • Mémoire de Bounds - déverse les signaux les plus anciens si la queue se remplit

Pourquoi cela fonctionne?

Les signaux sont puissants parce qu’ils d'une durée indéterminée:

Biens Avantage
Bounded Peut-il ' ne croître sans limite - les vieux signaux vieillissent
Utilisation autonome Aucun code de nettoyage n'est nécessaire
Découplé Les émetteurs ne connaissent pas les auditeurs ' ne savent pas de ceux-ci
Observables Tout code peut percevoir l'état actuel
Sous-traitance Aucune donnée d'utilisateur - simplement des noms de signaux
Soutien rapide OM SK1 détection à courte distance-circuiting

La fenêtre éphémère est déjà là pour le débugging. Les signaux lui donnent simplement une signification sémantique.


Conclusion

Les signaux transforment les atomes d'exécution isolés en Réseau de détection. Chaque coordonnateur peut

  • Émettre les signaux de ce qu'il a vécu
  • Sens signals provenant de sa propre histoire ou d'un bassin commun
  • Réaction par l'entremise de CancelOnSignals et DeferOnSignals

Aucun courtier de message. Aucun État partagé. Aucun protocole de coordinationM SK2 Seules les opérations avec des métadonnées qui se détruisent naturellement .

Les atomes ne communiquent pas directement entre eux, ils laissent des traces dans une fenêtre éphémère que d'autres peuvent observer.

Fire... SignaleM SK1 Sens... ForgetMSC3


Liens

Finding related posts...
logo

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