我的需求场景是这样的,在后台用户可以添加/编辑某个 Post 记录,在 after_commit 后,需要执行一个比较耗时的基于当前表的所有记录的异步缓存任务 PopulateGlobalPostsCacheJob。如果用户在一条条操作记录,这样会依次创建多任务在执行,而实际上只有最后一个任务是有效的。
我想到的办法就是在模型的 after_commit 中,插入一个在 2 分钟后执行的异步任务。如果这时发现有等待中的任务,就取消之前的这个任务。
要是 SolidQueue 能支持这个用法就好了。
没用过,但是扫了一眼文档
https://github.com/rails/solid_queue?tab=readme-ov-file#concurrency-controls
这里的 on_conflict 似乎能支持这个用法
on_conflict 支持的:discard 参数是将后面的入队的 Job 忽略掉,不是这个使用场景。
on_conflict 支持的:discard 是用在这样的场景:例如 在进行全站数据库备份等比较耗时的操作时,保证只有一个任务在进行,任务正在执行时,后续同样的任务入队了就忽略掉。
我想到的办法就是在模型的 after_commit 中,插入一个在 2 分钟后执行的异步任务。如果这时发现有等待中的任务,就取消之前的这个任务。
如果 PopulateGlobalPostsCacheJob 的语义符合要求(执行结果只和执行时间相关,和 Job enqueue 时间无关),这种场景 discard 新的异步任务,让等待中的任务正常执行,结果是相同的。
如楼上所说,感觉可以调整任务来解决,比如 PopulateGlobalPostsCacheJob 写入一个 redis key 带过期时间,后续的任务如果发现过期就执行,否则跳过
还有一个思路,不要盯着新的 Job,而是在执行 PopulateGlobalPostsCacheJob 时,利用队列的 API,检查是否有新的 PopulateGlobalPostsCacheJob 在排队,有的话就跳过当前 Job,不再执行。
楼主的需求明明是一个很常见的需求。问了一下高人,这样解决:
class Post < ApplicationRecord
after_commit :debounce_populate_global_posts_cache, on: [:create, :update]
private
def debounce_populate_global_posts_cache
# 从缓存读取上一个任务的 ID
old_job_id = Rails.cache.read(:global_posts_cache_job_id)
if old_job_id
# 找到旧任务并丢弃(如果存在)
old_job = SolidQueue::Job.find_by(id: old_job_id)
old_job&.discard # discard 会标记为 discarded,不会执行
end
# 调度新任务,延迟 2 分钟执行
new_job = PopulateGlobalPostsCacheJob.set(wait: 2.minutes).perform_later
# 将新任务的 provider_job_id 存入缓存(注意:perform_later 返回 ActiveJob 实例)
Rails.cache.write(:global_posts_cache_job_id, new_job.provider_job_id, expires_in: 3.minutes) # 缓存稍长于延迟时间,避免过期
end
end