This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Signales éphémères - Transformation d'atomes en réseau de détection
Friday, 12 December 2025
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.
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éeKey- La clé de l’opérationM SK1 ( s’il y a une,)Timestamp- Lorsque la rétraction a eu lieuWasPatternMatch- Oui si retraité parRetractMatchingPattern- 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 -
OnSignalretourne 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
CancelOnSignalsetDeferOnSignals
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
- Partie 1: feu et Don't Bien sûr. Forget (en anglais seulement) - la théorie et le modèle
- Partie 2: Établissement d'une bibliothèque réutilisable d'exécution temporaire - la mise en œuvre complète
- Stigmergy (Wikipedia) - Coordination indirecte par modification de l'environnement
- Circuit Breaker Pattern
- Extensions réactives