Skip to content

Commit

Permalink
Use bulk setManyForGroup for GroupLoader (#278)
Browse files Browse the repository at this point in the history
  • Loading branch information
kibertoad committed Sep 11, 2023
1 parent 72636c6 commit 715946e
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 11 deletions.
25 changes: 16 additions & 9 deletions lib/GroupLoader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { GroupCache, GroupDataSource, IdResolver } from './types/DataSources'
import type { GroupCache, GroupDataSource, IdResolver, CacheEntry } from './types/DataSources'
import type { LoaderConfig } from './Loader'
import { AbstractGroupCache } from './AbstractGroupCache'
import type { InMemoryGroupCacheConfiguration, InMemoryGroupCache } from './memory/InMemoryGroupCache'
Expand Down Expand Up @@ -97,14 +97,21 @@ export class GroupLoader<LoadedValue, LoaderParams = undefined> extends Abstract
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)
// ToDo replace with a bulk operation
await this.asyncCache.setForGroup(id, resolvedValue, group).catch((err) => {
this.cacheUpdateErrorHandler(err, id, this.asyncCache!, this.logger)
})
}
const cacheEntries: CacheEntry<LoadedValue>[] = loadValues.map((loadValue) => {
return {
key: idResolver(loadValue),
value: loadValue,
}
})

await this.asyncCache.setManyForGroup(cacheEntries, group).catch((err) => {
this.cacheUpdateErrorHandler(
err,
cacheEntries.map((entry) => entry.key).join(', '),
this.asyncCache!,
this.logger,
)
})
}

return {
Expand Down
1 change: 1 addition & 0 deletions lib/Loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export class Loader<LoadedValue, LoaderParams = undefined> extends AbstractFlatC
value: loadValue,
}
})

