Skip to content

Commit 2059725

Browse files
authored
Implement getMany for groupLoader (#272)
1 parent 61c266c commit 2059725

20 files changed

+463
-42
lines changed

lib/AbstractFlatCache.ts

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export abstract class AbstractFlatCache<LoadedValue, ResolveParams = undefined>
2424
}
2525

2626
public getManyInMemoryOnly(keys: string[]): GetManyResult<LoadedValue> {
27-
// ToDo no support for refresh, maybe later
27+
// Note that it doesn't support preemptive refresh
2828
return this.inMemoryCache.getMany(keys)
2929
}
3030

@@ -51,22 +51,22 @@ export abstract class AbstractFlatCache<LoadedValue, ResolveParams = undefined>
5151
return loadingPromise
5252
}
5353

54-
public getManyAsyncOnly(keys: string[], idResolver: IdResolver<LoadedValue>, resolveParams?: ResolveParams) {
55-
// ToDo There is currently no deduplication. What would be a way to implement it without destroying the perf?..
56-
54+
public getManyAsyncOnly(
55+
keys: string[],
56+
idResolver: IdResolver<LoadedValue>,
57+
resolveParams?: ResolveParams,
58+
): Promise<GetManyResult<LoadedValue>> {
59+
// This doesn't support deduplication, and never might, as that would affect perf strongly. Maybe as an opt-in option in the future?
5760
const loadingPromise = this.resolveManyValues(keys, idResolver, resolveParams)
5861

59-
loadingPromise
60-
.then((result) => {
61-
for (let i = 0; i < result.resolvedValues.length; i++) {
62-
const resolvedValue = result.resolvedValues[i]
63-
const id = idResolver(resolvedValue)
64-
this.inMemoryCache.set(id, resolvedValue)
65-
}
66-
})
67-
.catch(() => {})
68-
69-
return loadingPromise
62+
return loadingPromise.then((result) => {
63+
for (let i = 0; i < result.resolvedValues.length; i++) {
64+
const resolvedValue = result.resolvedValues[i]
65+
const id = idResolver(resolvedValue)
66+
this.inMemoryCache.set(id, resolvedValue)
67+
}
68+
return result
69+
})
7070
}
7171

7272
public get(key: string, resolveParams?: ResolveParams): Promise<LoadedValue | undefined | null> {
@@ -78,20 +78,22 @@ export abstract class AbstractFlatCache<LoadedValue, ResolveParams = undefined>
7878
return this.getAsyncOnly(key, resolveParams)
7979
}
8080

81-
public async getMany(
81+
public getMany(
8282
keys: string[],
8383
idResolver: IdResolver<LoadedValue>,
8484
resolveParams?: ResolveParams,
8585
): Promise<LoadedValue[]> {
8686
const inMemoryValues = this.getManyInMemoryOnly(keys)
8787
// everything is in memory, hurray
8888
if (inMemoryValues.unresolvedKeys.length === 0) {
89-
return inMemoryValues.resolvedValues
89+
return Promise.resolve(inMemoryValues.resolvedValues)
9090
}
9191

92-
const asyncRetrievedValues = await this.getManyAsyncOnly(inMemoryValues.unresolvedKeys, idResolver, resolveParams)
93-
94-
return [...inMemoryValues.resolvedValues, ...asyncRetrievedValues.resolvedValues]
92+
return this.getManyAsyncOnly(inMemoryValues.unresolvedKeys, idResolver, resolveParams).then(
93+
(asyncRetrievedValues) => {
94+
return [...inMemoryValues.resolvedValues, ...asyncRetrievedValues.resolvedValues]
95+
},
96+
)
9597
}
9698

9799
protected async resolveValue(key: string, _resolveParams?: ResolveParams): Promise<LoadedValue | undefined | null> {
@@ -112,7 +114,7 @@ export abstract class AbstractFlatCache<LoadedValue, ResolveParams = undefined>
112114
_resolveParams?: ResolveParams,
113115
): Promise<GetManyResult<LoadedValue>> {
114116
if (this.asyncCache) {
115-
return this.asyncCache.getManyCached(keys).catch((err) => {
117+
return this.asyncCache.getMany(keys).catch((err) => {
116118
this.loadErrorHandler(err, keys.toString(), this.asyncCache!, this.logger)
117119
return {
118120
unresolvedKeys: keys,

lib/AbstractGroupCache.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AbstractCache } from './AbstractCache'
2-
import type { GroupCache } from './types/DataSources'
3-
import type { SynchronousGroupCache } from './types/SyncDataSources'
2+
import type { GroupCache, IdResolver } from './types/DataSources'
3+
import type { GetManyResult, SynchronousGroupCache } from './types/SyncDataSources'
44
import type { InMemoryGroupCacheConfiguration } from './memory/InMemoryGroupCache'
55
import type { GroupNotificationPublisher } from './notifications/GroupNotificationPublisher'
66

@@ -47,6 +47,11 @@ export abstract class AbstractGroupCache<LoadedValue, ResolveParams = undefined>
4747
return this.inMemoryCache.getFromGroup(key, group)
4848
}
4949

50+
public getManyInMemoryOnly(keys: string[], group: string) {
51+
// Note that it doesn't support preemptive refresh
52+
return this.inMemoryCache.getManyFromGroup(keys, group)
53+
}
54+
5055
public getAsyncOnly(
5156
key: string,
5257
group: string,
@@ -75,6 +80,23 @@ export abstract class AbstractGroupCache<LoadedValue, ResolveParams = undefined>
7580
return loadingPromise
7681
}
7782

83+
public getManyAsyncOnly(
84+
keys: string[],
85+
group: string,
86+
idResolver: IdResolver<LoadedValue>,
87+
resolveParams?: ResolveParams,
88+
): Promise<GetManyResult<LoadedValue>> {
89+
// This doesn't support deduplication, and never might, as that would affect perf strongly. Maybe as an opt-in option in the future?
90+
return this.resolveManyGroupValues(keys, group, idResolver, resolveParams).then((result) => {
91+
for (let i = 0; i < result.resolvedValues.length; i++) {
92+
const resolvedValue = result.resolvedValues[i]
93+
const id = idResolver(resolvedValue)
94+
this.inMemoryCache.setForGroup(id, resolvedValue, group)
95+
}
96+
return result
97+
})
98+
}
99+
78100
public get(key: string, group: string, resolveParams?: ResolveParams): Promise<LoadedValue | undefined | null> {
79101
const inMemoryValue = this.getInMemoryOnly(key, group, resolveParams)
80102
if (inMemoryValue !== undefined) {
@@ -84,6 +106,25 @@ export abstract class AbstractGroupCache<LoadedValue, ResolveParams = undefined>
84106
return this.getAsyncOnly(key, group, resolveParams)
85107
}
86108

109+
public getMany(
110+
keys: string[],
111+
group: string,
112+
idResolver: IdResolver<LoadedValue>,
113+
resolveParams?: ResolveParams,
114+
): Promise<LoadedValue[]> {
115+
const inMemoryValues = this.getManyInMemoryOnly(keys, group)
116+
// everything is in memory, hurray
117+
if (inMemoryValues.unresolvedKeys.length === 0) {
118+
return Promise.resolve(inMemoryValues.resolvedValues)
119+
}
120+
121+
return this.getManyAsyncOnly(inMemoryValues.unresolvedKeys, group, idResolver, resolveParams).then(
122+
(asyncRetrievedValues) => {
123+
return [...inMemoryValues.resolvedValues, ...asyncRetrievedValues.resolvedValues]
124+
},
125+
)
126+
}
127+
87128
public async invalidateCacheFor(key: string, group: string) {
88129
this.inMemoryCache.deleteFromGroup(key, group)
89130
if (this.asyncCache) {
@@ -118,6 +159,27 @@ export abstract class AbstractGroupCache<LoadedValue, ResolveParams = undefined>
118159
return undefined
119160
}
120161

162+
protected async resolveManyGroupValues(
163+
keys: string[],
164+
group: string,
165+
_idResolver: IdResolver<LoadedValue>,
166+
_resolveParams?: ResolveParams,
167+
) {
168+
if (this.asyncCache) {
169+
return this.asyncCache.getManyFromGroup(keys, group).catch((err) => {
170+
this.loadErrorHandler(err, keys.toString(), this.asyncCache!, this.logger)
171+
return {
172+
unresolvedKeys: keys,
173+
resolvedValues: [],
174+
}
175+
})
176+
}
177+
return {
178+
unresolvedKeys: keys,
179+
resolvedValues: [],
180+
}
181+
}
182+
121183
protected resolveGroupLoads(group: string) {
122184
const load = this.runningLoads.get(group)
123185
if (load) {

lib/GroupLoader.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import type { GroupCache, GroupDataSource } from './types/DataSources'
1+
import type { GroupCache, GroupDataSource, IdResolver } from './types/DataSources'
22
import type { LoaderConfig } from './Loader'
33
import { AbstractGroupCache } from './AbstractGroupCache'
44
import type { InMemoryGroupCacheConfiguration, InMemoryGroupCache } from './memory/InMemoryGroupCache'
55
import type { GroupNotificationPublisher } from './notifications/GroupNotificationPublisher'
6+
import type { GetManyResult } from './types/SyncDataSources'
67

78
export type GroupLoaderConfig<LoadedValue, LoaderParams = undefined> = LoaderConfig<
89
LoadedValue,
@@ -79,6 +80,40 @@ export class GroupLoader<LoadedValue, LoaderParams = undefined> extends Abstract
7980
})
8081
}
8182

83+
protected override async resolveManyGroupValues(
84+
keys: string[],
85+
group: string,
86+
idResolver: IdResolver<LoadedValue>,
87+
loadParams?: LoaderParams,
88+
): Promise<GetManyResult<LoadedValue>> {
89+
// load what is available from async cache
90+
const cachedValues = await super.resolveManyGroupValues(keys, group, idResolver, loadParams)
91+
92+
// everything was cached, no need to load anything
93+
if (cachedValues.unresolvedKeys.length === 0) {
94+
return cachedValues
95+
}
96+
97+
const loadValues = await this.loadManyFromLoaders(cachedValues.unresolvedKeys, group, loadParams)
98+
99+
if (this.asyncCache) {
100+
for (let i = 0; i < loadValues.length; i++) {
101+
const resolvedValue = loadValues[i]
102+
const id = idResolver(resolvedValue)
103+
await this.asyncCache.setForGroup(id, resolvedValue, group).catch((err) => {
104+
this.cacheUpdateErrorHandler(err, id, this.asyncCache!, this.logger)
105+
})
106+
}
107+
}
108+
109+
return {
110+
resolvedValues: [...cachedValues.resolvedValues, ...loadValues],
111+
112+
// there actually may still be some unresolved keys, but we no longer know that
113+
unresolvedKeys: [],
114+
}
115+
}
116+
82117
private async loadFromLoaders(key: string, group: string, loadParams?: LoaderParams) {
83118
for (let index = 0; index < this.dataSources.length; index++) {
84119
const resolvedValue = await this.dataSources[index].getFromGroup(key, group, loadParams).catch((err) => {
@@ -104,4 +139,28 @@ export class GroupLoader<LoadedValue, LoaderParams = undefined> extends Abstract
104139

105140
return undefined
106141
}
142+
143+
private async loadManyFromLoaders(keys: string[], group: string, loadParams?: LoaderParams) {
144+
let lastResolvedValues
145+
for (let index = 0; index < this.dataSources.length; index++) {
146+
lastResolvedValues = await this.dataSources[index].getManyFromGroup(keys, group, loadParams).catch((err) => {
147+
this.loadErrorHandler(err, keys.toString(), this.dataSources[index], this.logger)
148+
if (this.throwIfLoadError) {
149+
throw err
150+
}
151+
return [] as LoadedValue[]
152+
})
153+
154+
if (lastResolvedValues.length === keys.length) {
155+
return lastResolvedValues
156+
}
157+
}
158+
159+
if (this.throwIfUnresolved) {
160+
throw new Error(`Failed to resolve value for some of the keys (group ${group}): ${keys.join(', ')}`)
161+
}
162+
163+
// ToDo do we want to return results of a query that returned the most amount of entities?
164+
return lastResolvedValues ?? []
165+
}
107166
}

lib/Loader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export class Loader<LoadedValue, LoaderParams = undefined> extends AbstractFlatC
108108
return undefined
109109
}
110110

111-
protected async resolveManyValues(
111+
protected override async resolveManyValues(
112112
keys: string[],
113113
idResolver: IdResolver<LoadedValue>,
114114
loadParams?: LoaderParams,

lib/redis/RedisCache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class RedisCache<T> extends AbstractRedisCache<RedisCacheConfiguration, T
4141
})
4242
}
4343

44-
getManyCached(keys: string[]): Promise<GetManyResult<T>> {
44+
getMany(keys: string[]): Promise<GetManyResult<T>> {
4545
const transformedKeys = keys.map((entry) => this.resolveKey(entry))
4646
const resolvedValues: T[] = []
4747
const unresolvedKeys: string[] = []

lib/redis/RedisExpirationTimeGroupDataSource.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,9 @@ export class RedisExpirationTimeGroupDataSource implements GroupDataSource<numbe
1111
getFromGroup(key: string, group: string): Promise<number | undefined> {
1212
return this.parentAsyncCache.getExpirationTimeFromGroup(key, group)
1313
}
14+
15+
/* c8 ignore next 3 */
16+
getManyFromGroup(): Promise<number[]> {
17+
throw new Error('Not supported')
18+
}
1419
}

lib/redis/RedisGroupCache.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { GroupCache, GroupCacheConfiguration, GroupDataSource } from '../types/DataSources'
1+
import type { GroupCache, GroupCacheConfiguration } from '../types/DataSources'
22
import type Redis from 'ioredis'
33
import { GET_OR_SET_ZERO_WITH_TTL, GET_OR_SET_ZERO_WITHOUT_TTL } from './lua'
44
import { GroupLoader } from '../GroupLoader'
@@ -13,10 +13,7 @@ export interface RedisGroupCacheConfiguration extends RedisCacheConfiguration, G
1313
groupTtlInMsecs?: number
1414
}
1515

16-
export class RedisGroupCache<T>
17-
extends AbstractRedisCache<RedisGroupCacheConfiguration, T>
18-
implements GroupCache<T>, GroupDataSource<T>
19-
{
16+
export class RedisGroupCache<T> extends AbstractRedisCache<RedisGroupCacheConfiguration, T> implements GroupCache<T> {
2017
public readonly expirationTimeLoadingGroupedOperation: GroupLoader<number>
2118
public ttlLeftBeforeRefreshInMsecs?: number
2219
name = 'Redis group cache'

lib/types/DataSources.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export interface Cache<LoadedValue> extends WriteCache<LoadedValue> {
2626
readonly ttlLeftBeforeRefreshInMsecs?: number
2727
readonly expirationTimeLoadingOperation: Loader<number>
2828
get: (key: string) => Promise<LoadedValue | undefined | null>
29-
getManyCached: (keys: string[]) => Promise<GetManyResult<LoadedValue>>
29+
getMany: (keys: string[]) => Promise<GetManyResult<LoadedValue>>
3030
getExpirationTime: (key: string) => Promise<number | undefined>
3131
}
3232

@@ -56,6 +56,7 @@ export interface DataSource<LoadedValue, LoadParams = undefined> {
5656

5757
export interface GroupDataSource<LoadedValue, LoadParams = undefined> {
5858
getFromGroup: (key: string, group: string, loadParams?: LoadParams) => Promise<LoadedValue | undefined | null>
59+
getManyFromGroup: (keys: string[], group: string, loadParams?: LoadParams) => Promise<LoadedValue[]>
5960

6061
name: string
6162
}

0 commit comments

Comments
 (0)