OpenSearch's randomized election timers: the random delay of an attempt, the firing times of competing candidates, uncontested rounds, and the per-round bound
DefinitionOpenSearchElectionTimerA model of one round of competing randomized election timers of OpenSearch's ElectionSchedulerFactory at revision b2a54ba5c0defba3d1d20423e911d4bf920bab19, over the scheduler functions transcribed by the OpenSearchPreVote mission (nonNegative, toPositiveLongAtMost), with Mathlib. In Java, a cluster-manager-eligible candidate's scheduler fires attempt k after toPositiveLongAtMost(random.nextLong(), maxDelay) + gracePeriod milliseconds (ElectionSchedulerFactory.java:203-204), where maxDelay = min(max_timeout, initial_timeout + k · back_off_time), toPositiveLongAtMost(r, M) = nonNegative(r) mod M + 1, and the grace period is 0 for the first attempt and cluster.election.duration for the later ones; when an attempt fires it schedules the next one and, if the node is still a candidate that may win the election, is healthy and is commissioned, starts a new pre-voting round, closing the previous one (Coordinator.java:1578-1608).
toLong x — for x : Fin (2^64), the integer x − 2^63: the 2^64 possible values of random.nextLong() are the elements of Fin (2^64), read as Java longs. randomDelay M x — toPositiveLongAtMost (toLong x) M, the random part of the delay of an attempt with bound M for the draw x. For M = 0, which Java rules out (assert 0 < upperBound), the Lean value is nonNegative (toLong x) + 1. For M = 0, which Java rules out (assert 0 < upperBound), the Lean value is nonNegative (toLong x) + 1. fireTime offset bound x i — for n candidates i : Fin n, offsets offset : Fin n → ℤ, bounds bound : Fin n → ℕ and draws x : Fin n → Fin (2^64): offset i + randomDelay (bound i) (x i), the time at which candidate i fires; offset i stands for the time its timer was started plus its grace period, and bound i for its attempt's maxDelay, but only informally: no Lean term links offset or bound to maxDelay, SchedulerSettings or a grace period, and the theorems quantify over them as free parameters. Uncontested offset bound δ x — the round is uncontested with margin δ : ℕ: some candidate i fires at least δ before every other, fireTime offset bound x i + δ ≤ fireTime offset bound x j for every j ≠ i (a decidable predicate). contestedBound n δ Mmin — the rational number C(n, 2) · (2δ − 1) · ⌈2^63 / Mmin⌉ / 2^63, where 2δ − 1 is computed in the natural numbers and ⌈2^63 / Mmin⌉ is written as the natural-number quotient (2^63 + Mmin − 1) / Mmin, which is the ceiling when Mmin ≥ 1. The value is 0 when n ≤ 1, δ = 0 or Mmin = 0, and it is not capped at 1.
Probabilities are counts: a draw of the n candidates of a round is a function Fin n → Fin (2^64), and the probability of a set of draws is its number of elements divided by (2^64)^n, which is the uniform distribution with independent candidates; over K rounds a draw is a function Fin K → Fin n → Fin (2^64) and the denominator is (2^64)^(n K).
Formalization Note. The draws are idealized. Each node's scheduler draws from the node's own java.util.Random, created as new Random(Randomness.get().nextLong()) (DiscoveryModule.java:213) and also used by the Coordinator to pick a node to abdicate to (Coordinator.java:1818); the model treats every draw as uniform over the 2^64 longs and independent of every other draw, across nodes and attempts. java.util.Random is a 48-bit linear congruential generator, so nextLong() takes at most 2^48 of the 2^64 values, and the successive draws of one node are successive outputs of one generator; neither this nor any correlation between the seeds of different nodes is modelled. Grouping competing timers into rounds is an abstraction of the scheduler's timeline. Within a round the offsets and bounds are arbitrary integers and naturals (the theorems require every bound to lie in [Mmin, 2^63]); across rounds they may be any deterministic function of the earlier rounds' draws, never of the current round's. Every round has the same n candidates, each drawing afresh (a candidate that sits a round out can only be represented by a very large offset); the rounds need not be ordered in time, and nothing ties a round to the scheduler's attempt number. What happens between rounds is not modelled, nor whether an uncontested round leads to a successful election. Imports Mathlib and the OpenSearchPreVote definitions.
import Mathlib
import Definitions.Def_OpenSearchPreVote
/-!
# OpenSearch cluster coordination: randomized election timers
A cluster-manager-eligible node that has no leader schedules a pre-voting round after a random delay
(`ElectionSchedulerFactory`, OpenSearch `b2a54ba5c0defba3d1d20423e911d4bf920bab19`,
`server/src/main/java/org/opensearch/cluster/coordination/ElectionSchedulerFactory.java`, cited `ESF:<line>`):
attempt `k` fires `toPositiveLongAtMost(random.nextLong(), maxDelay k) + gracePeriod` milliseconds after it was
scheduled (ESF:201-204; the timer is scheduled at ESF:240), with `maxDelay` and `toPositiveLongAtMost` transcribed in
`Def_OpenSearchPreVote`. When two candidates run at nearly the same time they can split the votes and both fail. The
randomization aims to make one candidate's timer fire far enough ahead of every other's that it runs alone, its
pre-voting round and election completing before anyone else starts. That step is informal and not modelled here:
whether a firing starts a pre-voting round at all (only if the node is still a candidate that may win the election, is
healthy and is commissioned, `Coordinator.java:1579-1608`), and whether the round and the election complete within that
head start, depend on timing and state this file does not have. The progress mission (`OpenSearchProgress`) proves that
an election succeeds in a stable period, in its sense; that an uncontested round yields such a period is not proved.
This file models one round of competing timers. Candidate `i` fires at `offset i + randomDelay (bound i) (x i)`, where
`x i` is its draw of `random.nextLong()`; `bound i` stands for its attempt's `maxDelay` and `offset i` for when its
timer was started plus the grace period, a reading that no definition here formalizes: offsets and bounds are free
parameters. The probability theorems hold whatever the offsets and bounds, even chosen by an adversary that schedules
the network (as a function of the earlier rounds' draws), provided `n ≥ 1`, `δ ≥ 1`, `Mmin ≥ 1` and every bound lies in
`[Mmin, 2^63]` (`close_pair_prob` constrains only the second timer's bound). With `Mmin = cluster.election.max_timeout`
they apply once every competing attempt has reached that timeout, which with the default settings is from attempt 99
on, counting from 0. Probabilities are counts of draws divided by `2^(64 n)`.
Idealization: each node's scheduler draws from the node's `java.util.Random` (the Coordinator's, which also uses it to
pick a node to abdicate to, `Coordinator.java:1818`), seeded from `Randomness.get().nextLong()`
(`DiscoveryModule.java:213`); the draws are modelled as uniform over the `2^64` Java `long`s and independent across
nodes and attempts. `java.util.Random` is a 48-bit linear congruential generator, so `nextLong()` takes at most `2^48`
distinct values; the model does not capture that, nor any correlation between seeds. Grouping competing timers into
rounds is itself an abstraction of the scheduler's timeline.
-/
namespace OpenSearchElectionTimer
open OpenSearchPreVote
/-- A value of `random.nextLong()`: an element of `Fin (2^64)`, read as the Java `long` `x - 2^63`. -/
def toLong (x : Fin (2 ^ 64)) : Int := (x.val : Int) - 2 ^ 63
/-- The random part of the delay of an attempt with bound `M` for the draw `x`: `toPositiveLongAtMost(x, M)`
(ESF:204). -/
def randomDelay (M : Nat) (x : Fin (2 ^ 64)) : Nat := toPositiveLongAtMost (toLong x) M
/-- The time at which candidate `i` fires, given every candidate's offset, bound and draw. -/
def fireTime {n : Nat} (offset : Fin n → Int) (bound : Fin n → Nat) (x : Fin n → Fin (2 ^ 64)) (i : Fin n) : Int :=
offset i + randomDelay (bound i) (x i)
/-- A round is *uncontested with margin `δ`*: some candidate fires at least `δ` before every other candidate. -/
def Uncontested {n : Nat} (offset : Fin n → Int) (bound : Fin n → Nat) (δ : Nat) (x : Fin n → Fin (2 ^ 64)) : Prop :=
∃ i, ∀ j, j ≠ i → fireTime offset bound x i + δ ≤ fireTime offset bound x j
instance {n : Nat} (offset : Fin n → Int) (bound : Fin n → Nat) (δ : Nat) :
DecidablePred (Uncontested offset bound δ) := fun x => by
unfold Uncontested; infer_instance
/-- The per-round bound on the probability of a contested round: `C(n,2) · (2δ - 1) · ⌈2^63 / Mmin⌉ / 2^63` for `δ ≥ 1`
and `Mmin ≥ 1`. It is computed with natural-number subtraction and division, so it is 0 when `δ = 0` or `Mmin = 0`, and
it is not capped at 1. -/
def contestedBound (n δ Mmin : Nat) : ℚ :=
(n.choose 2 : ℚ) * (2 * δ - 1 : ℕ) * ((2 ^ 63 + Mmin - 1) / Mmin : ℕ) / 2 ^ 63
end OpenSearchElectionTimer
Read-back
What the Lean code literally says, in plain math · claude-opus-5-5 (independent sub-agent, blind: Lean code + auditor instructions only)
Read-back: Def_OpenSearchElectionTimer.lean
Sources read: mathlib/Definitions/Def_OpenSearchElectionTimer.lean and, for the imported definitions, Definitions/Def_OpenSearchPreVote.lean (the top-level copy, as directed; see the last section). The file imports all of Mathlib and Definitions.Def_OpenSearchPreVote. It opens the namespace OpenSearchElectionTimer (with open OpenSearchPreVote) and introduces, in order, five definitions and one instance: toLong, randomDelay, fireTime, Uncontested, an anonymous DecidablePred instance for Uncontested, and contestedBound. There are no structures, axioms, hypotheses or sorrys.
Notation used below: (Lean Nat), (Int), (ℚ), . is truncated natural-number subtraction ( when ). is natural-number floor division, with . is the natural-number remainder, with . These are Lean's conventions.
Imported notions, expanded
Only two imported definitions occur in Lean terms of this file: toPositiveLongAtMost, used by randomDelay, and through it nonNegative.
nonNegative (imported). ,
Lean's -2 ^ 63 means , i.e. Java's Long.MIN_VALUE. The function is defined on every integer. On the Java long range its values lie in . Each value is taken exactly twice: by and by , and each by and . (I derived this; the file does not state it.)
toPositiveLongAtMost (imported). ,
For the value lies in . For there is no guard, and the value is , because . The Java method instead asserts 0 < upperBound, and % 0 on a long throws. is an unbounded natural number, while Java's upperBound is a long, at most .
Scheduler notions (named in docstrings only). The imported file also defines the following:
SchedulerSettings: four natural-number fieldsinitialTimeout,backoffTime,maxTimeout,duration.SchedulerSettings.default.SchedulerSettings.Valid: , , , , and .- .
- .
No Lean term in this file, or in any of the seven theorem stubs, uses any of these. This file's docstrings mention maxDelay by name. So the relation between the parameters bound/offset below and the real scheduler is stated only in prose.
Declaration 1: toLong
, (the value of , cast to an integer, minus ).
- It is an order-preserving bijection from onto the Java
longrange :- ;
- ;
- .
- It is an offset encoding, not the two's-complement reading of a 64-bit pattern. That reading would map for and otherwise.
- Both are bijections, so any count taken over all is the same under either reading.
The docstring ("an element of Fin (2^64), read as the Java long x - 2^63") matches the code.
Declaration 2: randomDelay
,
- For the value is in .
- For the value is , a case Java excludes.
- has no upper bound. For the reduction does nothing and the values lie in .
A derived fact, not stated in the file: as ranges over , takes each value of exactly twice. Hence, for , exactly draws give delay ; this is the theorem randomDelay_card.
The docstring ("toPositiveLongAtMost(x, M) (ESF:204)") matches, with read through toLong.
Declaration 3: fireTime
Arguments:
- (implicit);
- ;
- ;
- ;
- .
Definition:
with the natural number cast to an integer. Below, denotes this value.
- depends only on , and .
- The offsets are unconstrained (any integers, including negative ones), and so are the bounds (any naturals, including ).
- The code fixes no unit; the docstrings speak of milliseconds.
The docstring ("the time at which candidate i fires, given every candidate's offset, bound and draw") matches.
Declaration 4: Uncontested
For , and as above, , and :
computed in with cast to an integer. Cases:
- : false, because there is no .
- : true for every , because the inner quantifier is vacuous.
- , : true for every , because any minimizer of is a witness.
- , : true exactly when one candidate has the strictly smallest fire time and the second-smallest fire time is at least . The witness is then unique: two witnesses would give .
- Equivalently, for and , "contested" () holds iff and the two smallest fire times, counted with multiplicity, differ by less than . Ties count as contested.
The docstring ("some candidate fires at least δ before every other candidate") matches. Nothing in the definition relates to message latency, the length of a pre-voting round, or the length of an election.
Instance: DecidablePred (Uncontested offset bound δ)
Decidability is obtained by unfolding the definition and calling infer_instance: the quantifiers range over the finite type , and on is decidable. The instance only lets Finset.filter be applied to Uncontested in the theorem statements. It adds no meaning: membership in a filtered finset does not depend on which decidability instance is used.
Declaration 5: contestedBound
,
The three factors are computed in and cast to , and the division by is in . 2 ^ 63 + Mmin - 1 associates as (2^63 + Mmin) - 1. Cases:
- , : the value is , the docstring's formula. Numerically it is , with a relative excess below .
- : , since .
- : , by truncated subtraction. The docstring's would be .
- : , since Lean's division by zero gives . The docstring's is undefined.
- Not capped at : for example, exactly when . For such parameters, a theorem that uses it as an upper bound on a probability is trivially true.
The docstring calls this "the per-round bound on the probability of a contested round". The definition itself is only this number; its role as a bound is the content of contested_prob_le.
Citation check against the Java source
The local checkout /home/user/opensearch-project/OpenSearch is at commit b2a54ba5c0defba3d1d20423e911d4bf920bab19, the cited commit.
-
ESF:195-204 (
ElectionScheduler.scheduleNextElection):- line 201:
thisAttempt = attempt.getAndIncrement(), so attempts are numbered from 0; - line 203:
maxDelayMillis = Math.min(maxTimeout.millis(), initialTimeout.millis() + thisAttempt * backoffTime.millis()); - line 204:
delayMillis = toPositiveLongAtMost(random.nextLong(), maxDelayMillis) + gracePeriod.millis().
This matches the module docstring's formula and the
randomDelaydocstring (ESF:204). The scheduling call itself,threadPool.scheduleUnlessShuttingDown(TimeValue.timeValueMillis(delayMillis), ...), is at ESF:240, outside the cited range. - line 201:
-
ESF:159-161 (
nonNegative):n == Long.MIN_VALUE ? 0 : Math.abs(n). This matches the imported definition on thelongrange. -
ESF:169-172 (
toPositiveLongAtMost):assert 0 < upperBound, thennonNegative(randomNumber) % upperBound + 1. This matches forupperBound. Java's%with a non-negative left operand and a positive right operand is the natural-number remainder. The Lean version has no counterpart of the assertion (see above). -
DiscoveryModule.java:213:
new Random(Randomness.get().nextLong()), passed to the Coordinator as itsrandom. Coordinator.java:263 passes it on asnew ElectionSchedulerFactory(settings, random, ...).- The same
Randomobject is also used at Coordinator.java:1818 (random.nextInt(...), which picks an abdication target). So the docstring's "each node's scheduler draws from its ownjava.util.Random" is slightly imprecise: the generator is the Coordinator's. - This does not matter under the stated idealization of independent uniform draws.
- The same
-
What a firing leads to. When the timer runs (
doRun, ESF:213-221), it does nothing if the scheduler has been closed. Otherwise it first schedules the next attempt, with grace periodduration(ESF:218), and then runs the Coordinator's runnable (CO:1577-1610). That runnable starts a pre-voting round only if the node is in CANDIDATE mode, may win the election, is healthy, and is commissioned. The file models only fire times. The first attempt's grace period isTimeValue.ZERO(CO:1574); CO:1575 is the only call ofstartElectionScheduleron the factory. -
"Uniform" in the Java comment. The comment at ESF:71-83 says the delay is "chosen uniformly from (0, min(...)]". The transcribed modular reduction is uniform only under the 64-bit idealization, and even then only up to the rounding quantified by
randomDelay_cardandrandomDelay_near_uniform: it is exactly uniform iff divides .
Docstring claims not carried by the Lean terms
-
"
bound iis its attempt'smaxDelay", and "offset i(when its timer was started, plus the grace period)". No Lean term linksboundoroffsettomaxDelay,SchedulerSettings, or a grace period. They are free parameters. -
"The theorems hold whatever the offsets and bounds … provided every bound is at least some
Mmin." The theorem stubs also require:- every bound ;
- , and .
close_pair_probconstrains only one of its two bounds. -
"(They hold, for example, once every attempt has reached
cluster.election.max_timeout.)" This is true as a choice of . Note, though, that withSchedulerSettings.default, reaches only for , i.e. from the 100th attempt on. -
"When one candidate's timer fires sufficiently earlier than every other's, it runs alone: its pre-voting round and election complete before anyone else starts (the progress missions prove that such a stable election succeeds)." Nothing in this file formalizes this.
Uncontestedconcerns fire times only, and is a free parameter. -
"Probabilities are counts of draws divided by
2^(64 n)." This matches the stubs' denominators: , and . -
The idealization paragraph is consistent with the code. Draws are elements of , and independence appears as counting over product types. The paragraph itself acknowledges that the real generator produces only distinct
nextLong()values. -
"This file models one round of competing timers." This is true of the definitions. The multi-round structure exists only in the stub
all_contested_prob_le, whosehcausalhypothesis matches "as a function of the earlier rounds' draws".
Note on the imported file
As instructed, I read /home/user/prove2me/Definitions/Def_OpenSearchPreVote.lean. This file and the theorem stubs live under mathlib/. A second copy, mathlib/Definitions/Def_OpenSearchPreVote.lean, exists there and is presumably what the import resolves to. I did not read that copy's content. A diff printing only line numbers shows that the two copies differ only in lines 141–145 (141–144 in the mathlib/ copy). In the copy I read, those lines are the docstring of handleJoins, not any definition used here. Def_OpenSearchPreVote itself imports Def_OpenSearchCoordination, which I did not read. The two definitions used here, nonNegative and toPositiveLongAtMost, do not depend on it.
Confirmed by the mission captain (proposal self-audit).
Confirmed by the moderator at approval.