Skip to content

Commit 6b0fa77

Browse files
committed
デバッグログの追加およびテストの修正
1 parent 8b29eb9 commit 6b0fa77

3 files changed

Lines changed: 121 additions & 6 deletions

File tree

backend/balancing.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44
"index": 0,
55
"weight": 1,
66
"registerBlock": {
7-
"dateAfter": "2026-02-01"
7+
"dateBefore": "2026-02-01T00:00:00.000Z"
88
}
99
},
1010
{ "index": 1, "weight": 1 },
1111
{ "index": 2, "weight": 1 },
1212
{
1313
"index": 3,
14-
"weight": 3,
14+
"weight": 4,
1515
"migration": {
16-
"dateAfter": "2026/02/01",
16+
"dateAfter": "2026-02-01T00:00:00.000Z",
1717
"weight": 1
1818
}
1919
}

backend/src/lib/redis.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Redis from 'ioredis';
33
import { z } from 'zod';
44
import { dbEndpointRule } from '../common/environments.js';
55
import { logger } from '../common/logger.js';
6+
import type { DbEndpointRule } from '../common/dbEndpoint.schema.js';
67

78
const DEFAULT_TTL = 60 * 60 * 24 * 365; // 365日
89

@@ -31,10 +32,15 @@ class RedisClient {
3132
endpoint?: string;
3233
ttl?: number;
3334
dbIndex?: number;
35+
dbEndpoints?: string[];
36+
// allow external injection of dbEndpointRule for testing/override
37+
dbEndpointRule?: DbEndpointRule;
3438
} = {
3539
endpoint: undefined,
3640
ttl: undefined,
3741
dbIndex: undefined,
42+
dbEndpoints: dbEndpoints,
43+
dbEndpointRule: dbEndpointRule,
3844
},
3945
) {
4046
this.ttl = opt.ttl || DEFAULT_TTL;
@@ -45,7 +51,7 @@ class RedisClient {
4551
this.endpoint = opt.endpoint;
4652
// dbIndex は明示されていれば保持
4753
if (opt.dbIndex !== undefined) this.redisIndex = opt.dbIndex;
48-
logger.debug(`Using specified Redis endpoint: ${this.endpoint}`);
54+
logger.debug(`Using specified Redis endpoint`);
4955
return;
5056
}
5157

@@ -61,7 +67,9 @@ class RedisClient {
6167

6268
// dbEndpointRule が設定されている場合は配分ルールに従って選出
6369
try {
64-
const balancing = dbEndpointRule?.balancing;
70+
// Allow externally provided rule via constructor option; fallback to imported value
71+
const rule = opt.dbEndpointRule;
72+
const balancing = rule?.balancing;
6573
if (Array.isArray(balancing) && balancing.length > 0) {
6674
// ログ用の収集
6775
const invalidIndices: number[] = [];
@@ -231,6 +239,7 @@ class RedisClient {
231239
try {
232240
const body = Buffer.from(JSON.stringify(raw)).toString('base64');
233241
await client.set(key, body, 'EX', this.ttl);
242+
logger.debug(`addPage succeeded key=${key} dbIndex=${this.redisIndex}`);
234243
} catch (e: unknown) {
235244
if (this.isUpstashRateLimitError(e)) {
236245
logger.warn(
@@ -262,7 +271,7 @@ class RedisClient {
262271
try {
263272
const res = await client.get(key);
264273
if (!res) return undefined;
265-
274+
logger.debug(`getPage key=${key} dbIndex=${this.redisIndex}`);
266275
try {
267276
const json = Buffer.from(res, 'base64').toString('utf8');
268277
const parsed = ZodPageDb.parse(JSON.parse(json));
@@ -299,6 +308,7 @@ class RedisClient {
299308
const client = this.createClient();
300309
try {
301310
await client.del(key);
311+
logger.debug(`deletePage succeeded key=${key} dbIndex=${this.redisIndex}`);
302312
} catch (e: unknown) {
303313
if (this.isUpstashRateLimitError(e)) {
304314
logger.warn(

backend/test/spec/lib.redis.spec.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,109 @@ describe('RedisClient endpoint selection', () => {
8181
const c = new RedisClient();
8282
expect(c.redisIndex).toBe(1);
8383
});
84+
85+
it('distribution approximates weights over many samples', async () => {
86+
const endpoints = ['e0', 'e1', 'e2'];
87+
const rule = { balancing: [ { index: 0, weight: 1 }, { index: 1, weight: 2 }, { index: 2, weight: 7 } ] };
88+
89+
const mod = await resetEnvAndImport(endpoints, rule);
90+
const { RedisClient } = mod as any;
91+
92+
const trials = 2000;
93+
const counts: Record<number, number> = { 0: 0, 1: 0, 2: 0 };
94+
for (let i = 0; i < trials; i++) {
95+
const c = new RedisClient();
96+
counts[c.redisIndex] = (counts[c.redisIndex] ?? 0) + 1;
97+
}
98+
99+
const totalWeight = 1 + 2 + 7;
100+
const expected = { 0: 1 / totalWeight, 1: 2 / totalWeight, 2: 7 / totalWeight };
101+
for (const idx of [0, 1, 2]) {
102+
const obs = counts[idx] / trials;
103+
const relErr = Math.abs(obs - expected[idx]) / Math.max(expected[idx], 1e-9);
104+
expect(relErr).toBeLessThan(0.3);
105+
}
106+
});
107+
108+
it('registerBlock excludes entries from selection', async () => {
109+
const endpoints = ['e0', 'e1', 'e2'];
110+
const future = new Date(); future.setFullYear(future.getFullYear() + 10);
111+
const rule = { balancing: [ { index: 0, weight: 10, registerBlock: { dateBefore: future.toISOString() } }, { index: 1, weight: 1 }, { index: 2, weight: 1 } ] };
112+
113+
const mod = await resetEnvAndImport(endpoints, rule);
114+
const { RedisClient } = mod as any;
115+
116+
const trials = 300;
117+
for (let i = 0; i < trials; i++) {
118+
const c = new RedisClient();
119+
expect(c.redisIndex).not.toBe(0);
120+
}
121+
});
122+
123+
it('migration weight changes are applied according to dates', async () => {
124+
const endpoints = ['e0', 'e1', 'e2'];
125+
const past = new Date(); past.setFullYear(past.getFullYear() - 1);
126+
const rule = { balancing: [ { index: 0, weight: 1, migration: { dateAfter: past.toISOString(), weight: 20 } }, { index: 1, weight: 1 }, { index: 2, weight: 1 } ] };
127+
128+
const mod = await resetEnvAndImport(endpoints, rule);
129+
const { RedisClient } = mod as any;
130+
131+
const trials = 2000;
132+
const counts: Record<number, number> = { 0: 0, 1: 0, 2: 0 };
133+
for (let i = 0; i < trials; i++) {
134+
const c = new RedisClient();
135+
counts[c.redisIndex] = (counts[c.redisIndex] ?? 0) + 1;
136+
}
137+
138+
// index 0 should be selected far more often due to migration weight
139+
expect(counts[0]).toBeGreaterThan(counts[1] * 5);
140+
});
141+
142+
it('respects registerBlock.dateAfter (blocks when now >= dateAfter)', async () => {
143+
const endpoints = ['e0', 'e1'];
144+
const past = new Date();
145+
past.setFullYear(past.getFullYear() - 1);
146+
147+
const rule = {
148+
balancing: [
149+
{ index: 0, weight: 1, registerBlock: { dateAfter: past.toISOString() } },
150+
{ index: 1, weight: 1 },
151+
],
152+
};
153+
154+
const mod = await resetEnvAndImport(endpoints, rule);
155+
const { RedisClient } = mod as any;
156+
157+
const trials = 200;
158+
for (let i = 0; i < trials; i++) {
159+
const c = new RedisClient();
160+
expect(c.redisIndex).not.toBe(0);
161+
}
162+
});
163+
164+
it('applies migration.dateBefore weight when now < dateBefore', async () => {
165+
const endpoints = ['e0', 'e1', 'e2'];
166+
const future = new Date();
167+
future.setFullYear(future.getFullYear() + 1);
168+
169+
const rule = {
170+
balancing: [
171+
{ index: 0, weight: 1, migration: { dateBefore: future.toISOString(), weight: 20 } },
172+
{ index: 1, weight: 1 },
173+
{ index: 2, weight: 1 },
174+
],
175+
};
176+
177+
const mod = await resetEnvAndImport(endpoints, rule);
178+
const { RedisClient } = mod as any;
179+
180+
const trials = 2000;
181+
const counts: Record<number, number> = { 0: 0, 1: 0, 2: 0 };
182+
for (let i = 0; i < trials; i++) {
183+
const c = new RedisClient();
184+
counts[c.redisIndex] = (counts[c.redisIndex] ?? 0) + 1;
185+
}
186+
187+
expect(counts[0]).toBeGreaterThan(counts[1] * 5);
188+
});
84189
});

0 commit comments

Comments
 (0)