Skip to content
Snippets Groups Projects
Commit 33295b20 authored by Philippe Suter's avatar Philippe Suter Committed by Rodric Rabbah
Browse files

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.
parent 12d311e8
No related branches found
No related tags found
No related merge requests found
...@@ -35,11 +35,11 @@ RUN SWIFT_ARCHIVE_NAME=swift-$SWIFT_VERSION-$SWIFT_PLATFORM && \ ...@@ -35,11 +35,11 @@ RUN SWIFT_ARCHIVE_NAME=swift-$SWIFT_VERSION-$SWIFT_PLATFORM && \
# Copy the Flask proxy. Following the pattern in nodejsAction. # Copy the Flask proxy. Following the pattern in nodejsAction.
ADD . /swiftAction 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. # Where the script will live.
RUN mkdir -p /swiftActionSource RUN mkdir -p /swiftAction
RUN touch /swiftActionSource/action.swift RUN touch /swiftAction/action.swift
ENV FLASK_PROXY_PORT 8080 ENV FLASK_PROXY_PORT 8080
......
...@@ -28,8 +28,11 @@ proxy = flask.Flask(__name__) ...@@ -28,8 +28,11 @@ proxy = flask.Flask(__name__)
proxy.debug = False proxy.debug = False
SRC_EPILOGUE_FILE = "./epilogue.swift" SRC_EPILOGUE_FILE = "./epilogue.swift"
DEST_SCRIPT_FILE = "/swiftActionSource/action.swift" DEST_SCRIPT_FILE = "/swiftAction/action.swift"
PROCESS = [ "swift", DEST_SCRIPT_FILE ] 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']) @proxy.route("/init", methods=['POST'])
def init(): def init():
...@@ -44,6 +47,22 @@ def init(): ...@@ -44,6 +47,22 @@ def init():
fp.write(str(message["code"])) fp.write(str(message["code"]))
with codecs.open(SRC_EPILOGUE_FILE, "r", "utf-8") as ep: with codecs.open(SRC_EPILOGUE_FILE, "r", "utf-8") as ep:
fp.write(ep.read()) 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) return ('OK', 200)
else: else:
flask.abort(403) flask.abort(403)
...@@ -63,10 +82,15 @@ def run(): ...@@ -63,10 +82,15 @@ def run():
if not isinstance(value, dict): if not isinstance(value, dict):
flask.abort(403) 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) } swift_env_in = { "WHISK_INPUT" : json.dumps(value) }
p = subprocess.Popen( p = subprocess.Popen(
PROCESS, RUN_PROCESS,
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
env=swift_env_in) env=swift_env_in)
...@@ -93,13 +117,13 @@ def run(): ...@@ -93,13 +117,13 @@ def run():
response = flask.jsonify(json_output) response = flask.jsonify(json_output)
return response return response
else: else:
reponse = { "error": "the action did not return an object", "action_output": json_output } response = flask.jsonify({ "error": "the action did not return an object", "action_output": json_output })
reponse.status_code = 502 response.status_code = 502
return response return response
except: except Exception as e:
sys.stderr.write("Couldn't parse Swift script output as JSON: %s.\n" % last_line) # 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" } # sys.stderr.write("%s\n%s\n" % (str(e),repr(e)))
response = flask.jsonify(json_output) response = flask.jsonify({ "error": "the action did not return a valid result" })
response.status_code = 502 response.status_code = 502
return response return response
......
...@@ -72,8 +72,13 @@ class SwiftActionContainerTests extends FlatSpec ...@@ -72,8 +72,13 @@ class SwiftActionContainerTests extends FlatSpec
it should "return some error on action error" in { it should "return some error on action error" in {
withSwiftContainer { c => withSwiftContainer { c =>
val code = """ 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] { | func main(args: [String: Any]) -> [String: Any] {
| return [ "divBy0": 5/0 ] | return [ "divBy0": div(5,0) ]
| } | }
""".stripMargin """.stripMargin
...@@ -96,18 +101,15 @@ class SwiftActionContainerTests extends FlatSpec ...@@ -96,18 +101,15 @@ class SwiftActionContainerTests extends FlatSpec
""".stripMargin """.stripMargin
val (initCode, _) = c.init(initPayload(code)) val (initCode, _) = c.init(initPayload(code))
initCode should not be(200)
// Unfortunately we don't know how to test valid Swift code for now. val (runCode, runRes) = c.run(runPayload(JsObject("basic" -> JsString("forever"))))
// initCode should not be(200)
val (runCode, _) = c.run(runPayload(JsObject("basic" -> JsString("forever"))))
runCode should be(502) runCode should be(502)
} }
err.toLowerCase should include("error") err.toLowerCase should include("error")
} }
it should "support application errors" in { it should "support application errors" in {
withSwiftContainer { c => withSwiftContainer { c =>
val code = """ val code = """
...@@ -129,16 +131,14 @@ class SwiftActionContainerTests extends FlatSpec ...@@ -129,16 +131,14 @@ class SwiftActionContainerTests extends FlatSpec
it should "enforce that the user returns an object" in { it should "enforce that the user returns an object" in {
withSwiftContainer { c => withSwiftContainer { c =>
// Funny how type inference lets you omit the return type and shoot
// yourself in the foot here.
val code = """ val code = """
| func main(args: [String: Any]) { | func main(args: [String: Any]) -> String {
| return "rebel, rebel" | return "rebel, rebel"
| } | }
""".stripMargin """.stripMargin
val (initCode, _) = c.init(initPayload(code)) 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())) val (runCode, runRes) = c.run(runPayload(JsObject()))
runCode should be(502) runCode should be(502)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment