TL;DR

Implementeremo un pattern Saga resiliente utilizzando gRPC per gestire transazioni distribuite tra microservizi. Copriremo le basi, ti mostreremo come configurarlo e includeremo anche alcuni esempi di codice interessanti. Alla fine, sarai in grado di orchestrare transazioni distribuite come un direttore d'orchestra che guida una sinfonia di microservizi.

La Saga della Saga: Una Breve Introduzione

Prima di addentrarci nei dettagli, facciamo un rapido riepilogo di cosa tratta il pattern Saga:

  • Una saga è una sequenza di transazioni locali
  • Ogni transazione aggiorna i dati all'interno di un singolo servizio
  • Se un passaggio fallisce, vengono eseguite transazioni di compensazione per annullare le modifiche precedenti

Pensalo come un pulsante di annullamento sofisticato per il tuo sistema distribuito. Ora, vediamo come possiamo implementarlo usando gRPC.

Perché gRPC per le Sagas?

Potresti chiederti, "Perché gRPC? Non posso semplicemente usare REST?" Beh, potresti, ma gRPC offre alcuni vantaggi significativi:

  • Serializzazione binaria efficiente (Protocol Buffers)
  • Tipizzazione forte
  • Streaming bidirezionale
  • Supporto integrato per autenticazione, bilanciamento del carico e altro

Inoltre, è incredibilmente veloce. Chi non ama la velocità?

Preparare il Palcoscenico

Iniziamo definendo il nostro servizio in Protocol Buffers. Creeremo un semplice servizio OrderSaga:

syntax = "proto3";

package ordersaga;

service OrderSaga {
  rpc StartSaga(SagaRequest) returns (SagaResponse) {}
  rpc CompensateSaga(CompensationRequest) returns (CompensationResponse) {}
}

message SagaRequest {
  string order_id = 1;
  double amount = 2;
}

message SagaResponse {
  bool success = 1;
  string message = 2;
}

message CompensationRequest {
  string order_id = 1;
}

message CompensationResponse {
  bool success = 1;
  string message = 2;
}

Questo imposta il nostro servizio di base con due RPC: uno per avviare la saga e un altro per la compensazione se qualcosa va storto.

Implementazione del Coordinatore della Saga

Ora, creiamo un Coordinatore della Saga che orchestrerà la nostra transazione distribuita. Useremo Go per questo esempio, ma sentiti libero di usare il linguaggio che preferisci.

package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    pb "path/to/your/proto"
)

type server struct {
    pb.UnimplementedOrderSagaServer
}

func (s *server) StartSaga(ctx context.Context, req *pb.SagaRequest) (*pb.SagaResponse, error) {
    // Implementa la logica della saga qui
    log.Printf("Inizio saga per ordine: %s", req.OrderId)

    // Chiama altri microservizi per eseguire la transazione distribuita
    if err := createOrder(req.OrderId); err != nil {
        return &pb.SagaResponse{Success: false, Message: "Creazione ordine fallita"}, nil
    }

    if err := processPayment(req.OrderId, req.Amount); err != nil {
        // Compensa per la creazione dell'ordine
        cancelOrder(req.OrderId)
        return &pb.SagaResponse{Success: false, Message: "Elaborazione pagamento fallita"}, nil
    }

    if err := updateInventory(req.OrderId); err != nil {
        // Compensa per la creazione dell'ordine e il pagamento
        cancelOrder(req.OrderId)
        refundPayment(req.OrderId, req.Amount)
        return &pb.SagaResponse{Success: false, Message: "Aggiornamento inventario fallito"}, nil
    }

    return &pb.SagaResponse{Success: true, Message: "Saga completata con successo"}, nil
}

func (s *server) CompensateSaga(ctx context.Context, req *pb.CompensationRequest) (*pb.CompensationResponse, error) {
    // Implementa la logica di compensazione qui
    log.Printf("Compensazione saga per ordine: %s", req.OrderId)

    // Chiama i metodi di compensazione per ogni passaggio
    cancelOrder(req.OrderId)
    refundPayment(req.OrderId, 0) // Potresti voler memorizzare l'importo da qualche parte
    restoreInventory(req.OrderId)

    return &pb.CompensationResponse{Success: true, Message: "Compensazione completata"}, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("Ascolto fallito: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterOrderSagaServer(s, &server{})
    log.Println("Server in ascolto su :50051")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("Servizio fallito: %v", err)
    }
}

// Implementa queste funzioni per interagire con altri microservizi
func createOrder(orderId string) error { /* ... */ }
func processPayment(orderId string, amount float64) error { /* ... */ }
func updateInventory(orderId string) error { /* ... */ }
func cancelOrder(orderId string) error { /* ... */ }
func refundPayment(orderId string, amount float64) error { /* ... */ }
func restoreInventory(orderId string) error { /* ... */ }

Questa implementazione mostra la struttura di base del nostro Coordinatore della Saga. Gestisce la logica principale della transazione distribuita e fornisce meccanismi di compensazione se un passaggio fallisce.

