You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by st...@apache.org on 2021/10/21 09:10:25 UTC
[openwhisk-client-js] branch master updated: Support client retries
(#227)
This is an automated email from the ASF dual-hosted git repository.
stanciu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk-client-js.git
The following commit(s) were added to refs/heads/master by this push:
new 4dfc896 Support client retries (#227)
4dfc896 is described below
commit 4dfc896d230f2d705c300ee1520ef25b8008762d
Author: Moritz Raho <ra...@gmail.com>
AuthorDate: Thu Oct 21 11:10:17 2021 +0200
Support client retries (#227)
* Support client retries
* typo
* Update README.md with Rodric's suggestion
Co-authored-by: rodric rabbah <ro...@gmail.com>
* add no retry test + fix test nock race conditions
* retry spy + test no retry on success
* trigger travis build
* install only production dependencies for nnewer npm versionns
* back to old npm prod install + increase size too 2k
* actually let s try to update the npm prod install
Co-authored-by: rodric rabbah <ro...@gmail.com>
---
README.md | 2 +-
lib/client.js | 42 ++++++++++++-
package-lock.json | 160 ++++++++++++++++++++++++++++++++++++++++++++---
package.json | 4 +-
test/unit/client.test.js | 82 +++++++++++++++++++++++-
tools/check_size.sh | 2 +-
6 files changed, 276 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
index a3a91ae..f581ab9 100644
--- a/README.md
+++ b/README.md
@@ -105,7 +105,7 @@ _Client constructor supports the following mandatory parameters:_
- **key**. Client key to use when connecting to the `apihost` (if `nginx_ssl_verify_client` is turned on in your apihost)
- **proxy.** HTTP(s) URI for proxy service to forwards requests through. Uses Needle's [built-in proxy support](https://github.com/tomas/needle#request-options).
- **agent.** Provide custom [http.Agent](https://nodejs.org/api/http.html#http_class_http_agent) implementation.
-
+- **retry**. Provide a retry options to retry on errors, for example, `{ retries: 2 }`. By default, no retries will be done. Uses [async-retry options](https://github.com/vercel/async-retry#api). Default values are different from async-retry, please refer to the API doc.
### environment variables
diff --git a/lib/client.js b/lib/client.js
index e75bf40..ee92620 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -22,6 +22,7 @@ const OpenWhiskError = require('./openwhisk_error')
const needle = require('needle')
const url = require('url')
const http = require('http')
+const retry = require('async-retry')
/**
* This implements a request-promise-like facade over the needle
@@ -77,6 +78,13 @@ const rp = opts => {
})
}
+const rpWithRetry = opts => {
+ return retry(bail => {
+ // will retry on exception
+ return rp(opts)
+ }, opts.retry)
+}
+
class Client {
/**
* @constructor
@@ -93,6 +101,13 @@ class Client {
* @param {boolean} [options.noUserAgent]
* @param {string} [options.cert]
* @param {string} [options.key]
+ * @param {object} [options.retry]
+ * @param {number} [options.retry.retries] Number of retries on top of the initial request, default is 2.
+ * @param {number} [options.retry.factor] Exponential factor, default is 2.
+ * @param {number} [options.retry.minTimeout] Milliseconds before the first retry, default is 100.
+ * @param {number} [options.retry.maxTimeout] Max milliseconds in between two retries, default is infinity.
+ * @param {boolean} [options.retry.randomize] Randomizes the timeouts by multiplying with a factor between 1 to 2. Default is true.
+ * @param {Function} [options.retry.onRetry] An optional function that is invoked after a new retry is performed. It's passed the Error that triggered it as a parameter.
*/
constructor (options) {
this.options = this.parseOptions(options || {})
@@ -135,7 +150,21 @@ class Client {
throw new Error(`${messages.INVALID_OPTIONS_ERROR} Missing either api or apihost parameters.`)
}
- return { apiKey: apiKey, api, apiVersion: apiversion, ignoreCerts: ignoreCerts, namespace: options.namespace, apigwToken: apigwToken, apigwSpaceGuid: apigwSpaceGuid, authHandler: options.auth_handler, noUserAgent: options.noUserAgent, cert: options.cert, key: options.key, proxy, agent }
+ // gather retry options
+ const retry = options.retry
+ if (retry && typeof options.retry !== 'object') {
+ throw new Error(`${messages.INVALID_OPTIONS_ERROR} 'retry' option must be an object, e.g. '{ retries: 2 }'.`)
+ }
+ if (retry) {
+ // overwrite async-retry defaults, see https://github.com/vercel/async-retry#api for more details
+ retry.retries = retry.retries || 2
+ retry.factor = retry.factor || 2
+ retry.minTimeout = retry.minTimeout || 100
+ retry.maxTimeout = retry.maxTimeout || Infinity
+ retry.randomize = retry.randomize || true
+ }
+
+ return { apiKey: apiKey, api, apiVersion: apiversion, ignoreCerts: ignoreCerts, namespace: options.namespace, apigwToken: apigwToken, apigwSpaceGuid: apigwSpaceGuid, authHandler: options.auth_handler, noUserAgent: options.noUserAgent, cert: options.cert, key: options.key, proxy, agent, retry }
}
urlFromApihost (apihost, apiversion = 'v1') {
@@ -152,7 +181,12 @@ class Client {
request (method, path, options) {
const params = this.params(method, path, options)
- return params.then(req => rp(req)).catch(err => this.handleErrors(err))
+ return params.then(req => {
+ if (req.retry) {
+ return rpWithRetry(req)
+ }
+ return rp(req)
+ }).catch(err => this.handleErrors(err))
}
params (method, path, options) {
@@ -194,6 +228,10 @@ class Client {
parms.agent = this.options.agent
}
+ if (this.options.retry) {
+ parms.retry = this.options.retry
+ }
+
return parms
})
}
diff --git a/package-lock.json b/package-lock.json
index 8bb56ed..3994504 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -455,6 +455,41 @@
"integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==",
"dev": true
},
+ "@sinonjs/commons": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz",
+ "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==",
+ "dev": true,
+ "requires": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "@sinonjs/fake-timers": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz",
+ "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==",
+ "dev": true,
+ "requires": {
+ "@sinonjs/commons": "^1.7.0"
+ }
+ },
+ "@sinonjs/samsam": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz",
+ "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==",
+ "dev": true,
+ "requires": {
+ "@sinonjs/commons": "^1.6.0",
+ "lodash.get": "^4.4.2",
+ "type-detect": "^4.0.8"
+ }
+ },
+ "@sinonjs/text-encoding": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz",
+ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
+ "dev": true
+ },
"@szmarczak/http-timer": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
@@ -724,6 +759,14 @@
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true
},
+ "async-retry": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
+ "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==",
+ "requires": {
+ "retry": "0.13.1"
+ }
+ },
"ava": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ava/-/ava-2.4.0.tgz",
@@ -1581,14 +1624,6 @@
"time-zone": "^1.0.0"
}
},
- "debug": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
- "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
- "requires": {
- "ms": "^2.1.1"
- }
- },
"debug-log": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/debug-log/-/debug-log-1.0.1.tgz",
@@ -1762,6 +1797,12 @@
}
}
},
+ "diff": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
+ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
+ "dev": true
+ },
"dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -1948,6 +1989,15 @@
"which": "^1.2.9"
}
},
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
"ignore": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
@@ -3567,6 +3617,12 @@
"set-immediate-shim": "~1.0.1"
}
},
+ "just-extend": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz",
+ "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==",
+ "dev": true
+ },
"keyv": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
@@ -3651,6 +3707,12 @@
"integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
"dev": true
},
+ "lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
+ "dev": true
+ },
"lodash.islength": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.islength/-/lodash.islength-4.0.1.tgz",
@@ -3895,6 +3957,16 @@
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ }
}
},
"nested-error-stacks": {
@@ -3909,6 +3981,19 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
+ "nise": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz",
+ "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==",
+ "dev": true,
+ "requires": {
+ "@sinonjs/commons": "^1.7.0",
+ "@sinonjs/fake-timers": "^7.0.4",
+ "@sinonjs/text-encoding": "^0.7.1",
+ "just-extend": "^4.0.2",
+ "path-to-regexp": "^1.7.0"
+ }
+ },
"nock": {
"version": "11.9.1",
"resolved": "https://registry.npmjs.org/nock/-/nock-11.9.1.tgz",
@@ -4349,6 +4434,23 @@
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
+ "path-to-regexp": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
+ "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+ "dev": true,
+ "requires": {
+ "isarray": "0.0.1"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+ "dev": true
+ }
+ }
+ },
"path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -4905,6 +5007,11 @@
"signal-exit": "^3.0.2"
}
},
+ "retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="
+ },
"reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -5019,6 +5126,37 @@
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
"dev": true
},
+ "sinon": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz",
+ "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==",
+ "dev": true,
+ "requires": {
+ "@sinonjs/commons": "^1.8.3",
+ "@sinonjs/fake-timers": "^7.1.2",
+ "@sinonjs/samsam": "^6.0.2",
+ "diff": "^5.0.0",
+ "nise": "^5.1.0",
+ "supports-color": "^7.2.0"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
"slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -5632,6 +5770,12 @@
"prelude-ls": "~1.1.2"
}
},
+ "type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true
+ },
"type-fest": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",
diff --git a/package.json b/package.json
index be9c689..f16c73d 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
"coverage:integration": "nyc --no-clean --silent npm run test:integration",
"coverage:report": "nyc report --reporter=lcov --reporter=text-summary",
"coverage:upload": "codecov",
- "check-deps-size": "./tools/check_size.sh 1100",
+ "check-deps-size": "./tools/check_size.sh 2000",
"lint": "standard"
},
"repository": {
@@ -47,9 +47,11 @@
"nock": "^11.7.0",
"nyc": "^14.1.1",
"pre-commit": "^1.2.2",
+ "sinon": "^11.1.2",
"standard": "^12.0.1"
},
"dependencies": {
+ "async-retry": "^1.3.3",
"needle": "^2.4.0"
}
}
diff --git a/test/unit/client.test.js b/test/unit/client.test.js
index 99f473d..7e93c60 100644
--- a/test/unit/client.test.js
+++ b/test/unit/client.test.js
@@ -21,19 +21,95 @@ const test = require('ava')
const Client = require('../../lib/client')
const http = require('http')
const nock = require('nock')
+const sinon = require('sinon')
+
+// Note: All client.request tests have to come before any of the proxy tests, as they interfere
+
+test('should return response', async t => {
+ const client = new Client({ api_key: 'secret', apihost: 'test_host', proxy: '' })
+ const METHOD = 'GET'
+ // NOTE: paths must be different as tests are run in parallel and adding/removing nock
+ // interceptors for a same path will create race conditions.
+ const PATH = '/return/response'
+
+ const mock = nock('https://test_host').get(PATH).times(1).reply(200, 'all good')
+ const result = await client.request(METHOD, PATH, {})
+ t.is(result.toString(), 'all good')
+ mock.interceptors.forEach(nock.removeInterceptor)
+})
-// Note: this has to come before any of the proxy tests, as they interfere
test('should handle http request errors', async t => {
const client = new Client({ api_key: 'secret', apihost: 'test_host', proxy: '' })
const METHOD = 'GET'
- const PATH = '/some/path'
+ const PATH = '/handle/error'
- nock('https://test_host').get(PATH).replyWithError('simulated error')
+ const mock = nock('https://test_host').get(PATH).times(1).replyWithError('simulated error')
const error = await t.throwsAsync(client.request(METHOD, PATH, {}))
t.truthy(error.message)
t.assert(error.message.includes('simulated error'))
+ mock.interceptors.forEach(nock.removeInterceptor)
+})
+
+test('should support retries on error', async t => {
+ const retrySpy = sinon.spy()
+ const client = new Client({ api_key: 'secret', apihost: 'test_host', proxy: '', retry: { retries: 2, onRetry: retrySpy } })
+ const METHOD = 'GET'
+ const PATH = '/retry/on/error'
+
+ const mock = nock('https://test_host')
+ .get(PATH).times(2).replyWithError('simulated error')
+ .get(PATH).times(1).reply(200, 'now all good')
+ const result = await client.request(METHOD, PATH, {})
+ t.is(result.toString(), 'now all good')
+ t.is(retrySpy.callCount, 2)
+ mock.interceptors.forEach(nock.removeInterceptor)
+})
+
+test('should not retry on success', async t => {
+ const retrySpy = sinon.spy()
+ const client = new Client({ api_key: 'secret', apihost: 'test_host', proxy: '', retry: { retries: 10, onRetry: retrySpy } })
+ const METHOD = 'GET'
+ const PATH = '/no/retry/on/sucess'
+
+ const mock = nock('https://test_host')
+ .get(PATH).times(1).reply(200, 'now all good')
+ const result = await client.request(METHOD, PATH, {})
+ t.is(result.toString(), 'now all good')
+ t.is(retrySpy.callCount, 0) // => no retries
+ mock.interceptors.forEach(nock.removeInterceptor)
})
+test('should not retry when no retry config available', async t => {
+ const client = new Client({ api_key: 'secret', apihost: 'test_host', proxy: '' })
+ const METHOD = 'GET'
+ const PATH = '/no/config/no/retry'
+
+ const mock = nock('https://test_host')
+ .get(PATH).times(1).replyWithError('simulated error')
+ .get(PATH).times(1).reply(200, 'now all good')
+ const error = await t.throwsAsync(client.request(METHOD, PATH, {}))
+ t.truthy(error.message)
+ t.assert(error.message.includes('simulated error'))
+ mock.interceptors.forEach(nock.removeInterceptor)
+})
+
+test('should handle errors even after retries', async t => {
+ const retrySpy = sinon.spy()
+ const client = new Client({ api_key: 'secret', apihost: 'test_host', proxy: '', retry: { retries: 2, onRetry: retrySpy } })
+ const METHOD = 'GET'
+ const PATH = '/handle/error/on/retry'
+
+ const mock = nock('https://test_host')
+ .get(PATH).times(3).replyWithError('simulated error')
+ .get(PATH).times(1).reply(200, 'not enough retries to come here')
+ const error = await t.throwsAsync(client.request(METHOD, PATH, {}))
+ t.truthy(error.message)
+ t.assert(error.message.includes('simulated error'))
+ mock.interceptors.forEach(nock.removeInterceptor)
+})
+
+// end client request tests
+
test('should use default constructor options', t => {
const client = new Client({ api_key: 'aaa', apihost: 'my_host' })
t.false(client.options.ignoreCerts)
diff --git a/tools/check_size.sh b/tools/check_size.sh
index a2b07ac..a65b03f 100755
--- a/tools/check_size.sh
+++ b/tools/check_size.sh
@@ -33,7 +33,7 @@ cd $UNPACK_DIR
tar -xzf openwhisk-*.tgz
cd package
-npm install --production --silent
+npm install --only=production --silent
cd node_modules
NODE_MODULES_SIZE=$(du -ks | cut -f 1)