jobtick 0.1.3 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -1
- data/README.md +71 -17
- data/lib/jobtick/client.rb +11 -31
- data/lib/jobtick/configuration.rb +9 -1
- data/lib/jobtick/dispatcher.rb +193 -0
- data/lib/jobtick/hooks/active_job.rb +1 -1
- data/lib/jobtick/middleware/sidekiq.rb +1 -1
- data/lib/jobtick/monitor.rb +10 -6
- data/lib/jobtick/parsers/sidekiq.rb +4 -1
- data/lib/jobtick/parsers/solid_queue.rb +3 -3
- data/lib/jobtick/railtie.rb +7 -0
- data/lib/jobtick/registry.rb +6 -4
- data/lib/jobtick/version.rb +1 -1
- data/lib/jobtick/whenever_setup.rb +38 -0
- data/lib/jobtick.rb +5 -8
- data/lib/tasks/jobtick.rake +15 -16
- metadata +3 -2
- data/jobtick-0.0.1.gem +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: abfce9cfddae0a798d132217b9376b130afe9446f85e279d3241e7016ed1a89f
|
|
4
|
+
data.tar.gz: 36590511cdf6e5b07541cdb66589fc5efd7f2250aeb4d5e5e4b20a18976642de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a81d1bf56a1e48ed5395bd5d913b8f49aafa163095261e7f066182f138c71f00259d7e428c5ee5a4e685a4320464aecd66aedde77a1f4c41cbb8431491999a39
|
|
7
|
+
data.tar.gz: f1fc3adfece35b9253e59be4beab12c5b2e1292c8330f287a386f37a71715d9cda922ca216a5219052e7097e1db13404b23c8118a10203fdbbf4024f37651e51
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [0.2.0] - 2026-05-28
|
|
2
|
+
|
|
3
|
+
- Performance: pings are now dispatched asynchronously on a single daemon thread, so job workers no longer block on network I/O. A persistent, keep-alive HTTPS connection is reused for all pings (no more TCP/TLS handshake per ping).
|
|
4
|
+
- Performance: switch to `Process.clock_gettime(CLOCK_MONOTONIC)` for duration measurement — no `Time` object allocation per job, and immune to wall-clock jumps.
|
|
5
|
+
- Performance: lazy-load parsers, hooks, middleware, and the registry — only Rails boots that have JobTick enabled pay for them.
|
|
6
|
+
- Performance: the monitor map is frozen after sync, and parser allocations are trimmed on the boot path.
|
|
7
|
+
- Add `Configuration#queue_limit` (default 1000) to bound the background ping queue; over-limit pings are dropped non-blockingly rather than back-pressuring the job thread.
|
|
8
|
+
|
|
9
|
+
### Measured impact
|
|
10
|
+
|
|
11
|
+
Benchmarked with `spec/benchmarks/monitor_bench.rb` (10,000 iterations, WebMock-stubbed endpoint so the numbers reflect gem-internal overhead, not real network latency):
|
|
12
|
+
|
|
13
|
+
| Metric (per monitored job) | v0.1.4 | v0.2.0 | Change |
|
|
14
|
+
|---|---:|---:|---:|
|
|
15
|
+
| Job-thread blocking time | 400.6 µs | 2.0 µs | **~200× faster** |
|
|
16
|
+
| Object allocations on job thread | 2,390 | 9 | **~265× fewer** |
|
|
17
|
+
| End-to-end CPU time (incl. background dispatch) | 400.6 µs | 18.1 µs | **~22× less CPU** |
|
|
18
|
+
|
|
19
|
+
In production, where each ping pays real network RTT, the job-thread speedup is significantly larger: a single 20 ms RTT × 2–3 pings per job is ~50 ms blocking under v0.1.4, versus ~2 µs under v0.2.0 (~25,000× on the worker thread). Run `bundle exec ruby spec/benchmarks/monitor_bench.rb [iterations]` to reproduce.
|
|
20
|
+
|
|
21
|
+
## [0.1.4] - 2026-05-05
|
|
22
|
+
|
|
23
|
+
- Add `prune` configuration option — when enabled, monitors absent from the latest sync payload are permanently deleted, keeping the dashboard in sync with your schedule config
|
|
24
|
+
- Remove stale `.gem` build artifact from repository
|
|
25
|
+
|
|
26
|
+
## [0.1.3] - 2026-05-05
|
|
27
|
+
|
|
28
|
+
- Add `JobTick::WheneverSetup.install!(self)` — one-line setup that overrides Whenever's built-in `runner`, `rake`, and `command` job types to send heartbeat pings automatically, with no per-job changes required
|
|
29
|
+
- Extract shared `Parsers.slugify` helper, remove redundant `.to_s` calls, unify guard style
|
|
30
|
+
|
|
31
|
+
## [0.1.2] - 2026-04-30
|
|
32
|
+
|
|
33
|
+
- Add automatic ping instrumentation for Solid Queue (via `ActiveJob::Base` `around_perform` hook) and Sidekiq (via server middleware)
|
|
34
|
+
|
|
35
|
+
## [0.1.1] - 2026-04-28
|
|
36
|
+
|
|
37
|
+
- Send `app_name` on monitor sync
|
|
2
38
|
|
|
3
39
|
## [0.1.0] - 2026-04-27
|
|
4
40
|
|
data/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# JobTick
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/jobtick)
|
|
3
4
|
[](https://github.com/clearstack-labs/jobtick/actions/workflows/main.yml)
|
|
4
5
|
|
|
5
6
|
**Rails job monitoring for Whenever, Solid Queue, and Sidekiq.**
|
|
@@ -57,6 +58,51 @@ That's it. On next deploy, JobTick reads your schedule config, registers a monit
|
|
|
57
58
|
|
|
58
59
|
No changes to individual job files. No manual monitor creation. No names to keep in sync.
|
|
59
60
|
|
|
61
|
+
### Environments
|
|
62
|
+
|
|
63
|
+
**JobTick is only active in production by default.** In `development`, `staging`, or any other environment it silently does nothing — no pings are sent, no monitors are registered, no errors are raised. This means you can deploy the gem and configure it without worrying about local runs polluting your monitors or counting toward your plan.
|
|
64
|
+
|
|
65
|
+
If you want to enable JobTick in a non-production environment (e.g. to test your setup on staging before going live), opt in explicitly:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
# config/initializers/jobtick.rb
|
|
69
|
+
|
|
70
|
+
# Enable on staging only
|
|
71
|
+
JobTick.configure do |config|
|
|
72
|
+
config.api_key = ENV['JOBTICK_API_KEY']
|
|
73
|
+
config.enabled = Rails.env.production? || Rails.env.staging?
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# Enable everywhere — useful for a quick local smoke-test
|
|
79
|
+
JobTick.configure do |config|
|
|
80
|
+
config.api_key = ENV['JOBTICK_API_KEY']
|
|
81
|
+
config.enabled = true
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# Drive it from an env var so you can toggle without a deploy
|
|
87
|
+
JobTick.configure do |config|
|
|
88
|
+
config.api_key = ENV['JOBTICK_API_KEY']
|
|
89
|
+
config.enabled = ENV['JOBTICK_ENABLED'] == 'true'
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Removing stale monitors automatically
|
|
94
|
+
|
|
95
|
+
By default, monitors are only added — nothing is removed when you delete a job from your schedule. To have each deploy also clean up monitors that are no longer in your config, enable pruning:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
JobTick.configure do |config|
|
|
99
|
+
config.api_key = ENV['JOBTICK_API_KEY']
|
|
100
|
+
config.prune = true # remove monitors absent from the latest sync
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
With `prune` enabled, a deploy acts as the single source of truth: any monitor whose key is not present in the current payload is permanently deleted. You can also remove individual monitors manually from the JobTick dashboard at any time.
|
|
105
|
+
|
|
60
106
|
---
|
|
61
107
|
|
|
62
108
|
## What gets monitored
|
|
@@ -97,25 +143,34 @@ JobTick installs a server middleware that wraps every job execution. For native
|
|
|
97
143
|
|
|
98
144
|
### Whenever (`config/schedule.rb`)
|
|
99
145
|
|
|
100
|
-
Whenever schedules jobs as cron shell commands, so there is no Ruby hook point to instrument automatically.
|
|
146
|
+
Whenever schedules jobs as cron shell commands, so there is no Ruby hook point to instrument automatically. Add one line to `config/schedule.rb`:
|
|
101
147
|
|
|
102
|
-
```
|
|
103
|
-
|
|
148
|
+
```ruby
|
|
149
|
+
JobTick::WheneverSetup.install!(self)
|
|
104
150
|
```
|
|
105
151
|
|
|
106
|
-
This
|
|
152
|
+
This overrides the built-in `runner`, `rake`, and `command` job types to wrap every execution with `curl` pings. Your existing schedule entries need no changes:
|
|
107
153
|
|
|
108
154
|
```ruby
|
|
155
|
+
JobTick::WheneverSetup.install!(self)
|
|
156
|
+
|
|
109
157
|
every 1.day, at: '2:00 am' do
|
|
110
|
-
|
|
158
|
+
runner 'InvoiceJob.perform_later'
|
|
111
159
|
end
|
|
112
160
|
|
|
113
161
|
every :hour do
|
|
114
|
-
|
|
162
|
+
rake 'reports:sync'
|
|
115
163
|
end
|
|
116
164
|
```
|
|
117
165
|
|
|
118
|
-
|
|
166
|
+
After adding the line, run `whenever --update-crontab` as normal and all jobs will start sending heartbeats.
|
|
167
|
+
|
|
168
|
+
If jobtick is not already loaded via your Rails environment, require it first:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
require 'jobtick/whenever_setup'
|
|
172
|
+
JobTick::WheneverSetup.install!(self)
|
|
173
|
+
```
|
|
119
174
|
|
|
120
175
|
---
|
|
121
176
|
|
|
@@ -123,7 +178,7 @@ The job types wrap execution with `curl` pings to the JobTick API, so no changes
|
|
|
123
178
|
|
|
124
179
|
**Silent failure detection** — alerts when a job stops running entirely, not just when it raises an exception. The failure mode your error monitor misses.
|
|
125
180
|
|
|
126
|
-
**Auto-sync on deploy** — add a job to your schedule, it appears in your dashboard at next deploy.
|
|
181
|
+
**Auto-sync on deploy** — add a job to your schedule, it appears in your dashboard at next deploy. Enable `config.prune = true` to automatically retire monitors when jobs are removed from your schedule.
|
|
127
182
|
|
|
128
183
|
**Run history** — see every execution: start time, duration, exit status. Spot when a job starts getting slower before it becomes a problem.
|
|
129
184
|
|
|
@@ -137,17 +192,16 @@ The job types wrap execution with `curl` pings to the JobTick API, so no changes
|
|
|
137
192
|
|
|
138
193
|
- Ruby >= 3.3
|
|
139
194
|
- Rails >= 7.0
|
|
140
|
-
- One or more of:
|
|
141
|
-
|
|
142
|
-
---
|
|
195
|
+
- One or more of:
|
|
143
196
|
|
|
144
|
-
|
|
197
|
+
| Adapter | Supported versions |
|
|
198
|
+
|---|---|
|
|
199
|
+
| Solid Queue | >= 0.1 |
|
|
200
|
+
| Sidekiq | >= 6 |
|
|
201
|
+
| sidekiq-cron | >= 1.0 |
|
|
202
|
+
| Whenever | >= 0.10 |
|
|
145
203
|
|
|
146
|
-
|
|
147
|
-
> Sign up for early access at [jobtick.app](https://jobtick.app).
|
|
148
|
-
> Launching June 2026.
|
|
149
|
-
|
|
150
|
-
If you want to follow along or give early feedback, open an issue or watch the repo.
|
|
204
|
+
---
|
|
151
205
|
|
|
152
206
|
---
|
|
153
207
|
|
data/lib/jobtick/client.rb
CHANGED
|
@@ -1,52 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require "json"
|
|
5
|
-
require "uri"
|
|
3
|
+
require_relative "dispatcher"
|
|
6
4
|
|
|
7
5
|
module JobTick
|
|
8
6
|
class Client
|
|
9
|
-
|
|
7
|
+
PING_PREFIX = "/ping/"
|
|
8
|
+
SYNC_PATH = "/monitors/sync"
|
|
10
9
|
|
|
11
10
|
def ping(monitor_key, status:, duration: nil, message: nil)
|
|
12
|
-
|
|
13
|
-
return
|
|
11
|
+
config = JobTick.config
|
|
12
|
+
return unless config.enabled && !config.api_key.nil?
|
|
14
13
|
|
|
15
14
|
payload = { status: status }
|
|
16
15
|
payload[:duration] = duration.round(3) if duration
|
|
17
16
|
payload[:message] = message if message
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
Dispatcher.enqueue("#{PING_PREFIX}#{monitor_key}", payload)
|
|
20
19
|
end
|
|
21
20
|
|
|
22
|
-
def register(monitors, app_name: nil)
|
|
23
|
-
|
|
24
|
-
return
|
|
21
|
+
def register(monitors, app_name: nil, prune: false)
|
|
22
|
+
config = JobTick.config
|
|
23
|
+
return unless config.enabled && !config.api_key.nil?
|
|
25
24
|
|
|
26
25
|
payload = { monitors: monitors }
|
|
27
26
|
payload[:app_name] = app_name if app_name && !app_name.empty?
|
|
28
|
-
|
|
29
|
-
end
|
|
27
|
+
payload[:prune] = true if prune
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def post(path, body)
|
|
34
|
-
uri = URI("#{JobTick.config.endpoint}#{path}")
|
|
35
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
36
|
-
http.use_ssl = uri.scheme == "https"
|
|
37
|
-
http.open_timeout = TIMEOUT
|
|
38
|
-
http.read_timeout = TIMEOUT
|
|
39
|
-
|
|
40
|
-
request = Net::HTTP::Post.new(uri)
|
|
41
|
-
request["Content-Type"] = "application/json"
|
|
42
|
-
request["Authorization"] = "Bearer #{JobTick.config.api_key}"
|
|
43
|
-
request["User-Agent"] = "jobtick-ruby/#{JobTick::VERSION}"
|
|
44
|
-
request.body = body.to_json
|
|
45
|
-
|
|
46
|
-
http.request(request)
|
|
47
|
-
rescue StandardError => e
|
|
48
|
-
JobTick.logger.warn("[JobTick] HTTP request failed (#{path}): #{e.message}")
|
|
49
|
-
nil
|
|
29
|
+
Dispatcher.send_sync(SYNC_PATH, payload)
|
|
50
30
|
end
|
|
51
31
|
end
|
|
52
32
|
end
|
|
@@ -2,12 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
module JobTick
|
|
4
4
|
class Configuration
|
|
5
|
-
|
|
5
|
+
DEFAULT_QUEUE_LIMIT = 1000
|
|
6
|
+
|
|
7
|
+
attr_accessor :api_key, :endpoint, :environment, :enabled, :prune, :queue_limit
|
|
6
8
|
|
|
7
9
|
def initialize
|
|
8
10
|
@endpoint = "https://api.jobtick.app/v1"
|
|
9
11
|
@environment = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "production"
|
|
10
12
|
@enabled = @environment == "production"
|
|
13
|
+
@prune = false
|
|
14
|
+
@queue_limit = DEFAULT_QUEUE_LIMIT
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def enabled?
|
|
18
|
+
@enabled && !@api_key.nil?
|
|
11
19
|
end
|
|
12
20
|
end
|
|
13
21
|
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "openssl"
|
|
7
|
+
|
|
8
|
+
module JobTick
|
|
9
|
+
# Asynchronous, single-threaded HTTP dispatcher with a persistent keep-alive
|
|
10
|
+
# connection. Job threads call .enqueue and return immediately; the dispatcher
|
|
11
|
+
# daemon thread drains the queue and posts to the JobTick API.
|
|
12
|
+
#
|
|
13
|
+
# All HTTP work (sync register + async pings) shares one Net::HTTP instance
|
|
14
|
+
# serialized by @http_mutex. The connection is reopened lazily after errors.
|
|
15
|
+
module Dispatcher
|
|
16
|
+
SHUTDOWN_SIGNAL = :__shutdown__
|
|
17
|
+
HEADER_CONTENT_TYPE = "application/json"
|
|
18
|
+
USER_AGENT = "jobtick-ruby/#{JobTick::VERSION}".freeze
|
|
19
|
+
OPEN_TIMEOUT = 5
|
|
20
|
+
READ_TIMEOUT = 5
|
|
21
|
+
KEEP_ALIVE_TIMEOUT = 30
|
|
22
|
+
|
|
23
|
+
NETWORK_ERRORS = [
|
|
24
|
+
IOError, EOFError,
|
|
25
|
+
Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ECONNABORTED,
|
|
26
|
+
Errno::EPIPE, Errno::ETIMEDOUT, Errno::EHOSTUNREACH,
|
|
27
|
+
Net::OpenTimeout, Net::ReadTimeout,
|
|
28
|
+
OpenSSL::SSL::SSLError, SocketError
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
attr_accessor :synchronous
|
|
33
|
+
|
|
34
|
+
def enqueue(path, payload)
|
|
35
|
+
return send_request(path, payload) if @synchronous
|
|
36
|
+
|
|
37
|
+
ensure_started
|
|
38
|
+
@queue.push([path, payload], true)
|
|
39
|
+
nil
|
|
40
|
+
rescue ThreadError
|
|
41
|
+
@dropped += 1
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def send_sync(path, payload)
|
|
46
|
+
send_request(path, payload)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def flush(timeout: 5)
|
|
50
|
+
return unless @running && @queue
|
|
51
|
+
|
|
52
|
+
deadline = monotonic + timeout
|
|
53
|
+
until @queue.empty? && @inflight.zero?
|
|
54
|
+
sleep 0.001
|
|
55
|
+
break if monotonic > deadline
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def shutdown(timeout: 2)
|
|
60
|
+
return unless @running
|
|
61
|
+
|
|
62
|
+
@running = false
|
|
63
|
+
@queue&.push(SHUTDOWN_SIGNAL)
|
|
64
|
+
@thread&.join(timeout)
|
|
65
|
+
close_http
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def dropped
|
|
70
|
+
@dropped || 0
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def reset!
|
|
74
|
+
shutdown(timeout: 1) if @running
|
|
75
|
+
@queue = nil
|
|
76
|
+
@thread = nil
|
|
77
|
+
@dropped = 0
|
|
78
|
+
@inflight = 0
|
|
79
|
+
@endpoint_uri = nil
|
|
80
|
+
@at_exit_registered = false
|
|
81
|
+
@synchronous = false
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def ensure_started
|
|
87
|
+
return if @running
|
|
88
|
+
|
|
89
|
+
boot_mutex.synchronize do
|
|
90
|
+
return if @running
|
|
91
|
+
|
|
92
|
+
@queue = SizedQueue.new(queue_limit)
|
|
93
|
+
@dropped = 0
|
|
94
|
+
@inflight = 0
|
|
95
|
+
@thread = Thread.new { run_loop }
|
|
96
|
+
@thread.name = "jobtick-dispatcher" if @thread.respond_to?(:name=)
|
|
97
|
+
@running = true
|
|
98
|
+
register_at_exit
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def boot_mutex
|
|
103
|
+
@boot_mutex ||= Mutex.new
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def http_mutex
|
|
107
|
+
@http_mutex ||= Mutex.new
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def queue_limit
|
|
111
|
+
JobTick.config.queue_limit || Configuration::DEFAULT_QUEUE_LIMIT
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def register_at_exit
|
|
115
|
+
return if @at_exit_registered
|
|
116
|
+
|
|
117
|
+
@at_exit_registered = true
|
|
118
|
+
at_exit { shutdown }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def run_loop
|
|
122
|
+
while (item = @queue.pop)
|
|
123
|
+
break if item == SHUTDOWN_SIGNAL
|
|
124
|
+
|
|
125
|
+
@inflight = 1
|
|
126
|
+
path, payload = item
|
|
127
|
+
send_request(path, payload)
|
|
128
|
+
@inflight = 0
|
|
129
|
+
end
|
|
130
|
+
rescue StandardError => e
|
|
131
|
+
JobTick.logger.warn("[JobTick] Dispatcher thread crashed: #{e.message}")
|
|
132
|
+
ensure
|
|
133
|
+
close_http
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def send_request(path, payload)
|
|
137
|
+
body = JSON.generate(payload)
|
|
138
|
+
full_path = "#{endpoint_uri.path}#{path}"
|
|
139
|
+
http_mutex.synchronize do
|
|
140
|
+
http = http_connection
|
|
141
|
+
request = Net::HTTP::Post.new(full_path)
|
|
142
|
+
request["Content-Type"] = HEADER_CONTENT_TYPE
|
|
143
|
+
request["Authorization"] = "Bearer #{JobTick.config.api_key}"
|
|
144
|
+
request["User-Agent"] = USER_AGENT
|
|
145
|
+
request.body = body
|
|
146
|
+
http.request(request)
|
|
147
|
+
end
|
|
148
|
+
rescue *NETWORK_ERRORS => e
|
|
149
|
+
JobTick.logger.warn("[JobTick] HTTP request failed (#{path}): #{e.message}")
|
|
150
|
+
teardown_http
|
|
151
|
+
nil
|
|
152
|
+
rescue StandardError => e
|
|
153
|
+
JobTick.logger.warn("[JobTick] HTTP request failed (#{path}): #{e.message}")
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def endpoint_uri
|
|
158
|
+
@endpoint_uri ||= URI(JobTick.config.endpoint)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def http_connection
|
|
162
|
+
return @http if @http&.started?
|
|
163
|
+
|
|
164
|
+
uri = endpoint_uri
|
|
165
|
+
@http = Net::HTTP.new(uri.host, uri.port)
|
|
166
|
+
@http.use_ssl = uri.scheme == "https"
|
|
167
|
+
@http.open_timeout = OPEN_TIMEOUT
|
|
168
|
+
@http.read_timeout = READ_TIMEOUT
|
|
169
|
+
@http.keep_alive_timeout = KEEP_ALIVE_TIMEOUT
|
|
170
|
+
@http.start
|
|
171
|
+
@http
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def teardown_http
|
|
175
|
+
return unless @http
|
|
176
|
+
|
|
177
|
+
@http.finish if @http.started?
|
|
178
|
+
rescue StandardError
|
|
179
|
+
nil
|
|
180
|
+
ensure
|
|
181
|
+
@http = nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def close_http
|
|
185
|
+
http_mutex.synchronize { teardown_http }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def monotonic
|
|
189
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
data/lib/jobtick/monitor.rb
CHANGED
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module JobTick
|
|
4
4
|
class Monitor
|
|
5
|
+
MONOTONIC = Process::CLOCK_MONOTONIC
|
|
6
|
+
|
|
5
7
|
def self.run(key)
|
|
6
|
-
|
|
8
|
+
config = JobTick.config
|
|
9
|
+
return yield unless config.enabled && !config.api_key.nil?
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
client = JobTick.client
|
|
12
|
+
client.ping(key, status: :started)
|
|
13
|
+
started = Process.clock_gettime(MONOTONIC)
|
|
14
|
+
result = yield
|
|
15
|
+
duration = Process.clock_gettime(MONOTONIC) - started
|
|
16
|
+
client.ping(key, status: :completed, duration: duration)
|
|
13
17
|
result
|
|
14
18
|
rescue StandardError => e
|
|
15
19
|
JobTick.client.ping(key, status: :failed, message: e.message)
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module JobTick
|
|
4
4
|
module Parsers
|
|
5
|
+
SLUG_RE = /[^a-z0-9]+/
|
|
6
|
+
SLUG_TRIM_RE = /\A_+|_+\z/
|
|
7
|
+
|
|
5
8
|
def self.slugify(str)
|
|
6
|
-
str.downcase.gsub(
|
|
9
|
+
str.downcase.gsub(SLUG_RE, "_").gsub(SLUG_TRIM_RE, "")
|
|
7
10
|
end
|
|
8
11
|
|
|
9
12
|
class Sidekiq
|
|
@@ -16,16 +16,16 @@ module JobTick
|
|
|
16
16
|
tasks = yaml[env] || yaml["default"] || yaml
|
|
17
17
|
return [] unless tasks.is_a?(Hash)
|
|
18
18
|
|
|
19
|
-
tasks.
|
|
19
|
+
tasks.each_with_object([]) do |(key, config), out|
|
|
20
20
|
next unless config.is_a?(Hash)
|
|
21
21
|
|
|
22
|
-
{
|
|
22
|
+
out << {
|
|
23
23
|
key: "solid_queue.#{key}",
|
|
24
24
|
schedule: config["schedule"],
|
|
25
25
|
source: "solid_queue",
|
|
26
26
|
task: config["class"]
|
|
27
27
|
}
|
|
28
|
-
end
|
|
28
|
+
end
|
|
29
29
|
rescue StandardError => e
|
|
30
30
|
JobTick.logger.warn("[JobTick] Solid Queue parser failed: #{e.message}")
|
|
31
31
|
[]
|
data/lib/jobtick/railtie.rb
CHANGED
|
@@ -6,6 +6,13 @@ module JobTick
|
|
|
6
6
|
ActiveSupport.on_load(:after_initialize) do
|
|
7
7
|
next unless JobTick.config.enabled
|
|
8
8
|
|
|
9
|
+
require_relative "parsers/whenever"
|
|
10
|
+
require_relative "parsers/solid_queue"
|
|
11
|
+
require_relative "parsers/sidekiq"
|
|
12
|
+
require_relative "registry"
|
|
13
|
+
require_relative "hooks/active_job"
|
|
14
|
+
require_relative "middleware/sidekiq"
|
|
15
|
+
|
|
9
16
|
JobTick::Registry.sync
|
|
10
17
|
|
|
11
18
|
::ActiveJob::Base.include(JobTick::Hooks::ActiveJob) if defined?(::ActiveJob::Base)
|
data/lib/jobtick/registry.rb
CHANGED
|
@@ -9,14 +9,16 @@ module JobTick
|
|
|
9
9
|
Parsers::Sidekiq.parse
|
|
10
10
|
].flatten.compact
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
map = {}
|
|
13
|
+
monitors.each { |m| map[m[:task]] = m[:key] if m[:task] }
|
|
14
|
+
JobTick.monitor_map = map.freeze
|
|
15
15
|
|
|
16
16
|
return [] if monitors.empty?
|
|
17
17
|
|
|
18
18
|
app_name = Rails.application.class.module_parent_name if defined?(Rails)
|
|
19
|
-
|
|
19
|
+
options = { app_name: app_name }
|
|
20
|
+
options[:prune] = true if JobTick.config.prune
|
|
21
|
+
JobTick.client.register(monitors, **options)
|
|
20
22
|
monitors
|
|
21
23
|
end
|
|
22
24
|
end
|
data/lib/jobtick/version.rb
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "jobtick"
|
|
4
|
+
|
|
5
|
+
module JobTick
|
|
6
|
+
# Overrides Whenever's built-in job types to wrap execution with jobtick pings.
|
|
7
|
+
#
|
|
8
|
+
# Usage — add one line to config/schedule.rb:
|
|
9
|
+
#
|
|
10
|
+
# JobTick::WheneverSetup.install!(self)
|
|
11
|
+
#
|
|
12
|
+
# This replaces the :runner, :rake, and :command job types so that every
|
|
13
|
+
# scheduled job automatically sends started/completed/failed heartbeats without
|
|
14
|
+
# any per-job configuration.
|
|
15
|
+
module WheneverSetup
|
|
16
|
+
def self.install!(schedule)
|
|
17
|
+
endpoint = JobTick.config.endpoint
|
|
18
|
+
|
|
19
|
+
schedule.job_type :runner, wrap(endpoint, "cd :path && bundle exec rails runner ':task' :output")
|
|
20
|
+
schedule.job_type :rake, wrap(endpoint, "cd :path && bundle exec rake :task :output")
|
|
21
|
+
schedule.job_type :command, wrap(endpoint, "cd :path && :task :output")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.wrap(endpoint, inner_cmd)
|
|
25
|
+
# Shell equivalent of Parsers.slugify: downcase, collapse non-alnum runs to _, strip leading/trailing _.
|
|
26
|
+
sed = "sed 's/[^a-z0-9][^a-z0-9]*/_/g' | sed 's/^_*//;s/_*$//'"
|
|
27
|
+
slug = "$(printf '%s' ':task' | tr '[:upper:]' '[:lower:]' | #{sed})"
|
|
28
|
+
key_assign = %(JOBTICK_KEY="whenever.#{slug}")
|
|
29
|
+
|
|
30
|
+
"#{key_assign} ; " \
|
|
31
|
+
"curl -sf \"#{endpoint}/ping/$JOBTICK_KEY/started\" ; " \
|
|
32
|
+
"#{inner_cmd} && " \
|
|
33
|
+
"curl -sf \"#{endpoint}/ping/$JOBTICK_KEY/completed\" || " \
|
|
34
|
+
"curl -sf \"#{endpoint}/ping/$JOBTICK_KEY/failed\""
|
|
35
|
+
end
|
|
36
|
+
private_class_method :wrap
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/jobtick.rb
CHANGED
|
@@ -5,15 +5,11 @@ require_relative "jobtick/version"
|
|
|
5
5
|
require_relative "jobtick/configuration"
|
|
6
6
|
require_relative "jobtick/client"
|
|
7
7
|
require_relative "jobtick/monitor"
|
|
8
|
-
require_relative "jobtick/parsers/whenever"
|
|
9
|
-
require_relative "jobtick/parsers/solid_queue"
|
|
10
|
-
require_relative "jobtick/parsers/sidekiq"
|
|
11
|
-
require_relative "jobtick/registry"
|
|
12
|
-
require_relative "jobtick/hooks/active_job"
|
|
13
|
-
require_relative "jobtick/middleware/sidekiq"
|
|
14
8
|
require_relative "jobtick/railtie" if defined?(Rails::Railtie)
|
|
15
9
|
|
|
16
10
|
module JobTick
|
|
11
|
+
EMPTY_MAP = {}.freeze
|
|
12
|
+
|
|
17
13
|
class Error < StandardError; end
|
|
18
14
|
|
|
19
15
|
class << self
|
|
@@ -34,7 +30,7 @@ module JobTick
|
|
|
34
30
|
end
|
|
35
31
|
|
|
36
32
|
def monitor_map
|
|
37
|
-
@monitor_map ||=
|
|
33
|
+
@monitor_map ||= EMPTY_MAP
|
|
38
34
|
end
|
|
39
35
|
|
|
40
36
|
attr_writer :monitor_map
|
|
@@ -44,9 +40,10 @@ module JobTick
|
|
|
44
40
|
end
|
|
45
41
|
|
|
46
42
|
def reset!
|
|
43
|
+
Dispatcher.reset! if defined?(Dispatcher)
|
|
47
44
|
@config = nil
|
|
48
45
|
@client = nil
|
|
49
|
-
@monitor_map =
|
|
46
|
+
@monitor_map = EMPTY_MAP
|
|
50
47
|
end
|
|
51
48
|
end
|
|
52
49
|
end
|
data/lib/tasks/jobtick.rake
CHANGED
|
@@ -3,32 +3,31 @@
|
|
|
3
3
|
namespace :jobtick do
|
|
4
4
|
desc "Sync discovered jobs with jobtick.app"
|
|
5
5
|
task sync: :environment do
|
|
6
|
+
require "jobtick/parsers/whenever"
|
|
7
|
+
require "jobtick/parsers/solid_queue"
|
|
8
|
+
require "jobtick/parsers/sidekiq"
|
|
9
|
+
require "jobtick/registry"
|
|
10
|
+
|
|
6
11
|
monitors = JobTick::Registry.sync
|
|
7
12
|
count = monitors&.length || 0
|
|
8
13
|
puts "[JobTick] Synced #{count} monitor(s)"
|
|
9
14
|
end
|
|
10
15
|
|
|
11
16
|
namespace :whenever do
|
|
12
|
-
desc "Print
|
|
17
|
+
desc "Print the line to add to config/schedule.rb to enable JobTick heartbeat injection"
|
|
13
18
|
task :setup do
|
|
14
|
-
endpoint = JobTick.config.endpoint
|
|
15
19
|
puts <<~RUBY
|
|
16
|
-
# Add to config/schedule.rb to enable JobTick heartbeat injection for Whenever jobs:
|
|
17
|
-
|
|
18
|
-
job_type :jobtick_runner, %(curl -sf "#{endpoint}/ping/:monitor_key/started" ; ) \\
|
|
19
|
-
%(bundle exec rails runner ':task' :output && ) \\
|
|
20
|
-
%(curl -sf "#{endpoint}/ping/:monitor_key/completed" || ) \\
|
|
21
|
-
%(curl -sf "#{endpoint}/ping/:monitor_key/failed")
|
|
20
|
+
# Add to config/schedule.rb to enable JobTick heartbeat injection for all Whenever jobs:
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
%(bundle exec rake :task :output && ) \\
|
|
25
|
-
%(curl -sf "#{endpoint}/ping/:monitor_key/completed" || ) \\
|
|
26
|
-
%(curl -sf "#{endpoint}/ping/:monitor_key/failed")
|
|
22
|
+
JobTick::WheneverSetup.install!(self)
|
|
27
23
|
|
|
28
|
-
#
|
|
29
|
-
# every
|
|
30
|
-
#
|
|
31
|
-
#
|
|
24
|
+
# This overrides the built-in runner, rake, and command job types so that
|
|
25
|
+
# every scheduled job automatically sends started/completed/failed heartbeats.
|
|
26
|
+
# No per-job changes are required.
|
|
27
|
+
#
|
|
28
|
+
# If jobtick is not already loaded via your Rails environment, add:
|
|
29
|
+
# require "jobtick/whenever_setup"
|
|
30
|
+
# before the install! call.
|
|
32
31
|
RUBY
|
|
33
32
|
end
|
|
34
33
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jobtick
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Clearstack Labs
|
|
@@ -35,10 +35,10 @@ files:
|
|
|
35
35
|
- LICENSE.txt
|
|
36
36
|
- README.md
|
|
37
37
|
- Rakefile
|
|
38
|
-
- jobtick-0.0.1.gem
|
|
39
38
|
- lib/jobtick.rb
|
|
40
39
|
- lib/jobtick/client.rb
|
|
41
40
|
- lib/jobtick/configuration.rb
|
|
41
|
+
- lib/jobtick/dispatcher.rb
|
|
42
42
|
- lib/jobtick/hooks/active_job.rb
|
|
43
43
|
- lib/jobtick/middleware/sidekiq.rb
|
|
44
44
|
- lib/jobtick/monitor.rb
|
|
@@ -48,6 +48,7 @@ files:
|
|
|
48
48
|
- lib/jobtick/railtie.rb
|
|
49
49
|
- lib/jobtick/registry.rb
|
|
50
50
|
- lib/jobtick/version.rb
|
|
51
|
+
- lib/jobtick/whenever_setup.rb
|
|
51
52
|
- lib/tasks/jobtick.rake
|
|
52
53
|
- sig/jobtick.rbs
|
|
53
54
|
homepage: https://jobtick.app
|
data/jobtick-0.0.1.gem
DELETED
|
Binary file
|