将复杂哈希传递给Sidekiq作业



从最佳实践指南到使用Sidekiq,我知道最好通过"string、integer、float、boolean、null(nil)、array和hash"作为作业的论据。

我通常只是将持久化对象的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

我的问题是,这里安全吗?尽管我将"简单"数据类型传递给Sidekiq,但它实际上包含复杂的对象。我会失去精确度吗?

谢谢!

这听起来像是一个"potayto;解决方案您不是在使用Sidekiq的序列化,而是自己序列化它。

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

即使它们确实正确地序列化了,如果您的队列备份,并且quote对象在此期间发生更改,会发生什么?[…]不要传递符号、命名参数、关键字参数或复杂的Ruby对象(如Date或Time!),因为这些对象将无法正确地完成转储/加载往返过程。

我想添加第三个:

序列化状态使得无法区分持久化和空灵(内存、内存化、延迟加载等)数据。例如,def sent_mails; @sent_mails ||= Mail.for(user_id: id); end现在被序列化了:你想要这样吗?

sidekiq:也提供该解决方案

不要将状态保存到Sidekiq,保存简单的标识符。一旦您在执行方法中实际需要这些对象,请查找它们。

此处的XY问题

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

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

延迟

你的存储速度慢吗?这不是一种验证,一种串行化,存储速度慢的一些副作用吗?

你能通过将其分为两步来改进吗:插入状态并稍后异步更新/丰富/验证它?如果你使用Rails,它在这里对你没有帮助,甚至可能对你不利,但一个常见的模型是将对象存储在一个特殊的";队列";表或事件队列;卡夫卡因此而出名。

例如,当存储发生在速度缓慢的网络上到速度缓慢的API时,这可能是无法解决的,但当存储发生于本地数据库中时,您可以使用数十年的解决方案来提高写入性能。两者都在数据库中,或者有一些专门的状态存储队列(sidekiq不是这样一个专门的存储队列),这取决于用于存储的技术。例如,Linux将允许您通过内存进行存储,使写入磁盘的速度非常快,但取消了它真正写入磁盘的保证。

例如,在记账api中,我们将验证对象存储在PostgreSQL中,然后让异步作业稍后向其添加昂贵的属性(例如,必须从遗留api或通过复杂计算检索的状态)。

例如,在重写的GIS系统中,我们将对象存储到";to_process_places";表,该表由处理Places的工具监控。这一切实际上取决于您的领域和需求。

未使用状态

一个常见的解决方案不是生成对象,而是由客户使用实际负载。只需发送HTTP有效负载(在rails中,即params),然后将其留在那里。也许可以合并一个标头(如请求日期)或过滤掉一些数据(标头令牌或cookie)。

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

显然,这需要权衡,比如不能同步验证,而是根据您的要求通过电子邮件、推送通知或延迟响应提供此类信息(例如,大型CSV导入可能只会通过电子邮件发送任何验证问题,但如果登录无效,登录请求可能需要立即得到响应)。

它还需要一些思考:您可能不想将Base64编码的CSV一起发送到sidekiq,而是将文件写入(临时)存储,然后传递filename/url。这听起来可能很明显,因为它是:文件上传本质上是前面提到的";"临时状态存储器":您不会将整个PDF/高分辨率标头图像/CSV一起传递给sidekiq,而是将其存储在某个地方,以便sidekaq稍后可以提取它进行处理。如果将其传递给siddkiq有问题,为什么其他属性不使用相同的模式?

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

复杂的Ruby对象不会转换为JSON

因此,您不应该将模型的实例传递给工作者。如果您使用的是Sidekiq worker,那么您应该遵守此语句,并且您传递的哈希应该很好。我不太确定TimeWithZone对象,但您可以尝试将其转换为JSON或字符串,就像最佳实践指南中所做的那样。

但是,如果您使用ActiveJob而不是Sidekiq worker(您的Job是从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-将模型记录为自变量

在我的工作中对Time对象进行了一些实验后,我发现我在工作的另一端正在失去纳秒的精度。

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"的更多信息,请参阅此答案)

一旦我们在对象上调用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类完全重叠。一旦半生不熟的my_object蓝图通过Sidekiq,它们就失去了一点精度,导致了轻微的错位。

解决这个问题的一种方法是通过monkey修补Time类。

在我们的案例中,一个更好的解决方案是朝着相反的方向前进,在测试中不要使用那么多精度。示例中的my_object是人类用户将在其日历上具有的内容;在生产中,我们从未从客户那里得到过如此高的精度。因此,我们通过指示一些测试对象使用类似Time.zone.now.beginning_of_minute而不是Time.zone.now的东西来修复测试。我们有意取消精确性,以解决问题,并更紧密地反映现实。

最新更新