Skip to content
This repository was archived by the owner on Jan 12, 2022. It is now read-only.

Commit 7e4011d

Browse files
committed
multicore runtime with dispatcher and tests
1 parent ad16b62 commit 7e4011d

7 files changed

Lines changed: 290 additions & 0 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Multicore web application server.
2+
gunicorn
3+
4+
# Better HTTP api calls, used by vmstub.py.
5+
requests
6+
7+
# Required by the base runtime.
8+
webapp2
9+
webob
10+
11+
# Required to parse app.yaml.
12+
pyyaml
13+
14+
# Better request/response processing for the meta app.
15+
werkzeug

multicore_runtime/dev/Dockerfile

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Base Dockerfile for all AppEngine runtime implementations.
2+
FROM google/debian:wheezy
3+
# Clean any CMD that might be inherited from previous image, because that
4+
# will pollute our ENTRYPOINT, see
5+
# http://docs.docker.io/en/latest/reference/builder/#entrypoint.
6+
CMD []
7+
ENV DEBIAN_FRONTEND noninteractive
8+
RUN apt-get -q update ; \
9+
apt-get install --no-install-recommends -y -q ca-certificates net-tools lsof && \
10+
apt-get -y -q upgrade
11+
12+
RUN apt-get -q update ; apt-get -y -q --no-install-recommends install python2.7 python-pip
13+
14+
ADD python_vm_runtime.tar.gz /home/vmagent/python_vm_runtime
15+
16+
EXPOSE 8080
17+
18+
RUN ln -s /home/vmagent/app /app
19+
WORKDIR /app
20+
21+
RUN ln -s /home/vmagent/python_vm_runtime/google /home/vmagent/app/google
22+
23+
ENTRYPOINT ["/usr/bin/env", "gunicorn", "-b", "0.0.0.0:8080", "wsgi:meta_app", "--log-file=-"]
24+
25+
ADD . /home/vmagent/app/
26+
27+
RUN pip install -r base_requirements.txt
28+

multicore_runtime/dispatcher.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Copyright 2015 Google Inc. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
"""A WSGI app that, once configured, dispatches requests to user apps."""
16+
17+
import logging
18+
import re
19+
20+
from werkzeug.wrappers import Request
21+
22+
23+
def dispatcher(handlers):
24+
"""Accepts handlers and returns a WSGI app that dispatches requests to them.
25+
26+
Args:
27+
handlers: a list of handlers as produced by
28+
wsgi_utils.load_user_scripts_into_handlers: a list of tuples of
29+
(url, script, app).
30+
31+
Returns:
32+
A WSGI app that dispatches to the user apps specified in the input.
33+
"""
34+
35+
def dispatch(env, start_response):
36+
"""Handle one request."""
37+
request = Request(env)
38+
for url, script, app in handlers: # Closure over parent func's handlers.
39+
matcher = re.match(url, request.path)
40+
if matcher and matcher.end() == len(request.path):
41+
if script:
42+
if app is not None:
43+
# Send a response via the app specified in the handler.
44+
return app(env, start_response)
45+
else:
46+
# The import must have failed. This will have been logged at import
47+
# time. Send a 500 error response.
48+
start_response('500 Internal Server Error', [])
49+
return ['<h1>500 Internal Server Error</h1>\n']
50+
# This is a static file; normally these are handled by the appserver
51+
# but in development or for local queries they may end up here.
52+
# TODO(apphosting): support static files.
53+
else:
54+
logging.error('Static file serving is not supported: %s ',
55+
request.path)
56+
start_response('500 Internal Server Error', [])
57+
return ['<h1>500 Internal Server Error</h1>\n']
58+
logging.error('No handler found for %s', request.path)
59+
start_response('404 Not Found', [])
60+
return ['<h1>404 Not Found</h1>\n']
61+
62+
return dispatch

multicore_runtime/google

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../python_vm_runtime/google

multicore_runtime/wsgi.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright 2015 Google Inc. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
"""Configure a user project and instantiate WSGI app meta_app to serve it.
16+
17+
Importing this module will result in side-effects, such as registering the
18+
project's default ticket via vmstub.Register. This is a straightforward and
19+
compatible way to induce the webserver to run initialization code before
20+
starting to serve the WSGI app.
21+
"""
22+
23+
import logging
24+
25+
from dispatcher import dispatcher
26+
from wsgi_utils import get_module_config
27+
from wsgi_utils import get_module_config_filename
28+
from wsgi_utils import load_user_scripts_into_handlers
29+
30+
from google.appengine.ext.vmruntime import vmconfig
31+
from google.appengine.ext.vmruntime import vmstub
32+
33+
34+
logging.basicConfig(level=logging.INFO)
35+
36+
appinfo_external = get_module_config(get_module_config_filename())
37+
appengine_config = vmconfig.BuildVmAppengineEnvConfig()
38+
39+
# Ensure API requests include a valid ticket by default.
40+
vmstub.Register(vmstub.VMStub(appengine_config.default_ticket))
41+
42+
# Load user code
43+
preloaded_handlers = load_user_scripts_into_handlers(appinfo_external.handlers)
44+
45+
# Now that all scripts are fully imported, it is safe to use asynchronous
46+
# API calls.
47+
# TODO(apphosting): change this to use an env variable instead of module state
48+
vmstub.app_is_loaded = True
49+
50+
# Create a "meta app" that dispatches requests based on handlers.
51+
meta_app = dispatcher(preloaded_handlers)

