#91 Remove blocking call to sqlite by using aiosqlite module.
Merged 4 years ago by pingou. Opened 4 years ago by cverna.
cverna/mdapi python3_friendly  into  master

file modified
+1
@@ -10,3 +10,4 @@ 

  alembic.ini

  .tox/

  .pytest_cache/

+ venv/

file modified
+6 -4
@@ -1,14 +1,16 @@ 

  # This Dockerfile is used to build the mdapi service on Openshift

  # mdapi.cfg configuration is managed by Openshift as a configmap

- FROM registry.fedoraproject.org/fedora:latest

+ FROM fedora:31

  

  LABEL maintainer "Clément Verna <cverna@fedoraproject.org>"

  

  EXPOSE 8080

  

- RUN dnf -y install python3-aiohttp python3-werkzeug python3-requests python3-sqlalchemy python3-fedora-messaging

+ RUN dnf -y install python3-aiohttp python3-werkzeug python3-requests python3-fedora-messaging python3-uvloop python3-pip python3-gunicorn\

+     && dnf clean all \

+     && pip3 install aiosqlite

  

- USER 1001

  ENV MDAPI_CONFIG=/etc/mdapi/mdapi.cfg

  COPY . /code

- ENTRYPOINT ["/code/mdapi-run"]

+ WORKDIR /code

+ ENTRYPOINT ["gunicorn", "mdapi.server:init_app", "--bind", "0.0.0.0:8080", "--worker-class", "aiohttp.GunicornUVLoopWebWorker", "-w", "2"]

file modified
+49 -55
@@ -37,7 +37,6 @@ 

  import argparse

  import contextlib

  import itertools

- import multiprocessing

  import os

  import shutil

  import tempfile
@@ -45,15 +44,14 @@ 

  import hashlib

  import xml.etree.ElementTree as ET

  import sys

+ import sqlite3

  

  

  import requests

  

- from sqlalchemy import text

  from fedora_messaging.api import Message, publish

  from fedora_messaging.exceptions import PublishReturned, ConnectionException

  

- import mdapi.lib as mdapilib

  

  KOJI_REPO = 'https://kojipkgs.fedoraproject.org/repos/'

  PKGDB2_URL = 'https://admin.fedoraproject.org/pkgdb/'
@@ -153,15 +151,14 @@ 

      ''' Return the list of Fedora branches corresponding to the given

      status.

      '''

-     url = PKGDB2_URL + 'api/collections?clt_status=%s' % status

+     url = PKGDB2_URL + f'api/collections?clt_status={status}'

      response = requests.get(url, verify=PKGDB2_VERIFY)

      data = response.json()

      return data['collections']

  

  

  def download_db(name, repomd_url, archive):

-     print('%s Downloading file: %s to %s' % (

-         name.ljust(padding), repomd_url, archive))

+     print(f'{name.ljust(padding)} Downloading file: {repomd_url} to {archive}')

      response = requests.get(repomd_url, verify=DL_VERIFY)

      with open(archive, 'wb') as stream:

          stream.write(response.content)
@@ -169,7 +166,7 @@ 

  

  def decompress_db(name, archive, location):

      ''' Decompress the given XZ archive at the specified location. '''

-     print('%s Extracting %s to %s' % (name.ljust(padding), archive, location))

+     print(f'{name.ljust(padding)} Extracting {archive} to {location}')

      if archive.endswith('.xz'):

          import lzma

          with contextlib.closing(lzma.LZMAFile(archive)) as stream_xz:
@@ -196,14 +193,15 @@ 

  

  

  def compare_dbs(name, db1, db2, cache1, cache2):

-     print('%s Comparing %s and %s' % (name.ljust(padding), db1, db2))

+     print(f'{name.ljust(padding)} Comparing {db1} and {db2}')

  

      def get_table_names(uri):

-         with mdapilib.session_manager('sqlite:///' + uri) as session:

-             for name in session.connection().engine.table_names():

-                 if name == 'db_info':

-                     continue

-                 yield name

+         conn = sqlite3.connect(uri)

+         for name in conn.execute("SELECT name FROM sqlite_master WHERE type='table'"):

+             if name[0] == 'db_info':

+                 continue

+             yield name[0]

+         conn.close()

  

      def row_to_package(row):

          if '/' in row[0]:
@@ -213,30 +211,27 @@ 

          return name.split('(')[0]

  

      def get_all_rows(uri, table, cache):

-         query = text(queries.get(table, default_query).format(table=table))

-         with mdapilib.session_manager('sqlite:///' + uri) as session:

-             engine = session.connection().engine

-             for i, row in enumerate(engine.execute(query)):

-                 if table in cache_dependant_tables:

-                     row = list(row)  # lists support item assignment

-                     if row[0] in cache:

-                         row[0] = cache[row[0]]

-                         yield tuple(row)

-                     else:

-                         print("%s ! %r does not appear in the "

-                               "%r cache for %r.  Dropping "

-                               "from comparison." % (

-                                   name.ljust(padding), row[0], table, uri))

-                 else:

+         conn = sqlite3.connect(uri)

+         query = queries.get(table, default_query).format(table=table)

+         for i, row in enumerate(conn.execute(query)):

+             if table in cache_dependant_tables:

+                 row = list(row)  # lists support item assignment

+                 if row[0] in cache:

+                     row[0] = cache[row[0]]

                      yield tuple(row)

- 

+                 else:

+                     print(f"{name.ljust(padding)} ! {row[0]!r} does not appear in the "

+                           f"{table!r} cache for {uri}. Dropping from comparison.")

+             else:

+                 yield tuple(row)

+         conn.close()

  

      def build_cache(uri, cache):

-         query = text(packages_cache_builder.format(table=table))

-         with mdapilib.session_manager('sqlite:///' + uri) as session:

-             engine = session.connection().engine

-             for pkgId, pkgname in engine.execute(query):

-                 cache[pkgId] = pkgname

+         conn = sqlite3.connect(uri)

+         query = queries.get(table, default_query).format(table=table)

+         for pkgId, pkgname in conn.execute(query):

+             cache[pkgId] = pkgname

+         conn.close()

  

      tables1 = list(get_table_names(db1))

      tables2 = list(get_table_names(db2))
@@ -248,7 +243,7 @@ 

          # We have never downloaded this before...

          # so we have nothing to compare it against.  Just return and say there

          # are "no differences".

-         print('%s Empty!  %s  Cannot compare.' % (name.ljust(padding), db2))

+         print(f'{name.ljust(padding)} Empty! {db2} Cannot compare.')

          return set()

  

      assert len(tables1) == len(tables2), "Cannot compare disparate dbs."
@@ -270,6 +265,7 @@ 

          # Same goes for the 'packages' table in the 'other' db.

          ('other', 'packages'),

      ]

+ 

      def should_compare(table):

          for test, target in ignored_db_tables:

              if test in db1 and table == target:
@@ -293,11 +289,11 @@ 

  

  

  def publish_changes(name, packages, repomd_url):

-     print('%s Publishing differences to fedora messaging:' % (name.ljust(padding)))

+     print(f'{name.ljust(padding)} Publishing differences to fedora messaging:')

  

      change = bool(packages)

      if not change:

-         print('%s No real changes.  Skipping fedora messaging.' % (name.ljust(padding)))

+         print(f'{name.ljust(padding)} No real changes.  Skipping fedora messaging.')

          return

  

      # Just publish the suffix of the URL.  The prefix is dl.fedoraproject.org
@@ -306,7 +302,7 @@ 

      # download.fedoraproject.org.. so, just obscure *exactly* which repo we're

      # talking about.

      url = '/'.join(repomd_url.split('/')[4:])

-     print("%s   url %s" % (name.ljust(padding), url))

