Skip to main content

Sorun: L1 her node’da ayrı

HybridCache’in L2’si (Redis) paylaşımlıdır — tüm node’lar aynı Redis’i görür, tag mark’ı herkesçe okunur. Ancak L1 (in-process memory) her node’da ayrıdır. Bir node tag’i düşürüp kendi L1’ini temizlese de, diğer node’ların L1’inde aynı veri TTL dolana kadar bayat kalır. Çözüm: bir node tag düşürdüğünde EventBus üzerinden diğer node’lara haber verip onların da sadece L1’lerini düşürmesini sağlamak.

EventBusRemoteTagBroadcaster

IRemoteTagBroadcaster’ın EventBus (MassTransit + RabbitMQ) backend’li implementasyonu (src/.../Application/SeedWork/Caching/EventBusRemoteTagBroadcaster.cs). IHybridRequestCache.RemoveByTagAsync içinden, BroadcastTagInvalidation açıksa tetiklenir.
public sealed class EventBusRemoteTagBroadcaster : IRemoteTagBroadcaster
{
    // Process yaşam süresi boyunca sabit, process başına unique node kimliği.
    public static readonly string NodeId = Guid.NewGuid().ToString("N");

    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<EventBusRemoteTagBroadcaster> _logger;

    public async ValueTask BroadcastInvalidationAsync(
        IReadOnlyCollection<string> tags, CancellationToken cancellationToken = default)
    {
        if (tags is null || tags.Count == 0) return;

        // Singleton broadcaster, Scoped IEventBus → per-call scope.
        using var scope = _scopeFactory.CreateScope();
        var eventBus = scope.ServiceProvider.GetRequiredService<IEventBus>();

        var evt = new CacheInvalidationIntegrationEvent(tags, NodeId);
        await eventBus.PublishAsync(evt, cancellationToken);
    }
}
Lifetime deseni: IHybridRequestCache Singleton, dolayısıyla broadcaster da Singleton; ama IEventBus Scoped. Bu yüzden ctor injection yerine IServiceScopeFactory tutulup her broadcast’te scope açılır — standart “Singleton consumes Scoped” pattern’i. Tag invalidation seyrek olduğu için maliyet düşük.
NodeId bilerek static’tir: aynı process içinde hem broadcaster (publish) hem handler (consume) aynı değeri okur — echo tespiti bunun üzerine kuruludur.

CacheInvalidationIntegrationEvent

Yayınlanan event (src/BuildingBlocks/.../Contracts.Events/Cache/CacheInvalidationIntegrationEvent.cs):
[MessageUrn("urn:event:cache-invalidation:v1")]
public sealed class CacheInvalidationIntegrationEvent : IntegrationEvent
{
    public IReadOnlyCollection<string> Tags { get; init; } = Array.Empty<string>();

    // Hangi node yayınladı — receiver kendi event'ini atlamak için karşılaştırır.
    public string SourceNodeId { get; init; } = string.Empty;

    public CacheInvalidationIntegrationEvent() { }
    public CacheInvalidationIntegrationEvent(IReadOnlyCollection<string> tags, string sourceNodeId)
    {
        Tags = tags;
        SourceNodeId = sourceNodeId;
    }
}

Echo guard + cascade önleme

Receiver CacheInvalidationIntegrationEventHandler (src/.../Application/IntegrationEventHandlers/CacheInvalidation/) iki önlem taşır:
public async Task Handle(CacheInvalidationIntegrationEvent @event)
{
    // 1) ECHO GUARD: kendi yayınladığımız event'i de görürüz → kendi NodeId'mizi atla.
    if (@event.SourceNodeId == EventBusRemoteTagBroadcaster.NodeId)
    {
        _logger.LogTrace("[CacheBroadcast] Skip echo: tags=[{Tags}]", string.Join(",", @event.Tags));
        return;
    }

    foreach (var tag in @event.Tags)
    {
        try
        {
            // 2) CASCADE ÖNLEME: RemoveByTagLocallyAsync — sadece L1, tekrar broadcast YOK.
            await _cache.RemoveByTagLocallyAsync(tag);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "[CacheBroadcast] Local tag drop failed: tag={Tag}", tag);
        }
    }
}
ÖnlemNeyi engeller
Echo guard (SourceNodeId == NodeId)Origin node kendi broadcast’ini tüketip L1’ini iki kez düşürmesin.
RemoveByTagLocallyAsyncReceiver tekrar broadcast yapmasın → sonsuz cascade önlenir (RemoveByTagAsync broadcast tetiklerdi).

Konfigürasyon

{
  "Cache": {
    "BroadcastTagInvalidation": true
  }
}
OrtamBroadcastTagInvalidation
Single-node (dev, küçük prod)false — broadcast gereksiz, RabbitMQ trafiği boşuna.
Multi-instance / multi-podtrue — yoksa diğer node’ların L1’i invalidate edilemez, bayat veri döner.
Broadcaster registration: BuildingBlocks.Caching.AddCache default olarak NoopRemoteTagBroadcaster’ı TryAdd ile koyar. Application katmanı EventBus aktifken bunu gerçek impl ile Replace eder (AddSingleton, TryAdd değil):
// Application/DependencyInjection.cs — EventBus register edildikten SONRA
builder.Services.AddSingleton<IRemoteTagBroadcaster, EventBusRemoteTagBroadcaster>();

İlgili

Invalidation

RemoveByTagAsync vs RemoveByTagLocallyAsync.

Integration Events

IntegrationEvent base ve consumer kaydı.

RabbitMQ Topology

Exchange, routing key, queue.

Cache Mimarisi

Genel bakış.