Skip to content

Commit 50efc5b

Browse files
authored
Merge pull request #689 from puppetlabs/P4DEVOPS-8570
Add performance instrumentation to key methods
2 parents 4656d8b + 7c95684 commit 50efc5b

5 files changed

Lines changed: 96 additions & 15 deletions

File tree

lib/vmpooler/api/helpers.rb

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -299,17 +299,35 @@ def get_queue_metrics(pools, backend)
299299
total: 0
300300
}
301301

302-
queue[:requested] = get_total_across_pools_redis_scard(pools, 'vmpooler__provisioning__request', backend) + get_total_across_pools_redis_scard(pools, 'vmpooler__provisioning__processing', backend) + get_total_across_pools_redis_scard(pools, 'vmpooler__odcreate__task', backend)
303-
304-
queue[:pending] = get_total_across_pools_redis_scard(pools, 'vmpooler__pending__', backend)
305-
queue[:ready] = get_total_across_pools_redis_scard(pools, 'vmpooler__ready__', backend)
306-
queue[:running] = get_total_across_pools_redis_scard(pools, 'vmpooler__running__', backend)
307-
queue[:completed] = get_total_across_pools_redis_scard(pools, 'vmpooler__completed__', backend)
302+
# Use a single pipeline to fetch all queue counts at once for better performance
303+
results = backend.pipelined do |pipeline|
304+
# Order matters - we'll use indices to extract values
305+
pools.each do |pool|
306+
pipeline.scard("vmpooler__provisioning__request#{pool['name']}") # 0..n-1
307+
pipeline.scard("vmpooler__provisioning__processing#{pool['name']}") # n..2n-1
308+
pipeline.scard("vmpooler__odcreate__task#{pool['name']}") # 2n..3n-1
309+
pipeline.scard("vmpooler__pending__#{pool['name']}") # 3n..4n-1
310+
pipeline.scard("vmpooler__ready__#{pool['name']}") # 4n..5n-1
311+
pipeline.scard("vmpooler__running__#{pool['name']}") # 5n..6n-1
312+
pipeline.scard("vmpooler__completed__#{pool['name']}") # 6n..7n-1
313+
end
314+
pipeline.get('vmpooler__tasks__clone') # 7n
315+
pipeline.get('vmpooler__tasks__ondemandclone') # 7n+1
316+
end
308317

309-
queue[:cloning] = backend.get('vmpooler__tasks__clone').to_i + backend.get('vmpooler__tasks__ondemandclone').to_i
310-
queue[:booting] = queue[:pending].to_i - queue[:cloning].to_i
311-
queue[:booting] = 0 if queue[:booting] < 0
312-
queue[:total] = queue[:requested] + queue[:pending].to_i + queue[:ready].to_i + queue[:running].to_i + queue[:completed].to_i
318+
n = pools.length
319+
# Safely extract results with default to empty array if slice returns nil
320+
queue[:requested] = (results[0...n] || []).sum(&:to_i) +
321+
(results[n...(2 * n)] || []).sum(&:to_i) +
322+
(results[(2 * n)...(3 * n)] || []).sum(&:to_i)
323+
queue[:pending] = (results[(3 * n)...(4 * n)] || []).sum(&:to_i)
324+
queue[:ready] = (results[(4 * n)...(5 * n)] || []).sum(&:to_i)
325+
queue[:running] = (results[(5 * n)...(6 * n)] || []).sum(&:to_i)
326+
queue[:completed] = (results[(6 * n)...(7 * n)] || []).sum(&:to_i)
327+
queue[:cloning] = (results[7 * n] || 0).to_i + (results[7 * n + 1] || 0).to_i
328+
queue[:booting] = queue[:pending].to_i - queue[:cloning].to_i
329+
queue[:booting] = 0 if queue[:booting] < 0
330+
queue[:total] = queue[:requested] + queue[:pending].to_i + queue[:ready].to_i + queue[:running].to_i + queue[:completed].to_i
313331

314332
queue
315333
end

lib/vmpooler/api/v3.rb

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ class V3 < Sinatra::Base
99
api_version = '3'
1010
api_prefix = "/api/v#{api_version}"
1111

12+
# Simple in-memory cache for status endpoint
13+
# rubocop:disable Style/ClassVars
14+
@@status_cache = {}
15+
@@status_cache_mutex = Mutex.new
16+
# rubocop:enable Style/ClassVars
17+
STATUS_CACHE_TTL = 30 # seconds
18+
19+
# Clear cache (useful for testing)
20+
def self.clear_status_cache
21+
@@status_cache_mutex.synchronize do
22+
@@status_cache.clear
23+
end
24+
end
25+
1226
helpers do
1327
include Vmpooler::API::Helpers
1428
end
@@ -464,6 +478,32 @@ def update_clone_target(payload)
464478
end
465479
end
466480

