diff --git a/invoker/function_invoker.py b/invoker/function_invoker.py index 96e0a79..4ffc146 100644 --- a/invoker/function_invoker.py +++ b/invoker/function_invoker.py @@ -26,6 +26,7 @@ from shutil import copyfile import grpc_server +import http_server def invoke_function(func,interaction_model, env): @@ -36,7 +37,11 @@ def invoke_function(func,interaction_model, env): :param env: a dict containing the runtime environment, usually os.environ :return: None """ - grpc_server.run(func, interaction_model, env.get("GRPC_PORT", 10382)) + + if env.get("GRPC_PORT") is not None: + grpc_server.run(func, interaction_model, env.get("GRPC_PORT", 10382)) + else: + http_server.run(func=func, port=int(env.get("HTTP_PORT", 8080))) def install_function(env): diff --git a/invoker/http_server.py b/invoker/http_server.py new file mode 100644 index 0000000..fdc3171 --- /dev/null +++ b/invoker/http_server.py @@ -0,0 +1,12 @@ +def run(func, port): + + from flask import Flask, request + + app = Flask("app") + + @app.route("/", methods=['POST', 'GET']) + def hello(): + arg = request.data.decode("utf-8") + return func(arg) + + app.run(port=port) \ No newline at end of file diff --git a/invoker/requirements.txt b/invoker/requirements.txt index 965b59a..64f8b92 100644 --- a/invoker/requirements.txt +++ b/invoker/requirements.txt @@ -1,2 +1,3 @@ protobuf -grpcio \ No newline at end of file +grpcio +flask \ No newline at end of file diff --git a/tests/test_http_functions.py b/tests/test_http_functions.py new file mode 100644 index 0000000..64a948c --- /dev/null +++ b/tests/test_http_functions.py @@ -0,0 +1,75 @@ +import sys + +from tests.utils import testutils + +import unittest +import subprocess +import os +import signal +import urllib.request + +sys.path.append('invoker') + +PYTHON = sys.executable + + +class HttpFunctionTest(unittest.TestCase): + """ + Spawns function_invoker in a separate process. + Assumes os.getcwd() is the project base directory + """ + + @classmethod + def setUpClass(cls): + cls.workingdir = os.path.abspath("./invoker") + cls.command = "%s function_invoker.py" % PYTHON + cls.PYTHONPATH = '%s:%s' % ('%s/tests/functions:$PYTHONPATH' % os.getcwd(), '%s/invoker' % os.getcwd()) + + def tearDown(self): + os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) + + def test_upper(self): + port = testutils.find_free_port() + + env = { + 'PYTHONPATH': self.PYTHONPATH, + 'HTTP_PORT': str(port), + 'FUNCTION_URI': 'file://%s/tests/functions/upper.py?handler=handle' % os.getcwd() + } + + self.process = subprocess.Popen(self.command, + cwd=self.workingdir, + shell=True, + env=env, + preexec_fn=os.setsid, + ) + + def generate_messages(): + messages = [ + ("hello", 'UTF-8'), + ("world", 'UTF-8'), + ("foo", 'UTF-8'), + ("bar", 'UTF-8'), + ] + for msg in messages: + yield msg + + responses = self.call_multiple_http_messages(port, generate_messages()) + expected = [b'HELLO', b'WORLD', b'FOO', b'BAR'] + + for response in responses: + self.assertTrue(response in expected) + + self.assertEqual(len(responses), len(expected)) + + def call_http(self, port, message): + url = 'http://localhost:' + str(port) + + req = urllib.request.Request(url, message.encode(), method="POST", headers={"content-type": "text/plain"}) + response = urllib.request.urlopen(req) + return response.read() + + def call_multiple_http_messages(self, port, messages): + import time + time.sleep(1) + return [self.call_http(port, message[0]) for message in messages]