railwatch 0.8.1 → 0.8.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c621aa5be8712487e490453e9b62ab5d3f75666b951270d50acf77b00ad03a29
4
- data.tar.gz: 5464f54bb6968924b1155456dcd22c5dada112980760340beb74632a169d5791
3
+ metadata.gz: 6426edd02fd2bb306da28aa55723598581751003cce2b10371fdabb5d5a726c7
4
+ data.tar.gz: 44f256ceff92b14dd0534e0f260f6d8d8b9fb4294e357091c25ec3e0eb3b3ef3
5
5
  SHA512:
6
- metadata.gz: 035642f4a96ae1803f01079f618315a7c1ff54657f6111f52071ae32896782118a40e3f7075ddfb60910625899e589c5cea8f5b3b7c999af42211d097eb2e161
7
- data.tar.gz: 4dc2f2217692fa974f06c7c2c7b487896206a0cefccd7301f9802d3eb192dc47900bdfdf96a9a07809d86b41c6a59292fc0df27382b7636167331fcb59c02978
6
+ metadata.gz: bdb8ba9db502d460a3a653a52238728993807fe5b374823d08f8418b865a1a0d6c0a39f18809a9a00fe95c96952f6a3db4d0337bb3b544e85e63c835830b5e62
7
+ data.tar.gz: 0ef6f54e5da09ae2b3fc7048ae8b9b4d50fbda878dbb42220c98236345054db8b6b24134e41c7eedf8d13d13f843f4405b0f037d3dd8e6e6caa3cb5d2a6f6d0e
data/CHANGELOG.md CHANGED
@@ -6,6 +6,23 @@
6
6
  filled in by the release commit, which is also the only commit that
7
7
  touches lib/railwatch/version.rb and Gemfile.lock. See CONTRIBUTING.md. -->
8
8
 
9
+ ## 0.8.3 (2026-09-23)
10
+
11
+ - Filtering jobs by `class:` seeks the class's group (a job's group is
12
+ the digest of its class name) instead of walking every job in the
13
+ window: 715 ms over a week on the platform's own tenant, now 9 ms, and
14
+ one MCP search took 49 s.
15
+ - The Scheduled tasks page and the missed-task check read each task's
16
+ newest schedule with one index seek per key. The single grouped query
17
+ they used read every scheduled run kept (10 s once, 185 ms warm).
18
+
19
+ ## 0.8.2 (2026-09-23)
20
+
21
+ - The Profiles page's profiled share counts only the executions inside
22
+ its window. A window starting or ending mid-hour counted the whole of
23
+ its first and last hours, because a rollup holds its whole hour; the
24
+ partial hours are now counted from the rows.
25
+
9
26
  ## 0.8.1 (2026-09-23)
10
27
 
11
28
  - The Tenants page reads only through the tenant-led indexes. Sparklines
@@ -30,16 +30,18 @@ module Railwatch
30
30
  private
31
31
 
32
32
  # How many executions the window holds, as the profiled share's
33
- # denominator. Every kind is rolled up, so the whole hours come from a
34
- # few hundred rollup rows rather than counting every execution (1.2 s
35
- # over a week of the platform's own); only the part of the first hour
36
- # inside the window is counted from the rows.
33
+ # denominator. Every kind is rolled up, so whole hours come from a few
34
+ # hundred rollup rows rather than counting every execution (1.2 s over a
35
+ # week of the platform's own). A rollup holds its whole hour, so the
36
+ # partial hours at either end of the window are counted from the rows.
37
37
  def executions_in(from, to)
38
- whole = from.beginning_of_hour == from ? from : from.beginning_of_hour + 1.hour
39
- return Telemetry::Execution.between(from, to).count if whole >= to
38
+ first = from.beginning_of_hour == from ? from : from.beginning_of_hour + 1.hour
39
+ last = to.beginning_of_hour
40
+ return Telemetry::Execution.between(from, to).count if first >= last
40
41
 
41
- Telemetry::Execution.where(occurred_at: from...whole).count +
42
- Telemetry::Rollup.for_type(Telemetry::Execution::KINDS).between(whole, to).sum(:count)
42
+ Telemetry::Execution.where(occurred_at: from...first).count +
43
+ Telemetry::Rollup.for_type(Telemetry::Execution::KINDS).where(bucket: first...last).sum(:count) +
44
+ Telemetry::Execution.where(occurred_at: last..to).count
43
45
  end
44
46
 
45
47
  # Rows arrive newest-first, so the head of each group is the profile the
@@ -125,7 +125,19 @@ module Railwatch
125
125
  outcome = fields["outcome"].presence || fields["status"].presence