+     print(f"{name.ljust(padding)} url {url}")

  

      try:

          msg = Message(
@@ -327,7 +323,7 @@ 

  

  

  def install_db(name, src, dest):

-     print('%s Installing %s to %s.' % (name.ljust(padding), src, dest))

+     print(f'{name.ljust(padding)} Installing {src} to {dest}.')

      shutil.move(src, dest)

  

  
@@ -365,8 +361,7 @@ 

      repomd_url = url + '/repomd.xml'

      response = requests.get(repomd_url, verify=DL_VERIFY)

      if not bool(response):

-         print('%s !! Failed to get %r %r' % (

-             name.ljust(padding), repomd_url, response))

+         print(f'{name.ljust(padding)} !! Failed to get {repomd_url!r} {response!r}')

          return

  

      # Parse the xml doc and get a list of locations and their shasum.
@@ -385,7 +380,7 @@ 

      files = ((f, s, t) for f, s, t in files if '.sqlite' in f)

  

      # We need to ensure the primary db comes first so we can build a pkey cache

-     primary_first = lambda item: not 'primary' in item[0]

+     primary_first = lambda item: 'primary' not in item[0]

      files = sorted(files, key=primary_first)

  

      # Primary-key caches built from the primary dbs so we can make sense
@@ -393,7 +388,7 @@ 

      cache1, cache2 = {}, {}

  

      if not files:

-         print('No sqlite database could be found in %s' % url)

+         print(f'No sqlite database could be found in {url}')

  

      for filename, shasum, shatype in files:

          repomd_url = url + '/' + filename
@@ -401,16 +396,16 @@ 

          # First, determine if the file has changed by comparing hash

          db = None

          if 'primary.sqlite' in filename:

-             db = 'mdapi-%s-primary.sqlite' % name

+             db = f'mdapi-{name}-primary.sqlite'

          elif 'filelists.sqlite' in filename:

-             db = 'mdapi-%s-filelists.sqlite' % name

+             db = f'mdapi-{name}-filelists.sqlite'

          elif 'other.sqlite' in filename:

-             db = 'mdapi-%s-other.sqlite' % name

+             db = f'mdapi-{name}-other.sqlite'

  

          # Have we downloaded this before?  Did it change?

          destfile = os.path.join(destfolder, db)

          if not needs_update(destfile, shasum, shatype):

-             print('%s No change of %s' % (name.ljust(padding), repomd_url))

+             print(f'{name.ljust(padding)} No change of {repomd_url}')

              continue

  

          # If it has changed, then download it and move it into place.
@@ -469,15 +464,14 @@ 

          version = release['version']

          if version == 'devel':

              version = 'rawhide'

-         url = '%s/pub/fedora/linux/' \

-             'development/%s/Everything/x86_64/os/repodata' % (DL_SERVER, version)

+         url = f'{DL_SERVER}/pub/fedora/linux/development/{version}/Everything/x86_64/os/repodata'

          print(release['koji_name'], version, release['status'], url)

          repositories.append(

              (url, release['koji_name'])

          )

  

          url = url.replace('/x86_64/os/', '/source/tree/')

-         repositories.append((url, 'src_%s' % release['koji_name']))

+         repositories.append((url, f'src_{release["koji_name"]}'))

  

      urls = {

          'Fedora':
@@ -509,18 +503,18 @@ 

                  url = url.replace('/x86_64/', '/Everything/x86_64/')

              else:

                  name = epel_repos[idx] % release['koji_name']

-             rurl =  url % (DL_SERVER, version)

+             rurl = url % (DL_SERVER, version)

              repositories.append((rurl, name))

  

              rurl = rurl.replace('/x86_64/os', '/source/tree')

-             repositories.append((rurl, 'src_%s' % name))

+             repositories.append((rurl, f'src_{name}'))

  

      # In parallel

-     #p = multiprocessing.Pool(10)

-     #p.map(process_repo, itertools.product(

+     # p = multiprocessing.Pool(10)

+     # p.map(process_repo, itertools.product(

      #    [CONFIG.get('DB_FOLDER', '/var/tmp')],

      #    repositories)

-     #)

+     # )

  

      # In serial

      sleep_for = CONFIG.get('CRON_SLEEP', 30)

file modified
+2 -4
@@ -1,5 +1,3 @@ 

- #!/usr/bin/env python3

+ #!/bin/bash

  

- from mdapi.server import main

- 

- main()

+ gunicorn  mdapi.server:init_app --bind 0.0.0.0:8080 --worker-class aiohttp.GunicornUVLoopWebWorker

file modified
+98 -339
@@ -1,6 +1,6 @@ 

  # -*- coding: utf-8 -*-

  #

- # Copyright © 2015  Red Hat, Inc.

+ # Copyright © 2015-2019  Red Hat, Inc.

  #

  # This copyrighted material is made available to anyone wishing to use,

  # modify, copy, or redistribute it subject to the terms and conditions
@@ -22,16 +22,27 @@ 

  '''

  Top level of the mdapi aiohttp application.

  '''

- import functools

- import json

  import logging

  import os

  

- import asyncio

+ import aiosqlite

  import werkzeug

+ 

  from aiohttp import web

  

- import mdapi.lib as mdapilib

+ from mdapi.db import (

+         GET_PACKAGE,

+         GET_PACKAGE_INFO,

+         GET_CO_PACKAGE,

+         GET_PACKAGE_BY_SRC,

+         GET_PACKAGE_BY,

+         GET_FILES,

+         GET_CHANGELOGS,

+         Packages,

+         Dependencies,

+         FileList,

+         ChangeLog

+ )

  

  

  CONFIG = dict()
@@ -46,46 +57,11 @@ 

          exec(compile(

              config_file.read(), os.environ['MDAPI_CONFIG'], 'exec'), CONFIG)

  

- indexfile = os.path.join(

-     os.path.dirname(os.path.abspath(__file__)), 'index.html')

- INDEX = ''

- with open(indexfile) as stream:

-     INDEX = stream.read()

-     INDEX = INDEX.replace('$PREFIX', CONFIG.get('PREFIX', ''))

- 

  

  _log = logging.getLogger(__name__)

  

  

- def allows_jsonp(function):

-     ''' Add support for JSONP queries to the endpoint decorated. '''

- 

-     @functools.wraps(function)

-     def wrapper(request, *args, **kwargs):

-         ''' Actually does the job with the arguments provided.

- 

-         :arg request: the request that was called that we want to add JSONP

-         support to

-         :type request: aiohttp.web_request.Request

- 

-         '''

-         response = yield from function(request, *args, **kwargs)

-         url_arg = request.query

-         callback = url_arg.get('callback')

-         if callback and request.method == 'GET':

-             if isinstance(callback, list):

-                 callback = callback[0]

-             response.mimetype = 'application/javascript'

-             response.content_type = 'application/javascript'

-             response.text = '%s(%s);' % (callback, response.text)

- 

-         return response

- 

-     return wrapper

- 

- 

- @asyncio.coroutine

- def _get_pkg(branch, name=None, action=None, srcname=None):

+ async def _get_pkg(branch, name=None, action=None, srcname=None):

      ''' Return the pkg information for the given package in the specified

      branch or raise an aiohttp exception.

      '''
@@ -95,56 +71,45 @@ 

      pkg = None

      wrongdb = False

      for repotype in ['updates-testing', 'updates', 'testing', None]:

- 

-         if repotype:

-             dbfile = '%s/mdapi-%s-%s-primary.sqlite' % (

-                 CONFIG['DB_FOLDER'], branch, repotype)

-         else:

-             dbfile = '%s/mdapi-%s-primary.sqlite' % (

-                 CONFIG['DB_FOLDER'], branch)

+         dbfile = f'{CONFIG["DB_FOLDER"]}/mdapi-{branch}{"-"+repotype if repotype else ""}'\

+                  '-primary.sqlite'

  

          if not os.path.exists(dbfile):

              wrongdb = True

              continue

  

          wrongdb = False

- 

-         session = yield from mdapilib.create_session(

-             'sqlite:///%s' % dbfile)

-         if name:

+         async with aiosqlite.connect(f'{dbfile}') as db:

              if action:

-                 pkg = yield from mdapilib.get_package_by(

-                     session, action, name)

+                 # It is safe to format the query since the action does not come from the

+                 # user.

+                 query = GET_PACKAGE_BY.format(action)

+                 async with db.execute(query, (name,)) as cursor:

+                     pkg = await cursor.fetchall()

+                 if pkg:

+                     pkg = [Packages(*item) for item in pkg]

+                     break

+             elif srcname:

+                 async with db.execute(GET_PACKAGE_BY_SRC, (srcname+'%',)) as cursor:

+                     pkg = await cursor.fetchone()

+                 if pkg:

+                     pkg = Packages(*pkg)

+                     break

              else:

-                 pkg = yield from mdapilib.get_package(session, name)

-         elif srcname:

-             pkg = yield from mdapilib.get_package_by_src(session, srcname)

-         session.close()

-         if pkg:

-             break

- 

+                 async with db.execute(GET_PACKAGE, (name,)) as cursor:

+                     pkg = await cursor.fetchone()

+                 if pkg:

+                     pkg = Packages(*pkg)

+                     break

      if wrongdb:

          raise web.HTTPBadRequest()

  

      if not pkg:

          raise web.HTTPNotFound()

- 

      return (pkg, repotype)

  

  

- def _get_pretty(request):

-     pretty = False

-     params = request.query

-     if params.get('pretty') in ['1', 'true']:

-         pretty = True

-     # Assume pretty if html is requested and pretty is not disabled

-     elif 'text/html' in request.headers.get('ACCEPT', ''):

-         pretty = True

-     return pretty

- 

- 

- @asyncio.coroutine

- def _expand_pkg_info(pkgs, branch, repotype=None):

+ async def _expand_pkg_info(pkgs, branch, repotype=None):

      ''' Return a JSON blob containing all the information we want to return

      for the provided package or packages.

      '''
@@ -155,292 +120,86 @@ 

      output = []

      for pkg in pkgs:

          out = pkg.to_json()

-         dbfile = '%s/mdapi-%s%s-primary.sqlite' % (

-             CONFIG['DB_FOLDER'], branch, '-%s' % repotype if repotype else '')

- 

-         session = yield from mdapilib.create_session(

-             'sqlite:///%s' % dbfile)

-         # Fill in some extra info

- 

-         # Basic infos, always present regardless of the version of the repo

-         for datatype in ['conflicts', 'obsoletes', 'provides', 'requires']:

-             data = yield from mdapilib.get_package_info(

-                 session, pkg.pkgKey, datatype.capitalize())

-             if data:

-                 out[datatype] = [item.to_json() for item in data]

+         dbfile = f'{CONFIG["DB_FOLDER"]}/mdapi-{branch}{"-"+repotype if repotype else ""}'\

+                  '-primary.sqlite'

+ 

+         async with aiosqlite.connect(f'{dbfile}') as db:

+             # Fill in some extra info

+             # Basic infos, always present regardless of the version of the repo

+             for datatype in ['conflicts',

+                              'obsoletes',

+                              'provides',

+                              'requires',

+                              'enhances',

+                              'recommends',

+                              'suggests',

+                              'supplements']:

+                 # It is safe to format the query since the datatype does not come from the

+                 # user.

+                 query = GET_PACKAGE_INFO.format(datatype)

+                 async with db.execute(query, (pkg.pkgKey,)) as cursor:

+                     data = await cursor.fetchall()

+                 if data:

+                     out[datatype] = [Dependencies(*item).to_json() for item in data]

+                 else:

+                     out[datatype] = data

+ 

+             # Add the list of packages built from the same src.rpm

+             if pkg.rpm_sourcerpm:

+                 async with db.execute(GET_CO_PACKAGE, (pkg.rpm_sourcerpm,)) as cursor:

+                     copkgs = await cursor.fetchall()

+                 out['co-packages'] = list(set([

+                     cpkg[2] for cpkg in copkgs

+                 ]))

              else:

-                 out[datatype] = data

+                 out['co-packages'] = []

+             out['repo'] = repotype if repotype else 'release'

+             output.append(out)

  

-         # New meta-data present for soft dependency management in RPM

-         for datatype in [

-                 'enhances', 'recommends', 'suggests', 'supplements']:

-             data = yield from mdapilib.get_package_info(

-                 session, pkg.pkgKey, datatype.capitalize())

-             if data:

-                 out[datatype] = [item.to_json() for item in data]

-             else:

-                 out[datatype] = data

- 

-         # Add the list of packages built from the same src.rpm

-         if pkg.rpm_sourcerpm:

-             copkgs = yield from mdapilib.get_co_packages(

-                 session, pkg.rpm_sourcerpm)

-             out['co-packages'] = list(set([

-                 cpkg.name for cpkg in copkgs

-             ]))

-         else:

-             out['co-packages'] = []

-         out['repo'] = repotype if repotype else 'release'

-         session.close()

-         output.append(out)

      if singleton:

          return output[0]

      else:

          return output

  

  

- @asyncio.coroutine

- @allows_jsonp

- def get_pkg(request):

-     _log.info('get_pkg %s', request)

-     branch = request.match_info.get('branch')

-     pretty = _get_pretty(request)

-     name = request.match_info.get('name')

-     pkg, repotype = yield from _get_pkg(branch, name)

- 

-     output = yield from _expand_pkg_info(pkg, branch, repotype)

- 

-     args = {}

-     if pretty:

-         args = dict(sort_keys=True, indent=4, separators=(',', ': '))

- 

-     output = web.Response(

-         body=json.dumps(output, **args).encode('utf-8'),

-         content_type='application/json')

-     return output

- 

- 

- @asyncio.coroutine

- @allows_jsonp

- def get_src_pkg(request):

-     _log.info('get_src_pkg %s', request)

-     branch = request.match_info.get('branch')

-     pretty = _get_pretty(request)

-     name = request.match_info.get('name')

-     pkg, repotype = yield from _get_pkg(branch, srcname=name)

- 

-     output = yield from _expand_pkg_info(pkg, branch, repotype)

- 

-     args = {}

-     if pretty:

-         args = dict(sort_keys=True, indent=4, separators=(',', ': '))

- 

-     return web.Response(

-         body=json.dumps(output, **args).encode('utf-8'),

-         content_type='application/json')

- 

- 

- @asyncio.coroutine

- @allows_jsonp

- def get_pkg_files(request):

-     _log.info('get_pkg_files %s', request)

-     branch = request.match_info.get('branch')

-     name = request.match_info.get('name')

-     pretty = _get_pretty(request)

-     pkg, repotype = yield from _get_pkg(branch, name)

- 

-     dbfile = '%s/mdapi-%s%s-filelists.sqlite' % (

-         CONFIG['DB_FOLDER'], branch, '-%s' % repotype if repotype else '')

+ async def _get_files(pkg_id, branch, repotype):

+     ''' Return the files list for the given package in the specified

+     branch.

+     '''

+     dbfile = f'{CONFIG["DB_FOLDER"]}/mdapi-{branch}{"-"+repotype if repotype else ""}'\

+              '-filelists.sqlite'

      if not os.path.exists(dbfile):

          raise web.HTTPBadRequest()

  

-     session2 = yield from mdapilib.create_session(

-         'sqlite:///%s' % dbfile)

-     filelist = yield from mdapilib.get_files(session2, pkg.pkgId)

-     session2.close()

+     async with aiosqlite.connect(f"{dbfile}") as db:

+         async with db.execute(GET_FILES, (pkg_id,)) as cursor:

+             filelists = await cursor.fetchall()

+ 

+     filelists = [FileList(*item) for item in filelists]

  

      output = {

-         'files': [fileinfo.to_json() for fileinfo in filelist],

+         'files': [fileinfo.to_json() for fileinfo in filelists],

          'repo': repotype if repotype else 'release',

      }

-     args = {}

-     if pretty:

-         args = dict(sort_keys=True, indent=4, separators=(',', ': '))

- 

-     return web.Response(

-         body=json.dumps(output, **args).encode('utf-8'),

-         content_type='application/json')

- 

+     return output

  

- @asyncio.coroutine

- @allows_jsonp

- def get_pkg_changelog(request):

-     _log.info('get_pkg_changelog %s', request)

-     branch = request.match_info.get('branch')

-     name = request.match_info.get('name')

-     pretty = _get_pretty(request)

-     pkg, repotype = yield from _get_pkg(branch, name)

  

-     dbfile = '%s/mdapi-%s%s-other.sqlite' % (

-         CONFIG['DB_FOLDER'], branch, '-%s' % repotype if repotype else '')

+ async def _get_changelog(pkg_id, branch, repotype):

+     ''' Return the changelog for the given package in the specified

+     branch.

+     '''

+     dbfile = f'{CONFIG["DB_FOLDER"]}/mdapi-{branch}{"-"+repotype if repotype else ""}-other.sqlite'

      if not os.path.exists(dbfile):

          raise web.HTTPBadRequest()

  

-     session2 = yield from mdapilib.create_session(

-         'sqlite:///%s' % dbfile)

-     changelogs = yield from mdapilib.get_changelog(session2, pkg.pkgId)

-     session2.close()

+     async with aiosqlite.connect(f"{dbfile}") as db:

+         async with db.execute(GET_CHANGELOGS, (pkg_id,)) as cursor:

+             changelogs = await cursor.fetchall()

+ 

+     changelogs = [ChangeLog(*item) for item in changelogs]

  

      output = {

          'changelogs': [changelog.to_json() for changelog in changelogs],

          'repo': repotype if repotype else 'release',

      }

-     args = {}

-     if pretty:

-         args = dict(sort_keys=True, indent=4, separators=(',', ': '))

- 

-     return web.Response(

-         body=json.dumps(output, **args).encode('utf-8'),

-         content_type='application/json')

- 

- 

- @asyncio.coroutine

- def list_branches(request):

-     ''' Return the list of all branches currently supported by mdapi

-     '''

-     _log.info('list_branches: %s', request)

-     pretty = _get_pretty(request)

-     output = sorted(list(set([

-         # Remove the front part `mdapi-` and the end part -<type>.sqlite

-         filename.replace('mdapi-', '').rsplit('-', 2)[0].replace(

-             '-updates', '')

-         for filename in os.listdir(CONFIG['DB_FOLDER'])

-         if filename.startswith('mdapi') and filename.endswith('.sqlite')

-     ])))

- 

-     args = {}

-     if pretty:

-         args = dict(sort_keys=True, indent=4, separators=(',', ': '))

- 

-     response = web.Response(body=json.dumps(output, **args).encode('utf-8'),

-                             content_type='application/json')

- 

-     # The decorator doesn't work for this endpoint, so do it manually here

-     # I am not really sure what doesn't work but it seems this endpoint is

-     # returning an object instead of the expected generator despite it being

-     # flagged as an asyncio coroutine

-     url_arg = request.query

-     callback = url_arg.get('callback')

-     if callback and request.method == 'GET':

-         if isinstance(callback, list):

-             callback = callback[0]

-         response.mimetype = 'application/javascript'

-         response.content_type = 'application/javascript'

-         response.text = '%s(%s);' % (callback, response.text)

- 

-     return response

- 

- 

- @asyncio.coroutine

- @allows_jsonp

- def process_dep(request, action):

-     ''' Return the information about the packages having the specified

-     action (provides, requires, obsoletes...)

-     '''

-     _log.info('process_dep %s: %s', action, request)

-     branch = request.match_info.get('branch')

-     pretty = _get_pretty(request)

-     name = request.match_info.get('name')

- 

-     try:

-         pkg, repotype = yield from _get_pkg(branch, name, action=action)

-     except:

-         raise web.HTTPBadRequest()

- 

-     output = yield from _expand_pkg_info(pkg, branch, repotype)

- 

-     args = {}

-     if pretty:

-         args = dict(sort_keys=True, indent=4, separators=(',', ': '))

- 

-     return web.Response(body=json.dumps(output, **args).encode('utf-8'),

-                         content_type='application/json')

- 

- 

- @asyncio.coroutine

- def get_provides(request):

-     return process_dep(request, 'provides')

- 

- 

- @asyncio.coroutine

- def get_requires(request):

-     return process_dep(request, 'requires')

- 

- 

- @asyncio.coroutine

- def get_obsoletes(request):

-     return process_dep(request, 'obsoletes')

- 

- 

- @asyncio.coroutine

- def get_conflicts(request):

-     return process_dep(request, 'conflicts')

- 

- 

- @asyncio.coroutine

- def get_enhances(request):

-     return process_dep(request, 'enhances')

- 

- 

- @asyncio.coroutine

- def get_recommends(request):

-     return process_dep(request, 'recommends')

- 

- 

- @asyncio.coroutine

- def get_suggests(request):

-     return process_dep(request, 'suggests')

- 

- 

- @asyncio.coroutine

- def get_supplements(request):

-     return process_dep(request, 'supplements')

- 

- 

- @asyncio.coroutine

- def index(request):

-     _log.info('index %s', request)

-     return web.Response(

-         body=INDEX.encode('utf-8'),

-         content_type='text/html',

-         charset='utf-8')

- 

- 

- def _set_routes(app):

-     routes = []

-     prefix = CONFIG.get('PREFIX', '')

-     if prefix:

-         routes.append(('', index))

- 

-     routes.extend([

-         ('/', index),

-         ('/branches', list_branches),

-         ('/{branch}/pkg/{name}', get_pkg),

-         ('/{branch}/srcpkg/{name}', get_src_pkg),

- 

-         ('/{branch}/provides/{name}', get_provides),

-         ('/{branch}/requires/{name}', get_requires),

-         ('/{branch}/obsoletes/{name}', get_obsoletes),

-         ('/{branch}/conflicts/{name}', get_conflicts),

- 

-         ('/{branch}/enhances/{name}', get_enhances),

-         ('/{branch}/recommends/{name}', get_recommends),

-         ('/{branch}/suggests/{name}', get_suggests),

-         ('/{branch}/supplements/{name}', get_supplements),

- 

-         ('/{branch}/files/{name}', get_pkg_files),

-         ('/{branch}/changelog/{name}', get_pkg_changelog),

-     ])

-     for route in routes:

-         app.router.add_route('GET', prefix + route[0], route[1])

-     return app

+     return output

file removed
-58
@@ -1,58 +0,0 @@ 

- # -*- coding: utf-8 -*-

- #

- # Copyright © 2015  Red Hat, Inc.

- #

- # This copyrighted material is made available to anyone wishing to use,

- # modify, copy, or redistribute it subject to the terms and conditions

- # of the GNU General Public License v.2, or (at your option) any later

- # version.  This program is distributed in the hope that it will be

- # useful, but WITHOUT ANY WARRANTY expressed or implied, including the

- # implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR

- # PURPOSE.  See the GNU General Public License for more details.  You

- # should have received a copy of the GNU General Public License along

- # with this program; if not, write to the Free Software Foundation,

- # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

- #

- # Any Red Hat trademarks that are incorporated in the source

- # code or documentation are not subject to the GNU General Public

- # License and may only be used or replicated with the express permission

- # of Red Hat, Inc.

- #

- 

- '''

- DB mapping for the other sqlite DB.

- '''

- 

- import sqlalchemy as sa

- 

- from sqlalchemy.ext.declarative import declarative_base

- 

- BASE = declarative_base()

- 

- 

- class Package(BASE):

-     ''' Maps the packages table in the primary.sqlite database from

-     repodata to a python object.

-     '''

-     __tablename__ = 'packages'

-     pkgKey = sa.Column(sa.Integer, primary_key=True)

-     pkgId = sa.Column(sa.Text)

- 

- 

- class Changelog(BASE):

-     ''' Maps the packages table in the filelists.sqlite database from

-     repodata to a python object.

-     '''

-     __tablename__ = 'changelog'

-     pkgKey = sa.Column(sa.Integer, primary_key=True)

-     author = sa.Column(sa.Text, primary_key=True)

-     changelog = sa.Column(sa.Text, primary_key=True)

-     date = sa.Column(sa.Integer, primary_key=True)

- 

-     def to_json(self):

-         filelist = {

-             'author': self.author,

-             'changelog': self.changelog,

-             'date': self.date,

-         }

-         return filelist

file added
+196
@@ -0,0 +1,196 @@ 

+ # -*- coding: utf-8 -*-

+ #

+ # Copyright © 2019  Red Hat, Inc.

+ #

+ # This copyrighted material is made available to anyone wishing to use,

+ # modify, copy, or redistribute it subject to the terms and conditions

+ # of the GNU General Public License v.2, or (at your option) any later

+ # version.  This program is distributed in the hope that it will be

+ # useful, but WITHOUT ANY WARRANTY expressed or implied, including the

+ # implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR

+ # PURPOSE.  See the GNU General Public License for more details.  You

+ # should have received a copy of the GNU General Public License along

+ # with this program; if not, write to the Free Software Foundation,

+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

+ #

+ # Any Red Hat trademarks that are incorporated in the source

+ # code or documentation are not subject to the GNU General Public

+ # License and may only be used or replicated with the express permission

+ # of Red Hat, Inc.

+ #

+ 

+ from dataclasses import dataclass

+ 

+ GET_PACKAGE = """SELECT pkgKey,

+                         pkgId,

+                         name,

+                         rpm_sourcerpm,

+                         epoch,

+                         version,

+                         release,

+                         arch,

+                         summary,

+                         description,

+                         url

+                  FROM packages

+                  WHERE name = ?

+                  ORDER BY epoch DESC, version DESC, release DESC"""

+ 

+ GET_PACKAGE_INFO = """SELECT rowid,

+                              pkgKey,

+                              name,

+                              epoch,

+                              version,

+                              release,

+                              flags

+                       FROM {}

+                       WHERE pkgKey = ?"""

+ 

+ GET_CO_PACKAGE = """SELECT pkgKey,

+                            pkgId,

+                            name,

+                            rpm_sourcerpm,

+                            epoch,

+                            version,

+                            release,

+                            arch,

+                            summary,

+                            description,

+                            url

+                     FROM packages

+                     WHERE rpm_sourcerpm = ?"""

+ 

+ GET_PACKAGE_BY_SRC = """SELECT pkgKey,

+                                pkgId,

+                                name,

+                                rpm_sourcerpm,

+                                epoch,

+                                version,

+                                release,

+                                arch,

+                                summary,

+                                description,

+                                url

+                         FROM packages

+                         WHERE rpm_sourcerpm LIKE ?

+                         ORDER BY epoch DESC, version DESC, release DESC"""

+ 

+ GET_PACKAGE_BY = """SELECT p.pkgKey,

+                            p.pkgId,

+                            p.name,

+                            p.rpm_sourcerpm,

+                            p.epoch,

+                            p.version,

+                            p.release,

+                            p.arch,

+                            p.summary,

+                            p.description,

+                            p.url

+                     FROM packages p

+                     JOIN {} t ON t.pkgKey = p.pkgKey

+                     WHERE t.name = ?

+                     ORDER BY p.epoch DESC, p.version DESC, p.release DESC"""

+ 

+ GET_FILES = """SELECT f.pkgKey,

+                       f.dirname,

+                       f.filenames,

+                       f.filetypes

+                FROM filelist f

+                JOIN packages p ON p.pkgId = ?

+                WHERE f.pkgKey = p.pkgKey

+                ORDER BY f.filenames"""

+ 

+ 

+ GET_CHANGELOGS = """SELECT c.pkgKey,

+                            c.author,

+                            c.changelog,

+                            c.date

+                     FROM changelog c

+                     JOIN packages p ON p.pkgId = ?

+                     WHERE c.pkgKey = p.pkgKey

+                     ORDER BY c.date DESC"""

+ 

+ 

+ @dataclass

+ class Packages:

+     pkgKey: int

+     pkgId: str

+     name: str

+     rpm_sourcerpm: str

+     epoch: str

+     version: str

+     release: str

+     arch: str

+     summary: str

+     description: str

+     url: str

+ 

+     @property

+     def basename(self):

+         return self.rpm_sourcerpm.rsplit("-", 2)[0]

+ 

+     def to_json(self):

+         pkg = {

+             'arch': self.arch,

+             'epoch': self.epoch,

+             'version': self.version,

+             'release': self.release,

+             'summary': self.summary,

+             'description': self.description,

+             'basename': self.basename,

+             'url': self.url,

+         }

+         return pkg

+ 

+ 

+ @dataclass

+ class Dependencies:

+     rowid: int

+     pkgKey: int

+     name: str

+     epoch: str

+     version: str

+     release: str

+     flags: str

+ 

+     def to_json(self):

+         pkg = {

+             'name': self.name,

+             'epoch': self.epoch,

+             'version': self.version,

+             'release': self.release,

+             'flags': self.flags,

+         }

+         return pkg

+ 

+ 

+ @dataclass

+ class FileList:

+     pkgKey: int

+     dirname: str

+     filenames: str

+     filetypes: str

+ 

+     def to_json(self):

+         filelist = {

+             'dirname': self.dirname,

+             'filenames': self.filenames,

+             'filetypes': self.filetypes,

+         }

+         return filelist

+ 

+ 

+ @dataclass

+ class ChangeLog:

+     pkgKey: int

+     author: str

+     changelog: str

+     date: int

+ 

+     def to_json(self):

+         changelog = {

+                 'author': self.author,

+                 'changelog': self.changelog,

+                 'date': self.date,

+         }

+         return changelog

file removed
-58
@@ -1,58 +0,0 @@ 

- # -*- coding: utf-8 -*-

- #

- # Copyright © 2015  Red Hat, Inc.

- #

- # This copyrighted material is made available to anyone wishing to use,

- # modify, copy, or redistribute it subject to the terms and conditions

- # of the GNU General Public License v.2, or (at your option) any later

- # version.  This program is distributed in the hope that it will be

- # useful, but WITHOUT ANY WARRANTY expressed or implied, including the

- # implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR

- # PURPOSE.  See the GNU General Public License for more details.  You

- # should have received a copy of the GNU General Public License along

- # with this program; if not, write to the Free Software Foundation,

- # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

- #

- # Any Red Hat trademarks that are incorporated in the source

- # code or documentation are not subject to the GNU General Public

- # License and may only be used or replicated with the express permission

- # of Red Hat, Inc.

- #

- 

- '''

- DB mapping for the filelists sqlite DB.

- '''

- 

- import sqlalchemy as sa

- 

- from sqlalchemy.ext.declarative import declarative_base

- 

- BASE = declarative_base()

- 

- 

- class Package(BASE):

-     ''' Maps the packages table in the primary.sqlite database from

-     repodata to a python object.

-     '''

-     __tablename__ = 'packages'

-     pkgKey = sa.Column(sa.Integer, primary_key=True)

-     pkgId = sa.Column(sa.Text)

- 

- 

- class Filelist(BASE):

-     ''' Maps the packages table in the filelists.sqlite database from

-     repodata to a python object.

-     '''

-     __tablename__ = 'filelist'

-     pkgKey = sa.Column(sa.Integer, primary_key=True)

-     dirname = sa.Column(sa.Text, primary_key=True)

-     filenames = sa.Column(sa.Text, primary_key=True)

-     filetypes = sa.Column(sa.Text, primary_key=True)

- 

-     def to_json(self):

-         filelist = {

-             'dirname': self.dirname,

-             'filenames': self.filenames,

-             'filetypes': self.filetypes,

-         }

-         return filelist

file modified
+13 -13
@@ -68,7 +68,7 @@ 

  

      /branches

  

-     <a href="$PREFIX/branches">/branches</a>

+     <a href="/branches">/branches</a>

  

  

  Note:
@@ -88,7 +88,7 @@ 

  

  So for example, for the kernel in rawhide:

  

-     <a href="$PREFIX/rawhide/pkg/kernel">/rawhide/pkg/kernel</a>

+     <a href="/rawhide/pkg/kernel">/rawhide/pkg/kernel</a>

  

  

  You can also retrieve information about a specific package on a specific
@@ -98,7 +98,7 @@ 

  

  So for example, for the python-natsort in rawhide that only exists as src.rpm:

  

-     <a href="$PREFIX/rawhide/srcpkg/python-natsort">/rawhide/srcpkg/python-natsort</a>

+     <a href="/rawhide/srcpkg/python-natsort">/rawhide/srcpkg/python-natsort</a>

  

  

  Retrieve the list of files in a package
@@ -111,7 +111,7 @@ 

  

  So for example, for the kernel-core in rawhide:

  

-     <a href="$PREFIX/rawhide/files/kernel-core">/rawhide/files/kernel-core</a>

+     <a href="/rawhide/files/kernel-core">/rawhide/files/kernel-core</a>

  

  

  Retrieve the changelog of a package
@@ -124,7 +124,7 @@ 

  

  So for example, for the kernel in rawhide:

  

-     <a href="$PREFIX/rawhide/changelog/kernel">/rawhide/changelog/kernel</a>

+     <a href="/rawhide/changelog/kernel">/rawhide/changelog/kernel</a>

  

  

  Retrieve the packages having a specific property
@@ -143,29 +143,29 @@ 

  Few examples:

  

      packages requiring R in rawhide:

-     <a href="$PREFIX/rawhide/requires/R">/rawhide/requires/R</a>

+     <a href="/rawhide/requires/R">/rawhide/requires/R</a>

        To see what R itself requires, check its information using: <a href="$PREFIX/rawhide/pkg/R">/rawhide/pkg/R</a>

  

      packages providing perl(SetupLog) in rawhide:

-     <a href="$PREFIX/rawhide/provides/perl(SetupLog)">/rawhide/provides/perl(SetupLog)</a>

+     <a href="/rawhide/provides/perl(SetupLog)">/rawhide/provides/perl(SetupLog)</a>

  

      packages obsoleting cabal2spec in rawhide:

-     <a href="$PREFIX/rawhide/obsoletes/cabal2spec">rawhide/obsoletes/cabal2spec</a>

+     <a href="/rawhide/obsoletes/cabal2spec">rawhide/obsoletes/cabal2spec</a>

  

      packages conflicting with mariadb in rawhide:

-     <a href="$PREFIX/rawhide/conflicts/mariadb">rawhide/conflicts/mariadb</a>

+     <a href="/rawhide/conflicts/mariadb">rawhide/conflicts/mariadb</a>

  

      packages enhancing httpd in rawhide:

-     <a href="$PREFIX/rawhide/enhances/httpd">rawhide/enhances/httpd</a>

+     <a href="/rawhide/enhances/httpd">rawhide/enhances/httpd</a>

  

      packages recommending flac in rawhide:

-     <a href="$PREFIX/rawhide/recommends/flac">rawhide/recommends/flac</a>

+     <a href="/rawhide/recommends/flac">rawhide/recommends/flac</a>

  

      packages suggesting R-tools in rawhide:

-     <a href="$PREFIX/rawhide/suggests/R-tools">rawhide/suggests/R-tools</a>

+     <a href="/rawhide/suggests/R-tools">rawhide/suggests/R-tools</a>

  

      packages supplementing `(hunspell and langpacks-fr)` in rawhide:

-     <a href="$PREFIX/rawhide/supplements/(hunspell and langpacks-fr)">rawhide/supplements/(hunspell and langpacks-fr)</a>

+     <a href="/rawhide/supplements/(hunspell and langpacks-fr)">rawhide/supplements/(hunspell and langpacks-fr)</a>

  

  

  

file removed
-276
@@ -1,276 +0,0 @@ 

- # -*- coding: utf-8 -*-

- #

- # Copyright © 2015  Red Hat, Inc.

- #

- # This copyrighted material is made available to anyone wishing to use,

- # modify, copy, or redistribute it subject to the terms and conditions

- # of the GNU General Public License v.2, or (at your option) any later

- # version.  This program is distributed in the hope that it will be

- # useful, but WITHOUT ANY WARRANTY expressed or implied, including the

- # implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR

- # PURPOSE.  See the GNU General Public License for more details.  You

- # should have received a copy of the GNU General Public License along

- # with this program; if not, write to the Free Software Foundation,

- # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

- #

- # Any Red Hat trademarks that are incorporated in the source

- # code or documentation are not subject to the GNU General Public

- # License and may only be used or replicated with the express permission

- # of Red Hat, Inc.

- #

- 

- '''

- MDAPI internal API to interact with the database.

- '''

- 

- import contextlib

- import time

- 

- import asyncio

- import sqlalchemy as sa

- 

- from sqlalchemy.orm import sessionmaker

- from sqlalchemy.orm import scoped_session

- from sqlalchemy.exc import SQLAlchemyError, OperationalError

- 

- import mdapi.changelog as changelog

- import mdapi.filelist as filelist

- import mdapi.primary as primary

- 

- 

- RETRY_ATTEMPT = 3

- 

- 

- @asyncio.coroutine

- def create_session(db_url, debug=False, pool_recycle=3600):

-     """ Create the Session object to use to query the database.

- 

-     :arg db_url: URL used to connect to the database. The URL contains

-     information with regards to the database engine, the host to connect

-     to, the user and password and the database name.

-       ie: <engine>://<user>:<password>@<host>/<dbname>

-     :kwarg debug: a boolean specifying wether we should have the verbose

-         output of sqlalchemy or not.

-     :return a Session that can be used to query the database.

- 

-     """

-     engine = sa.create_engine(

-         db_url, echo=debug, pool_recycle=pool_recycle)

-     scopedsession = scoped_session(sessionmaker(bind=engine))

-     return scopedsession

- 

- 

- @contextlib.contextmanager

- def session_manager(db_url, debug=False, pool_recycle=3600):

-     """ A handy context manager for our sessions. """

-     session = yield from create_session(

-         db_url, debug=debug, pool_recycle=pool_recycle)

-     try:

-         yield session

-         session.commit()

-     except:

-         session.rollback()

-         raise

-     finally:

-         session.close()

- 

- 

- @asyncio.coroutine

- def get_package(session, pkg_name):

-     ''' Return information about a package, if we can find it.

-     '''

-     output = None

-     cnt = 0

-     try:

-         pkg = session.query(

-             primary.Package

-         ).filter(

-             primary.Package.name == pkg_name

-         ).order_by(

-             primary.Package.epoch.desc(),

-             primary.Package.version.desc(),

-             primary.Package.release.desc(),

-         )

-         output = pkg.first()

-     except SQLAlchemyError as err:

-         cnt += 1

-         if cnt > RETRY_ATTEMPT:

-             raise

-         else:

-             time.sleep(0.1)

-             output = yield from get_package(session, pkg_name)

- 

-     return output

- 

- 

- @asyncio.coroutine

- def get_package_by_src(session, pkg_name):

-     ''' Return information about a package, if we can find it.

-     '''

-     # First try if there is a package matching exactly the provided name

-     simple_match = yield from get_package(session, pkg_name)

-     if simple_match and simple_match.basename == pkg_name:

-         return simple_match

- 

-     # If there is not a direct match, look by the sourcerpm name

-     cnt = 0

-     try:

-         pkg = session.query(

-             primary.Package

-         ).filter(

-             primary.Package.rpm_sourcerpm.like('{}%'.format(pkg_name))

-         ).order_by(

-             primary.Package.epoch.desc(),

-             primary.Package.version.desc(),

-             primary.Package.release.desc(),

-         )

-         for pkg in pkg.all():

-             if pkg.basename == pkg_name:

-                 return pkg

-     except SQLAlchemyError as err:

-         cnt += 1

-         if cnt > RETRY_ATTEMPT:

-             raise

-         else:

-             time.sleep(0.1)

-             yield from get_package_by_src(session, pkg_name)

- 

- 

- @asyncio.coroutine

- def get_package_by(session, tablename, key, cnt=None):

-     ''' Return information the package providing the provides, if we can find it.

-     '''

-     table = getattr(primary, tablename.capitalize())

- 

-     output = None

-     cnt = cnt or 0

-     try:

-         pkg = session.query(

-             primary.Package

-         ).filter(

-             table.name == key

-         ).filter(

-             table.pkgKey == primary.Package.pkgKey

-         ).order_by(

-             primary.Package.epoch.desc(),

-             primary.Package.version.desc(),

-             primary.Package.release.desc(),

-         )

-         output = pkg.all()

-     except SQLAlchemyError as err:

-         cnt += 1

-         if cnt > RETRY_ATTEMPT:

-             raise

-         else:

-             time.sleep(0.1)

-             output = yield from get_package_by(

-                 session, tablename, key, cnt=cnt)

- 

-     return output

- 

- 

- @asyncio.coroutine

- def get_package_info(session, pkgKey, tablename):

-     ''' Return the information contained in the specified table for the

-     given package.

-     '''

-     table = getattr(primary, tablename)

-     output = None

-     cnt = 0

-     try:

-         query = session.query(

-             table

-         ).filter(

-             table.pkgKey == pkgKey

-         )

-         output = query.all()

-     except OperationalError:

-         return None

-     except SQLAlchemyError as err:

-         cnt += 1

-         if cnt > RETRY_ATTEMPT:

-             raise

-         else:

-             time.sleep(0.1)

-             output = yield from get_package_info(session, pkgKey, tablename)

- 

-     return output

- 

- 

- @asyncio.coroutine

- def get_co_packages(session, srcpkg_name):

-     ''' Return the name of all the packages coming from the same

-     source-package.

-     '''

-     output = None

-     cnt = 0

-     try:

-         pkg = session.query(

-             primary.Package

-         ).filter(

-             primary.Package.rpm_sourcerpm == srcpkg_name

-         )

-         output = pkg.all()

-     except SQLAlchemyError as err:

-         cnt += 1

-         if cnt > RETRY_ATTEMPT:

-             raise

-         else:

-             time.sleep(0.1)

-             output = yield from get_co_packages(session, srcpkg_name)

- 

-     return output

- 

- 

- @asyncio.coroutine

- def get_files(session, pkg_id):

-     ''' Return the list of all the files in a package given its key.

-     '''

-     output = None

-     cnt = 0

-     try:

-         pkg = session.query(

-             filelist.Filelist

-         ).filter(

-             filelist.Package.pkgId == pkg_id,

-             filelist.Filelist.pkgKey == filelist.Package.pkgKey

-         ).order_by(

-             filelist.Filelist.filenames

-         )

-         output = pkg.all()

-     except SQLAlchemyError as err:

-         cnt += 1

-         if cnt > RETRY_ATTEMPT:

-             raise

-         else:

-             time.sleep(0.1)

-             output = yield from get_files(session, pkg_id)

- 

-     return output

- 

- 

- @asyncio.coroutine

- def get_changelog(session, pkg_id):

-     ''' Return the list of all the changelog in a package given its key.

-     '''

-     output = None

-     cnt = 0

-     try:

-         pkg = session.query(

-             changelog.Changelog

-         ).filter(

-             changelog.Package.pkgId == pkg_id,

-             changelog.Changelog.pkgKey == changelog.Package.pkgKey

-         ).order_by(

-             changelog.Changelog.date.desc()

-         )

-         output = pkg.all()

-     except SQLAlchemyError as err:

-         cnt += 1

-         if cnt > RETRY_ATTEMPT:

-             raise

-         else:

-             time.sleep(0.1)

-             output = yield from get_changelog(session, pkg_id)

- 

-     return output

file removed
-151
@@ -1,151 +0,0 @@ 

- # -*- coding: utf-8 -*-

- #

- # Copyright © 2015  Red Hat, Inc.

- #

- # This copyrighted material is made available to anyone wishing to use,

- # modify, copy, or redistribute it subject to the terms and conditions

- # of the GNU General Public License v.2, or (at your option) any later

- # version.  This program is distributed in the hope that it will be

- # useful, but WITHOUT ANY WARRANTY expressed or implied, including the

- # implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR

- # PURPOSE.  See the GNU General Public License for more details.  You

- # should have received a copy of the GNU General Public License along

- # with this program; if not, write to the Free Software Foundation,

- # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

- #

- # Any Red Hat trademarks that are incorporated in the source

- # code or documentation are not subject to the GNU General Public

- # License and may only be used or replicated with the express permission

- # of Red Hat, Inc.

- #

- 

- '''

- DB mapping for the primary sqlite DB.

- '''

- 

- import sqlalchemy as sa

- 

- from sqlalchemy.ext.declarative import declarative_base

- 

- BASE = declarative_base()

- 

- 

- class Package(BASE):

-     ''' Maps the packages table in the primary.sqlite database from

-     repodata to a python object.

-     '''

-     __tablename__ = 'packages'

-     pkgKey = sa.Column(sa.Integer, primary_key=True)

-     pkgId = sa.Column(sa.Text)

-     name = sa.Column(sa.Text)

-     rpm_sourcerpm = sa.Column(sa.Text)

-     epoch = sa.Column(sa.Text)

-     version = sa.Column(sa.Text)

-     release = sa.Column(sa.Text)

-     arch = sa.Column(sa.Text)

-     summary = sa.Column(sa.Text)

-     description = sa.Column(sa.Text)

-     url = sa.Column(sa.Text)

- 

-     @property

-     def basename(self):

-         ''' Return the base package name using the rpm_sourcerpms info. '''

-         return self.rpm_sourcerpm.rsplit('-', 2)[0]

- 

-     def to_json(self):

-         pkg = {

-             'arch': self.arch,

-             'epoch': self.epoch,

-             'version': self.version,

-             'release': self.release,

-             'summary': self.summary,

-             'description': self.description,

-             'basename': self.basename,

-             'url': self.url,

-         }

-         return pkg

- 

- 

- class BaseDependency(object):

-     ''' Base mapping for the tables in the primary.sqlite database that

-     contain all the dependencies information

-     '''

-     rowid = sa.Column(sa.Integer, primary_key=True)

-     pkgKey = sa.Column(sa.Integer, index=True)

-     name = sa.Column(sa.Text)

-     epoch = sa.Column(sa.Text)

-     version = sa.Column(sa.Text)

-     release = sa.Column(sa.Text)

-     flags = sa.Column(sa.Text)

- 

-     def to_json(self):

-         pkg = {

-             'name': self.name,

-             'epoch': self.epoch,

-             'version': self.version,

-             'release': self.release,

-             'flags': self.flags,

-         }

-         return pkg

- 

- 

- BASEDEP = declarative_base(cls=BaseDependency)

- 

- 

- class Requires(BASEDEP):

-     ''' Maps the requires table in the primary.sqlite database from

-     repodata to a python object.

-     '''

-     __tablename__ = 'requires'

- 

- 

- class Provides(BASEDEP):

-     ''' Maps the provides table in the primary.sqlite database from

-     repodata to a python object.

-     '''

-     __tablename__ = 'provides'

- 

- 

- class Conflicts(BASEDEP):

-     ''' Maps the conflicts table in the primary.sqlite database from

-     repodata to a python object.

-     '''

-     __tablename__ = 'conflicts'

- 

- 

- class Obsoletes(BASEDEP):

-     ''' Maps the provides table in the primary.sqlite database from

-     repodata to a python object.

-     '''

-     __tablename__ = 'obsoletes'

- 

- 

- # New soft-dependencies

- 

- 

- class Enhances(BASEDEP):

-     ''' Maps the enhances table in the primary.sqlite database from

-     repodata to a python object.

-     '''

-     __tablename__ = 'enhances'

- 

- 

- class Recommends(BASEDEP):

-     ''' Maps the recommends table in the primary.sqlite database from

-     repodata to a python object.

-     '''

-     __tablename__ = 'recommends'

- 

- 

- class Suggests(BASEDEP):

-     ''' Maps the suggests table in the primary.sqlite database from

-     repodata to a python object.

-     '''

-     __tablename__ = 'suggests'

- 

- 

- class Supplements(BASEDEP):

-     ''' Maps the supplements table in the primary.sqlite database from

-     repodata to a python object.

-     '''

-     __tablename__ = 'supplements'

file modified
+43 -8
@@ -3,18 +3,53 @@ 

  

  from aiohttp import web

  

- from mdapi import CONFIG, _set_routes

- 

- 

- def main():

+ from mdapi import CONFIG

+ from mdapi.views import (

+         index,

+         list_branches,

+         get_pkg,

+         get_src_pkg,

+         get_provides,

+         get_requires,

+         get_obsoletes,

+         get_conflicts,

+         get_enhances,

+         get_recommends,

+         get_suggests,

+         get_supplements,

+         get_pkg_files,

+         get_pkg_changelog

+ )

+ 

+ 

+ async def init_app():

+     """ Creates the aiohttp application.

+         This function creates a web application configure the routes and

+         returns the application object."""

  

      logging.basicConfig()

      logging.config.dictConfig(CONFIG.get("LOGGING") or {"version": 1})

  

      app = web.Application()

-     app = _set_routes(app)

  

-     host = CONFIG.get("HOST", "127.0.0.1")

-     port = CONFIG.get("PORT", 8080)

+     app.add_routes([

+         web.get('/', index),

+         web.get('/branches', list_branches),

+         web.get('/{branch}/pkg/{name}', get_pkg),

+         web.get('/{branch}/srcpkg/{name}', get_src_pkg),

+ 

+         web.get('/{branch}/provides/{name}', get_provides),

+         web.get('/{branch}/requires/{name}', get_requires),

+         web.get('/{branch}/obsoletes/{name}', get_obsoletes),

+         web.get('/{branch}/conflicts/{name}', get_conflicts),

+ 

+         web.get('/{branch}/enhances/{name}', get_enhances),

+         web.get('/{branch}/recommends/{name}', get_recommends),

+         web.get('/{branch}/suggests/{name}', get_suggests),

+         web.get('/{branch}/supplements/{name}', get_supplements),

+ 

+         web.get('/{branch}/files/{name}', get_pkg_files),

+         web.get('/{branch}/changelog/{name}', get_pkg_changelog),

+     ])

  

-     web.run_app(app, host=host, port=port)

+     return app

file added
+145
@@ -0,0 +1,145 @@ 

+ # -*- coding: utf-8 -*-

+ #

+ # Copyright © 2019  Red Hat, Inc.

+ #

+ # This copyrighted material is made available to anyone wishing to use,

+ # modify, copy, or redistribute it subject to the terms and conditions

+ # of the GNU General Public License v.2, or (at your option) any later

+ # version.  This program is distributed in the hope that it will be

+ # useful, but WITHOUT ANY WARRANTY expressed or implied, including the

+ # implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR

+ # PURPOSE.  See the GNU General Public License for more details.  You

+ # should have received a copy of the GNU General Public License along

+ # with this program; if not, write to the Free Software Foundation,

+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

+ #

+ # Any Red Hat trademarks that are incorporated in the source

+ # code or documentation are not subject to the GNU General Public

+ # License and may only be used or replicated with the express permission

+ # of Red Hat, Inc.

+ #

+ 

+ import logging

+ import os

+ 

+ from aiohttp import web

+ 

+ from mdapi import _get_pkg, _expand_pkg_info, _get_files, _get_changelog, CONFIG

+ 

+ indexfile = os.path.join(

+     os.path.dirname(os.path.abspath(__file__)), 'index.html')

+ 

+ 

+ _log = logging.getLogger(__name__)

+ 

+ 

+ async def index(request):

+     _log.info(f'index {request}')

+     return web.FileResponse(indexfile)

+ 

+ 

+ async def get_pkg(request):

+     _log.info(f'get_pkg {request}')

+     branch = request.match_info.get('branch')

+     name = request.match_info.get('name')

+     pkg, repotype = await _get_pkg(branch, name)

+ 

+     output = await _expand_pkg_info(pkg, branch, repotype)

+ 

+     return web.json_response(output)

+ 

+ 

+ async def get_src_pkg(request):

+     _log.info(f'get_src_pkg {request}')

+     branch = request.match_info.get('branch')

+     name = request.match_info.get('name')

+     pkg, repotype = await _get_pkg(branch, srcname=name)

+ 

+     output = await _expand_pkg_info(pkg, branch, repotype)

+ 

+     return web.json_response(output)

+ 

+ 

+ async def list_branches(request):

+     ''' Return the list of all branches currently supported by mdapi

+     '''

+     _log.info(f'list_branches: {request}')

+     output = sorted(list(set([

+         # Remove the front part `mdapi-` and the end part -<type>.sqlite

+         filename.replace('mdapi-', '').rsplit('-', 2)[0].replace(

+             '-updates', '')

+         for filename in os.listdir(CONFIG['DB_FOLDER'])

+         if filename.startswith('mdapi') and filename.endswith('.sqlite')

+     ])))

+ 

+     return web.json_response(output)

+ 

+ 

+ async def _process_dep(request, action):

+     ''' Return the information about the packages having the specified

+     action (provides, requires, obsoletes...)

+     '''

+     _log.info(f'process_dep {action}: {request}')

+     branch = request.match_info.get('branch')

+     name = request.match_info.get('name')

+ 

+     try:

+         pkg, repotype = await _get_pkg(branch, name, action=action)

+     except:

+         raise web.HTTPBadRequest()

+ 

+     output = await _expand_pkg_info(pkg, branch, repotype)

+ 

+     return web.json_response(output)

+ 

+ 

+ async def get_provides(request):

+     return await _process_dep(request, 'provides')

+ 

+ 

+ async def get_requires(request):

+     return await _process_dep(request, 'requires')

+ 

+ 

+ async def get_obsoletes(request):

+     return await _process_dep(request, 'obsoletes')

+ 

+ 

+ async def get_conflicts(request):

+     return await _process_dep(request, 'conflicts')

+ 

+ 

+ async def get_enhances(request):

+     return await _process_dep(request, 'enhances')

+ 

+ 

+ async def get_recommends(request):

+     return await _process_dep(request, 'recommends')

+ 

+ 

+ async def get_suggests(request):

+     return await _process_dep(request, 'suggests')

+ 

+ 

+ async def get_supplements(request):

+     return await _process_dep(request, 'supplements')

+ 

+ 

+ async def get_pkg_files(request):

+     _log.info(f'get_pkg_files {request}')

+     branch = request.match_info.get('branch')

+     name = request.match_info.get('name')

+     pkg, repotype = await _get_pkg(branch, name)

+     output = await _get_files(pkg.pkgId, branch, repotype)

+ 

+     return web.json_response(output)

+ 

+ 

+ async def get_pkg_changelog(request):

+     _log.info(f'get_pkg_changelog {request}')

+     branch = request.match_info.get('branch')

+     name = request.match_info.get('name')

+     pkg, repotype = await _get_pkg(branch, name)

+     output = await _get_changelog(pkg.pkgId, branch, repotype)

+ 

+     return web.json_response(output)

file modified
+3 -2
@@ -1,7 +1,8 @@ 

  aiohttp >= 3.5.4

+ aiosqlite

  fedora_messaging

  # this is a requirement of aiohttp but better safe than sorry

  requests

- sqlalchemy

  werkzeug

- flufl.lock

+ uvloop

+ gunicorn

file modified
+10 -8
@@ -41,6 +41,8 @@ 

      os.path.abspath(__file__)), '..'))

  

  import mdapi

