catpm 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/catpm/flusher.rb CHANGED
@@ -16,6 +16,16 @@ module Catpm
16
16
  @thread = nil
17
17
  @pid = nil
18
18
  @mutex = Mutex.new
19
+ @sleep_mutex = Mutex.new
20
+ @flush_signal = ConditionVariable.new
21
+
22
+ # When buffer reaches capacity, wake flusher immediately
23
+ @buffer.on_flush_needed { signal_flush }
24
+ end
25
+
26
+ # Wake the flusher thread for an immediate flush cycle.
27
+ def signal_flush
28
+ @sleep_mutex.synchronize { @flush_signal.signal }
19
29
  end
20
30
 
21
31
  def start
@@ -32,7 +42,9 @@ module Catpm
32
42
  @pid = Process.pid
33
43
  @thread = Thread.new do
34
44
  while @running
35
- sleep(effective_interval)
45
+ @sleep_mutex.synchronize do
46
+ @flush_signal.wait(@sleep_mutex, effective_interval)
47
+ end
36
48
  flush_cycle if @running
37
49
  end
38
50
  rescue => e
@@ -61,6 +73,7 @@ module Catpm
61
73
  @thread = nil
62
74
  end
63
75
 
76
+ signal_flush # Wake flusher thread for clean exit
64
77
  thread&.join(timeout)
65
78
  flush_cycle # Final flush
66
79
  end
@@ -83,8 +96,8 @@ module Catpm
83
96
  adapter.persist_buckets(buckets)
84
97
 
85
98
  bucket_map = build_bucket_map(buckets)
86
- samples = rotate_samples(samples)
87
99
  adapter.persist_samples(samples, bucket_map)
100
+ trim_samples(samples)
88
101
  adapter.persist_errors(errors)
89
102
  end
90
103
 
@@ -227,75 +240,43 @@ module Catpm
227
240
  end
228
241
 
229
242
 
230
- def rotate_samples(samples)
231
- return samples if samples.empty?
243
+ # Trim excess samples AFTER insert. Simpler and guaranteed correct —
244
+ # no stale-cache issues when a single flush batch crosses the limit.
245
+ def trim_samples(samples)
246
+ return if samples.empty?
232
247
 
233
- # Pre-fetch counts for all endpoints and types in bulk
234
248
  endpoint_keys = samples.map { |s| s[:bucket_key][0..2] }.uniq
235
- error_fps = samples.filter_map { |s| s[:error_fingerprint] }.uniq
236
-
237
- # Build counts cache: { [kind, target, op, type] => count }
238
- counts_cache = {}
239
- if endpoint_keys.any?
240
- Catpm::Sample.joins(:bucket)
241
- .where(catpm_buckets: { kind: endpoint_keys.map(&:first), target: endpoint_keys.map { |k| k[1] }, operation: endpoint_keys.map { |k| k[2] } })
242
- .where(sample_type: %w[random slow])
243
- .group('catpm_buckets.kind', 'catpm_buckets.target', 'catpm_buckets.operation', 'catpm_samples.sample_type')
244
- .count
245
- .each { |(kind, target, op, type), cnt| counts_cache[[kind, target, op, type]] = cnt }
246
- end
247
249
 
248
- error_counts = {}
249
- if error_fps.any?
250
- Catpm::Sample.where(sample_type: 'error', error_fingerprint: error_fps)
251
- .group(:error_fingerprint).count
252
- .each { |fp, cnt| error_counts[fp] = cnt }
250
+ endpoint_keys.each do |kind, target, operation|
251
+ endpoint_scope = Catpm::Sample.joins(:bucket)
252
+ .where(catpm_buckets: { kind: kind, target: target, operation: operation })
253
+
254
+ # Random: keep newest N
255
+ max_random = Catpm.config.max_random_samples_per_endpoint
256
+ trim_by_column(endpoint_scope.where(sample_type: 'random'), max_random, :recorded_at) if max_random
257
+
258
+ # Slow: keep highest-duration N
259
+ max_slow = Catpm.config.max_slow_samples_per_endpoint
260
+ trim_by_column(endpoint_scope.where(sample_type: 'slow'), max_slow, :duration) if max_slow
261
+
253
262
  end
