Repository: sidekiq/sidekiq (Ruby)

Issue:
CurrentAttributes issue/improvement - Serialization
Currently if an app uses current attributes with full objects, such as `Current.organization`, you can run into errors such as:

> NoMethodError: undefined method `name' for "#Organization:0x0000111111111111111":String

I don't see mention of any restrictions using Sidekiq's CurrentAttributes middleware besides in another [issue](https://github.com/sidekiq/sidekiq/issues/6143) which states the downsides of object hydration and recommends using lazy accessors.

I definitely see the downsides but have a couple of counter points:
- You also run into those issues with job deserialization but that still supports objects (at least with activerecord)
- By simply not supporting it but not preventing it, you get unexpected errors such as the one above

Since it is just an extra middleware, I took a shot at implementing it by modifying the CurrentAttributes middleware sidekiq provides. All it required was updating 2 lines and maybe adding a require:

https://github.com/sidekiq/sidekiq/blob/0e9419a7fe5a543794d896246e0eac594ea19235/lib/sidekiq/middleware/current_attributes.rb

Line 33:

```
-            attrs = strklass.constantize.attributes
+            attrs = ActiveJob::Arguments.serialize(strklass.constantize.attributes)
```

Line 71:

```
-          klass_attrs[strklass.constantize] = job[key]
+          klass_attrs[strklass.constantize] = ActiveJob::Arguments.deserialize(job[key])
```

It looks like `ActiveJob::Arguments` hasn't changed much and has long since had both methods, so there shouldn't be compatibility issues.


Discussion: Since this class already assumes Rails, it's ok to use Rails classes and methods, like `AJ::Arguments`.

Will this break existing job data already serialized in Redis? Is there existing code in Rails to hydrate CurrentAttributes that we can copy as a best practice? What does Active Job itself do?
It doesn't break existing job data already stored, but doesn't fix it. For example the `Organization` case above, you still get the same error since it comes back as a string.

I couldn't find any de/serialization code in CurrentAttributes but the AJ method made sense to me since it'll be using the same storage type as the job arguments.

Active Job uses these same `Arguments` methods to read and write the data to and from the job hash. It does allow for overriding the serialization since they're just methods on base job class. https://github.com/rails/rails/blob/main/activejob/lib/active_job/core.rb#L114
👍🏼 We can provide a pluggable serialize/deserialize impl which by default uses AJ::A. If you want to implement, PR welcome.
Awesome! I can give it a shot. Is it pluggable enough to update the existing CurrentAttributes middleware? Or should there be a way to also pass in a serializer (maybe `Sidekiq::CurrentAttributes.serializer = "ActiveJob::Attributes"`)?
Yep, any duck type with AJ::A should be fine. We can discuss more in the PR.



Fix (patch):
diff --git a/lib/sidekiq/middleware/current_attributes.rb b/lib/sidekiq/middleware/current_attributes.rb
index 7309a847b8..b33cf5e5c2 100644
--- a/lib/sidekiq/middleware/current_attributes.rb
+++ b/lib/sidekiq/middleware/current_attributes.rb
@@ -1,5 +1,6 @@
 # frozen_string_literal: true
 
+require "active_job/arguments"
 require "active_support/current_attributes"
 
 module Sidekiq
@@ -20,6 +21,8 @@ module Sidekiq
   #   Sidekiq::CurrentAttributes.persist(["Myapp::Current", "Myapp::OtherCurrent"])
   #
   module CurrentAttributes
+    Serializer = ::ActiveJob::Arguments
+
     class Save
       include Sidekiq::ClientMiddleware
 
@@ -33,26 +36,11 @@ def call(_, job, _, _)
             attrs = strklass.constantize.attributes
             # Retries can push the job N times, we don't
             # want retries to reset cattr. #5692, #5090
-            if attrs.any?
-              # Older rails has a bug that `CurrentAttributes#attributes` always returns
-              # the same hash instance. We need to dup it to avoid being accidentally mutated.
-              job[key] = if returns_same_object?
-                attrs.dup
-              else
-                attrs
-              end
-            end
+            job[key] = Serializer.serialize(attrs) if attrs.any?
           end
         end
         yield
       end
-
-      private
-
-      def returns_same_object?
-        ActiveSupport::VERSION::MAJOR < 8 ||
-          (ActiveSupport::VERSION::MAJOR == 8 && ActiveSupport::VERSION::MINOR == 0)
-      end
     end
 
     class Load
@@ -68,7 +56,7 @@ def call(_, job, _, &block)
         @cattrs.each do |(key, strklass)|
           next unless job.has_key?(key)
 
-          klass_attrs[strklass.constantize] = job[key]
+          klass_attrs[strklass.constantize] = Serializer.deserialize(job[key]).to_h
         end
 
         wrap(klass_attrs.to_a, &block)