Skip to content

Commit 2c24b7d

Browse files
OPDATA-6402 update comparison in CompositeTransport CompareResponseCache (#823)
* OPDATA-6402 update comparison in CompositeTransport CompareResponseCache * rework placing local cache staleness check inside the CompareResponseCache and leaving shouldUpdate as-is * remove localCache from CompareResponseCache, remove most of the updated code, falls back to responseCache/redis * caps * lint
1 parent 436db7a commit 2c24b7d

4 files changed

Lines changed: 57 additions & 20 deletions

File tree

src/cache/response-cache/compare.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ export class CompareResponseCache<
1414
readonly transportName: string
1515
// The actual cache where responses are written to
1616
responseCache: ResponseCache<T>
17-
// A local map to keep track of the most recent entries written to the responseCache
18-
// We compare with this first before comparing with value in cache
19-
// so that we can reduce cache reads
20-
localCache: Map<string, AdapterResponse<T['Response']>>
2117
// True if next should replace current in cache
2218
shouldUpdate: (
2319
next: AdapterResponse<T['Response']>,
@@ -41,7 +37,6 @@ export class CompareResponseCache<
4137
})
4238
this.transportName = transportName
4339
this.responseCache = responseCache
44-
this.localCache = new Map()
4540
this.shouldUpdate = shouldUpdate
4641
}
4742