+ from mdapi.server import init_app

+ 

  

  HERE = os.path.join(os.path.dirname(os.path.abspath(__file__)))

  
@@ -54,11 +56,11 @@ 

      """

      global TMPDIR

      TMPDIR = tempfile.mkdtemp(prefix="mdapi-test-")

-     print("Creating %s" % TMPDIR)

+     print(f"Creating {TMPDIR}")

      configfile = os.path.join(TMPDIR, "config")

  

      with open(configfile, "w") as stream:

-         stream.write("DB_FOLDER = '%s'\n" % TMPDIR)

+         stream.write(f"DB_FOLDER = '{TMPDIR}'\n")

  

      print("Downloading the databases...")

      subprocess.check_output(
@@ -68,7 +70,7 @@ 

      assert len(os.listdir(TMPDIR)) > 2

  

      def clean_up():

-         print("\nRemoving %s" % TMPDIR)

+         print(f"\nRemoving {TMPDIR}")

          shutil.rmtree(TMPDIR)

      request.addfinalizer(clean_up)

  
@@ -79,11 +81,11 @@ 

  

  

  @pytest.fixture

- def cli(set_env, tmpdir, loop, aiohttp_client):

+ async def cli(set_env, tmpdir, loop, test_client):

      mdapi.CONFIG['DB_FOLDER'] = tmpdir

-     app = web.Application()

-     app = mdapi._set_routes(app)

-     return loop.run_until_complete(aiohttp_client(app))

+     app = await init_app()

+     return await test_client(app)

+ 

  

  

  async def test_view_index_page(cli):
@@ -153,7 +155,7 @@ 

      ("supplements", "(hunspell and langpacks-fr)", 200),

  ])

  async def test_view_property_koji(cli, action, package, status_code):

-     resp = await cli.get('/koji/%s/%s' % (action, package))

+     resp = await cli.get(f'/koji/{action}/{package}')

      assert resp.status == status_code

      if status_code == 200:

          json.loads(await resp.text())

file modified
+5 -6
@@ -38,17 +38,16 @@ 

      os.path.abspath(__file__)), '..'))

  

  import mdapi

- import mdapi.lib

+ from mdapi.server import init_app

  

  HERE = os.path.join(os.path.dirname(os.path.abspath(__file__)))

  

  

  @pytest.fixture

- def cli(loop, aiohttp_client):

+ async def cli(loop, test_client):

      mdapi.CONFIG['DB_FOLDER'] = '.'

-     app = web.Application()

-     app = mdapi._set_routes(app)

-     return loop.run_until_complete(aiohttp_client(app))

+     app = await init_app()

+     return await test_client(app)

  

  

  async def test_view_index_page(cli):
@@ -109,6 +108,6 @@ 

      "enhances", "recommends", "suggests", "supplements",

  ])

  async def test_view_property_koji(cli, action):

-     resp = await cli.get('/koji/%s/R' % action)

+     resp = await cli.get(f'/koji/{action}/R')

      assert resp.status == 400

      assert '400: Bad Request' == await resp.text()

This commit removes the dependency to sqlalchemy to manage the
sqlite connection and queries. The reason behind is that sqlalchemy
is blocking the event loop in an asyncio context which made each
request needed to have access to the sqlite databases blocking.

The commit replaces sqlalchemy by aiosqlite a async sqlite wrapper
around the sqlite3 module from the standard library.
In order to make it easier to understand the code base, all the
queries used and the classes used to store the queries results
were moved to the mdapi/db.py module.
The views are now stored in the mdapi.views.py and added to the
application router in mdapi/server.py.
The logic which forms the response to the requests based on the
data gathered in the databases is present in mdapi/init.py.
It might make sense to move this to the db.py module in a later
commit.

Before this commit mdapi answered an average of 28.5 requests
per seconds for 100 concurent requests.
After this commit mdapi answers an average of 97 requests
per seconds for 100 concurent requests.

Signed-off-by: Clement Verna cverna@tutanota.com

2 new commits added

  • Remove blocking call to sqlite by using aiosqlite module.
  • Replace old string formating by f-strings
4 years ago

Any reason to remove the spec file?

Why do we remove this one?

We need to use ? instead of {} and provide a tuple as a second argument to query.execute()
Cf: https://docs.python.org/3/library/sqlite3.html 4th exemple

After this commit mdapi answers an average of 97 requests
per seconds for 100 concurent requests.

Out of curiosity, how did you test this?

Any reason to remove the spec file?

Since we deploy this in OpenShift building directly from git, I did not see much value in keeping the spec file.
But happy to keep it if you prefer.

Why do we remove this one?

The application is now run by gunicorn instead of directly aiohttp. This way we can use more than 1 CPU if we wish by starting more than 1 gunicorn worker.

We need to use ? instead of {} and provide a tuple as a second argument to query.execute()
Cf: https://docs.python.org/3/library/sqlite3.html 4th exemple

Oh nice :-) I ll update that.

After this commit mdapi answers an average of 97 requests
per seconds for 100 concurent requests.

Out of curiosity, how did you test this?

Following your blog post http://blog.pingoured.fr/index.php?post/2015/11/19/Introducing-mdapi and using the ab tool on the master branch and on my PR branch.

Any reason to remove the spec file?

Since we deploy this in OpenShift building directly from git, I did not see much value in keeping the spec file.
But happy to keep it if you prefer.

It's where we have the changelog at the moment, so I'd prefer that we keep it, at least until we find another place for the changelog.

The application is now run by gunicorn instead of directly aiohttp. This way we can use more than 1 CPU if we wish by starting more than 1 gunicorn worker.

Let's put a note in the README on how to get mdapi running then (or we just update this script to run gunicorn). I was using it to run it locally :-p

using the ab tool on the master branch and on my PR branch.

Cool, thanks :)

2 new commits added

  • Remove blocking call to sqlite by using aiosqlite module.
  • Replace old string formating by f-strings
4 years ago

The application is now run by gunicorn instead of directly aiohttp. This way we can use more than 1 CPU if we wish by starting more than 1 gunicorn worker.

Let's put a note in the README on how to get mdapi running then (or we just update this script to run gunicorn). I was using it to run it locally :-p

I did update the README --> https://pagure.io/mdapi/pull-request/91#_4__6, but yes I can also just modify the mdapi-run script to start gunicorn. I don't have a strong preference :smile:

If we keep the spec file, let's keep it in the tarballs as well :)

If we keep the spec file, we need to keep this one as well (and potentially adjust it)

2 new commits added

  • Remove blocking call to sqlite by using aiosqlite module.
  • Replace old string formating by f-strings
4 years ago

I have added back the .spec, .service, and mdapi-run and changed mdapi-run to start gunicorn instead of aiohttp directly.

I still think it would be nice to remove these file and even the setup.py which isn't really needed. But that can be done in another PR which will make it easier to review :smile:

Any reason not to use latest here?

This is what I got when trying to run the container in podman (btw, we should document how to do this in the README, especially the make available the sqlite databases to the container)

container_linux.go:346: starting container process caused "exec: \"gunicorn\": executable file not found in $PATH": OCI runtime command not found error

Any reason not to use latest here?

Using latest is generally a bad idea since you don't have control which version of the base image your run. Without knowing it you will jump from f31 to f32 and so on.
It is much better to be in control of when you want to upgrade you base image.

This is what I got when trying to run the container in podman (btw, we should document how to do this in the README, especially the make available the sqlite databases to the container)

Yes, I have not use the container for development just a simple venv. The Dockerfile here is what is used by OpenShift to build the container.

container_linux.go:346: starting container process caused "exec: \"gunicorn\": executable file not found in $PATH": OCI runtime command not found error

Ha yes gunicorn is missing from the image, fixing that

2 new commits added

  • Remove blocking call to sqlite by using aiosqlite module.
  • Replace old string formating by f-strings
4 years ago

2 new commits added

  • Remove blocking call to sqlite by using aiosqlite module.
  • Replace old string formating by f-strings
4 years ago

I've needed these changes to the Dockerfile to get it to run:

-ENTRYPOINT ["gunicorn", "mdapi.server:init_app", "--bind 0.0.0.0:8080", "--worker-class", "aiohttp.GunicornUVLoopWebWorker", "-w", "2"]
+WORKDIR /code
+ENTRYPOINT ["gunicorn", "--bind", "0.0.0.0:8080", "mdapi.server:init_app", "--worker-class", "aiohttp.GunicornUVLoopWebWorker", "-w", "2"]

Then I ran into:

  File "/code/mdapi/views.py", line 45, in get_pkg
    pkg, repotype = await _get_pkg(branch, name)
  File "/code/mdapi/__init__.py", line 96, in _get_pkg
    async with db.execute(GET_PACKAGE, name) as cursor:
  File "/usr/local/lib/python3.7/site-packages/aiosqlite/context.py", line 35, in __aenter__
    self._obj = await self._coro
  File "/usr/local/lib/python3.7/site-packages/aiosqlite/core.py", line 209, in execute
    cursor = await self._execute(self._conn.execute, sql, parameters)
  File "/usr/local/lib/python3.7/site-packages/aiosqlite/core.py", line 167, in _execute
    return await future
  File "/usr/local/lib/python3.7/site-packages/aiosqlite/core.py", line 153, in run
    result = function()
sqlite3.ProgrammingError: Incorrect number of bindings supplied. The current statement uses 0, and there are 6 supplied.

trying to access http://127.0.0.1:8080/rawhide/pkg/kernel

response is undefined here :)

2 new commits added

  • Remove blocking call to sqlite by using aiosqlite module.
  • Replace old string formating by f-strings
4 years ago

I've needed these changes to the Dockerfile to get it to run:
-ENTRYPOINT ["gunicorn", "mdapi.server:init_app", "--bind 0.0.0.0:8080", "--worker-class", "aiohttp.GunicornUVLoopWebWorker", "-w", "2"]
+WORKDIR /code
+ENTRYPOINT ["gunicorn", "--bind", "0.0.0.0:8080", "mdapi.server:init_app", "--worker-class", "aiohttp.GunicornUVLoopWebWorker", "-w", "2"]

Then I ran into:
File "/code/mdapi/views.py", line 45, in get_pkg
pkg, repotype = await _get_pkg(branch, name)
File "/code/mdapi/init.py", line 96, in _get_pkg
async with db.execute(GET_PACKAGE, name) as cursor:
File "/usr/local/lib/python3.7/site-packages/aiosqlite/context.py", line 35, in aenter
self._obj = await self._coro
File "/usr/local/lib/python3.7/site-packages/aiosqlite/core.py", line 209, in execute
cursor = await self._execute(self._conn.execute, sql, parameters)
File "/usr/local/lib/python3.7/site-packages/aiosqlite/core.py", line 167, in _execute
return await future
File "/usr/local/lib/python3.7/site-packages/aiosqlite/core.py", line 153, in run
result = function()
sqlite3.ProgrammingError: Incorrect number of bindings supplied. The current statement uses 0, and there are 6 supplied.

trying to access http://127.0.0.1:8080/rawhide/pkg/kernel

Ok, I did not use correctly the sqlite formating with '?', Should be fixed now :smile:

response is undefined here :)

Updated :)

I still think my changes to the Dockerfile are needed, especially the WORKDIR one as otherwise gunicorn doesn't find the mdapi module

I still think my changes to the Dockerfile are needed, especially the WORKDIR one as otherwise gunicorn doesn't find the mdapi module

Nevermind I see it's there, somehow my pull did not have it when I did it :(

Thanks

I have deployed this branch in communishift so we could get a idea of the performances (no nfs storage).

ab -c 100 -n 1000 http://mdapi-git-mdapi.apps.os.fedorainfracloud.org/rawhide/pkg/kernel
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking mdapi-git-mdapi.apps.os.fedorainfracloud.org (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        Python/3.7
Server Hostname:        mdapi-git-mdapi.apps.os.fedorainfracloud.org
Server Port:            80

Document Path:          /rawhide/pkg/kernel
Document Length:        1091 bytes

Concurrency Level:      100
Time taken for tests:   5.308 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      1371000 bytes
HTML transferred:       1091000 bytes
Requests per second:    188.41 [#/sec] (mean)
Time per request:       530.768 [ms] (mean)
Time per request:       5.308 [ms] (mean, across all concurrent requests)
Transfer rate:          252.25 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      161  167   7.9    166     245
Processing:   194  307 131.1    254     775
Waiting:      193  306 131.1    254     774
Total:        357  474 132.7    420     943

Percentage of the requests served within a certain time (ms)
  50%    420
  66%    464
  75%    524
  80%    545
  90%    623
  95%    871
  98%    917
  99%    934
 100%    943 (longest request)

I could try to build the master branch in communishift to compare. Also I expect these number to be higher in stg and prod because of the NFS storage access.

If you have the time, could you run the master branch on communishift as well?
It'll give us a baseline to compare things to before merging this.

(actually, we could just run this against the current staging instance of mdapi for comparison)

Here is the outcome against the current mdapi instance in staging:

 ab -c 100 -n 1000 https://mdapi.stg.fedoraproject.org/rawhide/pkg/kernel 
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking mdapi.stg.fedoraproject.org (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        Python/3.7
Server Hostname:        mdapi.stg.fedoraproject.org
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,4096,128
Server Temp Key:        X25519 253 bits
TLS Server Name:        mdapi.stg.fedoraproject.org

Document Path:          /rawhide/pkg/kernel
Document Length:        1091 bytes

Concurrency Level:      100
Time taken for tests:   200.136 seconds
Complete requests:      1000
Failed requests:        963
   (Connect: 0, Receive: 0, Length: 963, Exceptions: 0)
Non-2xx responses:      963
Total transferred:      1924658 bytes
HTML transferred:       1422539 bytes
Requests per second:    5.00 [#/sec] (mean)
Time per request:       20013.640 [ms] (mean)
Time per request:       200.136 [ms] (mean, across all concurrent requests)
Transfer rate:          9.39 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      490  726 271.7    626    2047
Processing:   165 17841 14315.9  30165   38959
Waiting:      164 17827 14305.5  30165   38959
Total:        659 18567 14410.0  30662   39490

Percentage of the requests served within a certain time (ms)
  50%  30662
  66%  30845
  75%  31070
  80%  31130
  90%  31455
  95%  31545
  98%  32346
  99%  32398
 100%  39490 (longest request)

Alright, let's merge this and push it to staging and see how it behaves :)

Pull-Request has been merged by pingou

4 years ago

And here are the numbers for the new code in staging:

 ab -c 100 -n 1000 https://mdapi.stg.fedoraproject.org/rawhide/pkg/kernel
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking mdapi.stg.fedoraproject.org (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        Python/3.7
Server Hostname:        mdapi.stg.fedoraproject.org
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,4096,128
Server Temp Key:        X25519 253 bits
TLS Server Name:        mdapi.stg.fedoraproject.org

Document Path:          /rawhide/pkg/kernel
Document Length:        1091 bytes

Concurrency Level:      100
Time taken for tests:   33.458 seconds
Complete requests:      1000
Failed requests:        1
   (Connect: 0, Receive: 0, Length: 1, Exceptions: 0)
Non-2xx responses:      1
Total transferred:      1721844 bytes
HTML transferred:       1090328 bytes
Requests per second:    29.89 [#/sec] (mean)
Time per request:       3345.809 [ms] (mean)
Time per request:       33.458 [ms] (mean, across all concurrent requests)
Transfer rate:          50.26 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      497 1291 423.1   1142    3534
Processing:   166 1649 420.7   1800    2495
Waiting:      166 1649 420.7   1800    2495
Total:        944 2940 376.5   2938    5300

Percentage of the requests served within a certain time (ms)
  50%   2938
  66%   2995
  75%   3051
  80%   3088
  90%   3137
  95%   3168
  98%   3196
  99%   5222
 100%   5300 (longest request)

So we went from

Requests per second:    5.00 [#/sec] (mean)
Time per request:       20013.640 [ms] (mean)

to

Requests per second:    29.89 [#/sec] (mean)
Time per request:       3345.809 [ms] (mean)

The production numbers:

Before the upgrade

$ ab -c 100 -n 1000 https://mdapi.fedoraproject.org/rawhide/pkg/kernel                        
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking mdapi.fedoraproject.org (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        Python/3.7
Server Hostname:        mdapi.fedoraproject.org
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,4096,128
Server Temp Key:        X25519 253 bits
TLS Server Name:        mdapi.fedoraproject.org

Document Path:          /rawhide/pkg/kernel
Document Length:        1036 bytes

Concurrency Level:      100
Time taken for tests:   182.775 seconds
Complete requests:      1000
Failed requests:        943
   (Connect: 0, Receive: 0, Length: 943, Exceptions: 0)
Non-2xx responses:      943
Total transferred:      1956607 bytes
HTML transferred:       1460389 bytes
Requests per second:    5.47 [#/sec] (mean)
Time per request:       18277.459 [ms] (mean)
Time per request:       182.775 [ms] (mean, across all concurrent requests)
Transfer rate:          10.45 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      355  560 177.5    510    1165
Processing:   362 16992 13980.8  25634   31538
Waiting:      361 16991 13981.4  25634   31538
Total:        724 17553 13878.6  26163   31900

Percentage of the requests served within a certain time (ms)
  50%  26163
  66%  30734
  75%  30776
  80%  30876
  90%  30919
  95%  31314
  98%  31378
  99%  31390
 100%  31900 (longest request)

After the upgrade:

$ ab -c 100 -n 1000 https://mdapi.fedoraproject.org/rawhide/pkg/kernel
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking mdapi.fedoraproject.org (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        Python/3.7
Server Hostname:        mdapi.fedoraproject.org
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,4096,128
Server Temp Key:        X25519 253 bits
TLS Server Name:        mdapi.fedoraproject.org

Document Path:          /rawhide/pkg/kernel
Document Length:        1036 bytes

Concurrency Level:      100
Time taken for tests:   30.205 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      1658914 bytes
HTML transferred:       1036000 bytes
Requests per second:    33.11 [#/sec] (mean)
Time per request:       3020.499 [ms] (mean)
Time per request:       30.205 [ms] (mean, across all concurrent requests)
Transfer rate:          53.63 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      377  765 236.0    707    1593
Processing:   505 2013 859.7   1888    5511
Waiting:      505 2012 859.7   1888    5511
Total:       1138 2777 893.3   2603    6283

Percentage of the requests served within a certain time (ms)
  50%   2603
  66%   2714
  75%   2841
  80%   2959
  90%   3383
  95%   6115
  98%   6233
  99%   6254
 100%   6283 (longest request)

So we went from:

Requests per second:    5.47 [#/sec] (mean)
Time per request:       18277.459 [ms] (mean)

to:

Requests per second:    33.11 [#/sec] (mean)
Time per request:       3020.499 [ms] (mean)

In production as well :)