diff --git a/docs/configuration.rst b/docs/configuration.rst index 4ee98fba9..5b034c064 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -7,6 +7,33 @@ Configuration The GA4GH reference server `Configuration file`_. allows Flask and application specific configuration values to be set. +------------------- +Starting the server +------------------- + +The ``ga4gh_server`` command looks attempts to start a server for a given +configuration. Please see below for mandatory configuration settings, including +``DATA_SOURCE``. + +.. argparse:: + :module: ga4gh.server.cli.server + :func: getServerParser + :prog: ga4gh_server + :nodefault: + +++++++++++++++++++++ +Gunicorn WSGI Server +++++++++++++++++++++ + +Using the ``-g`` option allows one to run the server in an experimental +mode behind the http://gunicorn.org/ WSGI HTTP Server. This allows multiple +workers to spawn to handle simultaneous requests. For some purposes, this will +allow an implementor to avoid the need to configure an Apache or nginx process. + +This feature is experimental. Please post your issues to https://github.com/ga4gh/schemas/issues . +Currently, the logging facility and TLS mode are known to not work under +gunicorn. + ------------------ Configuration file ------------------ diff --git a/ga4gh/server/cli/server.py b/ga4gh/server/cli/server.py index 88b6a643f..0dc7abb30 100644 --- a/ga4gh/server/cli/server.py +++ b/ga4gh/server/cli/server.py @@ -6,6 +6,8 @@ from __future__ import unicode_literals import requests +import multiprocessing +import gunicorn.app.base import ga4gh.server.cli as cli import ga4gh.server.frontend as frontend @@ -13,6 +15,23 @@ import ga4gh.common.cli as common_cli +class StandaloneApplication(gunicorn.app.base.BaseApplication): + def __init__(self, app, options=None): + self.options = options or {} + self.application = app + super(StandaloneApplication, self).__init__() + + def load_config(self): + config = dict( + [(key, value) for key, value in self.options.iteritems() + if key in self.cfg.settings and value is not None]) + for key, value in config.iteritems(): + self.cfg.set(key.lower(), value) + + def load(self): + return self.application + + def addServerOptions(parser): parser.add_argument( "--port", "-P", default=8000, type=int, @@ -28,20 +47,42 @@ def addServerOptions(parser): help="The configuration file to use") parser.add_argument( "--tls", "-t", action="store_true", default=False, - help="Start in TLS (https) mode.") + help="Start in TLS (https) mode (for Flask debug)") parser.add_argument( "--dont-use-reloader", default=False, action="store_true", - help="Don't use the flask reloader") + help="Don't use the Flask reloader (for Flask debug)") + parser.add_argument( + "--debug", "-d", action='store_true', default=False, + help="Runs the server using the gunicorn WSGI server.") cli.addVersionArgument(parser) cli.addDisableUrllibWarningsArgument(parser) +def runGunicornServer(parsedArgs): + options = { + 'bind': '%s:%s' % (parsedArgs.host, parsedArgs.port), + 'workers': number_of_workers(), + 'accesslog': '-', # Puts the access log on stdout + 'errorlog': '-' # Puts the error log on stdout + } + app = StandaloneApplication(frontend.app, options) + app.run() + return app + + def getServerParser(): + """ + Used by sphinx.argparse. + """ parser = common_cli.createArgumentParser("GA4GH reference server") addServerOptions(parser) return parser +def number_of_workers(): + return (multiprocessing.cpu_count() * 2) + 1 + + def server_main(args=None): parser = getServerParser() parsedArgs = parser.parse_args(args) @@ -52,7 +93,10 @@ def server_main(args=None): sslContext = None if parsedArgs.tls or ("OIDC_PROVIDER" in frontend.app.config): sslContext = "adhoc" - frontend.app.run( - host=parsedArgs.host, port=parsedArgs.port, - use_reloader=not parsedArgs.dont_use_reloader, - ssl_context=sslContext) + if parsedArgs.debug: + frontend.app.run(host=parsedArgs.host, + port=parsedArgs.port, + use_reloader=not parsedArgs.dont_use_reloader, + ssl_context=sslContext) + else: + runGunicornServer(parsedArgs) diff --git a/requirements.txt b/requirements.txt index f8de77ec0..13c9a8176 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,9 @@ # these libraries are the set listed by pipdeptree -f -w # that are dependencies of libraries listed in the next section -# Adding the constraints.txt allows you to choose a specific +# Adding the constraints.txt allows you to choose a specific # way to resolve our internal dependencies. During development, -# the constraints file will point at the current master branch +# the constraints file will point at the current master branch # of the respective module. ga4gh-common ga4gh-schemas @@ -37,6 +37,7 @@ future==0.15.2 pyjwkest==1.0.1 PyJWT==1.4.2 peewee==2.8.5 +gunicorn==19.7.0 ### This section is for the actual libraries ### # these libraries are imported in code that can be reached via diff --git a/tests/end_to_end/server.py b/tests/end_to_end/server.py index 3c5561bd5..ba5a24a52 100644 --- a/tests/end_to_end/server.py +++ b/tests/end_to_end/server.py @@ -194,6 +194,7 @@ def getCmdLine(self): python server_dev.py --dont-use-reloader --disable-urllib-warnings +-d --host 0.0.0.0 --config TestConfig --config-file {} diff --git a/tests/unit/test_cli_server.py b/tests/unit/test_cli_server.py new file mode 100644 index 000000000..fb6873d96 --- /dev/null +++ b/tests/unit/test_cli_server.py @@ -0,0 +1,32 @@ +""" +Tests related to the server start script +""" +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import unittest + +import ga4gh.server.cli.server as server +import ga4gh.server.frontend as frontend + + +class TestExceptionHandler(unittest.TestCase): + """ + Test that the server script functions behave in expected ways. + """ + def testGetServerParser(self): + self.assertIsNotNone(server.getServerParser(), + "The server parser should be returned so " + "that we can create docs for sphinx") + + def test_number_of_workers(self): + self.assertTrue(type(server.number_of_workers()) == int, + "The number of workers function should return an " + "integer.") + + def testStandaloneApplicationInstance(self): + app = server.StandaloneApplication(frontend.app) + self.assertIsNotNone(app.run, "Ensures the class instantiates from " + "our WSGI app properly. The run " + "function will spawn many processes.")