Gunicorn: la recarga no funciona para uvicorn.workers.UvicornWorker

Creado en 26 may. 2020  ·  28Comentarios  ·  Fuente: benoitc/gunicorn

esta es mi línea gunicorn -k uvicorn.workers.UvicornWorker --reload-engine=inotify --workers=1 --log-level debug --access-logfile - --error-logfile - --disable-redirect-access-to-syslog --reload main:app comando

tenga en cuenta que he probado tanto con la rama maestra de gunicorn como con la última versión estable.

Este es el código que estoy usando

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def read_root():
    # raise 1
    return {"Hello": "World"}


@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str = None):

    return {"item_id 2": item_id, "q": q}

la recarga no se recupera cuando hago cambios en la aplicación. estoy usando vscode como editor

(

Comentario más útil

El problema parece estar en el método gunicorn -> workers/base.py -> init_process() -> def changed(fname) . Esta función es llamada por el subproceso del observador de cambios y termina con sys.exit(0) que, desafortunadamente, solo da como resultado la eliminación de ese subproceso (es por eso que los cambios se detectan solo una vez).

Para solucionar el problema, puede definir un UvicornWorker personalizado con un hilo adicional que envíe una señal KILL al proceso actual si la bandera active del trabajador está configurada en False (lo que se hace con changed(fname) función anterior).

import os
import signal
import threading
import time
from typing import Any, List, Dict

from uvicorn.workers import UvicornWorker


class ReloaderThread(threading.Thread):

    def __init__(self, worker: UvicornWorker, sleep_interval: float = 1.0):
        super().__init__()
        self.setDaemon(True)
        self._worker = worker
        self._interval = sleep_interval

    def run(self) -> None:
        while True:
            if not self._worker.alive:
                os.kill(os.getpid(), signal.SIGINT)
            time.sleep(self._interval)


class RestartableUvicornWorker(UvicornWorker):

    CONFIG_KWARGS = {"loop": "uvloop", "http": "httptools"}

    def __init__(self, *args: List[Any], **kwargs: Dict[str, Any]):
        super().__init__(*args, **kwargs)
        self._reloader_thread = ReloaderThread(self)

    def run(self) -> None:
        if self.cfg.reload:
            self._reloader_thread.start()
        super().run()

Para usar ese trabajador, debe especificarlo con el parámetro worker_class gunicorn, por ejemplo:

gunicorn -k some_package.RestartableUvicornWorker ...

Todos 28 comentarios

Solo debe usar uvicorn en lugar de agregar gunicorn encima para el desarrollo local. Algo como esto:

uvicorn main:app --reload

Y luego, si tiene watchgod instalado, uvicorn debería recogerlo automáticamente para mejorar el proceso de recarga. Si está en Docker, agregue --host 0.0.0.0

Este es mi comando de inicio gunicorn my_pro.asgi:application -b unix:/run/day01/gunicorn.socket --reload -w 2 -t 1 -k uvicorn.workers.UvicornWorker , se puede recargar normalmente, pero parece haber un retraso de 1-2 segundos, @sandys

He observado un comportamiento similar al de @sandys .

La solución alternativa propuesta por @ Andrew-Chen-Wang no siempre es útil (no se acerca a la configuración de producción; problemas con Windows + Docker).

@sandys , @systemime : ¿Qué versiones de Python y qué sistema operativo estaba usando?

@ tobias-hd Esta no es mi solución; es el método recomendado por la documentación de uvicorn: https://www.uvicorn.org/deployment/

Ejecute la CLI de gunicorn durante la implementación; ejecute uvicorn's durante el desarrollo.

Si está usando --reload y DEBUG log level, supongo que está desarrollando localmente. Si está actuando para la producción como supongo que dice tobias, entonces debe usar gunicorn y no usar la bandera de recarga (use supervisord).

Además, en su IDE, debe hacer CTRL + S o guardar el archivo con algunas teclas de acceso rápido. Así es como funcionan / comprueban flask, Django o uvicorn en busca de nuevos cambios en los archivos.

Este es mi comando de inicio gunicorn my_pro.asgi:application -b unix:/run/day01/gunicorn.socket --reload -w 2 -t 1 -k uvicorn.workers.UvicornWorker , se puede recargar normalmente, pero parece haber un retraso de 1-2 segundos, @sandys

Eso es porque el argumento -t hace que los trabajadores inactivos se recarguen cada segundo.

¿Existe alguna posibilidad de que esto se pueda revisar? Tengo algunos problemas con Uvicorn en los que las migraciones se cargan al inicio de la aplicación, lo que activa django.core.exceptions.SynchronousOnlyOperation . Ejecutar Gunicorn, por supuesto, resuelve esto, ya que ahora estamos usando trabajadores en lugar de subprocesos, pero realmente me gustaría tener una opción para recargar.

De cualquier manera, ¿cuál es el problema de que su servidor de desarrollo intente estar lo más cerca posible de su servidor de producción?

Por ejemplo, si intenta usar Django Prometheus con Uvicorn sin Gunicorn, obtendrá un rastro como este (también usando la biblioteca Django Cookie Cutter Library @ Andrew-Chen-Wang:

django            | Process SpawnProcess-1:
django            | Traceback (most recent call last):
django            |   File "/usr/local/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
django            |     self.run()
django            |   File "/usr/local/lib/python3.8/multiprocessing/process.py", line 108, in run
django            |     self._target(*self._args, **self._kwargs)
django            |   File "/usr/local/lib/python3.8/site-packages/uvicorn/subprocess.py", line 61, in subprocess_started
django            |     target(sockets=sockets)
django            |   File "/usr/local/lib/python3.8/site-packages/uvicorn/_impl/asyncio.py", line 47, in run
django            |     loop.run_until_complete(self.serve(sockets=sockets))
django            |   File "uvloop/loop.pyx", line 1456, in uvloop.loop.Loop.run_until_complete
django            |   File "/usr/local/lib/python3.8/site-packages/uvicorn/_impl/asyncio.py", line 54, in serve
django            |     config.load()
django            |   File "/usr/local/lib/python3.8/site-packages/uvicorn/config.py", line 306, in load
django            |     self.loaded_app = import_from_string(self.app)
django            |   File "/usr/local/lib/python3.8/site-packages/uvicorn/importer.py", line 20, in import_from_string
django            |     module = importlib.import_module(module_str)
django            |   File "/usr/local/lib/python3.8/importlib/__init__.py", line 127, in import_module
django            |     return _bootstrap._gcd_import(name[level:], package, level)
django            |   File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
django            |   File "<frozen importlib._bootstrap>", line 991, in _find_and_load
django            |   File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
django            |   File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
django            |   File "<frozen importlib._bootstrap_external>", line 783, in exec_module
django            |   File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
django            |   File "./config/asgi.py", line 23, in <module>
django            |     django.setup(set_prefix=False)
django            |   File "/usr/local/lib/python3.8/site-packages/django/__init__.py", line 24, in setup
django            |     apps.populate(settings.INSTALLED_APPS)
django            |   File "/usr/local/lib/python3.8/site-packages/django/apps/registry.py", line 122, in populate
django            |     app_config.ready()
django            |   File "/usr/local/lib/python3.8/site-packages/django_prometheus/apps.py", line 23, in ready
django            |     ExportMigrations()
django            |   File "/usr/local/lib/python3.8/site-packages/django_prometheus/migrations.py", line 52, in ExportMigrations
django            |     executor = MigrationExecutor(connections[alias])
django            |   File "/usr/local/lib/python3.8/site-packages/django/db/migrations/executor.py", line 18, in __init__
django            |     self.loader = MigrationLoader(self.connection)
django            |   File "/usr/local/lib/python3.8/site-packages/django/db/migrations/loader.py", line 49, in __init__
django            |     self.build_graph()
django            |   File "/usr/local/lib/python3.8/site-packages/django/db/migrations/loader.py", line 212, in build_graph
django            |     self.applied_migrations = recorder.applied_migrations()
django            |   File "/usr/local/lib/python3.8/site-packages/django/db/migrations/recorder.py", line 76, in applied_migrations
django            |     if self.has_table():
django            |   File "/usr/local/lib/python3.8/site-packages/django/db/migrations/recorder.py", line 56, in has_table
django            |     return self.Migration._meta.db_table in self.connection.introspection.table_names(self.connection.cursor())
django            |   File "/usr/local/lib/python3.8/site-packages/django/utils/asyncio.py", line 24, in inner
django            |     raise SynchronousOnlyOperation(message)
django            | django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.

No tengo fácil acceso para modificar la biblioteca.

Supongo que esto tiene que ver con el bucle de eventos asyncio que interfiere con el resto. Le echaré un vistazo.

@benoitc Alguien en Stackoverflow realmente solucionó mi problema exactamente, pero significa que necesitaba agregar mi raíz de proyecto a mi ruta, lo cual ... es algo que no hice anteriormente y estoy investigando las repercusiones de hacerlo .

Aquí está el enlace:
https://stackoverflow.com/questions/64306187/how-to-make-django-3-channels-and-uvicorn-work-together

@omarsumadi Para ejecutar migraciones, use el comando manage.py python manage.py makemigrations && python manage.py migrate . Para ejecutar el servidor: uvicorn config.asgi:application --host 0.0.0.0 --reload . El --host 0.0.0.0 permite que su dispositivo móvil (por ejemplo, un teléfono) acceda también a su servidor web. Además, en su IDE, debe hacer CTRL + S o guardar el archivo con algunas teclas de acceso rápido. Así es como funcionan / comprueban flask, Django o uvicorn en busca de nuevos cambios en los archivos.

Realmente, solo sigue la documentación de uvicorn. El comentario que dijo Tobias acerca de que Windows + Docker es un problema no lo es si solo usa uvicorn sin Docker en primer lugar ...

De cualquier manera, ¿cuál es el problema de que su servidor de desarrollo intente estar lo más cerca posible de su servidor de producción?

Es solo la configuración. ¿Las diferencias? Niveles de registro predeterminados, configuración de HTTPS, barra de herramientas de depuración de django, DEBUG = False, etc. De lo contrario, su servidor de desarrollo no debería desviarse demasiado de su servidor de producción (y eso también depende de las opiniones personales).

Hola andrew
Gracias por la respuesta. Sé que intentas ayudar, pero la mayoría de
trabajar en aplicaciones de producción quisiera 1) Docker y 2) Gunicorn para que el desarrollador
está cerca de la producción.

