From 4bc7d99634084d65c29ca11bcbcf5c450b70081d Mon Sep 17 00:00:00 2001 From: skshim Date: Tue, 25 Mar 2025 15:06:11 +0900 Subject: [PATCH 1/4] add test-fastlock.js --- jest.config.js | 9 +++-- package-lock.json | 6 ++- src/fast-lock.ts | 21 ++++++++-- test-fastlock.js | 98 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 test-fastlock.js diff --git a/jest.config.js b/jest.config.js index 142c679..73e3f09 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,11 +1,12 @@ module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', globals: { 'ts-jest': { tsConfig: 'tsconfig.json', }, }, - transform: { - '^.+\\.(ts|tsx)$': 'ts-jest', - }, - testEnvironment: 'node', + testTimeout: 30000, + verbose: true, + collectCoverage: true, }; diff --git a/package-lock.json b/package-lock.json index 5ee8264..c5e748c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "typescript": "^5.3.3" }, "peerDependencies": { - "@day1co/pebbles": "~3.2.7", + "@day1co/pebbles": "^3.2.0", "redlock": "^5.0.0-beta.2" } }, @@ -4776,7 +4776,8 @@ "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", "dev": true, "dependencies": { - "@isaacs/cliui": "^8.0.2" + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" }, "engines": { "node": ">=14" @@ -5017,6 +5018,7 @@ "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", diff --git a/src/fast-lock.ts b/src/fast-lock.ts index e87acf3..22c6ec0 100644 --- a/src/fast-lock.ts +++ b/src/fast-lock.ts @@ -8,6 +8,7 @@ type FastLockOpts = { redis?: RedisOptions; createRedisClient?: (opts: RedisOptions) => Redis; redlock?: any; + useMock?: boolean; }; export class FastLock { @@ -20,8 +21,22 @@ export class FastLock { private locker: any; private constructor(opts?: FastLockOpts) { - this.client = opts?.createRedisClient ? opts?.createRedisClient(opts?.redis ?? {}) : new IORedis(opts?.redis ?? {}); - logger.debug(`connect redis: ${opts?.redis?.host}:${opts?.redis?.port}/${opts?.redis?.db}`); + if (opts?.useMock) { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const RedisMock = require('ioredis-mock'); + this.client = new RedisMock(); + logger.debug('Using Redis mock client'); + } catch (err) { + logger.error('Failed to load ioredis-mock. Please install it with: npm install --save-dev ioredis-mock'); + throw new Error('ioredis-mock is required when useMock=true'); + } + } else { + this.client = opts?.createRedisClient + ? opts.createRedisClient(opts?.redis ?? {}) + : new IORedis(opts?.redis ?? {}); + logger.debug(`connect redis: ${opts?.redis?.host}:${opts?.redis?.port}/${opts?.redis?.db}`); + } this.redlock = new Redlock( [this.client], @@ -50,7 +65,7 @@ export class FastLock { public destroy() { logger.debug('destroy'); - this.client.end(true); + this.client.disconnect?.() || this.client.quit?.() || this.client.end?.(true); } //-------------------------------------------------------- diff --git a/test-fastlock.js b/test-fastlock.js new file mode 100644 index 0000000..486830e --- /dev/null +++ b/test-fastlock.js @@ -0,0 +1,98 @@ +const { FastLock } = require('./lib/index'); + +// 공유 리소스 +const sharedResource = []; + +// 프로세스 1 시뮬레이션 +async function process1() { + const locker = FastLock.create({ useMock: true }); + let lockAcquired = false; + + console.log('프로세스 1: 시작...'); + try { + console.log('프로세스 1: 락 획득 시도...'); + await locker.lock('test-lock', 5000); + lockAcquired = true; + console.log('프로세스 1: 락 획득 성공!'); + + // 공유 리소스 작업 + sharedResource.push('프로세스 1: 아이템 1'); + await new Promise(resolve => setTimeout(resolve, 1000)); + console.log('프로세스 1: 작업 중...'); + sharedResource.push('프로세스 1: 아이템 2'); + await new Promise(resolve => setTimeout(resolve, 1000)); + sharedResource.push('프로세스 1: 아이템 3'); + + console.log('프로세스 1: 작업 완료'); + } catch (err) { + console.log('프로세스 1: 에러 발생 -', err.message); + } finally { + if (lockAcquired) { + console.log('프로세스 1: 락 해제'); + await locker.unlock(); + } + locker.destroy(); + } +} + +// 프로세스 2 시뮬레이션 +async function process2() { + const locker = FastLock.create({ useMock: true }); + let lockAcquired = false; + + console.log('프로세스 2: 시작...'); + try { + console.log('프로세스 2: 락 획득 시도...'); + await locker.lock('test-lock', 5000); + lockAcquired = true; + console.log('프로세스 2: 락 획득 성공!'); + + // 공유 리소스 작업 + sharedResource.push('프로세스 2: 아이템 1'); + await new Promise(resolve => setTimeout(resolve, 500)); + console.log('프로세스 2: 작업 중...'); + sharedResource.push('프로세스 2: 아이템 2'); + await new Promise(resolve => setTimeout(resolve, 500)); + sharedResource.push('프로세스 2: 아이템 3'); + + console.log('프로세스 2: 작업 완료'); + } catch (err) { + console.log('프로세스 2: 에러 발생 -', err.message); + } finally { + if (lockAcquired) { + console.log('프로세스 2: 락 해제'); + await locker.unlock(); + } + locker.destroy(); + } +} + +// 동시성 테스트 실행 +async function runConcurrencyTest() { + console.log('=== FastLock 동시성 테스트 시작 ===\n'); + + // 두 프로세스를 거의 동시에 시작 + const p1 = process1(); + const p2 = process2(); + + // 두 프로세스가 모두 완료될 때까지 대기 + await Promise.all([p1, p2]); + + console.log('\n최종 공유 리소스 상태:'); + console.log(sharedResource); + + // 결과 검증 + const process1Items = sharedResource.filter(item => item.startsWith('프로세스 1')); + const process2Items = sharedResource.filter(item => item.startsWith('프로세스 2')); + + console.log('\n=== 테스트 결과 ==='); + console.log('프로세스 1 아이템:', process1Items.length === 3 ? '완료' : '미완료'); + console.log('프로세스 2 아이템:', process2Items.length === 3 ? '완료' : '미완료'); + console.log('총 아이템 수:', sharedResource.length === 6 ? '정상' : '비정상'); + + console.log('\n=== FastLock 동시성 테스트 완료 ==='); +} + +// 테스트 실행 +runConcurrencyTest() + .catch(err => console.error('테스트 실행 중 에러 발생:', err)); \ No newline at end of file From 3bf9db6600ac4ae20f625161632b55d94111e97e Mon Sep 17 00:00:00 2001 From: skshim Date: Tue, 25 Mar 2025 15:18:04 +0900 Subject: [PATCH 2/4] refactroring test files --- README.md | 19 +++ mocking-test-fastlock.spec.js | 240 ++++++++++++++++++++++++++++++++++ package.json | 1 + src/fast-lock.spec.ts | 94 +++++++++++++ test-fastlock.js | 98 -------------- 5 files changed, 354 insertions(+), 98 deletions(-) create mode 100644 mocking-test-fastlock.spec.js delete mode 100644 test-fastlock.js diff --git a/README.md b/README.md index 6b1c5a0..3946221 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,21 @@ locker.lock('locks:fastlock',5000); array.push(1); array.push(2); locker.unlock(); +``` + +### Using Mock Redis (for development) + +You can use a mock Redis instance for development and testing without running a real Redis server: +```[js](js) +const { FastLock } = require('@fastcampus/fastlock'); +const locker = FastLock.create({ useMock: true }); +const array = []; +await locker.lock('locks:fastlock', 5000); +array.push(1); +array.push(2); +await locker.unlock(); ``` ## Contributing @@ -27,6 +40,12 @@ locker.unlock(); $ npm run ci:test ``` +### test with mock Redis + +```console +$ npm run test:mock +``` + ### build ```console diff --git a/mocking-test-fastlock.spec.js b/mocking-test-fastlock.spec.js new file mode 100644 index 0000000..a0333bf --- /dev/null +++ b/mocking-test-fastlock.spec.js @@ -0,0 +1,240 @@ +const { FastLock } = require('./lib/index'); + +// 공유 리소스 +const sharedResource = []; + +// 프로세스 1 시뮬레이션 +async function process1() { + const locker = FastLock.create({ useMock: true }); + let lockAcquired = false; + + console.log('프로세스 1: 시작...'); + try { + console.log('프로세스 1: 락 획득 시도...'); + await locker.lock('test-lock', 5000); + lockAcquired = true; + console.log('프로세스 1: 락 획득 성공!'); + + // 공유 리소스 작업 + sharedResource.push('프로세스 1: 아이템 1'); + await new Promise(resolve => setTimeout(resolve, 1000)); + console.log('프로세스 1: 작업 중...'); + sharedResource.push('프로세스 1: 아이템 2'); + await new Promise(resolve => setTimeout(resolve, 1000)); + sharedResource.push('프로세스 1: 아이템 3'); + + console.log('프로세스 1: 작업 완료'); + } catch (err) { + console.log('프로세스 1: 에러 발생 -', err.message); + } finally { + if (lockAcquired) { + console.log('프로세스 1: 락 해제'); + await locker.unlock(); + } + locker.destroy(); + } +} + +// 프로세스 2 시뮬레이션 +async function process2() { + const locker = FastLock.create({ useMock: true }); + let lockAcquired = false; + + console.log('프로세스 2: 시작...'); + try { + console.log('프로세스 2: 락 획득 시도...'); + await locker.lock('test-lock', 5000); + lockAcquired = true; + console.log('프로세스 2: 락 획득 성공!'); + + // 공유 리소스 작업 + sharedResource.push('프로세스 2: 아이템 1'); + await new Promise(resolve => setTimeout(resolve, 500)); + console.log('프로세스 2: 작업 중...'); + sharedResource.push('프로세스 2: 아이템 2'); + await new Promise(resolve => setTimeout(resolve, 500)); + sharedResource.push('프로세스 2: 아이템 3'); + + console.log('프로세스 2: 작업 완료'); + } catch (err) { + console.log('프로세스 2: 에러 발생 -', err.message); + } finally { + if (lockAcquired) { + console.log('프로세스 2: 락 해제'); + await locker.unlock(); + } + locker.destroy(); + } +} + +// 락 만료 테스트 +async function testLockExpiration() { + console.log('\n=== 락 만료 테스트 시작 ==='); + const locker1 = FastLock.create({ useMock: true }); + const locker2 = FastLock.create({ useMock: true }); + const results = []; + + try { + // 첫 번째 락 획득 (짧은 만료 시간) + await locker1.lock('expire-test-lock', 1000); + results.push('락1 획득'); + console.log('락1 획득됨 (만료 시간: 1초)'); + + // 만료 시간보다 조금 더 대기 + await new Promise(resolve => setTimeout(resolve, 1100)); + console.log('1.1초 대기 완료'); + + // 두 번째 클라이언트가 락 획득 시도 + await locker2.lock('expire-test-lock', 1000); + results.push('락2 획득'); + console.log('락2 획득됨 (락1 만료 후)'); + await locker2.unlock(); + + console.log('\n테스트 결과:', results); + console.log('예상 결과:', ['락1 획득', '락2 획득']); + console.log('테스트 ' + (results.join(',') === ['락1 획득', '락2 획득'].join(',') ? '성공' : '실패')); + } catch (err) { + console.error('테스트 중 에러 발생:', err); + } finally { + locker1.destroy(); + locker2.destroy(); + } + console.log('=== 락 만료 테스트 완료 ===\n'); +} + +// 락 재시도 테스트 +async function testLockRetry() { + console.log('\n=== 락 재시도 테스트 시작 ==='); + const locker1 = FastLock.create({ useMock: true }); + const results = []; + + try { + // 첫 번째 락 획득 + await locker1.lock('retry-test-lock', 2000); + results.push('첫 번째 락 획득'); + console.log('첫 번째 락 획득됨'); + + // 재시도 설정으로 두 번째 락커 생성 + const retryLocker = FastLock.create({ + useMock: true, + redlock: { + driftFactor: 0.01, + retryCount: 3, + retryDelay: 200, + retryJitter: 100, + } + }); + + try { + // 이미 잠긴 리소스에 대한 락 획득 시도 + await retryLocker.lock('retry-test-lock', 1000); + results.push('재시도 락 획득 (예상치 못한 결과)'); + } catch (err) { + results.push('재시도 실패 (예상된 결과)'); + console.log('예상된 재시도 실패 발생'); + } + + await locker1.unlock(); + retryLocker.destroy(); + + console.log('\n테스트 결과:', results); + console.log('예상 결과:', ['첫 번째 락 획득', '재시도 실패 (예상된 결과)']); + console.log('테스트 ' + (results.join(',') === ['첫 번째 락 획득', '재시도 실패 (예상된 결과)'].join(',') ? '성공' : '실패')); + } catch (err) { + console.error('테스트 중 에러 발생:', err); + } finally { + locker1.destroy(); + } + console.log('=== 락 재시도 테스트 완료 ===\n'); +} + +// 네트워크 지연 시뮬레이션 테스트 +async function testNetworkDelay() { + console.log('\n=== 네트워크 지연 시뮬레이션 테스트 시작 ==='); + const locker1 = FastLock.create({ useMock: true }); + const locker2 = FastLock.create({ useMock: true }); + const results = []; + + try { + // 첫 번째 락 획득 + await locker1.lock('delay-test-lock', 2000); + results.push('첫 번째 락 획득'); + console.log('첫 번째 락 획득됨'); + + // 지연된 언락 시뮬레이션 + const delayedUnlock = async () => { + await new Promise(resolve => setTimeout(resolve, 2500)); // 락 만료 시간보다 긴 지연 + try { + await locker1.unlock(); + results.push('지연된 언락 성공'); + } catch (err) { + results.push('지연된 언락 실패'); + console.log('예상된 지연 언락 실패 발생'); + } + }; + + // 지연된 언락 시작 + delayedUnlock(); + + // 지연 중 두 번째 클라이언트의 락 획득 시도 + await new Promise(resolve => setTimeout(resolve, 2200)); + await locker2.lock('delay-test-lock', 2000); + results.push('두 번째 락 획득'); + console.log('두 번째 락 획득됨 (첫 번째 락 만료 후)'); + await locker2.unlock(); + + // 모든 작업 완료 대기 + await new Promise(resolve => setTimeout(resolve, 500)); + + console.log('\n테스트 결과:', results); + console.log('예상 결과:', ['첫 번째 락 획득', '두 번째 락 획득', '지연된 언락 실패']); + console.log('테스트 ' + (results.join(',') === ['첫 번째 락 획득', '두 번째 락 획득', '지연된 언락 실패'].join(',') ? '성공' : '실패')); + } catch (err) { + console.error('테스트 중 에러 발생:', err); + } finally { + locker1.destroy(); + locker2.destroy(); + } + console.log('=== 네트워크 지연 시뮬레이션 테스트 완료 ===\n'); +} + +// 동시성 테스트 실행 +async function runConcurrencyTest() { + console.log('=== FastLock 동시성 테스트 시작 ===\n'); + + // 두 프로세스를 거의 동시에 시작 + const p1 = process1(); + const p2 = process2(); + + // 두 프로세스가 모두 완료될 때까지 대기 + await Promise.all([p1, p2]); + + console.log('\n최종 공유 리소스 상태:'); + console.log(sharedResource); + + // 결과 검증 + const process1Items = sharedResource.filter(item => item.startsWith('프로세스 1')); + const process2Items = sharedResource.filter(item => item.startsWith('프로세스 2')); + + console.log('\n=== 테스트 결과 ==='); + console.log('프로세스 1 아이템:', process1Items.length === 3 ? '완료' : '미완료'); + console.log('프로세스 2 아이템:', process2Items.length === 3 ? '완료' : '미완료'); + console.log('총 아이템 수:', sharedResource.length === 6 ? '정상' : '비정상'); + + console.log('\n=== FastLock 동시성 테스트 완료 ==='); +} + +// 모든 테스트 실행 +async function runAllTests() { + try { + await runConcurrencyTest(); + await testLockExpiration(); + await testLockRetry(); + await testNetworkDelay(); + } catch (err) { + console.error('테스트 실행 중 에러 발생:', err); + } +} + +// 테스트 실행 +runAllTests(); \ No newline at end of file diff --git a/package.json b/package.json index 2a376b3..b122f0a 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "serve": "echo no serve", "start": "npm run serve", "test": "jest --runInBand --forceExit --coverage --verbose ./src", + "test:mock": "npm run build && node mocking-test-fastlock.spec.js", "ci:test": "./ci_test.sh", "watch": "tsc -w" }, diff --git a/src/fast-lock.spec.ts b/src/fast-lock.spec.ts index 109843c..80a6dc1 100644 --- a/src/fast-lock.spec.ts +++ b/src/fast-lock.spec.ts @@ -85,5 +85,99 @@ describe('FastLock', () => { expect(testArray).toEqual(['bar', 'foo']); } }); + + // 락 만료 시간 테스트 + test('lock expiration', async () => { + const testArray: string[] = []; + try { + // 짧은 만료 시간으로 락 획득 + await locker.lock(lockKey, 1000); + testArray.push('acquired'); + + // 만료 시간보다 조금 더 대기 + await sleep(1100); + + // 다른 클라이언트가 락을 획득할 수 있어야 함 + await locker2.lock(lockKey, 1000); + testArray.push('reacquired'); + await locker2.unlock(); + + expect(testArray).toEqual(['acquired', 'reacquired']); + } catch (err) { + logger.error('%o', err); + } + }); + + // 락 재시도 테스트 + test('lock retry', async () => { + const testArray: string[] = []; + try { + // 첫 번째 클라이언트가 락 획득 + await locker.lock(lockKey, 2000); + testArray.push('first'); + + // 두 번째 클라이언트의 락 획득 시도 (실패 예상) + const retryConfig = { + driftFactor: 0.01, + retryCount: 3, + retryDelay: 200, + retryJitter: 100, + }; + + const retryLocker = FastLock.create({ + redis: { host: 'localhost', port: 6379, db: 0 }, + redlock: retryConfig, + }); + + try { + await retryLocker.lock(lockKey, 1000); + } catch (err) { + testArray.push('retry-failed'); + } + + await locker.unlock(); + retryLocker.destroy(); + + expect(testArray).toEqual(['first', 'retry-failed']); + } catch (err) { + logger.error('%o', err); + } + }); + + // 네트워크 지연 시뮬레이션 테스트 + test('network delay simulation', async () => { + const testArray: string[] = []; + try { + // 첫 번째 클라이언트가 락 획득 + await locker.lock(lockKey, 2000); + testArray.push('acquired'); + + // 네트워크 지연 시뮬레이션 + const delayedUnlock = async () => { + await sleep(2500); // 락 만료 시간보다 더 긴 지연 + try { + await locker.unlock(); + testArray.push('unlock-after-delay'); + } catch (err) { + testArray.push('unlock-failed'); + } + }; + + // 지연된 언락 시작 + delayedUnlock(); + + // 지연 동안 다른 클라이언트가 락 획득 시도 + await sleep(2200); + await locker2.lock(lockKey, 2000); + testArray.push('reacquired'); + await locker2.unlock(); + + await sleep(500); // 지연된 언락이 완료될 때까지 대기 + + expect(testArray).toEqual(['acquired', 'reacquired', 'unlock-failed']); + } catch (err) { + logger.error('%o', err); + } + }); }); }); diff --git a/test-fastlock.js b/test-fastlock.js deleted file mode 100644 index 486830e..0000000 --- a/test-fastlock.js +++ /dev/null @@ -1,98 +0,0 @@ -const { FastLock } = require('./lib/index'); - -// 공유 리소스 -const sharedResource = []; - -// 프로세스 1 시뮬레이션 -async function process1() { - const locker = FastLock.create({ useMock: true }); - let lockAcquired = false; - - console.log('프로세스 1: 시작...'); - try { - console.log('프로세스 1: 락 획득 시도...'); - await locker.lock('test-lock', 5000); - lockAcquired = true; - console.log('프로세스 1: 락 획득 성공!'); - - // 공유 리소스 작업 - sharedResource.push('프로세스 1: 아이템 1'); - await new Promise(resolve => setTimeout(resolve, 1000)); - console.log('프로세스 1: 작업 중...'); - sharedResource.push('프로세스 1: 아이템 2'); - await new Promise(resolve => setTimeout(resolve, 1000)); - sharedResource.push('프로세스 1: 아이템 3'); - - console.log('프로세스 1: 작업 완료'); - } catch (err) { - console.log('프로세스 1: 에러 발생 -', err.message); - } finally { - if (lockAcquired) { - console.log('프로세스 1: 락 해제'); - await locker.unlock(); - } - locker.destroy(); - } -} - -// 프로세스 2 시뮬레이션 -async function process2() { - const locker = FastLock.create({ useMock: true }); - let lockAcquired = false; - - console.log('프로세스 2: 시작...'); - try { - console.log('프로세스 2: 락 획득 시도...'); - await locker.lock('test-lock', 5000); - lockAcquired = true; - console.log('프로세스 2: 락 획득 성공!'); - - // 공유 리소스 작업 - sharedResource.push('프로세스 2: 아이템 1'); - await new Promise(resolve => setTimeout(resolve, 500)); - console.log('프로세스 2: 작업 중...'); - sharedResource.push('프로세스 2: 아이템 2'); - await new Promise(resolve => setTimeout(resolve, 500)); - sharedResource.push('프로세스 2: 아이템 3'); - - console.log('프로세스 2: 작업 완료'); - } catch (err) { - console.log('프로세스 2: 에러 발생 -', err.message); - } finally { - if (lockAcquired) { - console.log('프로세스 2: 락 해제'); - await locker.unlock(); - } - locker.destroy(); - } -} - -// 동시성 테스트 실행 -async function runConcurrencyTest() { - console.log('=== FastLock 동시성 테스트 시작 ===\n'); - - // 두 프로세스를 거의 동시에 시작 - const p1 = process1(); - const p2 = process2(); - - // 두 프로세스가 모두 완료될 때까지 대기 - await Promise.all([p1, p2]); - - console.log('\n최종 공유 리소스 상태:'); - console.log(sharedResource); - - // 결과 검증 - const process1Items = sharedResource.filter(item => item.startsWith('프로세스 1')); - const process2Items = sharedResource.filter(item => item.startsWith('프로세스 2')); - - console.log('\n=== 테스트 결과 ==='); - console.log('프로세스 1 아이템:', process1Items.length === 3 ? '완료' : '미완료'); - console.log('프로세스 2 아이템:', process2Items.length === 3 ? '완료' : '미완료'); - console.log('총 아이템 수:', sharedResource.length === 6 ? '정상' : '비정상'); - - console.log('\n=== FastLock 동시성 테스트 완료 ==='); -} - -// 테스트 실행 -runConcurrencyTest() - .catch(err => console.error('테스트 실행 중 에러 발생:', err)); \ No newline at end of file From 6e6949cb3f26b4597b8ddd5c9bd1b13079bebb99 Mon Sep 17 00:00:00 2001 From: skshim Date: Tue, 25 Mar 2025 15:19:35 +0900 Subject: [PATCH 3/4] fix lint --- mocking-test-fastlock.spec.js | 43 +++++++++++++++++++---------------- src/fast-lock.spec.ts | 12 +++++----- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/mocking-test-fastlock.spec.js b/mocking-test-fastlock.spec.js index a0333bf..0628fc5 100644 --- a/mocking-test-fastlock.spec.js +++ b/mocking-test-fastlock.spec.js @@ -7,7 +7,7 @@ const sharedResource = []; async function process1() { const locker = FastLock.create({ useMock: true }); let lockAcquired = false; - + console.log('프로세스 1: 시작...'); try { console.log('프로세스 1: 락 획득 시도...'); @@ -17,10 +17,10 @@ async function process1() { // 공유 리소스 작업 sharedResource.push('프로세스 1: 아이템 1'); - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); console.log('프로세스 1: 작업 중...'); sharedResource.push('프로세스 1: 아이템 2'); - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); sharedResource.push('프로세스 1: 아이템 3'); console.log('프로세스 1: 작업 완료'); @@ -39,7 +39,7 @@ async function process1() { async function process2() { const locker = FastLock.create({ useMock: true }); let lockAcquired = false; - + console.log('프로세스 2: 시작...'); try { console.log('프로세스 2: 락 획득 시도...'); @@ -49,10 +49,10 @@ async function process2() { // 공유 리소스 작업 sharedResource.push('프로세스 2: 아이템 1'); - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 500)); console.log('프로세스 2: 작업 중...'); sharedResource.push('프로세스 2: 아이템 2'); - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 500)); sharedResource.push('프로세스 2: 아이템 3'); console.log('프로세스 2: 작업 완료'); @@ -81,7 +81,7 @@ async function testLockExpiration() { console.log('락1 획득됨 (만료 시간: 1초)'); // 만료 시간보다 조금 더 대기 - await new Promise(resolve => setTimeout(resolve, 1100)); + await new Promise((resolve) => setTimeout(resolve, 1100)); console.log('1.1초 대기 완료'); // 두 번째 클라이언트가 락 획득 시도 @@ -122,7 +122,7 @@ async function testLockRetry() { retryCount: 3, retryDelay: 200, retryJitter: 100, - } + }, }); try { @@ -139,7 +139,9 @@ async function testLockRetry() { console.log('\n테스트 결과:', results); console.log('예상 결과:', ['첫 번째 락 획득', '재시도 실패 (예상된 결과)']); - console.log('테스트 ' + (results.join(',') === ['첫 번째 락 획득', '재시도 실패 (예상된 결과)'].join(',') ? '성공' : '실패')); + console.log( + '테스트 ' + (results.join(',') === ['첫 번째 락 획득', '재시도 실패 (예상된 결과)'].join(',') ? '성공' : '실패') + ); } catch (err) { console.error('테스트 중 에러 발생:', err); } finally { @@ -163,7 +165,7 @@ async function testNetworkDelay() { // 지연된 언락 시뮬레이션 const delayedUnlock = async () => { - await new Promise(resolve => setTimeout(resolve, 2500)); // 락 만료 시간보다 긴 지연 + await new Promise((resolve) => setTimeout(resolve, 2500)); // 락 만료 시간보다 긴 지연 try { await locker1.unlock(); results.push('지연된 언락 성공'); @@ -177,18 +179,21 @@ async function testNetworkDelay() { delayedUnlock(); // 지연 중 두 번째 클라이언트의 락 획득 시도 - await new Promise(resolve => setTimeout(resolve, 2200)); + await new Promise((resolve) => setTimeout(resolve, 2200)); await locker2.lock('delay-test-lock', 2000); results.push('두 번째 락 획득'); console.log('두 번째 락 획득됨 (첫 번째 락 만료 후)'); await locker2.unlock(); // 모든 작업 완료 대기 - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 500)); console.log('\n테스트 결과:', results); console.log('예상 결과:', ['첫 번째 락 획득', '두 번째 락 획득', '지연된 언락 실패']); - console.log('테스트 ' + (results.join(',') === ['첫 번째 락 획득', '두 번째 락 획득', '지연된 언락 실패'].join(',') ? '성공' : '실패')); + console.log( + '테스트 ' + + (results.join(',') === ['첫 번째 락 획득', '두 번째 락 획득', '지연된 언락 실패'].join(',') ? '성공' : '실패') + ); } catch (err) { console.error('테스트 중 에러 발생:', err); } finally { @@ -201,7 +206,7 @@ async function testNetworkDelay() { // 동시성 테스트 실행 async function runConcurrencyTest() { console.log('=== FastLock 동시성 테스트 시작 ===\n'); - + // 두 프로세스를 거의 동시에 시작 const p1 = process1(); const p2 = process2(); @@ -211,16 +216,16 @@ async function runConcurrencyTest() { console.log('\n최종 공유 리소스 상태:'); console.log(sharedResource); - + // 결과 검증 - const process1Items = sharedResource.filter(item => item.startsWith('프로세스 1')); - const process2Items = sharedResource.filter(item => item.startsWith('프로세스 2')); + const process1Items = sharedResource.filter((item) => item.startsWith('프로세스 1')); + const process2Items = sharedResource.filter((item) => item.startsWith('프로세스 2')); console.log('\n=== 테스트 결과 ==='); console.log('프로세스 1 아이템:', process1Items.length === 3 ? '완료' : '미완료'); console.log('프로세스 2 아이템:', process2Items.length === 3 ? '완료' : '미완료'); console.log('총 아이템 수:', sharedResource.length === 6 ? '정상' : '비정상'); - + console.log('\n=== FastLock 동시성 테스트 완료 ==='); } @@ -237,4 +242,4 @@ async function runAllTests() { } // 테스트 실행 -runAllTests(); \ No newline at end of file +runAllTests(); diff --git a/src/fast-lock.spec.ts b/src/fast-lock.spec.ts index 80a6dc1..929528f 100644 --- a/src/fast-lock.spec.ts +++ b/src/fast-lock.spec.ts @@ -93,15 +93,15 @@ describe('FastLock', () => { // 짧은 만료 시간으로 락 획득 await locker.lock(lockKey, 1000); testArray.push('acquired'); - + // 만료 시간보다 조금 더 대기 await sleep(1100); - + // 다른 클라이언트가 락을 획득할 수 있어야 함 await locker2.lock(lockKey, 1000); testArray.push('reacquired'); await locker2.unlock(); - + expect(testArray).toEqual(['acquired', 'reacquired']); } catch (err) { logger.error('%o', err); @@ -123,7 +123,7 @@ describe('FastLock', () => { retryDelay: 200, retryJitter: 100, }; - + const retryLocker = FastLock.create({ redis: { host: 'localhost', port: 6379, db: 0 }, redlock: retryConfig, @@ -137,7 +137,7 @@ describe('FastLock', () => { await locker.unlock(); retryLocker.destroy(); - + expect(testArray).toEqual(['first', 'retry-failed']); } catch (err) { logger.error('%o', err); @@ -173,7 +173,7 @@ describe('FastLock', () => { await locker2.unlock(); await sleep(500); // 지연된 언락이 완료될 때까지 대기 - + expect(testArray).toEqual(['acquired', 'reacquired', 'unlock-failed']); } catch (err) { logger.error('%o', err); From c3dab5f6524cd468951e5ce0b15a704b2fc1823a Mon Sep 17 00:00:00 2001 From: skshim Date: Tue, 25 Mar 2025 15:22:52 +0900 Subject: [PATCH 4/4] add comments --- example/example.ts | 100 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 81 insertions(+), 19 deletions(-) diff --git a/example/example.ts b/example/example.ts index c37b2ed..ecef963 100644 --- a/example/example.ts +++ b/example/example.ts @@ -1,30 +1,92 @@ import { LoggerFactory } from '@day1co/pebbles'; import { FastLock } from '../src/fast-lock'; +/** + * FastLock 라이브러리 사용 예제 + * + * 이 예제는 분산 락을 사용하여 공유 리소스(배열)에 대한 동시 접근을 제어하는 방법을 보여줍니다. + * 두 개의 프로세스가 동일한 리소스에 접근하려 할 때, FastLock이 어떻게 동작하는지 시연합니다. + */ + const logger = LoggerFactory.getLogger('fastlock:example'); -const sleep = async (ms) => new Promise((res) => setTimeout(res, ms)); +const sleep = async (ms: number) => new Promise((res) => setTimeout(res, ms)); + +// 공유 리소스로 사용될 배열 +const sharedArray: number[] = []; -const myArray = []; -const anotherProcess = async () => { +/** + * 두 번째 프로세스를 시뮬레이션하는 함수 + * 첫 번째 프로세스가 락을 보유하고 있는 동안 리소스 접근을 시도합니다. + * + * @returns Promise + */ +const simulateSecondProcess = async (): Promise => { const locker = FastLock.create({ redis: { host: 'localhost', port: 6379, db: 0 } }); - await locker.lock('locks:example', 2000); - myArray.push(4); - myArray.push(5); - myArray.push(6); - await sleep(2000); + try { + // 2초 동안 유효한 락 획득 시도 + await locker.lock('locks:example', 2000); + logger.debug('Second process: Lock acquired'); + + // 공유 리소스 수정 + sharedArray.push(4); + sharedArray.push(5); + sharedArray.push(6); + + // 작업 시뮬레이션을 위한 대기 + await sleep(2000); + + await locker.unlock(); + logger.debug('Second process: Lock released'); + } catch (err) { + logger.error('Second process error: %o', err); + } finally { + locker.destroy(); + } }; -const main = async () => { +/** + * 메인 프로세스 실행 함수 + * 첫 번째로 락을 획득하고, 작업 중에 다른 프로세스의 접근을 시도합니다. + * + * 시나리오: + * 1. 첫 번째 프로세스가 락을 획득하고 배열에 1,2,3을 추가 + * 2. 100ms 후 두 번째 프로세스가 시작되어 락 획득 시도 + * 3. 첫 번째 프로세스가 작업을 완료하고 락을 해제 + * 4. 두 번째 프로세스가 락을 획득하고 배열에 4,5,6을 추가 + * + * @returns Promise + */ +const demonstrateLocking = async (): Promise => { const locker = FastLock.create({ redis: { host: 'localhost', port: 6379, db: 0 } }); - await locker.lock('locks:example', 2000); - myArray.push(1); - myArray.push(2); - myArray.push(3); - setTimeout(async () => anotherProcess(), 100); - await sleep(2000); - await locker.unlock(); - locker.destroy(); - logger.debug('%o', myArray); + try { + // 2초 동안 유효한 락 획득 + await locker.lock('locks:example', 2000); + logger.debug('Main process: Lock acquired'); + + // 공유 리소스 수정 + sharedArray.push(1); + sharedArray.push(2); + sharedArray.push(3); + + // 두 번째 프로세스 시작 + setTimeout(async () => simulateSecondProcess(), 100); + + // 작업 시뮬레이션을 위한 대기 + await sleep(2000); + + await locker.unlock(); + logger.debug('Main process: Lock released'); + } catch (err) { + logger.error('Main process error: %o', err); + } finally { + locker.destroy(); + logger.debug('Final shared array state: %o', sharedArray); + } }; -main().then(console.info).catch(console.error); +// 예제 실행 +demonstrateLocking().then(() => { + logger.info('Example completed'); +}).catch(err => { + logger.error('Example failed: %o', err); +});