SQL-backed leader election (gemstone_utils.election)¶
Optional leader election for multi-worker applications that already use gemstone_utils.db. Candidates register in the database, heartbeats keep them eligible, and elect acquires or renews a lease on a per-namespace leader row. Leader-only work should be gated with is_leader.
Import the module to register models on the shared GemstoneDB base (see gemstone_utils.db). Tables are created by init_db after import gemstone_utils.election.
Concepts¶
Concept |
Description |
|---|---|
Namespace ( |
Isolated election domain; defaults to |
Candidate |
Process identity as a |
Leader lease |
Row in |
Expiry window |
|
sequenceDiagram
participant App
participant DB as gemstone_election_*
App->>DB: register_candidate / heartbeat
App->>DB: elect(candidate_id)
DB-->>App: current leader UUID
loop while leader
App->>DB: elect renew lease
App->>App: is_leader check
end
Schema¶
gemstone_election_candidate (ElectionCandidate)¶
Field |
Role |
|---|---|
|
Namespace PK (part 1). |
|
UUID string PK (part 2). |
|
UTC timestamp of last register/heartbeat/elect touch. |
|
Candidate considered inactive when |
gemstone_election_leader (ElectionLeader)¶
Field |
Role |
|---|---|
|
Namespace PK. |
|
UUID string of current leader, or |
|
Leader lease end; |
|
UTC timestamp of last leader-row change. |
Typical application loop¶
Startup
set_expire(sec)once if the default 60s window is wrong for your heartbeat interval.init_db(db_url)after importinggemstone_utils.election(and any otherGemstoneDBplugins).Choose a stable
candidate_id(UUID) per process instance.register_candidate(candidate_id, ns=...).
Background
On a timer shorter than
set_expire:heartbeat(candidate_id)andelect(candidate_id).Before leader-only work:
if is_leader(candidate_id): ....
Shutdown
unregister_candidate(candidate_id)removes the candidate and clears the leader row if this instance was leader (faster failover).
All public functions accept an optional session= so you can share a transaction with other GemstoneDB work. When omitted, each call opens and closes a session via get_session().
Protocol rules¶
electreturns the current leader UUID after the call — which may be the caller or another candidate.Leadership is assigned when there is no leader, the lease is expired, or the caller is the current leader (lease renewal).
If another candidate holds an unexpired lease,
electdoes not preempt; the return value is that leader’s id.heartbeatis an alias forregister_candidate(creates the row if missing).list_candidatesreturns UUIDs withexpires_at > nowonly.unregister_candidatedeletes the candidate row; if that candidate was leader, clearsleader_idandlease_expires_aton the leader row.
Implementation notes¶
Row locking:
electusesSELECT ... FOR UPDATEon the leader row when the driver supports it; SQLite and some drivers fall back to a plaingetwithout row lock.Contention: Creating the leader row under concurrent
electcalls may raiseIntegrityErroronce; the implementation retries the transaction once, then reads the current leader.Not consensus: This is a single-database lease pattern, not Raft or Paxos. Correctness depends on all participants using the same database and respecting
is_leader/ lease expiry.Limitations: Clock skew between app hosts and the database can shorten or lengthen perceived leases. If the database is unavailable, election stops. Processes that ignore
is_leaderand run leader work anyway can cause split-brain regardless of lease state.
Public API¶
See api.md. Related: key-storage.md (shared init_db and GemstoneDB pattern). Introduced in v0.2.0 as emerald_utils.election; see release-notes.md.