active_job_tracker 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +273 -0
- data/Rakefile +8 -0
- data/app/assets/stylesheets/active_job_tracker/style.css +197 -0
- data/app/helpers/active_job_tracker/records_helper.rb +11 -0
- data/app/models/active_job_tracker_record.rb +99 -0
- data/app/views/active_job_tracker/_active_job_tracker.html.erb +23 -0
- data/app/views/active_job_tracker/_active_job_tracker_wrapper.html.erb +13 -0
- data/lib/active_job_tracker/configuration.rb +35 -0
- data/lib/active_job_tracker/engine.rb +16 -0
- data/lib/active_job_tracker/version.rb +3 -0
- data/lib/active_job_tracker.rb +81 -0
- data/lib/generators/active_job_tracker/initializer_generator.rb +15 -0
- data/lib/generators/active_job_tracker/migrations_generator.rb +21 -0
- data/lib/generators/active_job_tracker/templates/config/initializers/active_job_tracker.rb +25 -0
- data/lib/generators/active_job_tracker/templates/migrations/create_active_job_tracker_records.rb +21 -0
- data/lib/tasks/active_job_tracker_tasks.rake +4 -0
- metadata +155 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3102a3e7bcb824b3db71003baa5f401f0598178c503b1f5b4b49f7c3e580d835
|
4
|
+
data.tar.gz: e043943a466eb2d514a906acd8ae2b72c9fe0a294f0b080f0ddceb6e561affef
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f030c69148089178e8d0dd8693835af4407bb9d62ea789f08b3875fff399d282603eae68411e1eb10acff0f0d707c0e6470f53114df0534f369816c49ca3d460
|
7
|
+
data.tar.gz: 0146a953fee7b01f7e203eff001e974799f8e8ba364dcc79fa54a5a6b81eb6d13670354bd1fbb1dc59cf95b62d8b7b9c4b1bdb3f35103ba29be6414eae3cb7dc
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,273 @@
|
|
1
|
+
# ActiveJobTracker
|
2
|
+
|
3
|
+
ActiveJobTracker provides persisted, real-time tracking and monitoring of ActiveJob jobs in Ruby on Rails applications. It allows you to track job status, progress, and errors with a simple API and real-time UI updates via ActionCable.
|
4
|
+
|
5
|
+
<img width="796" alt="Screenshot 2025-03-04 at 1 09 38 PM" src="https://github.com/user-attachments/assets/d34e6fb8-bb3c-4d71-a737-2f7597a23c43" />
|
6
|
+
|
7
|
+
## Features
|
8
|
+
|
9
|
+
- Track job status (pending, running, completed, failed)
|
10
|
+
- Monitor job progress with percentage completion
|
11
|
+
- Real-time UI updates via ActionCable
|
12
|
+
- Error tracking and reporting
|
13
|
+
- Efficient progress caching to minimize database updates
|
14
|
+
- Configurable options
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add this line to your application's Gemfile:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'active_job_tracker'
|
22
|
+
```
|
23
|
+
|
24
|
+
And then execute:
|
25
|
+
|
26
|
+
```bash
|
27
|
+
bundle install
|
28
|
+
```
|
29
|
+
|
30
|
+
After installation, run the generators to set up the gem:
|
31
|
+
|
32
|
+
```bash
|
33
|
+
# Create the necessary database migrations
|
34
|
+
rails generate active_job_tracker:migrations
|
35
|
+
|
36
|
+
# Run the migrations
|
37
|
+
rails db:migrate
|
38
|
+
|
39
|
+
# Generate the configuration initializer (optional)
|
40
|
+
rails generate active_job_tracker:initializer
|
41
|
+
```
|
42
|
+
|
43
|
+
## Configuration
|
44
|
+
|
45
|
+
You can configure ActiveJobTracker with the following options:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
# config/initializers/active_job_tracker.rb
|
49
|
+
ActiveJobTracker.configure do |config|
|
50
|
+
# Default target value for jobs (default: 100)
|
51
|
+
# This represents the total number of items to process in a job
|
52
|
+
config.default_target = 100
|
53
|
+
|
54
|
+
# Default cache threshold for progress updates (default: 10)
|
55
|
+
# Progress updates are batched until this threshold is reached to reduce database writes
|
56
|
+
config.cache_threshold = 10
|
57
|
+
|
58
|
+
# Whether to automatically broadcast changes (default: true)
|
59
|
+
# When true, job updates are automatically broadcast via ActionCable
|
60
|
+
config.auto_broadcast = true
|
61
|
+
|
62
|
+
# Default partial path for rendering job trackers
|
63
|
+
# (default: 'active_job_tracker/active_job_tracker')
|
64
|
+
config.default_partial = 'active_job_tracker/active_job_tracker'
|
65
|
+
|
66
|
+
# Whether to include the style in the job tracker (default: true)
|
67
|
+
# When true, the gem's CSS styles are automatically included
|
68
|
+
config.include_style = true
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
## Usage
|
73
|
+
|
74
|
+
### Basic Setup
|
75
|
+
|
76
|
+
Set up the model that creates jobs:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
class CsvUpload < ApplicationRecord
|
80
|
+
# Sets up polymorphic association to tie this record to the ActiveJobTracker
|
81
|
+
has_one :job, as: :active_job_trackable, class_name: 'ActiveJobTrackerRecord'
|
82
|
+
|
83
|
+
after_create :create_jobs
|
84
|
+
|
85
|
+
def create_jobs
|
86
|
+
# The tracked record must be passed into the job as the first argument
|
87
|
+
ProcessImportJob.perform_later(self)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
```
|
92
|
+
|
93
|
+
Include the `ActiveJobTracker` module in your job classes:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
class ProcessImportJob < ApplicationJob
|
97
|
+
include ActiveJobTracker
|
98
|
+
|
99
|
+
def perform(csv_upload)
|
100
|
+
# Your job logic here
|
101
|
+
end
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
This automatically tracks the job's status (pending, running, completed, failed) throughout its lifecycle.
|
106
|
+
|
107
|
+
### Tracking Progress
|
108
|
+
|
109
|
+
To track progress within your job:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
class ProcessImportJob < ApplicationJob
|
113
|
+
include ActiveJobTracker
|
114
|
+
|
115
|
+
def perform(file_path)
|
116
|
+
records = CSV.read(file_path)
|
117
|
+
|
118
|
+
# Set the target (here, total number of items to process)
|
119
|
+
# Defaults to 100 if unspecified
|
120
|
+
active_job_tracker_target(records.size)
|
121
|
+
|
122
|
+
records.each do |record|
|
123
|
+
# Process item
|
124
|
+
|
125
|
+
# Update progress (increments by 1)
|
126
|
+
active_job_tracker_progress
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
For more efficient progress tracking with many updates, use threadsafe progress caching:
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
# In your job
|
136
|
+
def perform
|
137
|
+
# You can override the cache threshold for when to flush progress updates to the database
|
138
|
+
active_job_tracker_cache_threshold(20)
|
139
|
+
active_job_tracker_target(records.size)
|
140
|
+
1000.times do |i|
|
141
|
+
# Process item
|
142
|
+
|
143
|
+
# This will only update the database every 20th increment
|
144
|
+
active_job_tracker_progress(cache: true)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
### Displaying Progress in Views
|
150
|
+
|
151
|
+
#### Basic Usage
|
152
|
+
|
153
|
+
To display job progress in your views:
|
154
|
+
|
155
|
+
```erb
|
156
|
+
<%= active_job_tracker_wrapper do %>
|
157
|
+
<% @csv_uploads.each do |csv_upload| %>
|
158
|
+
<% if (job = csv_upload.job) %>
|
159
|
+
<%= render partial: 'active_job_tracker/active_job_tracker', locals: { active_job_tracker_record: job } %>
|
160
|
+
<% end %>
|
161
|
+
<% end %>
|
162
|
+
<% end %>
|
163
|
+
```
|
164
|
+
|
165
|
+
This will render a default tracker UI with progress bar, status badge, and job information.
|
166
|
+
|
167
|
+
#### Custom Rendering
|
168
|
+
|
169
|
+
You can customize the tracker UI by creating your own partials and using the ActiveJobTrackerRecord model attributes:
|
170
|
+
- Make sure to set the `config.default_partial` to the new partial path
|
171
|
+
- Each job block needs to be wrapped with `id="active_job_tracker_<%= tracker.id %>"` for turbo to update your frontend
|
172
|
+
|
173
|
+
```erb
|
174
|
+
<%= active_job_tracker_wrapper(html_options: { class: 'custom-container' }) do %>
|
175
|
+
<% ActiveJobTrackerRecord.find_each do |tracker| %>
|
176
|
+
<div class="custom-tracker" id="active_job_tracker_<%= tracker.id %>">
|
177
|
+
<h3>Job #<%= tracker.id %></h3>
|
178
|
+
|
179
|
+
<div class="status">
|
180
|
+
Status: <span class="badge"><%= tracker.status %></span>
|
181
|
+
</div>
|
182
|
+
|
183
|
+
<div class="progress-bar">
|
184
|
+
<progress value="<%= tracker.current %>" max="<%= tracker.target %>"></progress>
|
185
|
+
<span><%= tracker.progress_percentage %>%</span>
|
186
|
+
</div>
|
187
|
+
|
188
|
+
<% if tracker.started_at.present? %>
|
189
|
+
<div class="timing">
|
190
|
+
Started: <%= tracker.started_at %>
|
191
|
+
<% if tracker.completed_at.present? %>
|
192
|
+
<br>Completed: <%= tracker.completed_at %>
|
193
|
+
<br>Duration: <%= tracker.duration %> seconds
|
194
|
+
<% end %>
|
195
|
+
</div>
|
196
|
+
<% end %>
|
197
|
+
|
198
|
+
<% if tracker.failed? && tracker.error.present? %>
|
199
|
+
<div class="error">
|
200
|
+
<h4>Error:</h4>
|
201
|
+
<p><%= tracker.error %></p>
|
202
|
+
<% if tracker.backtrace.present? %>
|
203
|
+
<pre><%= tracker.backtrace %></pre>
|
204
|
+
<% end %>
|
205
|
+
</div>
|
206
|
+
<% end %>
|
207
|
+
</div>
|
208
|
+
<% end %>
|
209
|
+
<% end %>
|
210
|
+
```
|
211
|
+
|
212
|
+
### Helper Methods
|
213
|
+
|
214
|
+
The gem provides the following helper method for displaying job trackers:
|
215
|
+
|
216
|
+
- `active_job_tracker_wrapper(options = {}, &block)` - Renders a wrapper for job trackers with Turbo Stream support
|
217
|
+
|
218
|
+
### Available Model Methods
|
219
|
+
|
220
|
+
The `ActiveJobTrackerRecord` model provides these useful methods:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
# Progress calculation
|
224
|
+
tracker.progress_ratio # => 0.75 (ratio between 0 and 1)
|
225
|
+
tracker.progress_percentage # => 75 (percentage between 0 and 100)
|
226
|
+
|
227
|
+
# Time tracking
|
228
|
+
tracker.duration # => 123.45 (seconds since started_at)
|
229
|
+
|
230
|
+
# Status methods (from enum)
|
231
|
+
tracker.pending? # => true/false
|
232
|
+
tracker.running? # => true/false
|
233
|
+
tracker.completed? # => true/false
|
234
|
+
tracker.failed? # => true/false
|
235
|
+
```
|
236
|
+
|
237
|
+
### Error Handling
|
238
|
+
|
239
|
+
Errors are automatically tracked when a job fails. The gem adds a rescue_from handler that logs the error details before re-raising the exception:
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
# This happens automatically when you include ActiveJobTracker
|
243
|
+
rescue_from(Exception) do |exception|
|
244
|
+
active_job_tracker_log_error(exception)
|
245
|
+
raise exception
|
246
|
+
end
|
247
|
+
```
|
248
|
+
|
249
|
+
To handle errors:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
def perform
|
253
|
+
begin
|
254
|
+
# Risky operation
|
255
|
+
rescue => e
|
256
|
+
# Handle the error
|
257
|
+
end
|
258
|
+
end
|
259
|
+
```
|
260
|
+
|
261
|
+
## Development
|
262
|
+
|
263
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
264
|
+
|
265
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
266
|
+
|
267
|
+
## Contributing
|
268
|
+
|
269
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/seenasabti/active_job_tracker.
|
270
|
+
|
271
|
+
## License
|
272
|
+
|
273
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
/* ActiveJob Tracker - Modern Styling */
|
2
|
+
.active_job_tracker {
|
3
|
+
--primary-color: #4f46e5;
|
4
|
+
--primary-light: #6366f1;
|
5
|
+
--completed-color: #059669;
|
6
|
+
--danger-color: #dc2626;
|
7
|
+
--danger-light: #ef4444;
|
8
|
+
--neutral-100: #f4f4f5;
|
9
|
+
--neutral-200: #e4e4e7;
|
10
|
+
--neutral-300: #d4d4d8;
|
11
|
+
--neutral-400: #a1a1aa;
|
12
|
+
--neutral-500: #71717a;
|
13
|
+
--neutral-600: #52525b;
|
14
|
+
--neutral-800: #27272a;
|
15
|
+
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
16
|
+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
17
|
+
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
18
|
+
width: 100%;
|
19
|
+
margin: 0.75rem auto;
|
20
|
+
background-color: white;
|
21
|
+
border-radius: 0.5rem;
|
22
|
+
overflow: hidden;
|
23
|
+
box-shadow: var(--shadow-sm);
|
24
|
+
border: 1px solid var(--neutral-200);
|
25
|
+
transition: all 0.2s ease;
|
26
|
+
}
|
27
|
+
|
28
|
+
.active_job_tracker progress::-webkit-progress-bar {
|
29
|
+
background-color: var(--neutral-200);
|
30
|
+
border-radius: 6px;
|
31
|
+
}
|
32
|
+
|
33
|
+
.active_job_tracker-default {
|
34
|
+
padding: 1rem;
|
35
|
+
}
|
36
|
+
|
37
|
+
.active_job_tracker-header {
|
38
|
+
display: flex;
|
39
|
+
align-items: center;
|
40
|
+
justify-content: space-between;
|
41
|
+
margin-bottom: 1rem;
|
42
|
+
flex-wrap: wrap;
|
43
|
+
gap: 0.5rem;
|
44
|
+
}
|
45
|
+
|
46
|
+
.active_job_tracker-status-badge {
|
47
|
+
padding: 0.375rem 0.75rem;
|
48
|
+
border-radius: 9999px;
|
49
|
+
font-size: 0.75rem;
|
50
|
+
font-weight: 600;
|
51
|
+
letter-spacing: 0.025em;
|
52
|
+
text-transform: uppercase;
|
53
|
+
display: inline-flex;
|
54
|
+
align-items: center;
|
55
|
+
box-shadow: var(--shadow-sm);
|
56
|
+
}
|
57
|
+
|
58
|
+
.active_job_tracker-title {
|
59
|
+
font-size: 1.125rem;
|
60
|
+
font-weight: 600;
|
61
|
+
display: inline-flex;
|
62
|
+
align-items: center;
|
63
|
+
color: var(--neutral-800);
|
64
|
+
}
|
65
|
+
|
66
|
+
.active_job_tracker-progress-wrapper {
|
67
|
+
display: flex;
|
68
|
+
align-items: center;
|
69
|
+
gap: 5px;
|
70
|
+
margin-bottom: 0.75rem;
|
71
|
+
}
|
72
|
+
|
73
|
+
.active_job_tracker-progress {
|
74
|
+
flex: 1;
|
75
|
+
height: 8px;
|
76
|
+
appearance: none;
|
77
|
+
border-radius: 4px;
|
78
|
+
overflow: hidden;
|
79
|
+
transition: all 0.4s ease-in-out;
|
80
|
+
}
|
81
|
+
|
82
|
+
.active_job_tracker-progress-label {
|
83
|
+
font-size: 0.875rem;
|
84
|
+
font-weight: 600;
|
85
|
+
color: var(--neutral-600);
|
86
|
+
width: 2rem;
|
87
|
+
text-align: right;
|
88
|
+
}
|
89
|
+
|
90
|
+
.active_job_tracker-body {
|
91
|
+
padding: 0.5rem 0;
|
92
|
+
}
|
93
|
+
|
94
|
+
.active_job_tracker-progress-container {
|
95
|
+
margin: 1rem 0;
|
96
|
+
transition: transform 0.2s ease;
|
97
|
+
}
|
98
|
+
|
99
|
+
/* Status Styles */
|
100
|
+
.active_job_tracker-status-pending .active_job_tracker-status-badge {
|
101
|
+
background-color: var(--neutral-500);
|
102
|
+
color: white;
|
103
|
+
}
|
104
|
+
|
105
|
+
.active_job_tracker-status-pending progress::-webkit-progress-value {
|
106
|
+
background: var(--neutral-400);
|
107
|
+
}
|
108
|
+
|
109
|
+
.active_job_tracker-status-pending progress::-moz-progress-bar {
|
110
|
+
background: var(--neutral-400);
|
111
|
+
}
|
112
|
+
|
113
|
+
.active_job_tracker-status-running .active_job_tracker-status-badge {
|
114
|
+
background-color: var(--primary-color);
|
115
|
+
color: white;
|
116
|
+
}
|
117
|
+
|
118
|
+
.active_job_tracker-status-running progress::-webkit-progress-value {
|
119
|
+
background: linear-gradient(
|
120
|
+
90deg,
|
121
|
+
var(--primary-light),
|
122
|
+
var(--primary-color)
|
123
|
+
);
|
124
|
+
background-size: 200% 100%;
|
125
|
+
}
|
126
|
+
|
127
|
+
.active_job_tracker-status-running progress::-moz-progress-bar {
|
128
|
+
background: linear-gradient(
|
129
|
+
90deg,
|
130
|
+
var(--primary-light),
|
131
|
+
var(--primary-color)
|
132
|
+
);
|
133
|
+
background-size: 200% 100%;
|
134
|
+
}
|
135
|
+
|
136
|
+
.active_job_tracker-status-completed .active_job_tracker-status-badge {
|
137
|
+
background-color: var(--completed-color);
|
138
|
+
color: white;
|
139
|
+
}
|
140
|
+
|
141
|
+
.active_job_tracker-status-completed progress::-webkit-progress-value {
|
142
|
+
background: var(--completed-color);
|
143
|
+
}
|
144
|
+
|
145
|
+
.active_job_tracker-status-completed progress::-moz-progress-bar {
|
146
|
+
background: var(--completed-color);
|
147
|
+
}
|
148
|
+
|
149
|
+
.active_job_tracker-status-failed .active_job_tracker-status-badge {
|
150
|
+
background-color: var(--danger-color);
|
151
|
+
color: white;
|
152
|
+
}
|
153
|
+
|
154
|
+
.active_job_tracker-status-failed progress::-webkit-progress-value {
|
155
|
+
background: var(--danger-color);
|
156
|
+
}
|
157
|
+
|
158
|
+
.active_job_tracker-status-failed progress::-moz-progress-bar {
|
159
|
+
background: var(--danger-color);
|
160
|
+
}
|
161
|
+
|
162
|
+
/* Responsive Styles */
|
163
|
+
@media (max-width: 768px) {
|
164
|
+
.active_job_tracker {
|
165
|
+
margin: 1rem 0;
|
166
|
+
}
|
167
|
+
|
168
|
+
.active_job_tracker-default {
|
169
|
+
padding: 1rem;
|
170
|
+
}
|
171
|
+
|
172
|
+
.active_job_tracker-header {
|
173
|
+
flex-direction: column;
|
174
|
+
align-items: flex-start;
|
175
|
+
gap: 0.75rem;
|
176
|
+
}
|
177
|
+
}
|
178
|
+
|
179
|
+
@media (max-width: 640px) {
|
180
|
+
.active_job_tracker-default {
|
181
|
+
padding: 0.875rem;
|
182
|
+
}
|
183
|
+
|
184
|
+
.active_job_tracker-progress-container {
|
185
|
+
padding: 0.5rem;
|
186
|
+
margin: 1rem 0;
|
187
|
+
}
|
188
|
+
|
189
|
+
.active_job_tracker-progress-wrapper {
|
190
|
+
gap: 0.75rem;
|
191
|
+
}
|
192
|
+
|
193
|
+
.active_job_tracker-error pre {
|
194
|
+
font-size: 0.75rem;
|
195
|
+
max-height: 12rem;
|
196
|
+
}
|
197
|
+
}
|
@@ -0,0 +1,99 @@
|
|
1
|
+
class ActiveJobTrackerRecord < ApplicationRecord
|
2
|
+
enum :status, { pending: 0, running: 1, completed: 2, failed: 3 }
|
3
|
+
validates :job_id, presence: true, uniqueness: true
|
4
|
+
belongs_to :active_job_trackable, polymorphic: true
|
5
|
+
|
6
|
+
after_update :broadcast_changes, if: -> { ActiveJobTracker.configuration.auto_broadcast }
|
7
|
+
|
8
|
+
# Class-level mutex for thread-safety
|
9
|
+
@@mutex = Mutex.new
|
10
|
+
|
11
|
+
def cache_threshold=(value)
|
12
|
+
@cache_threshold = value || ActiveJobTracker.configuration.cache_threshold
|
13
|
+
end
|
14
|
+
|
15
|
+
def cache_threshold
|
16
|
+
@cache_threshold ||= ActiveJobTracker.configuration.cache_threshold
|
17
|
+
end
|
18
|
+
|
19
|
+
def progress_cache
|
20
|
+
Rails.cache.fetch(progress_cache_key, expires_in: 1.week) { 0 }.to_i
|
21
|
+
end
|
22
|
+
|
23
|
+
def progress_cache=(value)
|
24
|
+
Rails.cache.write(progress_cache_key, value, expires_in: 1.week)
|
25
|
+
end
|
26
|
+
|
27
|
+
def progress_cache_key
|
28
|
+
"active_job_tracker:#{self.id}:progress_cache"
|
29
|
+
end
|
30
|
+
|
31
|
+
def progress_percentage
|
32
|
+
(progress_ratio * 100).to_i
|
33
|
+
end
|
34
|
+
|
35
|
+
def duration
|
36
|
+
return nil unless started_at
|
37
|
+
end_time = completed_at || failed_at || Time.current
|
38
|
+
(end_time - started_at).to_f
|
39
|
+
end
|
40
|
+
|
41
|
+
def progress_ratio
|
42
|
+
return 0.0 if target.to_i.zero?
|
43
|
+
[ current.to_f / target.to_f, 1.0 ].min
|
44
|
+
end
|
45
|
+
|
46
|
+
def progress(use_cache = true)
|
47
|
+
if use_cache
|
48
|
+
key = progress_cache_key
|
49
|
+
should_flush = false
|
50
|
+
|
51
|
+
@@mutex.synchronize do
|
52
|
+
current_value = Rails.cache.fetch(key, expires_in: 1.week) { 0 }.to_i
|
53
|
+
new_value = current_value + 1
|
54
|
+
Rails.cache.write(key, new_value, expires_in: 1.week)
|
55
|
+
|
56
|
+
should_flush = new_value >= self.cache_threshold
|
57
|
+
end
|
58
|
+
|
59
|
+
# Flush outside the mutex to avoid deadlocks
|
60
|
+
flush_progress_cache if should_flush
|
61
|
+
else
|
62
|
+
with_lock do
|
63
|
+
self.current += 1
|
64
|
+
save!
|
65
|
+
end
|
66
|
+
end
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
def flush_progress_cache
|
71
|
+
key = progress_cache_key
|
72
|
+
|
73
|
+
cache_value = 0
|
74
|
+
@@mutex.synchronize do
|
75
|
+
cache_value = Rails.cache.read(key).to_i
|
76
|
+
Rails.cache.delete(key)
|
77
|
+
end
|
78
|
+
|
79
|
+
if cache_value > 0
|
80
|
+
with_lock do
|
81
|
+
self.current += cache_value
|
82
|
+
save!
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def broadcast_changes
|
90
|
+
broadcast_replace_to(
|
91
|
+
"active_job_trackers",
|
92
|
+
target: "active_job_tracker_#{self.id}",
|
93
|
+
partial: ActiveJobTracker.configuration.default_partial,
|
94
|
+
locals: {
|
95
|
+
active_job_tracker_record: self
|
96
|
+
}
|
97
|
+
)
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<%
|
2
|
+
html_classes = %W[active_job_tracker active_job_tracker-status-#{active_job_tracker_record.status}]
|
3
|
+
id = "active_job_tracker_#{active_job_tracker_record.id}"
|
4
|
+
%>
|
5
|
+
|
6
|
+
<div id="<%= id %>" class="<%= html_classes.join(" ") %>">
|
7
|
+
<div class="active_job_tracker-default">
|
8
|
+
<div class="active_job_tracker-body">
|
9
|
+
<div class="active_job_tracker-progress-container">
|
10
|
+
<div class="active_job_tracker-header">
|
11
|
+
<span class="active_job_tracker-title">
|
12
|
+
Job #<%= active_job_tracker_record.id %>
|
13
|
+
</span>
|
14
|
+
<span class="active_job_tracker-status-badge"><%= active_job_tracker_record.status %></span>
|
15
|
+
</div>
|
16
|
+
<div class="active_job_tracker-progress-wrapper">
|
17
|
+
<progress class="active_job_tracker-progress" value="<%= active_job_tracker_record.current %>" max="<%= active_job_tracker_record.target %>"></progress>
|
18
|
+
<span class="active_job_tracker-progress-label"><%= active_job_tracker_record.progress_percentage %>%</span>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
</div>
|
23
|
+
</div>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<% if ActiveJobTracker.configuration.include_style %>
|
2
|
+
<%= stylesheet_link_tag "active_job_tracker/style", media: "all" %>
|
3
|
+
<% end %>
|
4
|
+
|
5
|
+
<%
|
6
|
+
html_options ||= {}
|
7
|
+
css_class = html_options[:class] || "active_job_tracker-container"
|
8
|
+
%>
|
9
|
+
<%= turbo_stream_from "active_job_trackers" %>
|
10
|
+
<div class="<%= css_class %>" data-controller="active_job_tracker-container">
|
11
|
+
<%= content %>
|
12
|
+
</div>
|
13
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJobTracker
|
4
|
+
# Configuration options for ActiveJobTracker
|
5
|
+
class Configuration
|
6
|
+
# Default target value for jobs
|
7
|
+
# @return [Integer]
|
8
|
+
attr_accessor :default_target
|
9
|
+
|
10
|
+
# Default cache threshold for progress updates
|
11
|
+
# @return [Integer]
|
12
|
+
attr_accessor :cache_threshold
|
13
|
+
|
14
|
+
# Whether to automatically broadcast changes
|
15
|
+
# @return [Boolean]
|
16
|
+
attr_accessor :auto_broadcast
|
17
|
+
|
18
|
+
# Default partial path for rendering job trackers
|
19
|
+
# @return [String]
|
20
|
+
attr_accessor :default_partial
|
21
|
+
|
22
|
+
# Whether to include the style in the job tracker
|
23
|
+
# @return [Boolean]
|
24
|
+
attr_accessor :include_style
|
25
|
+
|
26
|
+
# Initialize with default values
|
27
|
+
def initialize
|
28
|
+
@default_target = 100
|
29
|
+
@cache_threshold = 10
|
30
|
+
@auto_broadcast = true
|
31
|
+
@default_partial = "active_job_tracker/active_job_tracker"
|
32
|
+
@include_style = true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ActiveJobTracker
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace ActiveJobTracker
|
4
|
+
|
5
|
+
generators do
|
6
|
+
require "generators/active_job_tracker/initializer_generator"
|
7
|
+
require "generators/active_job_tracker/migrations_generator"
|
8
|
+
end
|
9
|
+
|
10
|
+
initializer "active_job_tracker.helpers" do
|
11
|
+
ActiveSupport.on_load(:action_controller_base) do
|
12
|
+
helper ActiveJobTracker::RecordsHelper
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require "active_job_tracker/version"
|
2
|
+
require "active_job_tracker/engine"
|
3
|
+
require "active_job_tracker/configuration"
|
4
|
+
|
5
|
+
module ActiveJobTracker
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class Error < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.configuration
|
12
|
+
@configuration ||= Configuration.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.configure
|
16
|
+
yield(configuration)
|
17
|
+
end
|
18
|
+
|
19
|
+
included do
|
20
|
+
before_enqueue :initialize_tracker
|
21
|
+
before_perform :mark_as_running
|
22
|
+
after_perform :mark_as_completed
|
23
|
+
|
24
|
+
rescue_from(Exception) do |exception|
|
25
|
+
active_job_tracker_log_error(exception)
|
26
|
+
raise exception
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def active_job_tracker_cache_threshold(value)
|
31
|
+
active_job_tracker.cache_threshold = value
|
32
|
+
end
|
33
|
+
|
34
|
+
def active_job_tracker_target(target)
|
35
|
+
active_job_tracker.update(target: target)
|
36
|
+
end
|
37
|
+
|
38
|
+
def active_job_tracker_progress(cache: false)
|
39
|
+
active_job_tracker.progress(cache)
|
40
|
+
end
|
41
|
+
|
42
|
+
def active_job_tracker
|
43
|
+
@active_job_tracker ||= ::ActiveJobTrackerRecord.find_or_create_by(job_id: job_id, active_job_trackable: trackable)
|
44
|
+
end
|
45
|
+
|
46
|
+
def trackable
|
47
|
+
raise ArgumentError, "Trackable object is required as the first argument." if arguments.empty?
|
48
|
+
arguments.first
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize_tracker
|
52
|
+
active_job_tracker.update(
|
53
|
+
status: "pending",
|
54
|
+
started_at: nil,
|
55
|
+
completed_at: nil,
|
56
|
+
target: ActiveJobTracker.configuration.default_target,
|
57
|
+
current: 0
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def mark_as_running
|
62
|
+
active_job_tracker.update(status: "running", started_at: Time.current)
|
63
|
+
end
|
64
|
+
|
65
|
+
def mark_as_completed
|
66
|
+
active_job_tracker.flush_progress_cache
|
67
|
+
if active_job_tracker.current == active_job_tracker.target
|
68
|
+
active_job_tracker.update(status: "completed", completed_at: Time.current)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def active_job_tracker_log_error(exception)
|
73
|
+
active_job_tracker.flush_progress_cache
|
74
|
+
active_job_tracker.update(
|
75
|
+
status: "failed",
|
76
|
+
failed_at: Time.current,
|
77
|
+
error: exception.message,
|
78
|
+
backtrace: exception.backtrace&.join("\n").to_s.truncate(1000)
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJobTracker
|
4
|
+
module Generators
|
5
|
+
class InitializerGenerator < Rails::Generators::Base
|
6
|
+
desc "Creates an initializer file for configuring ActiveJobTracker"
|
7
|
+
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
def copy_initializer
|
11
|
+
template "config/initializers/active_job_tracker.rb", "config/initializers/active_job_tracker.rb"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJobTracker
|
4
|
+
module Generators
|
5
|
+
class MigrationsGenerator < Rails::Generators::Base
|
6
|
+
desc "Creates migration files for ActiveJobTracker"
|
7
|
+
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
def copy_migrations
|
11
|
+
migration_files = Dir.glob(File.join(self.class.source_root, "migrations", "*.rb"))
|
12
|
+
migration_files.each do |file|
|
13
|
+
migration_filename = File.basename(file)
|
14
|
+
new_filename = "#{Time.now.utc.strftime("%Y%m%d%H%M%S")}_#{migration_filename}"
|
15
|
+
copy_file "migrations/#{migration_filename}", "db/migrate/#{new_filename}"
|
16
|
+
sleep(1) if migration_files.count > 1
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Configuration for ActiveJobTracker
|
4
|
+
# This file was generated by the active_job_tracker gem
|
5
|
+
ActiveJobTracker.configure do |config|
|
6
|
+
# Default target value for jobs (default: 100)
|
7
|
+
# This represents the total number of items to process in a job
|
8
|
+
config.default_target = 100
|
9
|
+
|
10
|
+
# Default cache threshold for progress updates (default: 10)
|
11
|
+
# Progress updates are batched until this threshold is reached to reduce database writes
|
12
|
+
config.cache_threshold = 10
|
13
|
+
|
14
|
+
# Whether to automatically broadcast changes (default: true)
|
15
|
+
# When true, job updates are automatically broadcast via ActionCable
|
16
|
+
config.auto_broadcast = true
|
17
|
+
|
18
|
+
# Default partial path for rendering job trackers
|
19
|
+
# (default: 'active_job_tracker/shared/active_job_tracker')
|
20
|
+
config.default_partial = "active_job_tracker/active_job_tracker"
|
21
|
+
|
22
|
+
# Whether to include the style in the job tracker (default: true)
|
23
|
+
# When true, the gem's CSS styles are automatically included
|
24
|
+
config.include_style = true
|
25
|
+
end
|
data/lib/generators/active_job_tracker/templates/migrations/create_active_job_tracker_records.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
class CreateActiveJobTrackerRecords < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :active_job_tracker_records do |t|
|
4
|
+
t.belongs_to :active_job_trackable, polymorphic: true, index: { name: "index_active_job_tracker_records_on_active_job_trackable" }
|
5
|
+
|
6
|
+
t.string :job_id, null: false, index: true
|
7
|
+
t.integer :status, default: 0, null: false
|
8
|
+
t.integer :current, default: 0, null: false
|
9
|
+
t.integer :target, default: 100, null: false
|
10
|
+
|
11
|
+
t.text :error
|
12
|
+
t.text :backtrace
|
13
|
+
|
14
|
+
t.datetime :started_at
|
15
|
+
t.datetime :failed_at
|
16
|
+
t.datetime :completed_at
|
17
|
+
|
18
|
+
t.timestamps
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_job_tracker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Seena Sabti
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-03-06 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rails
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 8.0.1
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 8.0.1
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: mocha
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: sqlite3
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: puma
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: propshaft
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: rubocop-rails-omakase
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: debug
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 1.0.0
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 1.0.0
|
110
|
+
description: ActiveJobTracker provides a way to track the progress of ActiveJob jobs.
|
111
|
+
executables: []
|
112
|
+
extensions: []
|
113
|
+
extra_rdoc_files: []
|
114
|
+
files:
|
115
|
+
- MIT-LICENSE
|
116
|
+
- README.md
|
117
|
+
- Rakefile
|
118
|
+
- app/assets/stylesheets/active_job_tracker/style.css
|
119
|
+
- app/helpers/active_job_tracker/records_helper.rb
|
120
|
+
- app/models/active_job_tracker_record.rb
|
121
|
+
- app/views/active_job_tracker/_active_job_tracker.html.erb
|
122
|
+
- app/views/active_job_tracker/_active_job_tracker_wrapper.html.erb
|
123
|
+
- lib/active_job_tracker.rb
|
124
|
+
- lib/active_job_tracker/configuration.rb
|
125
|
+
- lib/active_job_tracker/engine.rb
|
126
|
+
- lib/active_job_tracker/version.rb
|
127
|
+
- lib/generators/active_job_tracker/initializer_generator.rb
|
128
|
+
- lib/generators/active_job_tracker/migrations_generator.rb
|
129
|
+
- lib/generators/active_job_tracker/templates/config/initializers/active_job_tracker.rb
|
130
|
+
- lib/generators/active_job_tracker/templates/migrations/create_active_job_tracker_records.rb
|
131
|
+
- lib/tasks/active_job_tracker_tasks.rake
|
132
|
+
homepage: https://github.com/seenasabti/active_job_tracker
|
133
|
+
licenses:
|
134
|
+
- MIT
|
135
|
+
metadata:
|
136
|
+
homepage_uri: https://github.com/seenasabti/active_job_tracker
|
137
|
+
source_code_uri: https://github.com/seenasabti/active_job_tracker
|
138
|
+
rdoc_options: []
|
139
|
+
require_paths:
|
140
|
+
- lib
|
141
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - ">="
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '0'
|
151
|
+
requirements: []
|
152
|
+
rubygems_version: 3.6.2
|
153
|
+
specification_version: 4
|
154
|
+
summary: ActiveJobTracker provides a way to track the progress of ActiveJob jobs.
|
155
|
+
test_files: []
|