delayed_job:每个租户一次一份工作?
delayed_job: One job per tenant at a time?
我有一个多租户-Rails 应用程序,有多个 delayed_job 工作人员。
为了避免重叠租户特定的工作,我想将工人彼此分开,这样每个人一次只处理一项租户特定的任务。
我考虑过使用(命名的)queue 列并添加 "tenant_1"、"tenant_2" 等。不幸的是,队列必须在配置过程中命名,所以这个原则对于许多租户来说不够灵活。
有没有办法自定义 delayed_job 选择下一个任务的方式?还有其他定义范围的方法吗?
您最好的选择可能是旋转一个实现分布式锁的自定义解决方案 - 本质上,所有工作人员都 运行 正常并从通常的队列中拉出,但在执行工作检查之前与另一个系统(Redis, RDBMS,API,随便什么)来验证没有其他工人正在为该租户执行工作。如果该租户没有工作,则为有问题的租户设置锁并开始工作。如果租户被锁定,请不要执行该工作。这是您对许多实施细节的要求,例如是否继续尝试另一项工作,将工作重新排到队列的后面,是否将其视为失败并将其绑定到您的重试限制,或者做其他事情完全。这是相当开放的,所以我会把细节留给你,但这里有一些提示:
- 继承将是你的朋友;在基础工作上定义此行为,并在您希望员工从事的工作上继承它 运行。这也允许您自定义行为,如果您有 "special" 某些工作的案例,并且不会破坏其他一切。
- 假设您没有 运行通过 ActiveJob(因为没有提到),请阅读
delayed_job
钩子:https://github.com/collectiveidea/delayed_job/#hooks - 它们可能是合适的 and/or 有用的工具
- 熟悉悲观和乐观锁定策略的一些差异和权衡 - 这个答案是一个很好的起点:Optimistic vs. Pessimistic locking
- 阅读有关分布式锁概念的一般实践,以便您可以为自己选择最好的工具和策略(它不一定是一个疯狂复杂的解决方案,一个简单的 table 在数据库中存储租户标识符就足够了,但您需要考虑失败情况 - 例如,如何管理被放弃的锁)
认真考虑不这样做;系统正常运行真的有严格要求吗?如果是这样,这可能表明您的数据模型存在潜在缺陷,或者您如何围绕该数据构建转换。在考虑对数据的操作时,在您的应用程序中争取 ACIDity,您可以避免很多此类问题。它不是后台作业 运行 人员常用的 "out of the box" 功能是有原因的。如果存在潜在的缺陷,它不仅会在这个问题上困扰您,还会在其他问题上困扰您 - 保证!
如果您试图避免两个不同的工作人员在同一个租户上工作,那么这是一个糟糕的设计选择。有东西在闻。先解决这个问题。但是,如果您希望相同类型的工作实例在不同的租户上工作,下面是最简单的解决方案。这些关系是我的假设。
ExpiredOrderCleaner = Struct.new(:tenant_id) do
def perform
Order.where(tenant_id: tenant_id).expired.delete_all
end
end
Tenant.each do |tenant|
Delayed::Job.enqueue ExpiredOrderCleaner.new(tenant.id)
end
这将为每个租户创造独特的就业机会。单个工作实例将在特定租户上工作。但是,可以有其他类型的工作在同一个租户上工作。这应该是好的。如果您需要更小的范围,只需为工作人员传递更多参数并在查询中使用并使用数据库事务来避免冲突。
这些 best practices 适用于任何后台工作人员。
- 让你的工作幂等和事务 意味着你的工作可以安全地执行多次
- 拥抱并发设计你的工作,这样你就可以运行许多并行
如果您使用 apartment gem and active job 包装器,您的工作会容易得多。从那里的文档中查看示例。
我有一个多租户-Rails 应用程序,有多个 delayed_job 工作人员。
为了避免重叠租户特定的工作,我想将工人彼此分开,这样每个人一次只处理一项租户特定的任务。
我考虑过使用(命名的)queue 列并添加 "tenant_1"、"tenant_2" 等。不幸的是,队列必须在配置过程中命名,所以这个原则对于许多租户来说不够灵活。
有没有办法自定义 delayed_job 选择下一个任务的方式?还有其他定义范围的方法吗?
您最好的选择可能是旋转一个实现分布式锁的自定义解决方案 - 本质上,所有工作人员都 运行 正常并从通常的队列中拉出,但在执行工作检查之前与另一个系统(Redis, RDBMS,API,随便什么)来验证没有其他工人正在为该租户执行工作。如果该租户没有工作,则为有问题的租户设置锁并开始工作。如果租户被锁定,请不要执行该工作。这是您对许多实施细节的要求,例如是否继续尝试另一项工作,将工作重新排到队列的后面,是否将其视为失败并将其绑定到您的重试限制,或者做其他事情完全。这是相当开放的,所以我会把细节留给你,但这里有一些提示:
- 继承将是你的朋友;在基础工作上定义此行为,并在您希望员工从事的工作上继承它 运行。这也允许您自定义行为,如果您有 "special" 某些工作的案例,并且不会破坏其他一切。
- 假设您没有 运行通过 ActiveJob(因为没有提到),请阅读
delayed_job
钩子:https://github.com/collectiveidea/delayed_job/#hooks - 它们可能是合适的 and/or 有用的工具 - 熟悉悲观和乐观锁定策略的一些差异和权衡 - 这个答案是一个很好的起点:Optimistic vs. Pessimistic locking
- 阅读有关分布式锁概念的一般实践,以便您可以为自己选择最好的工具和策略(它不一定是一个疯狂复杂的解决方案,一个简单的 table 在数据库中存储租户标识符就足够了,但您需要考虑失败情况 - 例如,如何管理被放弃的锁)
认真考虑不这样做;系统正常运行真的有严格要求吗?如果是这样,这可能表明您的数据模型存在潜在缺陷,或者您如何围绕该数据构建转换。在考虑对数据的操作时,在您的应用程序中争取 ACIDity,您可以避免很多此类问题。它不是后台作业 运行 人员常用的 "out of the box" 功能是有原因的。如果存在潜在的缺陷,它不仅会在这个问题上困扰您,还会在其他问题上困扰您 - 保证!
如果您试图避免两个不同的工作人员在同一个租户上工作,那么这是一个糟糕的设计选择。有东西在闻。先解决这个问题。但是,如果您希望相同类型的工作实例在不同的租户上工作,下面是最简单的解决方案。这些关系是我的假设。
ExpiredOrderCleaner = Struct.new(:tenant_id) do
def perform
Order.where(tenant_id: tenant_id).expired.delete_all
end
end
Tenant.each do |tenant|
Delayed::Job.enqueue ExpiredOrderCleaner.new(tenant.id)
end
这将为每个租户创造独特的就业机会。单个工作实例将在特定租户上工作。但是,可以有其他类型的工作在同一个租户上工作。这应该是好的。如果您需要更小的范围,只需为工作人员传递更多参数并在查询中使用并使用数据库事务来避免冲突。
这些 best practices 适用于任何后台工作人员。
- 让你的工作幂等和事务 意味着你的工作可以安全地执行多次
- 拥抱并发设计你的工作,这样你就可以运行许多并行
如果您使用 apartment gem and active job 包装器,您的工作会容易得多。从那里的文档中查看示例。