mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[2026] rollout support
This commit is contained in:
@@ -75,6 +75,14 @@ export type Env = SharedHonoEnv & {
|
||||
* accounts use the live pool. See `tachyonPoolFor` in match.app.ts.
|
||||
*/
|
||||
TACHYON_HOST_PORT_SANDBOX?: string
|
||||
/**
|
||||
* The Tachyon pool a caller on a build under staged rollout (`ROLLOUT_GAME_VERSIONS` in
|
||||
* @repo/domain, read off their token's `rn.ver`) is served from
|
||||
* (`RECFLARE_TACHYON_HOST_PORT_ROLLOUT` in `.env`) — same list format, ids `rollout-1`,
|
||||
* `rollout-2`, …. A sandbox account is served the sandbox pool even on that build. Unset
|
||||
* means rollout callers use the live pool. See `tachyonPoolFor` in match.app.ts.
|
||||
*/
|
||||
TACHYON_HOST_PORT_ROLLOUT?: string
|
||||
/**
|
||||
* The Photon region every session is pinned to — both the region named in the connection
|
||||
* info and the one stamped on every room instance, which must agree. Optional; unlike the
|
||||
|
||||
+46
-15
@@ -38,6 +38,7 @@ import {
|
||||
isClubMember,
|
||||
isMatchmakingPolicy,
|
||||
isPlayerBannedFromRoom,
|
||||
isRolloutGameVersion,
|
||||
MatchmakingErrorCode,
|
||||
MessageType,
|
||||
MOST_ACTIVE_CLUBHOUSE_LIMIT,
|
||||
@@ -241,23 +242,46 @@ function tachyonServerFor(pool: TachyonServer[], roomInstanceId: number): Tachyo
|
||||
/**
|
||||
* The prefix of a sandbox server's generated id (see {@link tachyonPoolFor}) — `dev-1`,
|
||||
* `dev-2` rather than the live pool's `tachyon-N`, so the (cosmetic) id the client
|
||||
* displays says which pool it is on.
|
||||
* displays says which pool it is on. The rollout pool's is `rollout-N` for the same reason.
|
||||
*/
|
||||
const SANDBOX_TACHYON_SERVER_ID_PREFIX = 'dev-'
|
||||
const ROLLOUT_TACHYON_SERVER_ID_PREFIX = 'rollout-'
|
||||
|
||||
/** What decides which Tachyon pool a caller is served from; see {@link tachyonPoolFor}. */
|
||||
interface TachyonCaller {
|
||||
/** The account's `isSandbox` flag. */
|
||||
isSandbox: boolean
|
||||
/** Running a build under staged rollout (`ROLLOUT_GAME_VERSIONS`, off the token's `rn.ver`). */
|
||||
isRollout: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The pool the caller's instance is picked out of. A SANDBOX account runs against its own
|
||||
* Tachyon servers, apart from the live pool, so the whole list is swapped for
|
||||
* `TACHYON_HOST_PORT_SANDBOX` rather than any one entry: selection within it is the same
|
||||
* instance-id round-robin {@link tachyonServerFor} applies to the live pool. When that var
|
||||
* is unset a sandbox account falls back to the live pool, so an operator who runs no
|
||||
* sandbox servers doesn't leave those accounts without voice.
|
||||
* The pool the caller's instance is picked out of — one of three, each the same
|
||||
* comma-separated list, with selection inside it the same instance-id round-robin
|
||||
* {@link tachyonServerFor} applies to all of them:
|
||||
*
|
||||
* - `TACHYON_HOST_PORT_SANDBOX` for a SANDBOX account, which runs against its own servers
|
||||
* apart from the live pool;
|
||||
* - `TACHYON_HOST_PORT_ROLLOUT` for a caller on a build under staged rollout — the build
|
||||
* `auth` only lets Discord supporters sign in on, which gets its own voice servers while
|
||||
* it rolls out. Sessions are already split by build ({@link callerGameVersion}), so a
|
||||
* rollout instance's players are all on that build and all land here together;
|
||||
* - `TACHYON_HOST_PORT`, the live pool, for everyone else.
|
||||
*
|
||||
* Sandbox wins when both apply: a sandbox account on the rollout build is still a sandbox
|
||||
* account. Either special pool left unset falls back to the live one, so an operator who
|
||||
* runs no sandbox or rollout servers doesn't leave those callers without voice.
|
||||
*/
|
||||
function tachyonPoolFor(env: Env, isSandbox: boolean): TachyonServer[] {
|
||||
function tachyonPoolFor(env: Env, caller: TachyonCaller): TachyonServer[] {
|
||||
const sandbox = varOr(env.TACHYON_HOST_PORT_SANDBOX, '')
|
||||
return isSandbox && sandbox.trim() !== ''
|
||||
? tachyonPool(sandbox, SANDBOX_TACHYON_SERVER_ID_PREFIX)
|
||||
: tachyonPool(varOr(env.TACHYON_HOST_PORT, ''))
|
||||
if (caller.isSandbox && sandbox.trim() !== '') {
|
||||
return tachyonPool(sandbox, SANDBOX_TACHYON_SERVER_ID_PREFIX)
|
||||
}
|
||||
const rollout = varOr(env.TACHYON_HOST_PORT_ROLLOUT, '')
|
||||
if (!caller.isSandbox && caller.isRollout && rollout.trim() !== '') {
|
||||
return tachyonPool(rollout, ROLLOUT_TACHYON_SERVER_ID_PREFIX)
|
||||
}
|
||||
return tachyonPool(varOr(env.TACHYON_HOST_PORT, ''))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3088,8 +3112,11 @@ const app = new Hono<App>()
|
||||
'`TACHYON_HOST_PORT` pool, chosen by instance id so every player in a session is',
|
||||
'handed the same one, with a generated `voiceServerId` (`tachyon-1`, `tachyon-2`,',
|
||||
'…). Both are empty when the pool is unset or the caller is in no instance. A',
|
||||
'sandbox account is picked out of the `TACHYON_HOST_PORT_SANDBOX` pool instead, the same way,',
|
||||
'with ids `dev-1`, `dev-2`, ….',
|
||||
'sandbox account is picked out of the `TACHYON_HOST_PORT_SANDBOX` pool instead, the',
|
||||
'same way, with ids `dev-1`, `dev-2`, …; a caller whose token’s `rn.ver` is a build',
|
||||
'under staged rollout (`ROLLOUT_GAME_VERSIONS`, currently `20260323.14`) out of',
|
||||
'`TACHYON_HOST_PORT_ROLLOUT`, with ids `rollout-1`, `rollout-2`, …. Sandbox wins when',
|
||||
'both apply, and either special pool left unset falls back to the live one.',
|
||||
'`experiments` carries the client’s networking flags.',
|
||||
].join(' '),
|
||||
security: AUTHED,
|
||||
@@ -3130,9 +3157,13 @@ const app = new Hono<App>()
|
||||
}
|
||||
const account = await getAccount(c.env.DB, id)
|
||||
// The instance's server, the same one every other player in it is handed — except
|
||||
// that a SANDBOX account is picked out of the sandbox pool instead of the live one.
|
||||
// that a SANDBOX account is picked out of the sandbox pool, and a caller on a
|
||||
// rollout build out of the rollout pool, instead of the live one.
|
||||
const tachyon = tachyonServerFor(
|
||||
tachyonPoolFor(c.env, account?.isSandbox === true),
|
||||
tachyonPoolFor(c.env, {
|
||||
isSandbox: account?.isSandbox === true,
|
||||
isRollout: isRolloutGameVersion(await callerVersion(c)),
|
||||
}),
|
||||
roomInstanceId
|
||||
)
|
||||
|
||||
|
||||
@@ -1615,7 +1615,7 @@ describe('auth-gated endpoints', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('a sandbox account is served out of the sandbox Tachyon pool', async () => {
|
||||
test('a sandbox account is served out of the sandbox Tachyon pool, a rollout build out of the rollout one', async () => {
|
||||
// Sandbox accounts run against their own Tachyon servers, so the whole pool is swapped
|
||||
// for them; selection inside it is the same instance-id round-robin as the live pool.
|
||||
await env.DB.prepare('INSERT OR IGNORE INTO account (data) VALUES (?1)')
|
||||
@@ -1623,6 +1623,7 @@ describe('auth-gated endpoints', () => {
|
||||
.run()
|
||||
const original = env.TACHYON_HOST_PORT
|
||||
const originalSandbox = env.TACHYON_HOST_PORT_SANDBOX
|
||||
const originalRollout = env.TACHYON_HOST_PORT_ROLLOUT
|
||||
try {
|
||||
env.TACHYON_HOST_PORT = '198.51.100.12:7777'
|
||||
env.TACHYON_HOST_PORT_SANDBOX = '203.0.113.30:7777, 203.0.113.30:7778'
|
||||
@@ -1667,6 +1668,67 @@ describe('auth-gated endpoints', () => {
|
||||
// In no instance there is no server, sandbox or not.
|
||||
expect(await voiceFor('974')).toEqual({ voiceConnectionInfo: '', voiceServerId: '' })
|
||||
|
||||
// A caller on a build under staged rollout gets the ROLLOUT pool, a third list with
|
||||
// its own ids, read off the token's rn.ver. A sandbox account on that build still
|
||||
// gets the sandbox pool: sandbox wins.
|
||||
env.TACHYON_HOST_PORT_ROLLOUT = '198.51.100.13:7777,198.51.100.13:7778'
|
||||
const rolloutPool = [
|
||||
{ voiceConnectionInfo: '198.51.100.13:7777', voiceServerId: 'rollout-1' },
|
||||
{ voiceConnectionInfo: '198.51.100.13:7778', voiceServerId: 'rollout-2' },
|
||||
]
|
||||
const rolloutVoiceFor = async (player: string, version: string, roomInstanceId: number) => {
|
||||
const res = await exports.default.fetch(
|
||||
`${ORIGIN}/player/connection-info?roomInstanceId=${roomInstanceId}`,
|
||||
{ headers: await bearer(player, version) }
|
||||
)
|
||||
const body = (await res.json()) as {
|
||||
value: { voiceConnectionInfo: string; voiceServerId: string }
|
||||
}
|
||||
return {
|
||||
voiceConnectionInfo: body.value.voiceConnectionInfo,
|
||||
voiceServerId: body.value.voiceServerId,
|
||||
}
|
||||
}
|
||||
for (const photonRoomId of ['tachyon-rollout-a', 'tachyon-rollout-b']) {
|
||||
const rollout = await createRoomInstance(env.DB, {
|
||||
ownerAccountId: 976,
|
||||
roomId: 2,
|
||||
photonRoomId,
|
||||
maxCapacity: 12,
|
||||
})
|
||||
expect(await rolloutVoiceFor('976', '20260323.14', rollout.roomInstanceId)).toEqual(
|
||||
rolloutPool[rollout.roomInstanceId % rolloutPool.length]
|
||||
)
|
||||
expect(await rolloutVoiceFor('974', '20260323.14', rollout.roomInstanceId)).toEqual(
|
||||
sandboxPool[rollout.roomInstanceId % sandboxPool.length]
|
||||
)
|
||||
// Released builds — the default, the beta, and a token carrying no build — stay live.
|
||||
for (const version of [GAME_VERSION, '20250718.01']) {
|
||||
expect(await rolloutVoiceFor('976', version, rollout.roomInstanceId)).toEqual({
|
||||
voiceConnectionInfo: '198.51.100.12:7777',
|
||||
voiceServerId: 'tachyon-1',
|
||||
})
|
||||
}
|
||||
expect(await voiceFor('976', rollout.roomInstanceId)).toEqual({
|
||||
voiceConnectionInfo: '198.51.100.12:7777',
|
||||
voiceServerId: 'tachyon-1',
|
||||
})
|
||||
}
|
||||
|
||||
// With no rollout pool configured, a rollout build falls back to the live one, not
|
||||
// the sandbox one.
|
||||
env.TACHYON_HOST_PORT_ROLLOUT = undefined
|
||||
const unsetRollout = await createRoomInstance(env.DB, {
|
||||
ownerAccountId: 976,
|
||||
roomId: 2,
|
||||
photonRoomId: 'tachyon-rollout-unset',
|
||||
maxCapacity: 12,
|
||||
})
|
||||
expect(await rolloutVoiceFor('976', '20260323.14', unsetRollout.roomInstanceId)).toEqual({
|
||||
voiceConnectionInfo: '198.51.100.12:7777',
|
||||
voiceServerId: 'tachyon-1',
|
||||
})
|
||||
|
||||
// With no sandbox pool configured, a sandbox account falls back to the live one.
|
||||
env.TACHYON_HOST_PORT_SANDBOX = undefined
|
||||
const instance = await createRoomInstance(env.DB, {
|
||||
@@ -1682,6 +1744,7 @@ describe('auth-gated endpoints', () => {
|
||||
} finally {
|
||||
env.TACHYON_HOST_PORT = original
|
||||
env.TACHYON_HOST_PORT_SANDBOX = originalSandbox
|
||||
env.TACHYON_HOST_PORT_ROLLOUT = originalRollout
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user