tj-scale 1.1.0 → 1.1.1

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: 76a39f424b8bf07fbe1cd0812fa63833e403ce9c5983639e6f2644db2700b798
4
- data.tar.gz: 78b517bd5a20e91d617dfbeddea44d085e1808442c24840034252b2140af286f
3
+ metadata.gz: a15d2cfe793359d5678938c3f6cf974f496ac716b173acbf4f9b408668b88da4
4
+ data.tar.gz: '0490a176f40d292a3868a135ddd8ecdddfad30025a5d7458163c7159f2dbe144'
5
5
  SHA512:
6
- metadata.gz: 8995d5a220b907b77126f766c9f2c7b382cd445c8bd4f3da3b20509e072ef26e84092ee16462a0218693c211d44ad3f65769a10b421c639bdf0d02386a844a59
7
- data.tar.gz: d534b2ceb99df75e41b4ee9b2495e00d5e0a105e2cdf9d3c579ef6202242d8ec953a1f71d8026a9f06359d6a03ba8c16dbc4123ac4dde9768dd34cb685d4a686
6
+ metadata.gz: d5978c29aeaac97ade3368b4c693ab5d445c9c43716490d8299fe22c2785a0014b335df040c3836db385de4f28093aa4c74b0abe2642f731eeb4295e69f4bbbc
7
+ data.tar.gz: 4e3ff1c12ff935b3f211c9698a718e54f1e5b41501dcf565cbdc8a64c08f7df1e50258cb4444b029040f117fce5cac9385e36a995dbe8b714c8f684baf6b52fb
data/CHANGELOG.md CHANGED
@@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+
11
+ ## [1.1.1] (tj-scale) - 2026-07-20
12
+
13
+ ### Fixed
14
+
15
+ - **Web `queue_time_ms` is now captured on Heroku.** The Rack queue-time
16
+ middleware only parsed the nginx-style `X-Request-Start: t=<seconds>` form, but
17
+ Heroku's router sends a **bare millisecond epoch** (e.g. `1731523200000`). The
18
+ header is now parsed in both formats, so `queue_time_ms` is actually produced on
19
+ Heroku when `TJ_SCALE_ENABLE_QUEUE_TIME_MIDDLEWARE=1` (previously always `nil`).
20
+ - **Stale `queue_time_ms` is no longer re-reported.** The captured router
21
+ queue-time snapshot is now consumed each ingest tick, so after web traffic stops
22
+ the gem stops resending the last elevated value (which kept the web dyno scaled
23
+ up and blocked downscaling on the `queue_time_ms` metric).
24
+ - **Delayed Job counting no longer depends on PostgreSQL.** The runnable-jobs
25
+ query used a Postgres-only `interval '5 seconds'` SQL literal that raises on
26
+ MySQL/SQLite; the exception was swallowed to `0`, silently masking the entire
27
+ worker backlog. The 5-second buffer is now computed in Ruby (`run_at < now - 5`),
28
+ so counting works on any Delayed Job-supported database.
29
+
30
+ ### Added
31
+
32
+ - Integration coverage for the Sidekiq backend against the real `Sidekiq::Queue`
33
+ API and a live Redis, alongside the existing double-based specs — those only
34
+ asserted our assumptions about `size` and `latency`, so an upstream API change
35
+ would have surfaced as broken autoscaling in a host app rather than a red test.
36
+ `sidekiq` is now a development dependency. The specs use
37
+ `TJ_SCALE_TEST_REDIS_URL` (default `redis://localhost:6379/15`), `FLUSHDB`
38
+ between examples, and skip themselves when no Redis is reachable, so
39
+ `rake spec` still passes without one.
40
+ - An explicit test that enqueued counts exclude the retry and scheduled sets,
41
+ pinning that as a deliberate limitation: a backlog waiting in `retry` or
42
+ `schedule` does not by itself scale workers up.
43
+
44
+
10
45
  ## [1.1.0] (tj-scale) - 2026-06-12
11
46
 
12
47
  ### Changed
@@ -111,6 +111,17 @@ module TjScaleRuby
111
111
  web_queue_mutex.synchronize { @web_queue_time_ms }
112
112
  end
113
113
 
