news 2026/9/10 1:53:41

深入解析 Sentry 异步删除子系统:从 ScheduledDeletion 调度到级联删除任务

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入解析 Sentry 异步删除子系统:从 ScheduledDeletion 调度到级联删除任务

深入解析 Sentry 异步删除子系统:从 ScheduledDeletion 调度到级联删除任务

【免费下载链接】sentryDeveloper-first error tracking and performance monitoring项目地址: https://gitcode.com/GitHub_Trending/sen/sentry

导读

当你在 Sentry 中删除一个组织、项目或 Issue(Group)时,涉及的数据并非只删一行那么简单——它们可能横跨数十张 PostgreSQL 关联表、事件存储(Eventstore/Nodestore)甚至外部服务。Sentry 在 src/sentry/deletions/README.md 中定义了一套完整的异步删除子系统(deletion subsystem):它通过 PostgreSQL 记录删除任务与状态,在后台按计划批量执行,并支持级联删除、失败重试与删除取消。读完本文,你将掌握该子系统的整体工作流、调度与重试机制、两大基础删除任务的区别、如何为新增模型接入自定义删除逻辑,以及如何直接通过 deletions 管理器同步执行删除。

子系统概览:为什么需要"删除子系统"

在 Sentry 的数据模型中,Organization → Project → Group(Issue)→ Event 层层嵌套,且大量业务模型(AlertRule、Rule、Monitor、SentryApp、Release 等)都挂靠在组织或项目之下。当应用新增一个模型时,就必须考虑:这条记录所属的组织或项目被删除后,它应当如何被清理?

删除子系统主要解决三个工程问题:

  1. 异步化与批量化:一次性同步删除海量关联行会长时间占用数据库连接并拖垮请求,因此删除被拆成可恢复的异步任务,逐 chunk 推进。
  2. 可重试与可靠性:删除任务可能因一次发布(deploy)被打断,或因新增外键关系、数据库故障而失败。子系统用 PostgreSQL 表跟踪每次删除的状态(是否 in_progress、计划时间等),从而支持失败后重新拾取。
  3. 级联策略可定制:不同模型的删除行为差异很大——有的需要逐条触发 Django signal(如依赖子关系清理的模型),有的可以单条 SQL 批量删除(无关联的叶子模型),有的还需要联动事件存储与外部分组服务(Seer)。

调度执行核心:Taskbroker 定时任务与重试机制

README 指出两个关键调度入口:

  • run_scheduled_deletions()每 15 分钟执行一次,它会查询所有"计划时间已到、且当前未被处理(不在 in_progress)"的删除任务,并为每一条任务派生(spawn)对应的删除处理任务。
  • reattempt_deletions()每天执行一次,用来清理陈旧任务:它会清掉那些"卡住"的旧任务的in_progress标记,让它们能被下一次 15 分钟调度重新拾取。

对照当前仓库,这一节早已演化成Control / Cell 双轨实现。在 src/sentry/deletions/tasks/scheduled.py 中,实际存在一组配对任务:

  • run_scheduled_deletions_control(处理 Control 侧ScheduledDeletion)与run_scheduled_deletions(处理 Cell 侧CellScheduledDeletion);
  • reattempt_deletions_controlreattempt_deletions同理,二者共用_reattempt_deletions()

其中 scheduled.py 中_reattempt_deletions的实现细节是:只重置in_progress=Truedate_scheduled早于当前时间 6 小时以上的任务——即"若删除进行中且计划时间已过去 6 小时,可认为上次任务已经死亡/失败",随后把in_progress翻转为False,使任务在下一个周期被重新拾取。

def _reattempt_deletions(model_class: type[BaseScheduledDeletion]) -> None: queryset = model_class.objects.filter( in_progress=True, date_scheduled__lte=timezone.now() - timedelta(hours=6) ) queryset.update(in_progress=False)

而调度执行端_run_scheduled_deletions()(scheduled.py)则通过原子的条件更新来抢占任务,避免同一删除被多个 worker 重复执行:

queryset = model_class.objects.filter(in_progress=False, date_scheduled__lte=timezone.now()) for item in queryset: with transaction.atomic(router.db_for_write(model_class)): affected = model_class.objects.filter( id=item.id, in_progress=False, ).update(in_progress=True) if not affected: continue process_task.delay(deletion_id=item.id)

即:只有成功把in_progressFalse更新为True的进程才负责执行该删除,这正对应 README 中"查询过去到期、且未在进行中的任务"的描述。

实际的删除处理器run_deletion/run_deletion_control(scheduled.py)还配置了:

  • 处理时限processing_deadline_duration分别为 15 分钟(control)与 20 分钟(cell);
  • 重试策略:最多重试 5 次(MAX_RETRIES = 5),每次间隔 5 分钟,超过次数后丢弃任务;对DeleteAborted(删除被取消)异常不重试且静默处理。

