Fix one second counters

This commit is contained in:
Andrey Sobolev
2025-10-27 22:02:15 +07:00
parent 774865076f
commit d61bb82693
3 changed files with 39 additions and 4 deletions
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@hcengineering/server-core",
"comment": "Fix one second counters",
"type": "patch"
}
],
"packageName": "@hcengineering/server-core"
}
+22
View File
@@ -84,6 +84,14 @@ describe('OneSecondCountersImpl', () => {
expect(entries).toEqual([['op', 0]])
})
it('focused: counter returns to 0 after operation finishes', async () => {
// Ensure explicitly that counter returns to 0 after the operation completes
await counters.withCounter('focus', 3, async () => 'ok')
const entries = Array.from(counters.entries()).sort((a, b) => a[0].localeCompare(b[0]))
// 'focus' counter should be 0
expect(entries.find((e) => e[0] === 'focus')).toEqual(['focus', 0])
})
it('should decrement counter on operation error', async () => {
const operation = jest.fn(async () => {
throw new Error('Operation failed')
@@ -198,6 +206,20 @@ describe('OneSecondCountersImpl', () => {
expect(entries).toEqual([['op', 0]])
})
it('focused: counter returns to 0 after timeout', async () => {
// Start an operation that never resolves
void counters.withCounter('timen', 4, async (): Promise<void> => {
// never resolves
})
// Counter should be incremented
expect(Array.from(counters.entries()).find((e) => e[0] === 'timen')?.[1]).toBe(4)
// Fast-forward past the timeout and run check
jest.advanceTimersByTime(61 * 1000)
counters.check()
// After timeout the counter for 'timen' should be back to 0
expect(Array.from(counters.entries()).find((e) => e[0] === 'timen')).toEqual(['timen', 0])
})
it('should clean up multiple expired timeouts', async () => {
// Create multiple operations
void counters.withCounter('op1', 1, async (): Promise<void> => {})
+7 -4
View File
@@ -431,7 +431,8 @@ export async function calcHashHash (ctx: MeasureContext, domain: Domain, adapter
export class OneSecondCountersImpl implements OneSecondCounters {
private readonly counters = new Map<string, number>()
private counterTimeouts: [number, () => void][] = []
private readonly counterTimeouts = new Map<number, [number, () => void]>()
ids: number = 0
add (counter: string, count: number): void {
this.counters.set(counter, (this.counters.get(counter) ?? 0) + count)
@@ -440,7 +441,8 @@ export class OneSecondCountersImpl implements OneSecondCounters {
async withCounter<T>(counter: string, count: number, op: () => Promise<T>): Promise<T> {
this.add(counter, count)
let cleared = false
this.counterTimeouts.push([
const id = ++this.ids
this.counterTimeouts.set(id, [
platformNow() + 60 * 1000, // One minute timeout
() => {
if (!cleared) {
@@ -455,6 +457,7 @@ export class OneSecondCountersImpl implements OneSecondCounters {
if (!cleared) {
this.add(counter, -count)
}
cleared = true
}
}
@@ -465,11 +468,11 @@ export class OneSecondCountersImpl implements OneSecondCounters {
check (): void {
// Check for timeouts
const now = platformNow()
for (const [timeout, cb] of this.counterTimeouts) {
for (const [k, [timeout, cb]] of [...this.counterTimeouts.entries()]) {
if (timeout < now) {
cb()
this.counterTimeouts.delete(k)
}
}
this.counterTimeouts = this.counterTimeouts.filter((it) => it[0] >= now)
}
}