Skip to content

Commit 8f2f02d

Browse files
authored
Merge pull request #306 from seanmil/get_call_optimization
Provider get() call optimization
2 parents ef0f655 + 53d764d commit 8f2f02d

File tree

11 files changed

+393
-38
lines changed

11 files changed

+393
-38
lines changed

lib/puppet/resource_api.rb

Lines changed: 75 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require 'puppet/resource_api/glue'
66
require 'puppet/resource_api/parameter'
77
require 'puppet/resource_api/property'
8+
require 'puppet/resource_api/provider_get_cache'
89
require 'puppet/resource_api/puppet_context' unless RUBY_PLATFORM == 'java'
910
require 'puppet/resource_api/read_only_parameter'
1011
require 'puppet/resource_api/transport'
@@ -69,6 +70,15 @@ def type_definition
6970
apply_to_device
7071
end
7172

73+
define_singleton_method(:rsapi_provider_get_cache) do
74+
# This gives a new cache per resource provider on each Puppet run:
75+
@rsapi_provider_get_cache ||= Puppet::ResourceApi::ProviderGetCache.new
76+
end
77+
78+
def rsapi_provider_get_cache
79+
self.class.rsapi_provider_get_cache
80+
end
81+
7282
def initialize(attributes)
7383
# $stderr.puts "A: #{attributes.inspect}"
7484
if attributes.is_a? Puppet::Resource
@@ -154,8 +164,17 @@ def generate
154164
end
155165

156166
def rsapi_current_state
157-
refresh_current_state unless @rsapi_current_state
158-
@rsapi_current_state
167+
return @rsapi_current_state if @rsapi_current_state
168+
# If the current state is not set, then check the cache and, if a value is
169+
# found, ensure it passes strict_check before allowing it to be used:
170+
cached_value = rsapi_provider_get_cache.get(rsapi_title)
171+
strict_check(cached_value) if cached_value
172+
@rsapi_current_state = cached_value
173+
end
174+
175+
def rsapi_current_state=(value)
176+
rsapi_provider_get_cache.add(rsapi_title, value)
177+
@rsapi_current_state = value
159178
end
160179

161180
def to_resource
@@ -242,57 +261,77 @@ def to_resource_shim(resource)
242261
type_definition.create_attribute_in(self, name, param_or_property, parent, options)
243262
end
244263

264+
def self.rsapi_provider_get(names = nil)
265+
# If the cache has been marked as having all instances, then just return the
266+
# full contents:
267+
return rsapi_provider_get_cache.all if rsapi_provider_get_cache.cached_all? && names.nil?
268+
269+
fetched = if type_definition.feature?('simple_get_filter')
270+
my_provider.get(context, names)
271+
else
272+
my_provider.get(context)
273+
end
274+
275+
fetched.each do |resource_hash|
276+
type_definition.check_schema(resource_hash)
277+
rsapi_provider_get_cache.add(build_title(type_definition, resource_hash), resource_hash)
278+
end
279+
280+
if names.nil? && !type_definition.feature?('simple_get_filter')
281+
# Mark the cache as having all possible instances:
282+
rsapi_provider_get_cache.cached_all
283+
end
284+
285+
fetched
286+
end
287+
245288
def self.instances
246289
# puts 'instances'
247290
# force autoloading of the provider
248291
provider(type_definition.name)
249292

250-
initial_fetch = if type_definition.feature?('simple_get_filter')
251-
my_provider.get(context, nil)
252-
else
253-
my_provider.get(context)
254-
end
255-
256-
initial_fetch.map do |resource_hash|
257-
type_definition.check_schema(resource_hash)
293+
rsapi_provider_get.map do |resource_hash|
258294
# allow a :title from the provider to override the default
259295
result = if resource_hash.key? :title
260296
new(title: resource_hash[:title])
261297
else
262298
new(title: build_title(type_definition, resource_hash))
263299
end
300+
# Cache the state in the generated resource, but unfortunately
301+
# this only benefits "puppet resource", not apply runs:
264302
result.cache_current_state(resource_hash)
265303
result
266304
end
267305
end
268306

