← Back to Blog

Dual-Queue Buffering Strategy in Distributed State Machines

In high-throughput distributed systems, state synchronization often becomes the primary bottleneck. A recurring challenge is: how to prevent local, high-frequency writes from being blocked by global synchronization operations?

In a recent system refactor, I implemented a Dual-Queue Buffering strategy for the TDistributedStateMaintainer module. The core idea is to physically separate "active mutations" from "stable reads" (snapshots), decoupling the write path from the sync path.

Core Design: Write-Read Separation

Traditional locking schemes (Mutex) introduce significant tail latency under heavy contention. The dual-queue strategy solves this by introducing two distinct layers of buffering:

  1. Delta Queue (Write Queue): A pure in-memory operation that only appends local state changes (Deltas).
  2. Snapshot Store (Read Store): A read-only copy of the global state, updated asynchronously in the background.

Writers simply append changes to the Delta Queue with near-zero latency. A background Worker periodically "drains" the Delta Queue, merges the changes, and atomically replaces the Snapshot Store.

Code Demonstration (Go)

Below is a simplified Go implementation demonstrating this non-blocking write flow.

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

// State represents the global state snapshot
type State map[string]int

// Mutation represents a single change
type Mutation struct {
	Key   string
	Value int
}

// DualQueueMaintainer manages the separation of concerns
type DualQueueMaintainer struct {
	// deltaQueue accepts high-frequency writes
	deltaQueue chan Mutation
	
	// snapshot holds the read-only global state
	// atomic.Value allows lock-free reads
	snapshot atomic.Value
	
	// signals the background worker to stop
	stopCh chan struct{}
}

func NewMaintainer() *DualQueueMaintainer {
	dq := &DualQueueMaintainer{
		deltaQueue: make(chan Mutation, 1000), // Buffered for bursts
		stopCh:     make(chan struct{}),
	}
	dq.snapshot.Store(make(State)) // Initial empty state
	
	// Start the background merger
	go dq.backgroundMerger()
	
	return dq
}

// Apply queues a mutation without blocking on global locks
func (d *DualQueueMaintainer) Apply(key string, val int) {
	d.deltaQueue <- Mutation{Key: key, Value: val}
}

// GetState returns a consistent snapshot instantly
func (d *DualQueueMaintainer) GetState() State {
	return d.snapshot.Load().(State)
}

func (d *DualQueueMaintainer) backgroundMerger() {
	// Local buffer for batching updates before merging
	pendingBatch := make(map[string]int)
	ticker := time.NewTicker(100 * time.Millisecond)
	
	for {
		select {
		case mutation := <-d.deltaQueue:
			// Accumulate locally first (Micro-batching)
			pendingBatch[mutation.Key] = mutation.Value
			
		case <-ticker.C:
			if len(pendingBatch) == 0 {
				continue
			}
			
			// 1. Load current immutable snapshot
			oldState := d.snapshot.Load().(State)
			
			// 2. Create new state (Copy-On-Write)
			newState := make(State)
			for k, v := range oldState {
				newState[k] = v
			}
			
			// 3. Apply pending batch
			for k, v := range pendingBatch {
				newState[k] = v
			}
			
			// 4. Atomically swap
			d.snapshot.Store(newState)
			fmt.Printf("Synched %d mutations to global state\n", len(pendingBatch))
			
			// 5. Reset batch
			pendingBatch = make(map[string]int)
			
		case <-d.stopCh:
			return
		}
	}
}

func main() {
	dm := NewMaintainer()
	
	// Simulate high-frequency writes
	go func() {
		for i := 0; i < 50; i++ {
			dm.Apply(fmt.Sprintf("sensor_%d", i%5), i)
			time.Sleep(10 * time.Millisecond)
		}
	}()
	
	// Simulate reads
	for i := 0; i < 5; i++ {
		state := dm.GetState()
		fmt.Printf("Read Tick %d: State size: %d\n", i, len(state))
		time.Sleep(150 * time.Millisecond)
	}
}

Performance Trade-offs

This design fundamentally embraces Eventual Consistency.

  • Pros: The Apply operation is extremely fast and unaffected by background synchronization logic (like network IO or disk IO). Reads via GetState are lock-free, achieved through atomic.Value pointer swapping.
  • Cons: Readers always see a state that lags behind writers by a few milliseconds (depending on the Ticker interval).

In scenarios requiring ultra-low latency writes (such as monitoring data ingestion or real-time messaging queues), this "dual-queue" isolation strategy is significantly more efficient than direct global locking.