delayable 0.4.1 → 0.5.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 +10 -0
- data/README.md +469 -5
- data/lib/delayable/version.rb +1 -1
- data/lib/delayable.rb +61 -9
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fbc0e3130501e50157a5dd2807576126022d145f89a3a005eb596b41c3456ecf
|
|
4
|
+
data.tar.gz: 729e2cf6548e11b220d7f8f19da46e55c1e520de3046ced844c5b858e4b1b2e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3bd0b541a0d948c9c320a0ed7caf4e8f5585a147ed551c7e1b1c397c119cf4a23969ac510888c0f985e1d7c00997fac896fbdc3aa850d9ba0dc0865670336bf2
|
|
7
|
+
data.tar.gz: c81b5c272398ea577e9030852410a7813a8abcad24d7ec3ea3843e36b1529a0dfd79647d80cfe12edc093dc298ea2963c45680894376405ee15d4c8a94e58fa0
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.5.0 - 2026-08-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added class-level `_all_later` helpers for delayed instance methods.
|
|
10
|
+
- Added bulk enqueue support through `ActiveJob.perform_all_later`, with individual enqueue fallback for Rails 7.0 and adapters without bulk support.
|
data/README.md
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
# Delayable
|
|
2
|
-
Short description and motivation.
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A Ruby gem that makes it easy to convert any method call into a background job using ActiveJob. Simply include the `Delayable` module and use the `delay` class method to mark methods that should be available for background execution.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Easy Background Jobs**: Convert any instance or class method into a background job with minimal setup
|
|
8
|
+
- **Current Attributes Preservation**: Automatically preserves Rails `Current` attributes across job boundaries
|
|
9
|
+
- **Flexible Configuration**: Configure queue names, delays, and concurrency limits per method
|
|
10
|
+
- **Method Parameter Support**: Full support for regular arguments, keyword arguments, and blocks
|
|
11
|
+
- **Bang Method Handling**: Automatic support for methods ending with `!`
|
|
12
|
+
- **Bulk Enqueuing**: Enqueue the same instance method for many receivers at once
|
|
13
|
+
- **ActiveJob Integration**: Seamlessly integrates with your existing ActiveJob setup
|
|
6
14
|
|
|
7
15
|
## Installation
|
|
16
|
+
|
|
8
17
|
Add this line to your application's Gemfile:
|
|
9
18
|
|
|
10
19
|
```ruby
|
|
@@ -13,7 +22,7 @@ gem "delayable"
|
|
|
13
22
|
|
|
14
23
|
And then execute:
|
|
15
24
|
```bash
|
|
16
|
-
$ bundle
|
|
25
|
+
$ bundle install
|
|
17
26
|
```
|
|
18
27
|
|
|
19
28
|
Or install it yourself as:
|
|
@@ -21,8 +30,463 @@ Or install it yourself as:
|
|
|
21
30
|
$ gem install delayable
|
|
22
31
|
```
|
|
23
32
|
|
|
33
|
+
## Prerequisites
|
|
34
|
+
|
|
35
|
+
Before using Delayable, ensure you have the following defined in your Rails application:
|
|
36
|
+
|
|
37
|
+
1. **ApplicationJob**: Your base job class
|
|
38
|
+
2. **Current**: A CurrentAttributes class for maintaining context
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# app/jobs/application_job.rb
|
|
42
|
+
class ApplicationJob < ActiveJob::Base
|
|
43
|
+
# Your job configuration
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# app/models/current.rb
|
|
47
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
48
|
+
attribute :user, :request_id, :user_agent
|
|
49
|
+
# Add other attributes as needed
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Basic Usage
|
|
54
|
+
|
|
55
|
+
### Instance Methods
|
|
56
|
+
|
|
57
|
+
Include `Delayable` in your class and use the `delay` method to mark methods for background execution:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
class DataProcessor
|
|
61
|
+
include Delayable
|
|
62
|
+
|
|
63
|
+
def process_csv_import(file_path, user_id)
|
|
64
|
+
user = User.find(user_id)
|
|
65
|
+
CSV.foreach(file_path, headers: true) do |row|
|
|
66
|
+
user.records.create!(row.to_h)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
delay :process_csv_import
|
|
70
|
+
|
|
71
|
+
def sync_external_data(api_endpoint, last_sync_time)
|
|
72
|
+
# Fetch and process data from external API
|
|
73
|
+
ExternalApiClient.new(api_endpoint).sync_data_since(last_sync_time)
|
|
74
|
+
end
|
|
75
|
+
delay :sync_external_data
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Usage
|
|
79
|
+
processor = DataProcessor.new
|
|
80
|
+
processor.process_csv_import_later("/tmp/users.csv", 123)
|
|
81
|
+
processor.sync_external_data_later("https://api.example.com/data", 1.hour.ago)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Bulk Instance Methods
|
|
85
|
+
|
|
86
|
+
Every delayed instance method also gets a class-level `_all_later` helper. It creates one job per receiver and passes the same positional and keyword arguments to each:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
class Unit
|
|
90
|
+
include Delayable
|
|
91
|
+
|
|
92
|
+
def sync_channex_availability(date_from, date_to, channel:)
|
|
93
|
+
# Sync availability for this unit
|
|
94
|
+
end
|
|
95
|
+
delay :sync_channex_availability, queue: :integrations
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
Unit.sync_channex_availability_all_later(
|
|
99
|
+
units,
|
|
100
|
+
Date.current,
|
|
101
|
+
1.year.from_now.to_date,
|
|
102
|
+
channel: :channex
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The helper preserves `Current.attributes` and all generated job configuration. A configured wait applies to every job and can be overridden per bulk call:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
Unit.sync_channex_availability_all_later(
|
|
110
|
+
units,
|
|
111
|
+
date_from,
|
|
112
|
+
date_to,
|
|
113
|
+
channel: :channex,
|
|
114
|
+
wait: 30.seconds
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Bang methods use `_all_later!`, such as `Unit.refresh_all_later!(units)`. Delayable does not remove duplicate receivers; use `units.uniq` when needed. An empty collection returns without enqueuing anything.
|
|
119
|
+
|
|
120
|
+
Bulk helpers are only generated for instance methods. Delayed class methods still use their regular `_later` helper.
|
|
121
|
+
|
|
122
|
+
Delayable uses `ActiveJob.perform_all_later` when available (Rails 7.1+). A true bulk queue operation requires an ActiveJob adapter that implements `enqueue_all`; otherwise ActiveJob enqueues each job individually. On Rails 7.0, Delayable also falls back to individual enqueues.
|
|
123
|
+
|
|
124
|
+
### Class Methods
|
|
125
|
+
|
|
126
|
+
You can also delay class methods by setting the `class_method: true` option:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
class ReportGenerator
|
|
130
|
+
include Delayable
|
|
131
|
+
|
|
132
|
+
def self.generate_monthly_report(month, year)
|
|
133
|
+
# Generate report logic
|
|
134
|
+
end
|
|
135
|
+
delay :generate_monthly_report, class_method: true
|
|
136
|
+
|
|
137
|
+
def self.cleanup_old_reports!
|
|
138
|
+
# Cleanup logic
|
|
139
|
+
end
|
|
140
|
+
delay :cleanup_old_reports!, class_method: true
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Usage
|
|
144
|
+
ReportGenerator.generate_monthly_report_later(12, 2023)
|
|
145
|
+
ReportGenerator.cleanup_old_reports_later!
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Methods with Bang (!)
|
|
149
|
+
|
|
150
|
+
Delayable automatically handles methods ending with `!`:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
class DataProcessor
|
|
154
|
+
include Delayable
|
|
155
|
+
|
|
156
|
+
def process_data!
|
|
157
|
+
# Processing logic
|
|
158
|
+
end
|
|
159
|
+
delay :process_data!
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Usage - note the bang is preserved in the delayed method name
|
|
163
|
+
processor = DataProcessor.new
|
|
164
|
+
processor.process_data_later! # Creates ProcessDataBangJob
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Advanced Configuration
|
|
168
|
+
|
|
169
|
+
### Queue Configuration
|
|
170
|
+
|
|
171
|
+
Specify which queue to use for the background job:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
class DataProcessor
|
|
175
|
+
include Delayable
|
|
176
|
+
|
|
177
|
+
def process_csv_import(file_path)
|
|
178
|
+
# Large CSV processing logic
|
|
179
|
+
end
|
|
180
|
+
delay :process_csv_import, queue: :data_processing
|
|
181
|
+
|
|
182
|
+
def sync_external_api(endpoint)
|
|
183
|
+
# API synchronization logic
|
|
184
|
+
end
|
|
185
|
+
delay :sync_external_api, queue: :integrations
|
|
186
|
+
end
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Delay Execution
|
|
190
|
+
|
|
191
|
+
Set a default delay for job execution:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
class CacheManager
|
|
195
|
+
include Delayable
|
|
196
|
+
|
|
197
|
+
def warm_cache(cache_key)
|
|
198
|
+
# Pre-populate cache logic
|
|
199
|
+
end
|
|
200
|
+
delay :warm_cache, wait: 5.minutes
|
|
201
|
+
|
|
202
|
+
def cleanup_expired_entries
|
|
203
|
+
# Cache cleanup logic
|
|
204
|
+
end
|
|
205
|
+
delay :cleanup_expired_entries, wait: 1.hour
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# You can also override the delay at call time
|
|
209
|
+
manager = CacheManager.new
|
|
210
|
+
manager.warm_cache_later("user_stats", wait: 30.seconds) # Overrides the 5.minutes default
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Concurrency Limits
|
|
214
|
+
|
|
215
|
+
Limit how many jobs of this type can run concurrently using SolidQueue's concurrency controls:
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
class ResourceIntensiveTask
|
|
219
|
+
include Delayable
|
|
220
|
+
|
|
221
|
+
def process_large_file(file_path)
|
|
222
|
+
# Heavy processing that should be limited
|
|
223
|
+
end
|
|
224
|
+
delay :process_large_file, limits_concurrency: { to: 2 }
|
|
225
|
+
|
|
226
|
+
def generate_report(report_type)
|
|
227
|
+
# Report generation that should run one at a time per report type
|
|
228
|
+
end
|
|
229
|
+
delay :generate_report, limits_concurrency: {
|
|
230
|
+
to: 1,
|
|
231
|
+
key: ->(run_data, report_type) { "report_#{report_type}" }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
def process_batch(batch_id, options = {})
|
|
235
|
+
# Process a batch with concurrency control per batch
|
|
236
|
+
end
|
|
237
|
+
delay :process_batch, limits_concurrency: {
|
|
238
|
+
to: 3,
|
|
239
|
+
key: ->(_run_data, batch_id) { "batch_#{batch_id}" }
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Important:** When using the `key` proc with `limits_concurrency`, the proc receives the delayed job arguments. The first argument is Delayable's internal `run_data` hash (`current_attributes` and `record`), followed by the arguments passed to your delayed method.
|
|
245
|
+
|
|
246
|
+
**Note:** The `limits_concurrency` feature requires SolidQueue as your ActiveJob backend. Other job backends (Sidekiq, Resque, etc.) do not support this feature and will ignore the setting.
|
|
247
|
+
|
|
248
|
+
## Current Attributes
|
|
249
|
+
|
|
250
|
+
Delayable automatically preserves Rails `Current` attributes across job boundaries:
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
class AuditLogger
|
|
254
|
+
include Delayable
|
|
255
|
+
|
|
256
|
+
def log_action(action, resource_id)
|
|
257
|
+
AuditLog.create!(
|
|
258
|
+
action: action,
|
|
259
|
+
resource_id: resource_id,
|
|
260
|
+
user: Current.user, # This will be preserved from the original request
|
|
261
|
+
request_id: Current.request_id
|
|
262
|
+
)
|
|
263
|
+
end
|
|
264
|
+
delay :log_action
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# In a controller
|
|
268
|
+
class PostsController < ApplicationController
|
|
269
|
+
def create
|
|
270
|
+
Current.user = current_user
|
|
271
|
+
Current.request_id = request.id
|
|
272
|
+
|
|
273
|
+
post = Post.create!(post_params)
|
|
274
|
+
|
|
275
|
+
# The job will have access to Current.user and Current.request_id
|
|
276
|
+
AuditLogger.new.log_action_later("create", post.id)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Working with ActiveRecord Models
|
|
282
|
+
|
|
283
|
+
Delayable works seamlessly with ActiveRecord models:
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
class User < ApplicationRecord
|
|
287
|
+
include Delayable
|
|
288
|
+
|
|
289
|
+
def update_profile_completeness
|
|
290
|
+
score = calculate_completeness_score
|
|
291
|
+
update!(profile_completeness: score)
|
|
292
|
+
end
|
|
293
|
+
delay :update_profile_completeness
|
|
294
|
+
|
|
295
|
+
def calculate_metrics!
|
|
296
|
+
update!(
|
|
297
|
+
total_posts: posts.count,
|
|
298
|
+
total_comments: comments.count,
|
|
299
|
+
last_activity: Time.current
|
|
300
|
+
)
|
|
301
|
+
end
|
|
302
|
+
delay :calculate_metrics!
|
|
303
|
+
|
|
304
|
+
def archive_old_data!
|
|
305
|
+
# Archive user's old posts, comments, etc.
|
|
306
|
+
posts.where('created_at < ?', 1.year.ago).update_all(archived: true)
|
|
307
|
+
comments.where('created_at < ?', 1.year.ago).delete_all
|
|
308
|
+
end
|
|
309
|
+
delay :archive_old_data!
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Usage
|
|
313
|
+
user = User.find(123)
|
|
314
|
+
user.update_profile_completeness_later
|
|
315
|
+
user.calculate_metrics_later!
|
|
316
|
+
user.archive_old_data_later!
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Job Naming Convention
|
|
320
|
+
|
|
321
|
+
Delayable automatically generates job class names based on your method names to ensure logging and debugging remain clear and meaningful:
|
|
322
|
+
|
|
323
|
+
| Method Name | Method Type | Generated Job Class |
|
|
324
|
+
|-------------|-------------|-------------------|
|
|
325
|
+
| `process_data` | instance | `YourClass::ProcessDataJob` |
|
|
326
|
+
| `process_data!` | instance | `YourClass::ProcessDataBangJob` |
|
|
327
|
+
| `generate_report` | class | `YourClass::ClassGenerateReportJob` |
|
|
328
|
+
| `cleanup!` | class | `YourClass::ClassCleanupBangJob` |
|
|
329
|
+
|
|
330
|
+
This naming convention ensures that when you're monitoring job queues, reading logs, or debugging failed jobs, you can immediately understand what method was being executed and in what context. The job name directly corresponds to your original method, making it easy to trace issues back to your application code.
|
|
331
|
+
|
|
332
|
+
## Error Handling
|
|
333
|
+
|
|
334
|
+
Since Delayable creates standard ActiveJob jobs, you can use all the standard ActiveJob error handling mechanisms. Put defaults on `ApplicationJob`, or pass a block to `delay` to configure the generated job class:
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
# In your ApplicationJob
|
|
338
|
+
class ApplicationJob < ActiveJob::Base
|
|
339
|
+
retry_on StandardError, wait: :exponentially_longer, attempts: 5
|
|
340
|
+
discard_on ActiveRecord::RecordNotFound
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
class BookingSync
|
|
344
|
+
include Delayable
|
|
345
|
+
|
|
346
|
+
def sync_channex_bookings
|
|
347
|
+
# Sync bookings
|
|
348
|
+
end
|
|
349
|
+
delay :sync_channex_bookings,
|
|
350
|
+
limits_concurrency: {
|
|
351
|
+
to: 1,
|
|
352
|
+
key: ->(run_data) { "#{run_data[:record].to_global_id}-channex-bookings" }
|
|
353
|
+
} do
|
|
354
|
+
retry_on Channex::Api::ConnectionError,
|
|
355
|
+
wait: :polynomially_longer,
|
|
356
|
+
attempts: 5
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Testing
|
|
362
|
+
|
|
363
|
+
Delayable integrates with ActiveJob's testing helpers:
|
|
364
|
+
|
|
365
|
+
```ruby
|
|
366
|
+
require 'test_helper'
|
|
367
|
+
|
|
368
|
+
class DataProcessorTest < ActiveSupport::TestCase
|
|
369
|
+
include ActiveJob::TestHelper
|
|
370
|
+
|
|
371
|
+
test "CSV import is enqueued" do
|
|
372
|
+
processor = DataProcessor.new
|
|
373
|
+
|
|
374
|
+
assert_enqueued_jobs 1, only: DataProcessor::ProcessCsvImportJob do
|
|
375
|
+
processor.process_csv_import_later("/tmp/data.csv", 123)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
test "CSV data is imported" do
|
|
380
|
+
processor = DataProcessor.new
|
|
381
|
+
|
|
382
|
+
perform_enqueued_jobs do
|
|
383
|
+
processor.process_csv_import_later("/tmp/data.csv", 123)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Assert data was imported
|
|
387
|
+
user = User.find(123)
|
|
388
|
+
assert user.records.count > 0
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
test "current attributes are preserved" do
|
|
392
|
+
Current.user = users(:admin)
|
|
393
|
+
|
|
394
|
+
perform_enqueued_jobs do
|
|
395
|
+
AuditLogger.new.log_action_later("test", 123)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
log = AuditLog.last
|
|
399
|
+
assert_equal users(:admin), log.user
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Best Practices
|
|
405
|
+
|
|
406
|
+
1. **Keep Jobs Idempotent**: Design your delayed methods to be safely retried
|
|
407
|
+
2. **Use Appropriate Queues**: Separate different types of work into different queues
|
|
408
|
+
3. **Handle Failures Gracefully**: Use ActiveJob's retry and error handling features
|
|
409
|
+
4. **Monitor Job Performance**: Use tools like Sidekiq's web UI or similar for your job backend
|
|
410
|
+
5. **Test Background Behavior**: Always test both the enqueuing and execution of your jobs
|
|
411
|
+
|
|
412
|
+
## Configuration Examples
|
|
413
|
+
|
|
414
|
+
### Multiple Options Combined
|
|
415
|
+
|
|
416
|
+
```ruby
|
|
417
|
+
class ComplexProcessor
|
|
418
|
+
include Delayable
|
|
419
|
+
|
|
420
|
+
def process_batch(batch_id, options = {})
|
|
421
|
+
# Complex processing logic
|
|
422
|
+
end
|
|
423
|
+
delay :process_batch,
|
|
424
|
+
queue: :batch_processing,
|
|
425
|
+
wait: 30.seconds,
|
|
426
|
+
limits_concurrency: { to: 3, key: ->(_run_data, batch_id) { "batch_#{batch_id}" } }
|
|
427
|
+
end
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Dynamic Delays
|
|
431
|
+
|
|
432
|
+
```ruby
|
|
433
|
+
class NotificationSender
|
|
434
|
+
include Delayable
|
|
435
|
+
|
|
436
|
+
def send_reminder(user_id, urgency = :normal)
|
|
437
|
+
# Send reminder logic
|
|
438
|
+
end
|
|
439
|
+
delay :send_reminder
|
|
440
|
+
|
|
441
|
+
def send_reminder_with_delay(user_id, urgency = :normal)
|
|
442
|
+
delay_time = case urgency
|
|
443
|
+
when :urgent then 0.seconds
|
|
444
|
+
when :normal then 1.hour
|
|
445
|
+
when :low then 1.day
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
send_reminder_later(user_id, urgency, wait: delay_time)
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
## Troubleshooting
|
|
454
|
+
|
|
455
|
+
### Debugging
|
|
456
|
+
|
|
457
|
+
To see what jobs are being created, you can inspect the generated job classes:
|
|
458
|
+
|
|
459
|
+
```ruby
|
|
460
|
+
# In rails console
|
|
461
|
+
DataProcessor::ProcessCsvImportJob.new.class.name
|
|
462
|
+
# => "DataProcessor::ProcessCsvImportJob"
|
|
463
|
+
|
|
464
|
+
DataProcessor::ProcessCsvImportJob.queue_name
|
|
465
|
+
# => "default"
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Requirements
|
|
469
|
+
|
|
470
|
+
- Ruby >= 3.4.0
|
|
471
|
+
- Rails >= 7.0.4.3
|
|
472
|
+
- ActiveJob configured with a backend (Sidekiq, Resque, etc.)
|
|
473
|
+
- An ActiveJob adapter with `enqueue_all` support for a true bulk queue operation (optional)
|
|
474
|
+
- **SolidQueue** (optional) - Required only if using `limits_concurrency` feature
|
|
475
|
+
|
|
24
476
|
## Contributing
|
|
25
|
-
|
|
477
|
+
|
|
478
|
+
1. Fork the repository
|
|
479
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
480
|
+
3. Add tests for your changes
|
|
481
|
+
4. Make sure all tests pass (`bundle exec rake test`)
|
|
482
|
+
5. Commit your changes (`git commit -am 'Add some feature'`)
|
|
483
|
+
6. Push to the branch (`git push origin my-new-feature`)
|
|
484
|
+
7. Create a new Pull Request
|
|
26
485
|
|
|
27
486
|
## License
|
|
487
|
+
|
|
28
488
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
489
|
+
|
|
490
|
+
## Changelog
|
|
491
|
+
|
|
492
|
+
See [CHANGELOG.md](https://github.com/trobrock/delayable/CHANGELOG.md) for details about changes in each version.
|
data/lib/delayable/version.rb
CHANGED
data/lib/delayable.rb
CHANGED
|
@@ -12,28 +12,54 @@ module Delayable
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
class_methods do
|
|
15
|
-
def delay(method_name, class_method: false, wait: nil, limits_concurrency: nil, queue: :default)
|
|
15
|
+
def delay(method_name, class_method: false, wait: nil, limits_concurrency: nil, queue: :default, &job_configuration)
|
|
16
16
|
job = const_set(
|
|
17
17
|
delayable_job_name(method_name, class_method:),
|
|
18
|
-
delayable_job_class(
|
|
18
|
+
delayable_job_class(
|
|
19
|
+
method_name,
|
|
20
|
+
class_method:,
|
|
21
|
+
concurrency_limits: limits_concurrency,
|
|
22
|
+
queue:,
|
|
23
|
+
&job_configuration
|
|
24
|
+
)
|
|
19
25
|
)
|
|
20
26
|
|
|
21
27
|
defined_wait = wait
|
|
28
|
+
build_job = method(:delayable_build_job)
|
|
29
|
+
extract_wait = method(:delayable_extract_wait)
|
|
30
|
+
enqueue_jobs = method(:delayable_enqueue_jobs)
|
|
22
31
|
delayable_define_method(class_method:, method_name:) do |*arguments, **kwargs|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
job_wait = extract_wait.call(kwargs, defined_wait)
|
|
33
|
+
build_job.call(job, self, job_wait, arguments, kwargs, enqueue: true)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
return if class_method
|
|
37
|
+
|
|
38
|
+
delayable_define_bulk_method(method_name) do |receivers, *arguments, **kwargs|
|
|
39
|
+
receivers = receivers.to_a
|
|
40
|
+
next if receivers.empty?
|
|
41
|
+
|
|
42
|
+
job_wait = extract_wait.call(kwargs, defined_wait)
|
|
43
|
+
jobs = receivers.map { |receiver| build_job.call(job, receiver, job_wait, arguments, kwargs) }
|
|
44
|
+
enqueue_jobs.call(jobs)
|
|
26
45
|
end
|
|
27
46
|
end
|
|
28
47
|
|
|
29
48
|
private
|
|
30
49
|
|
|
31
|
-
def delayable_job_class(
|
|
50
|
+
def delayable_job_class(
|
|
51
|
+
method_name,
|
|
52
|
+
class_method: false,
|
|
53
|
+
concurrency_limits: nil,
|
|
54
|
+
queue: :default,
|
|
55
|
+
&job_configuration
|
|
56
|
+
)
|
|
32
57
|
klass = self
|
|
33
58
|
Class.new(ApplicationJob) do
|
|
34
59
|
queue_as queue
|
|
35
60
|
|
|
36
|
-
limits_concurrency(**concurrency_limits) if concurrency_limits.is_a?(Hash)
|
|
61
|
+
limits_concurrency(**concurrency_limits) if concurrency_limits.is_a?(Hash) && respond_to?(:limits_concurrency)
|
|
62
|
+
class_eval(&job_configuration) if job_configuration
|
|
37
63
|
|
|
38
64
|
define_method(:perform) do |run_data = {}, *arguments, **kwargs|
|
|
39
65
|
Current.set(run_data[:current_attributes] || {}) do
|
|
@@ -43,6 +69,24 @@ module Delayable
|
|
|
43
69
|
end
|
|
44
70
|
end
|
|
45
71
|
|
|
72
|
+
def delayable_build_job(job, receiver, wait, arguments, kwargs, enqueue: false)
|
|
73
|
+
run_data = { current_attributes: Current.attributes, record: receiver }
|
|
74
|
+
return job.set(wait:).perform_later(run_data, *arguments, **kwargs) if enqueue
|
|
75
|
+
|
|
76
|
+
job.new(run_data, *arguments, **kwargs).set(wait:)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def delayable_extract_wait(kwargs, defined_wait)
|
|
80
|
+
kwargs.key?(:wait) ? kwargs.delete(:wait) : defined_wait
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def delayable_enqueue_jobs(jobs)
|
|
84
|
+
return ActiveJob.perform_all_later(*jobs) if ActiveJob.respond_to?(:perform_all_later)
|
|
85
|
+
|
|
86
|
+
jobs.each(&:enqueue)
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
46
90
|
def delayable_job_name(method_name, class_method: false)
|
|
47
91
|
class_name = bang_method?(method_name) ? "#{method_name.to_s.chomp('!').camelize}Bang" : method_name.to_s.camelize
|
|
48
92
|
:"#{'Class' if class_method}#{class_name}Job"
|
|
@@ -50,8 +94,16 @@ module Delayable
|
|
|
50
94
|
|
|
51
95
|
def delayable_define_method(class_method:, method_name:, &)
|
|
52
96
|
define_method_type = class_method ? :define_singleton_method : :define_method
|
|
53
|
-
|
|
54
|
-
|
|
97
|
+
public_send(define_method_type, delayable_method_name(method_name), &)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def delayable_define_bulk_method(method_name, &)
|
|
101
|
+
define_singleton_method(delayable_method_name(method_name, bulk: true), &)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def delayable_method_name(method_name, bulk: false)
|
|
105
|
+
suffix = bulk ? '_all_later' : '_later'
|
|
106
|
+
bang_method?(method_name) ? "#{method_name.to_s.chomp('!')}#{suffix}!" : "#{method_name}#{suffix}"
|
|
55
107
|
end
|
|
56
108
|
|
|
57
109
|
def bang_method?(method_name)
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: delayable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Trae Robrock
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-08-03 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -33,6 +33,7 @@ extra_rdoc_files: []
|
|
|
33
33
|
files:
|
|
34
34
|
- ".rubocop.yml"
|
|
35
35
|
- ".ruby-version"
|
|
36
|
+
- CHANGELOG.md
|
|
36
37
|
- MIT-LICENSE
|
|
37
38
|
- README.md
|
|
38
39
|
- Rakefile
|