From fb7538926117aef4857cd9d59fbb9530e93ca72b Mon Sep 17 00:00:00 2001 From: Alexander Haase Date: Wed, 24 Jul 2024 15:57:25 +0200 Subject: [PATCH] Add name attribute for BackgroundJob Instead of defining individual names on enqueue, BackgroundJob classes can now set a job name in their meta class. This is equivalent to other Django classes and NetBox scripts. --- docs/plugins/development/background-tasks.md | 15 +++++++++++++-- netbox/core/jobs.py | 3 +++ netbox/extras/jobs.py | 5 +++++ netbox/utilities/jobs.py | 14 +++++++++++--- netbox/utilities/tests/test_jobs.py | 18 +++++++++++++++++- 5 files changed, 49 insertions(+), 6 deletions(-) diff --git a/docs/plugins/development/background-tasks.md b/docs/plugins/development/background-tasks.md index 7a0382987..1d0be6814 100644 --- a/docs/plugins/development/background-tasks.md +++ b/docs/plugins/development/background-tasks.md @@ -17,19 +17,30 @@ A background job implements a basic [Job](../../models/core/job.md) executor for from utilities.jobs import BackgroundJob class MyTestJob(BackgroundJob): + class Meta: + name = "My Test Job" + @classmethod def run(cls, job, *args, **kwargs): obj = job.object # your logic goes here ``` -You can schedule the background job from within your code (e.g. from a model's `save()` method or a view) by calling `MyTestJob.enqueue()`. This method passes through all arguments to `Job.enqueue()`. +You can schedule the background job from within your code (e.g. from a model's `save()` method or a view) by calling `MyTestJob.enqueue()`. This method passes through all arguments to `Job.enqueue()`. However, no `name` argument must be passed, as the background job name will be used instead. ::: core.models.Job.enqueue +#### Job Attributes + +Background job attributes are defined under a class named `Meta` within the job. These are optional, but encouraged. + +##### `name` + +This is the human-friendly names of your background job. If omitted, the class name will be used. + #### Scheduled Jobs -As described above, jobs can be scheduled for immediate execution or at any later time using the `enqueue()` method. However, for management purposes, the `enqueue_once()` method allows a job to be scheduled exactly once avoiding duplicates. If a job is already scheduled for a particular instance, a second one won't be scheduled, respecting thread safety. An example use case would be to schedule a periodic task that is bound to an instance in general, but not to any event of that instance (such as updates). The parameters of the `enqueue_once()` method are identical to those of `enqueue()`. Note that this class doesn't allow you to pass the `name` parameter, but uses a generic name instead. +As described above, jobs can be scheduled for immediate execution or at any later time using the `enqueue()` method. However, for management purposes, the `enqueue_once()` method allows a job to be scheduled exactly once avoiding duplicates. If a job is already scheduled for a particular instance, a second one won't be scheduled, respecting thread safety. An example use case would be to schedule a periodic task that is bound to an instance in general, but not to any event of that instance (such as updates). The parameters of the `enqueue_once()` method are identical to those of `enqueue()`. !!! tip It is not forbidden to `enqueue()` additional jobs while an interval schedule is active. An example use of this would be to schedule a periodic daily synchronization, but also trigger additional synchronizations on demand when the user presses a button. diff --git a/netbox/core/jobs.py b/netbox/core/jobs.py index 8605a6dec..e7e70dad7 100644 --- a/netbox/core/jobs.py +++ b/netbox/core/jobs.py @@ -14,6 +14,9 @@ class SyncDataSourceJob(BackgroundJob): Call sync() on a DataSource. """ + class Meta: + name = 'Synchronization' + @classmethod def run(cls, job, *args, **kwargs): datasource = DataSource.objects.get(pk=job.object_id) diff --git a/netbox/extras/jobs.py b/netbox/extras/jobs.py index 917e2bbdf..eb7eb1681 100644 --- a/netbox/extras/jobs.py +++ b/netbox/extras/jobs.py @@ -21,6 +21,11 @@ class ScriptJob(BackgroundJob): exists outside the Script class to ensure it cannot be overridden by a script author. """ + class Meta: + # An explicit job name is not set because it doesn't make sense in this context. Currently, there's no scenario + # where jobs other than this one are used. Therefore, it is hidden, resulting in a cleaner job table overview. + name = '' + @staticmethod def run_script(script, job, request, data, commit): """ diff --git a/netbox/utilities/jobs.py b/netbox/utilities/jobs.py index c627687b3..07e663cdc 100644 --- a/netbox/utilities/jobs.py +++ b/netbox/utilities/jobs.py @@ -3,6 +3,7 @@ from abc import ABC, abstractmethod from datetime import timedelta from django.db.backends.signals import connection_created +from django.utils.functional import classproperty from django_pglocks import advisory_lock from rq.timeouts import JobTimeoutException @@ -24,6 +25,13 @@ class BackgroundJob(ABC): and scheduling recurring jobs. """ + class Meta: + pass + + @classproperty + def name(cls): + return getattr(cls.Meta, 'name', cls.__name__) + @classmethod @abstractmethod def run(cls, *args, **kwargs): @@ -74,7 +82,7 @@ class BackgroundJob(ABC): return Job.objects.filter( object_type=object_type, object_id=instance.pk, - name=cls.__name__, + name=cls.name, ) @classmethod @@ -85,7 +93,7 @@ class BackgroundJob(ABC): This method is a wrapper of `Job.enqueue()` using `handle()` as function callback. See its documentation for parameters. """ - return Job.enqueue(cls.handle, *args, **kwargs) + return Job.enqueue(cls.handle, name=cls.name, *args, **kwargs) @classmethod @advisory_lock(ADVISORY_LOCK_KEYS['job-schedules']) @@ -115,7 +123,7 @@ class BackgroundJob(ABC): return job job.delete() - return cls.enqueue(instance=instance, name=cls.__name__, interval=interval, *args, **kwargs) + return cls.enqueue(instance=instance, interval=interval, *args, **kwargs) class SystemJob(BackgroundJob): diff --git a/netbox/utilities/tests/test_jobs.py b/netbox/utilities/tests/test_jobs.py index 1e3349268..1f0d4a274 100644 --- a/netbox/utilities/tests/test_jobs.py +++ b/netbox/utilities/tests/test_jobs.py @@ -29,6 +29,22 @@ class BackgroundJobTestCase(TestCase): return timezone.now() + timedelta(weeks=1) +class BackgroundJobTest(BackgroundJobTestCase): + """ + Test internal logic of `BackgroundJob`. + """ + + def test_name_default(self): + self.assertEqual(TestBackgroundJob.name, TestBackgroundJob.__name__) + + def test_name_set(self): + class NamedBackgroundJob(TestBackgroundJob): + class Meta: + name = 'TestName' + + self.assertEqual(NamedBackgroundJob.name, 'TestName') + + class EnqueueTest(BackgroundJobTestCase): """ Test enqueuing of `BackgroundJob`. @@ -40,7 +56,7 @@ class EnqueueTest(BackgroundJobTestCase): job = TestBackgroundJob.enqueue(instance, schedule_at=self.get_schedule_at()) self.assertIsInstance(job, Job) - self.assertEqual(Job.objects.count(), i) + self.assertEqual(TestBackgroundJob.get_jobs(instance).count(), i) def test_enqueue_once(self): job = TestBackgroundJob.enqueue_once(instance=Job(), schedule_at=self.get_schedule_at())