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 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,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -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,11 @@
1
+ module ActiveJobTracker
2
+ module RecordsHelper
3
+ def active_job_tracker_wrapper(options = {}, &block)
4
+ render partial: "active_job_tracker/active_job_tracker_wrapper",
5
+ locals: {
6
+ content: capture(&block),
7
+ options: options
8
+ }
9
+ end
10
+ end
11
+ end
@@ -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,3 @@
1
+ module ActiveJobTracker
2
+ VERSION = "0.1.0"
3
+ 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
@@ -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
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :active_job_tracker do
3
+ # # Task goes here
4
+ # 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: []