agent-notes 2.0.4__py3-none-any.whl

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.
Files changed (162) hide show
  1. agent_notes/VERSION +1 -0
  2. agent_notes/__init__.py +1 -0
  3. agent_notes/__main__.py +4 -0
  4. agent_notes/cli.py +348 -0
  5. agent_notes/commands/__init__.py +27 -0
  6. agent_notes/commands/_install_helpers.py +262 -0
  7. agent_notes/commands/build.py +170 -0
  8. agent_notes/commands/doctor.py +112 -0
  9. agent_notes/commands/info.py +95 -0
  10. agent_notes/commands/install.py +99 -0
  11. agent_notes/commands/list.py +169 -0
  12. agent_notes/commands/memory.py +430 -0
  13. agent_notes/commands/regenerate.py +152 -0
  14. agent_notes/commands/set_role.py +143 -0
  15. agent_notes/commands/uninstall.py +26 -0
  16. agent_notes/commands/update.py +169 -0
  17. agent_notes/commands/validate.py +199 -0
  18. agent_notes/commands/wizard.py +720 -0
  19. agent_notes/config.py +154 -0
  20. agent_notes/data/agents/agents.yaml +352 -0
  21. agent_notes/data/agents/analyst.md +45 -0
  22. agent_notes/data/agents/api-reviewer.md +47 -0
  23. agent_notes/data/agents/architect.md +46 -0
  24. agent_notes/data/agents/coder.md +28 -0
  25. agent_notes/data/agents/database-specialist.md +45 -0
  26. agent_notes/data/agents/debugger.md +47 -0
  27. agent_notes/data/agents/devil.md +47 -0
  28. agent_notes/data/agents/devops.md +38 -0
  29. agent_notes/data/agents/explorer.md +23 -0
  30. agent_notes/data/agents/integrations.md +44 -0
  31. agent_notes/data/agents/lead.md +216 -0
  32. agent_notes/data/agents/performance-profiler.md +44 -0
  33. agent_notes/data/agents/refactorer.md +48 -0
  34. agent_notes/data/agents/reviewer.md +44 -0
  35. agent_notes/data/agents/security-auditor.md +44 -0
  36. agent_notes/data/agents/system-auditor.md +38 -0
  37. agent_notes/data/agents/tech-writer.md +32 -0
  38. agent_notes/data/agents/test-runner.md +36 -0
  39. agent_notes/data/agents/test-writer.md +39 -0
  40. agent_notes/data/cli/claude.yaml +25 -0
  41. agent_notes/data/cli/copilot.yaml +18 -0
  42. agent_notes/data/cli/opencode.yaml +22 -0
  43. agent_notes/data/commands/brainstorm.md +8 -0
  44. agent_notes/data/commands/debug.md +9 -0
  45. agent_notes/data/commands/review.md +10 -0
  46. agent_notes/data/global-claude.md +290 -0
  47. agent_notes/data/global-copilot.md +27 -0
  48. agent_notes/data/global-opencode.md +40 -0
  49. agent_notes/data/hooks/session-context.md.tpl +19 -0
  50. agent_notes/data/models/claude-haiku-4-5.yaml +15 -0
  51. agent_notes/data/models/claude-opus-4-1.yaml +16 -0
  52. agent_notes/data/models/claude-opus-4-5.yaml +16 -0
  53. agent_notes/data/models/claude-opus-4-6.yaml +16 -0
  54. agent_notes/data/models/claude-opus-4-7.yaml +15 -0
  55. agent_notes/data/models/claude-sonnet-4-5.yaml +16 -0
  56. agent_notes/data/models/claude-sonnet-4-6.yaml +15 -0
  57. agent_notes/data/models/claude-sonnet-4.yaml +16 -0
  58. agent_notes/data/pricing.yaml +33 -0
  59. agent_notes/data/roles/orchestrator.yaml +5 -0
  60. agent_notes/data/roles/reasoner.yaml +5 -0
  61. agent_notes/data/roles/scout.yaml +5 -0
  62. agent_notes/data/roles/worker.yaml +5 -0
  63. agent_notes/data/rules/code-quality.md +9 -0
  64. agent_notes/data/rules/safety.md +10 -0
  65. agent_notes/data/scripts/cost-report +211 -0
  66. agent_notes/data/skills/brainstorming/SKILL.md +57 -0
  67. agent_notes/data/skills/code-review/SKILL.md +64 -0
  68. agent_notes/data/skills/debugging-protocol/SKILL.md +51 -0
  69. agent_notes/data/skills/docker-compose/SKILL.md +318 -0
  70. agent_notes/data/skills/docker-compose-advanced/SKILL.md +575 -0
  71. agent_notes/data/skills/docker-dockerfile/SKILL.md +385 -0
  72. agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +293 -0
  73. agent_notes/data/skills/git/SKILL.md +87 -0
  74. agent_notes/data/skills/rails-active-storage/SKILL.md +321 -0
  75. agent_notes/data/skills/rails-broadcasting/SKILL.md +374 -0
  76. agent_notes/data/skills/rails-concerns/SKILL.md +806 -0
  77. agent_notes/data/skills/rails-controllers/SKILL.md +510 -0
  78. agent_notes/data/skills/rails-controllers-advanced/SKILL.md +441 -0
  79. agent_notes/data/skills/rails-helpers/SKILL.md +677 -0
  80. agent_notes/data/skills/rails-initializers/SKILL.md +79 -0
  81. agent_notes/data/skills/rails-javascript/SKILL.md +567 -0
  82. agent_notes/data/skills/rails-jobs/SKILL.md +700 -0
  83. agent_notes/data/skills/rails-kamal/SKILL.md +483 -0
  84. agent_notes/data/skills/rails-lib/SKILL.md +101 -0
  85. agent_notes/data/skills/rails-mailers/SKILL.md +321 -0
  86. agent_notes/data/skills/rails-migrations/SKILL.md +268 -0
  87. agent_notes/data/skills/rails-models/SKILL.md +459 -0
  88. agent_notes/data/skills/rails-models-advanced/SKILL.md +398 -0
  89. agent_notes/data/skills/rails-routes/SKILL.md +804 -0
  90. agent_notes/data/skills/rails-style/SKILL.md +538 -0
  91. agent_notes/data/skills/rails-testing-controllers/SKILL.md +343 -0
  92. agent_notes/data/skills/rails-testing-models/SKILL.md +296 -0
  93. agent_notes/data/skills/rails-testing-system/SKILL.md +375 -0
  94. agent_notes/data/skills/rails-validations/SKILL.md +108 -0
  95. agent_notes/data/skills/rails-view-components/SKILL.md +511 -0
  96. agent_notes/data/skills/rails-view-components-advanced/SKILL.md +376 -0
  97. agent_notes/data/skills/rails-views/SKILL.md +413 -0
  98. agent_notes/data/skills/rails-views-advanced/SKILL.md +450 -0
  99. agent_notes/data/skills/refactoring-protocol/SKILL.md +64 -0
  100. agent_notes/data/skills/tdd/SKILL.md +57 -0
  101. agent_notes/data/templates/__init__.py +1 -0
  102. agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
  103. agent_notes/data/templates/frontmatter/__init__.py +1 -0
  104. agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
  105. agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
  106. agent_notes/data/templates/frontmatter/__pycache__/cursor.cpython-314.pyc +0 -0
  107. agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
  108. agent_notes/data/templates/frontmatter/claude.py +44 -0
  109. agent_notes/data/templates/frontmatter/opencode.py +104 -0
  110. agent_notes/doctor_checks.py +189 -0
  111. agent_notes/domain/__init__.py +17 -0
  112. agent_notes/domain/agent.py +34 -0
  113. agent_notes/domain/cli_backend.py +40 -0
  114. agent_notes/domain/diagnostics.py +29 -0
  115. agent_notes/domain/diff.py +44 -0
  116. agent_notes/domain/model.py +27 -0
  117. agent_notes/domain/role.py +13 -0
  118. agent_notes/domain/rule.py +13 -0
  119. agent_notes/domain/skill.py +15 -0
  120. agent_notes/domain/state.py +46 -0
  121. agent_notes/install_state.py +11 -0
  122. agent_notes/registries/__init__.py +16 -0
  123. agent_notes/registries/_base.py +46 -0
  124. agent_notes/registries/agent_registry.py +107 -0
  125. agent_notes/registries/cli_registry.py +89 -0
  126. agent_notes/registries/model_registry.py +85 -0
  127. agent_notes/registries/role_registry.py +64 -0
  128. agent_notes/registries/rule_registry.py +80 -0
  129. agent_notes/registries/skill_registry.py +141 -0
  130. agent_notes/services/__init__.py +8 -0
  131. agent_notes/services/diagnostics/__init__.py +47 -0
  132. agent_notes/services/diagnostics/_checks.py +272 -0
  133. agent_notes/services/diagnostics/_display.py +346 -0
  134. agent_notes/services/diagnostics/_fix.py +169 -0
  135. agent_notes/services/diff.py +349 -0
  136. agent_notes/services/fs.py +195 -0
  137. agent_notes/services/install_state_builder.py +210 -0
  138. agent_notes/services/installer.py +293 -0
  139. agent_notes/services/memory_backend.py +155 -0
  140. agent_notes/services/rendering.py +329 -0
  141. agent_notes/services/session_context.py +23 -0
  142. agent_notes/services/settings_writer.py +79 -0
  143. agent_notes/services/state_store.py +249 -0
  144. agent_notes/services/ui.py +419 -0
  145. agent_notes/services/user_config.py +62 -0
  146. agent_notes/services/validation.py +67 -0
  147. agent_notes/state.py +21 -0
  148. agent_notes-2.0.4.dist-info/METADATA +14 -0
  149. agent_notes-2.0.4.dist-info/RECORD +162 -0
  150. agent_notes-2.0.4.dist-info/WHEEL +5 -0
  151. agent_notes-2.0.4.dist-info/entry_points.txt +2 -0
  152. agent_notes-2.0.4.dist-info/licenses/LICENSE +21 -0
  153. agent_notes-2.0.4.dist-info/top_level.txt +2 -0
  154. tests/conftest.py +20 -0
  155. tests/functional/__init__.py +0 -0
  156. tests/functional/test_build_commands.py +88 -0
  157. tests/functional/test_registries.py +128 -0
  158. tests/integration/__init__.py +0 -0
  159. tests/integration/test_build_output.py +129 -0
  160. tests/plugins/__init__.py +0 -0
  161. tests/plugins/test_agents.py +93 -0
  162. tests/plugins/test_skills.py +77 -0
