Gestión de tareas programadas con FastAPI y APScheduler

La integración de un sistema de tareas programdaas es un requisito común en aplicaciones web. Dentro del ecosistema Python, APScheduler se destaca como una solución robusta y flexilbe, especialmente adecuada para aplicaciones basadas en frameworks como FastAPI. A diferencia de soluciones como Celery, que requieren un broker de mensajes (como Redis o RabbitMQ) para un procesamiento distribuido y asíncrono, APScheduler puede funcionar autónomamante con almacenamiento local o en base de datos.

Un error común al integrar APScheduler en una aplicación web es utilizar el BlockingScheduler, ya que este detiene la ejecución del hilo principal y bloquea el servidor web. Para entornos como FastAPI o Flask, se debe emplear el BackgroundScheduler.

Configuración fundamental del planificador

El siguiente fragmento muestra una configuración base que define almacenes de trabajos, ejecutores y el propio planificador. Se utiliza una base de datos SQLite para persistir los trabajos, lo que permite que sobrevivan a reinicios de la aplicación.

from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.executors.pool import ThreadPoolExecutor
from pytz import utc

# Definición de almacén de trabajos y ejecutores
almacen_trabajos = {'persistente': SQLAlchemyJobStore(url='sqlite:///mis_trabajos.db')}
ejecutores = {'defecto': ThreadPoolExecutor(max_workers=20)}

# Instanciación del planificador
planificador = BackgroundScheduler(
    jobstores=almacen_trabajos,
    executors=ejecutores,
    timezone=utc
)

# Ejemplo de una tarea simple
def mostrar_mensaje(mensaje: str):
    print(f"Tarea ejecutada: {mensaje}")

Endpoints de gestión con FastAPI

Para controlar el planificador durante la ejecución, se crean rutas en la aplicación FastAPI. Estos endpoints permiten listar, pausar, reanudar y eliminar tareas de manera dinámica.

from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse

app = FastAPI()

@app.get("/tareas", summary="Lista todas las tareas programadas")
async def listar_tareas():
    trabajos = planificador.get_jobs()
    resultado = []
    for trabajo in trabajos:
        info = {
            "id": trabajo.id,
            "nombre": trabajo.name,
            "proxima_ejecucion": str(trabajo.next_run_time),
            "estado": "Activo" if trabajo.next_run_time else "Pausado"
        }
        resultado.append(info)
    return JSONResponse(content=resultado)

@app.post("/tareas/{trabajo_id}/pausar", summary="Pausa una tarea específica")
async def pausar_tarea(trabajo_id: str):
    trabajo = planificador.get_job(trabajo_id)
    if not trabajo:
        raise HTTPException(status_code=404, detail="Tarea no encontrada")
    trabajo.pause()
    return {"mensaje": f"Tarea {trabajo_id} pausada"}

@app.post("/tareas/{trabajo_id}/reanudar", summary="Reanuda una tarea pausada")
async def reanudar_tarea(trabajo_id: str):
    trabajo = planificador.get_job(trabajo_id)
    if not trabajo:
        raise HTTPException(status_code=404, detail="Tarea no encontrada")
    trabajo.resume()
    return {"mensaje": f"Tarea {trabajo_id} reanudada"}

@app.delete("/tareas/{trabajo_id}", summary="Elimina una tarea definitivamente")
async def eliminar_tarea(trabajo_id: str):
    trabajo = planificador.get_job(trabajo_id)
    if not trabajo:
        raise HTTPException(status_code=404, detail="Tarea no encontrada")
    trabajo.remove()
    return {"mensaje": f"Tarea {trabajo_id} eliminada"}

Integración con el ciclo de vida de FastAPI

Es esencial iniciar y detener el planificador de manera sincronizada con el ciclo de vida de la aplicación FastAPI. Esto se logra utilizando los eventos startup y shutdown.

@app.on_event("startup")
async def iniciar_planificador():
    # Aquí se pueden añadir trabajos iniciales
    planificador.add_job(
        mostrar_mensaje,
        'interval',
        args=["Tarea periódica cada 60 segundos"],
        seconds=60,
        id="tarea_periodica_1"
    )
    planificador.start()
    print("Planificador APScheduler iniciado.")

@app.on_event("shutdown")
async def detener_planificador():
    planificador.shutdown()
    print("Planificador APScheduler detenido.")

Etiquetas: FastAPI APScheduler Python SQLite tareas-programadas

Publicado el 7-8 08:24