Saltar a contenido

Cluster Internal Protocols

Dos protocolos internos del cluster Zeebe: deployment distribution (replicar deployments cross-partition con ACK + redistribution scheduler para eventual consistency) y snapshot transfer (chunk-based con CRC32C checksums, autocontained chunks, no retry built-in, reservation pattern para prevenir GC durante transfer). Para single-node MVP, ambos son skippeable — PostgreSQL replication maneja todo.

1. Deployment Distribution

Problema

Cuando un usuario hace deploy en la DEPLOYMENT_PARTITION (partition 1), todas las demás particiones necesitan tener el deployment también, para poder crear process instances usando esa definition.

Flujo cross-partition

sequenceDiagram
    participant C as Cliente
    participant G as Gateway
    participant P1 as Partition 1 (DEPLOYMENT_PARTITION)
    participant CDB as CommandDistributionBehavior
    participant PN as Partition 2..N
    participant DCP as DeploymentDistributionCompleteProcessor

    C->>G: Deploy request
    G->>P1: DeploymentCreate
    Note over P1: DeploymentCreateProcessor:<br/>Authorize, validate, transform<br/>Emit DEPLOYMENT.CREATED<br/>distributeCommand()
    P1->>CDB: distribute
    CDB->>PN: DEPLOYMENT.DISTRIBUTE (InterPartitionCommandSender)
    Note over PN: DeploymentDistributeProcessor:<br/>Emit DEPLOYMENT.DISTRIBUTED (local)<br/>manage start event subscriptions
    PN-->>DCP: DEPLOYMENT_DISTRIBUTION.COMPLETE (ACK)
    Note over DCP: Marca partition como ACK<br/>Cuando TODAS ACK → finalized
    DCP-->>G: Final response
    G-->>C: Final response

Componentes

Clase Responsabilidad
DeploymentDistributeProcessor Procesa DISTRIBUTE en receiver partitions
DeploymentDistributionCommandSender Envía commands cross-partition
DeploymentDistributionCompleteProcessor Maneja ACKs en sender partition
DeploymentRedistributionScheduler Reintentos periódicos para distributions pendientes

@ExcludeAuthorizationCheck

DeploymentDistributeProcessor está marcado con esta anotación:

@ExcludeAuthorizationCheck
public final class DeploymentDistributeProcessor implements TypedRecordProcessor<DeploymentRecord>

Razón: commands distribuidos cross-partition son commands internos del sistema, no del cliente. La authorization ya se validó en la origin partition. Re-validar requeriría que el sender (el sistema) tenga credenciales, lo cual es overhead innecesario.

Pattern reusable: marker para distinguir commands internos de commands de cliente.

Redistribution Scheduler

DeploymentRedistributionScheduler es un listener que corre periódicamente:

cada N segundos:
    pending_distributions = deploymentState.getPendingDistributions()
    for distribution in pending_distributions:
        for partition in distribution.unacknowledged_partitions:
            re-send DEPLOYMENT.DISTRIBUTE to partition

Garantiza eventual consistency incluso si una partition estaba caída durante el primer attempt.

Lo que se distribuye

Para cada partition, se envía: - DeploymentRecord completo (BPMN XML, DMN, forms) - Metadata: keys generadas, versions, checksums - TODO el deployment es atomic — partial deployment no es permitido

2. Snapshot Transfer

Problema

Cuando un nuevo node se une al cluster, o un node viejo necesita catch-up después de quedar atrás, necesita el state completo. Replay desde el inicio del log puede tomar horas. Solución: transferir un snapshot reciente + replay solo del log restante.

Chunk-based design

Un snapshot completo puede ser gigabytes. Transferir como un solo blob es problemático: - Memoria del receiver bloqueada - Failure recovery completo si red falla - No paralelizable

Por eso, chunk-based:

public interface SnapshotChunk {
  String getSnapshotId();         // Identificación del snapshot completo
  int getTotalCount();             // Total de chunks
  String getChunkName();           // Nombre del archivo (e.g., "MANIFEST-000005")
  long getChecksum();              // Checksum del chunk para integrity
  byte[] getContent();             // Contenido binario
  long getFileBlockPosition();     // Posición dentro del archivo
  long getTotalFileSize();         // Tamaño total del archivo
}