126
126
  scope = scope.where(queue: fields["queue"]) if fields["queue"].present?
127
127
  scope = scope.where(outcome: outcome) if outcome
128
- scope = scope.where(name: fields["class"]) if fields["class"].present?
128
+ # A job's group is the digest of its class name, and group_hash is
129
+ # indexed with occurred_at; name is not, so class: alone walked every
130
+ # job in the window (49 s for a week through MCP). Not every group
131
+ # follows that rule -- Solid Queue's pruned attempts are named
132
+ # "(pruned)" under the SolidQueue::Pruned group -- so the window's
133
+ # rollups supply any other group recorded under the name, and the last
134
+ # FRESH_NAMES, which rollups have not caught up with, match by name.
135
+ if fields["class"].present?
136
+ name = fields["class"]
137
+ groups = [ Record.group_hash(name) ] |
138
+ Telemetry::Rollup.for_type("job_attempt").between(range.begin, range.end).where(name: name).distinct.pluck(:group_hash)
139
+ scope = scope.merge(Telemetry::Execution.in_groups_or_fresh("job_attempt", groups, range)).where(name: name)
140
+ end
129
141
  scope = scope.where(job_id: fields["job_id"]) if fields["job_id"].present?
130
142
  return scope unless text.present?
131
143
  scope.merge(Telemetry::Execution.named_like("job_attempt", text, range, previews: true))
@@ -61,6 +61,19 @@ module Railwatch
61
61
  .where(ids.map { |branch| "#{quoted_table_name}.id IN (#{branch.select(:id).to_sql})" }.join(" OR "))
62
62
  end
63
63
 
64
+ # Executions of `kind` in `range` that are in one of `groups`, or recent
65
+ # enough (FRESH_NAMES) that their rollup may not exist yet. Each branch
66
+ # names its index and the outer lookup is by rowid, as in named_like:
67
+ # an OR over the two conditions made SQLite walk the whole window.
68
+ def self.in_groups_or_fresh(kind, groups, range)
69
+ by_group = indexed_by("index_executions_on_group_hash_and_occurred_at").where(group_hash: groups, occurred_at: range, kind: kind)
70
+ fresh = where(kind: kind, occurred_at: [ range.begin, range.end - FRESH_NAMES ].max..range.end)
71
+ return where(kind: kind, occurred_at: range).where(group_hash: groups).or(fresh) unless TelemetryRecord.sqlite?
72
+
73
+ from("#{quoted_table_name} NOT INDEXED").where(kind: kind, occurred_at: range)
74
+ .where([ by_group, fresh ].map { |branch| "#{quoted_table_name}.id IN (#{branch.select(:id).to_sql})" }.join(" OR "))
75
+ end
76
+
64
77
  def self.indexed_by(index) = unscoped.from("#{quoted_table_name} INDEXED BY #{index}")
65
78
  private_class_method :indexed_by
66
79
 
@@ -74,14 +87,17 @@ module Railwatch
74
87
  KINDS.flat_map { |kind| where(kind: kind, occurred_at: time..).distinct.pluck(:server) }.uniq
75
88
  end
76
89
 
77
- # The schedule string each task key last ran with, in one query: the
78
- # newest scheduled_task row per key carries it in detail. The scheduled
79
- # tasks page and CheckScheduledTasksJob both used to look it up per key.
90
+ # The schedule string each task key last ran with: the newest
91
+ # scheduled_task row per key carries it in detail. A task's runs share
92
+ # its group (the digest of its key), so that row is the last entry of
93
+ # its group_hash index -- one seek per key, a few dozen keys. A single
94
+ # MAX(id) GROUP BY task_key read every scheduled run ever kept instead:
95
+ # 10 s on the platform's own tenant.
80
96
  def self.latest_schedules(keys)
81
- return {} if keys.empty?
82
- latest = scheduled.where(task_key: keys).group(:task_key).select(:task_key, Arel.sql("MAX(id) AS id"))
83
- scheduled.where(id: latest.map(&:id))
84
- .pluck(:task_key, Arel.sql("json_extract(detail, '$.schedule')")).to_h
97
+ keys.index_with do |key|
98
+ scheduled.where(group_hash: Record.group_hash(key), task_key: key).order(occurred_at: :desc)
99
+ .pick(Arel.sql("json_extract(detail, '$.schedule')"))
100
+ end.compact
85
101
  end
86
102
 
87
103
  CHILDREN = {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Railwatch
4
- VERSION = "0.8.1"
4
+ VERSION = "0.8.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: railwatch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.8.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cole Robertson