@@ -60,11 +55,8 @@ export class CompareResponseCache<
6055
const filteredEntries = (
6156
await Promise.all(
6257
entries.flatMap(async ({ key, value }) => {
63-
if (!this.shouldUpdate(value, this.localCache.get(key))) {
64-
return []
65-
}
66-
const entryInCache = await this.get(key)
67-
if (!this.shouldUpdate(value, entryInCache)) {
58+
const current = await this.get(key)
59+
if (!this.shouldUpdate(value, current)) {
6860
return []
6961
}
7062
return [{ key, value }]
@@ -74,10 +66,6 @@ export class CompareResponseCache<
7466

7567
if (filteredEntries.length > 0) {
7668
await this.responseCache.writeEntries(filteredEntries)
77-
78-
filteredEntries.forEach(({ key, value }) => {
79-
this.localCache.set(key, value)
80-
})
8169
}
8270
}
8371

src/transports/composite.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export class CompositeTransport<T extends TransportGenerics> implements Transpor
2828
transportName,
2929
this.responseCache,
3030
(next, current) =>
31+
// Write if the incoming value has a newer provider timestamp
32+
// Also writes if the previous cache entry is stale/expired
3133
(next.timestamps?.providerIndicatedTimeUnixMs ?? 0) >
3234
(current?.timestamps?.providerIndicatedTimeUnixMs ?? 0),
3335
)
@@ -61,6 +63,10 @@ export class CompositeTransport<T extends TransportGenerics> implements Transpor
6163

6264
async backgroundExecute(context: EndpointContext<T>): Promise<void> {
6365
const entries = Object.entries(this.transports)
66+
67+
// Note that this will wait for the slowest transport to resolve before completing
68+
// this shared backgroundExecute loop and stalling the faster transport(s).
69+
// Consider setting lower timeouts on the individual transports if this becomes an issue.
6470
const results = await Promise.allSettled(entries.map(([, t]) => t.backgroundExecute?.(context)))
6571

6672
results.forEach((r, i) => {

test/cache/response-cache/compare.test.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ test('writes under CompareResponseCache transportName', async (t) => {
5757
t.is(entry?.meta?.transportName, 'ws')
5858
})
5959

60-
test('second write override first write', async (t) => {
60+
test('second write overrides first write', async (t) => {
6161
const compareCache = new CompareResponseCache('merged', buildSimpleCache(), () => true)
6262

6363
const params = { base: 'ETH', factor: 1 }
@@ -81,12 +81,10 @@ test('shouldUpdate can block write when new value is not fresher than cache', as
8181

8282
await compareCache.write('merged', [providerResult(params, 25)])
8383
t.is((await compareCache.get(compareCache.getCacheKey('merged', params)))?.result, 50)
84-
t.is(compareCache.localCache.size, 1)
8584
})
8685

87-
test('shouldUpdate can block write without old value in localCache', async (t) => {
86+
test('shouldUpdate checks the backing cache when no prior write through compareCache', async (t) => {
8887
const simpleCache = buildSimpleCache()
89-
9088
const compareCache = new CompareResponseCache(
9189
'merged',
9290
simpleCache,
@@ -95,10 +93,11 @@ test('shouldUpdate can block write without old value in localCache', async (t) =
9593

9694
const params = { base: 'ETH', factor: 1 }
9795

96+
// Write directly to the backing cache, bypassing compareCache
9897
await simpleCache.write('merged', [providerResult(params, 100)])
9998
t.is((await compareCache.get(compareCache.getCacheKey('merged', params)))?.result, 100)
10099

100+
// Sees the backing cache value and blocks the lower write
101101
await compareCache.write('merged', [providerResult(params, 25)])
102102
t.is((await compareCache.get(compareCache.getCacheKey('merged', params)))?.result, 100)
103-
t.is(compareCache.localCache.size, 0)
104103
})

test/transports/composite.test.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ test.serial(
140140
)
141141

142142
test.serial(
143-
'composite transport merges child writes by providerIndicatedTimeUnixMs when run under an adapter',
143+
'composite transport merges child writes by providerIndicatedTimeUnixMs when run under an adapter, takes newest timestamped value',
144144
async (t) => {
145145
axiosMock
146146
.onGet(`${WS_PROVIDER}/price`, { params: { base: 'BTC', factor: 3 } })
@@ -161,6 +161,50 @@ test.serial(
161161
},
162162
)
163163

164+
test.serial(
165+
'composite transport refreshes TTL when both transports return same value at same timestamp',
166+
async (t) => {
167+
axiosMock
168+
.onGet(`${WS_PROVIDER}/price`, { params: { base: 'XRP', factor: 1 } })
169+
.reply(200, { result: 42, ts: 100 })
170+
axiosMock
171+
.onGet(`${REST_PROVIDER}/price`, { params: { base: 'XRP', factor: 1 } })
172+
.reply(200, { result: 42, ts: 100 })
173+
174+
const res = await t.context.testAdapter.request({ base: 'XRP', factor: 1 })
175+
176+
// How to test whether the params were refreshed in the cache?
177+
// we can check that the response is correct and then check that the ws transport was called again
178+
// on a subsequent request, indicating that the cache entry was refreshed and the ws transport was used again instead of the rest transport
179+
180+
t.is(res.statusCode, 200)
181+
t.is(res.json().result, 42)
182+
},
183+
)
184+
185+
test.serial(
186+
'composite transport does not rubberband when transports return conflicting values at same timestamp',
187+
async (t) => {
188+
// Send first request
189+
axiosMock
190+
.onGet(`${WS_PROVIDER}/price`, { params: { base: 'ABC', factor: 1 } })
191+
.reply(200, { result: 10, ts: 200 })
192+
193+
const res = await t.context.testAdapter.request({ base: 'ABC', factor: 1 })
194+
t.is(res.statusCode, 200)
195+
t.is(res.json().result, 10)
196+
197+
// Send second request with same timestamp but different value
198+
axiosMock
199+
.onGet(`${REST_PROVIDER}/price`, { params: { base: 'ABC', factor: 1 } })
200+
.reply(200, { result: 99, ts: 200 })
201+
202+
const res2 = await t.context.testAdapter.request({ base: 'ABC', factor: 1 })
203+
t.is(res2.statusCode, 200)
204+
t.is(res2.json().result, 10)
205+
},
206+
)
207+
164208
class ThrowingTransport<T extends TransportGenerics> implements Transport<T> {
165209
name!: string
166210
responseCache!: ResponseCache<T>

0 commit comments

Comments
 (0)