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.
- agent_notes/VERSION +1 -0
- agent_notes/__init__.py +1 -0
- agent_notes/__main__.py +4 -0
- agent_notes/cli.py +348 -0
- agent_notes/commands/__init__.py +27 -0
- agent_notes/commands/_install_helpers.py +262 -0
- agent_notes/commands/build.py +170 -0
- agent_notes/commands/doctor.py +112 -0
- agent_notes/commands/info.py +95 -0
- agent_notes/commands/install.py +99 -0
- agent_notes/commands/list.py +169 -0
- agent_notes/commands/memory.py +430 -0
- agent_notes/commands/regenerate.py +152 -0
- agent_notes/commands/set_role.py +143 -0
- agent_notes/commands/uninstall.py +26 -0
- agent_notes/commands/update.py +169 -0
- agent_notes/commands/validate.py +199 -0
- agent_notes/commands/wizard.py +720 -0
- agent_notes/config.py +154 -0
- agent_notes/data/agents/agents.yaml +352 -0
- agent_notes/data/agents/analyst.md +45 -0
- agent_notes/data/agents/api-reviewer.md +47 -0
- agent_notes/data/agents/architect.md +46 -0
- agent_notes/data/agents/coder.md +28 -0
- agent_notes/data/agents/database-specialist.md +45 -0
- agent_notes/data/agents/debugger.md +47 -0
- agent_notes/data/agents/devil.md +47 -0
- agent_notes/data/agents/devops.md +38 -0
- agent_notes/data/agents/explorer.md +23 -0
- agent_notes/data/agents/integrations.md +44 -0
- agent_notes/data/agents/lead.md +216 -0
- agent_notes/data/agents/performance-profiler.md +44 -0
- agent_notes/data/agents/refactorer.md +48 -0
- agent_notes/data/agents/reviewer.md +44 -0
- agent_notes/data/agents/security-auditor.md +44 -0
- agent_notes/data/agents/system-auditor.md +38 -0
- agent_notes/data/agents/tech-writer.md +32 -0
- agent_notes/data/agents/test-runner.md +36 -0
- agent_notes/data/agents/test-writer.md +39 -0
- agent_notes/data/cli/claude.yaml +25 -0
- agent_notes/data/cli/copilot.yaml +18 -0
- agent_notes/data/cli/opencode.yaml +22 -0
- agent_notes/data/commands/brainstorm.md +8 -0
- agent_notes/data/commands/debug.md +9 -0
- agent_notes/data/commands/review.md +10 -0
- agent_notes/data/global-claude.md +290 -0
- agent_notes/data/global-copilot.md +27 -0
- agent_notes/data/global-opencode.md +40 -0
- agent_notes/data/hooks/session-context.md.tpl +19 -0
- agent_notes/data/models/claude-haiku-4-5.yaml +15 -0
- agent_notes/data/models/claude-opus-4-1.yaml +16 -0
- agent_notes/data/models/claude-opus-4-5.yaml +16 -0
- agent_notes/data/models/claude-opus-4-6.yaml +16 -0
- agent_notes/data/models/claude-opus-4-7.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4-5.yaml +16 -0
- agent_notes/data/models/claude-sonnet-4-6.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4.yaml +16 -0
- agent_notes/data/pricing.yaml +33 -0
- agent_notes/data/roles/orchestrator.yaml +5 -0
- agent_notes/data/roles/reasoner.yaml +5 -0
- agent_notes/data/roles/scout.yaml +5 -0
- agent_notes/data/roles/worker.yaml +5 -0
- agent_notes/data/rules/code-quality.md +9 -0
- agent_notes/data/rules/safety.md +10 -0
- agent_notes/data/scripts/cost-report +211 -0
- agent_notes/data/skills/brainstorming/SKILL.md +57 -0
- agent_notes/data/skills/code-review/SKILL.md +64 -0
- agent_notes/data/skills/debugging-protocol/SKILL.md +51 -0
- agent_notes/data/skills/docker-compose/SKILL.md +318 -0
- agent_notes/data/skills/docker-compose-advanced/SKILL.md +575 -0
- agent_notes/data/skills/docker-dockerfile/SKILL.md +385 -0
- agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +293 -0
- agent_notes/data/skills/git/SKILL.md +87 -0
- agent_notes/data/skills/rails-active-storage/SKILL.md +321 -0
- agent_notes/data/skills/rails-broadcasting/SKILL.md +374 -0
- agent_notes/data/skills/rails-concerns/SKILL.md +806 -0
- agent_notes/data/skills/rails-controllers/SKILL.md +510 -0
- agent_notes/data/skills/rails-controllers-advanced/SKILL.md +441 -0
- agent_notes/data/skills/rails-helpers/SKILL.md +677 -0
- agent_notes/data/skills/rails-initializers/SKILL.md +79 -0
- agent_notes/data/skills/rails-javascript/SKILL.md +567 -0
- agent_notes/data/skills/rails-jobs/SKILL.md +700 -0
- agent_notes/data/skills/rails-kamal/SKILL.md +483 -0
- agent_notes/data/skills/rails-lib/SKILL.md +101 -0
- agent_notes/data/skills/rails-mailers/SKILL.md +321 -0
- agent_notes/data/skills/rails-migrations/SKILL.md +268 -0
- agent_notes/data/skills/rails-models/SKILL.md +459 -0
- agent_notes/data/skills/rails-models-advanced/SKILL.md +398 -0
- agent_notes/data/skills/rails-routes/SKILL.md +804 -0
- agent_notes/data/skills/rails-style/SKILL.md +538 -0
- agent_notes/data/skills/rails-testing-controllers/SKILL.md +343 -0
- agent_notes/data/skills/rails-testing-models/SKILL.md +296 -0
- agent_notes/data/skills/rails-testing-system/SKILL.md +375 -0
- agent_notes/data/skills/rails-validations/SKILL.md +108 -0
- agent_notes/data/skills/rails-view-components/SKILL.md +511 -0
- agent_notes/data/skills/rails-view-components-advanced/SKILL.md +376 -0
- agent_notes/data/skills/rails-views/SKILL.md +413 -0
- agent_notes/data/skills/rails-views-advanced/SKILL.md +450 -0
- agent_notes/data/skills/refactoring-protocol/SKILL.md +64 -0
- agent_notes/data/skills/tdd/SKILL.md +57 -0
- agent_notes/data/templates/__init__.py +1 -0
- agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__init__.py +1 -0
- agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/cursor.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/claude.py +44 -0
- agent_notes/data/templates/frontmatter/opencode.py +104 -0
- agent_notes/doctor_checks.py +189 -0
- agent_notes/domain/__init__.py +17 -0
- agent_notes/domain/agent.py +34 -0
- agent_notes/domain/cli_backend.py +40 -0
- agent_notes/domain/diagnostics.py +29 -0
- agent_notes/domain/diff.py +44 -0
- agent_notes/domain/model.py +27 -0
- agent_notes/domain/role.py +13 -0
- agent_notes/domain/rule.py +13 -0
- agent_notes/domain/skill.py +15 -0
- agent_notes/domain/state.py +46 -0
- agent_notes/install_state.py +11 -0
- agent_notes/registries/__init__.py +16 -0
- agent_notes/registries/_base.py +46 -0
- agent_notes/registries/agent_registry.py +107 -0
- agent_notes/registries/cli_registry.py +89 -0
- agent_notes/registries/model_registry.py +85 -0
- agent_notes/registries/role_registry.py +64 -0
- agent_notes/registries/rule_registry.py +80 -0
- agent_notes/registries/skill_registry.py +141 -0
- agent_notes/services/__init__.py +8 -0
- agent_notes/services/diagnostics/__init__.py +47 -0
- agent_notes/services/diagnostics/_checks.py +272 -0
- agent_notes/services/diagnostics/_display.py +346 -0
- agent_notes/services/diagnostics/_fix.py +169 -0
- agent_notes/services/diff.py +349 -0
- agent_notes/services/fs.py +195 -0
- agent_notes/services/install_state_builder.py +210 -0
- agent_notes/services/installer.py +293 -0
- agent_notes/services/memory_backend.py +155 -0
- agent_notes/services/rendering.py +329 -0
- agent_notes/services/session_context.py +23 -0
- agent_notes/services/settings_writer.py +79 -0
- agent_notes/services/state_store.py +249 -0
- agent_notes/services/ui.py +419 -0
- agent_notes/services/user_config.py +62 -0
- agent_notes/services/validation.py +67 -0
- agent_notes/state.py +21 -0
- agent_notes-2.0.4.dist-info/METADATA +14 -0
- agent_notes-2.0.4.dist-info/RECORD +162 -0
- agent_notes-2.0.4.dist-info/WHEEL +5 -0
- agent_notes-2.0.4.dist-info/entry_points.txt +2 -0
- agent_notes-2.0.4.dist-info/licenses/LICENSE +21 -0
- agent_notes-2.0.4.dist-info/top_level.txt +2 -0
- tests/conftest.py +20 -0
- tests/functional/__init__.py +0 -0
- tests/functional/test_build_commands.py +88 -0
- tests/functional/test_registries.py +128 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_build_output.py +129 -0
- tests/plugins/__init__.py +0 -0
- tests/plugins/test_agents.py +93 -0
- 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
|