Back to "Apprentissage des LRU - Lorsque la capacité excédentaire rend votre système meilleur"

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

Architecture ASP.NET Bot Detection Caching CQRS Systems Design

Apprentissage des LRU - Lorsque la capacité excédentaire rend votre système meilleur

Tuesday, 09 December 2025

La plupart des systèmes se dégradent lorsqu’ils sont surchargés. La mémoire s’accroît, Les demandes ralentissent , les utilisateurs se plaignent

Il y en a quelques inhabituels. améliorer.

Cet article montre comment un Memory comportementale basée sur LRU- - optimise lorsqu’il atteint la capacité - et comment ce modèle alimente le système d’apprentissage dans mon moteur de détection des robots. Il s'agit aussi de la version la plus petite possible Architecture DiSE - Évolution contrôlée par pression sur les ressourcesM SK1

Si vous' avez lu mon article sur CQRS et recherche d'événements, vous ' vous reconnaîtrez quelques-uns des patrons ici. Mais c'est le CQRS déchiré jusqu'à l'os CSK3 il n'y a pas de stockage d'événements, , aucune projection, MSC5 aucun Marten, M SK6 simplement un cache de mémoire, , un travailleur de base, MST8 et SQLite, Mst9

L’idée fondamentale - La mémoire comportementale sur un budget

Avant de plonger dans l’expression ,, permettez-moi de définir un terme que j’utiliserai tout au long de cette expression. Signature. Une signature est toute clé stable qui représente un modèle de comportement - un hash d'IP

Que se passerait-il si vous pouvions construire un système qui

  • Réponses en 1ms
  • Ne bloque jamais les écritures de la base de données
  • Self-optimise sous pression
  • Ne sait pas ce qui n’est pas important
  • Se souvient de ce qui fait

C'est exactement ce que ' IMemoryCache si vous comprenez ce que vous construisez

Ce qu'une cache LRU fait en réalité

LRU (Usage le moins récent ) les caches éliminent les entrées qui n’ont pas été consultées récemment ' ne sont pas consultées dernièrement M SK3 Lorsque le cache s’acquitte de ses fonctions, les inscriptions les plus froides se jettent pour y faire place aux plus chaudes.

La plupart des développeurs voient cela comme une limitation. "Oh non, mon cache est pleine , les données sont perduesM SK3

Mais pour les systèmes comportementaux, c’est une caractéristique.

// From WeightStore.cs - the bounded memory window
_cache = new MemoryCache(new MemoryCacheOptions
{
    SizeLimit = _cacheSize,           // e.g., 1000 entries
    CompactionPercentage = 0.25       // Remove 25% when limit reached
});

Que SizeLimit n’est-ce pas seulement une contrainte de mémoire? pression de sélection. Il détermine à quel point le système " se souvient de ses tâches et l’oblige à se concentrer sur ce qui importe.

L'expiration des Sliding - Clock That Forgets

Combiné à l'expiration de sliding, vous obtiendrez une forgetting automatique:

// From WeightStore.cs:254-259
private MemoryCacheEntryOptions GetCacheEntryOptions()
{
    return new MemoryCacheEntryOptions()
        .SetSlidingExpiration(_slidingExpiration)  // 30 minutes
        .SetSize(1);  // Each entry counts as 1 toward size limit
}

Si une signature n’est pas 'accessible dans les minutes 30minutes, elle est évacuéeM SK4 Non parce qu’elle est incorrecteMSC5, mais parce que celle-ci n’a plus d’importanceMST8

Cela crée pertes naturelles:

  • Les IP sont réassignés
  • Les bottes tournent les signatures
  • Changement des patrons de trafic
  • Hier, les attaques de ' ne sont pas celles d’aujourd’hui.

Les listes de blocs statiques sont bloquées. L’expiration progressive maintient la mémoire fraîche.

Le modèle IMemoryCache - CQRS mince sans dire CQ RS

Voici le modèle qui permet de fonctionner. SqliteWeightStore Classe:

/// <summary>
///     SQLite implementation of the weight store with sliding expiration memory cache.
///     Uses a CQRS-style pattern with write-behind:
///     - Reads: Hit memory cache first (fast path), fall back to SQLite on miss
///     - Writes: Update cache immediately, queue SQLite writes for background flush
///     Sliding expiration provides automatic LRU-like eviction behavior.
/// </summary>

