Use activation db agent if concurrency is not supported (#32)
Fixes Concurrency error when using wskdebug with IBM Cloud Functions #7
if concurrency > 1 is not supported, fallback to activation db agent
diff --git a/src/agentmgr.js b/src/agentmgr.js
index 8b59f28..15ea202 100644
--- a/src/agentmgr.js
+++ b/src/agentmgr.js
@@ -158,7 +158,7 @@
console.log("This OpenWhisk does not support action concurrency. Debugging will be a bit slower. Consider using '--ngrok' which might be a faster option.");
agentName = "polling activation db";
- agentCode = await this.getPollingActivationRecordAgent();
+ agentCode = await this.getPollingActivationDbAgent();
}
}
@@ -185,7 +185,17 @@
});
}
- await this.pushAgent(action, agentCode, backupName);
+ try {
+ await this.pushAgent(action, agentCode, backupName);
+ } catch (e) {
+ // openwhisk does not support concurrent nodejs actions, try with another
+ if (e.statusCode === 400 && e.error && typeof e.error.error === "string" && e.error.error.includes("concurrency")) {
+ console.log(`The Openwhisk server does not support concurrent actions, using alternative agent. Consider using --ngrok for a possibly faster agent.`);
+ this.concurrency = false;
+ agentCode = await this.getPollingActivationDbAgent();
+ await this.pushAgent(action, agentCode, backupName);
+ }
+ }
if (this.argv.verbose) {
console.log(`Agent installed.`);
@@ -259,6 +269,11 @@
const a = activations[0];
if (a.response && a.response.result && !this.activationsSeen[a.activationId]) {
activation = a;
+ if (!activation.response.success) {
+ throw {
+ error: activation
+ };
+ }
break;
}
}
@@ -389,7 +404,7 @@
return fs.readFileSync(`${__dirname}/../agent/agent-concurrency.js`, {encoding: 'utf8'});
}
- async getPollingActivationRecordAgent() {
+ async getPollingActivationDbAgent() {
// this needs 2 helper actions in addition to the agent in place of the action
await this.createHelperAction(`${this.actionName}_wskdebug_invoked`, `${__dirname}/../agent/echo.js`);
await this.createHelperAction(`${this.actionName}_wskdebug_completed`, `${__dirname}/../agent/echo.js`);
diff --git a/test/agentmgr.test.js b/test/agentmgr.test.js
new file mode 100644
index 0000000..cf069b7
--- /dev/null
+++ b/test/agentmgr.test.js
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-env mocha */
+
+'use strict';
+
+const Debugger = require("../src/debugger");
+
+const test = require('./test');
+
+describe('agentmgr', function() {
+ this.timeout(30000);
+
+ before(function() {
+ test.isDockerInstalled();
+ });
+
+ beforeEach(async function() {
+ await test.beforeEach();
+ });
+
+ afterEach(function() {
+ test.afterEach();
+ });
+
+ it("should use non-concurrrent agent if openwhisk does not support concurrency", async function() {
+ const action = "myaction";
+ const code = `const main = () => ({ msg: 'WRONG' });`;
+
+ test.mockAction(action, code);
+
+ test.mockCreateBackupAction(action);
+
+ // wskdebug overwriting the action with the agent
+ test.openwhiskNock()
+ .put(
+ `${test.openwhiskApiUrlActions()}/${action}?overwrite=true`,
+ body => body.annotations.some(v => v.key === "wskdebug" && v.value === true)
+ )
+ .matchHeader("authorization", test.openwhiskApiAuthHeader())
+ .reply(400, {
+ code: 'df940ccf1d076f103c3743685c25d2b2',
+ error: 'The request content was malformed:\nrequirement failed: concurrency 200 exceeds allowed threshold of 1'
+ });
+
+ // another wskdebug with non-concurrent action
+ test.openwhiskNock()
+ .put(
+ `${test.openwhiskApiUrlActions()}/${action}?overwrite=true`,
+ body => body.annotations.some(v => v.key === "wskdebug" && v.value === true)
+ )
+ .matchHeader("authorization", test.openwhiskApiAuthHeader())
+ .reply(200, test.nodejsActionDescription(action));
+
+ // helper actions for non-concurrent action
+ test.openwhiskNock()
+ .put(`${test.openwhiskApiUrlActions()}/${action}_wskdebug_invoked?overwrite=true`)
+ .matchHeader("authorization", test.openwhiskApiAuthHeader())
+ .reply(200, test.nodejsActionDescription(action));
+ test.openwhiskNock()
+ .put(`${test.openwhiskApiUrlActions()}/${action}_wskdebug_completed?overwrite=true`)
+ .matchHeader("authorization", test.openwhiskApiAuthHeader())
+ .reply(200, test.nodejsActionDescription(action));
+
+
+ // invocation
+ test.openwhiskNock()
+ .get(`${test.openwhiskApiUrl()}/activations`)
+ .query(query => query.name === `${action}_wskdebug_invoked`)
+ .matchHeader("authorization", test.openwhiskApiAuthHeader())
+ .reply(200, [{
+ activationId: "dummy-invoked",
+ response: {
+ success: true,
+ result: {
+ $activationId: "1234567890"
+ }
+ }
+ }]);
+
+ // completion of invocation
+ test.openwhiskNock()
+ .post(`${test.openwhiskApiUrlActions()}/${action}_wskdebug_completed?blocking=true`, {
+ msg: "CORRECT",
+ $activationId: "1234567890"
+ })
+ .matchHeader("authorization", test.openwhiskApiAuthHeader())
+ .reply(200, test.nodejsActionDescription(action));
+
+ // abort polling
+ test.openwhiskNock()
+ .get(`${test.openwhiskApiUrl()}/activations`)
+ .query(query => query.name === `${action}_wskdebug_invoked`)
+ .matchHeader("authorization", test.openwhiskApiAuthHeader())
+ .reply(200, [{
+ activationId: "dummy-invoked-2",
+ response: {
+ success: false,
+ result: {
+ error: {
+ code: 43
+ },
+ $activationId: "99999999999"
+ }
+ }
+ }]);
+
+ // shutdown/restore process
+ test.mockReadBackupAction(action, code);
+ test.mockRestoreAction(action, code);
+ test.mockRemoveBackupAction(action);
+ test.openwhiskNock()
+ .get(`${test.openwhiskApiUrlActions()}/${action}_wskdebug_invoked?code=false`)
+ .matchHeader("authorization", test.openwhiskApiAuthHeader())
+ .reply(200, {});
+ test.openwhiskNock()
+ .delete(`${test.openwhiskApiUrlActions()}/${action}_wskdebug_invoked`)
+ .matchHeader("authorization", test.openwhiskApiAuthHeader())
+ .reply(200, {});
+ test.openwhiskNock()
+ .get(`${test.openwhiskApiUrlActions()}/${action}_wskdebug_completed?code=false`)
+ .matchHeader("authorization", test.openwhiskApiAuthHeader())
+ .reply(200, {});
+ test.openwhiskNock()
+ .delete(`${test.openwhiskApiUrlActions()}/${action}_wskdebug_completed`)
+ .matchHeader("authorization", test.openwhiskApiAuthHeader())
+ .reply(200, {});
+
+
+ process.chdir("test/nodejs/plain-flat");
+ const argv = {
+ port: test.port,
+ action: "myaction",
+ sourcePath: `${process.cwd()}/action.js`,
+ invokeParams: '{ "key": "invocationOnSourceModification" }'
+ };
+
+ const dbgr = new Debugger(argv);
+ await dbgr.start();
+ dbgr.run();
+
+ // wait a bit
+ await test.sleep(500);
+
+ await dbgr.stop();
+
+ test.assertAllNocksInvoked();
+ });
+});
diff --git a/test/test.js b/test/test.js
index 5a266d7..c01f8c2 100644
--- a/test/test.js
+++ b/test/test.js
@@ -74,6 +74,11 @@
return openwhisk;
}
+function openwhiskApiUrl() {
+ return `/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}`;
+}
+
+
function openwhiskApiUrlActions() {
return `/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions`;
}
@@ -522,6 +527,7 @@
mockActionDoubleInvocation,
// advanced
openwhiskNock,
+ openwhiskApiUrl,
openwhiskApiUrlActions,
openwhiskApiAuthHeader,
mockAction,