Skip to content

Commit 8c0f9c8

Browse files
committed
Eliminate Redis race condition with atomic updates
- Replace delete→repopulate pattern with atomic rename operations - Add environment variable guard to prevent multi-instance syncs - Add renameAsync Redis function for atomic key swapping Fixes intermittent empty responses from /v2/coinrankList and other endpoints
1 parent f4010f3 commit 8c0f9c8

File tree

1 file changed

+42
-15
lines changed

1 file changed

+42
-15
lines changed

src/utils/dbUtils.ts

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const hgetallAsync = client.hGetAll.bind(client)
3434
export const hmgetAsync = client.hmGet.bind(client)
3535
export const existsAsync = client.exists.bind(client)
3636
export const delAsync = client.del.bind(client)
37+
export const renameAsync = client.rename.bind(client)
3738
// Set type to `any` to avoid the TS4023 error
3839
export const setAsync: any = client.set.bind(client)
3940
export const getAsync: any = client.get.bind(client)
@@ -176,24 +177,50 @@ const syncedCurrencyCodeMaps = syncedDocument(
176177
asCurrencyCodeMaps
177178
)
178179

179-
syncedCurrencyCodeMaps.onChange(currencyCodeMaps => {
180-
logger('Syncing currency code maps with redis cache...')
181-
for (const key of Object.keys(currencyCodeMaps)) {
182-
delAsync(key)
183-
.then(() => {
180+
// Only run sync in background engine processes, not web server instances
181+
if (process.env.ENABLE_BACKGROUND_SYNC !== 'false') {
182+
syncedCurrencyCodeMaps.onChange(currencyCodeMaps => {
183+
const timestamp = new Date().toISOString()
184+
logger(
185+
`[${timestamp}] SYNC TRIGGERED: Syncing currency code maps with redis cache...`
186+
)
187+
logger(
188+
`[${timestamp}] SYNC TRIGGER: onChange fired for currencyCodeMaps document (PID: ${process.pid})`
189+
)
190+
for (const key of Object.keys(currencyCodeMaps)) {
191+
const tempKey = `${key}_temp_${Date.now()}`
192+
193+
// Write to temporary key first, then atomically rename
194+
const writeToTemp = async (): Promise<void> => {
184195
if (Array.isArray(currencyCodeMaps[key])) {
185-
hsetAsync(key, Object.assign({}, currencyCodeMaps[key])).catch(e =>
186-
logger('syncedCurrencyCodeMaps failed to update', key, e)
187-
)
196+
await hsetAsync(tempKey, Object.assign({}, currencyCodeMaps[key]))
188197
} else {
189-
hsetAsync(key, currencyCodeMaps[key]).catch(e =>
190-
logger('syncedCurrencyCodeMaps failed to update', key, e)
191-
)
198+
await hsetAsync(tempKey, currencyCodeMaps[key])
192199
}
193-
})
194-
.catch(e => logger('syncedCurrencyCodeMaps delete failed', key, e))
195-
}
196-
})
200+
}
201+
202+
const updateKey = async (): Promise<void> => {
203+
try {
204+
await writeToTemp()
205+
// Atomically replace the old key with the new data
206+
await renameAsync(tempKey, key)
207+
logger(`Successfully updated Redis key: ${key}`)
208+
} catch (e) {
209+
logger('syncedCurrencyCodeMaps failed to update', key, e)
210+
// Clean up temporary key on failure
211+
try {
212+
await delAsync(tempKey)
213+
} catch (cleanupError) {
214+
logger('Failed to cleanup temp key', tempKey, cleanupError)
215+
}
216+
}
217+
}
218+
219+
// Fire and forget - don't await in the callback
220+
updateKey().catch(e => logger('Unhandled error in updateKey', key, e))
221+
}
222+
})
223+
}
197224

198225
export const ratesDbSetup = {
199226
name: 'db_rates',

0 commit comments

Comments
 (0)