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¶
- Eventual consistency con scheduler: para cualquier proceso que requiera ACKs
- @ExcludeAuthorizationCheck marker: distinguir internal vs external commands
- Chunk + checksum: para transferencia de archivos grandes (uploads, downloads)
- Reservation pattern: para reads que duran más que el snapshot lifetime
- 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.