From 302bab518e50cb097508a37ae28712829ff90173 Mon Sep 17 00:00:00 2001 From: Hide_D Date: Sun, 13 Aug 2023 09:39:02 +0000 Subject: [PATCH] =?UTF-8?q?UniqueNumberAllocator=20=EA=B5=AC=ED=98=84=20-?= =?UTF-8?q?=20DB=EC=97=90=20=EC=9B=90=EC=9E=90=EC=A0=81=20=EA=B0=92=20?= =?UTF-8?q?=EC=A6=9D=EA=B0=80=20=ED=98=B8=EC=B6=9C=EC=9D=B4=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=ED=95=98=EB=8B=A4=EB=8A=94=20=EA=B0=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/util/UniqueNumberAllocator.ts | 200 +++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 server/util/UniqueNumberAllocator.ts diff --git a/server/util/UniqueNumberAllocator.ts b/server/util/UniqueNumberAllocator.ts new file mode 100644 index 0000000..eb386aa --- /dev/null +++ b/server/util/UniqueNumberAllocator.ts @@ -0,0 +1,200 @@ +import { HEAD } from "../apiStructure/defs.js"; + +export interface StateIncrementer { + (state: number): Promise; +} + +export type NumberGroup = { + start: number; + remain: number; +} + +const AllocatorGroupBucketSize = 100; +const AllocatorPreserveThreshold = 50; + +export function* numberGenerator(numberGroups: NumberGroup[]): Generator { + for (const numberGroup of numberGroups) { + for (let i = 0; i < numberGroup.remain; i++) { + yield numberGroup.start + i; + } + } + return null; +} + +export class UniqueNumberAllocator { + private preservedNumbers: NumberGroup[]; + private incrementer: StateIncrementer; + private totalRemain: number; + private promisedCnt: number; + private promise: Promise; + + public constructor(incrementer: StateIncrementer, initialState?: NumberGroup) { + this.incrementer = incrementer; + if (!initialState) { + this.preservedNumbers = []; + this.totalRemain = 0; + } + else { + if (initialState.remain < 1) { + throw new Error('initialState.remain must be greater than 0'); + } + this.preservedNumbers = [initialState]; + this.totalRemain = initialState.remain; + } + this.promisedCnt = 0; + this.promise = Promise.resolve(); + this.preserveNext(); + } + + public static async generate(incrementer: StateIncrementer): Promise { + const initialStateEnd = await incrementer(AllocatorGroupBucketSize); + const initialState: NumberGroup = { + start: initialStateEnd - AllocatorGroupBucketSize + 1, + remain: AllocatorGroupBucketSize, + } + return new UniqueNumberAllocator(incrementer, initialState); + } + + private preserveNext(): void { + if (this.promisedCnt + this.totalRemain >= AllocatorGroupBucketSize) { + return; + } + + if (this.totalRemain >= AllocatorPreserveThreshold) { + return; + } + + const waiter = this.promise; + + this.promise = (async () => { + this.promisedCnt += AllocatorGroupBucketSize; + const nextP = this.incrementer(AllocatorGroupBucketSize); + await waiter; + const next = await nextP; + if (this.preservedNumbers.length > 0) { + const last = this.preservedNumbers[this.preservedNumbers.length - 1]; + if (last.start + last.remain === next - AllocatorGroupBucketSize + 1) { + // 마지막 그룹과 연속됨 + last.remain += AllocatorGroupBucketSize; + this.totalRemain += AllocatorGroupBucketSize; + this.promisedCnt -= AllocatorGroupBucketSize; + return; + } + } + const nextGroup: NumberGroup = { + start: next - AllocatorGroupBucketSize + 1, + remain: AllocatorGroupBucketSize, + } + this.preservedNumbers.push(nextGroup); + this.totalRemain += AllocatorGroupBucketSize; + this.promisedCnt -= AllocatorGroupBucketSize; + })(); + } + + public async allocateOne(): Promise { + if (this.totalRemain > 0) { + const head = this.preservedNumbers[0]; + if (head.remain > 1) { + const next = head.start; + head.start++; + head.remain--; + this.totalRemain--; + this.preserveNext(); + return next; + } + else { + this.preservedNumbers.shift(); + this.totalRemain--; + this.preserveNext(); + return head.start; + } + } + + const next = (await this.allocate(1)).next().value; + if (next === null) { + throw new Error('incrementer failed to allocate enough numbers'); + } + return next; + } + + public async allocate(n: number): Promise> { + n = Math.ceil(n); + if (n < 1) { + throw new Error('n must be greater than 0'); + } + + if (n > this.totalRemain + this.promisedCnt) { + const endP = this.incrementer(n); + this.preserveNext(); + const end = await endP; + + return numberGenerator([{ + start: end - n + 1, + remain: n, + }]); + } + + const result: NumberGroup[] = []; + + if (this.totalRemain > 0) { + const head = this.preservedNumbers[0]; + const headRemain = head.remain; + + if (n < headRemain) { + result.push({ + start: head.start, + remain: n, + }); + head.start += n; + head.remain -= n; + this.totalRemain -= n; + this.preserveNext(); + return numberGenerator(result); + } + if (n === headRemain) { + this.preservedNumbers.shift(); + result.push(head); + this.totalRemain -= n; + this.preserveNext(); + return numberGenerator(result); + } + + } + + //assert n <= this.totalRemain + this.promisedRemain + + if (n > this.totalRemain) { + await this.promise; + if (n > this.totalRemain) { + throw new Error('incrementer failed to allocate enough numbers'); + } + } + + this.totalRemain -= n; + this.preserveNext(); + + let remain = n; + let idx = 0; + while (remain > 0) { + const head = this.preservedNumbers[idx]; + if (head.remain <= remain) { + remain -= head.remain; + result.push(head); + idx++; + continue; + } + + result.push({ + start: head.start, + remain: remain, + }); + head.start += remain; + head.remain -= remain; + remain = 0; + break; + } + this.preservedNumbers = this.preservedNumbers.slice(idx); + + return numberGenerator(result); + } +} \ No newline at end of file