delayable 0.4.1 → 0.4.2

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