railwatch 0.8.2 → 0.8.4

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: d3e41b1890c573cc71fe35d7a9ac134dc3964e4250126bd7c898132800ba47d9
4
- data.tar.gz: 84f59bb0bc68c014618c564533b0f3a5266062b1fac93a252a5a41c3632f0136
3
+ metadata.gz: f52b89a567e0ac0a05281db9b77499961ee7abf9cbfbf663070600d8885818a3
4
+ data.tar.gz: 05cdb2e7d19bf73edb2dd80550b1a59bf39c83ad24b951c7f7d43b3ddee9a850
5
5
  SHA512:
6
- metadata.gz: 4472da6ecdbb054cd17cae2a31cf3d1f0a5a6237e4964af2c9da04fa2b754a95a7aa91c268dd43ef9a29791762b82c2edffb446557b0f2b6b5a3a47f6b504a8c
7
- data.tar.gz: 276639c1d1f4c9632892a419f83443407a41af5e33c062a426e50bf9a3f5fa8daf039b267157c14079e2c826dbd5b025e0a2e955d43450d28b59be31637fb6b4
6
+ metadata.gz: d3d4c4f9698de7d6f522d4c171962e959efccd4435ff07e036505ced415aaa43e4b134df949f18c61952c4b22738d7074f49afcc1209cf1088fe41ea6cae564b
7
+ data.tar.gz: 8f4c96092fc3a62f3538667c4d4f2e40de420e9f2ccacf7305b0b4b4fd99ca9b098cb830cced5b9def8a618d3d1e16e24e478d4461f2599e0047121b79e2df2e
data/CHANGELOG.md CHANGED
@@ -6,6 +6,24 @@
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.4 (2026-09-23)
10
+
11
+ - A `class:` filter for a job class with many attempts in the window
12
+ walks the window again, where the newest page comes first. 0.8.3
13
+ gathered and sorted every attempt of the class through its group:
14
+ 10 s for the platform's 93,000 RollupJob attempts over 30 days, 2 ms by
15
+ walking.
16
+
17
+ ## 0.8.3 (2026-09-23)
18
+
19
+ - Filtering jobs by `class:` seeks the class's group (a job's group is
20
+ the digest of its class name) instead of walking every job in the
21
+ window: 715 ms over a week on the platform's own tenant, now 9 ms, and
22
+ one MCP search took 49 s.
23
+ - The Scheduled tasks page and the missed-task check read each task's
24
+ newest schedule with one index seek per key. The single grouped query
25
+ they used read every scheduled run kept (10 s once, 185 ms warm).
26
+
9
27
  ## 0.8.2 (2026-09-23)
10
28
 
11
29
  - The Profiles page's profiled share counts only the executions inside
@@ -125,7 +125,28 @@ 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
+ #
136
+ # A class with many attempts in the window is the opposite case: the
137
+ # newest page is in the first rows of the walk (2 ms), while the seek
138
+ # gathers and sorts every attempt (10 s for 93,000 RollupJobs over 30
139
+ # days). The rollups say how many there are, so a common class walks.
140
+ if fields["class"].present?
141
+ name = fields["class"]
142
+ rollups = Telemetry::Rollup.for_type("job_attempt").between(range.begin, range.end).where(name: name)
143
+ scope = if rollups.sum(:count) >= Telemetry::Execution::DENSE_MATCHES
144
+ scope.where(name: name)
145
+ else
146
+ groups = [ Record.group_hash(name) ] | rollups.distinct.pluck(:group_hash)
147
+ scope.merge(Telemetry::Execution.in_groups_or_fresh("job_attempt", groups, range)).where(name: name)
148
+ end
149
+ end
129
150
  scope = scope.where(job_id: fields["job_id"]) if fields["job_id"].present?
130
151
  return scope unless text.present?
131
152
  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.2"
4
+ VERSION = "0.8.4"
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.2
4
+ version: 0.8.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cole Robertson