diff --git a/requirements.txt b/requirements.txt index 3b6834a0..45c4dc03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,15 +4,15 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 Babel!=2.4.0,>=2.3.4 # BSD -keystoneauth1>=3.3.0 # Apache-2.0 +keystoneauth1>=3.4.0 # Apache-2.0 keystonemiddleware>=4.17.0 # Apache-2.0 -oslo.concurrency>=3.20.0 # Apache-2.0 +oslo.concurrency>=3.25.0 # Apache-2.0 oslo.config>=5.1.0 # Apache-2.0 oslo.db>=4.27.0 # Apache-2.0 oslo.messaging>=5.29.0 # Apache-2.0 oslo.policy>=1.30.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 -oslo.log>=3.30.0 # Apache-2.0 +oslo.log>=3.36.0 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.service!=1.28.1,>=1.24.0 # Apache-2.0 pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD @@ -22,7 +22,7 @@ SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT sqlalchemy-migrate>=0.11.0 # Apache-2.0 stevedore>=1.20.0 # Apache-2.0 WSME>=0.8.0 # MIT -kubernetes>=4.0.0 # Apache-2.0 +kubernetes>=4.0.0 # Apache-2.0 PyYAML>=3.10 # MIT python-swiftclient>=3.2.0 # Apache-2.0 croniter>=0.3.4 # MIT License @@ -30,4 +30,4 @@ python-dateutil>=2.4.2 # BSD tenacity>=3.2.1 # Apache-2.0 PyMySQL>=0.7.6 # MIT License etcd3gw>=0.2.0 # Apache-2.0 -cotyledon>=1.3.0 # Apache-2.0 +cotyledon>=1.3.0 # Apache-2.0 diff --git a/runtimes/python2/Dockerfile b/runtimes/python2/Dockerfile index 2a4192c7..d9a515d9 100644 --- a/runtimes/python2/Dockerfile +++ b/runtimes/python2/Dockerfile @@ -6,12 +6,14 @@ RUN useradd -Ms /bin/bash qinling RUN apt-get update && \ apt-get -y install python-dev python-setuptools libffi-dev libxslt1-dev libxml2-dev libyaml-dev libssl-dev python-pip && \ - pip install -U pip setuptools + pip install -U pip setuptools uwsgi COPY . /app WORKDIR /app RUN pip install -r requirements.txt && \ chmod 0750 custom-entrypoint.sh && \ - chown -R qinling:qinling /app + mkdir -p /var/lock/qinling && \ + chown -R qinling:qinling /app /var/lock/qinling -CMD ["/bin/bash", "custom-entrypoint.sh"] +# uwsgi --http :9090 --uid qinling --wsgi-file server.py --callable app --master --processes 5 --threads 1 +CMD ["/usr/local/bin/uwsgi", "--http", ":9090", "--uid", "qinling", "--wsgi-file", "server.py", "--callable", "app", "--master", "--processes", "5", "--threads", "1"] diff --git a/runtimes/python2/README.md b/runtimes/python2/README.md deleted file mode 100644 index 4ebd8c37..00000000 --- a/runtimes/python2/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Qinling: Python Environment - -This is the Python environment for Qinling. - -It's a Docker image containing a Python 2.7 runtime, along with a -dynamic loader. A few common dependencies are included in the -requirements.txt file. End users need to provide their own dependencies -in their function packages through Qinling API or CLI. - -## Rebuilding and pushing the image - -You'll need access to a Docker registry to push the image, by default it's -docker hub. After modification, build a new image and upload to docker hub: - - docker build -t USER/python-runtime . && docker push USER/python-runtime - - -## Using the image in Qinling - -After the image is ready in docker hub, create a runtime in Qinling: - - http POST http://127.0.0.1:7070/v1/runtimes name=python2.7 image=USER/python-runtime diff --git a/runtimes/python2/custom-entrypoint.sh b/runtimes/python2/custom-entrypoint.sh deleted file mode 100644 index 79345a16..00000000 --- a/runtimes/python2/custom-entrypoint.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -# This is expected to run as root for setting the ulimits - -set -e - -# ensure increased ulimits - for nofile - for the runtime containers -# the limit on the number of files that a single process can have open at a time -ulimit -n 1024 - -# ensure increased ulimits - for nproc - for the runtime containers -# the limit on the number of processes -ulimit -u 128 - -# ensure increased ulimits - for file size - for the runtime containers -# the limit on the total file size that a single process can create, 30M -ulimit -f 61440 - -/sbin/setuser qinling python -u server.py diff --git a/runtimes/python2/requirements.txt b/runtimes/python2/requirements.txt index 32dddd28..d571a5b5 100644 --- a/runtimes/python2/requirements.txt +++ b/runtimes/python2/requirements.txt @@ -1,4 +1,5 @@ Flask>=0.10,!=0.11,<1.0 # BSD +oslo.concurrency>=3.25.0 # Apache-2.0 python-openstackclient>=3.3.0,!=3.10.0 # Apache-2.0 python-neutronclient>=6.3.0 # Apache-2.0 python-swiftclient>=3.2.0 # Apache-2.0 diff --git a/runtimes/python2/server.py b/runtimes/python2/server.py index f9139077..e911a0ab 100644 --- a/runtimes/python2/server.py +++ b/runtimes/python2/server.py @@ -14,10 +14,10 @@ import importlib import json -import logging from multiprocessing import Manager from multiprocessing import Process import os +import resource import sys import time import traceback @@ -27,35 +27,37 @@ from flask import request from flask import Response from keystoneauth1.identity import generic from keystoneauth1 import session +from oslo_concurrency import lockutils import requests app = Flask(__name__) -downloaded = False -downloading = False DOWNLOAD_ERROR = "Failed to download function package from %s, error: %s" INVOKE_ERROR = "Function execution failed because of too much resource " \ "consumption" -def setup_logger(loglevel): - global app - root = logging.getLogger() - root.setLevel(loglevel) - ch = logging.StreamHandler(sys.stdout) - ch.setLevel(loglevel) - ch.setFormatter( - logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') - ) - app.logger.addHandler(ch) - - def _print_trace(): exc_type, exc_value, exc_traceback = sys.exc_info() lines = traceback.format_exception(exc_type, exc_value, exc_traceback) print(''.join(line for line in lines)) +def _set_ulimit(): + """Limit resources usage for the current process and/or its children. + + Refer to https://docs.python.org/2.7/library/resource.html + """ + customized_limits = { + resource.RLIMIT_NOFILE: 1024, + resource.RLIMIT_NPROC: 128, + resource.RLIMIT_FSIZE: 61440 + } + for t, soft in customized_limits.items(): + _, hard = resource.getrlimit(t) + resource.setrlimit(t, (soft, hard)) + + def _get_responce(output, duration, logs, success, code): return Response( response=json.dumps( @@ -71,8 +73,13 @@ def _get_responce(output, duration, logs, success, code): ) +@lockutils.synchronized('download_function', external=True, + lock_path='/var/lock/qinling') def _download_package(url, zip_file, token=None): - app.logger.info('Downloading function, download_url:%s' % url) + if os.path.isfile(zip_file): + return True, None + + print('Downloading function, download_url:%s' % url) headers = {} if token: @@ -94,8 +101,7 @@ def _download_package(url, zip_file, token=None): DOWNLOAD_ERROR % (url, str(e)), 0, '', False, 500 ) - app.logger.info('Downloaded function package to %s' % zip_file) - + print('Downloaded function package to %s' % zip_file) return True, None @@ -135,9 +141,6 @@ def execute(): reason, e.g. unlimited memory allocation) - Deal with os error for process (e.g. Resource temporarily unavailable) """ - global downloading - global downloaded - params = request.get_json() or {} input = params.get('input') or {} execution_id = params['execution_id'] @@ -155,25 +158,20 @@ def execute(): if entry: function_module, function_method = tuple(entry.rsplit('.', 1)) - app.logger.info( + print( 'Request received, request_id: %s, execution_id: %s, input: %s, ' 'auth_url: %s' % (request_id, execution_id, input, auth_url) ) - while downloading: - time.sleep(3) - - if not downloading and not downloaded: - downloading = True - - ret, resp = _download_package(download_url, zip_file, - params.get('token')) - if not ret: - return resp - - downloading = False - downloaded = True + # Download function package if needed. + ret, resp = _download_package( + download_url, + zip_file, + params.get('token') + ) + if not ret: + return resp # Provide an openstack session to user's function os_session = None @@ -188,6 +186,9 @@ def execute(): os_session = session.Session(auth=auth, verify=False) input.update({'context': {'os_session': os_session}}) + # Set resource limit + _set_ulimit() + manager = Manager() return_dict = manager.dict() return_dict['success'] = False @@ -223,10 +224,3 @@ def execute(): @app.route('/ping') def ping(): return 'pong' - - -setup_logger(logging.DEBUG) -app.logger.info("Starting server") - -# Just for testing purpose -app.run(host='0.0.0.0', port=9090, threaded=True) diff --git a/setup.py b/setup.py index 056c16c2..566d8443 100644 --- a/setup.py +++ b/setup.py @@ -25,5 +25,5 @@ except ImportError: pass setuptools.setup( - setup_requires=['pbr'], + setup_requires=['pbr>=2.0.0'], pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt index b8f95300..d94a1eb4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,14 +4,14 @@ hacking<0.13,>=0.12.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 -sphinx>=1.6.2 # BSD -oslotest>=1.10.0 # Apache-2.0 +sphinx!=1.6.6,>=1.6.2 # BSD +oslotest>=3.2.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 futurist>=1.2.0 # Apache-2.0 -openstackdocstheme>=1.17.0 # Apache-2.0 +openstackdocstheme>=1.18.1 # Apache-2.0 reno>=2.5.0 # Apache-2.0 -kubernetes>=4.0.0 # Apache-2.0 +kubernetes>=4.0.0 # Apache-2.0