将复杂的哈希传递给 Sidekiq 作业

Passing Complex Hashes to Sidekiq Jobs

Best Practices Guide 到使用 Sidekiq,我知道最好将“字符串、整数、浮点数、布尔值、null(nil)、数组和散列”作为参数传递给作业。

我通常只是将持久对象的 ID 传递给我的作业,但由于延迟限制,我需要在 运行 作业之后保存对象。

我正在使用的非持久对象包含多种数据类型:

#MyObject<00x000>{
id: nil
start_time: Fri, 11 Dec 2020 08:45:00 PST -08:00 (*this is a TimeWithZone object)
rate: 18.0 (*this is a BigDecimal object)
...
}

我计划先将此对象转换为散列,然后将其传递给我的工作:

MyJob.perform_async(my_object.attributes)

然后像这样持久化对象:

MyObject.new(my_object_hash).save

我的问题是,这样安全吗?即使我将 'simple' 数据类型传递给 Sidekiq,它实际上包含复杂的对象。我会失去精度吗?

谢谢!

您链接的最佳实践中最重要的部分是

Complex Ruby objects do not convert to JSON

因此,您不应该将模型的实例传递给工作人员。 如果您使用的是 Sidekiq worker,则应遵守此声明,并且您传递的哈希值应该没问题。我不太确定 TimeWithZone 对象,但您可以尝试将其转换为 JSON 或字符串,就像他们在最佳实践指南中所做的那样。

但是,如果您使用的是 ActiveJob 而不是 Sidekiq worker(您的工作是继承自 ApplicationJob 还是 include Sidekiq::Worker ?),那么你没有那个问题,因为 ActiveJob 使用全局 ID 将对象转换为字符串。然后在执行作业之前再次反序列化对象。这意味着您可以将对象传递给您的作业。

my_object = MyObject.find(1)
my_object.to_global_id #=> #<GlobalID:0x000045432da2344 [...] gid://your_app_name/MyObject/1>>
serialized_my_object = my_object.to_global_id.to_s

my_object = GlobalID.find(serialized_my_object)

您可以在此处找到更多信息 https://github.com/toptal/active-job-style-guide#active-record-models-as-arguments

在我的工作中对时间对象做了一些实验后,我发现我在工作的另一端失去了纳秒精度。

my_object.start_time
=> Mon, 21 Dec 2020 11:35:50 PST -08:00
my_object.strftime('%Y-%m-%d %H:%M:%S.%N')
=> "2020-12-21 11:35:50.151893000"

你可以看到这里,我们的精度包括小数点后6位。 (有关 'strftime' 的更多信息,请参阅 this answer

一旦我们在对象上调用 JSON 方法:

generated = JSON.generate(my_object.attributes))
=> \"start_time\":\"2020-12-21T11:35:50.151-08:00\"

你可以在这里看到我们将精度降低到小数点后 3 位。剩下的3位数字此时丢失。

parsed = JSON.parse(generated)
parsed[‘start_time’] = "2020-12-21T11:35:50.151-08:00"

它出现在最基本的层面上,JSON 库递归调用散列中的每个键值对 as_json。所以这实际上取决于您的特定对象如何实现 as_json.

此问题导致测试失败,涉及查询我们的数据库以查找持久对象(初始化为 start_time = Time.zone.now (!)),这些对象与我们的 MyObject class。一旦半生不熟的 my_object 蓝图通过 Sidekiq,它们就会失去一点精度,导致轻微的错位。

解决此问题的一种方法是

在我们的例子中,更好的解决方案是朝相反的方向前进,并且在我们的测试中不要使用那么高的精度。示例中的 my_object 是人类用户将在他们的日历上拥有的内容;在生产中,我们从未从客户那里得到如此精确的信息。因此,我们通过指示我们的一些测试对象使用类似 Time.zone.now.beginning_of_minute 而不是 Time.zone.now 来修复我们的测试。我们有意删除了精度以解决此问题,并更贴近现实。

这听起来像是一个“potayto, potahto”的解决方案。你不是不用Sidekiq的序列化,而是自己序列化。

我们来看看为什么sidekiq有这个规则:

Even if they did serialize correctly, what happens if your queue backs up and that quote object changes in the meantime? [...] Don't pass symbols, named parameters, keyword arguments or complex Ruby objects (like Date or Time!) as those will not survive the dump/load round trip correctly.

我想加第三个:

Serializing state makes it impossible to distinguish between persisted and ethereal (in-memory, memoized, lazy-loaded etc) data. E.g. a def sent_mails; @sent_mails ||= Mail.for(user_id: id); end now gets serialized: do you want that?

sidekiq也提供了解决方案:

Don't save state to Sidekiq, save simple identifiers. Look up the objects once you actually need them in your perform method.

XY problem here

您的真正的问题不是在何处或如何序列化状态。因为 sidekiq 警告不要序列化状态,无论您在哪里以及如何执行此操作。

你需要解决的问题是如何将状态存储在可以正确存储它的地方。或者完全避免存储状态:不在 redis/sidekiq 中,也不在给您带来问题的存储中。

延迟

你的存储速度慢吗?难道不是验证、序列化、一些 side-effect 缓慢的存储?

您能否通过将其设为 two-step 来改进它:插入状态并稍后 update/enrich/validate 异步?如果你使用 Rails,它不会在这里帮助你,甚至可能对你不利,但一个常见的模型是将 objects 存储在一个特殊的“queue”table 或事件 queue;例如卡夫卡因此而闻名。

当例如存储发生在慢速网络到慢速 API,这可能无法解决,但是当存储发生在本地数据库中时,您可以使用数十年的解决方案来提高写入性能。无论是在您的数据库中,还是在 state-storage 中使用一些专门的 queue(sidekiq 不是这样的专门存储 queue),具体取决于用于存储的技术。例如。 Linux 将允许您通过内存存储,使写入磁盘的速度非常快,但取消了它是否真的写入磁盘的保证。

例如在簿记 api 中,我们会将经过验证的 object 存储在 PostgreSQL 中,然后让异步作业稍后向其添加昂贵的属性(例如,必须从遗留 API 或通过复杂的计算)。

例如在 write-heavy GIS 系统中,我们会将 object 存储到“to_process_places”table 中,它由处理 Places 的工具监控。这完全取决于您的域和要求。

未使用状态。

一个常见的解决方案是不制作 objects,而是使用客户的实际负载。只需发送 HTTP 有效负载(在 rails、params 中)并将其留在那儿。也许合并 header(如 Request Date)或过滤掉一些数据(header 令牌或 cookie)。

如果您的控制器可以使用这些数据进行操作,那么延迟作业也可以。不要在控制器中构建 objects,而是将其留给延迟的作业。这甚至可以产生非常整洁和精简的控制器:他们所做的就是(一些身份验证和授权,然后)调用适当的作业并将其传递给经过消毒的 params.

显然,这需要 trade-offs 无法验证 in-sync,而是通过电子邮件提供此类信息,push-notification,或者延迟回复,具体取决于您的要求(例如大型 CSV 导入可以通过电子邮件发送任何验证问题,但如果登录无效,则登录请求可能需要立即得到响应。

它还需要一些思考:您可能不想将 Base64 编码的 CSV 一起发送到 sidekiq,而是将文件写入(临时)存储并传递 filename/url。这听起来很明显,因为它是:文件上传本质上是前面提到的“临时状态存储”的实现:你不会将整个 PDF/high-res-header-image/CSV 传递给 sidekiq,而是将它存储在某个地方以便 sidekiq 可以选择它稍后处理它。如果将其他属性传递给 sidekiq 有问题,为什么其他属性不采用相同的模式?