tickcli-js 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +3 -3
- package/dist/hub.d.ts +44 -4
- package/dist/hub.js +163 -8
- package/dist/hub.js.map +1 -1
- package/dist/remote.d.ts +10 -2
- package/dist/remote.js +5 -1
- package/dist/remote.js.map +1 -1
- package/package.json +1 -1
- package/src/hub.ts +111 -10
- package/src/remote.ts +15 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6
|
|
4
|
+
|
|
5
|
+
Blue/green hand-over of one namespace. `hub.exportScope(scope)` moves one namespace's live rooms to another process without touching the others: pending changes go out, open actions fail ("server restarting"), profile saves still loading land first, saves are written and in the store before it resolves, the rooms are exported (`exportRooms(scope)` now takes an optional namespace) and closed here (save leases given up), and its sockets hear 1012. Joins to it meanwhile are refused with 1012. The other process passes the snapshot to `importRooms` and the players carry on in the same match (`kept`). A router in front can re-join each page over its open socket; a page on its own reconnects as after a restart. `hub.scopeList()`: namespaces with live rooms and their rooms, seats and online players. `createRemoteServer({ onSnapshot })`: a restart of a remote room server keeps its matches.
|
|
6
|
+
|
|
7
|
+
## 0.1.5
|
|
8
|
+
|
|
9
|
+
Per-customer shares on a shared server. A deployment over its `kbPerSec` cap no longer only waits at the door: all its open rooms send less often (half, down to an eighth) until it is back under 80% of the cap, so one busy customer cannot take the whole uplink; other deployments keep their full rate and nobody is disconnected. New deployment cap `joinsPerMinute`: joins a minute across the deployment (reconnects count), on top of the per-address limit; over it the page hears 1013 busy and tries again. Hub: `scopeCaps` option (`{ outBytes, joinsPerMinute }` per namespace), `remote.scopeCaps(scope)`, `scopeStats(scope).slowed`, `stats().live.slowedScopes`, refusal reasons `scope_budget` and `scope_joins`.
|
|
10
|
+
|
|
3
11
|
## 0.1.4
|
|
4
12
|
|
|
5
13
|
Idle pages send less. A visible page with no key, pointer, touch or gamepad input for 1 min asks the hub for a quarter of the update frames (nothing is lost, changes are merged into each frame); the first input brings the full rate back. In a 4-player test room the idle reader's download fell from 4.4 to 1.2 KB/s. Hidden pages already got no frames.
|
package/README.md
CHANGED
|
@@ -248,17 +248,17 @@ import { createRemoteServer } from "tickcli-js/remote";
|
|
|
248
248
|
|
|
249
249
|
const deployments = {
|
|
250
250
|
// "http://localhost:*" lets the author test the same page from a laptop; drop it whenever you like
|
|
251
|
-
dep_ab12: { id: "dep_ab12", origins: ["https://mygame.example", "https://*.portal.example", "http://localhost:*"], caps: { rooms: 20, players: 200, kbPerSec: 2048 } },
|
|
251
|
+
dep_ab12: { id: "dep_ab12", origins: ["https://mygame.example", "https://*.portal.example", "http://localhost:*"], caps: { rooms: 20, players: 200, kbPerSec: 2048, joinsPerMinute: 600 } },
|
|
252
252
|
};
|
|
253
253
|
const app = createRemoteServer({ deployment: (id) => deployments[id], secret: process.env.TICKCLI_SECRET, saves: "./saves" });
|
|
254
254
|
await app.listen(8080); // behind https: the pages use wss://
|
|
255
255
|
```
|
|
256
256
|
|
|
257
|
-
Or in your own server: `createRemote({ hub, deployment })` gives `upgrade(req, socket, head)` and `request(req, res)` (pass `roomsPerScope: (s) => remote.roomsPerScope(s)` to the hub for room caps).
|
|
257
|
+
Or in your own server: `createRemote({ hub, deployment })` gives `upgrade(req, socket, head)` and `request(req, res)` (pass `roomsPerScope: (s) => remote.roomsPerScope(s)` and `scopeCaps: (s) => remote.scopeCaps(s)` to the hub for the room, data and joins caps).
|
|
258
258
|
|
|
259
259
|
- A deployment is a public id plus the sites allowed to use it: `https://site`, one wildcard level `https://*.example`, any port `:*`. The server checks the page's `Origin` when the socket opens: browsers send it and page scripts cannot change it, so another site cannot use your deployment. There is one kind of deployment: https sites, and http only for this machine (`localhost`, `127.0.0.1`, `[::1]`) when listed, e.g. `http://localhost:*` to test from a laptop. Pages with no site (`null`, `file://`) are refused.
|
|
260
260
|
- Guests: the page keeps a random 128-bit key in its own `localStorage` and sends it as the socket's first message (no cookie, so it works from any site). The same key is the same seat; other players see a short id made from it, never the key.
|
|
261
|
-
- Each deployment is its own namespace (rooms and saves never mix). `caps`: open rooms, connected sockets, KB a second going out (over it, new sockets are told the game is busy and retry
|
|
261
|
+
- Each deployment is its own namespace (rooms and saves never mix). `caps`: open rooms, connected sockets, KB a second going out (over it, new sockets are told the game is busy and retry, and its open rooms send less often, down to an eighth, until it is back under 80%; other deployments keep their full rate), joins a minute across the deployment (reconnects count: leave room for every player joining again after a restart). `revoked: true` refuses new sockets; `remote.revoke(id)` also closes its open rooms.
|
|
262
262
|
- A refused page stops and shows why ("This site is not allowed for deployment dep_ab12.").
|
|
263
263
|
- Not a guard: a script outside a browser can send any `Origin`. The caps, the per-address limits and revoking the id are what stop a misused deployment.
|
|
264
264
|
|
package/dist/hub.d.ts
CHANGED
|
@@ -367,6 +367,8 @@ export interface LiveStats {
|
|
|
367
367
|
/** over the data cap or a late loop now; rooms slowed by their own budget */
|
|
368
368
|
pressure: boolean;
|
|
369
369
|
slowedRooms: number;
|
|
370
|
+
/** namespaces slowed because they are over their own outBytes cap (ScopeCaps) */
|
|
371
|
+
slowedScopes: number;
|
|
370
372
|
/** the 5 rooms sending the most */
|
|
371
373
|
top: {
|
|
372
374
|
players: number;
|
|
@@ -421,6 +423,14 @@ export interface RoomsSnapshot {
|
|
|
421
423
|
at: number;
|
|
422
424
|
rooms: SnapRoom[];
|
|
423
425
|
}
|
|
426
|
+
/** One namespace's own share of the server (a host app's per-customer caps, tickcli-js/remote). */
|
|
427
|
+
export interface ScopeCaps {
|
|
428
|
+
/** data going out to its rooms, bytes a second (moving average over all its rooms): over it, all its rooms send half as often (up to 8 times
|
|
429
|
+
* less) until the faster rate would be under 80% of it again. Other namespaces keep their full rate. */
|
|
430
|
+
outBytes?: number;
|
|
431
|
+
/** joins a minute across the namespace (reconnects count), on top of the per-address limit; over it: 1013 busy, the page tries again */
|
|
432
|
+
joinsPerMinute?: number;
|
|
433
|
+
}
|
|
424
434
|
export interface HubStats {
|
|
425
435
|
rooms: number;
|
|
426
436
|
players: number;
|
|
@@ -441,6 +451,10 @@ export declare class RoomHub {
|
|
|
441
451
|
private perAddr;
|
|
442
452
|
private addrHits;
|
|
443
453
|
private joins;
|
|
454
|
+
/** per namespace with caps: its slow-down factor and moving average out (bytes a second) */
|
|
455
|
+
private scopeSlow;
|
|
456
|
+
/** per namespace with a joins cap: joins in this minute and the last one (a sliding estimate, no list per join) */
|
|
457
|
+
private scopeJoins;
|
|
444
458
|
private refused;
|
|
445
459
|
private timer;
|
|
446
460
|
private sub;
|
|
@@ -478,6 +492,8 @@ export declare class RoomHub {
|
|
|
478
492
|
pressure: boolean;
|
|
479
493
|
/** world save leases: "scope|slot|who" -> the room key that may write it */
|
|
480
494
|
private leases;
|
|
495
|
+
/** namespaces being handed to another process now (exportScope): joins to them are refused with 1012 */
|
|
496
|
+
private moving;
|
|
481
497
|
/** shutdown() hands the live rooms to this (a restart writes them to disk; importRooms() in the next process takes them back) */
|
|
482
498
|
private readonly onSnapshot;
|
|
483
499
|
/** the room list (rooms {}) is answered; off: the answer is {off: true} */
|
|
@@ -486,6 +502,8 @@ export declare class RoomHub {
|
|
|
486
502
|
private lists;
|
|
487
503
|
/** rooms allowed in one namespace when it differs from limits.maxRoomsPerScope (a host app's per-customer cap, tickcli-js/remote) */
|
|
488
504
|
private readonly roomsPerScope;
|
|
505
|
+
/** a namespace's own data and joins caps (tickcli-js/remote: a deployment's caps); undefined = only the server-wide limits */
|
|
506
|
+
private readonly scopeCaps;
|
|
489
507
|
constructor(opts: {
|
|
490
508
|
secret: string;
|
|
491
509
|
now?: () => number;
|
|
@@ -494,6 +512,7 @@ export declare class RoomHub {
|
|
|
494
512
|
onSnapshot?: (snap: RoomsSnapshot) => Promise<void> | void;
|
|
495
513
|
listRooms?: boolean;
|
|
496
514
|
roomsPerScope?: (scope: string) => number | undefined;
|
|
515
|
+
scopeCaps?: (scope: string) => ScopeCaps | undefined;
|
|
497
516
|
});
|
|
498
517
|
/** A short public id for a seat in one namespace; the seat key itself (a user id or guest pass id) is never shown to others. */
|
|
499
518
|
publicId(scope: string, seat: string): string;
|
|
@@ -596,9 +615,15 @@ export declare class RoomHub {
|
|
|
596
615
|
/** A room over its data budget sends half as often (up to 8 times less), its host is told once (error too_much_data); it speeds up again when
|
|
597
616
|
* the faster rate would be under 80% of the budget. */
|
|
598
617
|
private budget;
|
|
599
|
-
/** How many ticks apart a room sends its state: 1 (50 ms), 2 in rooms over 8 players, times its own budget factor, times
|
|
600
|
-
* for rooms over 4 players. */
|
|
618
|
+
/** How many ticks apart a room sends its state: 1 (50 ms), 2 in rooms over 8 players, times its own budget factor, times its namespace's factor
|
|
619
|
+
* (over its outBytes cap), times 2 under server pressure for rooms over 4 players. */
|
|
601
620
|
private every;
|
|
621
|
+
private scopeF;
|
|
622
|
+
/** Once a second: each namespace with an outBytes cap over its share slows all its rooms (twice as slow per second over, up to 8 times), and
|
|
623
|
+
* speeds up one step when the faster rate would be under 80% of the cap. A busy customer then costs its own players smoothness, not everyone's. */
|
|
624
|
+
private scopeBudgets;
|
|
625
|
+
/** One more join in a namespace with a joinsPerMinute cap, counted over a sliding minute (this minute plus the share of the last one still in it). */
|
|
626
|
+
private scopeJoinAllowed;
|
|
602
627
|
/** Once a second: the rates over the last second (server and per room). */
|
|
603
628
|
private meters;
|
|
604
629
|
/** Everything but the room sends; `sendAll` false when subTick sends rooms on their own phase. */
|
|
@@ -614,8 +639,21 @@ export declare class RoomHub {
|
|
|
614
639
|
/** Server shutdown: pending changes and saves are written, the live rooms go to onSnapshot (the next process takes them back), then every player
|
|
615
640
|
* hears 1012 and reconnects. Without onSnapshot a restart ends every match. */
|
|
616
641
|
shutdown(): Promise<void>;
|
|
617
|
-
/**
|
|
618
|
-
|
|
642
|
+
/** Blue/green hand-over of one namespace to another process: its pending changes go out, open actions fail ("server restarting"), saves are
|
|
643
|
+
* written, its rooms are exported, its sockets hear 1012 (a router in front re-joins them on the other process over the same page socket; a
|
|
644
|
+
* page on its own reconnects) and its rooms close here, giving up their save leases. Joins to it are refused with 1012 meanwhile. Resolves
|
|
645
|
+
* once every save is in the store, so the other process may take the snapshot (importRooms) and write the same saves. */
|
|
646
|
+
exportScope(scope: string): Promise<RoomsSnapshot>;
|
|
647
|
+
/** Namespaces with live rooms here and their size (a router's hand-over list, a dashboard). */
|
|
648
|
+
scopeList(): {
|
|
649
|
+
scope: string;
|
|
650
|
+
rooms: number;
|
|
651
|
+
players: number;
|
|
652
|
+
online: number;
|
|
653
|
+
}[];
|
|
654
|
+
/** The live rooms (of one namespace when given) as plain JSON: everything a new process needs to carry on (sockets, timers and open actions
|
|
655
|
+
* are not kept). */
|
|
656
|
+
exportRooms(scope?: string): RoomsSnapshot;
|
|
619
657
|
/** Take back rooms from exportRooms() after a restart: every seat offline (held 60 s for its player to come back), no host until one is back, a new
|
|
620
658
|
* term, save leases taken again. A snapshot older than maxAgeMs or of another format is ignored. Returns how many rooms came back. */
|
|
621
659
|
importRooms(snap: unknown, o?: {
|
|
@@ -629,11 +667,13 @@ export declare class RoomHub {
|
|
|
629
667
|
/** Close every room of one namespace (a public link stopped). */
|
|
630
668
|
closeScope(scope: string, reason?: string): void;
|
|
631
669
|
/** One namespace's use right now: rooms, seats, connected players and bytes out a second (over the last second). For per-customer caps. */
|
|
670
|
+
/** `slowed`: how many times less often its rooms send because the namespace is over its own outBytes cap (1 = full rate). */
|
|
632
671
|
scopeStats(scope: string): {
|
|
633
672
|
rooms: number;
|
|
634
673
|
players: number;
|
|
635
674
|
online: number;
|
|
636
675
|
bytesOut: number;
|
|
676
|
+
slowed: number;
|
|
637
677
|
};
|
|
638
678
|
stats(): HubStats;
|
|
639
679
|
}
|
package/dist/hub.js
CHANGED
|
@@ -350,6 +350,10 @@ export class RoomHub {
|
|
|
350
350
|
perAddr = new Map();
|
|
351
351
|
addrHits = new Map();
|
|
352
352
|
joins = new Map();
|
|
353
|
+
/** per namespace with caps: its slow-down factor and moving average out (bytes a second) */
|
|
354
|
+
scopeSlow = new Map();
|
|
355
|
+
/** per namespace with a joins cap: joins in this minute and the last one (a sliding estimate, no list per join) */
|
|
356
|
+
scopeJoins = new Map();
|
|
353
357
|
refused = {};
|
|
354
358
|
timer = null;
|
|
355
359
|
sub = 0;
|
|
@@ -381,6 +385,8 @@ export class RoomHub {
|
|
|
381
385
|
pressure = false;
|
|
382
386
|
/** world save leases: "scope|slot|who" -> the room key that may write it */
|
|
383
387
|
leases = new Map();
|
|
388
|
+
/** namespaces being handed to another process now (exportScope): joins to them are refused with 1012 */
|
|
389
|
+
moving = new Set();
|
|
384
390
|
/** shutdown() hands the live rooms to this (a restart writes them to disk; importRooms() in the next process takes them back) */
|
|
385
391
|
onSnapshot;
|
|
386
392
|
/** the room list (rooms {}) is answered; off: the answer is {off: true} */
|
|
@@ -389,8 +395,11 @@ export class RoomHub {
|
|
|
389
395
|
lists = new Map();
|
|
390
396
|
/** rooms allowed in one namespace when it differs from limits.maxRoomsPerScope (a host app's per-customer cap, tickcli-js/remote) */
|
|
391
397
|
roomsPerScope;
|
|
398
|
+
/** a namespace's own data and joins caps (tickcli-js/remote: a deployment's caps); undefined = only the server-wide limits */
|
|
399
|
+
scopeCaps;
|
|
392
400
|
constructor(opts) {
|
|
393
401
|
this.roomsPerScope = opts.roomsPerScope ?? null;
|
|
402
|
+
this.scopeCaps = opts.scopeCaps ?? null;
|
|
394
403
|
this.saves = opts.saves ?? null;
|
|
395
404
|
this.listRooms = opts.listRooms !== false;
|
|
396
405
|
this.onSnapshot = opts.onSnapshot ?? null;
|
|
@@ -575,6 +584,8 @@ export class RoomHub {
|
|
|
575
584
|
const L = this.limits, now = this.now();
|
|
576
585
|
if (s.room)
|
|
577
586
|
return this.bad(s, "already in a room");
|
|
587
|
+
if (this.moving.has(s.scope))
|
|
588
|
+
return s.end(CLOSE.restart, "moving to another server");
|
|
578
589
|
const spec = parseSpec(m.spec, L.maxPlayers);
|
|
579
590
|
if (!spec)
|
|
580
591
|
return this.bad(s, "bad multiplayer option");
|
|
@@ -589,6 +600,10 @@ export class RoomHub {
|
|
|
589
600
|
for (const [k, v] of this.joins)
|
|
590
601
|
if (!v.length || now - v[v.length - 1] > 60_000)
|
|
591
602
|
this.joins.delete(k);
|
|
603
|
+
if (!this.scopeJoinAllowed(s.scope, now)) {
|
|
604
|
+
this.refuse("scope_joins");
|
|
605
|
+
return s.end(CLOSE.busy, "this game is busy right now, trying again");
|
|
606
|
+
}
|
|
592
607
|
const who = s.who;
|
|
593
608
|
if (who.guest && typeof m.name === "string") {
|
|
594
609
|
const n = cleanName(m.name);
|
|
@@ -1439,7 +1454,7 @@ export class RoomHub {
|
|
|
1439
1454
|
continue;
|
|
1440
1455
|
const h = r.host?.session;
|
|
1441
1456
|
rooms.push({ _r: r, code: r.code, host: r.host?.name ?? null, players: on, away, max: r.spec.max, started: r.started, match: r.match,
|
|
1442
|
-
ping: h?.rtt ?? 0, slowed: r.slowF > 1 || (this.pressure && r.seats.size > 4), full: taken >= r.spec.max, spec: r.specKey, created: r.created });
|
|
1457
|
+
ping: h?.rtt ?? 0, slowed: r.slowF > 1 || (this.pressure && r.seats.size > 4) || this.scopeF(r.scope) > 1, full: taken >= r.spec.max, spec: r.specKey, created: r.created });
|
|
1443
1458
|
}
|
|
1444
1459
|
rooms.sort((a, b) => Number(a.full) - Number(b.full) || Number(a.started) - Number(b.started) || b.players - a.players || b.created - a.created);
|
|
1445
1460
|
if (this.lists.size > 1000)
|
|
@@ -1483,9 +1498,87 @@ export class RoomHub {
|
|
|
1483
1498
|
r.outAvg *= 2;
|
|
1484
1499
|
}
|
|
1485
1500
|
}
|
|
1486
|
-
/** How many ticks apart a room sends its state: 1 (50 ms), 2 in rooms over 8 players, times its own budget factor, times
|
|
1487
|
-
* for rooms over 4 players. */
|
|
1488
|
-
every(r) { return (r.seats.size > 8 ? 2 : 1) * r.slowF * (this.pressure && r.seats.size > 4 ? 2 : 1); }
|
|
1501
|
+
/** How many ticks apart a room sends its state: 1 (50 ms), 2 in rooms over 8 players, times its own budget factor, times its namespace's factor
|
|
1502
|
+
* (over its outBytes cap), times 2 under server pressure for rooms over 4 players. */
|
|
1503
|
+
every(r) { return (r.seats.size > 8 ? 2 : 1) * r.slowF * this.scopeF(r.scope) * (this.pressure && r.seats.size > 4 ? 2 : 1); }
|
|
1504
|
+
scopeF(scope) { return this.scopeSlow.get(scope)?.f ?? 1; }
|
|
1505
|
+
/** Once a second: each namespace with an outBytes cap over its share slows all its rooms (twice as slow per second over, up to 8 times), and
|
|
1506
|
+
* speeds up one step when the faster rate would be under 80% of the cap. A busy customer then costs its own players smoothness, not everyone's. */
|
|
1507
|
+
scopeBudgets() {
|
|
1508
|
+
if (!this.scopeCaps)
|
|
1509
|
+
return;
|
|
1510
|
+
const out = new Map();
|
|
1511
|
+
for (const r of this.rooms.values())
|
|
1512
|
+
out.set(r.scope, (out.get(r.scope) || 0) + r.outRate);
|
|
1513
|
+
for (const k of this.scopeSlow.keys())
|
|
1514
|
+
if (!out.has(k))
|
|
1515
|
+
this.scopeSlow.delete(k);
|
|
1516
|
+
for (const [scope, rate] of out) {
|
|
1517
|
+
let cap;
|
|
1518
|
+
try {
|
|
1519
|
+
cap = this.scopeCaps(scope)?.outBytes;
|
|
1520
|
+
}
|
|
1521
|
+
catch {
|
|
1522
|
+
cap = undefined;
|
|
1523
|
+
}
|
|
1524
|
+
if (!cap || cap <= 0) {
|
|
1525
|
+
this.scopeSlow.delete(scope);
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1528
|
+
let x = this.scopeSlow.get(scope);
|
|
1529
|
+
if (!x) {
|
|
1530
|
+
if (rate <= cap)
|
|
1531
|
+
continue;
|
|
1532
|
+
x = { f: 1, avg: 0 };
|
|
1533
|
+
this.scopeSlow.set(scope, x);
|
|
1534
|
+
}
|
|
1535
|
+
x.avg = x.avg ? x.avg * 0.7 + rate * 0.3 : rate;
|
|
1536
|
+
if (x.avg > cap && x.f < 8) {
|
|
1537
|
+
x.f *= 2;
|
|
1538
|
+
x.avg /= 2;
|
|
1539
|
+
this.refuse("scope_budget");
|
|
1540
|
+
}
|
|
1541
|
+
else if (x.f > 1 && x.avg * 2 < cap * 0.8) {
|
|
1542
|
+
x.f /= 2;
|
|
1543
|
+
x.avg *= 2;
|
|
1544
|
+
}
|
|
1545
|
+
if (x.f === 1 && x.avg <= cap)
|
|
1546
|
+
this.scopeSlow.delete(scope);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
/** One more join in a namespace with a joinsPerMinute cap, counted over a sliding minute (this minute plus the share of the last one still in it). */
|
|
1550
|
+
scopeJoinAllowed(scope, now) {
|
|
1551
|
+
if (!this.scopeCaps)
|
|
1552
|
+
return true;
|
|
1553
|
+
let cap;
|
|
1554
|
+
try {
|
|
1555
|
+
cap = this.scopeCaps(scope)?.joinsPerMinute;
|
|
1556
|
+
}
|
|
1557
|
+
catch {
|
|
1558
|
+
cap = undefined;
|
|
1559
|
+
}
|
|
1560
|
+
if (!cap || cap <= 0)
|
|
1561
|
+
return true;
|
|
1562
|
+
let w = this.scopeJoins.get(scope);
|
|
1563
|
+
if (!w || now - w.at >= 120_000) {
|
|
1564
|
+
w = { at: now, n: 0, prev: 0 };
|
|
1565
|
+
this.scopeJoins.set(scope, w);
|
|
1566
|
+
}
|
|
1567
|
+
else if (now - w.at >= 60_000) {
|
|
1568
|
+
w.prev = w.n;
|
|
1569
|
+
w.n = 0;
|
|
1570
|
+
w.at += 60_000;
|
|
1571
|
+
}
|
|
1572
|
+
if (this.scopeJoins.size > 10_000)
|
|
1573
|
+
for (const [k, v] of this.scopeJoins)
|
|
1574
|
+
if (now - v.at >= 120_000)
|
|
1575
|
+
this.scopeJoins.delete(k);
|
|
1576
|
+
const used = w.n + w.prev * Math.max(0, 1 - (now - w.at) / 60_000);
|
|
1577
|
+
if (used >= cap)
|
|
1578
|
+
return false;
|
|
1579
|
+
w.n++;
|
|
1580
|
+
return true;
|
|
1581
|
+
}
|
|
1489
1582
|
/** Once a second: the rates over the last second (server and per room). */
|
|
1490
1583
|
meters(now) {
|
|
1491
1584
|
if (now - this.mark.at < 1000)
|
|
@@ -1501,6 +1594,7 @@ export class RoomHub {
|
|
|
1501
1594
|
r.outMark = r.outBytes;
|
|
1502
1595
|
this.budget(r);
|
|
1503
1596
|
}
|
|
1597
|
+
this.scopeBudgets();
|
|
1504
1598
|
// the whole server: over 80% of its data cap, or the event loop running late (p99 over 30 ms for 10 s, from the host process), bigger rooms
|
|
1505
1599
|
// send at half rate and new rooms wait; back to normal under 60% and a loop on time
|
|
1506
1600
|
const mbit = (this.rates.bytesOut * 8) / 1e6, cap = this.limits.maxOutMbit;
|
|
@@ -1703,11 +1797,70 @@ export class RoomHub {
|
|
|
1703
1797
|
if (snap)
|
|
1704
1798
|
await this.onSnapshot(snap);
|
|
1705
1799
|
}
|
|
1706
|
-
/**
|
|
1707
|
-
|
|
1800
|
+
/** Blue/green hand-over of one namespace to another process: its pending changes go out, open actions fail ("server restarting"), saves are
|
|
1801
|
+
* written, its rooms are exported, its sockets hear 1012 (a router in front re-joins them on the other process over the same page socket; a
|
|
1802
|
+
* page on its own reconnects) and its rooms close here, giving up their save leases. Joins to it are refused with 1012 meanwhile. Resolves
|
|
1803
|
+
* once every save is in the store, so the other process may take the snapshot (importRooms) and write the same saves. */
|
|
1804
|
+
async exportScope(scope) {
|
|
1805
|
+
this.moving.add(scope);
|
|
1806
|
+
try {
|
|
1807
|
+
// a profile save waits for its profile to load: let those land (2 s at most) so the write below carries them
|
|
1808
|
+
const loads = [];
|
|
1809
|
+
for (const r of this.rooms.values())
|
|
1810
|
+
if (r.scope === scope)
|
|
1811
|
+
for (const x of r.seats.values())
|
|
1812
|
+
if (x.profLoad)
|
|
1813
|
+
loads.push(x.profLoad.catch(() => null));
|
|
1814
|
+
if (loads.length)
|
|
1815
|
+
await Promise.race([Promise.all(loads), new Promise((r) => setTimeout(r, 2000).unref?.())]);
|
|
1816
|
+
const mine = [...this.rooms.values()].filter((r) => r.scope === scope);
|
|
1817
|
+
for (const room of mine) {
|
|
1818
|
+
this.flush(room);
|
|
1819
|
+
for (const [aid, a] of room.actions) {
|
|
1820
|
+
a.from.session?.send({ t: "result", id: a.ref, ok: false, error: "server restarting" });
|
|
1821
|
+
room.actions.delete(aid);
|
|
1822
|
+
}
|
|
1823
|
+
this.saveWorld(room, true);
|
|
1824
|
+
for (const x of room.seats.values())
|
|
1825
|
+
this.flushProfile(room, x, true);
|
|
1826
|
+
}
|
|
1827
|
+
const snap = this.exportRooms(scope);
|
|
1828
|
+
for (const s of [...this.sessions])
|
|
1829
|
+
if (s.scope === scope)
|
|
1830
|
+
s.end(CLOSE.restart, "moving to another server");
|
|
1831
|
+
for (const room of mine)
|
|
1832
|
+
this.closeRoom(room);
|
|
1833
|
+
await this.settle();
|
|
1834
|
+
return snap;
|
|
1835
|
+
}
|
|
1836
|
+
finally {
|
|
1837
|
+
this.moving.delete(scope);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
/** Namespaces with live rooms here and their size (a router's hand-over list, a dashboard). */
|
|
1841
|
+
scopeList() {
|
|
1842
|
+
const by = new Map();
|
|
1843
|
+
for (const r of this.rooms.values()) {
|
|
1844
|
+
let x = by.get(r.scope);
|
|
1845
|
+
if (!x) {
|
|
1846
|
+
x = { scope: r.scope, rooms: 0, players: 0, online: 0 };
|
|
1847
|
+
by.set(r.scope, x);
|
|
1848
|
+
}
|
|
1849
|
+
x.rooms++;
|
|
1850
|
+
for (const z of r.seats.values()) {
|
|
1851
|
+
x.players++;
|
|
1852
|
+
if (z.session)
|
|
1853
|
+
x.online++;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
return [...by.values()];
|
|
1857
|
+
}
|
|
1858
|
+
/** The live rooms (of one namespace when given) as plain JSON: everything a new process needs to carry on (sockets, timers and open actions
|
|
1859
|
+
* are not kept). */
|
|
1860
|
+
exportRooms(scope) {
|
|
1708
1861
|
const rooms = [];
|
|
1709
1862
|
for (const r of this.rooms.values()) {
|
|
1710
|
-
if (r.seats.size === 0)
|
|
1863
|
+
if (r.seats.size === 0 || (scope !== undefined && r.scope !== scope))
|
|
1711
1864
|
continue;
|
|
1712
1865
|
rooms.push({
|
|
1713
1866
|
key: r.key, scope: r.scope, game: r.game, spec: r.spec, code: r.code, priv: r.priv, slot: r.slot, owner: r.owner, world: r.world, lease: r.lease, saveBusy: r.saveBusy,
|
|
@@ -1793,6 +1946,7 @@ export class RoomHub {
|
|
|
1793
1946
|
}
|
|
1794
1947
|
}
|
|
1795
1948
|
/** One namespace's use right now: rooms, seats, connected players and bytes out a second (over the last second). For per-customer caps. */
|
|
1949
|
+
/** `slowed`: how many times less often its rooms send because the namespace is over its own outBytes cap (1 = full rate). */
|
|
1796
1950
|
scopeStats(scope) {
|
|
1797
1951
|
let rooms = 0, players = 0, online = 0, bytesOut = 0;
|
|
1798
1952
|
for (const r of this.rooms.values()) {
|
|
@@ -1806,7 +1960,7 @@ export class RoomHub {
|
|
|
1806
1960
|
online++;
|
|
1807
1961
|
}
|
|
1808
1962
|
}
|
|
1809
|
-
return { rooms, players, online, bytesOut };
|
|
1963
|
+
return { rooms, players, online, bytesOut, slowed: this.scopeF(scope) };
|
|
1810
1964
|
}
|
|
1811
1965
|
stats() {
|
|
1812
1966
|
let players = 0, online = 0, started = 0, slow = 0;
|
|
@@ -1830,6 +1984,7 @@ export class RoomHub {
|
|
|
1830
1984
|
...this.rates, tickMs: { p50: pct(0.5), p99: pct(0.99), max: n ? Math.round(sorted[n - 1] * 100) / 100 : 0 }, slow, started,
|
|
1831
1985
|
savesOk: this.counts.saves_ok || 0, savesFailed: this.refused.save_failed || 0, loadsFailed: this.refused.load_failed || 0, worldWritesPerMin: this.worldWrites.length,
|
|
1832
1986
|
loop: this.loop, top, pressure: this.pressure, slowedRooms: [...this.rooms.values()].filter((r) => r.slowF > 1).length,
|
|
1987
|
+
slowedScopes: [...this.scopeSlow.values()].filter((x) => x.f > 1).length,
|
|
1833
1988
|
};
|
|
1834
1989
|
return { rooms: this.rooms.size, players, online, connections: this.sessions.size, pending: this.pending, refused: { ...this.refused }, live };
|
|
1835
1990
|
}
|