queue.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. """
  2. engine.queue — Job Queue abstraction (memory / Redis).
  3. Dev: asyncio.PriorityQueue (in-memory)
  4. Prod: Redis Sorted Set (ZADD score=priority*1e12+timestamp)
  5. """
  6. from __future__ import annotations
  7. import asyncio
  8. import json
  9. import time
  10. from typing import Any, Dict, Optional
  11. from dataclasses import dataclass, field
  12. from agentpaas.db.models import gen_id, now_utc
  13. @dataclass
  14. class Job:
  15. id: str = ""
  16. tenant_id: str = ""
  17. agent_id: str = ""
  18. agent_version: int = 1
  19. config: Dict[str, Any] = field(default_factory=dict)
  20. input: str = ""
  21. priority: int = 5
  22. status: str = "queued"
  23. worker_id: str = ""
  24. attempts: int = 0
  25. max_attempts: int = 3
  26. result: str = ""
  27. error: str = ""
  28. created_at: str = ""
  29. def __post_init__(self):
  30. if not self.id:
  31. self.id = gen_id("job_")
  32. if not self.created_at:
  33. self.created_at = now_utc()
  34. class MemoryQueue:
  35. """In-memory priority queue for development."""
  36. def __init__(self):
  37. self._queue: asyncio.PriorityQueue = asyncio.PriorityQueue()
  38. self._jobs: Dict[str, Job] = {}
  39. async def enqueue(self, job: Job) -> str:
  40. self._jobs[job.id] = job
  41. score = job.priority * 1_000_000_000_000 + time.time()
  42. await self._queue.put((score, job.id))
  43. return job.id
  44. async def dequeue(self, timeout: float = 30.0) -> Optional[Job]:
  45. try:
  46. _, job_id = await asyncio.wait_for(self._queue.get(), timeout=timeout)
  47. job = self._jobs.get(job_id)
  48. if job:
  49. job.status = "running"
  50. return job
  51. except asyncio.TimeoutError:
  52. return None
  53. def get_job(self, job_id: str) -> Optional[Job]:
  54. return self._jobs.get(job_id)
  55. def update_job(self, job_id: str, **kwargs):
  56. job = self._jobs.get(job_id)
  57. if job:
  58. for k, v in kwargs.items():
  59. setattr(job, k, v)
  60. @property
  61. def depth(self) -> int:
  62. return self._queue.qsize()
  63. class RedisQueue:
  64. """Redis-backed priority queue for production."""
  65. def __init__(self, redis_url: str, prefix: str = "agentpaas:queue:"):
  66. import redis
  67. self.r = redis.from_url(redis_url)
  68. self.prefix = prefix
  69. self.queue_key = f"{prefix}jobs"
  70. self.data_key = f"{prefix}data:"
  71. async def enqueue(self, job: Job) -> str:
  72. data = json.dumps({
  73. "id": job.id, "tenant_id": job.tenant_id, "agent_id": job.agent_id,
  74. "agent_version": job.agent_version, "config": job.config,
  75. "input": job.input, "priority": job.priority, "status": "queued",
  76. "max_attempts": job.max_attempts, "created_at": job.created_at,
  77. })
  78. score = job.priority * 1_000_000_000_000 + time.time()
  79. self.r.zadd(self.queue_key, {job.id: score})
  80. self.r.set(f"{self.data_key}{job.id}", data, ex=86400)
  81. return job.id
  82. async def dequeue(self, timeout: float = 30.0) -> Optional[Job]:
  83. result = self.r.zpopmin(self.queue_key, count=1)
  84. if not result:
  85. return None
  86. job_id = result[0][0].decode() if isinstance(result[0][0], bytes) else result[0][0]
  87. data = self.r.get(f"{self.data_key}{job_id}")
  88. if not data:
  89. return None
  90. d = json.loads(data)
  91. job = Job(**{k: v for k, v in d.items() if k in Job.__dataclass_fields__})
  92. job.status = "running"
  93. self.r.set(f"{self.data_key}{job_id}", json.dumps({**d, "status": "running"}), ex=86400)
  94. return job
  95. def get_job(self, job_id: str) -> Optional[Job]:
  96. data = self.r.get(f"{self.data_key}{job_id}")
  97. if not data:
  98. return None
  99. d = json.loads(data)
  100. return Job(**{k: v for k, v in d.items() if k in Job.__dataclass_fields__})
  101. def update_job(self, job_id: str, **kwargs):
  102. data = self.r.get(f"{self.data_key}{job_id}")
  103. if data:
  104. d = json.loads(data)
  105. d.update(kwargs)
  106. self.r.set(f"{self.data_key}{job_id}", json.dumps(d), ex=86400)
  107. @property
  108. def depth(self) -> int:
  109. return self.r.zcard(self.queue_key)
  110. def create_queue(redis_url: Optional[str] = None):
  111. """Factory: Redis if URL provided, else in-memory."""
  112. if redis_url:
  113. try:
  114. return RedisQueue(redis_url)
  115. except Exception:
  116. pass
  117. return MemoryQueue()