Files
zeekayandGitHub 36ef0867fd file: file:// Leaser; dblock: byte-range lock; MaxLeaseTTL on interface (#3)
* file: add file:// Leaser for filesystem-backed CAS

Mirrors the S3 Leaser CAS semantics using POSIX O_EXCL for initial creation
and content-hash ETag matching for renew/release. Atomic rename for the
cutover. Same Leaser interface contract — callers swap backends without code
changes.

Use cases: per-process / single-node leases for dev + tests, container-local
coordination where S3 is overkill, and cross-process tests of leader/standby
patterns without spinning up MinIO.

Tests cover: fresh acquire, expired-lease takeover, contended acquire
(LeaseExistsError), renew bumps expiry + etag without touching generation,
renew loses race to a stealer (ErrLeaseNotHeld), release roundtrip, release
after steal (ErrLeaseNotHeld), and concurrent acquire with exactly one
winner across 32 goroutines.

* file: harden Leaser — MaxTTL clamp + foreign-owner renew guard

Closes red findings #1 (HIGH) and #4 (MEDIUM) on the per-market shard
layer's lease coordinator.

#1 — Server-side MaxTTL clamp:
  - Add Leaser.MaxTTL (default 30s when zero).
  - Acquire + Renew reject TTL > MaxTTL with ErrTTLExceedsMax. Without
    this, one malicious or buggy caller pins the lease for an arbitrary
    duration regardless of liveness.
  - readLease rejects an on-disk ExpiresAt > now + MaxTTL with
    ErrLeaseCorrupt — defense in depth against a writer that bypassed
    the API.

#4 — Owner check in RenewLease:
  - ETag CAS alone permits a caller who read another Leaser's ETag from
    disk to craft a renew call carrying any Owner string. We now read
    the on-disk record first and reject when existing.Owner != l.Owner
    with ErrLeaseNotHeld. ETag CAS remains primary; Owner check is
    belt-and-suspenders.

Tests:
  - TestLeaser_RejectsTTLBeyondMax (acquire, renew, zero MaxTTL falls
    back to default)
  - TestLeaser_ReadRejectsCorruptedTTL (hand-write 1h ExpiresAt,
    Acquire rejects)
  - TestLeaser_RejectsRenewByForeignOwner (foreign Owner with stolen
    ETag → ErrLeaseNotHeld)

All existing leaser tests continue to pass.

* leaser: surface MaxLeaseTTL() on the interface

Heartbeat schedulers depend on the leaser's TTL ceiling to validate
that their renew cadence is at least 2x tighter than the
server-side bound. Without surfacing it, callers either hardcode
the bound or compute it from the leaser type — both brittle.

The struct field stays MaxTTL (configuration knob); the interface
method is MaxLeaseTTL() to avoid the Go name-collision rule
("field and method with the same name"). file:// and s3:// both
default to DefaultLeaseMaxTTL (30s) when unset.

* dblock: expose SQLite-compatible byte-range lock as a public package

External callers that mirror Replica.applyLTXFile (e.g. shard-layer
exclusive-lock primitive so their apply pipeline interlocks with
SQLite's own locking and with the upstream applier.

dblock.LockExclusive / dblock.Unlock target the SQLite pending-byte
(0x40000000) and shared-region addresses, matching the existing
internal.LockFileExclusive contract used by Replica.applyLTXFile.
The internal/lock_*.go shims now forward to dblock so there is one
canonical implementation across the module.

Behavior identical to the previous internal/ implementation —
file-level tests in file/ pass.

* chore: bump go to 1.26.4

Picks up upstream security fixes for crypto/x509, mime, net/textproto,
crypto/fips140. No dependency bumps — only the toolchain directive.

go test -race ./... is green.

* chore: fix gofmt + goimports drift across repo

Pre-existing formatting drift on main exposed by go-imports-repo
pre-commit hook. Pure whitespace alignment + import ordering — no
logic changes. Touches 16 files unrelated to the file:// Leaser PR.
2026-06-07 07:55:05 -07:00

99 lines
3.2 KiB
Go

// Package client — canonical client interface for Hanzo Replicate.
//
// import repl "github.com/hanzoai/replicate/client"
// var r repl.Replicator = hanzoreplicate.New(cfg)
package client
import (
"context"
"time"
)
// Replicator is the cross-region data-sync surface: declare streams,
// inspect lag, pause + resume, replay from a checkpoint. The data plane
// (CDC tailing, transport, conflict resolution) runs out-of-band; this
// interface is the management API every operator + admin tool uses.
type Replicator interface {
// Kind reports the backend identifier
// (hanzo-replicate | debezium | kafka-mirror | warpstream | litestream).
Kind() string
// UpsertStream creates or replaces a replication stream.
UpsertStream(ctx context.Context, s Stream) error
// DeleteStream removes a stream and its replication state.
DeleteStream(ctx context.Context, name string) error
// ListStreams returns every stream in scope.
ListStreams(ctx context.Context) ([]Stream, error)
// GetStatus returns the runtime state of one stream.
GetStatus(ctx context.Context, name string) (*StreamStatus, error)
// Pause halts a stream without losing its checkpoint. Idempotent.
Pause(ctx context.Context, name string) error
// Resume restarts a paused stream from its last checkpoint.
Resume(ctx context.Context, name string) error
// Replay rewinds a stream to a prior checkpoint (or a wall-clock
// timestamp) and re-emits from there. The destination is responsible
// for idempotent application of the replayed events.
Replay(ctx context.Context, name string, from Checkpoint) error
}
// Stream is one replication unit. Source + destination + filter.
type Stream struct {
Name string
Source Endpoint
Destination Endpoint
// Mode: cdc | snapshot | snapshot_then_cdc.
Mode string
// Filter restricts which rows/objects replicate. SQL-ish predicate
// (postgres CDC) or JSONPath (document stores).
Filter string
// Conflict policy when source and destination diverge:
// last_writer_wins | source_wins | dest_wins | manual_review.
Conflict string
// CheckpointInterval is the cadence at which durable progress
// markers are written.
CheckpointInterval time.Duration
}
// Endpoint identifies one end of a replication.
type Endpoint struct {
// Kind: postgres | sqlite | base | s3 | nats | kafka.
Kind string
// Region is the deployment region (us-east | us-west | eu-fra).
Region string
// URI is the dial string (postgres://..., s3://bucket, nats://...).
URI string
// AuthRef points at a KMS secret holding the credentials.
AuthRef string
}
// StreamStatus is the runtime state of one stream.
type StreamStatus struct {
Name string
State string // running | paused | failed | catching_up | snapshot
LastEvent time.Time
LastCheckpoint Checkpoint
LagSeconds float64
LagEvents int64
BytesReplicated int64
EventsReplicated int64
ErrorsLastHour int
LastError string
}
// Checkpoint identifies a position in a stream.
type Checkpoint struct {
// LSN is the source-native ordering token (postgres WAL LSN,
// kafka offset, s3 manifest version, ...).
LSN string
// Wall is the wall-clock timestamp at the source when this
// checkpoint was written.
Wall time.Time
}