From f3efbd2ea8de057e559c9530c397d977669a61d7 Mon Sep 17 00:00:00 2001 From: Kurt Mackey Date: Mon, 10 Sep 2018 15:24:37 -0500 Subject: [PATCH] feat: batch cache lookups with `cache.getMulti` Get multiple cache values in one call: ```javascript import cache from "@fly/cache" const results = await cache.getMultiString(["key1", "key1"]) // results = ["value1", "value2"] const buffers = await cache.getMulti(["key1", "key2"]) // buffers = [ArrayBuffer, ArrayBuffer] ``` --- src/bridge/fly/cache.ts | 26 ++++++++++++++++++++++++++ src/cache_store.ts | 1 + src/memory_cache_store.ts | 6 ++++++ src/redis_cache_store.ts | 8 ++++++++ v8env/src/fly/cache/index.ts | 32 ++++++++++++++++++++++++++++++++ v8env/test/fly.cache.spec.js | 15 +++++++++++++++ 6 files changed, 88 insertions(+) diff --git a/src/bridge/fly/cache.ts b/src/bridge/fly/cache.ts index 7bd4f518..85119d0d 100644 --- a/src/bridge/fly/cache.ts +++ b/src/bridge/fly/cache.ts @@ -61,6 +61,7 @@ registerBridge('flyCacheGet', bridge.cacheStore.get(rt.app.id, key).then((buf) => { rt.reportUsage("cache:get", { size: buf ? buf.byteLength : 0 }) + const b = transferInto(buf) callback.applyIgnored(null, [null, transferInto(buf)]) }).catch((err) => { log.error("got err in cache.get", err) @@ -68,6 +69,31 @@ registerBridge('flyCacheGet', }) }) +registerBridge('flyCacheGetMulti', + function cacheGet(rt: Runtime, bridge: Bridge, keys: string | string[], callback: ivm.Reference) { + if (!bridge.cacheStore) { + callback.applyIgnored(null, [errCacheStoreUndefined.toString()]) + return + } + + if (typeof keys === "string") { + keys = JSON.parse(keys) as string[] + } + bridge.cacheStore.getMulti(rt.app.id, keys).then((result) => { + let byteLength = 0 + const toTransfer: (null | ivm.Copy)[] = result.map((b) => { + byteLength += b ? b.byteLength : 0 + return transferInto(b) + }) + toTransfer.unshift(null) + rt.reportUsage("cache:get", { size: byteLength, keys: result.length }) + callback.applyIgnored(null, toTransfer) + }).catch((err) => { + log.error("got err in cache.getMulti", err) + callback.applyIgnored(null, [null, null]) // swallow errors on get for now + }) + }) + registerBridge('flyCacheDel', function cacheDel(rt: Runtime, bridge: Bridge, key: string, callback: ivm.Reference) { if (!bridge.cacheStore) { diff --git a/src/cache_store.ts b/src/cache_store.ts index 68c45ae7..ea50142c 100644 --- a/src/cache_store.ts +++ b/src/cache_store.ts @@ -9,6 +9,7 @@ export interface CacheSetOptions { } export interface CacheStore { get(ns: string, key: string): Promise + getMulti(ns: string, keys: string[]): Promise<(Buffer | null)[]> set(ns: string, key: string, value: any, options?: CacheSetOptions | number): Promise del(ns: string, key: string): Promise expire(ns: string, key: string, ttl: number): Promise diff --git a/src/memory_cache_store.ts b/src/memory_cache_store.ts index 637e6239..ce5c9e29 100644 --- a/src/memory_cache_store.ts +++ b/src/memory_cache_store.ts @@ -19,6 +19,12 @@ export class MemoryCacheStore implements CacheStore { return Buffer.from(buf) } + async getMulti(ns: string, keys: string[]) { + keys = keys.map((k) => keyFor(ns, k)) + const bufs = await this.redis.mget(...keys) + return bufs.map((b: any) => !b ? null : Buffer.from(b)) + } + async set(ns: string, key: string, value: any, options?: CacheSetOptions | number): Promise { const k = keyFor(ns, key) const pipeline = this.redis.pipeline() diff --git a/src/redis_cache_store.ts b/src/redis_cache_store.ts index 13eb0c09..67c7856c 100644 --- a/src/redis_cache_store.ts +++ b/src/redis_cache_store.ts @@ -22,6 +22,12 @@ export class RedisCacheStore implements CacheStore { return ret } + async getMulti(ns: string, keys: string[]) { + keys = keys.map((k) => keyFor(ns, k)) + const bufs = await this.redis.getMultiBufferAsync(keys) + return bufs.map((b: any) => !b ? null : Buffer.from(b)) + } + async set(ns: string, key: string, value: any, options?: CacheSetOptions | number): Promise { const k = keyFor(ns, key) let ttl: number | undefined @@ -154,6 +160,7 @@ async function* setScanner(redis: FlyRedis, key: string) { class FlyRedis { getBufferAsync: (key: Buffer | string) => Promise + getMultiBufferAsync: (keys: string[]) => Promise setAsync: (key: string, value: Buffer, mode?: number | string, duration?: number, exists?: string) => Promise<"OK" | undefined> expireAsync: (key: string, ttl: number) => Promise ttlAsync: (key: string) => Promise @@ -168,6 +175,7 @@ class FlyRedis { const p = promisify this.getBufferAsync = p(redis.get).bind(redis) + this.getMultiBufferAsync = p(redis.mget).bind(redis) this.setAsync = p(redis.set).bind(redis) this.expireAsync = p(redis.expire).bind(redis) this.ttlAsync = p(redis.ttl).bind(redis) diff --git a/v8env/src/fly/cache/index.ts b/v8env/src/fly/cache/index.ts index 9285d3d7..d67a242d 100644 --- a/v8env/src/fly/cache/index.ts +++ b/v8env/src/fly/cache/index.ts @@ -63,6 +63,36 @@ export async function getString(key: string) { } } +/** + * Get multiple values from the cache. + * @param keys list of keys to retrieve + * @returns List of results in the same order as the provided keys + */ +export function getMulti(keys: string[]): Promise<(ArrayBuffer | null)[]> { + return new Promise<(ArrayBuffer | null)[]>(function cacheGetMultiPromise(resolve, reject) { + bridge.dispatch( + "flyCacheGetMulti", + JSON.stringify(keys), + function cacheGetMultiCallback(err: string | null | undefined, ...values: (ArrayBuffer | null)[]) { + if (err != null) { + reject(err) + return + } + resolve(values) + }) + }) +} + +/** + * Get multiple string values from the cache + * @param keys list of keys to retrieve + * @returns list of results in the same order as the provided keys + */ +export async function getMultiString(keys: string[]) { + const raw = await getMulti(keys) + return raw.map((b) => b ? new TextDecoder("utf-8").decode(b) : null) +} + /** * Sets a value at the specified key, with an optional ttl * @param key The key to add or overwrite @@ -178,6 +208,8 @@ import { default as global } from "./global" const cache = { get, getString, + getMulti, + getMultiString, set, expire, del, diff --git a/v8env/test/fly.cache.spec.js b/v8env/test/fly.cache.spec.js index d5880a70..8d88a4e2 100644 --- a/v8env/test/fly.cache.spec.js +++ b/v8env/test/fly.cache.spec.js @@ -58,4 +58,19 @@ describe("@fly/cache", () => { expect(setResult).to.eq(false) expect(v).to.eq("asdf") }) + + it("gets multiple values", async () => { + const k = `cache-test${Math.random()}` + const k2 = `cache-test${Math.random()}` + + await cache.set(k, "multi-1") + await cache.set(k2, "multi-2") + + const result = await cache.getMultiString([k, k2]) + expect(result).to.be.an('array') + + const [r1, r2] = result; + expect(r1).to.eq("multi-1") + expect(r2).to.eq("multi-2") + }) }) \ No newline at end of file