This is an automated email from the ASF dual-hosted git repository. markusthoemmes pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-openwhisk.git
The following commit(s) were added to refs/heads/master by this push: new 51f8c18 Action time limit test refactoring and cleanup. (#3485) 51f8c18 is described below commit 51f8c1820c122cf7353039029c0496e948d32f01 Author: Sven Lange-Last <sven.lange-l...@de.ibm.com> AuthorDate: Wed Mar 28 12:55:49 2018 +0200 Action time limit test refactoring and cleanup. (#3485) * Added variations where the requested timeout and memory size limit is slightly above the maximum allowed limit to detect fuzzy constraint checking. In the past, values were much higher than allowed for negative tests. * Move action creation tests verifying that creation is allowed/rejected depending on specified limits from `WskBasicUsageTests` suite to `ActionLimitsTests` suite. Said tests fit better into the limits test suite. * Improve automated test naming so that the meaning of limit values can easily be spotted, i.e. which category it belongs to: minimum, standard, maximum, below minimum, above maximum, other. * Before this change, the test used Python and Java actions with a fixed sleep time. With this approach, the test may fail if the default maximum timeout limit setting is changed. Introducing new test actions `sleep.jar` (Java including source) and `sleep.py` (Python) which sleep as many milliseconds as specified during invocation. * `timeout.js` has been replaced by `sleep.js`. * `timedout.py` has been replaced by `sleep.py`. * `timedout.jar` has been replaced by `sleep.jar`. `TimedOut.java` is the Java source file for `timedout.jar`. * Use CPU-friendly `sleep.js` action instead of CPU-intensive `timeout.js` action --- tests/dat/actions/Sleep.java | 52 +++++++ tests/dat/actions/TimedOut.java | 24 --- tests/dat/actions/sleep.jar | Bin 0 -> 1167 bytes tests/dat/actions/sleep.js | 40 +++++ tests/dat/actions/sleep.py | 21 +++ tests/dat/actions/timedout.jar | Bin 771 -> 0 bytes tests/dat/actions/timedout.py | 7 - tests/dat/actions/timeout.js | 18 --- tests/src/test/scala/limits/ThrottleTests.scala | 12 +- .../test/scala/system/basic/WskBasicTests.scala | 14 +- .../test/scala/system/basic/WskSequenceTests.scala | 14 +- .../whisk/core/cli/test/WskBasicUsageTests.scala | 59 -------- .../whisk/core/limits/ActionLimitsTests.scala | 161 ++++++++++++++++++--- .../whisk/core/limits/MaxActionDurationTests.scala | 73 ++++++---- 14 files changed, 319 insertions(+), 176 deletions(-) diff --git a/tests/dat/actions/Sleep.java b/tests/dat/actions/Sleep.java new file mode 100644 index 0000000..1799852 --- /dev/null +++ b/tests/dat/actions/Sleep.java @@ -0,0 +1,52 @@ +/* + * 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. + */ + +/** + * Build instructions: + * - Assumption: the dependency GSON is in the local dicrectory, e.g. "gson-2.8.2.jar" + * - Compile with "javac -cp gson-2.8.2.jar Sleep.java" + * - Create .jar archive with "jar cvf sleep.jar Sleep.class" + */ + +/** + * Java based OpenWhisk action that sleeps for the specified number + * of milliseconds before returning. + * The function actually sleeps slightly longer than requested. + * + * @param parm JSON object with Number property sleepTimeInMs + * @returns JSON object with String property msg describing how long the function slept + */ + +import com.google.gson.JsonObject; +public class Sleep { + public static JsonObject main(JsonObject parm) throws InterruptedException { + int sleepTimeInMs = 1; + if (parm.has("sleepTimeInMs")) { + sleepTimeInMs = parm.getAsJsonPrimitive("sleepTimeInMs").getAsInt(); + } + System.out.println("Specified sleep time is " + sleepTimeInMs + " ms."); + + final String responseText = "Terminated successfully after around " + sleepTimeInMs + " ms."; + final JsonObject response = new JsonObject(); + response.addProperty("msg", responseText); + + Thread.sleep(sleepTimeInMs); + + System.out.println(responseText); + return response; + } +} diff --git a/tests/dat/actions/TimedOut.java b/tests/dat/actions/TimedOut.java deleted file mode 100644 index 4cfa3dd..0000000 --- a/tests/dat/actions/TimedOut.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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. - */ - -import com.google.gson.JsonObject; -public class TimedOut { - public static JsonObject main(JsonObject args) throws InterruptedException { - Thread.sleep(310000); - return new JsonObject(); - } -} diff --git a/tests/dat/actions/sleep.jar b/tests/dat/actions/sleep.jar new file mode 100644 index 0000000..536711b Binary files /dev/null and b/tests/dat/actions/sleep.jar differ diff --git a/tests/dat/actions/sleep.js b/tests/dat/actions/sleep.js new file mode 100644 index 0000000..c6c2e17 --- /dev/null +++ b/tests/dat/actions/sleep.js @@ -0,0 +1,40 @@ +/** + * Node.js based OpenWhisk action that sleeps for the specified number + * of milliseconds before returning. Uses a timer instead of a busy loop. + * The function actually sleeps slightly longer than requested. + * + * @param parm Object with Number property sleepTimeInMs + * @returns Object with String property msg describing how long the function slept + * or an error object on failure + */ +function main(parm) { + if(!('sleepTimeInMs' in parm)) { + const result = { error: "Parameter 'sleepTimeInMs' not specified." } + console.error(result.error) + return result + } + + if(!Number.isInteger(parm.sleepTimeInMs)) { + const result = { error: "Parameter 'sleepTimeInMs' must be an integer value." } + console.error(result.error) + return result + } + + if((parm.sleepTimeInMs < 0) || !Number.isFinite(parm.sleepTimeInMs)) { + const result = { error: "Parameter 'sleepTimeInMs' must be finite, postive integer value." } + console.error(result.error) + return result + } + + console.log("Specified sleep time is " + parm.sleepTimeInMs + " ms.") + + return new Promise(function(resolve, reject) { + const timeBeforeSleep = new Date() + setTimeout(function () { + const actualSleepTimeInMs = new Date() - timeBeforeSleep + const result = { msg: "Terminated successfully after around " + actualSleepTimeInMs + " ms." } + console.log(result.msg) + resolve(result) + }, parm.sleepTimeInMs) + }) +} diff --git a/tests/dat/actions/sleep.py b/tests/dat/actions/sleep.py new file mode 100644 index 0000000..c8b05db --- /dev/null +++ b/tests/dat/actions/sleep.py @@ -0,0 +1,21 @@ +# +# Python based OpenWhisk action that sleeps for the specified number +# of milliseconds before returning. +# The function actually sleeps slightly longer than requested. +# +# @param parm Object with Number property sleepTimeInMs +# @returns Object with String property msg describing how long the function slept +# +import sys +import time + +def main(parm): + sleepTimeInMs = parm.get("sleepTimeInMs", 1) + print("Specified sleep time is {} ms.".format(sleepTimeInMs)) + + result = { "msg": "Terminated successfully after around {} ms.".format(sleepTimeInMs) } + + time.sleep(sleepTimeInMs/1000.0) + + print(result['msg']) + return result diff --git a/tests/dat/actions/timedout.jar b/tests/dat/actions/timedout.jar deleted file mode 100644 index 4d8978e..0000000 Binary files a/tests/dat/actions/timedout.jar and /dev/null differ diff --git a/tests/dat/actions/timedout.py b/tests/dat/actions/timedout.py deleted file mode 100644 index 87d4785..0000000 --- a/tests/dat/actions/timedout.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Python timeout test.""" -import time - - -def main(dict): - """Main.""" - time.sleep(310) diff --git a/tests/dat/actions/timeout.js b/tests/dat/actions/timeout.js deleted file mode 100644 index f3bc74b..0000000 --- a/tests/dat/actions/timeout.js +++ /dev/null @@ -1,18 +0,0 @@ -function sleep(milliseconds) { - var start = new Date().getTime(); - while (true) { - var now = new Date().getTime(); - if ((now - start) > milliseconds){ - break; - } - } -} - -function main(msg) { - console.log('dosTimeout', 'timeout ' + msg.payload+''); - var n = msg.payload; - sleep(n); - var res = "[OK] message terminated successfully after " + msg.payload + " milliseconds."; - console.log('dosTimeout', 'result:', res); - return {msg: res}; -} diff --git a/tests/src/test/scala/limits/ThrottleTests.scala b/tests/src/test/scala/limits/ThrottleTests.scala index efeb909..f132995 100644 --- a/tests/src/test/scala/limits/ThrottleTests.scala +++ b/tests/src/test/scala/limits/ThrottleTests.scala @@ -235,7 +235,7 @@ class ThrottleTests it should "throttle 'concurrent' activations of one action" in withAssetCleaner(wskprops) { (wp, assetHelper) => val name = "checkConcurrentActionThrottle" assetHelper.withCleaner(wsk.action, name) { - val timeoutAction = Some(TestUtils.getTestActionFilename("timeout.js")) + val timeoutAction = Some(TestUtils.getTestActionFilename("sleep.js")) (action, _) => action.create(name, timeoutAction) } @@ -252,7 +252,10 @@ class ThrottleTests // These invokes will stay active long enough that all are issued and load balancer has recognized concurrency. val startSlowInvokes = Instant.now val slowResults = untilThrottled(slowInvokes) { () => - wsk.action.invoke(name, Map("payload" -> slowInvokeDuration.toMillis.toJson), expectedExitCode = DONTCARE_EXIT) + wsk.action.invoke( + name, + Map("sleepTimeInMs" -> slowInvokeDuration.toMillis.toJson), + expectedExitCode = DONTCARE_EXIT) } val afterSlowInvokes = Instant.now val slowIssueDuration = durationBetween(startSlowInvokes, afterSlowInvokes) @@ -266,7 +269,10 @@ class ThrottleTests // These fast invokes will trigger the concurrency-based throttling. val startFastInvokes = Instant.now val fastResults = untilThrottled(fastInvokes) { () => - wsk.action.invoke(name, Map("payload" -> slowInvokeDuration.toMillis.toJson), expectedExitCode = DONTCARE_EXIT) + wsk.action.invoke( + name, + Map("sleepTimeInMs" -> fastInvokeDuration.toMillis.toJson), + expectedExitCode = DONTCARE_EXIT) } val afterFastInvokes = Instant.now val fastIssueDuration = durationBetween(afterFastInvokes, startFastInvokes) diff --git a/tests/src/test/scala/system/basic/WskBasicTests.scala b/tests/src/test/scala/system/basic/WskBasicTests.scala index 9d9b0d6..6584a1c 100644 --- a/tests/src/test/scala/system/basic/WskBasicTests.scala +++ b/tests/src/test/scala/system/basic/WskBasicTests.scala @@ -26,7 +26,6 @@ import akka.http.scaladsl.model.StatusCodes.NotFound import java.time.Instant import scala.concurrent.duration.DurationInt -import scala.language.postfixOps import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner @@ -482,11 +481,16 @@ class WskBasicTests extends TestHelpers with WskTestHelpers { it should "create, and invoke an action that times out to ensure the proper response is received" in withAssetCleaner( wskprops) { (wp, assetHelper) => - val name = "sleepAction" - val params = Map("payload" -> "100000".toJson) - val allowedActionDuration = 120 seconds + val name = "sleepAction-" + System.currentTimeMillis() + // Must be larger than 60 seconds to see the expected exit code + val allowedActionDuration = 120.seconds + // Sleep time must be larger than 60 seconds to see the expected exit code + // Set sleep time to a value smaller than allowedActionDuration to not raise a timeout + val sleepTime = allowedActionDuration - 20.seconds + sleepTime should be >= 60.seconds + val params = Map("sleepTimeInMs" -> sleepTime.toMillis.toJson) val res = assetHelper.withCleaner(wsk.action, name) { (action, _) => - action.create(name, Some(TestUtils.getTestActionFilename("timeout.js")), timeout = Some(allowedActionDuration)) + action.create(name, Some(TestUtils.getTestActionFilename("sleep.js")), timeout = Some(allowedActionDuration)) action.invoke(name, parameters = params, result = true, expectedExitCode = Accepted.intValue) } } diff --git a/tests/src/test/scala/system/basic/WskSequenceTests.scala b/tests/src/test/scala/system/basic/WskSequenceTests.scala index f7dc39d..ca9ad0e 100644 --- a/tests/src/test/scala/system/basic/WskSequenceTests.scala +++ b/tests/src/test/scala/system/basic/WskSequenceTests.scala @@ -388,7 +388,7 @@ abstract class WskSequenceTests extends TestHelpers with ScalatestRouteTest with it should "execute a sequence in blocking fashion and finish execution even if longer than blocking response timeout" in withAssetCleaner( wskprops) { (wp, assetHelper) => val sName = "sSequence" - val sleep = "timeout" + val sleep = "sleep" val echo = "echo" // create actions @@ -403,22 +403,18 @@ abstract class WskSequenceTests extends TestHelpers with ScalatestRouteTest with assetHelper.withCleaner(wsk.action, sName) { (action, seqName) => action.create(seqName, artifact = Some(actions.mkString(",")), kind = Some("sequence")) } - // run sequence s with sleep equal to payload - val payload = 65000 + // run sequence s with sleep time + val sleepTime = 90 seconds val run = wsk.action.invoke( sName, - parameters = Map("payload" -> JsNumber(payload)), + parameters = Map("sleepTimeInMs" -> sleepTime.toMillis.toJson), blocking = true, expectedExitCode = ACCEPTED) withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 3 * allowedActionDuration) { activation => checkSequenceLogsAndAnnotations(activation, 2) // 2 actions activation.response.success shouldBe (true) - // the status should be error - //activation.response.status shouldBe("application error") val result = activation.response.result.get - // the result of the activation should be timeout - result shouldBe (JsObject( - "msg" -> JsString(s"[OK] message terminated successfully after $payload milliseconds."))) + result.toString should include("""Terminated successfully after around""") } } diff --git a/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala b/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala index 925c098..50d4c57 100644 --- a/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala +++ b/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala @@ -26,7 +26,6 @@ import java.time.Instant import java.time.Clock import scala.language.postfixOps -import scala.concurrent.duration.Duration import scala.concurrent.duration.DurationInt import scala.util.Random import org.junit.runner.RunWith @@ -41,9 +40,6 @@ import common.rest.WskRest import spray.json.DefaultJsonProtocol._ import spray.json._ import whisk.core.entity._ -import whisk.core.entity.LogLimit._ -import whisk.core.entity.MemoryLimit._ -import whisk.core.entity.TimeLimit._ import whisk.core.entity.size.SizeInt import TestJsonArgs._ import whisk.http.Messages @@ -699,59 +695,4 @@ class WskBasicUsageTests extends TestHelpers with WskTestHelpers { wsk.trigger.delete(triggerName).statusCode shouldBe OK } } - - behavior of "Wsk action parameters" - - it should "create an action with different permutations of limits" in withAssetCleaner(wskprops) { - (wp, assetHelper) => - val file = Some(TestUtils.getTestActionFilename("hello.js")) - - def testLimit(timeout: Option[Duration] = None, - memory: Option[ByteSize] = None, - logs: Option[ByteSize] = None, - ec: Int = SUCCESS_EXIT) = { - // Limits to assert, standard values if CLI omits certain values - val limits = JsObject( - "timeout" -> timeout.getOrElse(STD_DURATION).toMillis.toJson, - "memory" -> memory.getOrElse(stdMemory).toMB.toInt.toJson, - "logs" -> logs.getOrElse(STD_LOGSIZE).toMB.toInt.toJson) - - val name = "ActionLimitTests" + Instant.now.toEpochMilli - val createResult = assetHelper.withCleaner(wsk.action, name, confirmDelete = (ec == SUCCESS_EXIT)) { - (action, _) => - val result = action.create( - name, - file, - logsize = logs, - memory = memory, - timeout = timeout, - expectedExitCode = DONTCARE_EXIT) - withClue(s"create failed for parameters: timeout = $timeout, memory = $memory, logsize = $logs:") { - result.exitCode should be(ec) - } - result - } - - if (ec == SUCCESS_EXIT) { - val JsObject(parsedAction) = wsk.action.get(name).respBody - parsedAction("limits") shouldBe limits - } else { - createResult.stderr should include("allowed threshold") - } - } - - // Assert for valid permutations that the values are set correctly - for { - time <- Seq(None, Some(MIN_DURATION), Some(MAX_DURATION)) - mem <- Seq(None, Some(minMemory), Some(maxMemory)) - log <- Seq(None, Some(MIN_LOGSIZE), Some(MAX_LOGSIZE)) - } testLimit(time, mem, log) - - // Assert that invalid permutation are rejected - testLimit(Some(0.milliseconds), None, None, BAD_REQUEST) - testLimit(Some(100.minutes), None, None, BAD_REQUEST) - testLimit(None, Some(0.MB), None, BAD_REQUEST) - testLimit(None, Some(32768.MB), None, BAD_REQUEST) - testLimit(None, None, Some(32768.MB), BAD_REQUEST) - } } diff --git a/tests/src/test/scala/whisk/core/limits/ActionLimitsTests.scala b/tests/src/test/scala/whisk/core/limits/ActionLimitsTests.scala index 5c77f30..e820130 100644 --- a/tests/src/test/scala/whisk/core/limits/ActionLimitsTests.scala +++ b/tests/src/test/scala/whisk/core/limits/ActionLimitsTests.scala @@ -19,28 +19,25 @@ package whisk.core.limits import akka.http.scaladsl.model.StatusCodes.RequestEntityTooLarge import akka.http.scaladsl.model.StatusCodes.BadGateway - import java.io.File import java.io.PrintWriter +import java.time.Instant -import scala.concurrent.duration.DurationInt +import scala.concurrent.duration.{Duration, DurationInt} import scala.language.postfixOps - import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner - import common.ActivationResult import common.TestHelpers import common.TestUtils +import common.TestUtils.{BAD_REQUEST, DONTCARE_EXIT, SUCCESS_EXIT} import common.WhiskProperties import common.rest.WskRest import common.WskProps import common.WskTestHelpers import spray.json._ import spray.json.DefaultJsonProtocol._ -import whisk.core.entity.ActivationEntityLimit -import whisk.core.entity.ActivationResponse -import whisk.core.entity.Exec +import whisk.core.entity.{ActivationEntityLimit, ActivationResponse, ByteSize, Exec, LogLimit, MemoryLimit, TimeLimit} import whisk.core.entity.size._ import whisk.http.Messages @@ -50,7 +47,7 @@ class ActionLimitsTests extends TestHelpers with WskTestHelpers { implicit val wskprops = WskProps() val wsk = new WskRest - val defaultDosAction = TestUtils.getTestActionFilename("timeout.js") + val defaultSleepAction = TestUtils.getTestActionFilename("sleep.js") val allowedActionDuration = 10 seconds val testActionsDir = WhiskProperties.getFileRelativeToWhiskHome("tests/dat/actions") @@ -63,37 +60,155 @@ class ActionLimitsTests extends TestHelpers with WskTestHelpers { behavior of "Action limits" /** - * Test a long running action that exceeds the maximum execution time allowed for action - * by setting the action limit explicitly and attempting to run the action for an additional second. + * Helper class for the integration test following below. + * @param timeout the action timeout limit to be set in test + * @param memory the action memory size limit to be set in test + * @param logs the action log size limit to be set in test + * @param ec the expected exit code when creating the action + */ + sealed case class PermutationTestParameter(timeout: Option[Duration] = None, + memory: Option[ByteSize] = None, + logs: Option[ByteSize] = None, + ec: Int = SUCCESS_EXIT) { + override def toString: String = + s"timeout: ${toTimeoutString}, memory: ${toMemoryString}, logsize: ${toLogsString}" + + val toTimeoutString = timeout match { + case None => "None" + case Some(TimeLimit.MIN_DURATION) => s"${TimeLimit.MIN_DURATION} (= min)" + case Some(TimeLimit.STD_DURATION) => s"${TimeLimit.STD_DURATION} (= std)" + case Some(TimeLimit.MAX_DURATION) => s"${TimeLimit.MAX_DURATION} (= max)" + case Some(t) if (t < TimeLimit.MIN_DURATION) => s"${t} (< min)" + case Some(t) if (t > TimeLimit.MAX_DURATION) => s"${t} (> max)" + case Some(t) => s"${t} (allowed)" + } + + val toMemoryString = memory match { + case None => "None" + case Some(MemoryLimit.minMemory) => s"${MemoryLimit.minMemory.toMB.MB} (= min)" + case Some(MemoryLimit.stdMemory) => s"${MemoryLimit.stdMemory.toMB.MB} (= std)" + case Some(MemoryLimit.maxMemory) => s"${MemoryLimit.maxMemory.toMB.MB} (= max)" + case Some(m) if (m < MemoryLimit.minMemory) => s"${m.toMB.MB} (< min)" + case Some(m) if (m > MemoryLimit.maxMemory) => s"${m.toMB.MB} (> max)" + case Some(m) => s"${m.toMB.MB} (allowed)" + } + + val toLogsString = logs match { + case None => "None" + case Some(LogLimit.MIN_LOGSIZE) => s"${LogLimit.MIN_LOGSIZE} (= min)" + case Some(LogLimit.STD_LOGSIZE) => s"${LogLimit.STD_LOGSIZE} (= std)" + case Some(LogLimit.MAX_LOGSIZE) => s"${LogLimit.MAX_LOGSIZE} (= max)" + case Some(l) if (l < LogLimit.MIN_LOGSIZE) => s"${l} (< min)" + case Some(l) if (l > LogLimit.MAX_LOGSIZE) => s"${l} (> max)" + case Some(l) => s"${l} (allowed)" + } + + val toExpectedResultString: String = if (ec == SUCCESS_EXIT) "allow" else "reject" + } + + val perms = { // Assert for valid permutations that the values are set correctly + for { + time <- Seq(None, Some(TimeLimit.MIN_DURATION), Some(TimeLimit.MAX_DURATION)) + mem <- Seq(None, Some(MemoryLimit.minMemory), Some(MemoryLimit.maxMemory)) + log <- Seq(None, Some(LogLimit.MIN_LOGSIZE), Some(LogLimit.MAX_LOGSIZE)) + } yield PermutationTestParameter(time, mem, log) + } ++ + // Add variations for negative tests + Seq( + PermutationTestParameter(Some(0.milliseconds), None, None, BAD_REQUEST), // timeout that is lower than allowed + PermutationTestParameter(Some(TimeLimit.MAX_DURATION.plus(1 second)), None, None, BAD_REQUEST), // timeout that is slightly higher than allowed + PermutationTestParameter(Some(TimeLimit.MAX_DURATION * 10), None, None, BAD_REQUEST), // timeout that is much higher than allowed + PermutationTestParameter(None, Some(0.MB), None, BAD_REQUEST), // memory limit that is lower than allowed + PermutationTestParameter(None, Some(MemoryLimit.maxMemory + 1.MB), None, BAD_REQUEST), // memory limit that is slightly higher than allowed + PermutationTestParameter(None, Some((MemoryLimit.maxMemory.toMB * 5).MB), None, BAD_REQUEST), // memory limit that is much higher than allowed + PermutationTestParameter(None, None, Some((LogLimit.MAX_LOGSIZE.toMB * 5).MB), BAD_REQUEST)) // log size limit that is much higher than allowed + + /** + * Integration test to verify that valid timeout, memory and log size limits are accepted + * when creating an action while any invalid limit is rejected. + * + * At the first sight, this test looks like a typical unit test that should not be performed + * as an integration test. It is performed as an integration test requiring an OpenWhisk + * deployment to verify that limit settings of the tested deployment fit with the values + * used in this test. + */ + perms.foreach { parm => + it should s"${parm.toExpectedResultString} creation of an action with these limits: ${parm}" in withAssetCleaner( + wskprops) { (wp, assetHelper) => + val file = Some(TestUtils.getTestActionFilename("hello.js")) + + // Limits to assert, standard values if CLI omits certain values + val limits = JsObject( + "timeout" -> parm.timeout.getOrElse(TimeLimit.STD_DURATION).toMillis.toJson, + "memory" -> parm.memory.getOrElse(MemoryLimit.stdMemory).toMB.toInt.toJson, + "logs" -> parm.logs.getOrElse(LogLimit.STD_LOGSIZE).toMB.toInt.toJson) + + val name = "ActionLimitTests-" + Instant.now.toEpochMilli + val createResult = assetHelper.withCleaner(wsk.action, name, confirmDelete = (parm.ec == SUCCESS_EXIT)) { + (action, _) => + val result = action.create( + name, + file, + logsize = parm.logs, + memory = parm.memory, + timeout = parm.timeout, + expectedExitCode = DONTCARE_EXIT) + withClue(s"Unexpected result when creating action '${name}':\n${result.toString}\nFailed assertion:") { + result.exitCode should be(parm.ec) + } + result + } + + if (parm.ec == SUCCESS_EXIT) { + val JsObject(parsedAction) = wsk.action.get(name).respBody + parsedAction("limits") shouldBe limits + } else { + createResult.stderr should include("allowed threshold") + } + } + } + + /** + * Test an action that exceeds its specified time limit. Explicitly specify a rather low time + * limit to keep the test's runtime short. Invoke an action that sleeps for the specified time + * limit plus one second. */ it should "error with a proper warning if the action exceeds its time limits" in withAssetCleaner(wskprops) { (wp, assetHelper) => - val name = "TestActionCausingTimeout" + val name = "TestActionCausingTimeout-" + System.currentTimeMillis() assetHelper.withCleaner(wsk.action, name, confirmDelete = true) { (action, _) => - action.create(name, Some(defaultDosAction), timeout = Some(allowedActionDuration)) + action.create(name, Some(defaultSleepAction), timeout = Some(allowedActionDuration)) } - val run = wsk.action.invoke(name, Map("payload" -> allowedActionDuration.plus(1 second).toMillis.toJson)) - withActivation(wsk.activation, run) { - _.response.result.get.fields("error") shouldBe { - Messages.timedoutActivation(allowedActionDuration, false).toJson + val run = wsk.action.invoke(name, Map("sleepTimeInMs" -> allowedActionDuration.plus(1 second).toMillis.toJson)) + withActivation(wsk.activation, run) { result => + withClue("Activation result not as expected:") { + result.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ApplicationError) + result.response.result.get.fields("error") shouldBe { + Messages.timedoutActivation(allowedActionDuration, init = false).toJson + } + result.duration.toInt should be >= allowedActionDuration.toMillis.toInt } } } /** - * Test an action that does not exceed the allowed execution timeout of an action. + * Test an action that tightly stays within its specified time limit. Explicitly specify a rather low time + * limit to keep the test's runtime short. Invoke an action that sleeps for the specified time + * limit minus one second. */ it should "succeed on an action staying within its time limits" in withAssetCleaner(wskprops) { (wp, assetHelper) => - val name = "TestActionCausingNoTimeout" + val name = "TestActionCausingNoTimeout-" + System.currentTimeMillis() assetHelper.withCleaner(wsk.action, name, confirmDelete = true) { (action, _) => - action.create(name, Some(defaultDosAction), timeout = Some(allowedActionDuration)) + action.create(name, Some(defaultSleepAction), timeout = Some(allowedActionDuration)) } - val run = wsk.action.invoke(name, Map("payload" -> allowedActionDuration.minus(1 second).toMillis.toJson)) - withActivation(wsk.activation, run) { - _.response.result.get.toString should include("""[OK] message terminated successfully""") - + val run = wsk.action.invoke(name, Map("sleepTimeInMs" -> allowedActionDuration.minus(1 second).toMillis.toJson)) + withActivation(wsk.activation, run) { result => + withClue("Activation result not as expected:") { + result.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.Success) + result.response.result.get.toString should include("""Terminated successfully after around""") + } } } diff --git a/tests/src/test/scala/whisk/core/limits/MaxActionDurationTests.scala b/tests/src/test/scala/whisk/core/limits/MaxActionDurationTests.scala index dba2eeb..b35ffa8 100644 --- a/tests/src/test/scala/whisk/core/limits/MaxActionDurationTests.scala +++ b/tests/src/test/scala/whisk/core/limits/MaxActionDurationTests.scala @@ -32,6 +32,7 @@ import spray.json.DefaultJsonProtocol._ import spray.json._ import whisk.http.Messages import whisk.core.entity.TimeLimit +import org.scalatest.tagobjects.Slow /** * Tests for action duration limits. These tests require a deployed backend. @@ -42,38 +43,54 @@ class MaxActionDurationTests extends TestHelpers with WskTestHelpers { implicit val wskprops = WskProps() val wsk = new WskRest - // swift is not tested, because it uses the same proxy like python - "node-, python, and java-action" should "run up to the max allowed duration" in withAssetCleaner(wskprops) { - (wp, assetHelper) => - // When you add more runtimes, keep in mind, how many actions can be processed in parallel by the Invokers! - Map("node" -> "helloDeadline.js", "python" -> "timedout.py", "java" -> "timedout.jar").par.map { - case (k, name) => - assetHelper.withCleaner(wsk.action, name) { - if (k == "java") { (action, _) => - action.create( - name, - Some(TestUtils.getTestActionFilename(name)), - timeout = Some(TimeLimit.MAX_DURATION), - main = Some("TimedOut")) - } else { (action, _) => - action.create(name, Some(TestUtils.getTestActionFilename(name)), timeout = Some(TimeLimit.MAX_DURATION)) - } - } + /** + * Purpose of the following integration test is to verify that the action proxy + * supports the configured maximum action time limit and does not interrupt a + * running action before the invoker does. + * + * Action proxies have to run actions potentially endlessly. It's the invoker's + * duty to enforce action time limits. + * + * Background: in the past, the Node.js action proxy terminated an action + * before it actually reached its maximum action time limit. + * + * Swift is not tested because it uses the same action proxy as Python. + * + * ATTENTION: this test runs for at least TimeLimit.MAX_DURATION + 1 minute. + * With default settings, this is around 6 minutes. + */ + "node-, python, and java-action" should s"run up to the max allowed duration (${TimeLimit.MAX_DURATION})" taggedAs (Slow) in withAssetCleaner( + wskprops) { (wp, assetHelper) => + // When you add more runtimes, keep in mind, how many actions can be processed in parallel by the Invokers! + Map("node" -> "helloDeadline.js", "python" -> "sleep.py", "java" -> "sleep.jar").par.map { + case (k, name) => + println(s"Testing action kind '${k}' with action '${name}'") + assetHelper.withCleaner(wsk.action, name) { (action, _) => + val main = if (k == "java") Some("Sleep") else None + action.create( + name, + Some(TestUtils.getTestActionFilename(name)), + timeout = Some(TimeLimit.MAX_DURATION), + main = main) + } - val run = wsk.action.invoke(name, Map("forceHang" -> true.toJson)) - withActivation( - wsk.activation, - run, - initialWait = 1.minute, - pollPeriod = 1.minute, - totalWait = TimeLimit.MAX_DURATION + 1.minute) { activation => + val run = wsk.action.invoke( + name, + Map("forceHang" -> true.toJson, "sleepTimeInMs" -> (TimeLimit.MAX_DURATION + 30.seconds).toMillis.toJson)) + withActivation( + wsk.activation, + run, + initialWait = 1.minute, + pollPeriod = 1.minute, + totalWait = TimeLimit.MAX_DURATION + 2.minutes) { activation => + withClue("Activation result not as expected:") { activation.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ApplicationError) activation.response.result shouldBe Some( - JsObject("error" -> Messages.timedoutActivation(TimeLimit.MAX_DURATION, false).toJson)) + JsObject("error" -> Messages.timedoutActivation(TimeLimit.MAX_DURATION, init = false).toJson)) activation.duration.toInt should be >= TimeLimit.MAX_DURATION.toMillis.toInt - } - } + } + () // explicitly map to Unit + } } - } -- To stop receiving notification emails like this one, please contact markusthoem...@apache.org.