任务主流程_run_deletion()的逻辑是:取出ScheduledDeletion→ 通过get_instance()还原真实对象 → 用 deletions 管理器拿到对应删除任务 → 调用task.should_proceed(instance)校验(不通过则直接删除调度记录并终止,这就是"可取消删除"的落地)→ 首轮发送pending_deletesignal → 反复调用task.chunk(),只要返回True就继续投递下一轮子任务,直到chunk()返回False(全部删完)才删除调度记录本身。

调度删除:ScheduledDeletion 模型

对绝大多数应用代码而言,进入删除子系统的入口是ScheduledDeletion模型——通过它创建一条"未来某个时间点执行"的删除任务。README 给出了最核心的用法:

from sentry.deletions.models.scheduleddeletion import ScheduledDeletion ScheduledDeletion.schedule(organization, days=1, hours=2)

上面这行代码会把该 organization 调度为在1 天零 2 小时后被删除。从源码看,schedule() 是BaseScheduledDeletion上的类方法,其完整签名是:

@classmethod def schedule( cls, instance: Model, days: int = 30, hours: int = 0, data: Any = None, actor: Any = None ) -> Self:

关键行为说明:

  • days默认30hours默认0,即默认 30 天后执行;README 示例显式传参即可自定义更短的窗口。
  • 使用update_or_create(app_label, model_name, object_id)为唯一键,重复调度同一对象只会更新其计划时间,而不会产生重复任务。
  • actor会被记录到actor_id字段(用于删除审计);data存入 JSONField,可携带自定义上下文。
  • 该方法会校验模型的silo_limit:若当前 Silo 模式下无法操作该模型,则直接抛出SiloLimit.AvailabilityError,防止在错误的 silo 中调度删除。

模型字段层面(scheduleddeletion.py),一次删除任务记录包含:

字段含义
guid32 位 UUID hex,删除任务的唯一交易号(transaction_id)
app_label/model_name被删除模型的 Django 定位信息
object_id被删除记录主键
date_added创建时间
date_scheduled计划执行时间(默认now() + 30 days
actor_id发起删除的用户(可空)
dataJSON 扩展字段
in_progress是否正在处理中(调度与重试机制的核心开关)

此外,当前仓库因多区域(silo)架构将这一模型拆成了两张物理表(scheduleddeletion.py):Control 侧的ScheduledDeletion(表名sentry_scheduleddeletion,由control_silo_model装饰)与 Cell 侧的CellScheduledDeletion(表名sentry_regionscheduleddeletion,由cell_silo_model装饰)。README 写作时以单一模型为例,接入时需按被删除模型所在 silo 选择对应的类——可以推断:monolith 模式下二者都会照常被处理。get_model()内部还会经过 RELOCATED_MODELS 映射,把历史上"模型应用已迁移"的旧任务(例如sentry.Monitormonitors.Monitor)翻译到新的 app_label,保证旧调度任务仍能被正确还原与执行。

删除任务:两种内置基础策略

README 指出删除系统提供两个基类来覆盖常见场景:

  • ModelDeletionTask:逐条获取记录并分别删除每个实例。
    • 适合依赖 Django signals、或存在子关联的模型(例如删除一条Group时逐条触发post_delete让下游联动)。
    • 当某个模型没有显式注册删除任务时,它就是默认实现
  • BulkModelDeletionTask:用单条查询批量删除记录。
    • 适合没有任何关联关系的"叶子"模型(例如GroupAssigneeProjectKeyEnvironmentProject等中间表),效率最高。

对照源码 base.py 可以看到二者在设计上的具体差异:

默认 chunk 大小不同。BaseDeletionTask.DEFAULT_CHUNK_SIZE = 100(base.py),而BulkModelDeletionTask将其重写为10000(base.py)——批删模型单轮可处理万行,逐条删除模型则以 100 为粒度精细推进。

chunk() 的行为不同。ModelDeletionTask.chunk()(base.py)在while循环中反复拉取query命中的记录并调用delete_bulk(),直到耗尽chunk_size配额:一旦某轮查不到更多行就返回False(已删完),否则返回True("还有更多工作",需要调度器再次投递)。BulkModelDeletionTask.chunk()(base.py)则直接调用bulk_delete_objects()(在unguarded_write与写库路由保护下执行原始批量 DELETE),同样以返回值指示是否还有剩余行。

delete_bulk() 的级联编排。BaseDeletionTask.delete_bulk()(base.py)先根据mark_in_progress决定是否把实例状态置为DELETION_IN_PROGRESS,再分别从get_child_relations_bulk()(批量视角)与每个实例的get_child_relations()(单实例视角)收集子关系,用_delete_children()递归调用对应子任务的chunk(),全部子关系清理完后再删除自身。

内置限速。ModelDeletionTask.chunk()中每轮都会调用_throttle_deletes()(base.py):若任务配置了rate_limit_option(指向一个整型 option 名称),系统会基于漏桶限速器LeakyBucketRateLimiter,drip_rate 取 option 值、burst 取 max(rate, query_limit))按删除行数节流吞吐,避免海量删除压垮数据库。

