diff --git a/core/python3Action/lib/launcher.py b/core/python3Action/lib/launcher.py index e6c5e31a..7b6d9a61 100755 --- a/core/python3Action/lib/launcher.py +++ b/core/python3Action/lib/launcher.py @@ -19,7 +19,7 @@ from sys import stdout from sys import stderr from os import fdopen -import sys, os, json, traceback, warnings +import sys, os, json, traceback, time log_sentinel="XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX\n" @@ -45,6 +45,30 @@ # now import the action as process input/output from main__ import main as main +class Context: + def __init__(self, env): + self.function_name = env["__OW_ACTION_NAME"] + self.function_version = env["__OW_ACTION_VERSION"] + self.activation_id = env["__OW_ACTIVATION_ID"] + self.request_id = env["__OW_TRANSACTION_ID"] + self.deadline = int(env["__OW_DEADLINE"]) + self.api_host = env["__OW_API_HOST"] + self.api_key = env.get("__OW_AUTH_KEY", "") + self.namespace = env["__OW_NAMESPACE"] + + def get_remaining_time_in_millis(self): + epoch_now_in_ms = int(time.time() * 1000) + delta_ms = self.deadline - epoch_now_in_ms + return delta_ms if delta_ms > 0 else 0 + +def fun(payload, env): + # Compatibility: Supports "old" context-less functions. + if main.__code__.co_argcount == 1: + return main(payload) + + # Lambda-like "new-style" function. + return main(payload, Context(env)) + out = fdopen(3, "wb") if os.getenv("__OW_WAIT_FOR_ACK", "") != "": out.write(json.dumps({"ok": True}, ensure_ascii=False).encode('utf-8')) @@ -64,7 +88,7 @@ env["__OW_%s" % key.upper()]= args[key] res = {} try: - res = main(payload) + res = fun(payload, env) except Exception as ex: print(traceback.format_exc(), file=stderr) res = {"error": str(ex)} diff --git a/core/python3Action/lib/prelauncher.py b/core/python3Action/lib/prelauncher.py index c02e41e3..e4bfb3dd 100755 --- a/core/python3Action/lib/prelauncher.py +++ b/core/python3Action/lib/prelauncher.py @@ -21,7 +21,7 @@ from sys import stdout from sys import stderr from os import fdopen -import sys, os, json, traceback, base64, io, zipfile +import sys, os, json, traceback, base64, io, zipfile, time log_sentinel="XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX\n" def write_sentinels(): @@ -97,6 +97,30 @@ def cannot_start(msg): except Exception as ex: cannot_start("Invalid action: %s\n" % str(ex)) +class Context: + def __init__(self, env): + self.function_name = env["__OW_ACTION_NAME"] + self.function_version = env["__OW_ACTION_VERSION"] + self.activation_id = env["__OW_ACTIVATION_ID"] + self.request_id = env["__OW_TRANSACTION_ID"] + self.deadline = int(env["__OW_DEADLINE"]) + self.api_host = env["__OW_API_HOST"] + self.api_key = env.get("__OW_AUTH_KEY", "") + self.namespace = env["__OW_NAMESPACE"] + + def get_remaining_time_in_millis(self): + epoch_now_in_ms = int(time.time() * 1000) + delta_ms = self.deadline - epoch_now_in_ms + return delta_ms if delta_ms > 0 else 0 + +def fun(payload, env): + # Compatibility: Supports "old" context-less functions. + if main.__code__.co_argcount == 1: + return main(payload) + + # Lambda-like "new-style" function. + return main(payload, Context(env)) + # Acknowledge the initialization. write_result({"ok": True}) @@ -113,7 +137,7 @@ def cannot_start(msg): os.environ["__OW_%s" % key.upper()]= args[key] res = {} try: - res = main(payload) + res = fun(payload, os.environ) except Exception as ex: print(traceback.format_exc(), file=stderr) res = {"error": str(ex)} diff --git a/tests/src/test/scala/runtime/actionContainers/PythonAdvancedTests.scala b/tests/src/test/scala/runtime/actionContainers/PythonAdvancedTests.scala index d4bd9d9a..007485ef 100644 --- a/tests/src/test/scala/runtime/actionContainers/PythonAdvancedTests.scala +++ b/tests/src/test/scala/runtime/actionContainers/PythonAdvancedTests.scala @@ -16,7 +16,9 @@ */ package runtime.actionContainers -import spray.json.{JsObject, JsString} +import spray.json._ +import spray.json.DefaultJsonProtocol._ +import java.time.Instant trait PythonAdvancedTests { this: PythonBasicTests => @@ -91,4 +93,58 @@ trait PythonAdvancedTests { e shouldBe empty }) } + + Map( + "prelaunched" -> Map.empty[String, String], + "non-prelaunched" -> Map("OW_INIT_IN_ACTIONLOOP" -> ""), + ).foreach { case (name, env) => + it should s"support a function with a lambda-like signature $name" in { + val (out, err) = withActionContainer(env + ("__OW_API_HOST" -> "testhost")) { c => + val code = + """ + |def main(event, context): + | return { + | "remaining_time": context.get_remaining_time_in_millis(), + | "activation_id": context.activation_id, + | "request_id": context.request_id, + | "function_name": context.function_name, + | "function_version": context.function_version, + | "api_host": context.api_host, + | "api_key": context.api_key, + | "namespace": context.namespace + | } + """.stripMargin + + val (initCode, _) = c.init(initPayload(code)) + initCode should be(200) + + val (runCode, out) = c.run(runPayload( + JsObject(), + Some(JsObject( + "deadline" -> Instant.now.plusSeconds(10).toEpochMilli.toString.toJson, + "activation_id" -> "testaid".toJson, + "transaction_id" -> "testtid".toJson, + "action_name" -> "testfunction".toJson, + "action_version" -> "0.0.1".toJson, + "namespace" -> "testnamespace".toJson, + "auth_key" -> "testkey".toJson + )) + )) + runCode should be(200) + + val remainingTime = out.get.fields("remaining_time").convertTo[Int] + remainingTime should be > 9500 // We give the test 500ms of slack to invoke the function to avoid flakes. + out shouldBe Some(JsObject( + "remaining_time" -> remainingTime.toJson, + "activation_id" -> "testaid".toJson, + "request_id" -> "testtid".toJson, + "function_name" -> "testfunction".toJson, + "function_version" -> "0.0.1".toJson, + "api_host" -> "testhost".toJson, + "api_key" -> "testkey".toJson, + "namespace" -> "testnamespace".toJson + )) + } + } + } }