-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f2649e0
commit 4eb3369
Showing
6 changed files
with
271 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
96 changes: 96 additions & 0 deletions
96
src/hope_payment_gateway/apps/gateway/migrations/0027_asyncjob.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
# Generated by Django 5.1.4 on 2024-12-17 17:58 | ||
|
||
import concurrency.fields | ||
import django.db.models.deletion | ||
from django.conf import settings | ||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
("gateway", "0026_alter_paymentinstruction_status"), | ||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name="AsyncJob", | ||
fields=[ | ||
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), | ||
("version", concurrency.fields.AutoIncVersionField(default=0, help_text="record revision number")), | ||
( | ||
"curr_async_result_id", | ||
models.CharField( | ||
blank=True, | ||
editable=False, | ||
help_text="Current (active) AsyncResult is", | ||
max_length=36, | ||
null=True, | ||
), | ||
), | ||
( | ||
"last_async_result_id", | ||
models.CharField( | ||
blank=True, editable=False, help_text="Latest executed AsyncResult is", max_length=36, null=True | ||
), | ||
), | ||
("datetime_created", models.DateTimeField(auto_now_add=True, help_text="Creation date and time")), | ||
( | ||
"datetime_queued", | ||
models.DateTimeField( | ||
blank=True, help_text="Queueing date and time", null=True, verbose_name="Queued At" | ||
), | ||
), | ||
( | ||
"repeatable", | ||
models.BooleanField( | ||
blank=True, default=False, help_text="Indicate if the job can be repeated as-is" | ||
), | ||
), | ||
("celery_history", models.JSONField(blank=True, default=dict, editable=False)), | ||
("local_status", models.CharField(blank=True, default="", editable=False, max_length=100, null=True)), | ||
( | ||
"group_key", | ||
models.CharField( | ||
blank=True, | ||
editable=False, | ||
help_text="Tasks with the same group key will not run in parallel", | ||
max_length=255, | ||
null=True, | ||
), | ||
), | ||
( | ||
"type", | ||
models.CharField( | ||
choices=[("FQN", "Operation"), ("ACTION", "Action"), ("TASK", "Task")], max_length=50 | ||
), | ||
), | ||
("config", models.JSONField(blank=True, default=dict)), | ||
("action", models.CharField(blank=True, max_length=500, null=True)), | ||
("description", models.CharField(blank=True, max_length=255, null=True)), | ||
("sentry_id", models.CharField(blank=True, max_length=255, null=True)), | ||
( | ||
"instruction", | ||
models.ForeignKey( | ||
on_delete=django.db.models.deletion.CASCADE, | ||
related_name="jobs", | ||
to="gateway.paymentinstruction", | ||
), | ||
), | ||
( | ||
"owner", | ||
models.ForeignKey( | ||
blank=True, | ||
null=True, | ||
on_delete=django.db.models.deletion.CASCADE, | ||
related_name="%(app_label)s_%(class)s_jobs", | ||
to=settings.AUTH_USER_MODEL, | ||
), | ||
), | ||
], | ||
options={ | ||
"permissions": (("debug_job", "Can debug background jobs"),), | ||
}, | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import contextlib | ||
import logging | ||
from typing import TYPE_CHECKING, Any | ||
|
||
from django.core.cache import cache | ||
|
||
import sentry_sdk | ||
from redis_lock import Lock | ||
|
||
from hope_payment_gateway.apps.gateway.models import AsyncJob | ||
from hope_payment_gateway.celery import app | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
if TYPE_CHECKING: | ||
from redis_lock.django_cache import RedisCache | ||
|
||
cache: RedisCache | ||
|
||
|
||
@contextlib.contextmanager | ||
def lock_job(job: AsyncJob) -> Lock: | ||
lock = None | ||
if job.group_key: | ||
lock_key = f"lock:{job.group_key}" | ||
# Get a lock with a 60-second lifetime but keep renewing it automatically | ||
# to ensure the lock is held for as long as the Python process is running. | ||
lock = cache.lock(lock_key, 60, auto_renewal=True) | ||
yield lock.__enter__() | ||
else: | ||
yield | ||
if lock: | ||
lock.release() | ||
|
||
|
||
@app.task() | ||
def sync_job_task(pk: int, version: int) -> dict[str, Any]: | ||
try: | ||
job: AsyncJob = AsyncJob.objects.select_related("program", "program__country_office", "owner").get( | ||
pk=pk, version=version | ||
) | ||
except AsyncJob.DoesNotExist as e: | ||
sentry_sdk.capture_exception(e) | ||
raise e | ||
|
||
with lock_job(job): | ||
try: | ||
scope = sentry_sdk.get_current_scope() | ||
sentry_sdk.set_tag("business_area", job.program.country_office.slug) | ||
sentry_sdk.set_tag("project", job.program.name) | ||
sentry_sdk.set_user = {"id": job.owner.pk, "email": job.owner.email} | ||
return job.execute() | ||
except Exception: | ||
# error is logged in job.execute | ||
raise | ||
finally: | ||
scope.clear() | ||
|
||
|
||
@app.task() | ||
def removed_expired_jobs(**kwargs): | ||
AsyncJob.objects.filter(**kwargs).delete() |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.