Skip to content

Commit

Permalink
Don't run cached function two times concurrently if it could be cached
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo committed Oct 2, 2019
1 parent b7c7b54 commit 963a2bd
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 33 deletions.
190 changes: 164 additions & 26 deletions packages/babel-core/src/config/caching.js
@@ -1,7 +1,14 @@
// @flow

/*:: declare var invariant; */

import gensync, { type Handler } from "gensync";
import { maybeAsync, isAsync, isThenable } from "../gensync-utils/async";
import {
maybeAsync,
isAsync,
waitFor,
isThenable,
} from "../gensync-utils/async";
import { isIterableIterator } from "./util";

export type { CacheConfigurator };
Expand Down Expand Up @@ -39,61 +46,68 @@ function* genTrue(data: any) {

export function makeWeakCache<ArgT, ResultT, SideChannel>(
handler: (ArgT, CacheConfigurator<SideChannel>) => Handler<ResultT> | ResultT,
invalidateAsync?: boolean = false,
): (ArgT, SideChannel) => Handler<ResultT> {
return makeCachedFunction<ArgT, ResultT, SideChannel, *>(
WeakMap,
invalidateAsync,
handler,
);
return makeCachedFunction<ArgT, ResultT, SideChannel, *>(WeakMap, handler);
}

export function makeWeakCacheSync<ArgT, ResultT, SideChannel>(
handler: (ArgT, CacheConfigurator<SideChannel>) => ResultT,
): (ArgT, SideChannel) => ResultT {
return synchronize<[ArgT, SideChannel], ResultT>(
makeWeakCache<ArgT, ResultT, SideChannel>(handler, false),
makeWeakCache<ArgT, ResultT, SideChannel>(handler),
);
}

export function makeStrongCache<ArgT, ResultT, SideChannel>(
handler: (ArgT, CacheConfigurator<SideChannel>) => Handler<ResultT> | ResultT,
invalidateAsync?: boolean = false,
): (ArgT, SideChannel) => Handler<ResultT> {
return makeCachedFunction<ArgT, ResultT, SideChannel, *>(
Map,
invalidateAsync,
handler,
);
return makeCachedFunction<ArgT, ResultT, SideChannel, *>(Map, handler);
}

export function makeStrongCacheSync<ArgT, ResultT, SideChannel>(
handler: (ArgT, CacheConfigurator<SideChannel>) => ResultT,
): (ArgT, SideChannel) => ResultT {
return synchronize<[ArgT, SideChannel], ResultT>(
makeStrongCache<ArgT, ResultT, SideChannel>(handler, false),
makeStrongCache<ArgT, ResultT, SideChannel>(handler),
);
}

function makeCachedFunction<ArgT, ResultT, SideChannel, Cache: *>(
CallCache: Class<Cache>,
invalidateAsync: boolean,
handler: (ArgT, CacheConfigurator<SideChannel>) => Handler<ResultT> | ResultT,
): (ArgT, SideChannel) => Handler<ResultT> {
const callCacheSync = new CallCache();
const callCacheAsync = invalidateAsync ? new CallCache() : callCacheSync;
const callCacheAsync = new CallCache();
const futureCache = new CallCache();
const cacheNotConfigured = new CallCache();

return function* cachedFunction(arg: ArgT, data: SideChannel) {
const callCache =
invalidateAsync && (yield* isAsync()) ? callCacheAsync : callCacheSync;
const asyncContext = yield* isAsync();
const callCache = asyncContext ? callCacheAsync : callCacheSync;

const cached = yield* getCachedValueOrWait(
asyncContext,
callCache,
futureCache,
cacheNotConfigured,
arg,
data,
);

const cached = yield* getCachedValue(callCache, arg, data);
if (cached.valid) {
return cached.value;
switch (cached.kind) {
case "cached":
return cached.value;
case "retry":
return yield* cachedFunction(arg, data);
case "continue": // continue
}

const cache = new CacheConfigurator(data);

const finishLock: false | Lock<ResultT> =
asyncContext &&
setupAsyncLocks(cache, futureCache, cacheNotConfigured, arg);

const handlerResult = handler(arg, cache);

const gen = isIterableIterator(handlerResult);
Expand All @@ -103,6 +117,12 @@ function makeCachedFunction<ArgT, ResultT, SideChannel, Cache: *>(

updateFunctionCache(callCache, cache, arg, value);

if (asyncContext) {
/*:: invariant(finishLock) */
futureCache.delete(arg);
finishLock.release(value);
}

return value;
};
}
Expand Down Expand Up @@ -133,6 +153,91 @@ function* getCachedValue<
return { valid: false, value: null };
}

/* NOTE: This comment refers to both the getCachedValueOrWait and setupAsyncLocks functions.
*
* > There are only two hard things in Computer Science: cache invalidation and naming things.
* > -- Phil Karlton
*
* I don't know if Phil was also thinking about handling a cache whose invalidation function is
* defined asynchronously is considered, but it is REALLY hard to do correctly.
*
* The implemented logic (only when gensync is run asynchronously) is the following:
* 1. If there is a valid cache associated to the current "arg" parameter,
* a. RETURN the cached value
* 2. If there is a ConfigLock associated to the current "arg" parameter,
* a. Wait for that lock to be released
* b. GOTO step 1
* 3. If there is a FinishLock associated to the current "arg" parameter representing a valid cache,
* a. Wait for that lock to be released
* b. RETURN the value associated with that lock
* 4. Acquire to locks: ConfigLock and FinishLock
* 5. Start executing the cached function
* 6. When the cache if configured, release ConfigLock
* 7. Wait for the function to finish executing
* 8. Release FinishLock
* 9. Send the function result to anyone waiting on FinishLock
* 10. Store the result in the cache
* 11. RETURN the result
*/
function* getCachedValueOrWait<ArgT, ResultT, SideChannel>(
asyncContext: boolean,
callCache: CacheMap<ArgT, ResultT, SideChannel>,
futureCache: CacheMap<ArgT, Lock<ResultT>, SideChannel>,
cacheNotConfigured: Map<ArgT, Lock<void>>,
arg: ArgT,
data: SideChannel,
): Handler<
| { kind: "cached", value: ResultT }
| { kind: "retry" | "continue", value: null },
> {
const cached = yield* getCachedValue(callCache, arg, data);
if (cached.valid) {
return { kind: "cached", value: cached.value };
}

if (asyncContext) {
const lock = cacheNotConfigured.get(arg);
if (lock && !lock.released) {
yield* waitFor(lock.promise);
return { kind: "retry", value: null };
}

const cached = yield* getCachedValue(futureCache, arg, data);
if (cached.valid) {
const value = yield* waitFor<ResultT>(cached.value.promise);
return { kind: "cached", value: value };
}
}

return { kind: "continue", value: null };
}

function setupAsyncLocks<ArgT, ResultT, SideChannel>(
config: CacheConfigurator<SideChannel>,
futureCache: CacheMap<ArgT, Lock<ResultT>, SideChannel>,
cacheNotConfigured: Map<ArgT, Lock<void>>,
arg: ArgT,
): Lock<ResultT> {
const finishLock = new Lock<ResultT>();

const configLock = new Lock<void>();
config.onConfigured(() => {
cacheNotConfigured.delete(arg);
configLock.release();

updateFunctionCache(
futureCache,
config,
arg,
finishLock,
/* deactivate */ false,
);
});
cacheNotConfigured.set(arg, configLock);

return finishLock;
}

function updateFunctionCache<
ArgT,
ResultT,
Expand All @@ -144,12 +249,13 @@ function updateFunctionCache<
config: CacheConfigurator<SideChannel>,
arg: ArgT,
value: ResultT,
deactivate?: boolean = true,
) {
if (!config.configured()) config.forever();

let cachedValue: CacheEntry<ResultT, SideChannel> | void = cache.get(arg);

config.deactivate();
if (deactivate) config.deactivate();

switch (config.mode()) {
case "forever":
Expand Down Expand Up @@ -177,6 +283,7 @@ class CacheConfigurator<SideChannel = void> {
_invalidate: boolean = false;

_configured: boolean = false;
_configuredHandler: ?Function = null;

_pairs: Array<[mixed, (SideChannel) => Handler<mixed>]> = [];

Expand Down Expand Up @@ -205,7 +312,7 @@ class CacheConfigurator<SideChannel = void> {
throw new Error("Caching has already been configured with .never()");
}
this._forever = true;
this._configured = true;
this._fireConfigured();
}

never() {
Expand All @@ -216,7 +323,7 @@ class CacheConfigurator<SideChannel = void> {
throw new Error("Caching has already been configured with .forever()");
}
this._never = true;
this._configured = true;
this._fireConfigured();
}

using<T>(handler: SideChannel => T): T {
Expand All @@ -228,7 +335,7 @@ class CacheConfigurator<SideChannel = void> {
"Caching has already been configured with .never or .forever()",
);
}
this._configured = true;
this._fireConfigured();

const key = handler(this._data);

Expand Down Expand Up @@ -270,6 +377,20 @@ class CacheConfigurator<SideChannel = void> {
configured() {
return this._configured;
}

onConfigured(cb: Function) {
if (this._configuredHandler) {
throw new Error(
"An handler for the 'configured' event of the cache has already been registered",
);
}
this._configuredHandler = cb;
}

_fireConfigured() {
this._configured = true;
if (this._configuredHandler) this._configuredHandler();
}
}

function makeSimpleConfigurator(
Expand Down Expand Up @@ -318,3 +439,20 @@ export function assertSimpleType(value: mixed): SimpleType {
}
return value;
}

class Lock<T> {
released: boolean = false;
promise: Promise<T>;
_resolve: (value: T) => void;

constructor() {
this.promise = new Promise(resolve => {
this._resolve = resolve;
});
}

release(value: T) {
this.released = true;
this._resolve(value);
}
}
1 change: 0 additions & 1 deletion packages/babel-core/src/config/files/configuration.js
Expand Up @@ -206,7 +206,6 @@ const readConfigJS = makeStrongCache(function* readConfigJS(
}

if (typeof options.then === "function") {
// When removing this error, ENABLE the invalidateAsync param of makeStrongCache
throw new Error(
`You appear to be using an async configuration, ` +
`which your current version of Babel does not support. ` +
Expand Down
3 changes: 1 addition & 2 deletions packages/babel-core/src/config/files/utils.js
Expand Up @@ -23,8 +23,7 @@ export function makeStaticFileCache<T>(
}

return fn(filepath, yield* fs.readFile(filepath, "utf8"));
},
/* invalidateAsync */ true): Gensync<any, *>);
}): Gensync<any, *>);
}

