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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user