@bodyam8/core
Version:
most useful backend tools
163 lines (142 loc) • 5.65 kB
text/typescript
import DataLoader from 'dataloader';
import BluebirdPromise from 'bluebird';
import groupBy from 'lodash/groupBy';
import {Redis} from 'ioredis';
import {clearTimeout} from 'timers';
const REDIS_CLIENT_MAX_CONCURRENCY = process.env.REDIS_LOADER_MAX_CONCURRENCY || 100;
interface Serializable {
toString(): string,
}
/*
* First parameter on load is always shard number!
* Костыль пиздец, но у меня нету даже полдня на написание нормальной фабрики
* чтобы вообще не надо было париться по шардам)
* шардированые запросы не кешируем!
* */
export function createShardedLoader<K, N>(
batchFn: (shard: number, keys: K[]) => Promise<ArrayLike<N | Error>>,
options: DataLoader.Options<[number, K], N> = {},
): DataLoader<[number, K], N> {
return new DataLoader<[number, K], N>(localShardedBatchFn(batchFn), {
batchScheduleFn: (cb) => {
const timeoutId = setTimeout(() => {
cb();
clearTimeout(timeoutId);
}, 25)
},
...options,
cache: false,
});
}
export function createLoader<K extends Serializable, N>(
batchFn: DataLoader.BatchLoadFn<K, N>,
options: DataLoader.Options<K, N> = {},
ttlS: number = 0
): DataLoader<K, N> {
const cacheMap = new Map();
return new DataLoader<K, N>(
localCachedBatchFn(batchFn, cacheMap, ttlS), {
batchScheduleFn: (cb) => setTimeout(cb, 25),
cacheMap,
cache: options.cache ? options.cache : false,
...options,
}
);
}
export function createCachedLoader<K extends Serializable, N>(
batchFn: DataLoader.BatchLoadFn<K, N>,
redisClient: Redis,
options: DataLoader.Options<K, N> = {},
ttl: number = 0,
cacheKeyFn?: (key: K) => string,
): DataLoader<K, N> {
return new DataLoader<K, N>(centrallyCachedBatchFn<K, N>(batchFn, redisClient, ttl, cacheKeyFn), {
batchScheduleFn: (cb) => {
const timeoutId = +setTimeout(() => {
cb();
clearTimeout(timeoutId);
}, 25);
},
...options,
cache: false,
});
}
function _buildCacheKey(fnName: string, key: string): string {
return `bodya-dataloaders-${fnName}-${key}`;
}
function localShardedBatchFn<K, N>(
shardedBatchFn: (shard: number, keys: K[]) => Promise<ArrayLike<N | Error>>,
) {
return async (keys: ReadonlyArray<[number, K]>) => {
const groupedKeys = groupBy(keys, (k) => k[0]);
const shardsKeys = Object.entries(groupedKeys);
const shards = Object.keys(groupedKeys).map(s => +s);
const shardedResults = await Promise.all(
shardsKeys.map(async ([shard, keys]) => {
return shardedBatchFn(+shard, keys.map(k => k[1]));
})
)
const loaderResults = keys.map(([shard, key]: any) => {
return shardedResults[shards.indexOf(shard)][groupedKeys[shard].findIndex((k: any) => k[0] === shard && k[1] === key)];
});
return loaderResults;
}
}
function localCachedBatchFn<K extends Serializable, N>(
batchFn: DataLoader.BatchLoadFn<K, N>,
cacheMap: Map<K, N>,
ttlS: number,
) {
return async (keys: ReadonlyArray<K>) => {
const result = await batchFn(keys);
// clear local cache after TTL
const timeoutId = +setTimeout(() => {
keys.forEach(k => cacheMap.delete(k));
clearTimeout(timeoutId);
}, ttlS * 1000);
return result;
};
}
function centrallyCachedBatchFn<K extends Serializable, N>(
batchFn: DataLoader.BatchLoadFn<K, N>,
redisClient: Redis,
ttl: number,
cacheKeyFn?: (key: K) => string,
) {
return async (keys: ReadonlyArray<K>): Promise<ArrayLike<N | Error>> => {
const matches = new Map();
const unmatched: Array<K> = [];
const cacheKeys = keys.map(k => _buildCacheKey(batchFn.name, cacheKeyFn ? cacheKeyFn(k) : k.toString()));
const cachedValues: Array<string | null> = await redisClient.mget(cacheKeys);
cachedValues.forEach((value, i) => {
let parsedValue = null;
try {
if (value !== null) {
parsedValue = JSON.parse(value);
}
} catch (err) {
console.debug('Strange value in redis: ', value);
}
if (parsedValue !== null) {
return matches.set(cacheKeyFn ? cacheKeyFn(keys[i]) : keys[i].toString(), parsedValue);
}
return unmatched.push(keys[i]);
});
if (!unmatched.length) {
return keys.map(k => matches.get(cacheKeyFn ? cacheKeyFn(k) : k.toString()));
}
const results: Array<N | Error> = Array.from(await batchFn(unmatched));
const cacheables: Record<string, string> = {};
results.forEach((res, i) => {
if (!(res instanceof Error)) {
cacheables[_buildCacheKey(batchFn.name, cacheKeyFn ? cacheKeyFn(unmatched[i]) : unmatched[i].toString())] = JSON.stringify(res);
}
matches.set(cacheKeyFn ? cacheKeyFn(unmatched[i]) : unmatched[i].toString(), res)
});
await redisClient.mset(cacheables);
await BluebirdPromise.map(Object.keys(cacheables), (key) => {
return redisClient.expire(key.toString(), ttl);
}, {concurrency: +REDIS_CLIENT_MAX_CONCURRENCY});
return keys.map(k => matches.get(cacheKeyFn ? cacheKeyFn(k) : k.toString()));
};
}