为新增模型接入删除任务(自定义扩展指南)

当你的模型存在需要额外清理的子关联、或需要覆盖默认删除行为时,README 要求按以下两步注册自定义删除任务:

  1. 将删除任务子类加入sentry.deletions.defaults
  2. sentry.deletions.__init__的默认管理器映射中注册该任务

当前仓库中,defaults目录(src/sentry/deletions/defaults/)已包含 40+ 个模型的删除任务定义(如organization.pyproject.pygroup.pyalertrule.pymonitor.pyrule.pyrelease.py等),并通过 defaults/init.py 统一导出。

而映射注册位于 src/sentry/deletions/init.py 的load_defaults():它调用manager.register(Model, TaskClass)把模型绑定到具体任务,例如:

manager.register(models.Group, defaults.GroupDeletionTask) manager.register(models.Organization, defaults.OrganizationDeletionTask) manager.register(models.Project, defaults.ProjectDeletionTask) manager.register(models.Activity, BulkModelDeletionTask)

模块级还暴露了与 manager 一一对应的便捷函数(init.py):get()register()exec_sync()exec_sync_many()。默认管理器由 get_manager() 以DeletionTaskManager(default_task=ModelDeletionTask)构建,并用functools.cache缓存——任何未显式注册的模型都会回退到ModelDeletionTask默认实现,这正是 README 所说"未指定删除任务时的默认策略"。

manager.py 中get()的解析逻辑也印证了这一点:self.tasks.get(model, self.default_task)——先查精确注册表,未命中则落到default_task

实现子类时通常需要覆写的钩子

在 base.py 中,BaseDeletionTask预留了清晰的扩展点:

  • chunk():核心推进逻辑(一般继承ModelDeletionTask即可,无需重写);
  • should_proceed(instance):根任务在执行前调用,用于支持删除被取消(详见下节);
  • get_child_relations(instance)/get_child_relations_bulk(instance_list):返回该实例的子关联列表(元素为ModelRelation(model, query, task)或裸BaseRelation),是级联删除的"配方"来源;
  • filter_relations():配合构造参数skip_models剔除不需要处理的子模型;
  • mark_deletion_in_progress():默认把带status字段的实例批量更新为ObjectStatus.DELETION_IN_PROGRESS
  • 构造参数queryorder_byquery_limitchunk_sizeactor_idtransaction_id等用于定制单个任务的查询范围与执行方式。

取消删除:should_proceed 钩子

如果某个记录已被调度删除、但希望之后能够取消,README 的指导是:让删除任务实现should_proceed钩子。典型实现为:

def should_proceed(self, instance: ModelT) -> bool: return instance.status in { ObjectStatus.PENDING_DELETION, ObjectStatus.DELETION_IN_PROGRESS }

含义是:只有当记录当前状态仍属于"待删除/删除中"时才继续执行删除。仓库中最直接的落地例子是 defaults/organization.py 的OrganizationDeletionTask.should_proceed()——它只删除那些没有被撤销删除(undeleted)的组织:

class OrganizationDeletionTask(ModelDeletionTask[Organization]): def should_proceed(self, instance: Organization) -> bool: return instance.status in { OrganizationStatus.PENDING_DELETION, OrganizationStatus.DELETION_IN_PROGRESS, }

其配套支持来自模型侧的 cancel():它会以(model_name, object_id)查找一条in_progress=False的调度记录并删除它。因此整个"可取消"闭环是:外部 API 先改变记录状态(例如恢复为正常状态)→ 定时任务到期执行should_proceed()发现状态不符 →调度记录被移除,删除不会发生。README 特别强调:当删除被该钩子取消时,对应的ScheduledDeletion行会被删除。

直接使用 Deletions 管理器(同步删除)

多数情况下删除走异步调度,但子系统同样支持在代码里同步驱动删除任务。README 以"删除一个 organization"为例给出了如下写法:

from sentry import deletions task = deletions.get(model=Organization, query={}) work = True while work: work = task.chunk()

要点拆解:

  • deletions.get(model=..., query=...)会通过默认 manager 解析出该模型对应的删除任务并实例化(query为定位待删记录的过滤条件;exec_sync/exec_sync_many会把它封装成query={"id": instance.id}/{"id__in": [...]}的完整循环,见 manager.py);
  • 循环调用task.chunk(),每次执行一个 chunk 的数据清理;返回值True表示仍有剩余工作,False表示实体已彻底移除,据此决定是否继续循环。