269307
def refresh_current_state
270-
@rsapi_current_state = if type_definition.feature?('simple_get_filter')
271-
my_provider.get(context, [rsapi_title]).find { |h| namevar_match?(h) }
272-
else
273-
my_provider.get(context).find { |h| namevar_match?(h) }
274-
end
275-
276-
if @rsapi_current_state
277-
type_definition.check_schema(@rsapi_current_state)
278-
strict_check(@rsapi_current_state)
308+
current_state = self.class.rsapi_provider_get([rsapi_title]).find { |h| namevar_match?(h) }
309+
310+
if current_state
311+
strict_check(current_state)
279312
else
280-
@rsapi_current_state = if rsapi_title.is_a? Hash
281-
rsapi_title.dup
282-
else
283-
{ title: rsapi_title }
284-
end
285-
@rsapi_current_state[:ensure] = :absent if type_definition.ensurable?
313+
current_state = if rsapi_title.is_a? Hash
314+
rsapi_title.dup
315+
else
316+
{ title: rsapi_title }
317+
end
318+
current_state[:ensure] = :absent if type_definition.ensurable?
286319
end
320+
self.rsapi_current_state = current_state
287321
end
288322

289-
# Use this to set the current state from the `instances` method
323+
# Use this to set the current state from the `instances` method. "puppet resources"
324+
# needs this to minimize provider get() calls, but during a Puppet apply run
325+
# the instances() method is only used by resource generation, and resource
326+
# generators use and then discard the resources created by `instances``, so this
327+
# does not help with that:
290328
def cache_current_state(resource_hash)
291-
@rsapi_current_state = resource_hash
292-
strict_check(@rsapi_current_state)
329+
self.rsapi_current_state = resource_hash
330+
strict_check(resource_hash)
293331
end
294332

295333
def retrieve
334+
refresh_current_state unless rsapi_current_state
296335
Puppet.debug("Current State: #{rsapi_current_state.inspect}")
297336

298337
result = Puppet::Resource.new(self.class, title, parameters: rsapi_current_state)
@@ -316,17 +355,17 @@ def flush
316355
# puts 'flush'
317356
target_state = rsapi_canonicalized_target_state
318357

319-
retrieve unless @rsapi_current_state
358+
retrieve unless rsapi_current_state
320359

321-
return if @rsapi_current_state == target_state
360+
return if rsapi_current_state == target_state
322361

323362
Puppet.debug("Target State: #{target_state.inspect}")
324363

325364
# enforce init_only attributes
326-
if Puppet.settings[:strict] != :off && @rsapi_current_state && (@rsapi_current_state[:ensure] == 'present' && target_state[:ensure] == 'present')
365+
if Puppet.settings[:strict] != :off && rsapi_current_state && (rsapi_current_state[:ensure] == 'present' && target_state[:ensure] == 'present')
327366
target_state.each do |name, value|
328-
next unless type_definition.attributes[name][:behaviour] == :init_only && value != @rsapi_current_state[name]
329-
message = "Attempting to change `#{name}` init_only attribute value from `#{@rsapi_current_state[name]}` to `#{value}`"
367+
next unless type_definition.attributes[name][:behaviour] == :init_only && value != rsapi_current_state[name]
368+
message = "Attempting to change `#{name}` init_only attribute value from `#{rsapi_current_state[name]}` to `#{value}`"
330369
case Puppet.settings[:strict]
331370
when :warning
332371
Puppet.warning(message)
@@ -337,17 +376,17 @@ def flush
337376
end
338377

339378
if type_definition.feature?('supports_noop')
340-
my_provider.set(context, { rsapi_title => { is: @rsapi_current_state, should: target_state } }, noop: noop?)
379+
my_provider.set(context, { rsapi_title => { is: rsapi_current_state, should: target_state } }, noop: noop?)
341380
else
342-
my_provider.set(context, rsapi_title => { is: @rsapi_current_state, should: target_state }) unless noop?
381+
my_provider.set(context, rsapi_title => { is: rsapi_current_state, should: target_state }) unless noop?
343382
end
344383
if context.failed?
345384
context.reset_failed
346385
raise 'Execution encountered an error'
347386
end
348387