Todos los que trabajamos en equipos grandes nos hemos enfrentado a los problemas comunes de mac
(intel vs m1) vs windows vs linux.

Créame cuando digo esto: pagaríamos por lograr la consistencia de la producción en
dev ... versus elegir la opción conveniente. 🙏

El miércoles 3 de febrero de 2021 a las 21:24 Andrew Chen Wang, [email protected]
escribió:

@omarsumadi https://github.com/omarsumadi Para ejecutar migraciones, use el
comando manage.py python manage.py makemigrations && python manage.py
emigrar. Para ejecutar el servidor: uvicorn config. asgi: aplicación --host
0.0.0.0 --recargar. --Host 0.0.0.0 permite que su dispositivo móvil (por ejemplo, como
un teléfono) para acceder también a su servidor web. Además, en su IDE,
debe hacer CTRL + S o guardar el archivo con algunas teclas de acceso rápido. Por lo general, así es como
flask, Django o uvicorn funciona / comprueba si hay nuevos cambios en los archivos.

Realmente, solo sigue la documentación de uvicorn. El comentario que dijo Tobias sobre
Windows + Docker es un problema no es un problema si solo usa uvicorn
sin docker en primer lugar ...

De todos modos, ¿cuál es el problema de que su servidor de desarrollo intente
estar lo más cerca posible de su servidor de producción?

