Skip to content

Commit 4957f5f

Browse files
authored
Update python base image to support new funky server (#15)
Updates python base image to use funky proxy server. This simplifies the logic in the python server by moving log collection and function timeout responsibilities to the common funky server.
1 parent 88fd2dc commit 4957f5f

4 files changed

Lines changed: 66 additions & 120 deletions

File tree

‎Dockerfile‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
FROM vmware/photon2:20180424
22

3-
RUN tdnf install -y python3-3.6.5-1.ph2 python3-pip-3.6.5-1.ph2 && pip3 install --upgrade pip setuptools
3+
RUN tdnf install -y python3-3.6.5-1.ph2 python3-pip-3.6.5-1.ph2 gzip tar && pip3 install --upgrade pip setuptools
44

55
ARG IMAGE_TEMPLATE=/image-template
66
ARG FUNCTION_TEMPLATE=/function-template
7+
ARG servers=1
78

89
LABEL io.dispatchframework.imageTemplate="${IMAGE_TEMPLATE}" \
910
io.dispatchframework.functionTemplate="${FUNCTION_TEMPLATE}"
@@ -16,14 +17,17 @@ COPY validator /validator/
1617
COPY function-server /function-server/
1718
RUN pip install -r /function-server/requirements.txt
1819

19-
## Set WORKDIR and PORT, expose $PORT, cd to $WORKDIR
20+
## Set WORKDIR, PORT and SERVER_CMD, expose $PORT, cd to $WORKDIR
2021

21-
ENV WORKDIR=/function PORT=8080
22+
ENV WORKDIR=/function PORT=8080 SERVERS=$servers FUNKY_VERSION=0.1.1
2223

2324
EXPOSE ${PORT}
2425
WORKDIR ${WORKDIR}
2526

27+
RUN curl -L https://github.com/dispatchframework/funky/releases/download/${FUNKY_VERSION}/funky${FUNKY_VERSION}.linux-amd64.tgz -o funky${FUNKY_VERSION}.linux-amd64.tgz
28+
RUN tar -xzf funky${FUNKY_VERSION}.linux-amd64.tgz
29+
2630
# OpenFaaS readiness check depends on this file
2731
RUN touch /tmp/.lock
2832

29-
CMD python3 /function-server/main.py $(cat /tmp/handler)
33+
CMD SERVER_CMD="python3 /function-server/main.py $(cat /tmp/handler)" ./funky

‎function-server/main.py‎

Lines changed: 17 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,11 @@
1414
INPUT_ERROR = 'InputError'
1515
FUNCTION_ERROR = 'FunctionError'
1616
SYSTEM_ERROR = 'SystemError'
17-
HEALTHY = True
1817

1918

2019
class Health:
2120
@staticmethod
2221
def on_get(req, res):
23-
if not HEALTHY:
24-
res.status = falcon.HTTP_500
2522
res.body = '{}'
2623

2724

@@ -37,84 +34,37 @@ def get_msg(req):
3734
return msg
3835

3936

40-
def process_msg(msg, handle):
37+
def process_req(req, res, handle):
4138
r = None
4239
response = None
43-
err = None
44-
stacktrace = []
4540

46-
stderr = io.StringIO()
47-
old_stderr = sys.stderr
48-
49-
stdout = io.StringIO()
50-
old_stdout = sys.stdout
41+
try:
42+
msg = get_msg(req)
43+
except ValueError as e:
44+
res.status = falcon.HTTP_500
45+
return json.dumps({'type':SYSTEM_ERROR, 'stacktrace':traceback.format_exc().splitlines(), 'message' : str(e)}, ensure_ascii=False)
5146

5247
try:
53-
sys.stderr = stderr
54-
sys.stdout = stdout
55-
with ThreadPoolExecutor(max_workers=1) as executor:
56-
future = executor.submit(handle, msg['context'], msg['payload'])
57-
if msg['context'] is None:
58-
timeout = None
59-
else:
60-
timeout = msg['context'].get('timeout', None)
61-
r = future.result(None if timeout is None or timeout == 0 else timeout / 1000)
48+
r = handle(msg['context'], msg['payload'])
6249
except (ValueError, TypeError) as e:
63-
stacktrace = traceback.format_exc().splitlines()
64-
err = {'type': INPUT_ERROR, 'message': str(e), 'stacktrace': stacktrace}
65-
except TimeoutError as e:
66-
stacktrace = traceback.format_exc().splitlines()
67-
err = {'type': SYSTEM_ERROR, 'message': str(e), 'stacktrace': stacktrace}
68-
if not future.cancel():
69-
global HEALTHY
70-
HEALTHY = False
50+
res.status = falcon.HTTP_400
51+
return json.dumps({'type':INPUT_ERROR, 'stacktrace':traceback.format_exc().splitlines(), 'message' : str(e)}, ensure_ascii=False)
7152
except Exception as e:
72-
stacktrace = traceback.format_exc().splitlines()
73-
err = {'type': FUNCTION_ERROR, 'message': str(e), 'stacktrace': stacktrace}
74-
finally:
75-
sys.stderr = old_stderr
76-
stderr.flush()
77-
78-
sys.stdout = old_stdout
79-
stdout.flush()
80-
81-
context = {'error': err,
82-
'logs': {'stdout': read_logs(stdout),
83-
'stderr': read_logs(stderr) + stacktrace}}
53+
res.status = falcon.HTTP_500
54+
return json.dumps({'type':FUNCTION_ERROR, 'stacktrace':traceback.format_exc().splitlines(), 'message' : str(e)}, ensure_ascii=False)
8455

8556
try:
86-
response = json.dumps({'context': context, 'payload': r}, ensure_ascii=False)
57+
response = json.dumps(r, ensure_ascii=False)
8758
except TypeError as e:
8859
# Non-json serializable payload
89-
stacktrace = traceback.format_exc().splitlines()
90-
err = {'type': FUNCTION_ERROR, 'message': str(e), 'stacktrace': stacktrace}
91-
context['error'] = err
92-
context['logs']['stderr'].extend(stacktrace)
93-
response = json.dumps({'context': context, 'payload': None}, ensure_ascii=False)
60+
res.status = falcon.HTTP_422
61+
return json.dumps({'type':FUNCTION_ERROR, 'stacktrace':traceback.format_exc().splitlines(), 'message' : str(e)}, ensure_ascii=False)
9462
except Exception as e:
95-
stacktrace = traceback.format_exc().splitlines()
96-
err = {'type': SYSTEM_ERROR, 'message': str(e), 'stacktrace': stacktrace}
97-
context['error'] = err
98-
context['logs']['stderr'].extend(stacktrace)
99-
response = json.dumps({'context': context, 'payload': None}, ensure_ascii=False)
63+
res.status = falcon.HTTP_502
64+
return json.dumps({'type':SYSTEM_ERROR, 'stacktrace':traceback.format_exc().splitlines(), 'message' : str(e)}, ensure_ascii=False)
10065

10166
return response
10267

103-
104-
def process_req(req, f):
105-
try:
106-
msg = get_msg(req)
107-
except Exception as e:
108-
stacktrace = traceback.format_exc().splitlines()
109-
err = {'type': SYSTEM_ERROR, 'message': str(e), 'stacktrace': stacktrace}
110-
return json.dumps({'context': {'error': err,
111-
'logs': {'stdout': [],
112-
'stderr': stacktrace}},
113-
'payload': None},
114-
ensure_ascii=False)
115-
116-
return process_msg(msg, f)
117-
11868
def module_and_name(s):
11969
return s.rsplit('.', 1)
12070

@@ -130,7 +80,7 @@ def import_function(wd, func_fqn):
13080

13181
def exec_function(f):
13282
def handler(req, res):
133-
res.body = process_req(req, f)
83+
res.body = process_req(req, res, f)
13484

13585
return handler
13686

‎tests/test_main.py‎

Lines changed: 40 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ def __init__(self, content_length=0, stream=None):
4848
self.content_length = content_length
4949
self.stream = stream
5050

51+
class Response:
52+
"""
53+
Mock of a part of the falcon.Response class
54+
"""
55+
56+
def __init__(self, status=0):
57+
self.status = status
58+
5159

5260
class TestMainMethods(unittest.TestCase):
5361

@@ -82,89 +90,73 @@ def test_get_msg_empty(self):
8290

8391
self.assertIsNone(msg)
8492

85-
def test_process_msg_valid(self):
86-
msg = {'context': None, 'payload': {"name": "Jon", "place": "Winterfell"}}
87-
body = main.process_msg(msg, hello)
93+
def test_process_req_valid(self):
94+
msg = "{\"context\": null, \"payload\": {\"name\": \"Jon\", \"place\": \"Winterfell\"}}"
95+
req_stream = io.StringIO(msg)
96+
req = Request(content_length=len(msg), stream=req_stream)
97+
res = Response()
98+
body = main.process_req(req, res, hello)
8899

89100
r = json.loads(body)
90101

91-
self.assertEqual("Hello, Jon from Winterfell", r['payload'])
92-
self.assertEqual(0, len(r['context']['logs']['stdout']))
93-
self.assertEqual(0, len(r['context']['logs']['stderr']))
94-
self.assertIsNone(r['context']['error'])
102+
self.assertEqual("Hello, Jon from Winterfell", r)
95103

96-
def test_process_msg_function_error(self):
97-
msg = {'context': None, 'payload': None}
98-
body = main.process_msg(msg, fail)
104+
def test_process_req_function_error(self):
105+
msg = "{\"context\": null, \"payload\": null}"
106+
req_stream = io.StringIO(msg)
107+
req = Request(len(msg), req_stream)
108+
res = Response()
109+
body = main.process_req(req, res, fail)
99110

100-
r = json.loads(body)
101-
err = r['context']['error']
111+
err = json.loads(body)
102112

103-
self.assertIsNone(r['payload'])
104113
self.assertEqual(main.FUNCTION_ERROR, err['type'])
105114
self.assertEqual("oh no!", err['message'])
106-
self.assertEqual(err['stacktrace'], r['context']['logs']['stderr'])
107-
self.assertEqual(0, len(r['context']['logs']['stdout']))
108115

109-
def test_process_msg_input_error(self):
110-
msg = {'context': None, 'payload': 1}
111-
body = main.process_msg(msg, lower)
116+
def test_process_req_input_error(self):
117+
msg = "{\"context\": null, \"payload\": 1}"
118+
req_stream = io.StringIO(msg)
119+
req = Request(len(msg), req_stream)
120+
res = Response()
121+
body = main.process_req(req, res, lower)
112122

113-
r = json.loads(body)
114-
err = r['context']['error']
123+
err = json.loads(body)
115124

116-
self.assertIsNone(r['payload'])
117125
self.assertEqual(main.INPUT_ERROR, err['type'])
118126
self.assertEqual("payload is not of type str", err['message'])
119-
self.assertEqual(err['stacktrace'], r['context']['logs']['stderr'])
120-
self.assertEqual(0, len(r['context']['logs']['stdout']))
121-
122-
def test_process_msg_logs(self):
123-
msg = {'context': None, 'payload': None}
124-
body = main.process_msg(msg, logger)
125-
r = json.loads(body)
126-
127-
self.assertIsNone(r['payload'])
128-
self.assertIsNone(r['context']['error'])
129-
self.assertEqual(["stderr", "stderr2"], r['context']['logs']['stderr'])
130-
self.assertEqual(["stdout", "stdout2"], r['context']['logs']['stdout'])
131127

132128
def test_import_function(self):
133129
# succeeds if run from the project root (or when it's in PYTHONPATH)
134130
f = main.import_function(os.getcwd(), 'example.handler.dummy')
135131

136132
self.assertEqual("Hello!", f(None, "Hello!"))
137133

138-
def test_process_msg_non_serializable_json(self):
139-
msg = {'context': None, 'payload': None}
140-
body = main.process_msg(msg, bad_payload)
134+
def test_process_req_non_serializable_json(self):
135+
msg = "{\"context\": null, \"payload\": null}"
136+
req_stream = io.StringIO(msg)
137+
req = Request(content_length=len(msg), stream=req_stream)
138+
res = Response()
139+
body = main.process_req(req, res, bad_payload)
141140

142-
r = json.loads(body)
143-
err = r['context']['error']
141+
err = json.loads(body)
144142

145-
self.assertIsNone(r['payload'])
146143
self.assertEqual(main.FUNCTION_ERROR, err['type'])
147144
self.assertIn("not JSON serializable", err['message'])
148145
self.assertTrue(len(err['stacktrace']) > 0)
149-
self.assertTrue(len(r['context']['logs']['stderr']) > 0)
150-
self.assertEqual(0, len(r['context']['logs']['stdout']))
151146

152147

153148
def test_process_req_invalid_json(self):
154149
m = "{"
155150
req_stream = io.StringIO(m)
156151
req = Request(content_length=len(m), stream=req_stream)
152+
res = Response()
157153

158-
body = main.process_req(req, hello)
154+
body = main.process_req(req, res, hello)
159155

160-
r = json.loads(body)
161-
err = r['context']['error']
156+
err = json.loads(body)
162157

163-
self.assertIsNone(r['payload'])
158+
self.assertEqual('500 Internal Server Error', res.status)
164159
self.assertEqual(main.SYSTEM_ERROR, err['type'])
165-
self.assertTrue(len(err['message']) > 0)
166-
self.assertEqual(err['stacktrace'], r['context']['logs']['stderr'])
167-
self.assertEqual(0, len(r['context']['logs']['stdout']))
168160

169161

170162
if __name__ == '__main__':

‎version.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.0.10
1+
0.0.11

0 commit comments

Comments
 (0)