| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 | # tasks.pyimport asynciofrom typing import Dictfrom uuid import uuid4import jsonimport loggingfrom redis.asyncio import Redisfrom fastapi import Requestfrom typing import Dict, List, Optionalfrom open_webui.env import SRC_LOG_LEVELS, REDIS_KEY_PREFIXlog = logging.getLogger(__name__)log.setLevel(SRC_LOG_LEVELS["MAIN"])# A dictionary to keep track of active taskstasks: Dict[str, asyncio.Task] = {}item_tasks = {}REDIS_TASKS_KEY = f"{REDIS_KEY_PREFIX}:tasks"REDIS_ITEM_TASKS_KEY = f"{REDIS_KEY_PREFIX}:tasks:item"REDIS_PUBSUB_CHANNEL = f"{REDIS_KEY_PREFIX}:tasks:commands"async def redis_task_command_listener(app):    redis: Redis = app.state.redis    pubsub = redis.pubsub()    await pubsub.subscribe(REDIS_PUBSUB_CHANNEL)    async for message in pubsub.listen():        if message["type"] != "message":            continue        try:            command = json.loads(message["data"])            if command.get("action") == "stop":                task_id = command.get("task_id")                local_task = tasks.get(task_id)                if local_task:                    local_task.cancel()        except Exception as e:            log.exception(f"Error handling distributed task command: {e}")### ------------------------------### REDIS-ENABLED HANDLERS### ------------------------------async def redis_save_task(redis: Redis, task_id: str, item_id: Optional[str]):    pipe = redis.pipeline()    pipe.hset(REDIS_TASKS_KEY, task_id, item_id or "")    if item_id:        pipe.sadd(f"{REDIS_ITEM_TASKS_KEY}:{item_id}", task_id)    await pipe.execute()async def redis_cleanup_task(redis: Redis, task_id: str, item_id: Optional[str]):    pipe = redis.pipeline()    pipe.hdel(REDIS_TASKS_KEY, task_id)    if item_id:        pipe.srem(f"{REDIS_ITEM_TASKS_KEY}:{item_id}", task_id)        if (await pipe.scard(f"{REDIS_ITEM_TASKS_KEY}:{item_id}").execute())[-1] == 0:            pipe.delete(f"{REDIS_ITEM_TASKS_KEY}:{item_id}")  # Remove if empty set    await pipe.execute()async def redis_list_tasks(redis: Redis) -> List[str]:    return list(await redis.hkeys(REDIS_TASKS_KEY))async def redis_list_item_tasks(redis: Redis, item_id: str) -> List[str]:    return list(await redis.smembers(f"{REDIS_ITEM_TASKS_KEY}:{item_id}"))async def redis_send_command(redis: Redis, command: dict):    await redis.publish(REDIS_PUBSUB_CHANNEL, json.dumps(command))async def cleanup_task(redis, task_id: str, id=None):    """    Remove a completed or canceled task from the global `tasks` dictionary.    """    if redis:        await redis_cleanup_task(redis, task_id, id)    tasks.pop(task_id, None)  # Remove the task if it exists    # If an ID is provided, remove the task from the item_tasks dictionary    if id and task_id in item_tasks.get(id, []):        item_tasks[id].remove(task_id)        if not item_tasks[id]:  # If no tasks left for this ID, remove the entry            item_tasks.pop(id, None)async def create_task(redis, coroutine, id=None):    """    Create a new asyncio task and add it to the global task dictionary.    """    task_id = str(uuid4())  # Generate a unique ID for the task    task = asyncio.create_task(coroutine)  # Create the task    # Add a done callback for cleanup    task.add_done_callback(        lambda t: asyncio.create_task(cleanup_task(redis, task_id, id))    )    tasks[task_id] = task    # If an ID is provided, associate the task with that ID    if item_tasks.get(id):        item_tasks[id].append(task_id)    else:        item_tasks[id] = [task_id]    if redis:        await redis_save_task(redis, task_id, id)    return task_id, taskasync def list_tasks(redis):    """    List all currently active task IDs.    """    if redis:        return await redis_list_tasks(redis)    return list(tasks.keys())async def list_task_ids_by_item_id(redis, id):    """    List all tasks associated with a specific ID.    """    if redis:        return await redis_list_item_tasks(redis, id)    return item_tasks.get(id, [])async def stop_task(redis, task_id: str):    """    Cancel a running task and remove it from the global task list.    """    if redis:        # PUBSUB: All instances check if they have this task, and stop if so.        await redis_send_command(            redis,            {                "action": "stop",                "task_id": task_id,            },        )        # Optionally check if task_id still in Redis a few moments later for feedback?        return {"status": True, "message": f"Stop signal sent for {task_id}"}    task = tasks.pop(task_id, None)    if not task:        return {"status": False, "message": f"Task with ID {task_id} not found."}    task.cancel()  # Request task cancellation    try:        await task  # Wait for the task to handle the cancellation    except asyncio.CancelledError:        # Task successfully canceled        return {"status": True, "message": f"Task {task_id} successfully stopped."}    if task.cancelled() or task.done():        return {"status": True, "message": f"Task {task_id} successfully cancelled."}    return {"status": True, "message": f"Cancellation requested for {task_id}."}async def stop_item_tasks(redis: Redis, item_id: str):    """    Stop all tasks associated with a specific item ID.    """    task_ids = await list_task_ids_by_item_id(redis, item_id)    if not task_ids:        return {"status": True, "message": f"No tasks found for item {item_id}."}    for task_id in task_ids:        result = await stop_task(redis, task_id)        if not result["status"]:            return result  # Return the first failure    return {"status": True, "message": f"All tasks for item {item_id} stopped."}
 |