Skip to content

Commit dab4c15

Browse files
committed
fixup! Eliminate Redis race condition with atomic updates
1 parent 8c0f9c8 commit dab4c15

File tree

3 files changed

+80
-81
lines changed

3 files changed

+80
-81
lines changed

src/indexEngines.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,26 @@ import { coinrankEngine } from './coinrankEngine'
44
import { config } from './config'
55
import { ratesEngine } from './ratesEngine'
66
import { uidEngine } from './uidEngine'
7+
import {
8+
setupCurrencyCodeMapsSync,
9+
syncedCurrencyCodeMaps
10+
} from './utils/currencyCodeMapsSync'
711
import { ratesDbSetup } from './utils/dbUtils'
812
import { logger } from './utils/utils'
913

10-
// Initialize DB
14+
// Initialize DB and currency code maps sync
1115
async function initDb(): Promise<void> {
12-
await setupDatabase(config.couchUri, ratesDbSetup)
16+
// Create a new ratesDbSetup that includes the syncedCurrencyCodeMaps from the new sync module
17+
const indexEnginesRatesDbSetup = {
18+
...ratesDbSetup,
19+
syncedDocuments: [syncedCurrencyCodeMaps]
20+
}
21+
22+
await setupDatabase(config.couchUri, indexEnginesRatesDbSetup)
23+
24+
// Set up currency code maps sync (only runs in this process)
25+
setupCurrencyCodeMapsSync()
26+
1327
ratesEngine().catch(e => logger('ratesEngine failure', e))
1428
uidEngine().catch(e => logger('uidEngine failure', e))
1529
coinrankEngine().catch(e => logger('coinrankEngine failure', e))

src/utils/currencyCodeMapsSync.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { asArray, asMaybe, asObject, asString } from 'cleaners'
2+
import { syncedDocument } from 'edge-server-tools'
3+
4+
import currencyCodeMaps from './currencyCodeMaps.json'
5+
import { hsetAsync } from './dbUtils'
6+
import { logger } from './utils'
7+
8+
const asCurrencyCodeMapsCleaner = asObject({
9+
constantCurrencyCodes: asMaybe(asObject(asString)),
10+
zeroRates: asMaybe(asObject(asString)),
11+
fallbackConstantRates: asMaybe(asObject(asString)),
12+
coinMarketCap: asMaybe(asObject(asString)),
13+
coincap: asMaybe(asObject(asString)),
14+
coingecko: asMaybe(asObject(asString)),
15+
allEdgeCurrencies: asMaybe(asArray(asString)),
16+
fiatCurrencyCodes: asMaybe(asArray(asString))
17+
})
18+
19+
// Pass the defaults json through the cleaner so they're typed
20+
const defaultCurrencyCodeMaps = asCurrencyCodeMapsCleaner(currencyCodeMaps)
21+
22+
const asCurrencyCodeMaps = asMaybe(
23+
asCurrencyCodeMapsCleaner,
24+
defaultCurrencyCodeMaps
25+
)
26+
27+
export const syncedCurrencyCodeMaps = syncedDocument(
28+
'currencyCodeMaps',
29+
asCurrencyCodeMaps
30+
)
31+
32+
// Set up the sync logic - this will only run in the indexEngines process
33+
export const setupCurrencyCodeMapsSync = (): void => {
34+
syncedCurrencyCodeMaps.onChange(currencyCodeMaps => {
35+
const timestamp = new Date().toISOString()
36+
logger(
37+
`[${timestamp}] SYNC TRIGGERED: Syncing currency code maps with redis cache...`
38+
)
39+
logger(
40+
`[${timestamp}] SYNC TRIGGER: onChange fired for currencyCodeMaps document (PID: ${process.pid})`
41+
)
42+
43+
for (const key of Object.keys(currencyCodeMaps)) {
44+
const updateKey = async (): Promise<void> => {
45+
try {
46+
// Use hset directly as suggested by peachbits - simpler and avoids race conditions
47+
if (Array.isArray(currencyCodeMaps[key])) {
48+
await hsetAsync(key, Object.assign({}, currencyCodeMaps[key]))
49+
} else {
50+
await hsetAsync(key, currencyCodeMaps[key])
51+
}
52+
logger(`Successfully updated Redis key: ${key}`)
53+
} catch (e) {
54+
logger('currencyCodeMaps sync failed to update', key, e)
55+
}
56+
}
57+
58+
// Fire and forget - don't await in the callback
59+
updateKey().catch(e => logger('Unhandled error in updateKey', key, e))
60+
}
61+
})
62+
}

src/utils/dbUtils.ts

Lines changed: 2 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
import {
2-
asArray,
3-
asBoolean,
4-
asMaybe,
5-
asObject,
6-
asOptional,
7-
asString
8-
} from 'cleaners'
9-
import { syncedDocument } from 'edge-server-tools'
1+
import { asBoolean, asMaybe, asObject, asOptional, asString } from 'cleaners'
102
import nano from 'nano'
113
import promisify from 'promisify-node'
124
import { createClient } from 'redis'
@@ -153,80 +145,11 @@ export const getFromDb = async (
153145
export const wrappedGetFromDb = async (dates: string[]): Promise<DbDoc[]> =>
154146
getFromDb(dbRates, dates)
155147

156-
const asCurrencyCodeMapsCleaner = asObject({
157-
constantCurrencyCodes: asMaybe(asObject(asString)),
158-
zeroRates: asMaybe(asObject(asString)),
159-
fallbackConstantRates: asMaybe(asObject(asString)),
160-
coinMarketCap: asMaybe(asObject(asString)),
161-
coincap: asMaybe(asObject(asString)),
162-
coingecko: asMaybe(asObject(asString)),
163-
allEdgeCurrencies: asMaybe(asArray(asString)),
164-
fiatCurrencyCodes: asMaybe(asArray(asString))
165-
})
166-
167-
// Pass the defaults json through the cleaner so they're typed
168-
const defaultCurrencyCodeMaps = asCurrencyCodeMapsCleaner(currencyCodeMaps)
169-
170-
const asCurrencyCodeMaps = asMaybe(
171-
asCurrencyCodeMapsCleaner,
172-
defaultCurrencyCodeMaps
173-
)
174-
175-
const syncedCurrencyCodeMaps = syncedDocument(
176-
'currencyCodeMaps',
177-
asCurrencyCodeMaps
178-
)
179-
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> => {
195-
if (Array.isArray(currencyCodeMaps[key])) {
196-
await hsetAsync(tempKey, Object.assign({}, currencyCodeMaps[key]))
197-
} else {
198-
await hsetAsync(tempKey, currencyCodeMaps[key])
199-
}
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-
}
224-
225148
export const ratesDbSetup = {
226149
name: 'db_rates',
227150
options: { partitioned: false },
228151
templates: { currencyCodeMaps },
229-
syncedDocuments: [syncedCurrencyCodeMaps]
152+
syncedDocuments: [] // Empty array since sync is now handled in indexEngines
230153
}
231154

232155
export const getEdgeAssetDoc = memoize(

0 commit comments

Comments
 (0)