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
mostlylucid.ephemeralM SK1complet; un système parallèle de schémas concurrents étrange dans une cache LRU
Sunday, 14 December 2025
C’est ce qui a été mon obsession la semaine dernière. Voir le Parties précédentes et ce qui a mené à cette étudeQue se passerait-il si une LRU était un contexte d’exécution.'. Il s’agit maintenant d’un ensemble de paquets Nuget 30 couvrant la plupart des principaux modèles d’exécution simultanée
Trouver la source ici: httpsMSC1github.comM SK3scottgalMST4mostlylucidMSL5atomsMSR6blobMSS7mainMSSL8Mostly Lucid MSS9ephemeralM SS10srcMISS11mosthlylucideMSMSS12ephémeralMSSS13complete
Premières nations
Lire le partie précédente sur les signaux pour un aperçu de ce qui en est fait' ses utilisations . Nous présentons ici le ReadmeM SK2md provenant du mostlylucid.ephemeralMSC4pacakage complet qui contient à la fois le core mostlylucideMNK5 paquet éphéméral et tous les patronsMRK6 MMK7atoms MNK8 |MNK9coordinateurs etcMBK10 paquets dans une seule DLL pratiqueMGK11
Le noyau
OU utiliser le noyau mostlylucid.éphémère a TINY ( littéralement 10 classesM SK2 qui vous donne toutes les fonctionnalités brutes.
Attributes & DI
OU si vous voulez l'attribut complet basé sur la routage async avec simple [EphemeralJob] et service.Add
C’est vraisemblablement le sujet de mon blog qui va se poursuivre...vous avez été avertis 🤓
Mostlylucid.EphemeralM SK1Complet
Meta-paquet qui renvoie à tous les paquets Mostlylucid.Ephemeral - exécution asynchrone limitée avec le signal-coordination basée sur .
dotnet add package mostlylucid.ephemeral.complete
Il s'agit d'un paquet méta- qui intégre tous les atomes du noyau de , et des paquets de modèles en tant que dépendances. You can safely mix this with
- NuGet résoudra à une seule version sans collisions de noms de domaine.
Quoi de neuf dans 2.0
Résilier les changements
Modèle d'abonnement SignalSink modifié:
// OLD (1.x) - Event-based
sink.SignalRaised += handler;
// NEW (2.0) - Lock-free Subscribe() returning IDisposable
using var sub = sink.Subscribe(handler);
Le paquet complet est maintenant un méta-package:
- Précédemment, toutes les sources ont été compilées en un seul DLL ( collisions de nommés à risque)
- Références aux paquets individuels en tant que dépendances
- Sécuritaire à mélanger avec des références individuelles d'emballage dans la même solution
Nouvelles fonctionnalités
Système de détection des ledgers (
Mostlylucid.Ephemeral.Atoms.Taxonomy.Ledger)DetectionLedger- Accumule les contributions du détecteur avec l'agrégation sigmoïdeDetectionContribution- Méthodes d'usineM SK1Bot(),Human(),Info(),VerifiedBot(),VerifiedGoodBot()CategoryScore.TotalWeightpropriété (rénommée deWeight)
Surveillance de la propagation des signaux - Détection du cycle et limitation de la profondeur des chaînes de signalisation
Lien de source - Débug dans la source de bibliothèque directement à partir de votre IDE
Voir docs/Taxonomy.md pour le vocabulaire commun autour du substrat et escalation.
Table des matières
- Démarrage rapide
- Coordonnateurs de base
- Configuration
- Signales
- Atomes
- Paramètres
- SignalBasedCircuitBreaker
- SignalDrivenBackpressure
- FanOut contrôlé
- AdaptiveRateService
- Démonstration de la Concurrence dynamique
- KeyedPriorityFanOut
- ReactiveFanOutPipeline
- Détecteur d'anomalie de signal
- Les lectures coordonnées des signaux
- Signalisation du client Http
- SignalLogWatcher
- Gestionnaire de signatures de télémétrie
- LongWindowDemo
- SignalReactionShowcase
- fenêtre de signature persistante
- Injection de dépendance
Démarrage rapide
using Mostlylucid.Ephemeral;
// Long-lived work coordinator
await using var coordinator = new EphemeralWorkCoordinator<WorkItem>(
async (item, ct) => await ProcessAsync(item, ct),
new EphemeralOptions { MaxConcurrency = 8 });
await coordinator.EnqueueAsync(new WorkItem("data"));
// One-shot parallel processing
await items.EphemeralForEachAsync(
async (item, ct) => await ProcessAsync(item, ct),
new EphemeralOptions { MaxConcurrency = 8 });
Enregistrement des services
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCoordinator<WorkItem>(
async (item, ct) => await ProcessAsync(item, ct),
new EphemeralOptions { MaxConcurrency = 8, MaxTrackedOperations = 128 });
builder.Services.AddEphemeralSignalJobRunner<LogWatcherJobs>();
var app = builder.Build();
app.MapPost("/", async ([FromServices] IEphemeralCoordinatorFactory<WorkItem> factory, WorkItem item) =>
{
var coordinator = factory.CreateCoordinator();
await coordinator.EnqueueAsync(item);
return Results.Accepted();
});
await app.RunAsync();
La familiarité services.AddCoordinator<T>() aides et AddEphemeralSignalJobRunner<T>() conserver l'enregistrement du service
concise, laissez à DI la propriété de l’étangM SK1runner, et faites en sorte que les nouvelles responsabilités /cacheMST4logging stories a single click awayMSC5
Attribute-emplois dirigés
mostlylucid.ephemeral.complete bourses mostlylucid.ephemeral.attributes, de sorte que les pipelines attributifs font partie du noyau
surface. Traiter le coureur comme un premier consommateur de signaux de classe -M SK2 les méthodes décorées se joignent à la même cachée
les histoires de pinning, et chaque attribut peut déclarer Priority, emploiM SK1niveau MaxConcurrency, Lane, Key sources, signal
émissions, pinM SK1expiration surrides, et retraitsMSC3
Pouvoirs d'attribut clé:
- Ordonnance des voies &: Usage
Priority,MaxConcurrency, etLanepour maintenir le travail dans l'ordre déterministe alors que les chemins chauds demeurer séparés. - Keying & tagging:
OperationKey,KeyFromSignal,KeyFromPayload, et[KeySource]vous aider à travailler en groupe avec clés significatives pour l’enregistrement, Calendrier équitable , et diagnosticsM SK2 - Pinnage & pour les retraits:
Pin,ExpireAfterMs,AwaitSignals,MaxRetries, etRetryDelayMslaisser les gestionnaires s'étendre leur visibilité, exécution de la passerelle jusqu’à ce que les dépendances arrivent, et guérissent avec des retraits en émettant des signaux d’échecM SK2 - Choréographie de signaux: Emit
EmitOnStart,EmitOnComplete, etEmitOnFailuresignaler les étapes en aval, log appareils de surveillance, ou autres coordonnateurs sans câblage manuelM SK1
var sink = new SignalSink();
await using var runner = new EphemeralSignalJobRunner(sink, new[] { new LogWatcherJobs(sink) });
var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
builder.AddProvider(new SignalLoggerProvider(new TypedSignalSink<SignalLogPayload>(sink)));
});
var logger = loggerFactory.CreateLogger("orders");
logger.LogError(new EventId(1001, "DbFailure"), "Order store failed");
// Later tasks or other services can also raise watcher-friendly signals directly:
sink.Raise("log.error.orders.dbfailure", key: "orders");
public sealed class LogWatcherJobs
{
private readonly SignalSink _sink;
public LogWatcherJobs(SignalSink sink) => _sink = sink;
[EphemeralJob("log.error.*", Priority = 1, MaxConcurrency = 2, Lane = "hot:4", EmitOnComplete = new[] { "incident.created" })]
public Task EscalateAsync(SignalEvent signal)
{
Console.WriteLine($"escalating {signal.Signal} for {signal.Key}");
_sink.Raise("incident.created", key: signal.Key);
return Task.CompletedTask;
}
[EphemeralJob("incident.created", EmitOnStart = new[] { "incident.monitor.start" })]
public Task NotifyAsync(SignalEvent signal)
{
Console.WriteLine($"notified incident for {signal.Key}");
return Task.CompletedTask;
}
}
Ce coureur siège maintenant au démarrage et réagit chaque fois log.error.* ou tout signal émis touche le réservoir. Attribute
Les gestionnaires peuvent aussi lire les clés des signaux/payloadsM SK1 pinwork jusqu’à ce que le débit en aval ne soit pas satisfaisant
emplacement dans les voies de commande. Pour DI-les premières configurations sont utilisées services.AddEphemeralSignalJobRunner<T>() (ou la portée
variant) de sorte que le coureur et le puits sont gérés par le contenant.
[Emplois éphémères(Préfixe de signalisation M SK1 "stage", Voie par défaut ≥= ≤"pipelineMSC6 StageJobs classé public { [Emploi éphéméral("ingestM SK1 EmitOnComplete = nouveau[] Public Task IngestAsync(SignalEvent evt) MSC2 ConsoleM SK3OutMST4WriteLineAsynCMSSK5evtMS.SignaleMSS7
[EphemeralJob("finalize")]
public Task FinalizeAsync(SignalEvent evt) => Console.Out.WriteLineAsync("final stage");
}
var stageSink = nouvelle SignalSinkM SK1 await using var stageRunner = nouveau EphemeralSignalJobRunnerM SK1stageSink, nouveau[] { nouveaux postes d'encadrement stageSink.Raise ("stageM SK2ingest");
Pin-les emplois lourds peuvent compter sur ResponsibilitySignalManager.PinUntilQueried (patterne d'accouplement par défaut responsibility.ack.*) à
conserver leurs opérations visibles jusqu’à ce qu’un lecteur en aval reçoive la charge de paye, pendant OperationEchoMaker/
OperationEchoAtom persister le flux de signal final pour que les auditeurs ou les molécules puissent encore “tasteM SK1 l’état final même après
l'atome décède.
Dépenses prévues
mostlylucid.ephemeral.complete contient également mostlylucid.ephemeral.atoms.scheduledtasks. Définir cron ou JSON
les calendriers par le biais de ScheduledTaskDefinition (cronM SK1 signal, optionnel key, payload, description, timeZone, format,
runOnStartup, etc .), et laisser ScheduledTasksAtom d'effectuer des travaux durables par DurableTaskAtom. Chaque tâche prévue
améliore le signal configuré à l'intérieur d'une fenêtre de coordonnateur, afin qu'il hérisse du pinningM SK1 de la saisie, et de la sémantique de responsabilité
alors que vos molécules ou pipelines attribuables réagissent à la vague de signaux émise.
Tous les DurableTask a l’ordre du jour de la réunion Name, Signal, optionnel Key, même un type Payload, et Description, donc
Les auditeurs en aval savent immédiatement quel travail a été exécuté et quelles métadonnées doivent être utilisées (filenames, URLs , etcM SK3 pour consommer
DurableTaskAtom.WaitForIdleAsync() lorsque vous voulez simplement attendre que le travail en cours se termine
sans remplir l’atome, en gardant le calendrier prêt pour le prochain tick de cron.
Enregistrement des signaux &
mostlylucid.ephemeral.logging miroirs Microsoft.Extensions.Connexion aux signaux et vice-versaM SK2 Démarrer par attacher
SignalLoggerProvider à votre usine d'enregistrement pour que les événements soient signalés log.* signals, et crochet SignalToLoggerAdapter Si
vous voulez que les signaux retournent dans le pipeline standard de log.
var sink = new SignalSink();
var typedSink = new TypedSignalSink<SignalLogPayload>(sink);
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
builder.AddProvider(new SignalLoggerProvider(typedSink));
});
using var watcher = new EphemeralSignalJobRunner(sink, new[] { new LogWatcherJobs(sink) });
var logger = loggerFactory.CreateLogger("orders");
logger.LogError(new EventId(1001, "DbFailure"), "Order store failed");
public sealed class LogWatcherJobs
{
private readonly SignalSink _sink;
public LogWatcherJobs(SignalSink sink) => _sink = sink;
[EphemeralJob("log.error.*")]
public Task EscalateAsync(SignalEvent signal)
{
_sink.Raise("incident.created", key: signal.Key);
return Task.CompletedTask;
}
[EphemeralJob("incident.created")]
public Task NotifyAsync(SignalEvent signal)
{
Console.WriteLine($"Incident for {signal.Key}");
return Task.CompletedTask;
}
}
Utilisation SignalToLoggerAdapter pour refléter les signaux résultant en retour dans des registres standard, de sorte que votre stade de surveillance voit les deux
les côtés du pont.
Coordonnateurs de base
Package: mostlylucid.éphémère
Coordonnateur de travail temporaire<T>
Long-carte de travail vivante avec concurrence limitée et fenêtre observable
await using var coordinator = new EphemeralWorkCoordinator<Request>(
async (req, ct) => await HandleAsync(req, ct),
new EphemeralOptions
{
MaxConcurrency = 8,
MaxTrackedOperations = 200,
MaxOperationLifetime = TimeSpan.FromMinutes(5)
});
await coordinator.EnqueueAsync(request);
// Observe state
var running = coordinator.GetRunning();
var failed = coordinator.GetFailed();
var pending = coordinator.PendingCount;
// Graceful shutdown
coordinator.Complete();
await coordinator.DrainAsync();
Coordonnateur de travail à clés éphémères<TKey, T>
Par -processage séquentiel de clés - articles dont la même clé est traitée selon l’ordre.
await using var coordinator = new EphemeralKeyedWorkCoordinator<Order, string>(
order => order.CustomerId, // Key selector
async (order, ct) => await ProcessOrder(order, ct),
new EphemeralOptions
{
MaxConcurrency = 16, // Total parallel
MaxConcurrencyPerKey = 1 // Sequential per customer
});
await coordinator.EnqueueAsync(order);
Coordonnateur du résultat éphémère<TInput, Résultat>
Résultats des opérations asynciques de capture.
await using var coordinator = new EphemeralResultCoordinator<Request, Response>(
async (req, ct) => await FetchAsync(req, ct),
new EphemeralOptions { MaxConcurrency = 4 });
var id = await coordinator.EnqueueAsync(request);
var snapshot = await coordinator.WaitForResult(id);
if (snapshot.HasResult)
Console.WriteLine(snapshot.Result);
Coordonnateur de travail prioritaire<T>
Plusieurs voies prioritaires avec une cohérence configurable par voie.
var coordinator = new PriorityWorkCoordinator<WorkItem>(
async (item, ct) => await ProcessAsync(item, ct),
new PriorityWorkCoordinatorOptions<WorkItem>(
Lanes: new[] { new PriorityLane("high"), new PriorityLane("normal"), new PriorityLane("low") }
));
await coordinator.EnqueueAsync(item, "high");
Configuration (Options éphémères)
new EphemeralOptions
{
// Concurrency
MaxConcurrency = 8, // Max parallel operations
MaxConcurrencyPerKey = 1, // For keyed coordinators
EnableDynamicConcurrency = false, // Allow runtime adjustment
// Memory
MaxTrackedOperations = 200, // Window size (LRU eviction)
MaxOperationLifetime = TimeSpan.FromMinutes(5),
// Fair scheduling (keyed only)
EnableFairScheduling = false, // Prevent hot key starvation
FairSchedulingThreshold = 10,
// Signals
Signals = sharedSink, // Shared signal sink
OnSignal = evt => { }, // Sync callback
OnSignalAsync = async (evt, ct) => { }, // Async callback
CancelOnSignals = new HashSet<string> { "circuit-open" },
DeferOnSignals = new HashSet<string> { "backpressure" },
DeferCheckInterval = TimeSpan.FromMilliseconds(100),
MaxDeferAttempts = 50,
// Signal handler limits
MaxConcurrentSignalHandlers = 4,
MaxQueuedSignals = 1000
}
Signales
Opérations émettent des signaux pour le croisement-observabilité de coupeM SK1
// Query signals
bool hasError = coordinator.HasSignal("error");
int count = coordinator.CountSignals("error");
var errors = coordinator.GetSignalsByPattern("error.*");
// Shared sink across coordinators
var sink = new SignalSink();
var c1 = new EphemeralWorkCoordinator<A>(body, new EphemeralOptions { Signals = sink });
var c2 = new EphemeralWorkCoordinator<B>(body, new EphemeralOptions { Signals = sink });
sink.Raise("system.busy"); // Both see it
Signales de responsabilité & Finalisation
Nécessité de garder les résultats visibles assez longtemps pour les consommateurs en aval? ResponsibilitySignalManager vous permet d'établir un
opération jusqu’à ce qu’un signal d’ack arrive (patterne par défaut responsibility.ack.* avec la clé=operationId). Fournir un
optionnel description de façon à ce que l’opération puisse décrire sa responsabilité, et fixer maxPinDuration de façon gracieuse
auto-c'est clair si le consommateur n'apparaît jamais .
var manager = new ResponsibilitySignalManager(coordinator, sink, maxPinDuration: TimeSpan.FromMinutes(5));
if (manager.PinUntilQueried(operationId, "file.ready", ackKey: fileId, description: "Awaiting fetch"))
{
sink.Raise("file.ready", key: fileId);
}
// Consumer acknowledges the work
sink.Raise("file.ready.ack", key: fileId);
using Mostlylucid.Ephemeral.Patterns;
var notes = new LastWordsNoteAtom(async note => await noteRepository.SaveAsync(note));
coordinator.OperationFinalized += snapshot =>
{
var note = new LastWordsNote(
OperationId: snapshot.OperationId,
Key: snapshot.Key,
Signal: snapshot.Signals?.FirstOrDefault(),
Timestamp: DateTimeOffset.UtcNow);
_ = notes.EnqueueAsync(note);
};
LastWordsNote Il demeure mince (ID d'exploitation,keyM SK2signe ,tampe de tempsMSC4 afin que vous puissiez enregistrer tout état minimal que vous désiriez
avant la collecte de l’opération.
Le coordonnateur garde également un bref - écho vivant des signaux définitifs ( activé par EnableOperationEcho) que vous pouvez
d'inspection GetEchoes() lorsque vous avez besoin de répéter la vague signalisée trimmée sans maintenir le fonctionnement complet autour.
var recentErrors = coordinator.GetEchoes(pattern: "error.*")
.Where(e => e.Timestamp > DateTimeOffset.UtcNow - TimeSpan.FromMinutes(1))
.ToList();
if (recentErrors.Any())
logger.LogWarning("Trimmed errors: {Count}", recentErrors.Count);
OperationEchoRetention et OperationEchoCapacity laissez-vous équilibrer le nombre d’échoes que vous gardez et la durée de leur durée.
pour que vous puissiez répéter les mots suivants :
Le gestionnaire débouche automatiquement lorsque l'ack prend feu, mais vous pouvez appeler CompleteResponsibility(operationId) à la fin de l’exercice
Responsabilité précoce (eM SK1g ., sur les retraits). Les opérations augmentent encore OperationFinalized lorsque la fenêtre les trime, ainsi
souscrire si vous voulez émettre un signal définitif, diagnostic log, ou lancer M SK2 dernières mots ” nettoyageMSC4
Atomes (Bloques de construction)
Atome de travail fixe
**Package: ** mostlylucid.epheméralM SK1atoms.fixedwork
Base de travail fixe avec des statistiques. Enveloppeur API minimal autour du coordonnateur EphemeralWork.
using Mostlylucid.Ephemeral.Atoms.FixedWork;
await using var atom = new FixedWorkAtom<WorkItem>(
async (item, ct) => await ProcessAsync(item, ct),
maxConcurrency: 4,
maxTracked: 200);
await atom.EnqueueAsync(item);
// Get stats
var (pending, active, completed, failed) = atom.Stats();
Console.WriteLine($"Completed: {completed}, Failed: {failed}");
// Get recent operations
var snapshot = atom.Snapshot();
// Graceful shutdown
await atom.DrainAsync();
KeyedSequentialAtom
**Package: ** mostlylucid.epheméralM SK1atoms.keyedsequential
Per-processage séquentiel clé avec calendrier équitable facultatif
using Mostlylucid.Ephemeral.Atoms.KeyedSequential;
await using var atom = new KeyedSequentialAtom<Order, string>(
keySelector: order => order.CustomerId,
body: async (order, ct) => await ProcessOrder(order, ct),
maxConcurrency: 16,
perKeyConcurrency: 1, // Sequential per key
enableFairScheduling: true); // Prevent hot key starvation
await atom.EnqueueAsync(order1); // Customer A
await atom.EnqueueAsync(order2); // Customer A - waits for order1
await atom.EnqueueAsync(order3); // Customer B - parallel with A
var (pending, active, completed, failed) = atom.Stats();
await atom.DrainAsync();
SignalAwareAtom
**Package: ** mostlylucid.epheméralM SK1atoms.signalaware
Pauser ou annuler l'apport en fonction des signaux ambiants.
using Mostlylucid.Ephemeral.Atoms.SignalAware;
var sink = new SignalSink();
await using var atom = new SignalAwareAtom<WorkItem>(
async (item, ct) => await ProcessAsync(item, ct),
cancelOn: new HashSet<string> { "shutdown", "circuit-open" },
deferOn: new HashSet<string> { "backpressure.*" },
deferInterval: TimeSpan.FromMilliseconds(100),
maxDeferAttempts: 50,
signals: sink,
maxConcurrency: 8);
// Enqueue work
await atom.EnqueueAsync(item);
// Raise ambient signals
atom.Raise("backpressure.downstream"); // New items defer
sink.Raise("shutdown"); // New items rejected (returns -1)
await atom.DrainAsync();
BatchingAtom
**Package: ** mostlylucid.epheméralM SK1atoms.batching
Collecter les articles en lots selon la taille ou l’intervalle de temps.
using Mostlylucid.Ephemeral.Atoms.Batching;
await using var atom = new BatchingAtom<LogEntry>(
onBatch: async (batch, ct) =>
{
Console.WriteLine($"Flushing {batch.Count} entries");
await FlushToDatabase(batch, ct);
},
maxBatchSize: 100,
flushInterval: TimeSpan.FromSeconds(5));
// Items are batched automatically
atom.Enqueue(new LogEntry("User logged in"));
atom.Enqueue(new LogEntry("Request received"));
// ... batch flushes when full OR after 5 seconds
RetryAtom
Enveloppe de réessai d'arrêt expositionnel.
using Mostlylucid.Ephemeral.Atoms.Retry;
await using var atom = new RetryAtom<ApiRequest>(
async (req, ct) => await CallExternalApi(req, ct),
maxAttempts: 3,
backoff: attempt => TimeSpan.FromMilliseconds(100 * Math.Pow(2, attempt)),
maxConcurrency: 4);
// Automatically retries on failure with exponential backoff
// Attempt 1: immediate
// Attempt 2: 200ms delay
// Attempt 3: 400ms delay
await atom.EnqueueAsync(new ApiRequest("https://api.example.com"));
await atom.DrainAsync();
Atomes de stockage des données
Package: Données principalementlucides.éphémèresM SK1atoms.données
Configuration partagée pour le stockage des atomes (DataStorageConfig, IDataStorageAtom<TKey, TValue>) plus le signal
conventions qui conduisent le fichier, SQLite, et les adaptateurs PostgreSQLM SK2
using Mostlylucid.Ephemeral.Atoms.Data;
using Mostlylucid.Ephemeral.Atoms.Data.File;
var sink = new SignalSink();
var config = new DataStorageConfig
{
DatabaseName = "orders",
SignalPrefix = "save.data",
LoadSignalPrefix = "load.data",
DeleteSignalPrefix = "delete.data",
MaxConcurrency = 1
};
await using var storage = new FileDataStorageAtom<string, Order>(sink, config, "./orders");
storage.EnqueueSave("order-123", new Order { Id = "order-123", Total = 42.00m });
var loaded = await storage.LoadAsync("order-123");
Utiliser les mêmes DataStorageConfig avec
Mostlylucid.Ephemeral.Atoms.Data.Sqlite ou
Mostlylucid.Ephemeral.Atoms.Data.Postgres
implémentations pour une persistance durable, signal-driven persistence powered by SQLite /PostgresM SK3 Les tâches attributives peuvent être abonnées à
saved.data.{dbname} signals pour amorcer le travail en aval pendant que load.data.{dbname} déclenche les caches d'hydrates.
MoleculeRunner & AtomTrigger
**Package: ** Molécules principalementlucides.éphémèresM SK1atoms.molécules
Plans combinés avec MoleculeBlueprintBuilder vous définissez les atomes (payementM SK1inventaire,expéditionMSC3
notification) qui devrait fonctionner lorsque un signal tel que order.placed Arrivée. MoleculeRunner écoute le déclencheur
pattern, crée un modèle partagé MoleculeContext, et exécute chaque étape pendant que vous vous abonnez à la mise en place de l’événement.
AtomTrigger lorsque le signal d’un atome' doit démarrer un autre coordonnateur ou une moléculeM SK1
var sink = new SignalSink();
var blueprint = new MoleculeBlueprintBuilder("order", "order.placed")
.AddAtom(async (ctx, ct) => await paymentCoordinator.EnqueueAsync(ctx.TriggerSignal.Key!, ct))
.AddAtom(async (ctx, ct) =>
{
ctx.Raise("order.payment.complete", ctx.TriggerSignal.Key);
await inventoryCoordinator.EnqueueAsync(ctx.TriggerSignal.Key!, ct);
})
.Build();
await using var runner = new MoleculeRunner(sink, new[] { blueprint }, serviceProvider);
using var trigger = new AtomTrigger(sink, "order.payment.complete", async (signal, ct) =>
{
await notificationCoordinator.EnqueueAsync(signal.Key!, ct);
});
sink.Raise("order.placed", key: "order-42");
Les étapes moléculaires peuvent susciter des signaux additionnels (ctx.Raise("order.shipping.start")) de sorte que le reste du système reçoit
baton.
Atome de chevauchement Sliding
**Package: ** mostlylucid.epheméralM SK1atoms.slidingcache
Cache avec échéance sèche - l'accès à un résultat rétablit son TTL.
using Mostlylucid.Ephemeral.Atoms.SlidingCache;
await using var cache = new SlidingCacheAtom<string, UserProfile>(
async (userId, ct) => await LoadUserProfileAsync(userId, ct),
slidingExpiration: TimeSpan.FromMinutes(5),
absoluteExpiration: TimeSpan.FromHours(1),
maxSize: 1000);
// First call: computes and caches
var profile = await cache.GetOrComputeAsync("user-123");
// Second call within 5 minutes: returns cached, resets TTL
var cached = await cache.GetOrComputeAsync("user-123");
// Try get without computation (still resets TTL on hit)
if (cache.TryGet("user-123", out var profile))
Console.WriteLine(profile.Name);
// Get stats
var stats = cache.GetStats();
Console.WriteLine($"Entries: {stats.TotalEntries}, Hot: {stats.HotEntries}");
VolatileOperationAtom
**Package: ** mostlylucid.épheméral.atomsM SK2volatile
Les opérations de chute sont effectuées dès qu’elles émettent un signal d’arrêt, ce qui permet à la fenêtre de demeurer léger même sous une très haute puissance.
atom entend un modèle configurable (default kill.*) sur le partage SignalSink, appels IOperationEvictor.TryKill
avec l’opération capturée ID, et permet encore au coordonnateur d’allumer ses crochets de finalisation echo/ avant l’entrée
disparue.
var sink = new SignalSink();
await using var coordinator = new EphemeralWorkCoordinator<JobItem>(
async (job, ct) => await QuickProcessAsync(job, ct),
new EphemeralOptions
{
Signals = sink,
EnableOperationEcho = true,
OperationEchoRetention = TimeSpan.FromSeconds(30)
});
using var volatileAtom = new VolatileOperationAtom(sink, coordinator);
// inside the job: emitter.Emit("kill.job");
Paier l'atome avec OperationEchoMaker/OperationEchoAtom tel que typé *.echo.* les signaux survivent la mort comme un petit TTL’d
enregistrement.
EphemeralLruCache
Package: noyau (
mostlylucid.ephemeral) — auto- optimisation du cache avec TTL sliding sur chaque coup et TTL étendu pour clés chaudes.
using Mostlylucid.Ephemeral;
var cache = new EphemeralLruCache<string, Widget>(new EphemeralLruCacheOptions
{
DefaultTtl = TimeSpan.FromMinutes(5),
HotKeyExtension = TimeSpan.FromMinutes(30),
HotAccessThreshold = 3,
MaxSize = 10_000,
SampleRate = 5 // emit 1 in 5 signals
});
var widget = await cache.GetOrAddAsync("widget:42", async key =>
{
var data = await LoadWidgetAsync(key);
return data!;
});
// Stats and signals to see how the cache self-focuses on hot keys
var stats = cache.GetStats(); // hot/expired counts, size
var signals = cache.GetSignals("cache.*"); // cache.hot/evict/miss/hit
Remarque:
MemoryCachepeut être configuré pour l'expiration sliding, mais il n'émet jamais les signaux chauds/cold ou prolonge TTL pour clés chaudes.EphemeralLruCacheest l’auto-optimisation par défaut dans le paquet de base ( et dansSqliteSingleWriter) whenever you want the cache to focus on the active working set.
Taille de la fenêtre Atome
**Package: ** mostlylucid.epheméralM SK1atoms.windowsize
L'atome écoute window.size.* ou window.time.* commandes, colle la capacité requiseM SK1retention, et appelle
SignalSink.UpdateWindowSize vous pouvez ainsi élargir ou réduire la fenêtre partagée à partir d’un autre coordonnateur ou surveillant de signaux.
var sink = new SignalSink(maxCapacity: 250);
await using var atom = new WindowSizeAtom(sink);
sink.Raise("window.size.decrease:50");
sink.Raise("window.time.set:00:02:00");
Atomes de taxonomie
Packages:
- Base contrats: mostlylucid.epheméralM SK1atoms.taxonomie ( inclure
MultiTaxonomyAtom)- Types d'atomes:
mostlylucid.ephemeral.atoms.taxonomy.sensor,.extractor,.embedder,.retriever,.proposer,
.constrainer,.ranker,.renderer,.coordinator,.feedback,.guard
Types d'atomes génériques qui s'alignent sur la taxonomie :
Rendreur, CoordonnateurM SK1 Rétroaction , Garde). Chaque atome émet un signal type à l'achèvement et transporte un AtomContract pour
Déterminisme et métadonnées de persistance. Installer le paquet d'atomes spécifique dont vous avez besoin M SK1l'exemple ci-dessous utilise le proposer).
Utilisation MultiTaxonomyAtom du paquet de base lorsque vous devez combiner plusieurs types dans un seul contrat.
using Mostlylucid.Ephemeral;
using Mostlylucid.Ephemeral.Atoms.Taxonomy;
public sealed record Proposal(string Text, double Confidence);
var sink = new SignalSink();
var proposer = new ProposerAtom<string, Proposal>(
sink,
async (prompt, ct) => new Proposal($"Echo: {prompt}", 0.42),
outputSignal: "proposal.created",
keySelector: prompt => prompt);
await proposer.EnqueueAsync("hello");
Escalator Atom
**Package: ** mostlylucid.epheméralM SK1atoms.escalator
Promouvoir les signaux éphémères typés, dans des réservoirs durablesM SK1 EscalatorLes atomes sont la façon préférée de maintenir les sorties Short-coordonnateurs vivants.
using Mostlylucid.Ephemeral;
using Mostlylucid.Ephemeral.Atoms.Data.File;
using Mostlylucid.Ephemeral.Atoms.Escalator;
public sealed record EscalationPayload(string Kind, string? EvidenceId, double Confidence);
var sink = new SignalSink();
var typed = new TypedSignalSink<EscalationPayload>(sink);
await using var storage = new FileDataStorageAtom<string, EscalationPayload>(
sink,
new FileDataStorageConfig { DatabaseName = "signals" });
Func<SignalEvent<EscalationPayload>, CancellationToken, Task> audit = (evt, ct) =>
{
Console.WriteLine($"audit: {evt.Signal} {evt.Key}");
return Task.CompletedTask;
};
var targets = new[]
{
new EscalationTarget<EscalationPayload>(
"store",
(evt, ct) => storage.SaveAsync(evt.Key ?? evt.OperationId.ToString(), evt.Payload, ct)),
new EscalationTarget<EscalationPayload>("audit", audit)
};
await using var escalator = new EscalatorAtom<EscalationPayload>(
sink,
typed,
targets,
new EscalatorAtomOptions<EscalationPayload>
{
EscalateSignalPattern = "escalate.*",
EmitOnSuccess = "escalation.persisted",
EmitOnFailure = "escalation.failed"
});
typed.Raise("escalate.signal", new EscalationPayload("risk", "file-42", 0.81), key: "order-123");
Mécanicien d'écho
Package: mostlylucid.épheméralM SK1atoms.echo
Capture de la “ dernière phrase ” que l'opération émet avant qu'elle ne soit trimmée.
charges de travail (compatible ActivationSignalPattern / CaptureSignalPattern) et quand OperationFinalized les incendies qu’il produit
OperationEchoEntry<TPayload> les dossiers que vous pouvez conserver par OperationEchoAtom<TPayload>.
var sink = new SignalSink();
var typedSink = new TypedSignalSink<EchoPayload>(sink);
var echoAtom = new OperationEchoAtom<EchoPayload>(async echo => await repository.AppendAsync(echo));
await using var coordinator = new EphemeralWorkCoordinator<JobItem>(ProcessAsync);
using var maker = coordinator.EnableOperationEchoing(
typedSink,
echoAtom,
new OperationEchoMakerOptions<EchoPayload>
{
ActivationSignalPattern = "echo.capture",
CaptureSignalPattern = "echo.*",
MaxTrackedOperations = 128
});
typedSink.Raise("echo.capture", new EchoPayload("order-1", "archived"), key: "order-1");
Les emplois d’attributes ne font qu’augmenter le signal type avec quel que soit l’état qu’ils jugent critique, et le fabricant garde la série de travail bounded pendant que vous persistez à l’écho.
Patterns (Ready-toM SK2UseMSC3
SignalBasedCircuitBreaker
**Package: ** mostlylucid.epheméralM SK1patterns.circuitbreaker
Écrasateur de circuit sans état utilisant la fenêtre historique des signaux.
using Mostlylucid.Ephemeral.Patterns.CircuitBreaker;
var breaker = new SignalBasedCircuitBreaker(
failureSignal: "api.failure",
threshold: 5,
windowSize: TimeSpan.FromSeconds(30));
// Check before making calls
if (breaker.IsOpen(coordinator))
{
var retryAfter = breaker.GetTimeUntilClose(coordinator);
throw new CircuitOpenException("Too many failures", retryAfter);
}
// Pattern matching variant
if (breaker.IsOpenMatching(coordinator, "error.*"))
throw new CircuitOpenException("Error pattern detected");
// Get current failure count
int failures = breaker.GetFailureCount(coordinator);
SignalDrivenBackpressure
**Package: ** Prédominantment clairs.éphémèresM SK1patternes.pressure arrière
Gestion de la profondeur des couloirs avec report automatique sur les signaux de pression arrière.
using Mostlylucid.Ephemeral.Patterns.Backpressure;
var sink = new SignalSink();
await using var coordinator = SignalDrivenBackpressure.Create<WorkItem>(
async (item, ct) => await ProcessAsync(item, ct),
sink,
maxConcurrency: 4);
// Enqueue work
await coordinator.EnqueueAsync(item);
// When downstream is slow
sink.Raise("backpressure.downstream"); // New work auto-defers
// When recovered
sink.Retract("backpressure.downstream"); // Work resumes
FanOut contrôlé
**Package: ** plus lumineux.éphémèresM SK1patterns.fanout contrôlé
Global + par -gage de clés pour le parallèle contrôléM SK2
using Mostlylucid.Ephemeral.Patterns.ControlledFanOut;
await using var fanout = new ControlledFanOut<string, Request>(
keySelector: req => req.TenantId,
body: async (req, ct) => await ProcessAsync(req, ct),
maxGlobalConcurrency: 100, // Total parallel across all tenants
perKeyConcurrency: 5); // Max 5 parallel per tenant
// Items for same tenant processed with limit
await fanout.EnqueueAsync(requestA); // Tenant1
await fanout.EnqueueAsync(requestB); // Tenant1 - waits if 5 already running
await fanout.EnqueueAsync(requestC); // Tenant2 - parallel with Tenant1
await fanout.DrainAsync();
AdaptiveRateService
**Package: ** taux d'adaptation
Signale-limitation du taux de conduite avec arrêt automatiqueM SK1
using Mostlylucid.Ephemeral.Patterns.AdaptiveRate;
await using var service = new AdaptiveRateService<ApiRequest>(
async (req, ct) => await CallApiAsync(req, ct),
maxConcurrency: 8);
// Process with automatic rate limit handling
await service.ProcessAsync(request);
// When API returns 429, emit signal with retry-after
// Signal: "rate-limit:500ms"
// Service auto-parses and delays
Console.WriteLine($"Pending: {service.PendingCount}, Active: {service.ActiveCount}");
Démonstration de la Concurrence dynamique
**Package: ** .éphémèresM SK1patternes.concurrence dynamique
Étalonnage de la cohérence du temps d'exécution fondé sur les signaux de charge.
using Mostlylucid.Ephemeral.Patterns.DynamicConcurrency;
var sink = new SignalSink();
await using var demo = new DynamicConcurrencyDemo<WorkItem>(
async (item, ct) => await ProcessAsync(item, ct),
sink,
minConcurrency: 2,
maxConcurrency: 32,
scaleUpPattern: "load.high",
scaleDownPattern: "load.low");
await demo.EnqueueAsync(item);
// Concurrency adjusts automatically based on signals
sink.Raise("load.high"); // Concurrency doubles (up to max)
sink.Raise("load.low"); // Concurrency halves (down to min)
Console.WriteLine($"Current concurrency: {demo.CurrentMaxConcurrency}");
await demo.DrainAsync();
KeyedPriorityFanOut
**Package: ** plus clairs.éphémèresM SK1patternes.faut de priorité clé
Voies prioritaires avec per-conservation de l’ordonnance par cléM SK1
using Mostlylucid.Ephemeral.Patterns.KeyedPriorityFanOut;
await using var fanout = new KeyedPriorityFanOut<string, UserCommand>(
keySelector: cmd => cmd.UserId,
body: async (cmd, ct) => await HandleCommand(cmd, ct),
maxConcurrency: 32,
perKeyConcurrency: 1, // Sequential per user
maxPriorityDepth: 100);
// Normal lane
await fanout.EnqueueAsync(normalCommand);
// Priority lane - jumps the queue for that user
bool accepted = await fanout.EnqueuePriorityAsync(urgentCommand);
// Check lane depths
var counts = fanout.PendingCounts;
Console.WriteLine($"Priority: {counts.Priority}, Normal: {counts.Normal}");
await fanout.DrainAsync();
ReactiveFanOutPipeline
**Package: ** plus clairs.éphémèresM SK1patternes.réactivitéfanout
Deux-pipeline en phase avec répression automatique de l'arrière
using Mostlylucid.Ephemeral.Patterns.ReactiveFanOut;
await using var pipeline = new ReactiveFanOutPipeline<WorkItem>(
stage2Work: async (item, ct) => await SlowProcessing(item, ct),
preStageWork: async (item, ct) => await FastPreprocessing(item, ct),
stage1MaxConcurrency: 8,
stage1MinConcurrency: 1,
stage2MaxConcurrency: 4,
backpressureThreshold: 32, // Throttle when stage2 has 32+ pending
reliefThreshold: 8); // Resume when stage2 drops below 8
await pipeline.EnqueueAsync(item);
// Stage1 auto-throttles when stage2 backs up
Console.WriteLine($"Stage1 concurrency: {pipeline.Stage1CurrentMaxConcurrency}");
Console.WriteLine($"Stage2 pending: {pipeline.Stage2Pending}");
await pipeline.DrainAsync();
Détecteur d'anomalie de signal
**Package: ** [Traduit par la Rédaction]
Moving-détection d'anomalies de fenêtresM SK1
using Mostlylucid.Ephemeral.Patterns.AnomalyDetector;
var sink = new SignalSink();
var detector = new SignalAnomalyDetector(
sink,
pattern: "error.*",
threshold: 5,
window: TimeSpan.FromSeconds(10));
// Check for anomalies
if (detector.IsAnomalous())
{
Console.WriteLine("Anomaly detected! Too many errors.");
TriggerAlert();
}
// Get current match count
int errorCount = detector.GetMatchCount();
Console.WriteLine($"Errors in window: {errorCount}");
Les lectures coordonnées des signaux
**Package: ** mostlylucid.ephemeralM SK1patterns.signalcoordinatedreads
Quiesce lit pendant les mises à jour sans bouclage dur.
using Mostlylucid.Ephemeral.Patterns.SignalCoordinatedReads;
// Run demo: readers pause when update signal is present
var result = await SignalCoordinatedReads.RunAsync(
readCount: 10,
updateCount: 1);
Console.WriteLine($"Reads: {result.ReadsCompleted}, Updates: {result.UpdatesCompleted}");
Console.WriteLine($"Signals: {string.Join(", ", result.Signals)}");
// Manual implementation:
var sink = new SignalSink();
await using var readers = new EphemeralWorkCoordinator<Query>(
body,
new EphemeralOptions
{
DeferOnSignals = new HashSet<string> { "update.in-progress" },
Signals = sink
});
// Readers auto-defer when update is running
sink.Raise("update.in-progress"); // Readers wait
sink.Raise("update.done"); // Readers resume
Signalisation du client Http
**Package: ** mostlylucid.ephemeralM SK1patterns.signalisationhttp
Client HTTP avec des signaux de progrès.
using Mostlylucid.Ephemeral.Patterns.SignalingHttp;
var httpClient = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/large-file");
// Create an emitter from your coordinator
// (emitter is any ISignalEmitter - operations implement this)
byte[] data = await SignalingHttpClient.DownloadWithSignalsAsync(
httpClient,
request,
emitter);
// Signals emitted during download:
// - stage.starting
// - progress:0
// - stage.request
// - stage.headers
// - stage.reading
// - progress:25, progress:50, progress:75, progress:100
// - stage.completed
SignalLogWatcher
**Package: ** Vérificateur de signalisation
Voir la fenêtre de signalisation pour les modèles et déclencher des appels de rappel.
using Mostlylucid.Ephemeral.Patterns.SignalLogWatcher;
var sink = new SignalSink();
await using var watcher = new SignalLogWatcher(
sink,
onMatch: evt =>
{
Console.WriteLine($"Error detected: {evt.Signal} at {evt.Timestamp}");
AlertOps(evt);
},
pattern: "error.*",
pollInterval: TimeSpan.FromMilliseconds(200));
// Watcher runs in background, calling onMatch for each new error signal
sink.Raise("error.database"); // -> onMatch called
sink.Raise("error.timeout"); // -> onMatch called
sink.Raise("info.started"); // -> ignored (doesn't match pattern)
Gestionnaire de signatures de télémétrie
**Package: ** mostlylucid.epheméralM SK1patterns.telemétrie
OpenTelemetry/Intégration de l'inspection des applications.
using Mostlylucid.Ephemeral.Patterns.Telemetry;
// Use in-memory for testing, or implement ITelemetryClient for real telemetry
var telemetry = new InMemoryTelemetryClient();
await using var handler = new TelemetrySignalHandler(telemetry);
// Wire up to coordinator
var options = new EphemeralOptions
{
OnSignal = signal => handler.OnSignal(signal)
};
// Signals are processed asynchronously
// - "error.*" signals -> TrackExceptionAsync
// - "perf.*" signals -> TrackMetricAsync
// - all signals -> TrackEventAsync
Console.WriteLine($"Queued: {handler.QueuedCount}");
Console.WriteLine($"Processed: {handler.ProcessedCount}");
Console.WriteLine($"Dropped: {handler.DroppedCount}");
// Check recorded events
var events = telemetry.GetEvents();
LongWindowDemo
**Package: ** mostlylucid.ephemeralM SK1patterns.longwindowdemo
Démonstration de la configuration d'une large fenêtre pour les pistes de vérification.
using Mostlylucid.Ephemeral.Patterns.LongWindowDemo;
// Configure coordinator with large tracking window
var options = new EphemeralOptions
{
MaxTrackedOperations = 10000,
MaxOperationLifetime = TimeSpan.FromHours(24)
};
SignalReactionShowcase
**Package: ** mostlylucid.epheméralM SK1patterns.signalreactionshowcase
Démontre les patrons de diffusion des signaux et les appels de retour.
using Mostlylucid.Ephemeral.Patterns.SignalReactionShowcase;
// See source for signal dispatch examples
// Demonstrates OnSignal, OnSignalAsync, CancelOnSignals, DeferOnSignals
fenêtre de signature persistante
**Package: ** mostlylucid.epheméralM SK1patterns.peristentwindow
La fenêtre de signalisation avec la persistance SQLite - survivant le remarrage du processus.
using Mostlylucid.Ephemeral.Patterns.PersistentWindow;
await using var window = new PersistentSignalWindow(
"Data Source=signals.db",
flushInterval: TimeSpan.FromSeconds(30));
// On startup: restore previous signals
await window.LoadFromDiskAsync(maxAge: TimeSpan.FromHours(24));
// Raise signals as normal
window.Raise("order.completed", key: "order-service");
window.Raise("payment.processed", key: "payment-service");
// Query signals
var recentOrders = window.Sense("order.*");
// Signals automatically flush every 30 seconds
// Also flushes on dispose
// Get stats
var stats = window.GetStats();
Console.WriteLine($"In memory: {stats.InMemoryCount}, Flushed: {stats.LastFlushedId}");
Injection de dépendance
// Register in Startup/Program.cs
services.AddEphemeralWorkCoordinator<WorkItem>(
async (item, ct) => await ProcessAsync(item, ct),
new EphemeralOptions { MaxConcurrency = 8 });
// Named coordinators
services.AddEphemeralWorkCoordinator<WorkItem>("priority",
async (item, ct) => await ProcessPriorityAsync(item, ct));
// Inject and use
public class MyService(IEphemeralCoordinatorFactory<WorkItem> factory)
{
public async Task DoWork()
{
var coordinator = factory.CreateCoordinator();
await coordinator.EnqueueAsync(new WorkItem());
}
}
Les racines DI modernes peuvent préférer les aides plus courtes comme : services.AddCoordinator<T>(...),
services.AddScopedCoordinator<T>(...), ou services.AddKeyedCoordinator<T, TKey>(...) car ils lisent comme normalement
AddX les inscriptions; elles sont simplement déléguées aux aidants spéciaux sous le capuchon de l’Ephemeral-
Voie + Configuration de clés (Configuration simpleM SK2 puissance cachée)
mostlylucid.ephemeral.complete bourses PriorityWorkCoordinator et les variantes clés, donc la même M SK1AddCoordinator”
surface que vous utilisez au démarrage peut envoyer le travail à travers des voies désignées. Configurer quelques noms de voies, leur donner facultatif MaxDepth,
et s’appuyer sur le coordonnateur pour drainer les voies “hotM SK1 avant de les détruire, tout en observant toutes les clés de requête
var sink = new SignalSink();
var lanes = new[]
{
new PriorityLane("hot:4", CancelOnSignals: new HashSet<string> { "maintenance" }),
new PriorityLane("normal"),
new PriorityLane("slow:2")
};
await using var coordinator = new PriorityWorkCoordinator<WorkItem>(
new PriorityWorkCoordinatorOptions<WorkItem>(
async (item, ct) => await processor.ProcessAsync(item, ct),
lanes,
new EphemeralOptions { Signals = sink }));
await coordinator.EnqueueAsync(new WorkItem("order-42"), laneName: "hot");
await coordinator.EnqueueAsync(new WorkItem("order-43"), laneName: "slow");
Utilisation PriorityKeyedWorkCoordinator Si vous avez également besoin de commandes par clé, le choix des voies se fait toujours dans la pompe.
construite-le sélecteur de clés interne garde chaque partition séquentielle.
var keyed = new PriorityKeyedWorkCoordinator<WorkItem, string>(
new PriorityKeyedWorkCoordinatorOptions<WorkItem, string>(
order => order.CustomerId,
async (order, ct) => await processor.ProcessPerCustomerAsync(order, ct),
lanes,
new EphemeralOptions { Signals = sink }));
await keyed.EnqueueAsync(new WorkItem("order-42") { CustomerId = "A" }, laneName: "hot");
await keyed.EnqueueAsync(new WorkItem("order-44") { CustomerId = "B" }, laneName: "normal");
Pensez au coordonnateur comme à la cuisinier, aux voies de conduite comme aux vaisseaux et aux clés comme aux gouttes qui servent à maintenir l’ordre de chaque client. séquentiel—la puissance cachée qui maintient le travail ordonné,rachetéeM SK2 et signalée -aware without extra ceremonyMSC4
Cadre cible
- .NET 6.0, 7.0, | | 3 | 4 | 5
Licence
Non-licence (domaine publique)