Second pass on 657c0c6. That commit deleted the backends but left the
scaffolding that only existed to serve them:
- internal/testingutil: three empty `var ()` blocks whose flags went with
the backends — "Google cloud storage settings", "Azure blob storage
settings", "Alibaba Cloud OSS settings" declaring nothing.
- replica_url_test.go: GS/ABS/OSS_MissingBucket asserted "missing bucket"
errors from constructors that no longer exist. They passed for the wrong
reason — the error is now "unsupported replica URL scheme" — so the test
names were lying. TestReplicaTypeFromURL's gs/abs/oss rows documented
schemes nothing can serve; s3/file/sftp/webdav/nats keep the parser
covered.
- tests/integration/helpers.go: the abs:// arm of the config-path check
can never be taken.
- docs: llms.txt and .claude/agents/replica-client-developer.md pointed at
gs/replica_client.go and abs/replica_client.go, files that are gone;
VFS.md, PROVIDER_COMPATIBILITY.md, ARCHITECTURE.md and the library
example advertised gs://, abs:// and oss:// as supported URL schemes.
webdav was live the whole time and missing from every list — added.
- `-integration gcs|abs|oss` recipes in PATTERNS.md, the skills guide and
two .claude commands invoked flags with no backend behind them.
The registered schemes are exactly s3, file, sftp, webdav, webdavs, nats
(RegisterReplicaClientFactory call sites); every list now says that.
go build ./... and go vet ./... green; URL tests pass. go.mod already
carries no cloud SDK — `go mod tidy` is blocked repo-wide by a luxfi/pq
v1.0.3 moved-tag checksum, unrelated to this change, so go.mod is
untouched.
Co-authored-by: Hanzo Dev <[email protected]>
22 KiB
Replicate Architecture - Technical Deep Dive
Table of Contents
- System Layers
- Core Components
- IPC Server & Control Socket
- Distributed Leasing
- Library Convenience Methods
- LTX File Format
- WAL Monitoring Mechanism
- Compaction Process
- Transaction Management
- Concurrency Model
- State Management
- Initialization Flow
- Error Handling
System Layers
Replicate follows a layered architecture with clear separation of concerns:
graph TB
subgraph "Application Layer"
CLI[CLI Commands<br/>cmd/replicate/]
Config[Configuration<br/>config.go]
end
subgraph "Core Layer"
Store[Store Manager<br/>store.go]
DB[Database Manager<br/>db.go]
Replica[Replica Manager<br/>replica.go]
end
subgraph "Storage Abstraction"
RC[ReplicaClient Interface<br/>replica_client.go]
end
subgraph "Storage Implementations"
S3[s3/replica_client.go]
File[file/replica_client.go]
SFTP[sftp/replica_client.go]
WebDAV[webdav/replica_client.go]
NATS[nats/replica_client.go]
end
subgraph "External"
SQLite[SQLite Database]
Cloud[Cloud Storage]
end
CLI --> Store
Store --> DB
DB --> Replica
Replica --> RC
RC --> S3
RC --> File
RC --> SFTP
RC --> WebDAV
RC --> NATS
DB <--> SQLite
S3 --> Cloud
WebDAV --> Cloud
Layer Responsibilities
1. Application Layer
- CLI Commands: User interface for operations (replicate, restore, etc.)
- Configuration: YAML/environment variable parsing and validation
2. Core Layer
- Store: Multi-database coordination, compaction scheduling
- DB: Single database management, WAL monitoring, checkpointing
- Replica: Replication to single destination, position tracking
3. Storage Abstraction
- ReplicaClient Interface: Uniform API for all storage backends
4. Storage Implementations
- Backend-specific logic (authentication, retries, optimizations)
5. IPC Layer
- Server: Unix socket HTTP server for runtime control (
server.go) - Leaser: Distributed lease interface for coordination (
leaser.go,s3/leaser.go)
Core Components
DB Component (db.go)
The DB component is the heart of Replicate, managing a single SQLite database:
type DB struct {
// Core fields
path string // Database file path
metaPath string // Metadata directory path
db *sql.DB // SQLite connection
f *os.File // Long-running file descriptor
rtx *sql.Tx // Long-running read transaction
pageSize int // Database page size
// Synchronization
mu sync.RWMutex // Protects struct fields
chkMu sync.RWMutex // Checkpoint lock
notify chan struct{} // WAL change notifications
// Lifecycle
ctx context.Context
cancel func()
wg sync.WaitGroup
// Configuration
MinCheckpointPageN int // Min pages for passive checkpoint
TruncatePageN int // Pages before emergency truncate checkpoint
CheckpointInterval time.Duration // Time-based passive checkpoint interval
MonitorInterval time.Duration // WAL monitoring frequency
// Note: MaxCheckpointPageN removed (RESTART mode disabled due to #724)
// Metrics
dbSizeGauge prometheus.Gauge
walSizeGauge prometheus.Gauge
txIDGauge prometheus.Gauge
}
Key Methods
// Lifecycle
func (db *DB) Open() error
func (db *DB) Close(ctx context.Context) error
// Monitoring
func (db *DB) monitor() // Background WAL monitoring
func (db *DB) checkWAL() (bool, error) // Check for WAL changes
// Checkpointing
func (db *DB) Checkpoint(mode string) error
func (db *DB) autoCheckpoint() error
// Replication
func (db *DB) WALReader(pgno uint32) (io.ReadCloser, error)
func (db *DB) Sync(ctx context.Context) error
// Compaction
func (db *DB) Compact(ctx context.Context, destLevel int) (*ltx.FileInfo, error)
Replica Component (replica.go)
Manages replication to a single destination:
type Replica struct {
db *DB // Parent database
Client ReplicaClient // Storage backend client
mu sync.RWMutex
pos ltx.Pos // Current replication position
// Configuration
SyncInterval time.Duration
MonitorEnabled bool
// Lifecycle
cancel func()
wg sync.WaitGroup
}
Replication Position
type Pos struct {
TXID TXID // Transaction ID
PageNo uint32 // Page number within transaction
Checksum uint64 // Running checksum
}
Store Component (store.go)
Coordinates multiple databases and manages system-wide resources:
type Store struct {
mu sync.Mutex
dbs []*DB
levels CompactionLevels
// Configuration
SnapshotInterval time.Duration
SnapshotRetention time.Duration
L0Retention time.Duration
L0RetentionCheckInterval time.Duration
CompactionMonitorEnabled bool
// Lifecycle
ctx context.Context
cancel func()
wg sync.WaitGroup
}
IPC Server & Control Socket
Replicate exposes a Unix socket HTTP server for runtime control (server.go).
Socket Configuration
type SocketConfig struct {
Enabled bool `yaml:"enabled"` // Default: false
Path string `yaml:"path"` // Default: "/var/run/replicate.sock"
Permissions uint32 `yaml:"permissions"` // Default: 0600
}
HTTP Endpoints
All endpoints are served over the Unix socket via Go's net/http mux:
| Method | Path | Description |
|---|---|---|
POST |
/register |
Add a database at runtime |
POST |
/unregister |
Remove a database at runtime |
GET |
/txid?path= |
Get current transaction ID for a database |
GET |
/list |
List all managed databases |
GET |
/info |
Server info (version, PID, uptime, database count) |
POST |
/start |
Start replication for a database |
POST |
/stop |
Stop replication for a database |
GET |
/debug/pprof/* |
Standard Go pprof endpoints |
Request/Response Types
type RegisterDatabaseRequest struct {
Path string `json:"path"`
ReplicaURL string `json:"replica_url"`
}
type UnregisterDatabaseRequest struct {
Path string `json:"path"`
Timeout int `json:"timeout,omitempty"` // Seconds
}
type TXIDResponse struct {
TXID uint64 `json:"txid"`
}
Distributed Leasing
The Leaser interface (leaser.go) enables distributed coordination so multiple Replicate instances can safely share a replica destination.
Interface
type Leaser interface {
Type() string
AcquireLease(ctx context.Context) (*Lease, error)
RenewLease(ctx context.Context, lease *Lease) (*Lease, error)
ReleaseLease(ctx context.Context, lease *Lease) error
}
type Lease struct {
Generation int64 `json:"generation"`
ExpiresAt time.Time `json:"expires_at"`
Owner string `json:"owner,omitempty"`
ETag string `json:"-"`
}
S3 Implementation (s3/leaser.go)
- Defaults:
DefaultLeaseTTL = 30s,DefaultLeasePath = "lock.json" - Owner format:
hostname:pid(falls back topid-Nif hostname unavailable) - Conditional writes: Uses
If-Match/If-None-Matchon S3 PutObject to prevent races - Release: Uses
DeleteObjectwithIf-Matchto ensure only the holder can release - Error types:
ErrLeaseNotHeld,ErrLeaseAlreadyReleased,LeaseExistsError{Owner, ExpiresAt}
Acquisition Flow
- Read existing lease from S3 (
lock.json) - If lease exists and is not expired, return
LeaseExistsError - Write new lease with
If-None-Match: *(first acquire) orIf-Match: <etag>(expired takeover) - If
PreconditionFailed(412), another instance acquired first - Return lease with ETag for subsequent renewal
Library Convenience Methods
These methods on DB (db.go) simplify common operations when Replicate is used as a library:
type SyncStatus struct {
LocalTXID ltx.TXID // Local transaction ID
RemoteTXID ltx.TXID // Remote transaction ID
InSync bool // true if LocalTXID > 0 and equal to RemoteTXID
}
func (db *DB) SyncStatus(ctx context.Context) (SyncStatus, error)
func (db *DB) SyncAndWait(ctx context.Context) error
func (db *DB) EnsureExists(ctx context.Context) error
- SyncStatus: Compares local TXID against remote replica position (performs I/O to query remote)
- SyncAndWait: Runs
db.Sync()(WAL to LTX) thendb.Replica.Sync()(LTX to remote), blocking until both complete - EnsureExists: Restores database from replica if local file doesn't exist; no-op if file exists or no backup available. Must be called before
Open()
LTX File Format
LTX (Log Transaction) files are immutable files containing database changes:
+------------------+
| Header | Fixed size header with metadata
+------------------+
| |
| Page Frames | Variable number of page frames
| |
+------------------+
| Page Index | Index for efficient page lookup
+------------------+
| Trailer | Metadata and checksums
+------------------+
Header Structure
type Header struct {
Magic [4]byte // "LTX\x00"
Version uint32 // Format version
PageSize uint32 // Database page size
MinTXID TXID // Starting transaction ID
MaxTXID TXID // Ending transaction ID
Timestamp int64 // Creation timestamp
Checksum uint64 // Header checksum
}
Page Frame Structure
type PageFrame struct {
Header PageHeader
Data []byte // Page data (pageSize bytes)
}
type PageHeader struct {
PageNo uint32 // Page number in database
Size uint32 // Size of page data
Checksum uint64 // Page checksum
}
Page Index
Binary search tree for efficient page lookup:
type PageIndexElem struct {
PageNo uint32 // Page number
Offset int64 // Offset in file
Size uint32 // Size of page frame
}
Trailer
type Trailer struct {
PageIndexOffset int64 // Offset to page index
PageIndexSize int64 // Size of page index
PageCount uint32 // Total pages in file
Checksum uint64 // Full file checksum
}
WAL Monitoring Mechanism
Monitor Loop (db.go:1499)
func (db *DB) monitor() {
ticker := time.NewTicker(db.MonitorInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// Check WAL for changes
changed, err := db.checkWAL()
if err != nil {
slog.Error("wal check failed", "error", err)
continue
}
if changed {
// Notify replicas of changes
db.notifyReplicas()
// Check if checkpoint needed
if db.shouldCheckpoint() {
db.autoCheckpoint()
}
}
case <-db.ctx.Done():
return
}
}
}
WAL Change Detection
func (db *DB) checkWAL() (bool, error) {
// Get current WAL state
walInfo, err := db.walInfo()
if err != nil {
return false, err
}
// Compare with previous state
db.mu.Lock()
changed := walInfo.Size != db.prevWALSize ||
walInfo.Checksum != db.prevWALChecksum
db.prevWALSize = walInfo.Size
db.prevWALChecksum = walInfo.Checksum
db.mu.Unlock()
return changed, nil
}
Compaction Process
Compaction merges multiple LTX files to reduce storage overhead:
Compaction Algorithm (store.go:189)
High-level compaction flow:
- Determine whether the level is due for compaction (
Store.shouldCompact). - Enumerate level-
L-1files usingReplicaClient.LTXFiles, preferring local copies viaos.Open(db.LTXPath(...))and falling back toReplicaClient.OpenLTXFileonly when necessary. - Stream the source readers through
ltx.NewCompactor, which performs page-level deduplication and enforces lock-page skipping automatically. - Pipe the compactor output into
ReplicaClient.WriteLTXFileto create the merged LTX file for levelL. - Adjust the returned
ltx.FileInfo.CreatedAtto the earliest timestamp from the source files so point-in-time recovery remains accurate. - Update the cached max file info for the level and delete old L0 files when promoting to level 1.
Compaction Levels
type CompactionLevel struct {
Level int // Level number (0 = raw, 1+ = compacted)
Interval time.Duration // How often to compact from previous level
}
// Default configuration
var DefaultCompactionLevels = CompactionLevels{
{Level: 0, Interval: 0}, // Raw LTX files
{Level: 1, Interval: 1 * Hour}, // Hourly compaction
{Level: 2, Interval: 24 * Hour}, // Daily compaction
}
Transaction Management
Long-Running Read Transaction
Replicate maintains a long-running read transaction to ensure consistency:
func (db *DB) initReadTx() error {
// Start read transaction
tx, err := db.db.BeginTx(context.Background(), &sql.TxOptions{
ReadOnly: true,
})
if err != nil {
return err
}
// Execute dummy query to start transaction
var dummy string
err = tx.QueryRow("SELECT ''").Scan(&dummy)
if err != nil {
tx.Rollback()
return err
}
db.rtx = tx
return nil
}
Purpose:
- Prevents database from being modified during replication
- Ensures consistent view of database
- Allows reading historical pages from WAL
Checkpoint Coordination
func (db *DB) Checkpoint(mode string) error {
// Acquire checkpoint lock
db.chkMu.Lock()
defer db.chkMu.Unlock()
// Close read transaction temporarily
if db.rtx != nil {
db.rtx.Rollback()
db.rtx = nil
}
// Perform checkpoint
_, _, err := db.db.Exec(fmt.Sprintf("PRAGMA wal_checkpoint(%s)", mode))
if err != nil {
return err
}
// Restart read transaction
return db.initReadTx()
}
Concurrency Model
Mutex Usage Patterns
// DB struct mutexes
type DB struct {
mu sync.RWMutex // Protects struct fields
chkMu sync.RWMutex // Checkpoint coordination
}
// Replica struct mutexes
type Replica struct {
mu sync.RWMutex // Protects position
muf sync.Mutex // File descriptor lock
}
// Store struct mutex
type Store struct {
mu sync.Mutex // Protects database list
}
Thundering Herd Prevention
Store.Open() (store.go) limits concurrent database opens at startup:
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(50) // Max 50 concurrent DB opens
for _, db := range s.dbs {
db := db
g.Go(func() error { return db.Open() })
}
This prevents OS resource exhaustion (file descriptors, memory) when hundreds of databases are configured.
Lock Ordering (Prevent Deadlocks)
Always acquire locks in this order:
- Store.mu
- DB.mu
- DB.chkMu
- Replica.mu
Goroutine Management
// Start background task
func (db *DB) Start() {
db.wg.Add(1)
go func() {
defer db.wg.Done()
db.monitor()
}()
}
// Stop with timeout
func (db *DB) Close(ctx context.Context) error {
// Signal shutdown
db.cancel()
// Wait for goroutines with timeout
done := make(chan struct{})
go func() {
db.wg.Wait()
close(done)
}()
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
State Management
Database States
stateDiagram-v2
[*] --> Closed
Closed --> Opening: Open()
Opening --> Open: Success
Opening --> Closed: Error
Open --> Monitoring: Start()
Monitoring --> Syncing: Changes Detected
Syncing --> Monitoring: Sync Complete
Monitoring --> Checkpointing: Threshold Reached
Checkpointing --> Monitoring: Checkpoint Complete
Monitoring --> Closing: Close()
Closing --> Closed: Cleanup Complete
Replica States
stateDiagram-v2
[*] --> Idle
Idle --> Starting: Start()
Starting --> Monitoring: Success
Starting --> Idle: Error
Monitoring --> Syncing: Timer/Changes
Syncing --> Uploading: Have Changes
Uploading --> Monitoring: Success
Uploading --> Error: Failed
Error --> Monitoring: Retry
Monitoring --> Stopping: Stop()
Stopping --> Idle: Cleanup
Position Tracking
type Pos struct {
TXID TXID // Current transaction ID
PageNo uint32 // Current page number
Checksum uint64 // Running checksum for validation
}
// Update position atomically
func (r *Replica) SetPos(pos ltx.Pos) {
r.mu.Lock() // MUST use Lock, not RLock!
defer r.mu.Unlock()
r.pos = pos
}
// Read position safely
func (r *Replica) Pos() ltx.Pos {
r.mu.RLock()
defer r.mu.RUnlock()
return r.pos
}
Initialization Flow
System Startup Sequence
sequenceDiagram
participant Main
participant Store
participant DB
participant Replica
participant Monitor
Main->>Store: NewStore(config)
Store->>Store: Validate config
Main->>Store: Open()
loop For each database
Store->>DB: NewDB(path)
Store->>DB: Open()
DB->>DB: Open SQLite connection
DB->>DB: Read page size
DB->>DB: Init metadata
DB->>DB: Start read transaction
loop For each replica
DB->>Replica: NewReplica()
DB->>Replica: Start()
Replica->>Monitor: Start monitoring
end
end
Store->>Store: Start compaction monitors
Store-->>Main: Ready
Critical Initialization Steps
-
Database Opening
// Must happen in order: 1. Open SQLite connection 2. Read page size (PRAGMA page_size) 3. Create metadata directory 4. Start long-running read transaction 5. Initialize replicas 6. Start monitor goroutine -
Replica Initialization
// Must happen in order: 1. Create replica with client 2. Load previous position from metadata 3. Validate position against database 4. Start sync goroutine (if monitoring enabled)
Error Handling
Error Categories
-
Recoverable Errors
- Network timeouts
- Temporary storage unavailability
- Lock contention
-
Fatal Errors
- Database corruption
- Invalid configuration
- Disk full
-
Operational Errors
- Checkpoint failures
- Compaction conflicts
- Sync delays
Error Propagation
// Bottom-up error propagation
ReplicaClient.WriteLTXFile() error
↓
Replica.Sync() error
↓
DB.Sync() error
↓
Store.monitorDB() // Logs error, continues
Retry Logic
func (r *Replica) syncWithRetry(ctx context.Context) error {
backoff := time.Second
maxBackoff := time.Minute
for attempt := 0; ; attempt++ {
err := r.Sync(ctx)
if err == nil {
return nil
}
// Check if error is retryable
if !isRetryable(err) {
return err
}
// Check context
if ctx.Err() != nil {
return ctx.Err()
}
// Exponential backoff
time.Sleep(backoff)
backoff *= 2
if backoff > maxBackoff {
backoff = maxBackoff
}
}
}
Performance Characteristics
Time Complexity
| Operation | Complexity | Notes |
|---|---|---|
| WAL Monitor | O(1) | Fixed interval check |
| Page Write | O(1) | Append to LTX file |
| Compaction | O(n) | n = total pages |
| Restoration | O(n*log(m)) | n = pages, m = files |
| File List | O(k) | k = files in level |
Space Complexity
| Component | Memory Usage | Disk Usage |
|---|---|---|
| DB | O(1) + metrics | Original DB + WAL |
| Replica | O(1) | LTX files + metadata |
| Compaction | O(n) pages | Temporary during merge |
| Page Index | O(p) | p = pages in file |
Optimization Points
-
Page Index Caching
- Cache frequently accessed indices
- Use estimated size for initial fetch
-
Batch Operations
- Group small changes into larger LTX files
- Batch delete operations
-
Concurrent Operations
- Multiple replicas can sync in parallel
- Compaction runs independently per level
Security Considerations
Access Control
- File permissions: 0600 for database files
- Directory permissions: 0700 for metadata
- No built-in authentication (rely on storage backend)
Monitoring & Metrics
Prometheus Metrics
// Database metrics
db_size_bytes // Current database size
wal_size_bytes // Current WAL size
total_wal_bytes // Total bytes written to WAL
checkpoint_count // Number of checkpoints
sync_count // Number of syncs
sync_error_count // Number of sync errors
// Replica metrics
replica_lag_seconds // Replication lag
replica_position // Current replication position
Health Checks
func (db *DB) HealthCheck() error {
// Check database connection
if err := db.db.Ping(); err != nil {
return fmt.Errorf("database ping failed: %w", err)
}
// Check replication lag
for _, r := range db.replicas {
lag := time.Since(r.LastSync())
if lag > MaxAcceptableLag {
return fmt.Errorf("replica %s lag too high: %v", r.Name(), lag)
}
}
return nil
}