avwx-rest/avwx-api

Trying to run a miminally modified Dockerfile

Closed this issue · 8 comments

Hi,

I have a docker file that I got from Dockerfile.sample. It builds but hypercorn outputs this error. When I try with gunicorn, I get the same issue. Did I miss a doc or something?

 [2020-06-08 08:58:10 +0000] [10] [INFO] Starting gunicorn 20.0.4
 [2020-06-08 08:58:10 +0000] [10] [INFO] Listening at: http://0.0.0.0:5000 (10)
 [2020-06-08 08:58:10 +0000] [10] [INFO] Using worker: sync
 [2020-06-08 08:58:10 +0000] [16] [INFO] Booting worker with pid: 16
 [2020-06-08 08:58:11 +0000] [16] [ERROR] Exception in worker process
 Traceback (most recent call last):
   File "/usr/local/lib/python3.8/site-packages/gunicorn/arbiter.py", line 583, in spawn_worker
     worker.init_process()
   File "/usr/local/lib/python3.8/site-packages/gunicorn/workers/base.py", line 119, in init_process
     self.load_wsgi()
   File "/usr/local/lib/python3.8/site-packages/gunicorn/workers/base.py", line 144, in load_wsgi
     self.wsgi = self.app.wsgi()
   File "/usr/local/lib/python3.8/site-packages/gunicorn/app/base.py", line 67, in wsgi
     self.callable = self.load()
   File "/usr/local/lib/python3.8/site-packages/gunicorn/app/wsgiapp.py", line 49, in load
     return self.load_wsgiapp()
   File "/usr/local/lib/python3.8/site-packages/gunicorn/app/wsgiapp.py", line 39, in load_wsgiapp
     return util.import_app(self.app_uri)
   File "/usr/local/lib/python3.8/site-packages/gunicorn/util.py", line 358, in import_app
     mod = importlib.import_module(module)
   File "/usr/local/lib/python3.8/importlib/__init__.py", line 127, in import_module
     return _bootstrap._gcd_import(name[level:], package, level)
   File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
   File "<frozen importlib._bootstrap>", line 991, in _find_and_load
   File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
   File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
   File "<frozen importlib._bootstrap_external>", line 783, in exec_module
   File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
   File "/home/avwx/avwx_api/__init__.py", line 3, in <module>
     from avwx_api import api, views, status
   File "/home/avwx/avwx_api/api/__init__.py", line 1, in <module>
     from . import current, gfs, station
   File "/home/avwx/avwx_api/api/current.py", line 14, in <module>
     class MetarFetch(Report):
   File "/usr/local/lib/python3.8/site-packages/quart_openapi/pint.py", line 286, in decorator
     self._add_resource(func_or_viewcls, path, methods, *args, **kwargs)
   File "/usr/local/lib/python3.8/site-packages/quart_openapi/pint.py", line 199, in _add_resource
     super().add_url_rule(path, endpoint, view_func, methods,  # pylint: disable=no-member
 TypeError: add_url_rule() got multiple values for argument 'provide_automatic_options'

I tried directly on my machine, installing the requirements with pip -Ruv requirements.txt. I followed the docker file basically and ended up with the same error:

seanodea@aurvandr ~/code/flsb/flsb-flask/avwx-api (master)$ hypercorn avwx_api:app --bind 0.0.0.0:5000 -c python:hypercorn_config.py
Process Process-2:
Process Process-1:
Process Process-3:
Traceback (most recent call last):
Traceback (most recent call last):
  File "/usr/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
    self.run()
  File "/usr/lib/python3.8/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "/home/seanodea/.local/lib/python3.8/site-packages/hypercorn/asyncio/run.py", line 163, in asyncio_worker
    app = load_application(config.application_path)
  File "/home/seanodea/.local/lib/python3.8/site-packages/hypercorn/utils.py", line 92, in load_application
    module = import_module(import_name)
  File "/usr/lib/python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 783, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/home/seanodea/code/fl-statusboard/flsb-flask/avwx-api/avwx_api/__init__.py", line 3, in <module>
    from avwx_api import api, views
  File "/home/seanodea/code/fl-statusboard/flsb-flask/avwx-api/avwx_api/api/__init__.py", line 1, in <module>
    from . import current, gfs, station
  File "/home/seanodea/code/fl-statusboard/flsb-flask/avwx-api/avwx_api/api/current.py", line 14, in <module>
    class MetarFetch(Report):
  File "/home/seanodea/.local/lib/python3.8/site-packages/quart_openapi/pint.py", line 286, in decorator
    self._add_resource(func_or_viewcls, path, methods, *args, **kwargs)
  File "/home/seanodea/.local/lib/python3.8/site-packages/quart_openapi/pint.py", line 199, in _add_resource
    super().add_url_rule(path, endpoint, view_func, methods,  # pylint: disable=no-member
TypeError: add_url_rule() got multiple values for argument 'provide_automatic_options'
  File "/usr/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
    self.run()
  File "/usr/lib/python3.8/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "/home/seanodea/.local/lib/python3.8/site-packages/hypercorn/asyncio/run.py", line 163, in asyncio_worker
    app = load_application(config.application_path)
  File "/home/seanodea/.local/lib/python3.8/site-packages/hypercorn/utils.py", line 92, in load_application
    module = import_module(import_name)
  File "/usr/lib/python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 783, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/home/seanodea/code/fl-statusboard/flsb-flask/avwx-api/avwx_api/__init__.py", line 3, in <module>
    from avwx_api import api, views
  File "/home/seanodea/code/fl-statusboard/flsb-flask/avwx-api/avwx_api/api/__init__.py", line 1, in <module>
    from . import current, gfs, station
  File "/home/seanodea/code/fl-statusboard/flsb-flask/avwx-api/avwx_api/api/current.py", line 14, in <module>
    class MetarFetch(Report):
  File "/home/seanodea/.local/lib/python3.8/site-packages/quart_openapi/pint.py", line 286, in decorator
    self._add_resource(func_or_viewcls, path, methods, *args, **kwargs)
  File "/home/seanodea/.local/lib/python3.8/site-packages/quart_openapi/pint.py", line 199, in _add_resource
    super().add_url_rule(path, endpoint, view_func, methods,  # pylint: disable=no-member
TypeError: add_url_rule() got multiple values for argument 'provide_automatic_options'
Traceback (most recent call last):
  File "/usr/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
    self.run()
  File "/usr/lib/python3.8/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "/home/seanodea/.local/lib/python3.8/site-packages/hypercorn/asyncio/run.py", line 163, in asyncio_worker
    app = load_application(config.application_path)
  File "/home/seanodea/.local/lib/python3.8/site-packages/hypercorn/utils.py", line 92, in load_application
    module = import_module(import_name)
  File "/usr/lib/python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 783, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/home/seanodea/code/fl-statusboard/flsb-flask/avwx-api/avwx_api/__init__.py", line 3, in <module>
    from avwx_api import api, views
  File "/home/seanodea/code/fl-statusboard/flsb-flask/avwx-api/avwx_api/api/__init__.py", line 1, in <module>
    from . import current, gfs, station/home/seanodea
  File "/home/seanodea/code/fl-statusboard/flsb-flask/avwx-api/avwx_api/api/current.py", line 14, in <module>
    class MetarFetch(Report):
  File "/home/seanodea/.local/lib/python3.8/site-packages/quart_openapi/pint.py", line 286, in decorator
    self._add_resource(func_or_viewcls, path, methods, *args, **kwargs)
  File "/home/seanodea/.local/lib/python3.8/site-packages/quart_openapi/pint.py", line 199, in _add_resource
    super().add_url_rule(path, endpoint, view_func, methods,  # pylint: disable=no-member
TypeError: add_url_rule() got multiple values for argument 'provide_automatic_options'
Process Process-4:
Traceback (most recent call last):
  File "/usr/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
    self.run()
  File "/usr/lib/python3.8/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "/home/seanodea/.local/lib/python3.8/site-packages/hypercorn/asyncio/run.py", line 163, in asyncio_worker
    app = load_application(config.application_path)
  File "/home/seanodea/.local/lib/python3.8/site-packages/hypercorn/utils.py", line 92, in load_application
    module = import_module(import_name)
  File "/usr/lib/python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 783, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/home/seanodea/code/fl-statusboard/flsb-flask/avwx-api/avwx_api/__init__.py", line 3, in <module>
    from avwx_api import api, views
  File "/home/seanodea/code/fl-statusboard/flsb-flask/avwx-api/avwx_api/api/__init__.py", line 1, in <module>
    from . import current, gfs, station
  File "/home/seanodea/code/fl-statusboard/flsb-flask/avwx-api/avwx_api/api/current.py", line 14, in <module>
    class MetarFetch(Report):
  File "/home/seanodea/.local/lib/python3.8/site-packages/quart_openapi/pint.py", line 286, in decorator
    self._add_resource(func_or_viewcls, path, methods, *args, **kwargs)
  File "/home/seanodea/.local/lib/python3.8/site-packages/quart_openapi/pint.py", line 199, in _add_resource
    super().add_url_rule(path, endpoint, view_func, methods,  # pylint: disable=no-member
TypeError: add_url_rule() got multiple values for argument 'provide_automatic_options'

Could you send me a pip freeze? I believe this may be an issue with a quart/pint library update.

I'm using your repo as a git submodule to mine which contains my own Dockerfile, and a few patches(currently unapplied) to create a /status URL that puts out Mongo: true, Pgsql: false depending on the status. This image is going to run in heroku so it takes the PORT env which is always random and rerouted by the host on container up.

Dockerfile:

# Start from the official Python 3.8 container
FROM python:3.8.3-buster

RUN apt-get update
RUN apt-get install netcat-openbsd -y

# Set the main working directory
WORKDIR /home/avwx

# Set the service credentials as environment variables
ENV MONGO_URI=$MONGODB_URI \
    LOG_KEY=$ROLLBAR_KEY \
    PSQL_URI=$DATABASE_URL

# Install the require Python packages
COPY avwx-api/requirements.txt /home/avwx/requirements.txt
RUN pip install -U pip
RUN pip install -Ur avwx-api/requirements.txt --no-cache-dir --compile

# Copy the application code
COPY avwx-api/avwx_api /home/avwx/avwx_api
COPY status.py /home/avwx/avwx_api/
COPY status.patch /home/avwx/avwx_api/

WORKDIR /home/avwx/avwx_api
#RUN cat status.patch | patch
WORKDIR /home/avwx

# Run the application
ENTRYPOINT ["hypercorn"]
CMD ["avwx_api:app", "--bind", "0.0.0.0:${PORT}"]

On the container:

aiofiles==0.5.0
 attrs==19.3.0
 avwx-api-core @ git+git://github.com/avwx-rest/avwx-api-core.git@779cffd4826e43d12c0bf1e0fb43dca5d43c361a
 avwx-engine==1.4.4
 blinker==1.4
 certifi==2020.4.5.2
 chardet==3.0.4
 click==7.1.2
 dicttoxml==1.7.4
 dnspython==1.16.0
 geographiclib==1.50
 geopy==1.22.0
 gunicorn==20.0.4
 h11==0.9.0
 h2==3.2.0
 hpack==3.0.0
 hstspreload==2020.6.9
 httpcore==0.9.1
 httpx==0.13.3
 Hypercorn==0.10.0
 hyperframe==5.2.0
 idna==2.9
 itsdangerous==1.1.0
 Jinja2==2.11.2
 jsonschema==3.2.0
 MarkupSafe==1.1.1
 motor==2.1.0
 numpy==1.18.5
 priority==1.3.0
 pymongo==3.10.1
 pyrsistent==0.16.0
 python-dateutil==2.8.1
 PyYAML==5.3.1
 Quart==0.12.0
 quart-openapi==1.5.3
 requests==2.23.0
 rfc3986==1.4.0
 rollbar==0.15.0
 scipy==1.4.1
 six==1.15.0
 sniffio==1.1.0
 toml==0.10.1
 typing-extensions==3.7.4.2
 urllib3==1.25.9
 voluptuous==0.11.7
 Werkzeug==1.0.1
 wsproto==0.15.0
 xmltodict==0.12.0

On my workstation:

aiofiles==0.5.0
attrs==19.3.0
Automat==0.8.0
avwx-api-core==0.1.0
avwx-engine==1.4.4
blinker==1.4
cached-property==1.5.1
certifi==2019.11.28
chardet==3.0.4
Click==7.0
cloud-init==20.1
colorama==0.4.3
command-not-found==0.3
configobj==5.0.6
constantly==15.1.0
cryptography==2.8
dbus-python==1.2.16
dicttoxml==1.7.4
distro==1.4.0
distro-info===0.23ubuntu1
dnspython==1.16.0
docker==4.1.0
docker-compose==1.25.0
dockerpty==0.4.1
docopt==0.6.2
entrypoints==0.3
Flask==1.1.1
geographiclib==1.50
geopy==1.22.0
gunicorn==20.0.4
h11==0.9.0
h2==3.2.0
hpack==3.0.0
hstspreload==2020.6.5
httpcore==0.9.1
httplib2==0.14.0
httpx==0.13.3
Hypercorn==0.10.0
hyperframe==5.2.0
hyperlink==19.0.0
idna==2.8
importlib-metadata==1.5.0
incremental==16.10.1
itsdangerous==1.1.0
Jinja2==2.10.1
jsonpatch==1.22
jsonpointer==2.0
jsonschema==3.2.0
keyring==18.0.1
language-selector==0.1
launchpadlib==1.10.13
lazr.restfulclient==0.14.2
lazr.uri==1.0.3
MarkupSafe==1.1.0
more-itertools==4.2.0
motor==2.1.0
netifaces==0.10.4
numpy==1.18.5
oauthlib==3.1.0
pathspec==0.7.0
priority==1.3.0
pyasn1==0.4.2
pyasn1-modules==0.2.1
PyGObject==3.36.0
PyHamcrest==1.9.0
pyinotify==0.9.6
PyJWT==1.7.1
pymacaroons==0.13.0
pymongo==3.10.1
PyNaCl==1.3.0
pyOpenSSL==19.0.0
pyrsistent==0.15.5
pyserial==3.4
python-apt==2.0.0
python-dateutil==2.8.1
python-debian===0.1.36ubuntu1
PyYAML==5.3.1
Quart==0.12.0
quart-openapi==1.5.3
requests==2.22.0
requests-unixsocket==0.2.0
rfc3986==1.4.0
rollbar==0.15.0
scipy==1.4.1
SecretStorage==2.3.1
service-identity==18.1.0
simplejson==3.16.0
six==1.14.0
sniffio==1.1.0
ssh-import-id==5.10
systemd-python==234
texttable==1.6.2
toml==0.10.1
Twisted==18.9.0
typing-extensions==3.7.4.2
ubuntu-advantage-tools==20.3
ufw==0.36
unattended-upgrades==0.1
urllib3==1.25.8
voluptuous==0.11.7
wadllib==1.3.3
websocket-client==0.53.0
Werkzeug==1.0.1
wsproto==0.15.0
xmltodict==0.12.0
yamllint==1.20.0
zipp==1.0.0
zope.interface==4.7.1

Here is the modification I intend to make once it runs successfully:

status.patch

diff -ruN __init__.py __init__.py
--- __init__.py 2020-06-08 03:11:34.198730200 -0500
+++ __init__.py 2020-06-08 03:08:33.188631300 -0500
@@ -1,3 +1,3 @@
 from avwx_api.app_config import app

-from avwx_api import api, views
+from avwx_api import api, views, status

status.py

"""
App Status
"""

from pymongo import MongoClient
import time
import psycopg2
#import avwx_api.handle.gfs as handle
#from avwx_api import app
#from avwx_api.api.base import Report, Parse


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

@app.route("/status")
def Status():
    print("{mongo_connection: " + str(mongo_test()) + ", postgres_connection: " + str(postgres_test()) + "}")

def mongo_test():
    try:
        mongo_uri = os.env.get('MONGO_URI', "localhost:1111")
        client = MongoClient(host = ["localhost:1111"], serverSelectionTimeoutMS = 2000)
        client.server_info() # will throw an exception
        return True
    except:
        return False

def postgres_test():
    try:
        postgres_uri = os.env.get('PSQL_URI', "localhost:1111")
        conn = psycopg2.connect("dbname='mydb' user='myuser' host='my_ip' password='mypassword' connect_timeout=1 ")
        conn.close()
        return True
    except:
        return False

Here is the portion of my cicd file that is failing to start the application.

flsb-test:
  variables:
    PORT: 5000
    CHECKSTRING: "Hello World!"
    CHECKURL: "http://localhost:$PORT/hw"
  extends: .localtest
  script:
    - cd /home/avwx
    - pip freeze || pip3 freeze
    - gunicorn(hypercorn too) avwx_api:app --bind 0.0.0.0:$PORT &
    - export starttime=`date +%s`
    - while ! /bin/nc.openbsd -v 127.0.0.1 $PORT -w 1 && [ $(($(date +%s) - 120)) -lt $starttime ]; do sleep 5;echo "waiting on ports to be up...";done
    - if [ $(wget -q $CHECKURL -O - | grep "${CHECKSTRING}" | wc -l) -eq 0 ];then echo "Failed to find ${CHECKSTRING} in output";exit 1;fi

This turns out to be an issue where quart changed some parameters but quart-openapi hasn't been updated to work with it yet. I've submitted an issue/fix and updated the quart version number in the meantime. Fetching the update should fix your issue.

Looking at your status.py code, I'd highly recommend using async connection libraries from MongoDB and Postgres instead of pymongo and psycopg2 since you're running in an async context. An instance of MotorClient is already available at app.mdb for you to use. You can use the asyncpg library for Postgres.

Thank you for your help and your work on this!!!

Here is my new status.py:

"""
App Status
"""

import time, os, asyncio, asyncpg
import motor.motor_asyncio

from avwx_api import app #for MorotClient, a mongo db async module


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

@app.route("/status")
async def Status():
    if os.environ.get('MONGO_URI') != "":
        url = os.environ.get('MONGO_URI')
    else:
        url = os.environ.get('MONGODB_URI')

    try:
        client = motor.motor_asyncio.AsyncIOMotorClient(url)
        db = client.get_default_database()
        mongostat = True
    except:
        mongostat = False

    if os.environ.get('PSQL_URI') != "":
        url = os.environ.get('PSQL_URI')
    else:
        url = os.environ.get('DATABASE_URL')

    query = "SELECT * FROM pg_settings WHERE name = 'port';"
    try:
        conn = await asyncpg.connect(url)
        if callable(query):
            await query(conn)
        else:
            await conn.execute(query)
        await conn.close()
        pgstat = True
    except:
        pgstat = False
    return "{status: 'up', mongo_connection: " + str(mongostat) + ", postgres_connection: " + str(pgstat) + ", port: " + str(os.environ.get('PORT')) + "}"

You may find this to be a bit faster. This aio.gather allows multiple async calls to be made at once, so you don't have to wait for the mongo check to complete before testing the sql connection. Moved some other things to global since they won't change during runtime.

import os
import asyncio as aio
import asyncpg
from avwx_api import app

QUERY = "SELECT * FROM pg_settings WHERE name = 'port';"
DB_URL = os.environ.get('PSQL_URI') or os.environ.get('DATABASE_URL')
PORT = os.environ.get('PORT')

async def mongo_is_up() -> bool:
    """
    Returns True if there is an active connection to MongoDB
    """
    try:
        await app.mdb.server_info()
        return True
    except:
        return False

async def psql_is_up() -> bool:
    """
    Returns True if there is an active connection to Postgres
    """
    try:
        conn = await asyncpg.connect(DB_URL)
        await conn.execute(QUERY)
        await conn.close()
        return True
    except:
        return False

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

@app.route("/status")
async def status():
    """
    Returns server status information
    """
    results = await aio.gather(mongo_is_up(), psql_is_up())
    return {
        "status": "up",
        "mongo": results[0],
        "postgres": results[1],
        "port": PORT,
    }