Docker Compose Ortamında Dapr Statestore ile Durum Yönetimi

Son yıllarda hepimiz microservislerin yazılım dünyasını nasıl etkilediğini gördük, değil mi? Adeta bir devrim! Ama biliyoruz ki, her devrimin getirdiği yeniliklerin yanı sıra bazı zorlukları da oluyor. Microservislerde bu zorluklardan biri de şu: Servisler arasında veri durumunu nasıl yöneteceğiz?

Birçok firma bu sorunla boğuşurken, Statestore gibi mekanizmalar devreye girdi ve “Merak etmeyin, ben buradayım!” dedi. Peki, Statestore tam olarak ne yapıyor? Aslında oldukça basit: Microservisler arasında veriyi güvende tutmak, paylaşmak ve sorgulamak için bize yardımcı oluyor. Ancak, bu işlemi manuel olarak yapmak gerçekten baş ağrıtıcı olabiliyor.

İşte bu noktada Dapr sahneye çıkıyor ve microservislerin veri yönetimi karmaşasını ortadan kaldırıyor. Dapr’ın sunduğu API’ler sayesinde, bizim ekstra kod yazmamıza gerek kalmadan hem zaman kazanıyoruz, hem de hatalardan kaçınıyoruz.

Bu yazıda, Dapr’ın bize neler sunduğuna daha yakından bakacağız. Hazır mısınız?

Daha önceki yazımda Basket Api’de basket datasını herhangi bir yere yazmadık. Basket datasını siparişe dönüşene kadar geçici veriler olarak değerlendirebiliriz. Tabi bu iş kurallarına göre değişkenlik gösterir. Bu sebeple bu datayı Dapr kullanarak Redis’ e yazacağım. Peki Redis şart mı ? Aslında değil, Dapr stateleri saklayabilmek için birçok database destekliyor. Liste çok uzun olduğu için linki buraya bırakıyorum.

Yapacağımız işlemleri ana başlıklar ile belirlemek istiyorum.

  1. Dapr Statestore componentini hazırlamak.
  2. Docker-Compose hazırladığım componenti dahil etmek.
  3. StateStore’un kod tarafında yönetimi için bir alt yapı hazırlamak
  4. Basket datasını Statestore yazmak

Öncelikle Dapr’da statestore olarak belirlediğim db ile iletişim kurmasını sağlayacak componenti oluşturmam gerekecek.

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: redis:6379
  - name: redisPassword
    value: ""
scopes:
  - basket-api

Kırmızı ile vurguladığım yerler kritik.

name: statestore” : Burada verdiğimiz isim statestore temsil ediyor ve uygulama içerisinden statestore bu isim ile çağırıyoruz. O neden bu isim bizim için önemli.

type: state.redis“: Bu kısım da Dapr’ın statestore olarak hangi db ile çalışacağını belirtiyoruz. Burada Dapr’ın statestore için verdiği destek listesi çok geniş. Bir alt yapı kaygısı yaşamadan herhangi birisi ile yukarıdaki gibi bir yaml hazırlayıp bağlantı kurabiliriz.

value: redis:6379“: Bu kısımda Redis sunucusunun adresini veriyorum. Dikkat edin Redis sunucusunu “redis:6379” olarak belirtiyorum. Bunun nedeni docker-compose içerisinde Redis containerına “redis” ismini vermiş olmam.

scopes:” : Bu kısımda çok önemli. Eğer Dapr’da tanımladığınız componentlerin çalışacağı scope vermezseniz ilgili sidecar statestore ile bağlantı kuramayacak. Yani “basket-api” ile statestorun el sıkışabilmesi bu parametre çok önemli.

Şimdi componenti oluşturduğumuza göre Docker-Compose içerisinde bu componentlerin ilgili sidecarlara eklenmesi gerekiyor. Yoksa sidecarın bu componentten haberi olmayacak. Bu işlemi Docker-compose’da aşağıda kırmızı ile vurguladığım kısım ile yapıyorum.

  daprdotnetjourney.microservices.basket.api.dapr:
    command: ["./daprd",
      "-app-id", "basket-api",
      "-app-port", "80",
      "-components-path", "/components",
      "-config", "/configuration/config.yaml"
      ]
    volumes:
      - "./dapr/components/:/components"
      - "./dapr/configuration/:/configuration"

