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 +4 -4
- data/CHANGELOG.md +35 -0
- data/lib/tj_scale_ruby/configuration.rb +11 -0
- data/lib/tj_scale_ruby/job_backends/delayed_job.rb +4 -1
- data/lib/tj_scale_ruby/models/tj_scale_ruby.rb +1 -1
- data/lib/tj_scale_ruby/rack_queue_time_middleware.rb +15 -2
- data/lib/tj_scale_ruby/version.rb +1 -1
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a15d2cfe793359d5678938c3f6cf974f496ac716b173acbf4f9b408668b88da4
|
|
4
|
+
data.tar.gz: '0490a176f40d292a3868a135ddd8ecdddfad30025a5d7458163c7159f2dbe144'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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?
|
|
@@ -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?
|
|
39
|
+
return nil if raw.nil?
|
|
35
40
|
|
|
36
|
-
|
|
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
|
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.
|
|
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-
|
|
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
|