Es solo la configuración. ¿Las diferencias? Niveles de registro predeterminados, configurando
Configuración de HTTPS, barra de herramientas de depuración de django, DEBUG = False, etc. De lo contrario, sí
su servidor de desarrollo no debería desviarse demasiado de su servidor de producción (y
eso también se debe a opiniones personales).

-
Recibes esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/benoitc/gunicorn/issues/2339#issuecomment-772613108 ,
o darse de baja
https://github.com/notifications/unsubscribe-auth/AAASYUY4YKMKAGBQTLCR6OTS5FWSJANCNFSM4NKBSMLQ
.

¡Hola Sandy! Me encanta Docker, pero es posible que algunas personas no (refiriéndose al comentario inicial de Tobias). Los comandos que acabo de publicar funcionan de la misma manera en Docker y con un virtualenv, independientemente.

pagaríamos para obtener consistencia de producción en dev.

Yo también siento el dolor ... Pero sí, Docker: super útil y necesario, sin importar si estás en un equipo o no.

... gunicorn para que el desarrollador esté cerca de la producción.

Aún así, recomendaría que use uvicorn cuando trabaje en el entorno de desarrollo. Está lo suficientemente cerca de gunicorn, y será un poco más fácil en tu computadora (al menos para mí) en términos de consumo de recursos (y reducción de ruido debido a un ventilador fenomenal). Pero sí, estoy de acuerdo: el entorno de desarrollo no debería desviarse demasiado de prod, si es que lo hace.

