104 lines
3.0 KiB
JavaScript
104 lines
3.0 KiB
JavaScript
|
|
export class PrepareQuorum {
|
|
|
|
constructor({ ackChannel, timeoutMs, matchesChan, debug = false }) {
|
|
this.ackChannel = ackChannel
|
|
this.timeoutMs = timeoutMs
|
|
this.matchesChan = matchesChan
|
|
this.debug = debug
|
|
this.expected = new Set()
|
|
this.ready = new Map()
|
|
this.simulationId = null
|
|
this.active = false
|
|
this.resolve = null
|
|
this.timer = null
|
|
}
|
|
|
|
begin(expectedParticipantIds, simulationId) {
|
|
this.cancel()
|
|
this.expected = new Set(expectedParticipantIds)
|
|
this.ready.clear()
|
|
this.simulationId = simulationId
|
|
this.active = true
|
|
|
|
if(this.debug) {
|
|
console.log(
|
|
`[Maestro] Prepare quorum armed: ${this.expected.size} participant(s) ` +
|
|
`(agents + primordial daemons), timeout ${this.timeoutMs}ms`
|
|
)
|
|
}
|
|
|
|
return(new Promise(resolve => {
|
|
this.resolve = resolve
|
|
this.timer = setTimeout(() => {
|
|
const missing = [...this.expected].filter(id => !this.ready.has(id))
|
|
this.#finish({
|
|
ok: false,
|
|
err: `Timeout waiting for readyToStart (${this.timeoutMs}ms); ` +
|
|
`missing: ${missing.join(', ') || 'unknown'}`,
|
|
})
|
|
}, this.timeoutMs)
|
|
}))
|
|
}
|
|
|
|
handleMessage(msg, chan) {
|
|
if(!this.active) return(false)
|
|
if(typeof(this.matchesChan) !== 'function') return(false)
|
|
if(!this.matchesChan(chan, this.ackChannel)) return(false)
|
|
if(msg?.eventType !== 'readyToStart') return(false)
|
|
|
|
const payload = msg.payload ?? {}
|
|
if(payload.simulationId !== this.simulationId) return(false)
|
|
|
|
const sender = msg.sender
|
|
if(!sender || !this.expected.has(sender)) {
|
|
if(this.debug) {
|
|
console.warn(`[Maestro] Ignoring readyToStart from unexpected participant: ${sender}`)
|
|
}
|
|
return(true)
|
|
}
|
|
|
|
if(!payload.success) {
|
|
this.#finish({
|
|
ok: false,
|
|
err: payload.err ?? `Participant ${sender} failed prepare`,
|
|
})
|
|
return(true)
|
|
}
|
|
|
|
this.ready.set(sender, payload)
|
|
if(this.debug) {
|
|
console.log(
|
|
`[Maestro] readyToStart from ${sender} ` +
|
|
`(${this.ready.size}/${this.expected.size})`
|
|
)
|
|
}
|
|
|
|
for(const participantId of this.expected) {
|
|
if(!this.ready.has(participantId)) return(true)
|
|
}
|
|
|
|
this.#finish({ ok: true })
|
|
return(true)
|
|
}
|
|
|
|
cancel() {
|
|
if(this.timer) {
|
|
clearTimeout(this.timer)
|
|
this.timer = null
|
|
}
|
|
this.active = false
|
|
this.resolve = null
|
|
this.expected.clear()
|
|
this.ready.clear()
|
|
this.simulationId = null
|
|
}
|
|
|
|
#finish(result) {
|
|
const resolve = this.resolve
|
|
this.cancel()
|
|
if(typeof(resolve) === 'function') resolve(result)
|
|
}
|
|
|
|
}
|