Repository: sidekiq/sidekiq (Ruby)

Issue:
Current attributes lost after inline execution
Ruby version: 3.3.0
Rails version: 7.1.1
Sidekiq / Pro / Enterprise version(s): 7.2.4

While upgrading from Sidekiq 6 to 7, we ran into this problem through our tests. We have Sidekiq jobs which themselves call `perform_async` and found that the parent job lost their current attributes after performing the child job.

It's easiest to demonstrate this with a test against `perform_inline` directly though:

```ruby
# test/current_attributes_test.rb

class CurrentAttributesJob
  include Sidekiq::Job

  def perform; end
end

describe "Current attributes" do
  before do
    @config = reset!
  end

  it "persists after perform_inline" do
    Sidekiq::CurrentAttributes.persist(Myapp::Current, @config)
    Myapp::Current.user_id = 1
    assert_equal 1, Myapp::Current.user_id
    CurrentAttributesJob.perform_inline
    assert_equal 1, Myapp::Current.user_id # got nil
  end
end
```

The problem is `Sidekiq::CurrentAttributes::Load#call` resets all the attributes afterwards. 

We think that instead it should call `ActiveSupport::CurrentAttributes.set` on each class, i.e.

```ruby
Myapp::Current.set(user_id: 123) do
  Myapp::OtherCurrent.set(other_id: "abc") do
    yield
  end
end
```

One way of doing that:

```ruby
    def call(_, job, _, &block)
      klass_attrs = {}

      @cattrs.each do |(key, strklass)|
        next unless job.has_key?(key)

        klass_attrs[strklass.constantize] = job[key]
      end

      klass_attrs.inject(block) { |chain, (klass, attrs)|
        -> { klass.set(attrs, &chain) }
      }.call
    end
```

Or with recursion, easier to follow:

```ruby
    def call(_, job, _, &block)
      klass_attrs = {}

      @cattrs.each do |(key, strklass)|
        next unless job.has_key?(key)

        klass_attrs[strklass.constantize] = job[key]
      end

      wrap(klass_attrs.to_a, &block)
    end

    def wrap(klass_attrs, &block)
      klass, attrs = klass_attrs.shift
      return block.call unless klass

      klass.set(attrs) do
        wrap(klass_attrs, &block)
      end
    end
```


Discussion: Would you like to work on a PR?
Looks good, I prefer the recursion solution.



Fix (patch):
diff --git a/lib/sidekiq/middleware/current_attributes.rb b/lib/sidekiq/middleware/current_attributes.rb
index 80cbe9590e..3b52536c37 100644
--- a/lib/sidekiq/middleware/current_attributes.rb
+++ b/lib/sidekiq/middleware/current_attributes.rb
@@ -46,22 +46,26 @@ def initialize(cattrs)
       end
 
       def call(_, job, _, &block)
-        cattrs_to_reset = []
+        klass_attrs = {}
 
         @cattrs.each do |(key, strklass)|
-          if job.has_key?(key)
-            constklass = strklass.constantize
-            cattrs_to_reset << constklass
+          next unless job.has_key?(key)
 
-            job[key].each do |(attribute, value)|
-              constklass.public_send(:"#{attribute}=", value)
-            end
-          end
+          klass_attrs[strklass.constantize] = job[key]
         end
 
-        yield
-      ensure
-        cattrs_to_reset.each(&:reset)
+        wrap(klass_attrs.to_a, &block)
+      end
+
+      private
+
+      def wrap(klass_attrs, &block)
+        klass, attrs = klass_attrs.shift
+        return block.call unless klass
+
+        klass.set(attrs) do
+          wrap(klass_attrs, &block)
+        end
       end
     end