@ Andrew-Chen-Wang Hola Andrew: como dijiste, podría ser un problema de Docker, ya sea que lo sea o no, puse una mejora en el repositorio de Django Cookie Cutter para corregir Uvicorn que genera este error para la versión de Docker: ejecuta Uvicorn programáticamente. También usando VSCode, así que sí, todas las migraciones y la observación de cambios en los archivos funcionan perfectamente. El problema es que Uvicorn no funciona con esto, no es un problema con la siguiente documentación.

Aún así, es una perspectiva interesante porque tuve que agregar la raíz del proyecto a la ruta, ejecutar django.setup (), luego ejecutar asgi.py desde los scripts de inicio de Docker, que no es lo que hace actualmente la configuración de Cookie Cutter. Ejecutar Gunicorn con trabajadores de Uvicorn resuelve esto sin ninguna modificación, lo que me hace estar de acuerdo si Gunicorn tuvo trabajo de recarga con trabajadores de Uvicorn, esto podría ser una buena adición positiva para hacer que el desarrollo y la producción sean lo más similares posible. Quizás esto sirva como ejemplo de lo que sucede cuando no lo son: las cosas tienen que cambiar.

Gracias,
Omar

@omarsumadi Echando un vistazo a ese rastreo nuevamente, no tengo django.setup() en mi asgi, wsgi o manage.py. Eche un vistazo a los archivos del cortador de galletas Django de nuevo. También echaré un vistazo a ese tema que surgió.

@omarsumadi Echando un vistazo a ese rastreo nuevamente, no tengo django.setup() en mi asgi, wsgi o manage.py. Eche un vistazo a los archivos del cortador de galletas Django de nuevo. También echaré un vistazo a ese tema que surgió.

Yo tampoco, simplemente deshice todos los cambios que sugerí y lo ejecuté nuevamente (tengo algunos comentarios en el archivo, por lo que no coincide exactamente con el archivo ASGI.py de Cookie Cutter). Probablemente debería haberte dicho que también estoy ejecutando Django Channels (pero sé que no te gusta jaja)

