Skip to content

Phase 6: Distributed Primitives & Apache Curator Recipes

Audience: Principal Distributed Systems Architect & Application Platform Engineer
Source References: curator-recipes/src/main/java/org/apache/curator/framework/recipes/locks/


1. The Distributed Lock: Naive Stampede vs. Herd-Free Fair Lock

Implementing a distributed lock over a raw consensus engine is fraught with severe network stampede traps if designed incorrectly.

1.1 The Naive Lock Anti-Pattern (The Herd Effect Catastrophe)

  • 1,000 workers want a lock.
  • All 1,000 workers attempt: create("/exclusive_lock", EPHEMERAL).
  • Worker 1 succeeds; remaining 999 workers fail and place a Watch on /exclusive_lock.
  • When Worker 1 releases the lock:
  • ZooKeeper fires 999 concurrent watch notifications simultaneously!
  • All 999 workers wake up and bombard ZooKeeper with 999 create() requests at the exact same millisecond.
  • The server CPU and network NIC saturate, dropping connections.

1.2 The Herd-Free Fair Lock Algorithm (Curator InterProcessMutex)

Apache Curator solves this by ordering requests into a sequential queue using Ephemeral Sequential Znodes:

Herd-Free Lock Queue under /locks/mutex/:
┌─────────────────────────────────┐
│ node-0000000001 (Worker A)      │ <=== OWNS THE LOCK!
└─────────────────────────────────┘
                 ▲ (Watched by)
┌─────────────────────────────────┐
│ node-0000000002 (Worker B)      │
└─────────────────────────────────┘
                 ▲ (Watched by)
┌─────────────────────────────────┐
│ node-0000000003 (Worker C)      │
└─────────────────────────────────┘
Curator InterProcessMutex Algorithm:
1. Client creates ephemeral sequential node:
   path = zk.create("/locks/mutex/lock-", EPHEMERAL_SEQUENTIAL)
   (e.g., /locks/mutex/lock-0000000002)

2. Client calls: children = zk.getChildren("/locks/mutex", watch=false)
3. Sorts children lexicographically by sequence number.

4. Evaluates Ownership:
   - Is my path the LOWEST sequence number in the list?
     - YES: Lock is granted! Proceed to critical section.
     - NO: Find the node IMMEDIATELY PRECEDING my sequence number:
       (e.g., node-0000000002 watches node-0000000001).

5. Places a Watch on ONLY the preceding node:
   zk.exists(preceding_path, watch=true)

6. When Worker A finishes and deletes node-0000000001:
   - ZooKeeper sends a Watch notification to EXACTLY ONE CLIENT (Worker B)!
   - Worker C remains completely asleep!
   - Network traffic is strictly O(1)! Zero Herd Effect!

2. Distributed Leader Election Recipes

Curator provides two battle-tested recipes for electing a leader across multiple worker microservices:

┌────────────────────────────────────────────────────────────────────────┐
│ 1. LeaderLatch                                                         │
├────────────────────────────────────────────────────────────────────────┤
│ - Blocks client thread until leadership is acquired: latch.await()    │
│ - Ideal for applications that boot up as passive standbys              │
│ - If leader crashes, the next latch in line unblocks automatically     │
└────────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────┐
│ 2. LeaderSelector                                                      │
├────────────────────────────────────────────────────────────────────────┤
│ - Event-driven listener model: takeLeadership(CuratorFramework client) │
│ - Relinquishes leadership when method exits, allowing round-robin      │
│   leadership handoffs among cluster workers                            │
└────────────────────────────────────────────────────────────────────────┘

2.1 Production Java Implementation

CuratorFramework client = CuratorFrameworkFactory.newClient(
    "127.0.0.1:2181", 
    new ExponentialBackoffRetry(1000, 3)
);
client.start();

LeaderSelector selector = new LeaderSelector(client, "/election/master", 
    new LeaderSelectorListenerAdapter() {
        @Override
        public void takeLeadership(CuratorFramework client) throws Exception {
            System.out.println("I am now the authoritative master coordinator!");
            // Run master duties until interrupted or process shuts down...
            while (!Thread.currentThread().isInterrupted()) {
                Thread.sleep(1000);
            }
        }
    }
);
selector.autoRequeue();
selector.start();

3. Distributed Double Barriers (DistributedDoubleBarrier)

A Double Barrier coordinates distributed workers so that: 1. No worker begins execution until all \(N\) workers have arrived at the start. 2. No worker exits until all \(N\) workers have completed execution.

Double Barrier Lifecycle:
Phase 1: Enter Barrier
- Workers create ephemeral node under /barrier/nodes/worker-X.
- Watch child count: When count == N, all workers enter computing phase together.

Phase 2: Leave Barrier
- Upon finishing computation, workers delete their ephemeral node.
- When count == 0, all workers terminate cleanly.