fix(server): online and storage statistics (#14792)

#### PR Dependency Tree


* **PR #14792** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Admin dashboard returns more accurate sync and storage timelines with
carry‑forwarded minute buckets and corrected current totals.

* **Bug Fixes**
* Active-user flushes are debounced/scheduled to prevent overlapping
writes and reduce stale counts.
* Snapshot writes now retry and will skip gracefully when lock
contention prevents completion, avoiding partial snapshots.

* **Tests**
* New e2e tests cover carry‑forward behavior, no backfill outside
requested windows, and storage history accuracy.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-04-07 02:08:13 +08:00
committed by GitHub
parent 193ec14ad3
commit 5806ad8a3a
4 changed files with 332 additions and 38 deletions

View File

@@ -218,6 +218,9 @@ export class SpaceSyncGateway
private readonly localUserConnectionCounts = new Map<string, number>();
private unresolvedPresenceSockets = 0;
private flushTimer?: NodeJS.Timeout;
private activeUsersFlushTimer?: NodeJS.Timeout;
private activeUsersFlushInFlight = false;
private activeUsersFlushQueued = false;
constructor(
private readonly ac: AccessController,
@@ -229,12 +232,9 @@ export class SpaceSyncGateway
) {}
onModuleInit() {
this.scheduleActiveUsersFlush(0);
this.flushTimer = setInterval(() => {
this.flushActiveUsersMinute().catch(error => {
this.logger.warn(
`Failed to flush active users minute: ${this.formatError(error)}`
);
});
this.scheduleActiveUsersFlush(0);
}, 60_000);
this.flushTimer.unref?.();
}
@@ -244,6 +244,11 @@ export class SpaceSyncGateway
clearInterval(this.flushTimer);
this.flushTimer = undefined;
}
if (this.activeUsersFlushTimer) {
clearTimeout(this.activeUsersFlushTimer);
this.activeUsersFlushTimer = undefined;
}
this.activeUsersFlushQueued = false;
}
private encodeUpdates(updates: Uint8Array[]) {
@@ -331,13 +336,7 @@ export class SpaceSyncGateway
metrics.socketio.gauge('connections').record(this.connectionCount);
const userId = this.attachPresenceUserId(client);
this.trackConnectedSocket(client.id, userId);
void this.flushActiveUsersMinute({
aggregateAcrossCluster: false,
}).catch(error => {
this.logger.warn(
`Failed to flush active users minute: ${this.formatError(error)}`
);
});
this.scheduleActiveUsersFlush();
}
handleDisconnect(client: Socket) {
@@ -347,13 +346,7 @@ export class SpaceSyncGateway
`Connection disconnected, total: ${this.connectionCount}`
);
metrics.socketio.gauge('connections').record(this.connectionCount);
void this.flushActiveUsersMinute({
aggregateAcrossCluster: false,
}).catch(error => {
this.logger.warn(
`Failed to flush active users minute: ${this.formatError(error)}`
);
});
this.scheduleActiveUsersFlush();
}
private attachPresenceUserId(client: Socket): string | null {
@@ -435,13 +428,55 @@ export class SpaceSyncGateway
}
}
private scheduleActiveUsersFlush(delayMs = 250) {
if (this.activeUsersFlushTimer) {
return;
}
if (this.activeUsersFlushInFlight) {
this.activeUsersFlushQueued = true;
return;
}
this.activeUsersFlushTimer = setTimeout(() => {
this.activeUsersFlushTimer = undefined;
this.runScheduledActiveUsersFlush();
}, delayMs);
this.activeUsersFlushTimer.unref?.();
}
private runScheduledActiveUsersFlush() {
if (this.activeUsersFlushInFlight) {
this.activeUsersFlushQueued = true;
return;
}
this.activeUsersFlushInFlight = true;
void this.flushActiveUsersMinute()
.catch(error => {
this.logger.warn(
`Failed to flush active users minute: ${this.formatError(error)}`
);
})
.finally(() => {
this.activeUsersFlushInFlight = false;
if (this.activeUsersFlushQueued) {
this.activeUsersFlushQueued = false;
this.scheduleActiveUsersFlush(0);
}
});
}
private async flushActiveUsersMinute(options?: {
aggregateAcrossCluster?: boolean;
skipWriteOnAggregateError?: boolean;
}) {
const minute = new Date();
minute.setSeconds(0, 0);
const aggregateAcrossCluster = options?.aggregateAcrossCluster ?? true;
const skipWriteOnAggregateError =
options?.skipWriteOnAggregateError ?? aggregateAcrossCluster;
let activeUsers = this.resolveLocalActiveUsers();
if (aggregateAcrossCluster) {
try {
@@ -467,8 +502,9 @@ export class SpaceSyncGateway
}
} catch (error) {
this.logger.warn(
`Failed to aggregate active users from sockets, using local value: ${this.formatError(error)}`
`Failed to aggregate active users from sockets: ${this.formatError(error)}`
);
if (skipWriteOnAggregateError) return;
}
}