django            | Process SpawnProcess-1:
django            | Traceback (most recent call last):
django            |   File "/usr/local/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
django            |     self.run()
django            |   File "/usr/local/lib/python3.8/multiprocessing/process.py", line 108, in run
django            |     self._target(*self._args, **self._kwargs)
django            |   File "/usr/local/lib/python3.8/site-packages/uvicorn/subprocess.py", line 61, in subprocess_started
django            |     target(sockets=sockets)
django            |   File "/usr/local/lib/python3.8/site-packages/uvicorn/_impl/asyncio.py", line 47, in run
django            |     loop.run_until_complete(self.serve(sockets=sockets))
django            |   File "uvloop/loop.pyx", line 1456, in uvloop.loop.Loop.run_until_complete
django            |   File "/usr/local/lib/python3.8/site-packages/uvicorn/_impl/asyncio.py", line 54, in serve
django            |     config.load()
django            |   File "/usr/local/lib/python3.8/site-packages/uvicorn/config.py", line 306, in load
django            |     self.loaded_app = import_from_string(self.app)
django            |   File "/usr/local/lib/python3.8/site-packages/uvicorn/importer.py", line 20, in import_from_string
django            |     module = importlib.import_module(module_str)
django            |   File "/usr/local/lib/python3.8/importlib/__init__.py", line 127, in import_module
django            |     return _bootstrap._gcd_import(name[level:], package, level)
django            |   File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
django            |   File "<frozen importlib._bootstrap>", line 991, in _find_and_load
django            |   File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
django            |   File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
django            |   File "<frozen importlib._bootstrap_external>", line 783, in exec_module
django            |   File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
django            |   File "./config/asgi.py", line 29, in <module>
django            |     django_application = get_asgi_application()
django            |   File "/usr/local/lib/python3.8/site-packages/django/core/asgi.py", line 12, in get_asgi_application
django            |     django.setup(set_prefix=False)
django            |   File "/usr/local/lib/python3.8/site-packages/django/__init__.py", line 24, in setup
django            |     apps.populate(settings.INSTALLED_APPS)
django            |   File "/usr/local/lib/python3.8/site-packages/django/apps/registry.py", line 122, in populate
django            |     app_config.ready()
django            |   File "/usr/local/lib/python3.8/site-packages/django_prometheus/apps.py", line 23, in ready
django            |     ExportMigrations()
django            |   File "/usr/local/lib/python3.8/site-packages/django_prometheus/migrations.py", line 52, in ExportMigrations
django            |     executor = MigrationExecutor(connections[alias])
django            |   File "/usr/local/lib/python3.8/site-packages/django/db/migrations/executor.py", line 18, in __init__
django            |     self.loader = MigrationLoader(self.connection)
django            |   File "/usr/local/lib/python3.8/site-packages/django/db/migrations/loader.py", line 49, in __init__
django            |     self.build_graph()
django            |   File "/usr/local/lib/python3.8/site-packages/django/db/migrations/loader.py", line 212, in build_graph
django            |     self.applied_migrations = recorder.applied_migrations()
django            |   File "/usr/local/lib/python3.8/site-packages/django/db/migrations/recorder.py", line 76, in applied_migrations
django            |     if self.has_table():
django            |   File "/usr/local/lib/python3.8/site-packages/django/db/migrations/recorder.py", line 56, in has_table
django            |     return self.Migration._meta.db_table in self.connection.introspection.table_names(self.connection.cursor())
django            |   File "/usr/local/lib/python3.8/site-packages/django/utils/asyncio.py", line 24, in inner
django            |     raise SynchronousOnlyOperation(message)
django            | django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.

Mi sugerencia en los problemas que vio es una forma de solucionar este problema que tengo aquí. Algo está llamando al programa de instalación de Django desde este paquete.

@omarsumadi ya veo. Creo que sería prudente trasladar esta discusión a ese tema, ya que, en mi opinión, este problema debería cerrarse ya que varias personas ya han dado varias respuestas correctas, y su problema actual está más relacionado con Django.

@ Andrew-Chen-Wang Y, ¿cuál es la respuesta correcta?

gunicorn main:app --reload -w 2 -k uvicorn.workers.UvicornWorker

todavía no funciona para mí.
Simplemente usar uvicorn no es la respuesta que estoy buscando.

@alanwilter Hm, no soy un gurú en gunicorn, pero intenta instalar el paquete inotify. Si eso no funciona, intente usar watchgod. Podría ser simplemente que Gunicorn no detecta las notificaciones del sistema.

Editar: creo que necesitas agregar --reload-engine inotify

Me enfrento al mismo problema aquí en gunicorn

El jueves 11 de marzo de 2021, 20:53 Andrew Chen Wang, @ . * >
escribió:

@alanwilter https://github.com/alanwilter Hm, no soy un gurú en gunicorn,
pero intente instalar el paquete inotify. Si eso no funciona, intente usar
Dios del reloj. Podría ser simplemente que Gunicorn no detecta las notificaciones del sistema.

-
Recibes esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/benoitc/gunicorn/issues/2339#issuecomment-796815523 ,
o darse de baja
https://github.com/notifications/unsubscribe-auth/AAASYU3UM7OXNTOIFZGLBGTTDDG5XANCNFSM4NKBSMLQ
.

algo muy extraño está sucediendo con la última versión de gunicorn y uvicornworker.

Esto funciona UNA VEZ
gunicorn p:app -p 8080 --reload --reload-engine inotify -k uvicorn.workers.UvicornWorker

gunicorn recoge correctamente cualquier cambio de archivo y realiza una recarga en caliente