349388
# remember that we have successfully reached our desired state
350-
@rsapi_current_state = target_state
389+
self.rsapi_current_state = target_state
351390
end
352391

353392
def raise_missing_attrs
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
# rubocop:disable Style/Documentation
4+
module Puppet; end
5+
module Puppet::ResourceApi; end
6+
# rubocop:enable Style/Documentation
7+
8+
# This class provides a simple caching mechanism to support minimizing get()
9+
# calls into a provider.
10+
class Puppet::ResourceApi::ProviderGetCache
11+
def initialize
12+
clear
13+
end
14+
15+
def clear
16+
@cache = {}
17+
@cached_all = false
18+
end
19+
20+
def all
21+
raise 'all method called, but cached_all not called' unless @cached_all
22+
@cache.values
23+
end
24+
25+
def add(key, value)
26+
@cache[key] = value
27+
end
28+
29+
def get(key)
30+
@cache[key]
31+
end
32+
33+
def cached_all
34+
@cached_all = true
35+
end
36+
37+
def cached_all?
38+
@cached_all
39+
end
40+
end

spec/acceptance/composite_namevar_spec.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
stdout_str, status = Open3.capture2e("puppet resource #{common_args} composite_namevar php/gem")
3434
expect(stdout_str.strip).to match %r{^composite_namevar \{ \'php/gem\'}
3535
expect(stdout_str.strip).to match %r{ensure\s*=> \'present\'}
36-
expect(stdout_str.strip).to match %r{Looking for \[\{:package=>"php", :manager=>"gem"\}\]}
36+
# "Looking for" will return nil as puppet resource will have already fetched
37+
# the resource in instances():
38+
expect(stdout_str.strip).to match %r{Looking for nil}
3739
expect(status.exitstatus).to eq 0
3840
end
3941
it 'properly identifies an absent resource if only the title is provided' do

spec/acceptance/device_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
'Returned values: \{:name=>"wibble", :ensure=>"present", :string=>"sample", :string_ro=>"fixed"\}\n'\
3737
'Canonicalized values: \{:name=>"wibble", :ensure=>"present", :string=>"changed", :string_ro=>"fixed"\}'
3838
expect(stdout_str).to match %r{#{stdmatch}}
39-
expect(status).to be_success
39+
expect(status).not_to be_success
4040
end
4141
end
4242

spec/acceptance/get_calls_spec.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require 'tempfile'
5+
6+
RSpec.describe 'minimizing provider get calls' do
7+
let(:common_args) { '--verbose --trace --strict=error --modulepath spec/fixtures' }
8+
9+
describe 'using `puppet resource` with a basic type' do
10+
it 'calls get 1 time' do
11+
stdout_str, status = Open3.capture2e("puppet resource #{common_args} test_get_calls_basic")
12+
expect(stdout_str).to match %r{Notice: test_get_calls_basic: Provider get called 1 times}
13+
expect(stdout_str).not_to match %r{Notice: test_get_calls_basic: Provider get called 2 times}
14+
expect(status).to eq 0
15+
end
16+
end
17+
18+
describe 'using `puppet apply` with a basic type' do
19+
it 'calls get 2 times' do
20+
stdout_str, _status = Open3.capture2e("puppet apply #{common_args} -e \"test_get_calls_basic { foo: } test_get_calls_basic { bar: }\"")
21+
expect(stdout_str).to match %r{Notice: test_get_calls_basic: Provider get called 1 times}
22+
expect(stdout_str).not_to match %r{Notice: test_get_calls_basic: Provider get called 2 times}
23+
expect(stdout_str).not_to match %r{Creating}
24+
end
25+
26+
it 'calls get 1 time with resource purging' do
27+
stdout_str, _status = Open3.capture2e("puppet apply #{common_args} -e \"test_get_calls_basic { foo: } test_get_calls_basic { bar: } resources { test_get_calls_basic: purge => true }\"")
28+
expect(stdout_str).to match %r{Notice: test_get_calls_basic: Provider get called 1 times}
29+
expect(stdout_str).not_to match %r{Notice: test_get_calls_basic: Provider get called 2 times}
30+
expect(stdout_str).not_to match %r{Creating}
31+
end
32+
end
33+
34+
describe 'using `puppet resource` with a simple_get_filter type' do
35+
it 'calls get 1 time' do
36+
stdout_str, status = Open3.capture2e("puppet resource #{common_args} test_get_calls_sgf")
37+
expect(stdout_str).to match %r{Notice: test_get_calls_sgf: Provider get called 1 times}
38+
expect(stdout_str).not_to match %r{Notice: test_get_calls_sgf: Provider get called 2 times}
39+
expect(status.exitstatus).to be_zero
40+
end
41+
end
42+
43+
describe 'using `puppet apply` with a type using simple_get_filter' do
44+
it 'calls get 2 times' do
45+
stdout_str, _status = Open3.capture2e("puppet apply #{common_args} -e \"test_get_calls_sgf { foo: } test_get_calls_sgf { bar: }\"")
46+
expect(stdout_str).to match %r{Notice: test_get_calls_sgf: Provider get called 1 times}
47+
expect(stdout_str).to match %r{Notice: test_get_calls_sgf: Provider get called 2 times}
48+
expect(stdout_str).not_to match %r{Notice: test_get_calls_sgf: Provider get called 3 times}
49+
expect(stdout_str).not_to match %r{Creating}
50+
end
51+
52+
it 'calls get 1 time when resource purging' do
53+
stdout_str, _status = Open3.capture2e("puppet apply #{common_args} -e \"test_get_calls_sgf { foo: } test_get_calls_sgf { bar: } resources { test_get_calls_sgf: purge => true }\"")
54+
expect(stdout_str).to match %r{Notice: test_get_calls_sgf: Provider get called 1 times}
55+
expect(stdout_str).not_to match %r{Notice: test_get_calls_sgf: Provider get called 2 times}
56+
expect(stdout_str).not_to match %r{Creating}
57+
end
58+
end
59+
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
require 'puppet/resource_api/simple_provider'
4+
5+
# Implementation for the test_get_calls_basic type using the Resource API.
6+
class Puppet::Provider::TestGetCallsBasic::TestGetCallsBasic < Puppet::ResourceApi::SimpleProvider
7+
def get(context)
8+
@count ||= 0
9+
@count += 1
10+
context.notice("Provider get called #{@count} times")
11+
[
12+
{
13+
name: 'foo',
14+
ensure: 'present',
15+
prop: 'fooprop',
16+
},
17+
{
18+
name: 'bar',
19+
ensure: 'present',
20+
prop: 'barprop',
21+
},
22+
]
23+
end
24+
25+
def create(context, name, should)
26+
context.notice("Creating '#{name}' with #{should.inspect}")
27+
end
28+
29+
def update(context, name, should)
30+
context.notice("Updating '#{name}' with #{should.inspect}")
31+
end
32+
33+
def delete(context, name)
34+
context.notice("Deleting '#{name}'")
35+
end
36+
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
require 'puppet/resource_api/simple_provider'
4+
5+
# Implementation for the test_get_calls_sgf type using the Resource API.
6+
class Puppet::Provider::TestGetCallsSgf::TestGetCallsSgf < Puppet::ResourceApi::SimpleProvider
7+
def get(context, names = nil)
8+
@count ||= 0
9+
@count += 1
10+
context.notice("Provider get called #{@count} times with names=#{names}")
11+
data = [
12+
{
13+
name: 'foo',
14+
ensure: 'present',
15+
},
16+
{
17+
name: 'bar',
18+
ensure: 'present',
19+
},
20+
]
21+
if names.nil?
22+
data
23+
else
24+
data.select { |r| names.include?(r[:name]) }
25+
end
26+
end
27+
28+
def create(context, name, should)
29+
context.notice("Creating '#{name}' with #{should.inspect}")
30+
end
31+
32+
def update(context, name, should)
33+
context.notice("Updating '#{name}' with #{should.inspect}")
34+
end
35+
36+
def delete(context, name)
37+
context.notice("Deleting '#{name}'")
38+
end
39+
end

0 commit comments

Comments
 (0)