@@ -0,0 +1,700 @@
1
+ ---
2
+ name: rails-jobs
3
+ description: "Rails Active Job: background processing, error handling, recurring jobs, and testing"
4
+ group: rails
5
+ ---
6
+
7
+ # Jobs Guide
8
+
9
+ Comprehensive guide for Rails Active Job background processing.
10
+
11
+ ---
12
+
13
+ ## Philosophy
14
+
15
+ 1. **Jobs delegate to models** - Keep jobs thin
16
+ 2. **Use `_later` suffix** - Async method naming convention
17
+ 3. **Idempotent when possible** - Jobs can be retried safely
18
+ 4. **Error handling** - Retry on transient errors, discard on permanent
19
+ 5. **Queue organization** - Different queues for different priorities
20
+
21
+ ---
22
+
23
+ ## File Structure
24
+
25
+ ```
26
+ app/jobs/
27
+ ├── application_job.rb
28
+ ├── notification/
29
+ │ └── bundle/
30
+ │ └── deliver_job.rb
31
+ ├── event/
32
+ │ ├── relay_job.rb
33
+ │ └── webhook_dispatch_job.rb
34
+ └── card/
35
+ └── auto_postpone_job.rb
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Basic Job Structure
41
+
42
+ ### Simple Job
43
+
44
+ ```ruby
45
+ # app/jobs/notification_job.rb
46
+ class NotificationJob < ApplicationJob
47
+ queue_as :default
48
+
49
+ def perform(user, message)
50
+ user.notifications.create!(message: message)
51
+ end
52
+ end
53
+
54
+ # Enqueue
55
+ NotificationJob.perform_later(user, "Hello!")
56
+
57
+ # Enqueue with delay
58
+ NotificationJob.set(wait: 1.hour).perform_later(user, "Reminder")
59
+
60
+ # Enqueue at specific time
61
+ NotificationJob.set(wait_until: Time.current + 2.hours).perform_later(user, "Alert")
62
+ ```
63
+
64
+ ### Job with Options
65
+
66
+ ```ruby
67
+ class DataExportJob < ApplicationJob
68
+ queue_as :low_priority
69
+
70
+ # Retry configuration
71
+ retry_on Net::HTTPServerError, wait: :exponentially_longer, attempts: 5
72
+ retry_on Timeout::Error, wait: 5.seconds, attempts: 3
73
+
74
+ # Discard (don't retry)
75
+ discard_on ActiveJob::DeserializationError
76
+ discard_on ActiveRecord::RecordNotFound
77
+
78
+ def perform(user, export_type)
79
+ data = generate_export(user, export_type)
80
+ user.send_export(data)
81
+ end
82
+
83
+ private
84
+ def generate_export(user, type)
85
+ # Implementation
86
+ end
87
+ end
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Job Naming Conventions
93
+
94
+ ### Model Integration Pattern
95
+
96
+ ```ruby
97
+ # Model method
98
+ class Event < ApplicationRecord
99
+ # Enqueue job
100
+ def relay_later
101
+ Event::RelayJob.perform_later(self)
102
+ end
103
+
104
+ # Synchronous version
105
+ def relay_now
106
+ webhooks.active.each do |webhook|
107
+ webhook.trigger(self)
108
+ end
109
+ end
110
+ end
111
+
112
+ # Job delegates to model
113
+ class Event::RelayJob < ApplicationJob
114
+ queue_as :webhooks
115
+
116
+ def perform(event)
117
+ event.relay_now
118
+ end
119
+ end
120
+
121
+ # Usage
122
+ event.relay_later # Async
123
+ event.relay_now # Sync
124
+ ```
125
+
126
+ ### Callback Integration
127
+
128
+ ```ruby
129
+ module Event::Relaying
130
+ extend ActiveSupport::Concern
131
+
132
+ included do
133
+ after_create_commit :relay_later
134
+ end
135
+
136
+ def relay_later
137
+ Event::RelayJob.perform_later(self)
138
+ end
139
+
140
+ def relay_now
141
+ # Implementation
142
+ end
143
+ end
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Queue Configuration
149
+
150
+ ### Define Queues
151
+
152
+ ```ruby
153
+ # config/application.rb
154
+ config.active_job.queue_adapter = :solid_queue # or :sidekiq, :resque, etc.
155
+
156
+ # Define queue priorities
157
+ config.active_job.queue_name_prefix = Rails.env
158
+ config.active_job.queue_name_delimiter = "_"
159
+ ```
160
+
161
+ ### Queue Organization
162
+
163
+ ```ruby
164
+ class ApplicationJob < ActiveJob::Base
165
+ # Default queue
166
+ queue_as :default
167
+ end
168
+
169
+ class UrgentJob < ApplicationJob
170
+ queue_as :urgent
171
+ end
172
+
173
+ class LowPriorityJob < ApplicationJob
174
+ queue_as :low_priority
175
+ end
176
+
177
+ class ReportJob < ApplicationJob
178
+ # Dynamic queue based on user
179
+ queue_as do
180
+ user = arguments.first
181
+ user.premium? ? :premium : :default
182
+ end
183
+ end
184
+ ```
185
+
186
+ ---
187
+
188
+ ## Error Handling
189
+
190
+ ### Retry Strategies
191
+
192
+ ```ruby
193
+ class ExternalApiJob < ApplicationJob
194
+ # Exponential backoff: 3s, 18s, 83s, 258s, ...
195
+ retry_on Net::HTTPServerError, wait: :exponentially_longer, attempts: 5
196
+
197
+ # Polynomial backoff: 4s, 16s, 36s, 64s, ...
198
+ retry_on Timeout::Error, wait: :polynomially_longer, attempts: 4
199
+
200
+ # Fixed delay
201
+ retry_on SomeTransientError, wait: 30.seconds, attempts: 3
202
+
203
+ # Custom wait calculation
204
+ retry_on DatabaseError, wait: ->(executions) { executions * 10 }
205
+
206
+ # Conditional retry
207
+ retry_on SomeError, attempts: 3 do |job, exception|
208
+ should_retry?(exception)
209
+ end
210
+
211
+ def perform(data)
212
+ # Implementation
213
+ end
214
+ end
215
+ ```
216
+
217
+ ### Discard (Don't Retry)
218
+
219
+ ```ruby
220
+ class ProcessUserJob < ApplicationJob
221
+ # Discard on permanent errors
222
+ discard_on ActiveRecord::RecordNotFound
223
+ discard_on ActiveJob::DeserializationError
224
+
225
+ # Conditional discard
226
+ discard_on CustomError do |job, exception|
227
+ exception.message.include?("permanent")
228
+ end
229
+
230
+ def perform(user_id)
231
+ user = User.find(user_id)
232
+ process(user)
233
+ end
234
+ end
235
+ ```
236
+
237
+ ### Rescue From
238
+
239
+ ```ruby
240
+ class SmtpMailJob < ApplicationJob
241
+ rescue_from Net::SMTPSyntaxError do |exception|
242
+ case exception.message
243
+ when /\A501 5\.1\.3/
244
+ # Log and ignore invalid email addresses
245
+ Rails.logger.info "Invalid email: #{exception.message}"
246
+ else
247
+ raise # Re-raise for other SMTP syntax errors
248
+ end
249
+ end
250
+
251
+ def perform(email_data)
252
+ send_email(email_data)
253
+ end
254
+ end
255
+ ```
256
+
257
+ ---
258
+
259
+ ## Recurring Jobs
260
+
261
+ ### Configuration (Solid Queue)
262
+
263
+ ```yaml
264
+ # config/recurring.yml
265
+ production:
266
+ deliver_notifications:
267
+ class: Notification::Bundle::DeliverJob
268
+ schedule: "*/30 * * * *" # Every 30 minutes
269
+
270
+ auto_postpone_cards:
271
+ class: Card::AutoPostponeJob
272
+ schedule: "0 * * * *" # Every hour
273
+
274
+ cleanup_old_sessions:
275
+ class: Session::CleanupJob
276
+ schedule: "0 2 * * *" # Daily at 2 AM
277
+
278
+ weekly_digest:
279
+ class: DigestJob
280
+ schedule: "0 8 * * 1" # Monday at 8 AM
281
+ ```
282
+
283
+ ### Recurring Job Pattern
284
+
285
+ ```ruby
286
+ class Card::AutoPostponeJob < ApplicationJob
287
+ queue_as :maintenance
288
+
289
+ def perform
290
+ Card.stale.find_each do |card|
291
+ card.postpone
292
+ end
293
+ end
294
+ end
295
+ ```
296
+
297
+ ---
298
+
299
+ ## Job Concerns
300
+
301
+ ### Error Handling Concern
302
+
303
+ ```ruby
304
+ # app/jobs/concerns/smtp_delivery_error_handling.rb
305
+ module SmtpDeliveryErrorHandling
306
+ extend ActiveSupport::Concern
307
+
308
+ included do
309
+ # Retry on network errors
310
+ retry_on Net::OpenTimeout, Net::ReadTimeout, Socket::ResolutionError,
311
+ wait: :polynomially_longer
312
+
313
+ # Retry on temporary SMTP errors (4xx)
314
+ retry_on Net::SMTPServerBusy, wait: :polynomially_longer
315
+
316
+ # Handle specific SMTP errors
317
+ rescue_from Net::SMTPSyntaxError do |error|
318
+ case error.message
319
+ when /\A501 5\.1\.3/
320
+ # Ignore invalid email addresses
321
+ Sentry.capture_exception(error, level: :info) if defined?(Sentry)
322
+ else
323
+ raise
324
+ end
325
+ end
326
+
327
+ # Handle fatal SMTP errors (5xx)
328
+ rescue_from Net::SMTPFatalError do |error|
329
+ case error.message
330
+ when /\A550 5\.1\.1/, /\A552 5\.6\.0/
331
+ Sentry.capture_exception(error, level: :info) if defined?(Sentry)
332
+ else
333
+ raise
334
+ end
335
+ end
336
+ end
337
+ end
338
+
339
+ # Usage
340
+ class WelcomeEmailJob < ApplicationJob
341
+ include SmtpDeliveryErrorHandling
342
+
343
+ def perform(user)
344
+ UserMailer.welcome(user).deliver_now
345
+ end
346
+ end
347
+ ```
348
+
349
+ ---
350
+
351
+ ## Job Callbacks
352
+
353
+ ### Lifecycle Callbacks
354
+
355
+ ```ruby
356
+ class DataProcessingJob < ApplicationJob
357
+ before_perform :log_start
358
+ around_perform :measure_time
359
+ after_perform :log_completion
360
+
361
+ def perform(data)
362
+ process(data)
363
+ end
364
+
365
+ private
366
+ def log_start
367
+ Rails.logger.info "Starting job: #{job_id}"
368
+ end
369
+
370
+ def measure_time
371
+ start_time = Time.current
372
+ yield
373
+ duration = Time.current - start_time
374
+ Rails.logger.info "Job completed in #{duration}s"
375
+ end
376
+
377
+ def log_completion
378
+ Rails.logger.info "Job completed: #{job_id}"
379
+ end
380
+ end
381
+ ```
382
+
383
+ ### Error Callbacks
384
+
385
+ ```ruby
386
+ class ReportJob < ApplicationJob
387
+ rescue_from StandardError do |exception|
388
+ handle_error(exception)
389
+ end
390
+
391
+ def perform(report_id)
392
+ generate_report(report_id)
393
+ end
394
+
395
+ private
396
+ def handle_error(exception)
397
+ ErrorNotifier.notify(exception, job: self)
398
+ Report.find(arguments.first).mark_as_failed!
399
+ end
400
+ end
401
+ ```
402
+
403
+ ---
404
+
405
+ ## Testing Jobs
406
+
407
+ ### Basic Job Test
408
+
409
+ ```ruby
410
+ class NotificationJobTest < ActiveJob::TestCase
411
+ test "enqueues job" do
412
+ assert_enqueued_with(job: NotificationJob, args: [users(:david), "test"]) do
413
+ NotificationJob.perform_later(users(:david), "test")
414
+ end
415
+ end
416
+
417
+ test "performs job" do
418
+ user = users(:david)
419
+
420
+ assert_difference -> { user.notifications.count }, +1 do
421
+ NotificationJob.perform_now(user, "test message")
422
+ end
423
+ end
424
+
425
+ test "job is enqueued on correct queue" do
426
+ assert_enqueued_with(job: NotificationJob, queue: "default") do
427
+ NotificationJob.perform_later(users(:david), "test")
428
+ end
429
+ end
430
+ end
431
+ ```
432
+
433
+ ### Testing Retry Logic
434
+
435
+ ```ruby
436
+ test "retries on transient error" do
437
+ user = users(:david)
438
+
439
+ # Stub method to raise error
440
+ UserMailer.stub(:welcome, -> { raise Net::HTTPServerError }) do
441
+ assert_enqueued_jobs 2 do # Original + 1 retry
442
+ perform_enqueued_jobs do
443
+ WelcomeEmailJob.perform_later(user)
444
+ rescue Net::HTTPServerError
445
+ # Expected to raise after retries
446
+ end
447
+ end
448
+ end
449
+ end
450
+
451
+ test "discards on permanent error" do
452
+ assert_no_enqueued_jobs do
453
+ perform_enqueued_jobs do
454
+ ProcessUserJob.perform_later(999999) # Non-existent ID
455
+ rescue ActiveRecord::RecordNotFound
456
+ # Job should be discarded, not retried
457
+ end
458
+ end
459
+ end
460
+ ```
461
+
462
+ ### Testing Recurring Jobs
463
+
464
+ ```ruby
465
+ test "auto postpone job postpones stale cards" do
466
+ stale_card = cards(:old)
467
+ stale_card.update!(last_active_at: 2.months.ago)
468
+
469
+ recent_card = cards(:new)
470
+ recent_card.update!(last_active_at: 1.day.ago)
471
+
472
+ Card::AutoPostponeJob.perform_now
473
+
474
+ assert stale_card.reload.postponed?
475
+ assert_not recent_card.reload.postponed?
476
+ end
477
+ ```
478
+
479
+ ---
480
+
481
+ ## Advanced Patterns
482
+
483
+ ### Batch Processing
484
+
485
+ ```ruby
486
+ class BatchImportJob < ApplicationJob
487
+ def perform(file_path)
488
+ CSV.foreach(file_path, headers: true).each_slice(100) do |batch|
489
+ batch.each do |row|
490
+ ImportRowJob.perform_later(row.to_h)
491
+ end
492
+ end
493
+ end
494
+ end
495
+ ```
496
+
497
+ ### Progress Tracking
498
+
499
+ ```ruby
500
+ class LongRunningJob < ApplicationJob
501
+ def perform(total_items)
502
+ total_items.times do |i|
503
+ process_item(i)
504
+ update_progress(i + 1, total_items)
505
+ end
506
+ end
507
+
508
+ private
509
+ def update_progress(current, total)
510
+ percentage = (current.to_f / total * 100).round
511
+ Rails.cache.write("job:#{job_id}:progress", percentage)
512
+ end
513
+ end
514
+
515
+ # Check progress
516
+ progress = Rails.cache.read("job:#{job_id}:progress")
517
+ ```
518
+
519
+ ### Job Chaining
520
+
521
+ ```ruby
522
+ class ProcessDataJob < ApplicationJob
523
+ def perform(data_id)
524
+ data = Data.find(data_id)
525
+ data.process!
526
+
527
+ # Enqueue next job after completion
528
+ GenerateReportJob.set(wait: 5.minutes).perform_later(data_id)
529
+ end
530
+ end
531
+ ```
532
+
533
+ ### Unique Jobs
534
+
535
+ ```ruby
536
+ class UniqueProcessJob < ApplicationJob
537
+ def perform(user_id)
538
+ # Use lock to prevent duplicate jobs
539
+ lock_key = "unique_job:#{user_id}"
540
+
541
+ Rails.cache.fetch(lock_key, expires_in: 1.hour, race_condition_ttl: 10.seconds) do
542
+ process_user(user_id)
543
+ true
544
+ end
545
+ end
546
+ end
547
+ ```
548
+
549
+ ---
550
+
551
+ ## Monitoring & Debugging
552
+
553
+ ### Job Callbacks for Monitoring
554
+
555
+ ```ruby
556
+ class ApplicationJob < ActiveJob::Base
557
+ around_perform do |job, block|
558
+ start_time = Time.current
559
+
560
+ block.call
561
+
562
+ duration = Time.current - start_time
563
+ Metrics.record("job.duration", duration, tags: { job: job.class.name })
564
+ end
565
+
566
+ rescue_from StandardError do |exception|
567
+ Metrics.increment("job.error", tags: { job: self.class.name })
568
+ ErrorTracker.notify(exception, job_id: job_id, arguments: arguments)
569
+ raise
570
+ end
571
+ end
572
+ ```
573
+
574
+ ### Job Inspection
575
+
576
+ ```ruby
577
+ # In console
578
+ job = NotificationJob.new(user, "test")
579
+ job.serialize # => Hash representation
580
+ job.queue_name # => "default"
581
+ job.priority # => nil
582
+ job.scheduled_at # => Time
583
+ ```
584
+
585
+ ---
586
+
587
+ ## Best Practices
588
+
589
+ ### ✅ DO
590
+
591
+ 1. **Keep jobs simple**
592
+ ```ruby
593
+ # Good - delegates to model
594
+ class ProcessCardJob < ApplicationJob
595
+ def perform(card)
596
+ card.process
597
+ end
598
+ end
599
+ ```
600
+
601
+ 2. **Use `_later` suffix**
602
+ ```ruby
603
+ # Model
604
+ def send_notifications_later
605
+ NotificationJob.perform_later(self)
606
+ end
607
+
608
+ def send_notifications_now
609
+ # Implementation
610
+ end
611
+ ```
612
+
613
+ 3. **Make jobs idempotent**
614
+ ```ruby
615
+ def perform(user_id)
616
+ user = User.find(user_id)
617
+
618
+ # Idempotent - can run multiple times safely
619
+ user.update(processed: true) unless user.processed?
620
+ end
621
+ ```
622
+
623
+ 4. **Use appropriate queues**
624
+ ```ruby
625
+ class UrgentJob < ApplicationJob
626
+ queue_as :urgent
627
+ end
628
+
629
+ class ReportJob < ApplicationJob
630
+ queue_as :low_priority
631
+ end
632
+ ```
633
+
634
+ 5. **Handle errors appropriately**
635
+ ```ruby
636
+ retry_on TransientError, wait: :exponentially_longer
637
+ discard_on PermanentError
638
+ ```
639
+
640
+ ### ❌ DON'T
641
+
642
+ 1. **Complex business logic in jobs**
643
+ ```ruby
644
+ # Bad - too much logic
645
+ class ProcessJob < ApplicationJob
646
+ def perform(data)
647
+ # 100 lines of business logic
648
+ end
649
+ end
650
+
651
+ # Good - delegate to model
652
+ class ProcessJob < ApplicationJob
653
+ def perform(model)
654
+ model.process
655
+ end
656
+ end
657
+ ```
658
+
659
+ 2. **Long-running jobs without batching**
660
+ ```ruby
661
+ # Bad - processes everything at once
662
+ def perform
663
+ User.all.each { |user| process(user) }
664
+ end
665
+
666
+ # Good - batch processing
667
+ def perform
668
+ User.find_in_batches(batch_size: 100) do |batch|
669
+ batch.each { |user| ProcessUserJob.perform_later(user) }
670
+ end
671
+ end
672
+ ```
673
+
674
+ 3. **Ignoring job failures**
675
+ ```ruby
676
+ # Bad - swallows all errors
677
+ def perform(data)
678
+ process(data)
679
+ rescue => e
680
+ # Silent failure
681
+ end
682
+
683
+ # Good - let errors bubble up for retry logic
684
+ def perform(data)
685
+ process(data)
686
+ end
687
+ ```
688
+
689
+ ---
690
+
691
+ ## Summary
692
+
693
+ - **Purpose**: Background processing, async tasks
694
+ - **Delegation**: Jobs delegate to models, keep thin
695
+ - **Naming**: Use `_later` suffix for async methods
696
+ - **Queues**: Organize by priority and type
697
+ - **Error Handling**: Retry transient errors, discard permanent
698
+ - **Testing**: Test enqueuing, performing, and error handling
699
+ - **Monitoring**: Track job performance and failures
700
+ - **Idempotency**: Make jobs safe to retry