Skip to content

Commit 54609fd

Browse files
Update version to 5.5.3 and add retry utilities for handling eventual consistency in API calls
1 parent cd29ab7 commit 54609fd

3 files changed

Lines changed: 95 additions & 8 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "solapi",
3-
"version": "5.5.2",
3+
"version": "5.5.3",
44
"description": "SOLAPI SDK for Node.js(Server Side Only)",
55
"keywords": [
66
"solapi",

test/lib/retry-utils.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* 테스트용 재시도 유틸리티
3+
* API eventual consistency 문제를 해결하기 위한 재시도 로직 제공
4+
*/
5+
6+
interface RetryOptions {
7+
maxRetries?: number;
8+
initialDelayMs?: number;
9+
maxDelayMs?: number;
10+
backoffMultiplier?: number;
11+
}
12+
13+
const DEFAULT_OPTIONS: Required<RetryOptions> = {
14+
maxRetries: 5,
15+
initialDelayMs: 100,
16+
maxDelayMs: 2000,
17+
backoffMultiplier: 2,
18+
};
19+
20+
const sleep = (ms: number): Promise<void> =>
21+
new Promise(resolve => setTimeout(resolve, ms));
22+
23+
/**
24+
* 지수 백오프를 사용한 재시도 함수
25+
* @param fn 실행할 비동기 함수
26+
* @param options 재시도 옵션
27+
* @returns 함수 실행 결과
28+
*/
29+
export async function retryWithBackoff<T>(
30+
fn: () => Promise<T>,
31+
options: RetryOptions = {},
32+
): Promise<T> {
33+
const opts = {...DEFAULT_OPTIONS, ...options};
34+
let lastError: Error | undefined;
35+
let delay = opts.initialDelayMs;
36+
37+
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
38+
try {
39+
return await fn();
40+
} catch (error) {
41+
lastError = error instanceof Error ? error : new Error(String(error));
42+
43+
if (attempt < opts.maxRetries) {
44+
await sleep(delay);
45+
delay = Math.min(delay * opts.backoffMultiplier, opts.maxDelayMs);
46+
}
47+
}
48+
}
49+
50+
throw lastError;
51+
}
52+
53+
/**
54+
* 특정 조건이 충족될 때까지 재시도
55+
* @param fn 실행할 비동기 함수
56+
* @param predicate 조건 검사 함수
57+
* @param options 재시도 옵션
58+
* @returns 조건을 충족한 결과
59+
*/
60+
export async function retryUntil<T>(
61+
fn: () => Promise<T>,
62+
predicate: (result: T) => boolean,
63+
options: RetryOptions = {},
64+
): Promise<T> {
65+
const opts = {...DEFAULT_OPTIONS, ...options};
66+
let delay = opts.initialDelayMs;
67+
68+
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
69+
const result = await fn();
70+
71+
if (predicate(result)) {
72+
return result;
73+
}
74+
75+
if (attempt < opts.maxRetries) {
76+
await sleep(delay);
77+
delay = Math.min(delay * opts.backoffMultiplier, opts.maxDelayMs);
78+
}
79+
}
80+
81+
throw new Error(`Condition not met after ${opts.maxRetries + 1} attempts`);
82+
}

test/services/messages/groupService.e2e.test.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {RequestSendOneMessageSchema} from '@models/requests/messages/sendMessage';
22
import {beforeAll, describe, expect, it} from 'vitest';
33
import GroupService from '@/services/messages/groupService';
4+
import {retryWithBackoff} from '../../lib/retry-utils';
45

56
describe('GroupService E2E', () => {
67
let groupService: GroupService;
@@ -44,17 +45,21 @@ describe('GroupService E2E', () => {
4445
};
4546
await groupService.addMessagesToGroup(groupId, message);
4647

47-
// 3. Get the group info
48-
const groupInfo = await groupService.getGroup(groupId);
48+
// 3. Get the group info (retry to handle eventual consistency)
49+
const groupInfo = await retryWithBackoff(
50+
() => groupService.getGroup(groupId),
51+
{maxRetries: 5, initialDelayMs: 200},
52+
);
4953
expect(groupInfo.count.total).toBe(1);
5054

5155
// 4. Delete the group
5256
await groupService.removeGroup(groupId);
5357

54-
// 5. Verify the group is deleted
55-
const removedGroupStatus = await groupService
56-
.getGroup(groupId)
57-
.then(res => res.status);
58-
await expect(removedGroupStatus).toBe('DELETED');
58+
// 5. Verify the group is deleted (retry to handle eventual consistency)
59+
const removedGroupInfo = await retryWithBackoff(
60+
() => groupService.getGroup(groupId),
61+
{maxRetries: 5, initialDelayMs: 200},
62+
);
63+
expect(removedGroupInfo.status).toBe('DELETED');
5964
});
6065
});

0 commit comments

Comments
 (0)