unicorn p:app -p 8080 --reload --reload-engine inotify   -k uvicorn.workers.UvicornWorker                                                (git)-[master]-
[2021-03-17 14:35:42 +0530] [298736] [INFO] Starting gunicorn 20.0.4
[2021-03-17 14:35:42 +0530] [298736] [INFO] Listening at: http://127.0.0.1:8000 (298736)
[2021-03-17 14:35:42 +0530] [298736] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2021-03-17 14:35:42 +0530] [298739] [INFO] Booting worker with pid: 298739
[2021-03-17 14:35:43 +0530] [298739] [INFO] Started server process [298739]
[2021-03-17 14:35:43 +0530] [298739] [INFO] Waiting for application startup.
[2021-03-17 14:35:43 +0530] [298739] [INFO] Application startup complete.
[2021-03-17 14:35:48 +0530] [298739] [INFO] Worker reloading: p.py.a42bb679bd860346e8c7d564fdc6cc56.tmp modified

Pero después de esta única recarga, deja de funcionar. No recoge ningún cambio de archivo posterior. No estoy seguro de por qué está pasando

Lo he visto, pero en realidad no hace nada.
gunicorn main:app --reload -k uvicorn.workers.UvicornWorker

Sin embargo, si agrego inotify :

gunicorn main:app --reload --reload-engine inotify -k uvicorn.workers.UvicornWorker
[2021-03-17 10:43:49 +0100] [31332] [INFO] Starting gunicorn 20.0.4
[2021-03-17 10:43:49 +0100] [31332] [INFO] Listening at: http://127.0.0.1:8000 (31332)
[2021-03-17 10:43:49 +0100] [31332] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2021-03-17 10:43:49 +0100] [31333] [INFO] Booting worker with pid: 31333
[2021-03-17 10:43:49 +0100] [31333] [ERROR] Exception in worker process
Traceback (most recent call last):
  File "/Users/alan/Downloads/fastapi/venv/lib/python3.9/site-packages/gunicorn/arbiter.py", line 583, in spawn_worker
    worker.init_process()
  File "/Users/alan/Downloads/fastapi/venv/lib/python3.9/site-packages/uvicorn/workers.py", line 63, in init_process
    super(UvicornWorker, self).init_process()
  File "/Users/alan/Downloads/fastapi/venv/lib/python3.9/site-packages/gunicorn/workers/base.py", line 132, in init_process
    self.reloader = reloader_cls(extra_files=self.cfg.reload_extra_files,
TypeError: __init__() got an unexpected keyword argument 'extra_files'
[2021-03-17 10:43:49 +0100] [31333] [INFO] Worker exiting (pid: 31333)
[2021-03-17 10:43:49 +0100] [31332] [INFO] Shutting down: Master
[2021-03-17 10:43:49 +0100] [31332] [INFO] Reason: Worker failed to boot.

No funciona en absoluto. ¿Hay algo especial que hacer con respecto a inotify ? Parece una característica para los kernels de Linux, estoy ejecutando macOS Big Sur . De todos modos hice pip install -U uvloop httptools inotify .

Así que también probé en Linux (Ubuntu 18.04), pero fallé. Toda mi configuración (necesitas Python 3.7 o superior):

mkdir fastapi
cd fastapi

crear archivo main.py

from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

app = FastAPI()

@app.post("/items/")
async def create_item(item: Item):
    return item

Ahora, configure python venv:

python3 -m venv venv
source venv/bin/activate
pip install -U fastapi gunicorn pip wheel uvicorn uvloop httptools

La forma estándar funciona como se esperaba al cambiar main.py :

uvicorn main:app --reload
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [53638] using statreload
INFO:     Started server process [53640]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
WARNING:  StatReload detected file change in 'main.py'. Reloading...
INFO:     Shutting down
INFO:     Waiting for application shutdown.
INFO:     Application shutdown complete.
INFO:     Finished server process [53640]
INFO:     Started server process [53758]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

Ahora, intente con gunicorn básico, induciendo un cambio que haría que main.py fallara (como un error tipográfico returns item ):

gunicorn main:app --reload -k uvicorn.workers.UvicornWorker
[2021-03-17 11:43:37 +0000] [95542] [INFO] Starting gunicorn 20.0.4
[2021-03-17 11:43:37 +0000] [95542] [INFO] Listening at: http://127.0.0.1:8000 (95542)
[2021-03-17 11:43:37 +0000] [95542] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2021-03-17 11:43:37 +0000] [95544] [INFO] Booting worker with pid: 95544
[2021-03-17 11:43:37 +0000] [95544] [INFO] Started server process [95544]
[2021-03-17 11:43:37 +0000] [95544] [INFO] Waiting for application startup.
[2021-03-17 11:43:37 +0000] [95544] [INFO] Application startup complete.
[2021-03-17 11:44:02 +0000] [95544] [INFO] Worker reloading: /home/awilter/fastapi/main.py modified