await this.asyncCache.setMany(cacheEntries).catch((err) => {
this.cacheUpdateErrorHandler(
err,
Expand Down
39 changes: 38 additions & 1 deletion lib/redis/RedisGroupCache.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { GroupCache, GroupCacheConfiguration } from '../types/DataSources'
import type { GroupCache, GroupCacheConfiguration, CacheEntry } 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 Down Expand Up @@ -137,6 +137,43 @@ export class RedisGroupCache<T> extends AbstractRedisCache<RedisGroupCacheConfig
}
}

async setManyForGroup(entries: readonly CacheEntry<T>[], groupId: string): Promise<unknown> {
const getGroupKeyPromise = this.config.groupTtlInMsecs
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.redis.getOrSetZeroWithTtl(this.resolveGroupIndexPrefix(groupId), this.config.groupTtlInMsecs)
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.redis.getOrSetZeroWithoutTtl(this.resolveGroupIndexPrefix(groupId))

const currentGroupKey = await getGroupKeyPromise

if (this.config.ttlInMsecs) {
const setCommands = []
for (let i = 0; i < entries.length; i++) {
const entry = entries[i]
setCommands.push([
'set',
this.resolveKeyWithGroup(entry.key, groupId, currentGroupKey),
entry.value && this.config.json ? JSON.stringify(entry.value) : entry.value,
'PX',
this.config.ttlInMsecs,
])
}

return this.redis.multi(setCommands).exec()
}

// No TTL set
const commandParam = []
for (let i = 0; i < entries.length; i++) {
const entry = entries[i]
commandParam.push(this.resolveKeyWithGroup(entry.key, groupId, currentGroupKey))
commandParam.push(entry.value && this.config.json ? JSON.stringify(entry.value) : entry.value)
}
return this.redis.mset(commandParam)
}

resolveKeyWithGroup(key: string, groupId: string, groupIndexKey: string) {
return `${this.config.prefix}${this.config.separator}${groupId}${this.config.separator}${groupIndexKey}${this.config.separator}${key}`
}
Expand Down
1 change: 1 addition & 0 deletions lib/types/DataSources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface Cache<LoadedValue> extends WriteCache<LoadedValue> {

export interface GroupWriteCache<LoadedValue> {
setForGroup: (key: string, value: LoadedValue | null, group: string) => Promise<void>
setManyForGroup: (entries: readonly CacheEntry<LoadedValue>[], group: string) => Promise<unknown>
deleteGroup: (group: string) => Promise<unknown>
deleteFromGroup: (key: string, group: string) => Promise<unknown>
clear: () => Promise<unknown>
Expand Down
9 changes: 8 additions & 1 deletion test/fakes/DummyGroupedCache.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { GroupCache } from '../../lib/types/DataSources'
import type { CacheEntry, GroupCache } from '../../lib/types/DataSources'
import type { GroupValues, User } from '../types/testTypes'
import { cloneDeep } from '../utils/cloneUtils'
import type { GetManyResult } from '../../lib/types/SyncDataSources'
Expand Down Expand Up @@ -31,6 +31,13 @@ export class DummyGroupedCache implements GroupCache<User> {
return Promise.resolve()
}

setManyForGroup(entries: readonly CacheEntry<User>[], group: string): Promise<unknown> {
for (let entry of entries) {
this.groupValues[group][entry.key] = entry.value
}
return Promise.resolve()
}

get() {
return Promise.resolve(this.value)
}
Expand Down
6 changes: 6 additions & 0 deletions test/fakes/ThrowingGroupedCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ export class ThrowingGroupedCache implements GroupCache<User> {
})
}

setManyForGroup(): Promise<unknown> {
return Promise.resolve().then(() => {
throw new Error('Error has occurred')
})
}

deleteGroup(): Promise<void> {
return Promise.resolve().then(() => {
throw new Error('Error has occurred')
Expand Down
158 changes: 158 additions & 0 deletions test/redis/RedisGroupCache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,164 @@ describe('RedisGroupCache', () => {
})
})

describe('setManyForGroup', () => {
it('stores several items without ttl', async () => {
const cache = new RedisGroupCache(redis, {
ttlInMsecs: undefined,
})
await cache.setManyForGroup(
[
{
key: 'key',
value: 'value',
},
{
key: 'key2',
value: 'value2',
},
],
'group',
)

const ttl = await cache.getExpirationTimeFromGroup('key', 'group')
const value = await cache.getFromGroup('key', 'group')
const ttl2 = await cache.getExpirationTimeFromGroup('key2', 'group')
const value2 = await cache.getFromGroup('key2', 'group')

expect(ttl).toBeUndefined()
expect(value).toBe('value')
expect(ttl2).toBeUndefined()
expect(value2).toBe('value2')
})

it('stores several items with ttl', async () => {
const cache = new RedisGroupCache(redis, {
ttlInMsecs: 9999,
})
await cache.setManyForGroup(
[
{
key: 'key',
value: 'value',
},
{
key: 'key2',
value: 'value2',
},
],
'group',
)

const ttl = await cache.getExpirationTimeFromGroup('key', 'group')
const value = await cache.getFromGroup('key', 'group')
const ttl2 = await cache.getExpirationTimeFromGroup('key2', 'group')
const value2 = await cache.getFromGroup('key2', 'group')

expect(ttl).toEqual(expect.any(Number))
expect(value).toBe('value')
expect(ttl2).toEqual(expect.any(Number))
expect(value2).toBe('value2')
})

it('stores several items with ttl and group ttl', async () => {
const cache = new RedisGroupCache(redis, {
ttlInMsecs: 9999,
groupTtlInMsecs: 999999,
})
await cache.setManyForGroup(
[
{
key: 'key',
value: 'value',
},
{
key: 'key2',
value: 'value2',
},
],
'group',
)

const ttl = await cache.getExpirationTimeFromGroup('key', 'group')
const value = await cache.getFromGroup('key', 'group')
const ttl2 = await cache.getExpirationTimeFromGroup('key2', 'group')
const value2 = await cache.getFromGroup('key2', 'group')

expect(ttl).toEqual(expect.any(Number))
expect(value).toBe('value')
expect(ttl2).toEqual(expect.any(Number))
expect(value2).toBe('value2')
})

it('stores several JSON items without ttl', async () => {
const cache = new RedisGroupCache(redis, {
json: true,
ttlInMsecs: undefined,
})
await cache.setManyForGroup(
[
{
key: 'key',
value: {
value: 'value',
},
},
{
key: 'key2',
value: {
value: 'value2',
},
},
],
'group',
)

const ttl = await cache.getExpirationTimeFromGroup('key', 'group')
const value = await cache.getFromGroup('key', 'group')
const ttl2 = await cache.getExpirationTimeFromGroup('key2', 'group')
const value2 = await cache.getFromGroup('key2', 'group')

expect(ttl).toBeUndefined()
expect(value).toEqual({ value: 'value' })
expect(ttl2).toBeUndefined()
expect(value2).toEqual({ value: 'value2' })
})

it('stores several JSON items with ttl', async () => {
const cache = new RedisGroupCache(redis, {
json: true,
ttlInMsecs: 99999,
})
await cache.setManyForGroup(
[
{
key: 'key',
value: {
value: 'value',
},
},
{
key: 'key2',
value: {
value: 'value2',
},
},
],
'group',
)

const ttl = await cache.getExpirationTimeFromGroup('key', 'group')
const value = await cache.getFromGroup('key', 'group')
const ttl2 = await cache.getExpirationTimeFromGroup('key2', 'group')
const value2 = await cache.getFromGroup('key2', 'group')

expect(ttl).toEqual(expect.any(Number))
expect(value).toEqual({ value: 'value' })
expect(ttl2).toEqual(expect.any(Number))
expect(value2).toEqual({ value: 'value2' })
})
})

describe('clear', () => {
it('clears values', async () => {
const cache = new RedisGroupCache(redis)
Expand Down

0 comments on commit 715946e

Please sign in to comment.