114
+ # Reads and clears the latest captured router queue time (ms). Consuming it each
115
+ # ingest tick prevents a stale elevated value from being re-reported (and keeping
116
+ # the web dyno scaled up) after traffic stops. Thread-safe.
117
+ def flush_web_queue_time_ms!
118
+ web_queue_mutex.synchronize do
119
+ v = @web_queue_time_ms
120
+ @web_queue_time_ms = nil
121
+ v
122
+ end
123
+ end
124
+
114
125
  def clear_web_queue_time_snapshot!
115
126
  web_queue_mutex.synchronize { @web_queue_time_ms = nil }
116
127
  end
@@ -16,7 +16,10 @@ module TjScaleRuby
16
16
  min_priority = TjScaleRuby.configuration.min_priority
17
17
  max_priority = TjScaleRuby.configuration.max_priority
18
18
 
19
- query = ::Delayed::Job.where("run_at + interval '5 seconds' < ?", now)
19
+ # 5-second buffer past run_at. Compute the cutoff in Ruby (run_at < now - 5s)
20
+ # instead of a SQL "interval" literal, which is PostgreSQL-only and would raise
21
+ # on MySQL/SQLite — silently masking the whole backlog as 0.
22
+ query = ::Delayed::Job.where("run_at < ?", now - 5)
20
23
  query = query.where("failed_at IS NULL")
21
24
  query = query.where("locked_at IS NULL")
22
25
  query = query.where("priority >= ?", min_priority) unless min_priority.zero?
@@ -80,7 +80,7 @@ module TjScaleRuby
80
80
  private
81
81
 
82
82
  def web_queue_time_ms_for_payload
83
- snap = TjScaleRuby.last_web_queue_time_ms
83
+ snap = TjScaleRuby.flush_web_queue_time_ms!
84
84
  return snap unless snap.nil?
85
85
 
86
86
  env = ENV["TJ_SCALE_QUEUE_TIME_MS"]
@@ -29,14 +29,27 @@ module TjScaleRuby
29
29
  [now_ms - start_ms, 0].max
30
30
  end
31
31
 
32
+ # Heroku's router sends a bare unix-millisecond integer ("1751376000123"); nginx
33
+ # sends a "t=" prefixed value in seconds ("t=1751376000.123"). Match the prefixed
34
+ # form first, then fall back to a bare leading number, so the primary deployment
35
+ # target (Heroku) is not silently unparsed — a nil here means +queue_time_ms+ is
36
+ # never reported and web autoscaling on that metric can never fire.
32
37
  def self.request_start_epoch_ms(env)
33
38
  raw = env["HTTP_X_REQUEST_START"]
34
- return nil if raw.nil? || raw.strip.empty?
39
+ return nil if raw.nil?
35
40
 
36
- m = raw.to_s.match(/t=(\d+(?:\.\d+)?)/i)
41
+ s = raw.to_s.strip
42
+ return nil if s.empty?
43
+
44
+ # Heroku's router sends a bare millisecond epoch ("1731523200000"); nginx and
45
+ # some proxies send "t=<seconds>[.<frac>]". Accept both — prefer the +t=+ capture
46
+ # when present, otherwise fall back to the leading numeric timestamp.
47
+ m = s.match(/t=(\d+(?:\.\d+)?)/i) || s.match(/(\d+(?:\.\d+)?)/)
37
48
  return nil unless m
38
49
 
39
50
  v = m[1].to_f
51
+ return nil unless v.positive?
52
+
40
53
  v *= 1000 if v < 1_000_000_000_000 # seconds (or fractional) -> ms
41
54
  v.round
42
55
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Version module for TjScaleRuby gem
4
4
  module TjScaleRuby
5
- VERSION = "1.1.0"
5
+ VERSION = "1.1.1"
6
6
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tj-scale
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tanuj
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-06-12 00:00:00.000000000 Z
10
+ date: 2026-07-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -57,6 +57,20 @@ dependencies:
57
57
  - - "~>"
58
58
  - !ruby/object:Gem::Version
59
59
  version: '4.1'
60
+ - !ruby/object:Gem::Dependency
61
+ name: sidekiq
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '7.3'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '7.3'
60
74
  - !ruby/object:Gem::Dependency
61
75
  name: bundler
62
76
  requirement: !ruby/object:Gem::Requirement