Por lo tanto, puede detectar un "cambio", pero no se vuelve a cargar.

Usando, inotify :

gunicorn main:app --reload --reload-engine=inotify -k uvicorn.workers.UvicornWorker
[2021-03-17 11:49:58 +0000] [99660] [INFO] Starting gunicorn 20.0.4
[2021-03-17 11:49:58 +0000] [99660] [INFO] Listening at: http://127.0.0.1:8000 (99660)
[2021-03-17 11:49:58 +0000] [99660] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2021-03-17 11:49:58 +0000] [99662] [INFO] Booting worker with pid: 99662
[2021-03-17 11:49:58 +0000] [99662] [ERROR] Exception in worker process
Traceback (most recent call last):
  File "/home/awilter/fastapi/venv/lib/python3.8/site-packages/gunicorn/arbiter.py", line
 583, in spawn_worker
    worker.init_process()
  File "/home/awilter/fastapi/venv/lib/python3.8/site-packages/uvicorn/workers.py", line
63, in init_process
    super(UvicornWorker, self).init_process()
  File "/home/awilter/fastapi/venv/lib/python3.8/site-packages/gunicorn/workers/base.py",
 line 132, in init_process
    self.reloader = reloader_cls(extra_files=self.cfg.reload_extra_files,
TypeError: __init__() got an unexpected keyword argument 'extra_files'
[2021-03-17 11:49:58 +0000] [99662] [INFO] Worker exiting (pid: 99662)
[2021-03-17 11:49:58 +0000] [99660] [INFO] Shutting down: Master
[2021-03-17 11:49:58 +0000] [99660] [INFO] Reason: Worker failed to boot.

En cuanto a watchgod , no sé cómo usarlo, simplemente hacer --reload-engine=watchgod no funciona.

Creo que obtienes got an unexpected keyword argument 'extra_files' si gunicorn no puede detectar que tienes inotify instalado, ya sea porque no estás ejecutando en Linux o porque hubo errores de importación desde el paquete inotify. https://github.com/benoitc/gunicorn/blob/master/gunicorn/reloader.py#L121

En mi caso, estoy ejecutando Mac, por lo que parece que no puedo usar el motor inotify y mi única opción es usar el motor de encuestas. No sé lo suficiente sobre gunicorn + uvicorn para saber si la recarga ha funcionado alguna vez, pero para mí parece que necesitamos hacer que el servidor uvicorn salga a través de SIGINT o SIGTERM.

Para que el motor de encuestas vuelva a cargar el servidor, agregué un pequeño gancho worker_int en mi archivo gunicorn.conf.py que indica que debo salir del servidor uvicorn a través de SIGINT.

import os
import signal


def worker_int(worker):
    os.kill(worker.pid, signal.SIGINT)

Esto ha permitido que la recarga de código funcione, pero se siente bastante bárbaro. Estoy seguro de que hay una mejor manera de lograrlo. Ejecuto gunicorn así:
gunicorn main:app -w 4 -b 0.0.0.0:8000 -k uvicorn.workers.UvicornWorker --log-file=- --log-level DEBUG --reload

@ c-py u parece estar en el camino correcto. Aproximadamente lo estoy haciendo funcionar en Linux ... sin embargo, tengo que guardar DOS VECES, no una vez para que esto funcione

Si ya está utilizando supervisor dentro de un contenedor, puede agregar un proceso como este al final de su configuración de desarrollo. Necesitará procps y inotify-tools instalados para pkill e inotify-wait.

[program:inotifywait]
command=bash -c "inotifywait --excludei \"[^(\.py)]+$\" -e modify -r path-to-watch/ && pkill -SIGHUP gunicorn"
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

Me gusta este método porque se puede adaptar para trabajar con prácticamente cualquier cosa y no es necesario cambiar nada más en la producción frente al desarrollo.

algo muy extraño está sucediendo con la última versión de gunicorn y uvicornworker.

Esto funciona UNA VEZ
gunicorn p:app -p 8080 --reload --reload-engine inotify -k uvicorn.workers.UvicornWorker

gunicorn recoge correctamente cualquier cambio de archivo y realiza una recarga en caliente

unicorn p:app -p 8080 --reload --reload-engine inotify   -k uvicorn.workers.UvicornWorker                                                (git)-[master]-
[2021-03-17 14:35:42 +0530] [298736] [INFO] Starting gunicorn 20.0.4
[2021-03-17 14:35:42 +0530] [298736] [INFO] Listening at: http://127.0.0.1:8000 (298736)
[2021-03-17 14:35:42 +0530] [298736] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2021-03-17 14:35:42 +0530] [298739] [INFO] Booting worker with pid: 298739
[2021-03-17 14:35:43 +0530] [298739] [INFO] Started server process [298739]
[2021-03-17 14:35:43 +0530] [298739] [INFO] Waiting for application startup.
[2021-03-17 14:35:43 +0530] [298739] [INFO] Application startup complete.
[2021-03-17 14:35:48 +0530] [298739] [INFO] Worker reloading: p.py.a42bb679bd860346e8c7d564fdc6cc56.tmp modified

