Skip to content

Commit d3264b8

Browse files
fix(billing): address code-quality findings on cron lock
- Move distributedLock unit test to canonical lib location (or document deviation) - Add releaseLock throw-path test + non-fatal comment in 3 crons' finally - Harmonize log style in skip-on-contention path - Rename + document integration test (in-process concurrency clarification) - Add findOne verification step in RUNBOOKS before deleteOne Addresses code-quality review I1, I2, M1, M3, M4.
1 parent 6aaeb21 commit d3264b8

6 files changed

Lines changed: 36 additions & 6 deletions

File tree

lib/services/tests/distributedLock.unit.tests.js renamed to lib/tests/distributedLock.unit.tests.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('distributedLock — acquireLock:', () => {
3636
},
3737
}));
3838

39-
({ acquireLock } = await import('../../distributedLock.js'));
39+
({ acquireLock } = await import('../distributedLock.js'));
4040
});
4141

4242
afterEach(() => {
@@ -121,7 +121,7 @@ describe('distributedLock — releaseLock:', () => {
121121
},
122122
}));
123123

124-
({ releaseLock } = await import('../../distributedLock.js'));
124+
({ releaseLock } = await import('../distributedLock.js'));
125125
});
126126

127127
afterEach(() => {
@@ -137,4 +137,10 @@ describe('distributedLock — releaseLock:', () => {
137137
test('does not throw when deleteOne resolves', async () => {
138138
await expect(releaseLock({ name: 'job-b', holder: 'pod-2' })).resolves.toBeUndefined();
139139
});
140+
141+
test('propagates deleteOne errors to the caller', async () => {
142+
const dbErr = new Error('network timeout');
143+
mockDeleteOne.mockRejectedValue(dbErr);
144+
await expect(releaseLock({ name: 'job-c', holder: 'pod-1' })).rejects.toThrow('network timeout');
145+
});
140146
});

modules/billing/RUNBOOKS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,16 @@ Operational runbooks for the billing module. Each runbook references real endpoi
188188

189189
**If urgent — drop the stale lock manually:**
190190

191+
**Before drop:** verify the holder and TTL window first to avoid kicking a running cron.
192+
193+
```js
194+
db.cron_locks.findOne({ _id: "billing.weeklyReset" })
195+
// If lockedUntil is in the past → safe to drop.
196+
// If in the future → the lock is genuinely held; wait for TTL unless the holder pod is confirmed dead.
197+
```
198+
199+
Then drop:
200+
191201
```js
192202
// weeklyReset
193203
db.cron_locks.deleteOne({ _id: "billing.weeklyReset" })

modules/billing/crons/billing.dunningSweep.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ try {
5656
lockHolder = `${process.env.HOSTNAME ?? 'unknown'}:${randomUUID()}`;
5757
const acquired = await acquireLock({ name: LOCK_NAME, ttlMs: LOCK_TTL_MS, holder: lockHolder });
5858
if (!acquired) {
59-
logger.info({ cron: LOCK_NAME }, 'lock held by another pod, skipping');
59+
logger.info('[cron.dunningSweep] lock held by another pod, skipping');
6060
process.exitCode = 0;
6161
} else {
6262
try {
@@ -111,6 +111,10 @@ try {
111111
logger.info('[cron.dunningSweep] complete', { processed, errors, desyncErrors, durationMs: Date.now() - startMs });
112112
process.exitCode = errors > 0 ? 1 : 0;
113113
} finally {
114+
// releaseLock failure is non-fatal: lock auto-expires on TTL.
115+
// If this throws, the outer catch logs "failed" and sets exitCode=1
116+
// (misleading — the cron's actual work succeeded). Operators can grep
117+
// for "failed to release lock" to distinguish.
114118
await releaseLock({ name: LOCK_NAME, holder: lockHolder });
115119
}
116120
}

modules/billing/crons/billing.extrasExpiration.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ try {
5151
lockHolder = `${process.env.HOSTNAME ?? 'unknown'}:${randomUUID()}`;
5252
const acquired = await acquireLock({ name: LOCK_NAME, ttlMs: LOCK_TTL_MS, holder: lockHolder });
5353
if (!acquired) {
54-
logger.info({ cron: LOCK_NAME }, 'lock held by another pod, skipping');
54+
logger.info('[cron.extrasExpiration] lock held by another pod, skipping');
5555
process.exitCode = 0;
5656
} else {
5757
try {
@@ -81,6 +81,10 @@ try {
8181
logger.info('[cron.extrasExpiration] complete', { processed, errors, durationMs: Date.now() - startMs });
8282
process.exitCode = errors > 0 ? 1 : 0;
8383
} finally {
84+
// releaseLock failure is non-fatal: lock auto-expires on TTL.
85+
// If this throws, the outer catch logs "failed" and sets exitCode=1
86+
// (misleading — the cron's actual work succeeded). Operators can grep
87+
// for "failed to release lock" to distinguish.
8488
await releaseLock({ name: LOCK_NAME, holder: lockHolder });
8589
}
8690
}

modules/billing/crons/billing.weeklyReset.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ try {
5151
lockHolder = `${process.env.HOSTNAME ?? 'unknown'}:${randomUUID()}`;
5252
const acquired = await acquireLock({ name: LOCK_NAME, ttlMs: LOCK_TTL_MS, holder: lockHolder });
5353
if (!acquired) {
54-
logger.info({ cron: LOCK_NAME }, 'lock held by another pod, skipping');
54+
logger.info('[cron.weeklyReset] lock held by another pod, skipping');
5555
process.exitCode = 0;
5656
} else {
5757
try {
@@ -61,6 +61,10 @@ try {
6161
logger.info('[cron.weeklyReset] complete', { processed: result.processed, errors: result.errors, durationMs: Date.now() - startMs });
6262
process.exitCode = result.errors > 0 ? 1 : 0;
6363
} finally {
64+
// releaseLock failure is non-fatal: lock auto-expires on TTL.
65+
// If this throws, the outer catch logs "failed" and sets exitCode=1
66+
// (misleading — the cron's actual work succeeded). Operators can grep
67+
// for "failed to release lock" to distinguish.
6468
await releaseLock({ name: LOCK_NAME, holder: lockHolder });
6569
}
6670
}

modules/billing/tests/billing.cron-lock.integration.tests.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ describe('distributedLock integration — acquire / release contract:', () => {
7171
expect(gone).toBeNull();
7272
});
7373

74-
test('concurrent acquires: only one pod wins (Promise.all race)', async () => {
74+
test('only one of N concurrent in-process acquires wins via E11000', async () => {
75+
// Node event loop: 3 Promise.all fires hit Mongo concurrently (not serialized by await).
76+
// Real inter-pod race uses the same E11000 path; this exercises the same mechanism.
7577
const results = await Promise.all([
7678
acquireLock({ name: 'billing.dunningSweep', ttlMs: 60_000, holder: 'pod-A' }),
7779
acquireLock({ name: 'billing.dunningSweep', ttlMs: 60_000, holder: 'pod-B' }),

0 commit comments

Comments
 (0)