Skip to content

Commit

Permalink
Implement getMany for groupLoader (#272)
Browse files Browse the repository at this point in the history
  • Loading branch information
kibertoad authored Sep 10, 2023
1 parent 61c266c commit 2059725
Show file tree
Hide file tree
Showing 20 changed files with 463 additions and 42 deletions.
44 changes: 23 additions & 21 deletions lib/AbstractFlatCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export abstract class AbstractFlatCache<LoadedValue, ResolveParams = undefined>
}

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

Expand All @@ -51,22 +51,22 @@ export abstract class AbstractFlatCache<LoadedValue, ResolveParams = undefined>
return loadingPromise
}

public getManyAsyncOnly(keys: string[], idResolver: IdResolver<LoadedValue>, resolveParams?: ResolveParams) {
// ToDo There is currently no deduplication. What would be a way to implement it without destroying the perf?..

public getManyAsyncOnly(
keys: string[],
idResolver: IdResolver<LoadedValue>,
resolveParams?: ResolveParams,
): Promise<GetManyResult<LoadedValue>> {
// This doesn't support deduplication, and never might, as that would affect perf strongly. Maybe as an opt-in option in the future?
const loadingPromise = this.resolveManyValues(keys, idResolver, resolveParams)

loadingPromise
.then((result) => {
for (let i = 0; i < result.resolvedValues.length; i++) {
const resolvedValue = result.resolvedValues[i]
const id = idResolver(resolvedValue)
this.inMemoryCache.set(id, resolvedValue)
}
})
.catch(() => {})

return loadingPromise
return loadingPromise.then((result) => {
for (let i = 0; i < result.resolvedValues.length; i++) {
const resolvedValue = result.resolvedValues[i]
const id = idResolver(resolvedValue)
this.inMemoryCache.set(id, resolvedValue)
}
return result
})
}

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

public async getMany(
public getMany(
keys: string[],
idResolver: IdResolver<LoadedValue>,
resolveParams?: ResolveParams,
): Promise<LoadedValue[]> {
const inMemoryValues = this.getManyInMemoryOnly(keys)
// everything is in memory, hurray
if (inMemoryValues.unresolvedKeys.length === 0) {
return inMemoryValues.resolvedValues
return Promise.resolve(inMemoryValues.resolvedValues)
}

const asyncRetrievedValues = await this.getManyAsyncOnly(inMemoryValues.unresolvedKeys, idResolver, resolveParams)

return [...inMemoryValues.resolvedValues, ...asyncRetrievedValues.resolvedValues]
return this.getManyAsyncOnly(inMemoryValues.unresolvedKeys, idResolver, resolveParams).then(
(asyncRetrievedValues) => {
return [...inMemoryValues.resolvedValues, ...asyncRetrievedValues.resolvedValues]
},
)
}

protected async resolveValue(key: string, _resolveParams?: ResolveParams): Promise<LoadedValue | undefined | null> {
Expand All @@ -112,7 +114,7 @@ export abstract class AbstractFlatCache<LoadedValue, ResolveParams = undefined>
_resolveParams?: ResolveParams,
): Promise<GetManyResult<LoadedValue>> {
if (this.asyncCache) {
return this.asyncCache.getManyCached(keys).catch((err) => {
return this.asyncCache.getMany(keys).catch((err) => {
this.loadErrorHandler(err, keys.toString(), this.asyncCache!, this.logger)
return {
unresolvedKeys: keys,
Expand Down
66 changes: 64 additions & 2 deletions lib/AbstractGroupCache.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AbstractCache } from './AbstractCache'
import type { GroupCache } from './types/DataSources'
import type { SynchronousGroupCache } from './types/SyncDataSources'
import type { GroupCache, IdResolver } from './types/DataSources'
import type { GetManyResult, SynchronousGroupCache } from './types/SyncDataSources'
import type { InMemoryGroupCacheConfiguration } from './memory/InMemoryGroupCache'
import type { GroupNotificationPublisher } from './notifications/GroupNotificationPublisher'

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

public getManyInMemoryOnly(keys: string[], group: string) {
// Note that it doesn't support preemptive refresh
return this.inMemoryCache.getManyFromGroup(keys, group)
}

public getAsyncOnly(
key: string,
group: string,
Expand Down Expand Up @@ -75,6 +80,23 @@ export abstract class AbstractGroupCache<LoadedValue, ResolveParams = undefined>
return loadingPromise
}

public getManyAsyncOnly(
keys: string[],
group: string,
idResolver: IdResolver<LoadedValue>,
resolveParams?: ResolveParams,
): Promise<GetManyResult<LoadedValue>> {
// This doesn't support deduplication, and never might, as that would affect perf strongly. Maybe as an opt-in option in the future?
return this.resolveManyGroupValues(keys, group, idResolver, resolveParams).then((result) => {
for (let i = 0; i < result.resolvedValues.length; i++) {
const resolvedValue = result.resolvedValues[i]
const id = idResolver(resolvedValue)
this.inMemoryCache.setForGroup(id, resolvedValue, group)
}
return result
})
}

public get(key: string, group: string, resolveParams?: ResolveParams): Promise<LoadedValue | undefined | null> {
const inMemoryValue = this.getInMemoryOnly(key, group, resolveParams)
if (inMemoryValue !== undefined) {
Expand All @@ -84,6 +106,25 @@ export abstract class AbstractGroupCache<LoadedValue, ResolveParams = undefined>
return this.getAsyncOnly(key, group, resolveParams)
}

public getMany(
keys: string[],
group: string,
idResolver: IdResolver<LoadedValue>,
resolveParams?: ResolveParams,
): Promise<LoadedValue[]> {
const inMemoryValues = this.getManyInMemoryOnly(keys, group)
// everything is in memory, hurray
if (inMemoryValues.unresolvedKeys.length === 0) {
return Promise.resolve(inMemoryValues.resolvedValues)
}

return this.getManyAsyncOnly(inMemoryValues.unresolvedKeys, group, idResolver, resolveParams).then(
(asyncRetrievedValues) => {
return [...inMemoryValues.resolvedValues, ...asyncRetrievedValues.resolvedValues]
},
)
}

public async invalidateCacheFor(key: string, group: string) {
this.inMemoryCache.deleteFromGroup(key, group)
if (this.asyncCache) {
Expand Down Expand Up @@ -118,6 +159,27 @@ export abstract class AbstractGroupCache<LoadedValue, ResolveParams = undefined>
return undefined
}

protected async resolveManyGroupValues(
keys: string[],
group: string,
_idResolver: IdResolver<LoadedValue>,
_resolveParams?: ResolveParams,
) {
if (this.asyncCache) {
return this.asyncCache.getManyFromGroup(keys, group).catch((err) => {
this.loadErrorHandler(err, keys.toString(), this.asyncCache!, this.logger)
return {
unresolvedKeys: keys,
resolvedValues: [],
}
})
}
return {
unresolvedKeys: keys,
resolvedValues: [],
}
}

protected resolveGroupLoads(group: string) {
const load = this.runningLoads.get(group)
if (load) {
Expand Down
61 changes: 60 additions & 1 deletion lib/GroupLoader.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { GroupCache, GroupDataSource } from './types/DataSources'
import type { GroupCache, GroupDataSource, IdResolver } from './types/DataSources'
import type { LoaderConfig } from './Loader'
import { AbstractGroupCache } from './AbstractGroupCache'
import type { InMemoryGroupCacheConfiguration, InMemoryGroupCache } from './memory/InMemoryGroupCache'
import type { GroupNotificationPublisher } from './notifications/GroupNotificationPublisher'
import type { GetManyResult } from './types/SyncDataSources'

export type GroupLoaderConfig<LoadedValue, LoaderParams = undefined> = LoaderConfig<
LoadedValue,
Expand Down Expand Up @@ -79,6 +80,40 @@ export class GroupLoader<LoadedValue, LoaderParams = undefined> extends Abstract
})
}

protected override async resolveManyGroupValues(
keys: string[],
group: string,
idResolver: IdResolver<LoadedValue>,
loadParams?: LoaderParams,
): Promise<GetManyResult<LoadedValue>> {
// load what is available from async cache
const cachedValues = await super.resolveManyGroupValues(keys, group, idResolver, loadParams)

// everything was cached, no need to load anything
if (cachedValues.unresolvedKeys.length === 0) {
return cachedValues
}

const loadValues = await this.loadManyFromLoaders(cachedValues.unresolvedKeys, group, loadParams)

if (this.asyncCache) {
for (let i = 0; i < loadValues.length; i++) {
const resolvedValue = loadValues[i]
const id = idResolver(resolvedValue)
await this.asyncCache.setForGroup(id, resolvedValue, group).catch((err) => {
this.cacheUpdateErrorHandler(err, id, this.asyncCache!, this.logger)
})
}
}

return {
resolvedValues: [...cachedValues.resolvedValues, ...loadValues],

// there actually may still be some unresolved keys, but we no longer know that
unresolvedKeys: [],
}
}

private async loadFromLoaders(key: string, group: string, loadParams?: LoaderParams) {
for (let index = 0; index < this.dataSources.length; index++) {
const resolvedValue = await this.dataSources[index].getFromGroup(key, group, loadParams).catch((err) => {
Expand All @@ -104,4 +139,28 @@ export class GroupLoader<LoadedValue, LoaderParams = undefined> extends Abstract

return undefined
}

private async loadManyFromLoaders(keys: string[], group: string, loadParams?: LoaderParams) {
let lastResolvedValues
for (let index = 0; index < this.dataSources.length; index++) {
lastResolvedValues = await this.dataSources[index].getManyFromGroup(keys, group, loadParams).catch((err) => {
this.loadErrorHandler(err, keys.toString(), this.dataSources[index], this.logger)
if (this.throwIfLoadError) {
throw err
}
return [] as LoadedValue[]
})

if (lastResolvedValues.length === keys.length) {
return lastResolvedValues
}
}

if (this.throwIfUnresolved) {
throw new Error(`Failed to resolve value for some of the keys (group ${group}): ${keys.join(', ')}`)
}

// ToDo do we want to return results of a query that returned the most amount of entities?
return lastResolvedValues ?? []
}
}
2 changes: 1 addition & 1 deletion lib/Loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class Loader<LoadedValue, LoaderParams = undefined> extends AbstractFlatC
return undefined
}

protected async resolveManyValues(
protected override async resolveManyValues(
keys: string[],
idResolver: IdResolver<LoadedValue>,
loadParams?: LoaderParams,
Expand Down
2 changes: 1 addition & 1 deletion lib/redis/RedisCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class RedisCache<T> extends AbstractRedisCache<RedisCacheConfiguration, T
})
}

getManyCached(keys: string[]): Promise<GetManyResult<T>> {
getMany(keys: string[]): Promise<GetManyResult<T>> {
const transformedKeys = keys.map((entry) => this.resolveKey(entry))
const resolvedValues: T[] = []
const unresolvedKeys: string[] = []
Expand Down
5 changes: 5 additions & 0 deletions lib/redis/RedisExpirationTimeGroupDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ export class RedisExpirationTimeGroupDataSource implements GroupDataSource<numbe
getFromGroup(key: string, group: string): Promise<number | undefined> {
return this.parentAsyncCache.getExpirationTimeFromGroup(key, group)
}

/* c8 ignore next 3 */
getManyFromGroup(): Promise<number[]> {
throw new Error('Not supported')
}
}
7 changes: 2 additions & 5 deletions lib/redis/RedisGroupCache.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { GroupCache, GroupCacheConfiguration, GroupDataSource } from '../types/DataSources'
import type { GroupCache, GroupCacheConfiguration } from '../types/DataSources'
import type Redis from 'ioredis'
import { GET_OR_SET_ZERO_WITH_TTL, GET_OR_SET_ZERO_WITHOUT_TTL } from './lua'
import { GroupLoader } from '../GroupLoader'
Expand All @@ -13,10 +13,7 @@ export interface RedisGroupCacheConfiguration extends RedisCacheConfiguration, G
groupTtlInMsecs?: number
}

export class RedisGroupCache<T>
extends AbstractRedisCache<RedisGroupCacheConfiguration, T>
implements GroupCache<T>, GroupDataSource<T>
{
export class RedisGroupCache<T> extends AbstractRedisCache<RedisGroupCacheConfiguration, T> implements GroupCache<T> {
public readonly expirationTimeLoadingGroupedOperation: GroupLoader<number>
public ttlLeftBeforeRefreshInMsecs?: number
name = 'Redis group cache'
Expand Down
3 changes: 2 additions & 1 deletion lib/types/DataSources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface Cache<LoadedValue> extends WriteCache<LoadedValue> {
readonly ttlLeftBeforeRefreshInMsecs?: number
readonly expirationTimeLoadingOperation: Loader<number>
get: (key: string) => Promise<LoadedValue | undefined | null>
getManyCached: (keys: string[]) => Promise<GetManyResult<LoadedValue>>
getMany: (keys: string[]) => Promise<GetManyResult<LoadedValue>>
getExpirationTime: (key: string) => Promise<number | undefined>
}

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

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

name: string
}
Loading

0 comments on commit 2059725

Please sign in to comment.