Files
fastapi-best-architecture/backend/app/task/database.py
T
Wu Clan 646a0ec2fc Add granian and celery metrics collection (#1057)
* Add granian and celery metrics collection

* Update dashboards

* Add asyncio instrument

* Fix opera log finally
2026-02-03 20:44:05 +08:00

177 lines
7.0 KiB
Python

from celery import states
from celery.backends.base import BaseBackend
from celery.backends.database import retry, session_cleanup
from celery.exceptions import ImproperlyConfigured
from celery.utils.time import maybe_timedelta
from sqlalchemy import PickleType
from sqlalchemy.orm import Session
from backend.app.task.model.result import Task, TaskExtended, TaskSet
from backend.app.task.session import SessionManager
class DatabaseBackend(BaseBackend):
"""
重写 celery.backends.database DatabaseBackend,此类实现与模型配合不佳,导致 fba 创建表和 alembic 迁移困难
"""
# ResultSet.iterate should sleep this much between each pool,
# to not bombard the database with queries.
subpolling_interval = 0.5
task_cls = Task
taskset_cls = TaskSet
def __init__(self, dburi=None, engine_options=None, url=None, **kwargs) -> None: # noqa: ANN001
# The `url` argument was added later and is used by
# the app to set backend by url (celery.app.backends.by_url)
super().__init__(expires_type=maybe_timedelta, url=url, **kwargs)
conf = self.app.conf
if self.extended_result:
self.task_cls = TaskExtended
self.url = url or dburi or conf.database_url
self.engine_options = dict(engine_options or {}, **conf.database_engine_options or {})
self.short_lived_sessions = kwargs.get('short_lived_sessions', conf.database_short_lived_sessions)
schemas = conf.database_table_schemas or {}
tablenames = conf.database_table_names or {}
self.task_cls.configure(schema=schemas.get('task'), name=tablenames.get('task'))
self.taskset_cls.configure(schema=schemas.get('group'), name=tablenames.get('group'))
if not self.url:
raise ImproperlyConfigured(
'Missing connection string! Do you have the database_url setting set to a real value?',
)
self.session_manager = SessionManager()
create_tables_at_setup = conf.database_create_tables_at_setup
if create_tables_at_setup is True:
self._create_tables()
@property
def extended_result(self): # noqa: ANN201
return self.app.conf.find_value_for_key('extended', 'result')
def _create_tables(self) -> None:
"""Create the task and taskset tables."""
self.result_session()
def result_session(self, session_manager=None) -> Session: # noqa: ANN001
if session_manager is None:
session_manager = self.session_manager
return session_manager.session_factory(
dburi=self.url,
short_lived_sessions=self.short_lived_sessions,
**self.engine_options,
)
@retry
def _store_result(self, task_id, result, state, traceback=None, request=None, **kwargs) -> None: # noqa: ANN001
"""Store return value and state of an executed task."""
session = self.result_session()
with session_cleanup(session):
task = list(session.query(self.task_cls).filter(self.task_cls.task_id == task_id))
task = task and task[0]
if not task:
task = self.task_cls(task_id)
task.task_id = task_id
session.add(task)
session.flush()
self._update_result(task, result, state, traceback=traceback, request=request)
session.commit()
def _update_result(self, task, result, state, traceback=None, request=None) -> None: # noqa: ANN001
meta = self._get_result_meta(
result=result,
state=state,
traceback=traceback,
request=request,
format_date=False,
encode=True,
)
# Exclude the primary key id and task_id columns
# as we should not set it None
columns = [column.name for column in self.task_cls.__table__.columns if column.name not in {'id', 'task_id'}]
# Iterate through the columns name of the table
# to set the value from meta.
# If the value is not present in meta, set None
for column in columns:
value = meta.get(column)
setattr(task, column, value)
@retry
def _get_task_meta_for(self, task_id: str): # noqa: ANN202
"""Get task meta-data for a task by id."""
session = self.result_session()
with session_cleanup(session):
task = list(session.query(self.task_cls).filter(self.task_cls.task_id == task_id))
task = task and task[0]
if not task:
task = self.task_cls(task_id)
task.status = states.PENDING
task.result = None
data = task.to_dict()
if data.get('args', None) is not None:
data['args'] = self.decode(data['args'])
if data.get('kwargs', None) is not None:
data['kwargs'] = self.decode(data['kwargs'])
return self.meta_from_decoded(data)
@retry
def _save_group(self, group_id: str, result: PickleType): # noqa: ANN202
"""Store the result of an executed group."""
session = self.result_session()
with session_cleanup(session):
group = self.taskset_cls(group_id, result)
session.add(group)
session.flush()
session.commit()
return result
@retry
def _restore_group(self, group_id: str) -> dict | None:
"""Get meta-data for group by id."""
session = self.result_session()
with session_cleanup(session):
group = session.query(self.taskset_cls).filter(self.taskset_cls.taskset_id == group_id).first()
if group:
return group.to_dict()
@retry
def _delete_group(self, group_id: str) -> None:
"""Delete meta-data for group by id."""
session = self.result_session()
with session_cleanup(session):
session.query(self.taskset_cls).filter(self.taskset_cls.taskset_id == group_id).delete()
session.flush()
session.commit()
@retry
def _forget(self, task_id: str) -> None:
"""Forget about result."""
session = self.result_session()
with session_cleanup(session):
session.query(self.task_cls).filter(self.task_cls.task_id == task_id).delete()
session.commit()
def cleanup(self) -> None:
"""Delete expired meta-data."""
session = self.result_session()
expires = self.expires
now = self.app.now()
with session_cleanup(session):
session.query(self.task_cls).filter(self.task_cls.date_done < (now - expires)).delete()
session.query(self.taskset_cls).filter(self.taskset_cls.date_done < (now - expires)).delete()
session.commit()
def __reduce__(self, args=(), kwargs=None): # noqa: ANN001, ANN204
kwargs = kwargs or {}
kwargs.update({'dburi': self.url, 'expires': self.expires, 'engine_options': self.engine_options})
return super().__reduce__(args, kwargs)