From 36343b5400647893ffb8d5db0a0245ff8b3f6871 Mon Sep 17 00:00:00 2001 From: Ikraam Ghoor Date: Mon, 21 Sep 2026 09:26:44 +0200 Subject: [PATCH] Mix PageableResponse into Seahorse::Client::Response instead of extending each response Kernel#extend gives the response a singleton class and clears the method cache for every method the extension defines (each, count, to_h, respond_to?, and all of Enumerable), process-wide, on every request. Under YJIT that discards every compiled block that relied on one of those names being absent; a Rails app signing one S3 URL per request measured ~4 invalidations and 2.3 KB of regenerated code per URL, and its workers reached --yjit-mem-size within hours and stopped compiling. Including the extension in the response class once keeps the same interface on every response with no per-response work. apply still extends other objects, so its constant cache guarantee is unchanged. --- gems/aws-sdk-core/CHANGELOG.md | 2 ++ .../lib/aws-sdk-core/pageable_response.rb | 7 ++++++- .../spec/aws/pageable_response_spec.rb | 21 +++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/gems/aws-sdk-core/CHANGELOG.md b/gems/aws-sdk-core/CHANGELOG.md index e9eac7fa8e4..c7f09a60473 100644 --- a/gems/aws-sdk-core/CHANGELOG.md +++ b/gems/aws-sdk-core/CHANGELOG.md @@ -1,6 +1,8 @@ Unreleased Changes ------------------ +* Issue - Mix `PageableResponse` into `Seahorse::Client::Response` once instead of extending every response object, which cleared Ruby's method cache for each of the extension's methods on every request and made YJIT discard compiled code. + 3.257.0 (2026-09-14) ------------------ diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/pageable_response.rb b/gems/aws-sdk-core/lib/aws-sdk-core/pageable_response.rb index 564054495c0..f2ca7f527ce 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/pageable_response.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/pageable_response.rb @@ -49,7 +49,7 @@ module Aws module PageableResponse def self.apply(base) - base.extend Extension + base.extend Extension unless Extension === base base.instance_variable_set(:@last_page, nil) base.instance_variable_set(:@more_results, nil) base @@ -217,5 +217,10 @@ def next_page_params(params) end end + + # Mixed into the response class once rather than extended onto each + # response: Kernel#extend clears the method cache for every method the + # extension defines, process-wide, on every request. + Seahorse::Client::Response.include(Extension) end end diff --git a/gems/aws-sdk-core/spec/aws/pageable_response_spec.rb b/gems/aws-sdk-core/spec/aws/pageable_response_spec.rb index 34fa1a77885..a8a082e2852 100644 --- a/gems/aws-sdk-core/spec/aws/pageable_response_spec.rb +++ b/gems/aws-sdk-core/spec/aws/pageable_response_spec.rb @@ -274,6 +274,27 @@ def pageable(resp, pager) PageableResponse.apply(object) }.to_not change { RubyVM.stat(key) } end + + # Kernel#extend creates a singleton class and clears the method cache for every + # method the module defines (each, count, to_h, respond_to?, all of Enumerable...), + # process-wide, so it must not happen once per response. + it 'does not give a Seahorse::Client::Response a singleton class' do + skip 'Only applies to MRI' unless defined? RubyVM.stat + + resp = Seahorse::Client::Response.new + GC.disable + classes_before = ObjectSpace.count_objects.values_at(:T_CLASS, :T_ICLASS) + PageableResponse.apply(resp) + expect(ObjectSpace.count_objects.values_at(:T_CLASS, :T_ICLASS)).to eq(classes_before) + ensure + GC.enable + end + + it 'leaves every Seahorse::Client::Response pageable' do + resp = Seahorse::Client::Response.new + expect(resp).to be_kind_of(PageableResponse::Extension) + expect(resp).to be_kind_of(Enumerable) + end end end