function* fileMtime(filepath: string): Handler<number | null> {
Expand Down
4 changes: 1 addition & 3 deletions packages/babel-core/src/config/full.js
Expand Up @@ -210,7 +210,6 @@ const loadDescriptor = makeWeakCache(function*(
if (typeof item.then === "function") {
yield* []; // if we want to support async plugins

// When enabling this, ENABLE the invalidateAsync param of makeStrongCache
throw new Error(
`You appear to be using an async plugin, ` +
`which your current version of Babel does not support. ` +
Expand Down Expand Up @@ -287,8 +286,7 @@ const instantiatePlugin = makeWeakCache(function*(
}

return new Plugin(plugin, options, alias);
},
/* invalidateAsync */ true);
});

const validateIfOptionNeedsFilename = (
options: ValidatedOptions,
Expand Down
6 changes: 6 additions & 0 deletions packages/babel-core/src/gensync-utils/async.js
Expand Up @@ -64,6 +64,12 @@ export function forwardAsync<ActionArgs: mixed[], ActionReturn, Return>(
});
}

const id = x => x;
export const waitFor = (gensync<[any], any>({
sync: id,
async: id,
}): <T>(p: T | Promise<T>) => Handler<T>);

export function isThenable(val: mixed): boolean %checks {
return (
/*:: val instanceof Promise && */
Expand Down

0 comments on commit 963a2bd

Please sign in to comment.