【问题标题】:Passing Complex Hashes to Sidekiq Jobs将复杂哈希传递给 Sidekiq 作业
【发布时间】:2021-03-22 22:38:29
【问题描述】:

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

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

谢谢!

【问题讨论】:

    标签: ruby-on-rails ruby sidekiq


    【解决方案1】:

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

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

    即使它们确实序列化正确,如果您的队列备份并且引用对象同时发生变化,会发生什么? [...] 不要传递符号、命名参数、关键字参数或复杂的 Ruby 对象(如日期或时间!),因为它们无法正确地在转储/加载往返过程中存活。

    我喜欢添加第三个:

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

    sidekiq也提供了解决方案:

    不要将状态保存到 Sidekiq,保存简单的标识符。在您的 perform 方法中真正需要它们时查找这些对象。

    XY problem here

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

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

    延迟

    您的存储速度慢吗?这不是验证、序列化、存储的一些副作用吗?

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

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

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

    例如在写入繁重的 GIS 系统中,我们会将对象存储到“to_process_places”表中,该表由处理 Places 的工具监控。这完全取决于您的域和要求。

    不使用状态。

    一个常见的解决方案是不制作对象,而是使用客户的实际有效负载。只需发送 HTTP 有效负载(在 rails 中,params)并保留它。可能会合并到一个标头(如Request Date)或过滤掉一些数据(标头令牌或cookie)。

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

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

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

    【讨论】:

      【解决方案2】:

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

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

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

      但是,如果您使用 ActiveJob 而不是 Sidekiq 工作人员(您的 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-record-models-as-arguments

      【讨论】:

      • 我确实在使用 Sidekiq 工作人员,而不是 ActiveJob
      • 小心。 GlobalID 不序列化任何复杂的 Ruby 对象,只有 ActiveRecords。这对他的自定义 Ruby 对象没有帮助。
      【解决方案3】:

      在我的工作中对 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”的更多信息,请参阅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 类在时间上完全重叠。一旦半生不熟的my_object 蓝图通过 Sidekiq,它们就会失去一点精度,导致轻微的错位。

      解决此问题的一种方法是通过monkey patching the Time class

      在我们的案例中,一个更好的解决方案是反其道而行之,并且在我们的测试中不使用如此多的精度。示例中的my_object 是人类用户将在他们的日历上拥有的东西;在生产中,我们从未从客户那里得到如此精确的信息。因此,我们改为通过指示我们的一些测试对象使用类似Time.zone.now.beginning_of_minute 而不是Time.zone.now 来修复我们的测试。我们特意移除了精确度以解决问题,同时更贴近现实。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-06-14
        • 2013-09-04
        • 2015-07-12
        • 1970-01-01
        • 1970-01-01
        • 2012-05-18
        相关资源
        最近更新 更多