Docker-Compose kısmınıda hallettiğimize göre Statestore’a ekleme, silme, güncelleme, sorgulama yapabileceğiniz bir alt yapı oluşturmak istiyorum. Projeyi, Framework dizini altında “DaprDotNetJourney.Framework.Dapr” ismi ile oluşturacağım.

dotnet add package Dapr.Client --version 1.11.0
using Dapr.Client;

namespace DaprDotNetJourney.Framework.Dapr.Abstractions
{
    public interface IDaprStateStore
    {
        Task SaveStateAsync<T>(string storeName, string key, T value);
        Task SaveStateAsync<T>(string storeName, string key, T value, TimeSpan ttl);
        Task<T> GetStateAsync<T>(string storeName, string key);
        Task<T> UpdateStateAsync<T>(string storeName, string key, T newStore);
        Task<T> UpdateStateAsync<T>(string storeName, string key, T newStore, TimeSpan ttl);
        Task DeleteStateAsync(string storeName, string key);
        Task<StateQueryResponse<T>> QueryStateAsync<T>(string storeName, string jsonString);
    }
}
using Dapr.Client;
using DaprDotNetJourney.Framework.Dapr.Abstractions;
using Microsoft.Extensions.Logging;

public class DaprStateStore: IDaprStateStore
{
    private readonly ILogger<DaprStateStore> _logger;
    public DaprStateStore(ILogger<DaprStateStore> logger)
    {
        _logger = logger;
    }
    public async Task SaveStateAsync<T>(string storeName,string key, T value, TimeSpan ttl)
    {
        _logger.LogInformation($"Saving state with key '{key}' and TTL '{ttl}' to store '{storeName}'.");

        using var daprClient = new DaprClientBuilder().Build();

        if (ttl != null)
        {
            var metadata = new Dictionary<string, string>
            {
                { "ttlInSeconds", ttl.TotalSeconds.ToString() }
            };

            await daprClient.SaveStateAsync(storeName, key, value, metadata: metadata);
        }
        else
        {
            await daprClient.SaveStateAsync(storeName, key, value);
        }

        _logger.LogInformation($"State with key '{key}' saved successfully.");
    }

    public async Task SaveStateAsync<T>(string storeName, string key, T value)
    {
        _logger.LogInformation($"Saving state with key '{key}' to store '{storeName}'.");

        using var daprClient = new DaprClientBuilder().Build();

        await daprClient.SaveStateAsync(storeName, key, value);

        _logger.LogInformation($"State with key '{key}' saved successfully.");
    }

    public async Task<T> GetStateAsync<T>(string storeName, string key)
    {
        _logger.LogInformation($"Getting state with key '{key}' from store '{storeName}'.");

        using var daprClient = new DaprClientBuilder().Build();

        var result = await daprClient.GetStateAsync<T>(storeName, key);

        _logger.LogInformation($"State with key '{key}' retrieved successfully.");

        return result;
    }

    public async Task<T> UpdateStateAsync<T>(string storeName, string key, T newStore)
    {
        _logger.LogInformation($"Updating state with key '{key}' in store '{storeName}'.");

        using var daprClient = new DaprClientBuilder().Build();

        var state = await daprClient.GetStateEntryAsync<T>(storeName, key);

        state.Value = newStore;

        await state.SaveAsync();

        _logger.LogInformation($"State with key '{key}' updated successfully.");

        return newStore;
    }

    public async Task<T> UpdateStateAsync<T>(string storeName, string key, T newStore, TimeSpan ttl)
    {
        _logger.LogInformation($"Updating state with key '{key}' and TTL '{ttl}' in store '{storeName}'.");

        using var daprClient = new DaprClientBuilder().Build();

        var metadata = new Dictionary<string, string>
        {
            { "ttlInSeconds", ttl.TotalSeconds.ToString() }
        };

        var state = await daprClient.GetStateEntryAsync<T>(storeName, key, metadata: metadata);

        state.Value = newStore;

        await state.SaveAsync();

        _logger.LogInformation($"State with key '{key}' updated successfully.");

        return newStore;
    }