254
263
 
255
- samples.each do |sample|
256
- kind, target, operation = sample[:bucket_key][0..2]
257
-
258
- case sample[:sample_type]
259
- when 'random'
260
- max_random = Catpm.config.max_random_samples_per_endpoint
261
- if max_random
262
- cache_key = [kind, target, operation, 'random']
263
- if (counts_cache[cache_key] || 0) >= max_random
264
- oldest = Catpm::Sample.joins(:bucket)
265
- .where(catpm_buckets: { kind: kind, target: target, operation: operation })
266
- .where(sample_type: 'random').order(recorded_at: :asc).first
267
- oldest&.destroy
268
- end
269
- end
270
- when 'slow'
271
- max_slow = Catpm.config.max_slow_samples_per_endpoint
272
- if max_slow
273
- cache_key = [kind, target, operation, 'slow']
274
- if (counts_cache[cache_key] || 0) >= max_slow
275
- weakest = Catpm::Sample.joins(:bucket)
276
- .where(catpm_buckets: { kind: kind, target: target, operation: operation })
277
- .where(sample_type: 'slow').order(duration: :asc).first
278
- if weakest && sample[:duration] > weakest.duration
279
- weakest.destroy
280
- else
281
- sample[:_skip] = true
282
- end
283
- end
284
- end
285
- when 'error'
286
- max_err = Catpm.config.max_error_samples_per_fingerprint
287
- if max_err
288
- fp = sample[:error_fingerprint]
289
- if fp && (error_counts[fp] || 0) >= max_err
290
- oldest = Catpm::Sample.where(sample_type: 'error', error_fingerprint: fp)
291
- .order(recorded_at: :asc).first
292
- oldest&.destroy
293
- end
294
- end
264
+ # Errors: per-fingerprint cap (keep newest within each fingerprint)
265
+ max_err_fp = Catpm.config.max_error_samples_per_fingerprint
266
+ if max_err_fp
267
+ fps = samples.filter_map { |s| s[:error_fingerprint] }.uniq
268
+ fps.each do |fp|
269
+ trim_by_column(Catpm::Sample.where(sample_type: 'error', error_fingerprint: fp), max_err_fp, :recorded_at)
295
270
  end
296
271
  end
272
+ end
273
+
274
+ def trim_by_column(scope, max, keep_column)
275
+ count = scope.count
276
+ return if count <= max
297
277
 
298
- samples.reject { |s| s.delete(:_skip) }
278
+ excess_ids = scope.order(keep_column => :asc).limit(count - max).pluck(:id)
279
+ Catpm::Sample.where(id: excess_ids).delete_all if excess_ids.any?
299
280
  end
300
281
 
301
282
  def build_error_context(event)
@@ -65,7 +65,12 @@ module Catpm
65
65
  def pop_span(index)
66
66
  return unless index
67
67
 
68
- @span_stack.delete(index)
68
+ # Pop from stack — typically it's the last element (LIFO)
69
+ if @span_stack.last == index
70
+ @span_stack.pop
71
+ else
72
+ @span_stack.delete(index)
73
+ end
69
74
  segment = @segments[index]
70
75
  return unless segment
71
76
 
data/lib/catpm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Catpm
4
- VERSION = '0.6.5'
4
+ VERSION = '0.7.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: catpm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.5
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''
@@ -68,7 +68,9 @@ files:
68
68
  - app/views/catpm/shared/_segments_waterfall.html.erb
69
69
  - app/views/catpm/status/index.html.erb
70
70
  - app/views/catpm/system/index.html.erb
71
+ - app/views/catpm/system/pipeline.html.erb
71
72
  - app/views/layouts/catpm/application.html.erb
73
+ - app/views/layouts/catpm/pipeline.html.erb
72
74
  - config/routes.rb
73
75
  - db/migrate/20250601000001_create_catpm_tables.rb
74
76
  - lib/catpm.rb