@malamute/ai-rules 1.0.0 → 1.2.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.
- package/README.md +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/tasks/**/*.py"
|
|
4
|
+
- "**/celery.py"
|
|
5
|
+
- "**/celeryconfig.py"
|
|
6
|
+
- "**/*_task.py"
|
|
7
|
+
- "**/*_tasks.py"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Python Celery
|
|
11
|
+
|
|
12
|
+
## Configuration
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
# celery.py
|
|
16
|
+
from celery import Celery
|
|
17
|
+
from kombu import Queue, Exchange
|
|
18
|
+
|
|
19
|
+
app = Celery("myapp")
|
|
20
|
+
|
|
21
|
+
app.config_from_object({
|
|
22
|
+
# Broker
|
|
23
|
+
"broker_url": "redis://localhost:6379/0",
|
|
24
|
+
"result_backend": "redis://localhost:6379/1",
|
|
25
|
+
|
|
26
|
+
# Serialization
|
|
27
|
+
"task_serializer": "json",
|
|
28
|
+
"result_serializer": "json",
|
|
29
|
+
"accept_content": ["json"],
|
|
30
|
+
|
|
31
|
+
# Task settings
|
|
32
|
+
"task_acks_late": True,
|
|
33
|
+
"task_reject_on_worker_lost": True,
|
|
34
|
+
"task_time_limit": 300, # 5 minutes hard limit
|
|
35
|
+
"task_soft_time_limit": 240, # 4 minutes soft limit
|
|
36
|
+
|
|
37
|
+
# Result settings
|
|
38
|
+
"result_expires": 3600, # 1 hour
|
|
39
|
+
"result_extended": True,
|
|
40
|
+
|
|
41
|
+
# Worker settings
|
|
42
|
+
"worker_prefetch_multiplier": 1,
|
|
43
|
+
"worker_concurrency": 4,
|
|
44
|
+
|
|
45
|
+
# Task routing
|
|
46
|
+
"task_queues": [
|
|
47
|
+
Queue("default", Exchange("default"), routing_key="default"),
|
|
48
|
+
Queue("high_priority", Exchange("high_priority"), routing_key="high_priority"),
|
|
49
|
+
Queue("low_priority", Exchange("low_priority"), routing_key="low_priority"),
|
|
50
|
+
],
|
|
51
|
+
"task_default_queue": "default",
|
|
52
|
+
"task_routes": {
|
|
53
|
+
"app.tasks.email.*": {"queue": "high_priority"},
|
|
54
|
+
"app.tasks.reports.*": {"queue": "low_priority"},
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
# Beat scheduler
|
|
58
|
+
"beat_scheduler": "celery.beat:PersistentScheduler",
|
|
59
|
+
"beat_schedule_filename": "celerybeat-schedule",
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
# Auto-discover tasks
|
|
63
|
+
app.autodiscover_tasks(["app.tasks"])
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Basic Tasks
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# tasks/email.py
|
|
70
|
+
from celery import shared_task
|
|
71
|
+
from celery.exceptions import MaxRetriesExceededError
|
|
72
|
+
import logging
|
|
73
|
+
|
|
74
|
+
logger = logging.getLogger(__name__)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@shared_task(
|
|
78
|
+
bind=True,
|
|
79
|
+
max_retries=3,
|
|
80
|
+
default_retry_delay=60,
|
|
81
|
+
autoretry_for=(Exception,),
|
|
82
|
+
retry_backoff=True,
|
|
83
|
+
retry_backoff_max=600,
|
|
84
|
+
retry_jitter=True,
|
|
85
|
+
)
|
|
86
|
+
def send_email(self, to: str, subject: str, body: str) -> dict:
|
|
87
|
+
"""Send email with automatic retry."""
|
|
88
|
+
try:
|
|
89
|
+
logger.info(f"Sending email to {to}")
|
|
90
|
+
# Email sending logic
|
|
91
|
+
return {"status": "sent", "to": to}
|
|
92
|
+
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
logger.error(f"Failed to send email: {exc}")
|
|
95
|
+
raise self.retry(exc=exc)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@shared_task(bind=True)
|
|
99
|
+
def send_bulk_emails(self, recipients: list[str], subject: str, body: str) -> dict:
|
|
100
|
+
"""Send emails in bulk with progress tracking."""
|
|
101
|
+
total = len(recipients)
|
|
102
|
+
successful = 0
|
|
103
|
+
failed = []
|
|
104
|
+
|
|
105
|
+
for i, recipient in enumerate(recipients):
|
|
106
|
+
try:
|
|
107
|
+
send_email.delay(recipient, subject, body)
|
|
108
|
+
successful += 1
|
|
109
|
+
except Exception as e:
|
|
110
|
+
failed.append({"email": recipient, "error": str(e)})
|
|
111
|
+
|
|
112
|
+
# Update progress
|
|
113
|
+
self.update_state(
|
|
114
|
+
state="PROGRESS",
|
|
115
|
+
meta={"current": i + 1, "total": total, "successful": successful},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return {"total": total, "successful": successful, "failed": failed}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Task with Database
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
# tasks/orders.py
|
|
125
|
+
from celery import shared_task
|
|
126
|
+
from sqlalchemy.orm import Session
|
|
127
|
+
from app.db.session import SessionLocal
|
|
128
|
+
from app.models import Order, OrderStatus
|
|
129
|
+
from contextlib import contextmanager
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@contextmanager
|
|
133
|
+
def get_db():
|
|
134
|
+
db = SessionLocal()
|
|
135
|
+
try:
|
|
136
|
+
yield db
|
|
137
|
+
finally:
|
|
138
|
+
db.close()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@shared_task(bind=True)
|
|
142
|
+
def process_order(self, order_id: str) -> dict:
|
|
143
|
+
"""Process an order."""
|
|
144
|
+
with get_db() as db:
|
|
145
|
+
order = db.query(Order).filter(Order.id == order_id).first()
|
|
146
|
+
|
|
147
|
+
if not order:
|
|
148
|
+
return {"status": "error", "message": "Order not found"}
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
order.status = OrderStatus.PROCESSING
|
|
152
|
+
db.commit()
|
|
153
|
+
|
|
154
|
+
# Process order logic
|
|
155
|
+
process_payment(order)
|
|
156
|
+
reserve_inventory(order)
|
|
157
|
+
schedule_shipping(order)
|
|
158
|
+
|
|
159
|
+
order.status = OrderStatus.COMPLETED
|
|
160
|
+
db.commit()
|
|
161
|
+
|
|
162
|
+
return {"status": "completed", "order_id": order_id}
|
|
163
|
+
|
|
164
|
+
except Exception as e:
|
|
165
|
+
order.status = OrderStatus.FAILED
|
|
166
|
+
order.error_message = str(e)
|
|
167
|
+
db.commit()
|
|
168
|
+
raise
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Chaining and Groups
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
# tasks/workflows.py
|
|
175
|
+
from celery import shared_task, chain, group, chord
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@shared_task
|
|
179
|
+
def fetch_data(url: str) -> dict:
|
|
180
|
+
# Fetch data from URL
|
|
181
|
+
return {"data": "..."}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@shared_task
|
|
185
|
+
def process_data(data: dict) -> dict:
|
|
186
|
+
# Process the data
|
|
187
|
+
return {"processed": True}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@shared_task
|
|
191
|
+
def save_data(data: dict) -> dict:
|
|
192
|
+
# Save to database
|
|
193
|
+
return {"saved": True}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@shared_task
|
|
197
|
+
def aggregate_results(results: list) -> dict:
|
|
198
|
+
# Aggregate all results
|
|
199
|
+
return {"aggregated": results}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# Chain: Sequential execution
|
|
203
|
+
def run_pipeline(url: str):
|
|
204
|
+
workflow = chain(
|
|
205
|
+
fetch_data.s(url),
|
|
206
|
+
process_data.s(),
|
|
207
|
+
save_data.s(),
|
|
208
|
+
)
|
|
209
|
+
return workflow.apply_async()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# Group: Parallel execution
|
|
213
|
+
def process_multiple_urls(urls: list[str]):
|
|
214
|
+
job = group(fetch_data.s(url) for url in urls)
|
|
215
|
+
return job.apply_async()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# Chord: Parallel with callback
|
|
219
|
+
def process_and_aggregate(urls: list[str]):
|
|
220
|
+
workflow = chord(
|
|
221
|
+
group(fetch_data.s(url) for url in urls),
|
|
222
|
+
aggregate_results.s(),
|
|
223
|
+
)
|
|
224
|
+
return workflow.apply_async()
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Scheduled Tasks (Beat)
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
# celery.py
|
|
231
|
+
from celery.schedules import crontab
|
|
232
|
+
|
|
233
|
+
app.conf.beat_schedule = {
|
|
234
|
+
# Run every minute
|
|
235
|
+
"check-pending-orders": {
|
|
236
|
+
"task": "app.tasks.orders.check_pending_orders",
|
|
237
|
+
"schedule": 60.0,
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
# Run every hour
|
|
241
|
+
"cleanup-expired-sessions": {
|
|
242
|
+
"task": "app.tasks.cleanup.cleanup_sessions",
|
|
243
|
+
"schedule": crontab(minute=0),
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
# Run daily at midnight
|
|
247
|
+
"generate-daily-report": {
|
|
248
|
+
"task": "app.tasks.reports.generate_daily_report",
|
|
249
|
+
"schedule": crontab(hour=0, minute=0),
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
# Run every Monday at 9am
|
|
253
|
+
"send-weekly-digest": {
|
|
254
|
+
"task": "app.tasks.email.send_weekly_digest",
|
|
255
|
+
"schedule": crontab(hour=9, minute=0, day_of_week=1),
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
# Run on first day of month
|
|
259
|
+
"generate-monthly-invoice": {
|
|
260
|
+
"task": "app.tasks.billing.generate_monthly_invoices",
|
|
261
|
+
"schedule": crontab(hour=0, minute=0, day_of_month=1),
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Task Monitoring
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
# tasks/base.py
|
|
270
|
+
from celery import Task
|
|
271
|
+
from celery.signals import (
|
|
272
|
+
task_prerun,
|
|
273
|
+
task_postrun,
|
|
274
|
+
task_failure,
|
|
275
|
+
task_success,
|
|
276
|
+
)
|
|
277
|
+
import time
|
|
278
|
+
import logging
|
|
279
|
+
|
|
280
|
+
logger = logging.getLogger(__name__)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class BaseTask(Task):
|
|
284
|
+
"""Base task with monitoring."""
|
|
285
|
+
|
|
286
|
+
abstract = True
|
|
287
|
+
|
|
288
|
+
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
|
289
|
+
logger.error(
|
|
290
|
+
f"Task {self.name}[{task_id}] failed: {exc}",
|
|
291
|
+
extra={"task_id": task_id, "args": args, "kwargs": kwargs},
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def on_success(self, retval, task_id, args, kwargs):
|
|
295
|
+
logger.info(
|
|
296
|
+
f"Task {self.name}[{task_id}] succeeded",
|
|
297
|
+
extra={"task_id": task_id, "result": retval},
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def on_retry(self, exc, task_id, args, kwargs, einfo):
|
|
301
|
+
logger.warning(
|
|
302
|
+
f"Task {self.name}[{task_id}] retrying: {exc}",
|
|
303
|
+
extra={"task_id": task_id},
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@task_prerun.connect
|
|
308
|
+
def task_prerun_handler(task_id, task, args, kwargs, **extras):
|
|
309
|
+
task.start_time = time.time()
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@task_postrun.connect
|
|
313
|
+
def task_postrun_handler(task_id, task, args, kwargs, retval, state, **extras):
|
|
314
|
+
duration = time.time() - getattr(task, "start_time", time.time())
|
|
315
|
+
logger.info(f"Task {task.name} completed in {duration:.2f}s")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# Usage
|
|
319
|
+
@shared_task(base=BaseTask)
|
|
320
|
+
def my_task():
|
|
321
|
+
pass
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Task Cancellation
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
# tasks/long_running.py
|
|
328
|
+
from celery import shared_task
|
|
329
|
+
from celery.exceptions import SoftTimeLimitExceeded
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@shared_task(bind=True, soft_time_limit=300)
|
|
333
|
+
def long_running_task(self, data: dict) -> dict:
|
|
334
|
+
"""Task that respects cancellation."""
|
|
335
|
+
results = []
|
|
336
|
+
|
|
337
|
+
for i, item in enumerate(data["items"]):
|
|
338
|
+
# Check if task was revoked
|
|
339
|
+
if self.is_aborted():
|
|
340
|
+
return {"status": "aborted", "processed": i}
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
result = process_item(item)
|
|
344
|
+
results.append(result)
|
|
345
|
+
|
|
346
|
+
except SoftTimeLimitExceeded:
|
|
347
|
+
# Graceful shutdown
|
|
348
|
+
return {"status": "timeout", "processed": i, "results": results}
|
|
349
|
+
|
|
350
|
+
return {"status": "completed", "results": results}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# Revoking tasks
|
|
354
|
+
from celery.result import AsyncResult
|
|
355
|
+
|
|
356
|
+
def cancel_task(task_id: str):
|
|
357
|
+
result = AsyncResult(task_id)
|
|
358
|
+
result.revoke(terminate=True)
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## FastAPI Integration
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
# api/tasks.py
|
|
365
|
+
from fastapi import APIRouter, HTTPException
|
|
366
|
+
from celery.result import AsyncResult
|
|
367
|
+
from app.celery import app as celery_app
|
|
368
|
+
from app.tasks.orders import process_order
|
|
369
|
+
|
|
370
|
+
router = APIRouter(prefix="/tasks", tags=["tasks"])
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@router.post("/orders/{order_id}/process")
|
|
374
|
+
async def start_order_processing(order_id: str) -> dict:
|
|
375
|
+
task = process_order.delay(order_id)
|
|
376
|
+
return {"task_id": task.id, "status": "started"}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@router.get("/{task_id}")
|
|
380
|
+
async def get_task_status(task_id: str) -> dict:
|
|
381
|
+
result = AsyncResult(task_id, app=celery_app)
|
|
382
|
+
|
|
383
|
+
if result.ready():
|
|
384
|
+
if result.successful():
|
|
385
|
+
return {"status": "completed", "result": result.get()}
|
|
386
|
+
else:
|
|
387
|
+
return {"status": "failed", "error": str(result.result)}
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
"status": result.state,
|
|
391
|
+
"progress": result.info if result.info else None,
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@router.delete("/{task_id}")
|
|
396
|
+
async def cancel_task(task_id: str) -> dict:
|
|
397
|
+
result = AsyncResult(task_id, app=celery_app)
|
|
398
|
+
result.revoke(terminate=True)
|
|
399
|
+
return {"status": "cancelled", "task_id": task_id}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Commands
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
# Start worker
|
|
406
|
+
celery -A app.celery worker --loglevel=info
|
|
407
|
+
|
|
408
|
+
# Start worker with queues
|
|
409
|
+
celery -A app.celery worker -Q high_priority,default --loglevel=info
|
|
410
|
+
|
|
411
|
+
# Start beat scheduler
|
|
412
|
+
celery -A app.celery beat --loglevel=info
|
|
413
|
+
|
|
414
|
+
# Start flower (monitoring)
|
|
415
|
+
celery -A app.celery flower --port=5555
|
|
416
|
+
|
|
417
|
+
# Inspect active tasks
|
|
418
|
+
celery -A app.celery inspect active
|
|
419
|
+
|
|
420
|
+
# Purge queue
|
|
421
|
+
celery -A app.celery purge
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## Anti-patterns
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
# BAD: Database connection in task without cleanup
|
|
428
|
+
@shared_task
|
|
429
|
+
def bad_task():
|
|
430
|
+
db = SessionLocal() # Never closed!
|
|
431
|
+
db.query(...)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# GOOD: Use context manager
|
|
435
|
+
@shared_task
|
|
436
|
+
def good_task():
|
|
437
|
+
with get_db() as db:
|
|
438
|
+
db.query(...)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# BAD: Passing ORM objects
|
|
442
|
+
@shared_task
|
|
443
|
+
def bad_task(user: User): # Can't serialize!
|
|
444
|
+
pass
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# GOOD: Pass IDs, fetch in task
|
|
448
|
+
@shared_task
|
|
449
|
+
def good_task(user_id: str):
|
|
450
|
+
with get_db() as db:
|
|
451
|
+
user = db.query(User).get(user_id)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
# BAD: No retry limit
|
|
455
|
+
@shared_task(autoretry_for=(Exception,)) # Infinite retries!
|
|
456
|
+
def bad_task():
|
|
457
|
+
pass
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
# GOOD: Set retry limits
|
|
461
|
+
@shared_task(autoretry_for=(Exception,), max_retries=3)
|
|
462
|
+
def good_task():
|
|
463
|
+
pass
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# BAD: Blocking synchronous calls
|
|
467
|
+
@shared_task
|
|
468
|
+
def bad_task():
|
|
469
|
+
time.sleep(300) # Blocks worker
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# GOOD: Use appropriate timeouts
|
|
473
|
+
@shared_task(soft_time_limit=60, time_limit=120)
|
|
474
|
+
def good_task():
|
|
475
|
+
pass
|
|
476
|
+
```
|