Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions old
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the 'License'). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the 'license' file accompanying this file. This file is
# distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
from __future__ import absolute_import

import subprocess

import gunicorn.app.base
import pkg_resources

import sagemaker_containers as smc

UNIX_SOCKET_BIND = 'unix:/tmp/gunicorn.sock'
HTTP_BIND = '0.0.0.0:8080'


class GunicornApp(gunicorn.app.base.BaseApplication):
"""Standalone gunicorn application

"""

def __init__(self, app, timeout=None, worker_class=None, bind=None,
worker_connections=None, workers=None, log_level=None):
self.log_level = log_level
self.workers = workers
self.worker_connections = worker_connections
self.bind = bind
self.worker_class = worker_class
self.timeout = timeout
self.application = app
super(GunicornApp, self).__init__()

def load_config(self):
self.cfg.set('timeout', self.timeout)
self.cfg.set('worker_class', self.worker_class)
self.cfg.set('bind', self.bind)
self.cfg.set('worker_connections', self.worker_connections)
self.cfg.set('workers', self.workers)
self.cfg.set('loglevel', self.log_level)

def load(self):
return self.application


def start(app):
env = smc.environment.ServingEnvironment()
gunicorn_bind_address = HTTP_BIND
processes = []

if env.use_nginx:
gunicorn_bind_address = UNIX_SOCKET_BIND
nginx_config_file = pkg_resources.resource_filename(smc.__name__, '/etc/nginx.conf')
nginx = subprocess.Popen(['nginx', '-c', nginx_config_file])
processes.append(nginx)

try:
GunicornApp(app=app, timeout=env.model_server_timeout, worker_class='gevent',
bind=gunicorn_bind_address, worker_connections=1000 * env.model_server_workers,
workers=env.model_server_workers, log_level='debug').run()
finally:
for p in processes:
p.terminate()
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ def read(file_name):
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.5',
],
install_requires=['boto3', 'six', 'pip'],
install_requires=['boto3', 'six', 'pip', 'flask', 'gunicorn', 'gevent'],

extras_require={
'test': ['tox', 'flake8', 'pytest', 'pytest-cov', 'mock', 'sagemaker', 'numpy']
'test': ['tox', 'flake8', 'pytest', 'pytest-cov', 'mock', 'sagemaker', 'numpy', 'requests']
},

entry_points={
Expand Down
11 changes: 9 additions & 2 deletions src/sagemaker_containers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@
# language governing permissions and limitations under the License.
from __future__ import absolute_import

from sagemaker_containers import collections, environment, functions, modules, server # noqa ignore=F401
# imported but unused
import sagemaker_containers.collections
import sagemaker_containers.content_types
import sagemaker_containers.environment
import sagemaker_containers.functions
import sagemaker_containers.modules
import sagemaker_containers.server
import sagemaker_containers.status_codes
import sagemaker_containers.worker # noqa ignore=F401

from sagemaker_containers.environment import Environment, ServingEnvironment, TrainingEnvironment # noqa ignore=F401
# imported but unused
13 changes: 13 additions & 0 deletions src/sagemaker_containers/content_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the 'License'). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the 'license' file accompanying this file. This file is
# distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
APPLICATION_JSON = 'application/json'
68 changes: 36 additions & 32 deletions src/sagemaker_containers/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@
# language governing permissions and limitations under the License.
from __future__ import absolute_import

import signal
import subprocess
import sys

import gunicorn.app.base
import pkg_resources

import sagemaker_containers as smc
Expand All @@ -24,44 +23,49 @@
HTTP_BIND = '0.0.0.0:8080'


def add_terminate_signal(process):
def terminate(signal_number, stack_frame):
process.terminate()
class GunicornApp(gunicorn.app.base.BaseApplication):
"""Standalone gunicorn application

signal.signal(signal.SIGTERM, terminate)
"""

def __init__(self, app, timeout=None, worker_class=None, bind=None,
worker_connections=None, workers=None, log_level=None):
self.log_level = log_level
self.workers = workers
self.worker_connections = worker_connections
self.bind = bind
self.worker_class = worker_class
self.timeout = timeout
self.application = app
super(GunicornApp, self).__init__()

def start(module_app):
def load_config(self):
self.cfg.set('timeout', self.timeout)
self.cfg.set('worker_class', self.worker_class)
self.cfg.set('bind', self.bind)
self.cfg.set('worker_connections', self.worker_connections)
self.cfg.set('workers', self.workers)
self.cfg.set('loglevel', self.log_level)

def load(self):
return self.application


def start(app):
env = smc.environment.ServingEnvironment()
gunicorn_bind_address = HTTP_BIND

nginx = None
processes = []

