From 33295b20441eefbbe270148d8a289cd9eb545dd2 Mon Sep 17 00:00:00 2001 From: Philippe Suter <psuter@us.ibm.com> Date: Mon, 22 Feb 2016 15:51:52 -0500 Subject: [PATCH] Compile+run rather than interpret Swift actions. Swift actions are now compiled at `init` time, then ran as a simple binary, rather than always be interpreted using `swift`. First invocation is only slighltly slower, all following are dramatically faster. Compile-time errors are reported as before. Note that if/when the container gets garbage-collected, the binary is not persisted so will be recompiled at following deployment. --- core/swiftAction/Dockerfile | 6 +-- core/swiftAction/proxy.py | 42 +++++++++++++++---- .../SwiftActionContainerTests.scala | 22 +++++----- 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/core/swiftAction/Dockerfile b/core/swiftAction/Dockerfile index e2a4acbb..94831afc 100644 --- a/core/swiftAction/Dockerfile +++ b/core/swiftAction/Dockerfile @@ -35,11 +35,11 @@ RUN SWIFT_ARCHIVE_NAME=swift-$SWIFT_VERSION-$SWIFT_PLATFORM && \ # Copy the Flask proxy. Following the pattern in nodejsAction. ADD . /swiftAction -RUN cd /swiftAction; rm -rf .project .settings build.xml Dockerfile +RUN cd /swiftAction; rm -rf .project .settings build.xml Dockerfile # Where the script will live. -RUN mkdir -p /swiftActionSource -RUN touch /swiftActionSource/action.swift +RUN mkdir -p /swiftAction +RUN touch /swiftAction/action.swift ENV FLASK_PROXY_PORT 8080 diff --git a/core/swiftAction/proxy.py b/core/swiftAction/proxy.py index e298465f..64aab236 100644 --- a/core/swiftAction/proxy.py +++ b/core/swiftAction/proxy.py @@ -28,8 +28,11 @@ proxy = flask.Flask(__name__) proxy.debug = False SRC_EPILOGUE_FILE = "./epilogue.swift" -DEST_SCRIPT_FILE = "/swiftActionSource/action.swift" -PROCESS = [ "swift", DEST_SCRIPT_FILE ] +DEST_SCRIPT_FILE = "/swiftAction/action.swift" +DEST_BIN_FILE = "/swiftAction/action" +BUILD_PROCESS = [ "swiftc", "-O", DEST_SCRIPT_FILE, "-o", DEST_BIN_FILE ] +# RUN_PROCESS = [ "swift", DEST_SCRIPT_FILE ] +RUN_PROCESS = [ DEST_BIN_FILE ] @proxy.route("/init", methods=['POST']) def init(): @@ -44,6 +47,22 @@ def init(): fp.write(str(message["code"])) with codecs.open(SRC_EPILOGUE_FILE, "r", "utf-8") as ep: fp.write(ep.read()) + + p = subprocess.Popen(BUILD_PROCESS) + + (o,e) = p.communicate() + + if o is not None: + sys.stdout.write(o) + + if e is not None: + sys.stderr.write(e) + + if not (os.path.isfile(DEST_BIN_FILE) and os.access(DEST_BIN_FILE, os.X_OK)): + response = flask.jsonify({"error": "the action failed to compile. See logs for details." }) + response.status_code = 502 + return response + return ('OK', 200) else: flask.abort(403) @@ -63,10 +82,15 @@ def run(): if not isinstance(value, dict): flask.abort(403) + if not (os.path.isfile(DEST_BIN_FILE) and os.access(DEST_BIN_FILE, os.X_OK)): + response = flask.jsonify({ "error": "the action failed to compile. See logs for details." }) + response.status_code = 502 + return response + swift_env_in = { "WHISK_INPUT" : json.dumps(value) } p = subprocess.Popen( - PROCESS, + RUN_PROCESS, stdin=subprocess.PIPE, stdout=subprocess.PIPE, env=swift_env_in) @@ -93,13 +117,13 @@ def run(): response = flask.jsonify(json_output) return response else: - reponse = { "error": "the action did not return an object", "action_output": json_output } - reponse.status_code = 502 + response = flask.jsonify({ "error": "the action did not return an object", "action_output": json_output }) + response.status_code = 502 return response - except: - sys.stderr.write("Couldn't parse Swift script output as JSON: %s.\n" % last_line) - json_output = { "error": "the action did not return a valid result" } - response = flask.jsonify(json_output) + except Exception as e: + # sys.stderr.write("Couldn't parse Swift script output as JSON: %s.\n" % last_line) + # sys.stderr.write("%s\n%s\n" % (str(e),repr(e))) + response = flask.jsonify({ "error": "the action did not return a valid result" }) response.status_code = 502 return response diff --git a/tests/src/actionContainers/SwiftActionContainerTests.scala b/tests/src/actionContainers/SwiftActionContainerTests.scala index cb9ba68f..58d4c93b 100644 --- a/tests/src/actionContainers/SwiftActionContainerTests.scala +++ b/tests/src/actionContainers/SwiftActionContainerTests.scala @@ -72,8 +72,13 @@ class SwiftActionContainerTests extends FlatSpec it should "return some error on action error" in { withSwiftContainer { c => val code = """ + | // You need an indirection, or swiftc detects the div/0 + | // at compile-time. Smart. + | func div(x: Int, _ y: Int) -> Int { + | return x/y + | } | func main(args: [String: Any]) -> [String: Any] { - | return [ "divBy0": 5/0 ] + | return [ "divBy0": div(5,0) ] | } """.stripMargin @@ -96,18 +101,15 @@ class SwiftActionContainerTests extends FlatSpec """.stripMargin val (initCode, _) = c.init(initPayload(code)) + initCode should not be(200) - // Unfortunately we don't know how to test valid Swift code for now. - // initCode should not be(200) - - val (runCode, _) = c.run(runPayload(JsObject("basic" -> JsString("forever")))) - + val (runCode, runRes) = c.run(runPayload(JsObject("basic" -> JsString("forever")))) runCode should be(502) } - err.toLowerCase should include("error") } + it should "support application errors" in { withSwiftContainer { c => val code = """ @@ -129,16 +131,14 @@ class SwiftActionContainerTests extends FlatSpec it should "enforce that the user returns an object" in { withSwiftContainer { c => - // Funny how type inference lets you omit the return type and shoot - // yourself in the foot here. val code = """ - | func main(args: [String: Any]) { + | func main(args: [String: Any]) -> String { | return "rebel, rebel" | } """.stripMargin val (initCode, _) = c.init(initPayload(code)) - initCode should be(200) + initCode should be(200) // This could change if the action wrapper has strong type checks for `main`. val (runCode, runRes) = c.run(runPayload(JsObject())) runCode should be(502) -- GitLab