From 935260442c3ad652b57497b437768a3af4a034dc Mon Sep 17 00:00:00 2001 From: Tim Smith Date: Sun, 23 Aug 2026 12:40:13 -0700 Subject: [PATCH] Memoize Connection#rootless? correctly `@rootless ||= (info['Rootless'] == true)` cannot cache a false result, and #info is not memoized, so every call against a non-rootless daemon issued a fresh /info request: 5x Docker.rootless? -> 5 /info requests Test for the ivar with defined? instead. Every daemon that is not rootless is now one request rather than one per call: 5x Docker.rootless? -> 1 /info request Also adds a guard for #podman?, which uses the same `||=` idiom but is already backed by the memoized #version, so it makes a single /version request. The spec pins that behaviour so it does not regress into the same trap. --- lib/docker/connection.rb | 7 ++++++- spec/docker/connection_spec.rb | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/lib/docker/connection.rb b/lib/docker/connection.rb index d678d243..03c21af0 100644 --- a/lib/docker/connection.rb +++ b/lib/docker/connection.rb @@ -129,7 +129,12 @@ def podman? end def rootless? - @rootless ||= (info['Rootless'] == true) + # `||=` cannot cache a false result, so this has to test for the ivar + # itself. #info is not memoized, so re-evaluating means another /info + # request on every call against a non-rootless daemon. + return @rootless if defined?(@rootless) + + @rootless = (info['Rootless'] == true) end def version diff --git a/spec/docker/connection_spec.rb b/spec/docker/connection_spec.rb index 86291298..9778e8d1 100644 --- a/spec/docker/connection_spec.rb +++ b/spec/docker/connection_spec.rb @@ -112,6 +112,44 @@ end end + describe '#rootless?' do + subject { described_class.new('http://example.com:2375', {}) } + + # `@rootless ||= ...` cannot cache a false result, so on every non-rootless + # daemon each call re-issued the /info request. + it 'only queries /info once' do + expect(subject).to receive(:get).with('/info').once + .and_return('{"Rootless":false}') + + 3.times { subject.rootless? } + end + + it 'reports false for a non-rootless daemon' do + allow(subject).to receive(:get).with('/info') + .and_return('{"Rootless":false}') + + expect(subject.rootless?).to be false + end + + it 'reports true for a rootless daemon' do + allow(subject).to receive(:get).with('/info') + .and_return('{"Rootless":true}') + + expect(subject.rootless?).to be true + end + end + + describe '#podman?' do + subject { described_class.new('http://example.com:2375', {}) } + + it 'only queries /version once' do + expect(subject).to receive(:get).with('/version').once + .and_return('{"Components":[{"Name":"Engine"}]}') + + 3.times { subject.podman? } + end + end + describe '#to_s' do let(:url) { 'http://google.com:4000' } let(:options) { {} }