Pero después de esta única recarga, deja de funcionar. No recoge ningún cambio de archivo posterior. No estoy seguro de por qué está pasando

Experimentando exactamente el mismo comportamiento aquí en Mac, la recarga funciona solo una vez. Sin embargo, no especifiqué --reload-engine.

Después de agregar gunicorn.conf.py por @ c-py, funciona lo suficientemente bien por ahora. Gracias.

El problema parece estar en el método gunicorn -> workers/base.py -> init_process() -> def changed(fname) . Esta función es llamada por el subproceso del observador de cambios y termina con sys.exit(0) que, desafortunadamente, solo da como resultado la eliminación de ese subproceso (es por eso que los cambios se detectan solo una vez).

Para solucionar el problema, puede definir un UvicornWorker personalizado con un hilo adicional que envíe una señal KILL al proceso actual si la bandera active del trabajador está configurada en False (lo que se hace con changed(fname) función anterior).

import os
import signal
import threading
import time
from typing import Any, List, Dict

from uvicorn.workers import UvicornWorker


class ReloaderThread(threading.Thread):

    def __init__(self, worker: UvicornWorker, sleep_interval: float = 1.0):
        super().__init__()
        self.setDaemon(True)
        self._worker = worker
        self._interval = sleep_interval

    def run(self) -> None:
        while True:
            if not self._worker.alive:
                os.kill(os.getpid(), signal.SIGINT)
            time.sleep(self._interval)


class RestartableUvicornWorker(UvicornWorker):

    CONFIG_KWARGS = {"loop": "uvloop", "http": "httptools"}

    def __init__(self, *args: List[Any], **kwargs: Dict[str, Any]):
        super().__init__(*args, **kwargs)
        self._reloader_thread = ReloaderThread(self)

    def run(self) -> None:
        if self.cfg.reload:
            self._reloader_thread.start()
        super().run()

Para usar ese trabajador, debe especificarlo con el parámetro worker_class gunicorn, por ejemplo:

gunicorn -k some_package.RestartableUvicornWorker ...

Gracias @jvasi que funcionó muy bien.

Recibo una notificación INFO cada vez que gunicorn baja de esta manera:

[INFO] Error while closing socket [Errno 9] Bad file descriptor

Creo que esto viene de esta línea en gunicorn: https://github.com/benoitc/gunicorn/blob/1299ea9e967a61ae2edebe191082fd169b864c64/gunicorn/sock.py#L69

Esto parece estar relacionado con https://github.com/benoitc/gunicorn/issues/2604

¿Fue útil esta página
0 / 5 - 0 calificaciones