    public async Task<StateQueryResponse<T>> QueryStateAsync<T>(string storeName, string jsonString)
    {
        _logger.LogInformation($"Querying state in store '{storeName}' with query string '{jsonString}'.");

        using var daprClient = new DaprClientBuilder().Build();

        var response = await daprClient.QueryStateAsync<T>(storeName, jsonString);

        _logger.LogInformation($"State query completed successfully.");

        return response;
    }

    public async Task DeleteStateAsync(string storeName, string key)
    {
        _logger.LogInformation($"Deleting state with key '{key}' from store '{storeName}'.");

        using var daprClient = new DaprClientBuilder().Build();
        await daprClient.DeleteStateAsync(storeName, key);

        _logger.LogInformation($"State with key '{key}' deleted successfully.");
    }
}
using DaprDotNetJourney.Framework.Dapr.Abstractions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace DaprDotNetJourney.Framework.Dapr.Extensions;

public static class DaprServiceCollectionExtensions
{
        public static void AddConfigureStateStore(this WebApplicationBuilder builder)
        {
            builder.Services.AddScoped<IDaprStateStore>(sp => new DaprStateStore(sp.GetRequiredService<ILogger<DaprStateStore>>()));
        }

}

Hazırladığım basit altyapının kodlarını yukarıda paylaşım.

Artık basket datasını statestore yazabilirim. İlk olarak “Aggregator Api” de bulunan “UpdateBasketAsync” endpointine basket datasını gönderiyorum. Bu datayı Aggregator Api, service invocation yöntemi ile Basket Api’ sinde “UpdateBasketAsync” endpointine iletiyor. Artık veri Basket Api’sine geldiğine göre, müşteriye ait basket datasını satatestora kaydedebilirim.

    public class UpdateBasketCommandHandler : IRequestHandler<UpdateBasketCommand, Result<BasketDto>>
    {
        private readonly IDaprStateStore _stateStore;
        public UpdateBasketCommandHandler(IDaprStateStore stateStore)
        {
            _stateStore = stateStore;
        }
        private const string DAPR_PUBSUB_NAME = "pubsub";
        public async Task<Result<BasketDto>> Handle(UpdateBasketCommand request, CancellationToken cancellationToken)
        {
            await _stateStore.UpdateStateAsync(DAPR_PUBSUB_NAME, request.BuyerId, request);

            var basket = await _stateStore.GetStateAsync<BasketDto>(DAPR_PUBSUB_NAME, request.BuyerId);

            return new SuccessResult<BasketDto>(basket)
            {
                Messages = new List<string>
                    {
                        "Ürün sepete eklendi"
                    }
            };
        }
    }

“IDaprStateStore” interfacesi yukarıda oluşturduğum alt yapıdan gelmekte. Bu işlem sonucunda basket datası Redis’e kaydedilecektir. Aggragator Api’nin swagger arayüzünden aşağıdaki gibi bir basket datası yolluyorum.

Redis’te oluşan basket datasını görüntümeleniz için birkaç yol var. Redis containerına bağlanıp “redis-cli” ile tüm dataları görebilirsiniz. Ama sizin için Docker-Compose’ a “Redis Insight” ekledim. Buradan tüm dataları bu güzel UI’ dan görebilirsiniz.

Yukarıdaki basket datasında yolladığım “buyerId” datasını bu stateti saklamak ve daha sonra çağırmak için kullanıyorum.

“basket-api||90992345-DCFA-4C6F-BB1B-7C203EFA04D9” şekliden oluşan ifadeye dikkat edin, önce state datasını tutan Sidecar’ın adı ve sonra “buyerId” ifadesi gelmiş. Bu özetle şu demek, bu data “basket-api” microservisine ait ve başka bir microservis bu dataya ulaşamaz. Peki tüm microservislerin ulaşabiceği bir state oluşturabilir miyiz? Evet bu mümkün, sonraki makalelerde bu konuya değineceğim.

Projesinin linkini buraya bırakıyorum. Keyifli okumalar.

İnternet sitesi http://cahityusuf.com
Yazı oluşturuldu 6

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Benzer yazılar

Aramak istediğinizi üstte yazmaya başlayın ve aramak için enter tuşuna basın. İptal için ESC tuşuna basın.

Üste dön