if env.use_nginx:
gunicorn_bind_address = UNIX_SOCKET_BIND
nginx_config_file = pkg_resources.resource_filename(smc.__name__, '/etc/nginx.conf')
nginx = subprocess.Popen(['nginx', '-c', nginx_config_file])

add_terminate_signal(nginx)

gunicorn = subprocess.Popen(['gunicorn',
'--timeout', str(env.model_server_timeout),
'-k', 'gevent',
'-b', gunicorn_bind_address,
'--worker-connections', str(1000 * env.model_server_workers),
'-w', str(env.model_server_workers),
'--log-level', 'info',
module_app])

add_terminate_signal(gunicorn)

while True:
if nginx and nginx.poll():
nginx.terminate()
break
elif gunicorn.poll():
gunicorn.terminate()
break

sys.exit(0)
processes.append(nginx)

try:
GunicornApp(app=app, timeout=env.model_server_timeout, worker_class='gevent',
bind=gunicorn_bind_address, worker_connections=1000 * env.model_server_workers,
workers=env.model_server_workers, log_level='debug').run()
finally:
for p in processes:
p.terminate()
14 changes: 14 additions & 0 deletions src/sagemaker_containers/status_codes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the 'License'). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the 'license' file accompanying this file. This file is
# distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
OK = 200
ACCEPTED = 202
22 changes: 22 additions & 0 deletions src/sagemaker_containers/transformer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the 'License'). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the 'license' file accompanying this file. This file is
# distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
from __future__ import absolute_import


class Transformer(object):

def initialize(self):
pass

def transform(self):
pass
80 changes: 80 additions & 0 deletions src/sagemaker_containers/worker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the 'License'). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the 'license' file accompanying this file. This file is
# distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
from __future__ import absolute_import

import collections

from flask import Flask, Response

import sagemaker_containers as smc

env = smc.environment.ServingEnvironment()


def default_healthcheck_fn(): # type: () -> Response
"""Ping is default health-check handler. Returns 200 with no content.

During a new serving container startup, Amazon SageMaker starts sending periodic GET requests to the /ping endpoint
to ensure that the container is ready for predictions.

The simplest requirement on the container is to respond with an HTTP 200 status code and an empty body. This
indicates to Amazon SageMaker that the container is ready to accept inference requests at the /invocations endpoint.

If the container does not begin to consistently respond with 200s during the first 30 seconds after startup,
the CreateEndPoint and UpdateEndpoint APIs will fail.

While the minimum bar is for the container to return a static 200, a container developer can use this functionality
to perform deeper checks. The request timeout on /ping attempts is 2 seconds.

More information on how health-check works can be found here:
https://docs.aws.amazon.com/sagemaker/latest/dg/your-algorithms-inference-code.html#your-algorithms-inference-algo-ping-requests

Returns:
(flask.Response): with status code 200
"""
return Response(status=smc.status_codes.OK)


def run(transformer, healthcheck_fn=None, module_name=None):
# type: (smc.Transformer, function or None, str or None) -> Flask
"""Creates and Flask application from a transformer.

Args:
transformer (smc.Transformer): object responsible to load the model and make predictions.
healthcheck_fn (function): function that will be used for healthcheck calls when the containers starts,
if not specified, it will use ping as the default healthcheck call.
module_name (str): the module name which implements the worker. If not specified, ir will use
sagemaker_containers.ServingEnvironment().module_name as the default module name.

Returns:
(Flask): an instance of Flask ready for inferences.
"""
healthcheck_fn = healthcheck_fn or default_healthcheck_fn
app = Flask(import_name=module_name or env.module_name)

transformer.initialize()

def invocations_fn():
transform_spec = transformer.transform()

return Response(response=transform_spec.prediction,
status=smc.status_codes.OK,
mimetype=transform_spec.accept)

app.add_url_rule(rule='/invocations', endpoint='invocations', view_func=invocations_fn, methods=["POST"])
app.add_url_rule(rule='/ping', endpoint='ping', view_func=healthcheck_fn or default_healthcheck_fn)

return app


TransformSpec = collections.namedtuple('TransformSpec', 'prediction accept')
22 changes: 8 additions & 14 deletions test/functional/simple_flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,21 @@
# distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import os

from flask import Flask
app = Flask(__name__)


@app.route('/')
def hello():
return 'Hello World!'

import sagemaker_containers as smc

@app.route('/invocations')
def invocations():
return 'invocation'
app = Flask(__name__)


@app.route('/ping')
def ping():
return ':)'


@app.route('/shutdown')
def shutdown():
os._exit(4)
def start_server():
smc.server.start(app)


if __name__ == '__main__':
start_server()
Loading