fix(rust): skip servers known down for a link code, hold syncs while offline (F6, F7) #23
@@ -20,6 +20,7 @@ const core = require('../../core')
|
||||
const db = require('./links.db')
|
||||
const engagement = require('../../engagement/emit')
|
||||
const servers = require('../servers/servers.model')
|
||||
const serversDb = require('../servers/servers.db')
|
||||
const sidecar = require('../../sidecarClient')
|
||||
|
||||
const log = core.logger('links')
|
||||
@@ -213,7 +214,18 @@ async function redeem({ code, userId }) {
|
||||
const settled = asked.find((result) => result.ok || result.reason === 'taken')
|
||||
if (settled) return settled
|
||||
|
||||
const rest = await Promise.all(others.map((server) => confirmOne({ server, code, userId })))
|
||||
// **Not the ones this site already knows are down** (F6, option b of the step-2
|
||||
// walk). In parallel is not enough on its own: while any server is down, a code
|
||||
// no issuer holds — every made-up one — still waited out that server's whole
|
||||
// timeout, twelve seconds on both rigs. A server the poll has seen go down cannot
|
||||
// answer, and it is not an issuer, so it cannot hold the code: it counts as
|
||||
// `offline` without the wait. A server with no state yet is asked.
|
||||
const down = await knownDown()
|
||||
const reachable = others.filter((server) => !down.has(String(server.id)))
|
||||
const rest = [
|
||||
...(await Promise.all(reachable.map((server) => confirmOne({ server, code, userId })))),
|
||||
...others.filter((server) => down.has(String(server.id))).map(() => ({ ok: false, reason: 'offline' })),
|
||||
]
|
||||
const late = rest.find((result) => result.ok || result.reason === 'taken')
|
||||
if (late) return late
|
||||
|
||||
@@ -224,6 +236,20 @@ async function redeem({ code, userId }) {
|
||||
return { ok: false, reason: 'rejected' }
|
||||
}
|
||||
|
||||
/**
|
||||
* The servers the board poll last saw without a connected game, by id. A failed
|
||||
* read is an empty set — everybody is asked, which is slow and never wrong.
|
||||
*/
|
||||
async function knownDown() {
|
||||
try {
|
||||
const states = await serversDb.listState()
|
||||
return new Set(states.filter((state) => !Number(state.online)).map((state) => String(state.serverId)))
|
||||
} catch (err) {
|
||||
log.warn('could not read server state for the link fleet', { error: err.message })
|
||||
return new Set()
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove a link the caller owns. False when they did not hold it. */
|
||||
async function unlinkOwned(steamId, userId) {
|
||||
const removed = (await db.removeOwned(steamId, userId)) > 0
|
||||
|
||||
@@ -146,6 +146,13 @@ function reasonToSync({ desiredHash, sync, state, force }) {
|
||||
// the world is up, and the sync goes on the next tick. A human's "sync now" is
|
||||
// not held: they asked, and a failure then is theirs to read.
|
||||
if (state && state.worldReady === false) return null
|
||||
// Nor while the game is not connected (F7, the step-2 walk). The stored hello is
|
||||
// the LAST one: between a restart and the poll that reads the new boot's hello,
|
||||
// it still says the old world is ready. On Carbon a due audit went out in that
|
||||
// gap, 80 s before "Server startup complete". The poll marks the server offline
|
||||
// the moment it goes away, so offline holds until a fresh hello says otherwise.
|
||||
// Unknown (`online` absent) is not held — a state row always carries it.
|
||||
if (state && state.online !== undefined && state.online !== null && !Number(state.online)) return null
|
||||
if (!sync) return 'first'
|
||||
if (sync.state !== 'ok' && sync.lastAttemptAt && age(sync.lastAttemptAt) < FAIL_BACKOFF_MS && !sync.dirty) {
|
||||
return null
|
||||
|
||||
@@ -63,9 +63,13 @@ function withCore({ select = [], onInsert = null } = {}) {
|
||||
* `issuers` are the servers the site saw mint a link code in the last few
|
||||
* minutes (`account.link.requested`, F5) — none by default, which is also what a
|
||||
* code typed before its frame was ingested looks like.
|
||||
*
|
||||
* `down` are the servers the board poll last saw without a connected game
|
||||
* (`online: 0`); every other server has no state row, which is asked like an up one.
|
||||
*/
|
||||
function fleetOf(replies, issuers = []) {
|
||||
function fleetOf(replies, issuers = [], down = []) {
|
||||
const servers = require('../model/servers/servers.model')
|
||||
const serversDb = require('../model/servers/servers.db')
|
||||
const sidecar = require('../sidecarClient')
|
||||
const linksDb = require('../model/links/links.db')
|
||||
|
||||
@@ -73,6 +77,7 @@ function fleetOf(replies, issuers = []) {
|
||||
const ids = Object.keys(replies)
|
||||
|
||||
servers.listForPolling = async () => ids.map((id) => ({ id, baseUrl: `http://${id}`, token: 't' }))
|
||||
serversDb.listState = async () => down.map((serverId) => ({ serverId, online: 0 }))
|
||||
linksDb.recentLinkIssuers = async () => [...issuers]
|
||||
|
||||
sidecar.confirmLink = async (server, code) => {
|
||||
@@ -375,3 +380,52 @@ test('the fleet is asked in parallel, not one dead server after another (F6)', a
|
||||
assert.equal(result.ok, true)
|
||||
assert.equal(most, 5, 'every server was being asked at once')
|
||||
})
|
||||
|
||||
test('a server the poll saw go down is not waited on for a code nobody minted (F6, option b)', async () => {
|
||||
// The step-2 walk: with one rig stopped, a made-up code took twelve seconds on
|
||||
// both frameworks — in parallel, but still one whole timeout on the dead server.
|
||||
withCore()
|
||||
const links = require('../model/links/links.model')
|
||||
|
||||
const asked = fleetOf({ a: linkRefused, b: unreachable, c: linkRefused }, [], ['b'])
|
||||
|
||||
assert.equal((await links.redeem({ code: 'ZZZZZZ', userId: 4 })).reason, 'rejected')
|
||||
assert.deepEqual(asked.map((x) => x.server).sort(), ['a', 'c'], 'the server known down was not asked')
|
||||
})
|
||||
|
||||
test('an issuer is asked even when the poll saw it go down, so a good code stays "unsure"', async () => {
|
||||
// Skipping is for the servers that cannot hold the code. A server that minted
|
||||
// one in the window might — and a player whose code is on it must not be told
|
||||
// the code is wrong.
|
||||
withCore()
|
||||
const links = require('../model/links/links.model')
|
||||
|
||||
const asked = fleetOf({ a: unreachable, b: linkRefused }, ['a'], ['a'])
|
||||
|
||||
assert.equal((await links.redeem({ code: 'K7M2PQ', userId: 4 })).reason, 'unsure')
|
||||
assert.ok(asked.some((x) => x.server === 'a'), 'the down issuer was still asked')
|
||||
})
|
||||
|
||||
test('a fleet the poll saw all go down is offline, without asking any of it', async () => {
|
||||
withCore()
|
||||
const links = require('../model/links/links.model')
|
||||
|
||||
const asked = fleetOf({ a: unreachable, b: unreachable }, [], ['a', 'b'])
|
||||
|
||||
assert.equal((await links.redeem({ code: 'ZZZZZZ', userId: 4 })).reason, 'offline')
|
||||
assert.equal(asked.length, 0)
|
||||
})
|
||||
|
||||
test('a state read that fails asks everybody, which is slow and never wrong', async () => {
|
||||
withCore()
|
||||
const links = require('../model/links/links.model')
|
||||
const serversDb = require('../model/servers/servers.db')
|
||||
|
||||
const asked = fleetOf({ a: linkRefused, b: linkRefused })
|
||||
serversDb.listState = async () => {
|
||||
throw new Error('pool closed')
|
||||
}
|
||||
|
||||
assert.equal((await links.redeem({ code: 'ZZZZZZ', userId: 4 })).reason, 'rejected')
|
||||
assert.equal(asked.length, 2)
|
||||
})
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
// • F13/F14 — a zone that expired in the game is recorded `expired` by core,
|
||||
// through `ctx.events.expired` (D170, D183)
|
||||
// • F8 — a plugin that loads with permissions re-syncs within a tick (D184)
|
||||
// • F7 — nothing is pushed to a game whose world is still loading
|
||||
// • F7 — nothing is pushed to a game whose world is still loading, or
|
||||
// that the poll saw go away (the step-2 walk)
|
||||
// • D182 — the ZoneManager helper's state reaches the servers page
|
||||
|
||||
const test = require('node:test')
|
||||
@@ -97,6 +98,30 @@ test('no permission sync goes to a world that is still loading — unless a huma
|
||||
assert.equal(at({ worldReady: false }, true), 'requested')
|
||||
})
|
||||
|
||||
test('no permission sync goes to a game the poll saw go away, however ready its last hello said it was', () => {
|
||||
// The step-2 walk, on Carbon: the sidecar came back and a due audit went out in
|
||||
// the same moment, 80 s before "Server startup complete". The stored hello was the
|
||||
// OLD boot's (same boot id, hence "audit" and not "restart", worldReady true);
|
||||
// only `online: 0` — written by the poll when the server went away — said the
|
||||
// game was not there.
|
||||
withCore()
|
||||
const permSync = require('../permSync')
|
||||
const stale = new Date(Date.now() - permSync.AUDIT_MS - 1000)
|
||||
const sync = { state: 'ok', dirty: false, syncedHash: 'h1', bootId: 'boot-1', wipeId: 'w-1', lastAttemptAt: stale }
|
||||
const at = (state, force = false) => permSync.reasonToSync({ desiredHash: 'h1', sync, state, force })
|
||||
|
||||
assert.equal(at({ online: 0, bootId: 'boot-1', wipeId: 'w-1', worldReady: true }), null)
|
||||
assert.equal(at({ online: false, bootId: 'boot-1', wipeId: 'w-1', worldReady: true }), null)
|
||||
// Back, with the new boot's hello still loading: held by worldReady, as before.
|
||||
assert.equal(at({ online: 1, bootId: 'boot-2', wipeId: 'w-1', worldReady: false }), null)
|
||||
// Loaded: the restart sync goes.
|
||||
assert.equal(at({ online: 1, bootId: 'boot-2', wipeId: 'w-1', worldReady: true }), 'restart')
|
||||
// The audit itself still runs for a server that is up.
|
||||
assert.equal(at({ online: 1, bootId: 'boot-1', wipeId: 'w-1', worldReady: true }), 'audit')
|
||||
// A human's "sync now" is not held.
|
||||
assert.equal(at({ online: 0 }, true), 'requested')
|
||||
})
|
||||
|
||||
test('no title push goes to a world that is still loading', async () => {
|
||||
withCore()
|
||||
const titleSync = require('../titleSync')
|
||||
|
||||
Reference in New Issue
Block a user