Gestione dei Fallimenti e dei Tentativi

In un sistema distribuito, i fallimenti non sono solo possibili – sono inevitabili. Aggiungiamo un po' di resilienza alla nostra implementazione della Saga:

func (s *server) StartSaga(ctx context.Context, req *pb.SagaRequest) (*pb.SagaResponse, error) {
    maxRetries := 3
    var err error

    for i := 0; i < maxRetries; i++ {
        err = s.executeSaga(ctx, req)
        if err == nil {
            return &pb.SagaResponse{Success: true, Message: "Saga completata con successo"}, nil
        }
        log.Printf("Tentativo %d fallito: %v. Riprovo...", i+1, err)
    }

    // Se abbiamo esaurito tutti i tentativi, compensa e restituisci errore
    s.CompensateSaga(ctx, &pb.CompensationRequest{OrderId: req.OrderId})
    return &pb.SagaResponse{Success: false, Message: "Saga fallita dopo diversi tentativi"}, err
}

func (s *server) executeSaga(ctx context.Context, req *pb.SagaRequest) error {
    // Implementa qui la logica effettiva della saga
    // ...
}

Questo meccanismo di ripetizione offre alla nostra Saga alcune possibilità di successo prima di arrendersi e avviare la compensazione.

Monitoraggio e Logging

Quando si gestiscono transazioni distribuite, la visibilità è fondamentale. Aggiungiamo un po' di logging e metriche al nostro Coordinatore della Saga:

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
)

var (
    sagaSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
        Name: "saga_success_total",
        Help: "Il numero totale di saghe riuscite",
    })
    sagaFailureCounter = promauto.NewCounter(prometheus.CounterOpts{
        Name: "saga_failure_total",
        Help: "Il numero totale di saghe fallite",
    })
)

func (s *server) StartSaga(ctx context.Context, req *pb.SagaRequest) (*pb.SagaResponse, error) {
    log.Printf("Inizio saga per ordine: %s", req.OrderId)
    defer func(start time.Time) {
        log.Printf("Saga per ordine %s completata in %v", req.OrderId, time.Since(start))
    }(time.Now())

    // ... (logica della saga)

    if err != nil {
        sagaFailureCounter.Inc()
        log.Printf("Saga fallita per ordine %s: %v", req.OrderId, err)
        return &pb.SagaResponse{Success: false, Message: "Saga fallita"}, err
    }

    sagaSuccessCounter.Inc()
    return &pb.SagaResponse{Success: true, Message: "Saga completata con successo"}, nil
}

Queste metriche possono essere facilmente integrate con sistemi di monitoraggio come Prometheus per darti informazioni in tempo reale sulle prestazioni della tua Saga.

Testare la Tua Saga

Testare le transazioni distribuite può essere complicato, ma è cruciale. Ecco un semplice esempio di come potresti testare il tuo Coordinatore della Saga:

func TestStartSaga(t *testing.T) {
    // Configura un server mock
    s := &server{}

    // Crea una richiesta di test
    req := &pb.SagaRequest{
        OrderId: "test-order-123",
        Amount:  100.50,
    }

    // Chiama il metodo StartSaga
    resp, err := s.StartSaga(context.Background(), req)

    // Verifica i risultati
    if err != nil {
        t.Errorf("StartSaga ha restituito un errore: %v", err)
    }
    if !resp.Success {
        t.Errorf("StartSaga fallita: %s", resp.Message)
    }
}

Ricorda di testare anche gli scenari di fallimento e la logica di compensazione!

Conclusione

Ecco fatto! Abbiamo implementato un pattern Saga resiliente utilizzando gRPC per gestire transazioni distribuite. Ricapitoliamo ciò che abbiamo imparato:

  • Il pattern Saga aiuta a gestire transazioni distribuite tra microservizi
  • gRPC fornisce un modo efficiente e fortemente tipizzato per implementare le Sagas
  • Una corretta gestione degli errori e dei tentativi è cruciale per la resilienza
  • Il monitoraggio e il logging offrono visibilità sulle tue transazioni distribuite
  • Il testing è impegnativo ma essenziale per Sagas affidabili

Ricorda, le transazioni distribuite sono complesse. Questa implementazione è un punto di partenza, e probabilmente dovrai adattarla al tuo caso d'uso specifico. Ma con queste conoscenze, sei ben avviato a domare il mostro delle transazioni distribuite.

Spunti di Riflessione

Prima di andare, ecco alcune domande su cui riflettere:

  • Come gestiresti le Sagas a lungo termine che potrebbero superare i limiti di timeout di gRPC?
  • Quali strategie potresti adottare per rendere il tuo Coordinatore della Saga stesso tollerante ai guasti?
  • Come potresti integrare questo pattern Saga con architetture esistenti basate su eventi?

Buona programmazione, e che le tue transazioni siano sempre coerenti!