public class SqliteWeightStore : IWeightStore, IAsyncDisposable
{
    // Memory cache with sliding expiration - auto-evicts least recently used entries
    private readonly MemoryCache _cache;

    // Write-behind queue for batched SQLite persistence
    private readonly ConcurrentDictionary<string, PendingWrite> _pendingWrites = new();
    private readonly Timer _flushTimer;
    private readonly TimeSpan _flushInterval = TimeSpan.FromMilliseconds(500);

Il s'agit de CQRS informel - mais sans l'invalidation du cache acharnée . Au lieu d'en invalider les entrées de cache après avoir écrit, le cache est le modèle d'écriture. SQLite n’est qu’un livre dur

flowchart LR
    subgraph Cache["In-Memory Behaviour Store"]
        A[Hot Signatures] --- B[Sliding Expiry]
    end
    subgraph DB["SQLite Ledger"]
        C[(Durable Write-Behind)]
    end

    A --Periodic Flush--> C
    B --Eviction--> D[Forgotten]

    style Cache fill:none,stroke:#10b981,stroke-width:2px
    style DB fill:none,stroke:#6366f1,stroke-width:2px

Perspectives clés: lire et écrire passer à la mémoire. La base de données est définitivement cohérente - et que

Le chemin de lecture - En premier lieu dans la Cache

Lorsque le détecteur a besoin d’un poids appréhendé, il touche la cache.

// From WeightStore.cs:421-472
public async Task<double> GetWeightAsync(
    string signatureType,
    string signature,
    CancellationToken ct = default)
{
    var key = CacheKey(signatureType, signature);

    // Check cache first (fast path - no DB access)
    if (_cache.TryGetValue(key, out LearnedWeight? cached) && cached != null)
    {
        _metrics?.RecordCacheHit(signatureType);
        return cached.Weight * cached.Confidence;
    }

    _metrics?.RecordCacheMiss(signatureType);

    // Cache miss - load from DB
    await EnsureInitializedAsync(ct);

    await using var conn = new SqliteConnection(_connectionString);
    await conn.OpenAsync(ct);

    var sql = $@"
        SELECT weight, confidence, observation_count, first_seen, last_seen
        FROM {TableName}
        WHERE signature_type = @type AND signature = @sig
    ";

    // ... execute query ...

    if (await reader.ReadAsync(ct))
    {
        // Cache the result for future reads
        var learnedWeight = new LearnedWeight { /* ... */ };
        _cache.Set(key, learnedWeight, GetCacheEntryOptions());

        return weight * confidence;
    }

    return 0.0;  // No learned weight exists
}

Routes chaudes n'a jamais atteint la base de données. Le cache est la source de la vérité pour les lectures . SQLite n’est qu’un stockage de sauvegarde .

Le chemin d'écriture - Cache immédiatement, Persiste ultérieurement

Lorsque le système apprend quelque chose de nouveau, il met immédiatement à jour le cache et les queues de la base de données écrire:

// From WeightStore.cs:551-582
public Task UpdateWeightAsync(
    string signatureType,
    string signature,
    double weight,
    double confidence,
    int observationCount,
    CancellationToken ct = default)
{
    var key = CacheKey(signatureType, signature);

    // Update cache immediately (source of truth for reads)
    var learnedWeight = new LearnedWeight
    {
        SignatureType = signatureType,
        Signature = signature,
        Weight = weight,
        Confidence = confidence,
        ObservationCount = observationCount,
        FirstSeen = DateTimeOffset.UtcNow,
        LastSeen = DateTimeOffset.UtcNow
    };
    _cache.Set(key, learnedWeight, GetCacheEntryOptions());

    // Queue for async SQLite persistence (write-behind)
    QueueWrite(signatureType, signature, weight, confidence, observationCount);

    return Task.CompletedTask;
}

Avis: UpdateWeightAsync Retours Task.CompletedTask - ilM SK1 est essentiellement synchrone. L'écriture est en attenteMSC3 n'est pas exécutée . Cela signifie