需要说明的是,query={}这种"全表/全量匹配"的写法在示例中意在展示接口形态;实际生产路径(如run_deletion)总是携带query={"id": deletion.object_id}精确定位单条记录(见 scheduled.py)。

级联行为取决于"对象类型"

README 强调系统针对 Organization 有默认实现,能够高效地级联删除——该行为会因输入对象不同而变化,因为任务可以为自己的子级覆写行为。两个典型对照:

删除 Group(Issue):传统逐级批删。GroupDeletionTask(defaults/group.py)以GROUP_CHUNK_SIZE = 100的粒度"准批量"推进:它会一次性为列表中的所有 group 组装子关系(涵盖GroupHashGroupAssigneeActivityUserReportEventAttachmentDIRECT_GROUP_RELATED_MODELS+ADDITIONAL_GROUP_RELATED_MODELS中声明的模型),并按 Error / IssuePlatform 分类分别投递ErrorEventsDeletionTaskIssuePlatformEventsDeletionTask去清理事件存储中的 Event 数据。这符合 README"批处理每个子级(如 Event)"的描述——因为每一条 group 都有独立的事件负载需要异步清掉。

删除 Project:跳过 Group 任务、直接批量清理间接后代。而当删除一个 Project 时,并不会把事件交给已注册的Group任务逐组处理,而是采取更高效的路径:直接批量删除它的间接后代(如 Event)。从源码看,ProjectDeletionTask.get_child_relations()(defaults/project.py)把 20 余种子关联分门别类地列出来:像ProjectKeyGroupAssigneeEnvironmentProject这类模型直接挂BulkModelDeletionTask一次批量删掉;GroupRuleMonitorActivity等则走常规逐级任务。这样设计的原因是:既然整个 project 都要消失,就无需再为每个 Group 单独调度事件删除与级联——用更少的查询把其所有后代行成批清掉即可。

设计要点小结与扩展阅读

  • 可靠性优先:所有删除任务落库为ScheduledDeletion/CellScheduledDeletion记录,配合 15 分钟调度与每日重试清理,天然容忍发布中断与单次任务失败。
  • 按数据形态选择策略:无关联的叶子数据用BulkModelDeletionTask(默认 chunk 10000、单查询批删);依赖 signal/子关系的用ModelDeletionTask(默认 chunk 100);两者都以chunk()返回值作为"是否还有工作"的续跑信号。
  • 删除可取消:通过should_proceed()+ 对象状态(如PENDING_DELETION/DELETION_IN_PROGRESS)实现;取消时调度行会被清理,见ScheduledDeletion.cancel()
  • 模型扩展有纪律:新增模型接入只需两步(defaults 子类 + manager 注册);group 相关模型甚至由测试强制约束——defaults/group.py 的模块注释明确提醒:任何带group_id外键的新模型必须登记到_GROUP_RELATED_MODELS列表中,否则 tests/sentry/deletions/test_validate_group_related_models.py 一类的校验测试会失败。

需要继续深入时,建议按以下路径阅读仓库源码:

  • 整体说明文档:src/sentry/deletions/README.md
  • 任务基类与限速实现:src/sentry/deletions/base.py
  • 调度管理器与默认注册表:src/sentry/deletions/manager.py、src/sentry/deletions/init.py
  • 调度模型与定时任务:src/sentry/deletions/models/scheduleddeletion.py、src/sentry/deletions/tasks/scheduled.py
  • 三份代表性级联"配方":defaults/organization.py、defaults/project.py、defaults/group.py

【免费下载链接】sentryDeveloper-first error tracking and performance monitoring项目地址: https://gitcode.com/GitHub_Trending/sen/sentry

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 1:53:31

基于SpringBoot+Vue的充电桩管理系统设计与实现指南

简介:SpringBootVue新能源汽车充电桩管理系统是一份完整的Java毕业设计资源,面向计算机相关专业毕业生及SpringBoot、Vue开发者。系统基于SpringBoot框架与B/S模式,使用MySQL数据库及Tomcat服务器,涵盖首页、个人中心、维修员管理…

作者头像 李华
网站建设 2026/9/10 1:51:16

GB/T 11981-2024轻钢龙骨检测要点详解:取样、静载试验与镀锌层

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 1:50:39

CANN/ge模型描述创建函数

aclmdlCreateDesc 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、TensorFl…

作者头像 李华
网站建设 2026/9/10 1:50:32

CANN/ge HCCL子通信域配置参数

--hccl_sub_comm_config 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、Te…

作者头像 李华