Эх сурвалжийг харах

spyne test initial import (#4451)

Burak Arslan 6 жил өмнө
parent
commit
58c2584c10

+ 41 - 0
frameworks/Python/spyne/README.md

@@ -0,0 +1,41 @@
+# [Spyne](http://spyne.io/) Benchmark Test
+
+This is the Python Spyne portion of a [benchmarking tests suite](../../)
+comparing a variety of frameworks.
+
+The latest version is at https://github.com/arskom/spyne/tree/master/examples/tfb
+
+All test implementations are located within ([app.py](app.py))
+
+## Description
+
+Spyne + SQLAlchemy
+
+### Database
+
+PostgreSQL (psycopg2 on CPython)
+
+### Server
+
+* gunicorn+wsgi on CPython
+
+## Test URLs
+### JSON Encoding
+
+http://localhost:8080/json
+
+### Single Row Random Query
+
+With ORM:
+    http://localhost:8080/dbs
+
+Without ORM (raw):
+    http://localhost:8080/dbsraw
+
+### Variable Row Query Test
+
+With ORM:
+    http://localhost:8080/db?queries=2
+
+Without ORM (raw):
+    http://localhost:8080/dbraw?queries=2

+ 303 - 0
frameworks/Python/spyne/app.py

@@ -0,0 +1,303 @@
+#!/usr/bin/env python
+
+import sys
+
+import spyne.const
+spyne.const.MIN_GC_INTERVAL = float('inf')
+
+from lxml import html
+
+from random import randint, shuffle, choice
+from contextlib import closing
+from email.utils import formatdate
+
+from neurons import TableModel, Application
+from neurons.daemon import ServiceDefinition, HttpServer, StaticFileServer
+from neurons.daemon.main import Bootstrapper
+
+from spyne import Integer32, Unicode, rpc, ServiceBase, Integer, Array, Any
+from spyne.protocol.html import HtmlCloth
+from spyne.protocol.http import HttpRpc
+from spyne.protocol.json import JsonDocument
+from spyne.server.wsgi import WsgiApplication
+
+if sys.version_info[0] == 3:
+    xrange = range
+
+_is_pypy = hasattr(sys, 'pypy_version_info')
+
+DBDRIVER = 'postgresql+psycopg2cffi' if _is_pypy else 'postgresql+psycopg2'
+DBHOST = 'tfb-database'
+
+
+# models
+class DbSessionManager(object):
+    def __init__(self, config):
+        self.session = config.get_main_store().Session()
+
+    def close(self, with_err):
+        self.session.close()
+
+
+class DbConnectionManager(object):
+    def __init__(self, config):
+        self.conn = config.get_main_store().engine.connect()
+
+    def close(self, with_err):
+        self.conn.close()
+
+
+class World(TableModel):
+    __tablename__ = "world"
+    _type_info = [
+        ('id', Integer32(primary_key=True)),
+        ('randomNumber', Integer32(sqla_column_args=dict(name='randomnumber'))),
+    ]
+
+
+T_INDEX = html.fromstring(open('cloths/index.html', 'rb').read())
+
+
+class Fortune(TableModel):
+    __tablename__ = "fortune"
+
+    id = Integer32(primary_key=True)
+    message = Unicode
+
+
+outprot_plain = HttpRpc(mime_type='text/plain')
+
+
+class TfbSimpleService(ServiceBase):
+    @rpc(_returns=Any)
+    def json(ctx):
+        ctx.transport.add_header('Date', formatdate(usegmt=True))
+        return dict(message=u'Hello, World!')
+
+    @rpc(_returns=Any)
+    def plaintext(ctx):
+        """Test 6: Plaintext"""
+        ctx.out_protocol = outprot_plain
+        return b'Hello, World!'
+
+
+def _force_int(v):
+    try:
+        return min(500, max(int(v), 1))
+    except:
+        return 1
+
+
+NumQueriesType = Any(sanitizer=_force_int)
+
+
+class TfbOrmService(ServiceBase):
+    @rpc(_returns=World)
+    def db(ctx):
+        retval = ctx.udc.session.query(World).get(randint(1, 10000))
+        return retval
+
+    @rpc(NumQueriesType, _returns=Array(World))
+    def dbs(ctx, queries):
+        if queries is None:
+            queries = 1
+
+        q = ctx.udc.session.query(World)
+        return [q.get(randint(1, 10000)) for _ in xrange(queries)]
+
+    @rpc(_returns=Array(Fortune, html_cloth=T_INDEX), _body_style='out_bare')
+    def fortunes(ctx):
+        # This is normally specified at the application level as it's a good
+        # practice to group rpc endpoints with similar return types under the
+        # same url fragment. eg. https://example.com/api/json
+        ctx.out_protocol = HtmlCloth()
+        ctx.outprot_ctx = ctx.out_protocol.get_context(ctx, ctx.transport)
+
+        fortunes = ctx.udc.session.query(Fortune).all()
+        fortunes.append(
+            Fortune(id=0, message=u"Additional fortune added at request time.")
+        )
+
+        fortunes.sort(key=lambda x: x.message)
+
+        return fortunes
+
+    @rpc(NumQueriesType, _returns=Array(World))
+    def updates(ctx, queries):
+        """Test 5: Database Updates"""
+
+        if queries is None:
+            queries = 1
+
+        retval = []
+        q = ctx.udc.session.query(World)
+        for id in (randint(1, 10000) for _ in xrange(queries)):
+            world = q.get(id)
+            world.randomNumber = randint(1, 10000)
+            retval.append(world)
+
+        ctx.udc.session.commit()
+
+        return retval
+
+
+class TfbRawService(ServiceBase):
+    @rpc(_returns=World)
+    def dbraw(ctx):
+        conn = ctx.udc.conn
+
+        wid = randint(1, 10000)
+        return conn.execute(
+                     "SELECT id, randomNumber FROM world WHERE id = %s", wid) \
+                                                                     .fetchone()
+
+    # returning both Any+dict or ObjectMarker+ListOfLists works
+    @rpc(NumQueriesType, _returns=Any)
+    def dbsraw(ctx, queries):
+        if queries is None:
+            queries = 1
+
+        retval = []
+        conn = ctx.udc.conn
+        for i in xrange(queries):
+            wid = randint(1, 10000)
+            result = conn.execute(
+                     "SELECT id, randomNumber FROM world WHERE id = %s", wid) \
+                .fetchone()
+            retval.append(dict(id=result[0], randomNumber=result[1]))
+
+        return retval
+
+    @rpc(_returns=Array(Fortune, html_cloth=T_INDEX), _body_style='out_bare')
+    def fortunesraw(ctx):
+        # This is normally specified at the application level as it's a good
+        # practice to group rpc endpoints with similar return types under the
+        # same url fragment. eg. https://example.com/api/json
+        ctx.out_protocol = HtmlCloth()
+        ctx.outprot_ctx = ctx.out_protocol.get_context(ctx, ctx.transport)
+
+        res = ctx.udc.conn.execute("SELECT id, message FROM fortune")
+        fortunes = res.fetchall()
+
+        fortunes.append(Fortune(
+            id=0,
+            message=u"Additional fortune added at request time."
+        ))
+
+        fortunes.sort(key=lambda x: x.message)
+
+        return fortunes
+
+    @rpc(NumQueriesType, _returns=Any)
+    def updatesraw(ctx, queries):
+        """Test 5: Database Updates"""
+        if queries is None:
+            queries = 1
+
+        conn = ctx.udc.conn
+
+        ids = [randint(1, 10000) for _ in xrange(queries)]
+
+        retval = []
+        for i in ids:
+            wid, rn = conn.execute(
+                         "SELECT id, randomNumber FROM world WHERE id=%s", i) \
+                .fetchone()
+
+            rn = randint(1, 10000)
+            retval.append(dict(id=wid, randomNumber=rn))
+
+            conn.execute("UPDATE World SET randomNumber=%s WHERE id=%s",
+                                                                        rn, wid)
+
+        return retval
+
+
+def _on_method_call_db_sess(ctx):
+    ctx.transport.add_header('Date', formatdate(usegmt=True))
+    ctx.udc = DbSessionManager(ctx.app.config)
+
+
+def _on_method_call_db_conn(ctx):
+    ctx.transport.add_header('Date', formatdate(usegmt=True))
+    ctx.udc = DbConnectionManager(ctx.app.config)
+
+
+TfbRawService.event_manager.add_listener("method_call", _on_method_call_db_conn)
+TfbOrmService.event_manager.add_listener("method_call", _on_method_call_db_sess)
+
+
+def init_app(config):
+    subconfig = config.services['root']
+
+    app = Application(
+        [TfbOrmService, TfbRawService, TfbSimpleService],
+        tns='http://techempower.com/benchmarks/Python/Spyne',
+        in_protocol=HttpRpc(),
+        out_protocol=JsonDocument(),
+        config=config,
+    )
+    if subconfig.subapps is None:
+        subconfig.subapps = {}
+
+    subconfig.subapps.update({'': app})
+
+    return subconfig.gen_site()
+
+
+def init(config):
+    return {
+        'root': ServiceDefinition(
+            init=init_app,
+            default=HttpServer(
+                type='tcp4',
+                host='127.0.0.1',
+                port=8080,
+            ),
+        ),
+    }
+
+
+def parse_config(argv):
+    from neurons.daemon.main import boot
+
+    retcode, config = boot('tfb', argv, init, bootstrapper=TfbBootstrap)
+
+    if retcode is not None:
+        sys.exit(retcode)
+
+    return config
+
+
+def gen_wsgi_app():
+    config = parse_config([])
+    app = config.services['root'].subapps[''].app
+    return WsgiApplication(app)
+
+
+words = 'some random words for you and me somebody else if then the'.split()
+
+
+class TfbBootstrap(Bootstrapper):
+    # noinspection PyUnresolvedReferences
+    def after_tables(self, config):
+        print("Generating data...")
+        with closing(config.get_main_store().Session()) as session:
+            ints = list(range(10000))
+            shuffle(ints)
+            for _ in range(10000):
+                session.add(World(randomNumber=ints.pop()))
+
+            for _ in range(100):
+                session.add(Fortune(
+                    message=' '.join([choice(words)
+                                                for _ in range(randint(3, 10))])
+                ))
+
+            session.commit()
+
+
+if __name__ == '__main__':
+    parse_config(sys.argv)
+else:
+    application = gen_wsgi_app()

+ 72 - 0
frameworks/Python/spyne/benchmark_config.json

@@ -0,0 +1,72 @@
+{
+  "framework": "spyne",
+  "tests": [{
+    "default": {
+      "json_url": "/json",
+      "db_url": "/db",
+      "query_url": "/dbs?queries=",
+      "fortune_url": "/fortunes",
+      "update_url": "/updates?queries=",
+      "plaintext_url": "/plaintext",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Micro",
+      "database": "postgres",
+      "framework": "spyne",
+      "language": "Python",
+      "flavor": "Python3",
+      "orm": "Full",
+      "platform": "Spyne",
+      "webserver": "None",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "Spyne",
+      "notes": "",
+      "versus": "wsgi"
+    },
+    "raw": {
+      "db_url": "/dbraw",
+      "query_url": "/dbsraw?queries=",
+      "fortune_url": "/fortunesraw",
+      "update_url": "/updatesraw?queries=",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Micro",
+      "database": "postgres",
+      "framework": "spyne",
+      "language": "Python",
+      "flavor": "Python3",
+      "orm": "Raw",
+      "platform": "Spyne",
+      "webserver": "None",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "Spyne-raw",
+      "notes": "",
+      "versus": "wsgi"
+    },
+    "nginx-uwsgi": {
+      "json_url": "/json",
+      "db_url": "/db",
+      "query_url": "/dbs?queries=",
+      "fortune_url": "/fortunes",
+      "update_url": "/updates?queries=",
+      "plaintext_url": "/plaintext",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Micro",
+      "database": "postgres",
+      "framework": "spyne",
+      "language": "Python",
+      "flavor": "Python3",
+      "orm": "Full",
+      "platform": "None",
+      "webserver": "nginx",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "Spyne",
+      "notes": "",
+      "versus": "wsgi"
+    }
+  }]
+}

+ 18 - 0
frameworks/Python/spyne/cloths/index.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Fortunes</title>
+</head>
+<body>
+<table>
+<tr>
+  <th>id</th>
+  <th>message</th>
+</tr>
+<tr spyne-id="Fortune">
+  <td spyne-id="id"></td>
+  <td spyne-id="message"></td>
+</tr>
+</table>
+</body>
+</html>

+ 107 - 0
frameworks/Python/spyne/gen_benchmark_config.py

@@ -0,0 +1,107 @@
+#!/usr/bin/env python
+
+from __future__ import print_function
+
+import json
+
+from spyne import AnyUri, Unicode, ComplexModel, M, UnsignedInteger16, Array
+from spyne.protocol.json import JsonDocument
+from spyne.util.dictdoc import get_object_as_dict
+
+
+class BenchmarkConfigElement(ComplexModel):
+    # exclude this from the output document
+    key = Unicode(pa={JsonDocument: dict(exc=True)})
+
+    display_name = M(Unicode)
+    notes = Unicode
+    versus = Unicode
+
+    db_url = AnyUri
+    json_url = AnyUri
+    query_url = AnyUri
+    fortune_url = AnyUri
+    update_url = AnyUri
+    plaintext_url = AnyUri
+
+    port = M(UnsignedInteger16(default=8080))
+
+    approach = M(Unicode(values=['Realistic', 'Stripped'], default='Realistic'))
+    classification = M(Unicode(values=['Micro', 'Fullstack', 'Platform'], default='Micro'))
+    database = M(Unicode(values=['none', 'mongodb', 'postgres', 'mysql'], default='none'))
+    orm = M(Unicode(values=['Full', 'Micro', 'None', 'Raw']))
+
+    framework = M(Unicode)
+    language = M(Unicode)
+    flavor = M(Unicode)
+    platform = M(Unicode)
+    webserver = M(Unicode)
+
+    os = M(Unicode(default='Linux'))
+    database_os = M(Unicode(default='Linux'))
+    
+    
+class BenchmarkConfig(ComplexModel):
+    framework = M(Unicode)
+    tests = Array(BenchmarkConfigElement, wrapped=False)
+
+
+gen_raw_test = lambda: BenchmarkConfigElement(
+    display_name="Spyne RAW",
+    db_url="/dbsraw",
+    query_url="/dbraw?queries=",
+    fortune_url="/fortunesraw",
+    update_url="/raw-updates?queries=",
+    orm='Raw',
+)
+
+gen_normal_test = lambda: BenchmarkConfigElement(
+    display_name="Spyne ORM",
+    db_url="/dbs",
+    query_url="/db?queries=",
+    fortune_url="/fortunes",
+    update_url="/updatesraw?queries=",
+    orm='Full',
+)
+
+
+def add_common(bc):
+    bc.port = 8080
+    bc.approach = "Realistic"
+    bc.classification = "Micro"
+    bc.database = "postgres"
+    bc.framework = "spyne"
+    bc.language = "Python"
+    bc.platform = "Spyne"
+    bc.webserver = "None"
+    bc.os = "Linux"
+    bc.database_os = "Linux"
+    bc.versus = "wsgi"
+    bc.plaintext_url = "/plaintext"
+    return bc
+
+
+config = BenchmarkConfig(framework='spyne', tests=[])
+
+keys = iter(['default', 'raw', 'py3orm', 'py3raw'])
+
+for flav in ['CPython', 'Python3']:
+    bc = add_common(gen_normal_test())
+    bc.flavor = flav
+    bc.key = next(keys)
+    config.tests.append(bc)
+
+    bc = add_common(gen_raw_test())
+    bc.flavor = flav
+    bc.key = next(keys)
+    config.tests.append(bc)
+
+data = get_object_as_dict(config, complex_as=dict)
+data['tests'] = [{d['key']: d} for d in data['tests']]
+
+data = json.dumps(data, indent=2, sort_keys=True, separators=(',', ': '))
+
+open('benchmark_config.json', 'wb').write(data)
+
+
+print(data)

+ 10 - 0
frameworks/Python/spyne/gunicorn_conf.py

@@ -0,0 +1,10 @@
+import multiprocessing
+import os
+import sys
+
+workers = multiprocessing.cpu_count() * 3
+
+bind = "0.0.0.0:8080"
+keepalive = 120
+errorlog = '-'
+pidfile = 'gunicorn.pid'

+ 48 - 0
frameworks/Python/spyne/nginx.conf

@@ -0,0 +1,48 @@
+# This file is based on /usr/local/nginx/conf/nginx.conf.default.
+
+# One worker process per core
+error_log stderr error;
+
+events {
+    # This needed to be increased because the nginx error log said so.
+    # http://nginx.org/en/docs/ngx_core_module.html#worker_connections
+    worker_connections  65535;
+    multi_accept on;
+}
+
+http {
+    default_type  application/octet-stream;
+    client_body_temp_path      /tmp;
+
+    # turn off request logging for performance
+    access_log off;
+
+    # I think these only options affect static file serving
+    sendfile        on;
+    tcp_nopush      on;
+
+    # Allow many HTTP Keep-Alive requests in a single TCP connection before
+    # closing it (the default is 100). This will minimize the total number
+    # of TCP connections opened/closed. The problem is that this may cause
+    # some worker processes to be handling too connections relative to the
+    # other workers based on an initial imbalance, so this is disabled for
+    # now.
+#    keepalive_requests 1000;
+
+    #keepalive_timeout  0;
+    keepalive_timeout  65;
+
+    server {
+        # For information on deferred, see:
+        # http://nginx.org/en/docs/http/ngx_http_core_module.html#listen
+        # http://www.techrepublic.com/article/take-advantage-of-tcp-ip-options-to-optimize-data-transmission/
+        # The backlog argument to listen() is set to match net.ipv4.tcp_max_syn_backlog and net.core.somaxconn
+        listen       8080 default_server deferred reuseport backlog=65535;
+        server_name  localhost;
+
+        location / {
+            uwsgi_pass unix:/var/tmp/uwsgi.sock;
+            include /usr/local/nginx/conf/uwsgi_params;
+        }
+    }
+}

+ 28 - 0
frameworks/Python/spyne/requirements.txt

@@ -0,0 +1,28 @@
+attrs==18.2.0
+Automat==0.7.0
+colorama==0.4.1
+constantly==15.1.0
+greenlet==0.4.15
+gunicorn==19.9.0
+hyperlink==18.0.0
+idna==2.8
+incremental==17.5.0
+lxml==4.3.1
+meinheld==0.6.1
+msgpack-python==0.5.6
+neurons==0.8.4
+ply==3.11
+psycopg2==2.7.7
+pycrypto==2.6.1
+PyHamcrest==1.9.0
+pytz==2018.9
+PyYAML==4.2b4
+six==1.12.0
+slimit==0.8.1
+spyne==2.13.9a0
+SQLAlchemy==1.2.17
+Twisted==18.9.0
+txpostgres==1.6.0
+Werkzeug==0.14.1
+zope.interface==4.6.0
+uwsgi==2.0.18

+ 17 - 0
frameworks/Python/spyne/spyne-nginx-uwsgi.dockerfile

@@ -0,0 +1,17 @@
+FROM python:3.6.6-stretch
+
+RUN curl -s http://nginx.org/keys/nginx_signing.key | apt-key add -
+RUN echo "deb http://nginx.org/packages/debian/ stretch nginx" >> /etc/apt/sources.list
+RUN echo "deb-src http://nginx.org/packages/debian/ stretch nginx" >> /etc/apt/sources.list
+
+RUN apt update -yqq && apt install -yqq nginx
+
+ADD ./ /spyne
+
+WORKDIR /spyne
+
+RUN pip3 install -r /spyne/requirements.txt
+
+RUN sed -i 's|include .*/conf/uwsgi_params;|include /etc/nginx/uwsgi_params;|g' /spyne/nginx.conf
+
+CMD nginx -c /spyne/nginx.conf && uwsgi --ini /spyne/uwsgi.ini --processes $(($(nproc)*3)) --wsgi app:application

+ 9 - 0
frameworks/Python/spyne/spyne-raw.dockerfile

@@ -0,0 +1,9 @@
+FROM python:3.6.6-stretch
+
+ADD ./ /spyne
+
+WORKDIR /spyne
+
+RUN pip3 install -r /spyne/requirements.txt
+
+CMD gunicorn app:application -c gunicorn_conf.py

+ 9 - 0
frameworks/Python/spyne/spyne.dockerfile

@@ -0,0 +1,9 @@
+FROM python:3.6.6-stretch
+
+ADD ./ /spyne
+
+WORKDIR /spyne
+
+RUN pip3 install -r /spyne/requirements.txt
+
+CMD gunicorn app:application -c gunicorn_conf.py

+ 53 - 0
frameworks/Python/spyne/tfb.yaml

@@ -0,0 +1,53 @@
+ServiceDaemon:
+    alert_dests: []
+    autoreload: false
+    daemonize: false
+    debug: true
+    debug_reactor: false
+    file_version: 2
+    log_cloth: false
+    log_dbconn: false
+    log_interface: false
+    log_model: false
+    log_orm: false
+    log_protocol: false
+    log_queries: false
+    log_results: false
+    log_rss: false
+    log_sqlalchemy: false
+    logger_dest_rotation_compression: gzip
+    logger_dest_rotation_period: WEEKLY
+    loggers:
+    -   Logger:
+            level: ERROR
+            path: .
+    main_store: sql_main
+    name: tfb
+    services:
+    -   HttpServer:
+            backlog: 50
+            disabled: true
+            host: 127.0.0.1
+            name: root
+            port: 8080
+            subapps:
+            -   HttpApplication:
+                    url: ''
+            type: tcp4
+    stores:
+    -   RelationalStore:
+            async_pool: true
+            backend: sqlalchemy
+            conn_str: postgresql://benchmarkdbuser:benchmarkdbpass@tfb-database/hello_world
+            echo_pool: false
+            max_overflow: 3
+            name: sql_main
+            pool_pre_ping: true
+            pool_recycle: 3600
+            pool_size: 10
+            pool_timeout: 30
+            pool_use_lifo: false
+            sync_case_sensitive: false
+            sync_pool: true
+            sync_pool_type: QueuePool
+    uuid: fef52ff8-3103-11e9-b0e1-5453edabe249

+ 19 - 0
frameworks/Python/spyne/uwsgi.ini

@@ -0,0 +1,19 @@
+[uwsgi]
+master
+; Increase listen queue used for nginx connecting to uWSGI. This matches
+; net.ipv4.tcp_max_syn_backlog and net.core.somaxconn.
+listen = 16384
+; for performance
+disable-logging
+; use UNIX sockets instead of TCP loopback for performance
+socket = /var/tmp/uwsgi.sock
+; allow nginx to access the UNIX socket
+chmod-socket = 666
+; Avoid thundering herd problem http://uwsgi-docs.readthedocs.org/en/latest/articles/SerializingAccept.html .
+; This is currently disabled because when I tried it with flask, it caused a
+; 20% performance hit. The CPU cores could not be saturated with thunder-lock.
+; I'm not yet sure the full story, so this is presently disabled. Also,
+; disabling this caused bottle to get ~13% faster.
+;thunder-lock
+; used by uwsgi_stop.ini
+pidfile = /var/tmp/uwsgi.pid