diff --git a/README.md b/README.md index 906071138..b95f6c9b3 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ All CLI options are optional: --useDocker Run handlers in a docker container. --layersDir The directory layers should be stored in. Default: ${codeDir}/.serverless-offline/layers' --dockerReadOnly Marks if the docker code layer should be read only. Default: true +--allowCache Allows the code of lambda functions to cache if supported. ``` Any of the CLI options can be added to your `serverless.yml`. For example: diff --git a/package-lock.json b/package-lock.json index 857a422f4..5481944db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3711,8 +3711,7 @@ "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" }, "camelcase": { "version": "5.3.1", @@ -3835,6 +3834,30 @@ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true }, + "clear-module": { + "version": "4.1.1", + "resolved": "https://kdev-341194987187.d.codeartifact.us-east-1.amazonaws.com:443/npm/MainDevelopment/clear-module/-/clear-module-4.1.1.tgz", + "integrity": "sha512-ng0E7LeODcT3QkazOckzZqbca+JByQy/Q2Z6qO24YsTp+pLxCfohGz2gJYJqZS0CWTX3LEUiHOqe5KlYeUbEMw==", + "requires": { + "parent-module": "^2.0.0", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "parent-module": { + "version": "2.0.0", + "resolved": "https://kdev-341194987187.d.codeartifact.us-east-1.amazonaws.com:443/npm/MainDevelopment/parent-module/-/parent-module-2.0.0.tgz", + "integrity": "sha512-uo0Z9JJeWzv8BG+tRcapBKNJ0dro9cLyczGzulS6EfeyAdeC9sbojtW6XwvYxJkEne9En+J2XEl4zyglVeIwFg==", + "requires": { + "callsites": "^3.1.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://kdev-341194987187.d.codeartifact.us-east-1.amazonaws.com:443/npm/MainDevelopment/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + } + } + }, "cli-boxes": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.0.tgz", diff --git a/package.json b/package.json index 22a4bb90f..6d6551bb2 100644 --- a/package.json +++ b/package.json @@ -158,6 +158,7 @@ "aws-sdk": "^2.624.0", "boxen": "^4.2.0", "chalk": "^3.0.0", + "clear-module": "^4.1.1", "cuid": "^2.1.8", "execa": "^4.0.0", "extend": "^3.0.2", diff --git a/src/config/commandOptions.js b/src/config/commandOptions.js index a8f474f88..604cc9c69 100644 --- a/src/config/commandOptions.js +++ b/src/config/commandOptions.js @@ -99,4 +99,7 @@ export default { functionCleanupIdleTimeSeconds: { usage: 'Number of seconds until an idle function is eligible for cleanup', }, + allowCache: { + usage: 'Allows the code of lambda functions to cache if supported', + }, } diff --git a/src/config/defaultOptions.js b/src/config/defaultOptions.js index 7eb6ed82e..6d227cc37 100644 --- a/src/config/defaultOptions.js +++ b/src/config/defaultOptions.js @@ -28,4 +28,5 @@ export default { layersDir: null, dockerReadOnly: true, functionCleanupIdleTimeSeconds: 60, + allowCache: false, } diff --git a/src/lambda/handler-runner/HandlerRunner.js b/src/lambda/handler-runner/HandlerRunner.js index bd55b2d68..76fcb8ab2 100644 --- a/src/lambda/handler-runner/HandlerRunner.js +++ b/src/lambda/handler-runner/HandlerRunner.js @@ -21,7 +21,12 @@ export default class HandlerRunner { } async _loadRunner() { - const { useDocker, useChildProcesses, useWorkerThreads } = this.#options + const { + useDocker, + useChildProcesses, + useWorkerThreads, + allowCache, + } = this.#options const { functionKey, @@ -48,7 +53,7 @@ export default class HandlerRunner { const { default: ChildProcessRunner } = await import( './child-process-runner/index.js' ) - return new ChildProcessRunner(this.#funOptions, this.#env) + return new ChildProcessRunner(this.#funOptions, this.#env, allowCache) } if (useWorkerThreads) { @@ -58,7 +63,7 @@ export default class HandlerRunner { const { default: WorkerThreadRunner } = await import( './worker-thread-runner/index.js' ) - return new WorkerThreadRunner(this.#funOptions, this.#env) + return new WorkerThreadRunner(this.#funOptions, this.#env, allowCache) } const { default: InProcessRunner } = await import( @@ -70,22 +75,23 @@ export default class HandlerRunner { handlerName, this.#env, timeout, + allowCache, ) } if (supportedPython.has(runtime)) { const { default: PythonRunner } = await import('./python-runner/index.js') - return new PythonRunner(this.#funOptions, this.#env) + return new PythonRunner(this.#funOptions, this.#env, allowCache) } if (supportedRuby.has(runtime)) { const { default: RubyRunner } = await import('./ruby-runner/index.js') - return new RubyRunner(this.#funOptions, this.#env) + return new RubyRunner(this.#funOptions, this.#env, allowCache) } if (supportedJava.has(runtime)) { const { default: JavaRunner } = await import('./java-runner/index.js') - return new JavaRunner(this.#funOptions, this.#env) + return new JavaRunner(this.#funOptions, this.#env, allowCache) } // TODO FIXME diff --git a/src/lambda/handler-runner/child-process-runner/ChildProcessRunner.js b/src/lambda/handler-runner/child-process-runner/ChildProcessRunner.js index 55393f214..800cc2552 100644 --- a/src/lambda/handler-runner/child-process-runner/ChildProcessRunner.js +++ b/src/lambda/handler-runner/child-process-runner/ChildProcessRunner.js @@ -9,8 +9,9 @@ export default class ChildProcessRunner { #handlerName = null #handlerPath = null #timeout = null + #allowCache = false - constructor(funOptions, env) { + constructor(funOptions, env, allowCache) { const { functionKey, handlerName, handlerPath, timeout } = funOptions this.#env = env @@ -18,6 +19,7 @@ export default class ChildProcessRunner { this.#handlerName = handlerName this.#handlerPath = handlerPath this.#timeout = timeout + this.#allowCache = allowCache } // no-op @@ -37,6 +39,7 @@ export default class ChildProcessRunner { childProcess.send({ context, event, + allowCache: this.#allowCache, timeout: this.#timeout, }) diff --git a/src/lambda/handler-runner/child-process-runner/childProcessHelper.js b/src/lambda/handler-runner/child-process-runner/childProcessHelper.js index 38741dcb4..3102a5484 100644 --- a/src/lambda/handler-runner/child-process-runner/childProcessHelper.js +++ b/src/lambda/handler-runner/child-process-runner/childProcessHelper.js @@ -23,7 +23,7 @@ process.on('uncaughtException', (err) => { const [, , functionKey, handlerName, handlerPath] = process.argv process.on('message', async (messageData) => { - const { context, event, timeout } = messageData + const { context, event, allowCache, timeout } = messageData // TODO we could probably cache this in the module scope? const inProcessRunner = new InProcessRunner( @@ -32,6 +32,7 @@ process.on('message', async (messageData) => { handlerName, process.env, timeout, + allowCache, ) let result diff --git a/src/lambda/handler-runner/in-process-runner/InProcessRunner.js b/src/lambda/handler-runner/in-process-runner/InProcessRunner.js index 6ec07b76e..4839abdee 100644 --- a/src/lambda/handler-runner/in-process-runner/InProcessRunner.js +++ b/src/lambda/handler-runner/in-process-runner/InProcessRunner.js @@ -1,4 +1,5 @@ import { performance } from 'perf_hooks' +import clearModule from 'clear-module' export default class InProcessRunner { #env = null @@ -6,13 +7,15 @@ export default class InProcessRunner { #handlerName = null #handlerPath = null #timeout = null + #allowCache = false - constructor(functionKey, handlerPath, handlerName, env, timeout) { + constructor(functionKey, handlerPath, handlerName, env, timeout, allowCache) { this.#env = env this.#functionKey = functionKey this.#handlerName = handlerName this.#handlerPath = handlerPath this.#timeout = timeout + this.#allowCache = allowCache } // no-op @@ -36,7 +39,9 @@ export default class InProcessRunner { Object.assign(process.env, this.#env) // lazy load handler with first usage - + if (!this.#allowCache) { + clearModule(this.#handlerPath) + } const { [this.#handlerName]: handler } = await import(this.#handlerPath) if (typeof handler !== 'function') { diff --git a/src/lambda/handler-runner/java-runner/JavaRunner.js b/src/lambda/handler-runner/java-runner/JavaRunner.js index 8dc3c951a..69c08ee3a 100644 --- a/src/lambda/handler-runner/java-runner/JavaRunner.js +++ b/src/lambda/handler-runner/java-runner/JavaRunner.js @@ -10,8 +10,9 @@ export default class JavaRunner { #functionName = null #handler = null #deployPackage = null + #allowCache = false - constructor(funOptions, env) { + constructor(funOptions, env, allowCache) { const { functionName, handler, @@ -23,6 +24,7 @@ export default class JavaRunner { this.#functionName = functionName this.#handler = handler this.#deployPackage = functionPackage || servicePackage + this.#allowCache = allowCache } // no-op @@ -71,6 +73,7 @@ export default class JavaRunner { function: this.#functionName, jsonOutput: true, serverlessOffline: true, + allowCache: this.#allowCache, }) const httpOptions = { diff --git a/src/lambda/handler-runner/python-runner/PythonRunner.js b/src/lambda/handler-runner/python-runner/PythonRunner.js index f44583252..04d1a2805 100644 --- a/src/lambda/handler-runner/python-runner/PythonRunner.js +++ b/src/lambda/handler-runner/python-runner/PythonRunner.js @@ -13,14 +13,16 @@ export default class PythonRunner { #handlerName = null #handlerPath = null #runtime = null + #allowCache = false - constructor(funOptions, env) { + constructor(funOptions, env, allowCache) { const { handlerName, handlerPath, runtime } = funOptions this.#env = env this.#handlerName = handlerName this.#handlerPath = handlerPath this.#runtime = platform() === 'win32' ? 'python.exe' : runtime + this.#allowCache = allowCache if (process.env.VIRTUAL_ENV) { const runtimeDir = platform() === 'win32' ? 'Scripts' : 'bin' @@ -96,6 +98,7 @@ export default class PythonRunner { const input = stringify({ context, event, + allowCache: this.#allowCache, }) const onErr = (data) => { diff --git a/src/lambda/handler-runner/ruby-runner/RubyRunner.js b/src/lambda/handler-runner/ruby-runner/RubyRunner.js index d3aa72c5b..e9b89524b 100644 --- a/src/lambda/handler-runner/ruby-runner/RubyRunner.js +++ b/src/lambda/handler-runner/ruby-runner/RubyRunner.js @@ -10,13 +10,15 @@ export default class RubyRunner { #env = null #handlerName = null #handlerPath = null + #allowCache = false - constructor(funOptions, env) { + constructor(funOptions, env, allowCache) { const { handlerName, handlerPath } = funOptions this.#env = env this.#handlerName = handlerName this.#handlerPath = handlerPath + this.#allowCache = allowCache } // no-op @@ -64,6 +66,7 @@ export default class RubyRunner { const input = stringify({ context: _context, event, + allowCache: this.#allowCache, }) // console.log(input) diff --git a/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js b/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js index 238fa7a3b..f012cd852 100644 --- a/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js +++ b/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js @@ -5,12 +5,14 @@ const workerThreadHelperPath = resolve(__dirname, './workerThreadHelper.js') export default class WorkerThreadRunner { #workerThread = null + #allowCache = false - constructor(funOptions /* options */, env) { + constructor(funOptions /* options */, env, allowCache) { // this._options = options const { functionKey, handlerName, handlerPath, timeout } = funOptions + this.#allowCache = allowCache this.#workerThread = new Worker(workerThreadHelperPath, { // don't pass process.env from the main process! env, @@ -51,6 +53,7 @@ export default class WorkerThreadRunner { { context, event, + allowCache: this.#allowCache, // port2 is part of the payload, for the other side to answer messages port: port2, }, diff --git a/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js b/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js index ee78da6b2..1304b3b54 100644 --- a/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js +++ b/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js @@ -4,7 +4,7 @@ import InProcessRunner from '../in-process-runner/index.js' const { functionKey, handlerName, handlerPath } = workerData parentPort.on('message', async (messageData) => { - const { context, event, port, timeout } = messageData + const { context, event, port, timeout, allowCache } = messageData // TODO we could probably cache this in the module scope? const inProcessRunner = new InProcessRunner( @@ -13,6 +13,7 @@ parentPort.on('message', async (messageData) => { handlerName, process.env, timeout, + allowCache, ) let result diff --git a/tests/integration/lambda-invoke/serverless.yml b/tests/integration/lambda-invoke/serverless.yml index 24c4800d8..16fc34e06 100644 --- a/tests/integration/lambda-invoke/serverless.yml +++ b/tests/integration/lambda-invoke/serverless.yml @@ -51,3 +51,7 @@ functions: invokedAsyncHandler: handler: lambdaInvokeAsyncHandler.invokedAsyncHandler + +custom: + serverless-offline: + allowCache: true