multicore_runtime/wsgi_test.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright 2015 Google Inc. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# pylint: disable=g-import-not-at-top
16+
17+
import unittest
18+
from mock import MagicMock
19+
from mock import patch
20+
21+
from werkzeug.test import Client
22+
from werkzeug.wrappers import Request
23+
from werkzeug.wrappers import Response
24+
25+
from google.appengine.api import appinfo
26+
27+
FAKE_HANDLERS = [
28+
appinfo.URLMap(url='/hello', script='wsgi_test.hello_world'),
29+
appinfo.URLMap(url='/failure', script='wsgi_test.nonexistent_function')]
30+
HELLO_STRING = 'Hello World!'
31+
32+
33+
@Request.application
34+
def hello_world(request): # pylint: disable=unused-argument
35+
return Response(HELLO_STRING)
36+
37+
38+
class MetaAppTestCase(unittest.TestCase):
39+
40+
def setUp(self):
41+
# Pre-import modules to patch them in advance.
42+
from google.appengine.ext.vmruntime import vmconfig # pylint: disable=unused-variable
43+
44+
# Instantiate an app with a simple fake configuration.
45+
with patch('wsgi_utils.get_module_config_filename'):
46+
with patch('wsgi_utils.get_module_config',
47+
return_value=MagicMock(handlers=FAKE_HANDLERS)):
48+
with patch('google.appengine.ext.vmruntime.vmconfig.BuildVmAppengineEnvConfig'): # pylint: disable=line-too-long
49+
import wsgi
50+
self.app = wsgi.meta_app
51+
self.client = Client(self.app, Response)
52+
53+
def test_hello(self):
54+
response = self.client.get('/hello')
55+
self.assertEqual(response.status_code, 200)
56+
self.assertEqual(response.data, HELLO_STRING)
57+
58+
def test_failure(self):
59+
response = self.client.get('/failure')
60+
self.assertEqual(response.status_code, 500)
61+
62+
def test_notfound(self):
63+
response = self.client.get('/notfound')
64+
self.assertEqual(response.status_code, 404)

multicore_runtime/wsgi_utils.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright 2015 Google Inc. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
"""Utilities to configure the dispatcher."""
16+
17+
import logging
18+
import os
19+
20+
from google.appengine.api import appinfo_includes
21+
from google.appengine.runtime import wsgi
22+
23+
24+
def get_module_config_filename():
25+
"""Returns the name of the module configuration file (app.yaml)."""
26+
module_yaml_path = os.environ['MODULE_YAML_PATH']
27+
logging.info('Using module_yaml_path from env: %s', module_yaml_path)
28+
return module_yaml_path
29+
30+
31+
def get_module_config(filename):
32+
"""Returns the parsed module config."""
33+
with open(filename) as f:
34+
return appinfo_includes.Parse(f)
35+
36+
37+
def app_for_script(script):
38+
"""Returns the WSGI app specified in the input string, or None on failure."""
39+
if script:
40+
app, filename, err = wsgi.LoadObject(script) # pylint: disable=unused-variable
41+
if err:
42+
# Log the exception but do not reraise.
43+
logging.exception('Failed to import %s: %s', script, err)
44+
return None
45+
else:
46+
return app
47+
48+
49+
def load_user_scripts_into_handlers(handlers):
50+
"""Preloads user scripts.
51+
52+
Args:
53+
handlers: appinfo_external.handlers data as provided by get_module_config()
54+
55+
Returns:
56+
A list of tuples suitable for configuring the dispatcher() app,
57+
where the tuples are (url, script, app):
58+
- url: The url pattern which matches this handler.
59+
- script: The script to serve for this handler, as a string.
60+
- app: The fully loaded app corresponding to the script.
61+
"""
62+
loaded_handlers = [
63+
(x.url,
64+
x.script.replace('$PYTHON_LIB/', '') if x.script else x.script,
65+
app_for_script(x.script) if x.script else None)
66+
for x in handlers]
67+
logging.info('Parsed handlers: %s',
68+
[(url, script) for (url, script, _) in loaded_handlers])
69+
return loaded_handlers

0 commit comments

Comments
 (0)