  • La latence sub-millisecondes d'écriture
  • Aucun blocage sur I/O
  • Les écritures sont conjuguées (la dernière fois que l'on écrit, on obtient des victoiresM SK1

The Background Flusher - 500ms of Boring Magic

Tous les 500ms, écritures en suspens sont transférées à SQLite dans un seul lot

// From WeightStore.cs:274-357
public async Task FlushPendingWritesAsync(CancellationToken ct = default)
{
    if (_pendingWrites.IsEmpty) return;

    // Only one flush at a time
    if (!await _flushLock.WaitAsync(0, ct)) return;

    try
    {
        await EnsureInitializedAsync(ct);

        // Snapshot and clear pending writes atomically
        var writes = new List<PendingWrite>();
        foreach (var key in _pendingWrites.Keys.ToList())
        {
            if (_pendingWrites.TryRemove(key, out var write))
            {
                writes.Add(write);
            }
        }

        if (writes.Count == 0) return;

        await using var conn = new SqliteConnection(_connectionString);
        await conn.OpenAsync(ct);
        await using var transaction = await conn.BeginTransactionAsync(ct);

        try
        {
            var sql = $@"
                INSERT INTO {TableName}
                    (signature_type, signature, weight, confidence,
                     observation_count, first_seen, last_seen)
                VALUES (@type, @sig, @weight, @conf, @count, @now, @now)
                ON CONFLICT(signature_type, signature) DO UPDATE SET
                    weight = @weight,
                    confidence = @conf,
                    observation_count = @count,
                    last_seen = @now
            ";

            foreach (var write in writes)
            {
                await using var cmd = new SqliteCommand(sql, conn, transaction);
                // ... add parameters and execute ...
            }

            await transaction.CommitAsync(ct);
            _logger.LogDebug("Flushed {Count} pending writes in {Duration:F1}ms",
                writes.Count, sw.ElapsedMilliseconds);
        }
        catch
        {
            await transaction.RollbackAsync(ct);
            throw;
        }
    }
    finally
    {
        _flushLock.Release();
    }
}

Il s'agit de événement-sourcing-light. Vous obtiendrez

  • Batches écrites (efficient I/OM SK2
  • Cohérence des transactions
  • Mise à jour coopérative (si la même signature est mise à jour MSC1 fois dans SMC2msMS C'est seulement la valeur finale qui est écrite
  • SQLite est parfaitement satisfait de ce modèle d'accès

Mise à jour de l’EMA - Apprentissage avec les moyennes de déplacement exponentielles

Lorsqu’une nouvelle observation arrive,, le système utilise des moyennes mobiles exponentielles pour mettre à jour les poids:

// From WeightStore.cs:584-635
public Task RecordObservationAsync(
    string signatureType,
    string signature,
    bool wasBot,
    double detectionConfidence,
    CancellationToken ct = default)
{
    var key = CacheKey(signatureType, signature);

    // Calculate new weight using EMA in memory
    var alpha = 0.1;  // Learning rate
    var weightDelta = wasBot ? detectionConfidence : -detectionConfidence;

    double newWeight;
    double newConfidence;
    int newObservationCount;

    if (_cache.TryGetValue(key, out LearnedWeight? existing) && existing != null)
    {
        // Apply EMA: new_weight = old_weight * (1-α) + delta * α
        newWeight = existing.Weight * (1 - alpha) + weightDelta * alpha;
        newConfidence = Math.Min(1.0, existing.Confidence + detectionConfidence * 0.01);
        newObservationCount = existing.ObservationCount + 1;
    }
    else
    {
        // First observation
        newWeight = weightDelta;
        newConfidence = detectionConfidence;
        newObservationCount = 1;
    }

    // Update cache immediately
    var learnedWeight = new LearnedWeight
    {
        SignatureType = signatureType,
        Signature = signature,
        Weight = newWeight,
        Confidence = newConfidence,
        ObservationCount = newObservationCount,
        FirstSeen = existing?.FirstSeen ?? DateTimeOffset.UtcNow,
        LastSeen = DateTimeOffset.UtcNow
    };
    _cache.Set(key, learnedWeight, GetCacheEntryOptions());

    // Queue for persistence
    QueueWrite(signatureType, signature, newWeight, newConfidence, newObservationCount);

    return Task.CompletedTask;
}

La formule EMA facilite l’apprentissage: new_weight = old_weight × (1 - α) + new_value × α

Avec α = 0.1:

  • Nouveaux éléments de preuve contribuent 10%
  • Les données historiques contribuent 90%
  • Cela empêche les mouvements sauvages d'observations simples.

Pourquoi le débordement fait du système Améliorer

Voici ce que la plupart des gens manquent :

Lorsque le cache se remplit:

  • Les signatures à faible -frequence tombent
  • Seules les signatures chaudes (accessibles fréquemment) restent en mémoire
  • La base de données est retardée par un cycle d'écoulement - et que 's fine
  • Le système Les groupes cibles sont les suivants : sous pression

Pensez-en.

Le plus chaud 10,000 - qui représentent habituellement 99% du trafic réel

Dans la production, je vois environ 40,000 une signature par jour - de plus de signatures par jour. Scrapers qui essaient une seule fois , des sondes aléatoires , des utilisateurs légitimes qui ne se sont jamais rendus chez eux SMK6 et peut-être MESK7 qui se reproduisent constamment PSK8 Ces ESK9 sont là où vivent les USK10 des personnes à risque MUSK11 Les longues OSK12 les signatures de coquilles OSK13 le bruit USK14 L'évacuation n'entrave pas l'exactitude du dépistage.

flowchart TB
    subgraph Input["50,000 Unique Signatures"]
        Hot[Hot Signatures\n~10,000]
        Cold[Cold Signatures\n~40,000]
    end

    subgraph Cache["Bounded Cache (10,000)"]
        Kept[Kept in Memory]
    end

    subgraph Evicted["Evicted"]
        Lost[Forgotten\nNoise Traffic]
    end

    Hot --> Kept
    Cold --> Lost

    style Hot fill:none,stroke:#10b981,stroke-width:2px
    style Cold fill:none,stroke:#94a3b8,stroke-width:2px
    style Kept fill:none,stroke:#10b981,stroke-width:2px
    style Lost fill:none,stroke:#ef4444,stroke-width:2px

Débordement améliore la qualité de l'eau la mémoire comportementale. Le système lui-même

Lorsque le débordement n’est pas utile't

Il y a des cas où la pression LRU fonctionne contre vous.

  • Cache trop petite: Si votre cache ne contient que 100 des entrées, mais que vous avez

  • Trafic uniforme: Si vous utilisez un système interne mince où presque tout est " chaud M SK3

  • Problème de démarrage Cold-: Un système nouvellement déployé n’a pas de poids appréhendés . Tout est tout aussi froid M SK2 Les premières heures auront des taux positifs faux plus élevés jusqu’à ce que les signatures chaudes s’établissent.

Le modèle fonctionne le mieux lorsque vous avez cardinalité élevée avec puissance-répartition de la loi - un grand nombre de signatures uniques , mais un petit sous-ensemble qui domine le trafic

Maintenir la cache et la base de données dans le sync - Tag-Based Invalidation

Le cache est la source de la vérité pour les lectures, mais la base de données est le ledger durable. Qu’arrive-t-il quand ils débordentM SK2

Synchronisation des décès

Lorsque les pondérations de la base de données détruisent, le cache doit suivre. DecayOldWeightsAsync la méthode gère les deux :

// From WeightStore.cs:725-766
public async Task DecayOldWeightsAsync(TimeSpan maxAge, double decayFactor, CancellationToken ct = default)
{
    await EnsureInitializedAsync(ct);

    await using var conn = new SqliteConnection(_connectionString);
    await conn.OpenAsync(ct);

    var cutoff = DateTimeOffset.UtcNow.Subtract(maxAge).ToString("O");

    // Decay old weights in the database
    var sql = $@"
        UPDATE {TableName}
        SET weight = weight * @decay,
            confidence = confidence * @decay
        WHERE last_seen < @cutoff
    ";

    await using var cmd = new SqliteCommand(sql, conn);
    cmd.Parameters.AddWithValue("@decay", decayFactor);
    cmd.Parameters.AddWithValue("@cutoff", cutoff);

    var updated = await cmd.ExecuteNonQueryAsync(ct);

    // Delete weights that have decayed below threshold
    var deleteSql = $@"
        DELETE FROM {TableName}
        WHERE confidence < 0.01 OR (ABS(weight) < 0.01 AND observation_count < 5)
    ";

    await using var deleteCmd = new SqliteCommand(deleteSql, conn);
    var deleted = await deleteCmd.ExecuteNonQueryAsync(ct);

    if (updated > 0 || deleted > 0)
    {
        _logger.LogInformation(
            "Weight decay: {Updated} decayed, {Deleted} deleted",
            updated, deleted);

        // Compact cache to remove stale entries
        _cache.Compact(0.25);
    }
}

Après la dégradation des dossiers de base de données, nous appelons _cache.Compact(0.25) - ce qui oblige MemoryCache à éliminer 25% de ses entrées, en donnant la priorité aux valeurs les moins récentes utilisées . La lecture suivante rechargera des valeurs nouvelles de la base de donnéesM SK3

Tag-Évacuation fondée

Parfois, vous devez invalider une catégorie entière d'entrées cachées - par exemple , lors de la réinstruction d'un détecteur ou lorsque les données externes changent.

// From WeightStore.cs:768-777
/// <summary>
///     Evicts all cached entries for a specific signature type (tag-based eviction).
/// </summary>

public void EvictByTag(string signatureType)
{
    // MemoryCache doesn't natively support tag-based eviction, but we can compact
    // For now, just compact - sliding expiration will handle stale entries
    _cache.Compact(0.1);
    _logger.LogDebug("Compacted cache for signature type: {SignatureType}", signatureType);
}

.NET's MemoryCache n’a pas d’étiquette native ', - évacuation basée sur Redis , mais la compaction a le même effet

La stratégie de synchronisation

Le point de vue clé est que la synchronisation parfaite n’est pas nécessaire'. Le système tolère la dérive parce que

  1. Défaut de recharger le cache à partir de DB - si une entrée est évacuée, la lecture suivante reçoit des données fraîches
  2. L'expiration en cascade permet d'éviter l'obsolescence - les entrées n'ont pas été consultées à l'intérieur de 30 minutes auto
  3. Renouvellement des forces de compaction - la compaction périodique supprime les anciens entrées
  4. Write-behind coalesces updates - plusieurs mises à jour rapides deviennent un seul fichier d'écriture

Il s’agit là d’une cohérence ultérieure faite correctement. Le cache demeure dans la base de données "établi suffisammentM SK2 sans nécessiter une logique complexe d’invalidation .

flowchart TB
    subgraph Sync["Cache-Database Synchronisation"]
        D[Database Decay] --> C[Cache Compact]
        E[Tag Eviction] --> C
        S[Sliding Expiration] --> M[Cache Miss]
        M --> R[Reload from DB]
    end

    style Sync fill:none,stroke:#6366f1,stroke-width:2px

Aucune invalidation du cache hell. Pas de pub complexe/subM SK2 Just compaction and natural expiration .

Le système de réputation

Note: Si vous vouliez simplement la LRU + écrireM SK1 derrière le modèle , vous pouvez vous arrêter ici. Le reste de cet article montre comment j'applique les mêmes idées à la réputation complète du modèle |- machines d'État | , | hysterèse |

Hysterèse et décès

Pour ce qui est de la réputation des modèles (le suivi, au fil du temps, si une signature est un robot ou un être humain ), les mêmes principes s’appliquent mais avec une plus grande sophistication

// From PatternReputation.cs:42-108
public record PatternReputation
{
    public required string PatternId { get; init; }
    public required string PatternType { get; init; }
    public required string Pattern { get; init; }

    /// <summary>Current bot probability [0,1]. 0 = human, 1 = bot, 0.5 = neutral</summary>

    public double BotScore { get; init; } = 0.5;

    /// <summary>Effective sample count - decays over time, increases with observations</summary>

    public double Support { get; init; } = 0;

    /// <summary>Current reputation state - determines fast-path behavior</summary>

    public ReputationState State { get; init; } = ReputationState.Neutral;

    // Computed properties
    public double Confidence => Math.Min(1.0, Support / 100.0);

    public bool CanTriggerFastAbort =>
        State is ReputationState.ConfirmedBad or ReputationState.ManuallyBlocked;

    public bool CanTriggerFastAllow =>
        State is ReputationState.ConfirmedGood or ReputationState.ManuallyAllowed;
}

Transitions d'État avec hysterèse

Les patrons ne tournent pas directement de Neutre à Bad confirmé.

// From PatternReputation.cs:367-421 - simplified
public PatternReputation EvaluateStateChange(PatternReputation reputation)
{
    if (reputation.IsManual)
        return reputation;

    var newState = reputation.State;
    var score = reputation.BotScore;
    var support = reputation.Support;

    switch (reputation.State)
    {
        case ReputationState.Neutral:
            // Can promote to Suspect or ConfirmedGood
            if (score >= 0.6 && support >= 10)
                newState = ReputationState.Suspect;
            else if (score <= 0.1 && support >= 100)
                newState = ReputationState.ConfirmedGood;
            break;

        case ReputationState.Suspect:
            // Can promote to ConfirmedBad or demote to Neutral
            if (score >= 0.9 && support >= 50)
                newState = ReputationState.ConfirmedBad;
            else if (score <= 0.4 || support < 10)
                newState = ReputationState.Neutral;
            break;

        case ReputationState.ConfirmedBad:
            // Can demote to Suspect (requires MORE evidence to forgive)
            if (score <= 0.7 && support >= 100)
                newState = ReputationState.Suspect;
            break;
    }

    // ... log state change and return ...
}

Notez l’asymétrie: c’est plus facile d’être bloqué que de ne pas être bloquéM SK2 ConfirmedBad → Suspect exige 100 de l'appui, pendant Neutral → Suspect Ce n’est pas intentionnel, c’est plus difficile de pardonner que de suspecter.

Perte de temps - Forgetting exponentiel

Lorsque les patrons sont silencieux, ils décroissent en direction neutre.

// From PatternReputation.cs:334-361
public PatternReputation ApplyTimeDecay(PatternReputation reputation)
{
    if (reputation.IsManual)
        return reputation;

    var hoursSinceLastSeen = (DateTimeOffset.UtcNow - reputation.LastSeen).TotalHours;

    if (hoursSinceLastSeen < 1)
        return reputation;  // Too recent to decay

    // Score decay toward prior (0.5 = neutral)
    // new_score = old_score + (prior - old_score) × (1 - e^(-Δt/τ))
    var scoreDecayFactor = 1 - Math.Exp(-hoursSinceLastSeen / _options.ScoreDecayTauHours);
    var newScore = reputation.BotScore + (0.5 - reputation.BotScore) * scoreDecayFactor;

    // Support decay
    // new_support = old_support × e^(-Δt/τ)
    var supportDecayFactor = Math.Exp(-hoursSinceLastSeen / _options.SupportDecayTauHours);
    var newSupport = reputation.Support * supportDecayFactor;

    return reputation with
    {
        BotScore = Math.Clamp(newScore, 0, 1),
        Support = newSupport
    };
}

Les constantes de temps par défaut:

  • La dégradation de la note τ: 168 heures (7 joursMSC3 | | 4 | Les points se déplacent vers le neutre après une semaine d’inactivité
  • Soutien à la dégradation τLa confiance diminue après deux semaines 63%

Cela signifie qu’une IP non conforme confirmée qui reste silencieuse pendant un mois sera finalement ramenée à une IP neutre. Non parce qu’elle a réformé, mais parce que ses preuves sont devenues inexistantes.

Le service d'entretien de base

L'article 5 de la Loi sur l'immigration et le statut des réfugiés ReputationMaintenanceService exécute trois tâches périodiques:

// From ReputationMaintenanceService.cs:48-129
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    _logger.LogInformation("Reputation maintenance service starting");

    // Load persisted reputations on startup
    await _cache.LoadAsync(stoppingToken);

    var decayInterval = TimeSpan.FromMinutes(60);    // Hourly decay sweep
    var gcInterval = TimeSpan.FromHours(24);          // Daily garbage collection
    var persistInterval = TimeSpan.FromMinutes(5);   // Persist every 5 minutes

    var lastDecay = DateTimeOffset.UtcNow;
    var lastGc = DateTimeOffset.UtcNow;
    var lastPersist = DateTimeOffset.UtcNow;

    while (!stoppingToken.IsCancellationRequested)
    {
        await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        var now = DateTimeOffset.UtcNow;

        // Decay sweep: push stale scores toward neutral
        if (now - lastDecay >= decayInterval)
        {
            await _cache.DecaySweepAsync(stoppingToken);
            lastDecay = now;
        }

        // Garbage collection: remove old neutral patterns
        if (now - lastGc >= gcInterval)
        {
            await _cache.GarbageCollectAsync(stoppingToken);
            lastGc = now;

            var stats = _cache.GetStats();
            _logger.LogInformation(
                "Reputation stats: {Total} patterns, {Bad} bad, {Suspect} suspect",
                stats.TotalPatterns, stats.ConfirmedBadCount, stats.SuspectCount);
        }

        // Persistence: save to SQLite
        if (now - lastPersist >= persistInterval)
        {
            await _cache.PersistAsync(stoppingToken);
            lastPersist = now;
        }
    }

    // Final persist on shutdown
    await _cache.PersistAsync(CancellationToken.None);
}

Le collecteur de déchets élimine les patrons qui sont:

  • 90+ ans
  • Soutien ≤ 1.0
  • Dans l'état neutre

Cela empêche le dictionnaire de mémoire in- d’augmenter sans limite tout en préservant des modèles d’apprentissage précieux.

SQLite n’est pas une chose de frite - Il est parfait ici

Un grand nombre de développeurs s’adressent réflexivement à PostgreSQL ou Redis. Mais pour ce modèleM SK1 SQLite est idéal:

  1. Write-behind élimine les lacunes - SQLite 's singleM SK2writer limitation doesn't matter when writes are batched
  2. Stockage local - aucune latence de réseau, aucun bassin de connexion
  3. Configuration zéro - simplement un chemin de fichier
  4. Perfect pour le déploiement à bord - fonctionne sur un Raspberry Pi
  5. Portable - la base de données est simplement un fichier que vous pouvez copier autour

Le schéma est minimal:

CREATE TABLE IF NOT EXISTS learned_weights (
    signature_type TEXT NOT NULL,
    signature TEXT NOT NULL,
    weight REAL NOT NULL,
    confidence REAL NOT NULL,
    observation_count INTEGER NOT NULL DEFAULT 1,
    first_seen TEXT NOT NULL,
    last_seen TEXT NOT NULL,
    PRIMARY KEY (signature_type, signature)
);

CREATE INDEX IF NOT EXISTS idx_signature_type ON learned_weights(signature_type);
CREATE INDEX IF NOT EXISTS idx_confidence ON learned_weights(confidence);
CREATE INDEX IF NOT EXISTS idx_last_seen ON learned_weights(last_seen);

Si vous avez besoin d'une échelle plus grande, échanger à PostgreSQL. Si vous aurez besoin de HA ou de réplicationM SK2 échangez à Redis ou à un cache distribuéMSC3 L'architecture ne modifie pas ' n'a pas de changements - simplement la chaîne de connexionMNK6 SQLite est par défaut pour le déploiement sur les bords

Évolution de l'échec du lien DiSE-In -

Si DiSE Ce modèle de cache est le mitochondrie - la pièce la plus petite qui se comporte encore comme l’évolution sous des contraintes.

Ce modèle met en œuvre les principes de DiSE au niveau le plus minimal:

  • Constraints de ressources → Pression de sélection
  • Éviction des LRU → Selection naturelle (les survivants sont les plus aptesM SK2
  • La dégradation du temps → Le fait de oublier permet l’adaptation
  • Mise à jour de l'EMA → Mutation par observation
  • Hysterèse → Stabilité grâce à la résistance au changement

Un cache complet n’est pas un échec't . It's pression évolutive. Le système lui-mêmeM SK1 optimise : les signatures chaudes demeurent , les signataires froids évacuent MSC4 et la mémoire comportementale converge vers ce qui est réellement important

Aucune formation en ML. Pas de modèles externes. Une seule architecture qui se comporte comme un système vivant

Conclusion - Structures simples , comportement d'urgence

L’ensemble du modèle se résume à:

  1. Cache est la source de vérité pour les lectures - sous-accès en millisecondes
  2. Rédige immédiatement le cache de mise à jour, persiste plus tard - sans blocage IM SK1O
  3. L'expiration des Sliding fournit une LRU automatique - intégré à .NET
  4. La taille limitée crée une pression de sélection - le débordement accroît la concentration
  5. Flush de fond maintient SQLite en sync - la cohérence finale est bonne
  6. La dégradation du temps permet de oublier - Les preuves inexistantes disparaissent

Votre petit magasin comportemental fonctionne plus comme un système vivant que le CRUD. Il se souvient de ce qui est important , iloublie ce qui ne l’est pasM SK2t, et s’améliore sous pressionMSC4

Architecture minimale → correction émergente. Débordement → meilleur focus. Pression → stabilitéM SK1

Si vous voulez voir cela en action, consultez mostlylucid.botdetection - et Les séries d'architecture DiSE pour une philosophie plus profonde.

Liens

logo

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