481+
# Cache helper methods for status endpoint
482+
def get_cached_status(cache_key)
483+
@@status_cache_mutex.synchronize do
484+
cached = @@status_cache[cache_key]
485+
if cached && (Time.now - cached[:timestamp]) < STATUS_CACHE_TTL
486+
return cached[:data]
487+
end
488+
489+
nil
490+
end
491+
end
492+
493+
def set_cached_status(cache_key, data)
494+
@@status_cache_mutex.synchronize do
495+
@@status_cache[cache_key] = {
496+
data: data,
497+
timestamp: Time.now
498+
}
499+
# Cleanup old cache entries (keep only last 10 unique view combinations)
500+
if @@status_cache.size > 10
501+
oldest = @@status_cache.min_by { |_k, v| v[:timestamp] }
502+
@@status_cache.delete(oldest[0])
503+
end
504+
end
505+
end
506+
467507
def sync_pool_templates
468508
tracer.in_span("Vmpooler::API::V3.#{__method__}") do
469509
pool_index = pool_index(pools)
@@ -646,6 +686,13 @@ def generate_request_id
646686
get "#{api_prefix}/status/?" do
647687
content_type :json
648688

689+
# Create cache key based on view parameters
690+
cache_key = params[:view] ? "status_#{params[:view]}" : "status_all"
691+
692+
# Try to get cached response
693+
cached_response = get_cached_status(cache_key)
694+
return cached_response if cached_response
695+
649696
if params[:view]
650697
views = params[:view].split(",")
651698
end
@@ -706,7 +753,12 @@ def generate_request_id
706753

707754
result[:status][:uptime] = (Time.now - Vmpooler::API.settings.config[:uptime]).round(1) if Vmpooler::API.settings.config[:uptime]
708755

709-
JSON.pretty_generate(Hash[result.sort_by { |k, _v| k }])
756+
response = JSON.pretty_generate(Hash[result.sort_by { |k, _v| k }])
757+
758+
# Cache the response
759+
set_cached_status(cache_key, response)
760+
761+
response
710762
end
711763

712764
# request statistics for specific pools by passing parameter 'pool'

lib/vmpooler/pool_manager.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1687,7 +1687,12 @@ def check_pool(pool,
16871687

16881688
sync_pool_template(pool)
16891689
loop do
1690+
start_time = Time.now
16901691
result = _check_pool(pool, provider)
1692+
duration = Time.now - start_time
1693+
1694+
$metrics.gauge("vmpooler_performance.check_pool.#{pool['name']}", duration)
1695+
$logger.log('d', "[!] check_pool for #{pool['name']} took #{duration.round(2)}s") if duration > 5
16911696

16921697
if result[:cloned_vms] > 0 || result[:checked_pending_vms] > 0 || result[:discovered_vms] > 0
16931698
loop_delay = loop_delay_min

spec/integration/api/v3/status_spec.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ def app()
1717
# https://rubydoc.info/gems/sinatra/Sinatra/Base#reset!-class_method
1818
before(:each) do
1919
app.reset!
20+
# Clear status cache to prevent test interference
21+
Vmpooler::API::V3.clear_status_cache
2022
end
2123

2224
describe 'status and metrics endpoints' do

spec/unit/api/helpers_spec.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,12 @@ class TestHelpers
125125
{'name' => 'p2'}
126126
]
127127

128-
allow(redis).to receive(:pipelined).with(no_args).and_return [1,1]
129-
allow(redis).to receive(:get).and_return(1,0)
128+
# Mock returns 7*2 + 2 = 16 results (7 queue types for 2 pools + 2 global counters)
129+
# For each pool: [request, processing, odcreate, pending, ready, running, completed]
130+
# Plus 2 global counters: clone (1), ondemandclone (0)
131+
# Results array: [1,1, 1,1, 1,1, 1,1, 1,1, 1,1, 1,1, 1, 0]
132+
# [req, proc, odc, pend, rdy, run, comp, clone, odc]
133+
allow(redis).to receive(:pipelined).with(no_args).and_return [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0]
130134

131135
expect(subject.get_queue_metrics(pools, redis)).to eq({requested: 6, pending: 2, cloning: 1, booting: 1, ready: 2, running: 2, completed: 2, total: 14})
132136
end
@@ -137,8 +141,8 @@ class TestHelpers
137141
{'name' => 'p2'}
138142
]
139143

140-
allow(redis).to receive(:pipelined).with(no_args).and_return [1,1]
141-
allow(redis).to receive(:get).and_return(5,0)
144+
# Mock returns 7*2 + 2 = 16 results with clone=5 to cause negative booting
145+
allow(redis).to receive(:pipelined).with(no_args).and_return [1,1,1,1,1,1,1,1,1,1,1,1,1,1,5,0]
142146

143147
expect(subject.get_queue_metrics(pools, redis)).to eq({requested: 6, pending: 2, cloning: 5, booting: 0, ready: 2, running: 2, completed: 2, total: 14})
144148
end

0 commit comments

Comments
 (0)