Cada chunk es autocontained: tiene todo lo necesario para verificarse y posicionarse en el destino.

Flujo

flowchart TD
    Start[SnapshotTransfer.getLatestSnapshot] --> Loop[getNextChunk]
    Loop --> Sender[Sender responde con SnapshotChunk]
    Sender --> Put[ReceivableSnapshotStore.put]
    Put --> Verify{Verify checksum}
    Verify -->|invalid| Abort[Abort transfer]
    Verify -->|valid| Check{totalCount<br/>chunks recibidos?}
    Check -->|No| Loop
    Check -->|Yes| Promote[Promote a PersistedSnapshot]
    Promote --> Resume[Resume processing<br/>desde snapshot.position]

"No retry built-in"

Del javadoc oficial:

"No retry is done on the futures, if you want support for retry, wrap service with retries."

Decisión consciente: la layer de transfer NO maneja retry. Pros: - Composición con retry policies custom - No double-retry si caller ya implementa retry - Combina mejor con circuit breakers

Pattern reusable: separar mechanism de policy.

Checksums por chunk

CRC32CChecksumProvider genera checksum por chunk. CRC32C es: - Standard hardware-accelerated en CPUs modernas (Intel/AMD) - Más rápido que MD5/SHA en hardware - Detecta bit flips, no protege contra ataque deliberado (suficiente para integrity, no autenticación)

Para integrity end-to-end: - Chunk individual checksum (CRC32C) - File-level checksum vía MutableChecksumsSFV / ImmutableChecksumsSFV (formato SFV)

Reservation pattern

SnapshotReservation:

sequenceDiagram
    participant S as Sender
    participant St as SnapshotStore
    participant LC as Log Compactor

    S->>St: reserve(X) — marca X como "in use"
    Note over S,St: Transfer chunks de X...
    LC->>St: Quiere borrar snapshots viejos
    St-->>LC: X está reserved → NO borrar
    S->>St: reservation.close() — libera marker
    LC->>St: Puede borrar X si es viejo

Por qué importa: sin reservation, el snapshot podría ser GC'd mid-transfer (especialmente snapshots viejos que ya están atrás del nuevo current). El sender obtendría errors leyendo files que ya no existen.

Si el sender crashea, la reservation expira (timeout) — no leak permanente.

Restorable vs Receivable

Interface Caso de uso
ReceivableSnapshotStore Transfer normal chunk-by-chunk
RestorableSnapshotStore Restore manual desde backup externo (S3/GCS)

Separación permite: - Transfer: streaming, performance-optimized - Restore: bulk load, operational tool

Implicaciones para el MVP

Para single-node MVP

Ambos protocolos NO son necesarios:

Camunda needs MVP solution
Deployment distribution Single partition: skip
Snapshot transfer PostgreSQL replication: skip

Para multi-node MVP

Si eventualmente se necesita escalar horizontal:

Deployment: - Mismo pattern: DISTRIBUTE → ACK → COMPLETE - Redistribution scheduler para eventual consistency - @ExcludeAuthorizationCheck pattern para commands internos

Snapshots: - PostgreSQL streaming replication maneja state replication - pg_basebackup para snapshots iniciales - WAL streaming para catch-up

Para state externo (e.g., embedded SQLite): - Chunk-based desde el inicio - CRC32C por chunk - Reservation pattern para prevent GC - No retry en transfer layer (composición)

Patterns aplicables incluso single-node

  1. Eventual consistency con scheduler: para cualquier proceso que requiera ACKs
  2. @ExcludeAuthorizationCheck marker: distinguir internal vs external commands
  3. Chunk + checksum: para transferencia de archivos grandes (uploads, downloads)
  4. Reservation pattern: para reads que duran más que el snapshot lifetime
  5. No retry built-in: separar mechanism de policy

Trade-off principal

Camunda invirtió en estos protocolos porque opera multi-partition clusters mission-critical. Para un MVP single-node, PostgreSQL hace todo esto gratis. Si se necesita scale horizontal, replicar primero con un load tester antes de invertir en estos protocolos.

Recomendación: NO empezar con multi-partition. Es la complejidad incremental más cara del MVP.