Forráskód Böngészése

21129 Store queue_name in Job so correctly deleted in RQ (#21309)

* Add queue name to Job

* Add queue name to serializer, filterset, detail view

* fix job queue delete

* fix job queue delete

* review feedback
Arthur Hanson 2 hete
szülő
commit
c44e8606f7

+ 2 - 1
netbox/core/api/serializers_/jobs.py

@@ -31,7 +31,8 @@ class JobSerializer(BaseModelSerializer):
         model = Job
         fields = [
             'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'object', 'name', 'status', 'created',
-            'scheduled', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'log_entries',
+            'scheduled', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'queue_name',
+            'log_entries',
         ]
         brief_fields = ('url', 'created', 'completed', 'user', 'status')
 

+ 5 - 1
netbox/core/filtersets.py

@@ -129,10 +129,14 @@ class JobFilterSet(BaseFilterSet):
         choices=JobStatusChoices,
         null_value=None
     )
+    queue_name = django_filters.CharFilter()
 
     class Meta:
         model = Job
-        fields = ('id', 'object_type', 'object_type_id', 'object_id', 'name', 'interval', 'status', 'user', 'job_id')
+        fields = (
+            'id', 'object_type', 'object_type_id', 'object_id', 'name', 'interval', 'status', 'user', 'job_id',
+            'queue_name',
+        )
 
     def search(self, queryset, name, value):
         if not value.strip():

+ 5 - 1
netbox/core/forms/filtersets.py

@@ -72,7 +72,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
     model = Job
     fieldsets = (
         FieldSet('q', 'filter_id'),
-        FieldSet('object_type_id', 'status', name=_('Attributes')),
+        FieldSet('object_type_id', 'status', 'queue_name', name=_('Attributes')),
         FieldSet(
             'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
             'started__after', 'completed__before', 'completed__after', 'user', name=_('Creation')
@@ -88,6 +88,10 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
         choices=JobStatusChoices,
         required=False
     )
+    queue_name = forms.CharField(
+        label=_('Queue'),
+        required=False
+    )
     created__after = forms.DateTimeField(
         label=_('Created after'),
         required=False,

+ 18 - 0
netbox/core/migrations/0021_job_queue_name.py

@@ -0,0 +1,18 @@
+# Generated by Django 5.2.9 on 2026-01-27 00:39
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0020_owner'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='job',
+            name='queue_name',
+            field=models.CharField(blank=True, max_length=100),
+        ),
+    ]

+ 14 - 3
netbox/core/models/jobs.py

@@ -112,6 +112,12 @@ class Job(models.Model):
         verbose_name=_('job ID'),
         unique=True
     )
+    queue_name = models.CharField(
+        verbose_name=_('queue name'),
+        max_length=100,
+        blank=True,
+        help_text=_('Name of the queue in which this job was enqueued')
+    )
     log_entries = ArrayField(
         verbose_name=_('log entries'),
         base_field=models.JSONField(
@@ -179,11 +185,15 @@ class Job(models.Model):
         return f"{int(minutes)} minutes, {seconds:.2f} seconds"
 
     def delete(self, *args, **kwargs):
+        # Use the stored queue name, or fall back to get_queue_for_model for legacy jobs
+        rq_queue_name = self.queue_name or get_queue_for_model(self.object_type.model if self.object_type else None)
+        rq_job_id = str(self.job_id)
+
         super().delete(*args, **kwargs)
 
-        rq_queue_name = get_queue_for_model(self.object_type.model if self.object_type else None)
+        # Cancel the RQ job using the stored queue name
         queue = django_rq.get_queue(rq_queue_name)
-        job = queue.fetch_job(str(self.job_id))
+        job = queue.fetch_job(rq_job_id)
 
         if job:
             try:
@@ -288,7 +298,8 @@ class Job(models.Model):
             scheduled=schedule_at,
             interval=interval,
             user=user,
-            job_id=uuid.uuid4()
+            job_id=uuid.uuid4(),
+            queue_name=rq_queue_name
         )
         job.full_clean()
         job.save()

+ 4 - 1
netbox/core/tables/jobs.py

@@ -42,6 +42,9 @@ class JobTable(NetBoxTable):
     completed = columns.DateTimeColumn(
         verbose_name=_('Completed'),
     )
+    queue_name = tables.Column(
+        verbose_name=_('Queue'),
+    )
     log_entries = tables.Column(
         verbose_name=_('Log Entries'),
     )
@@ -53,7 +56,7 @@ class JobTable(NetBoxTable):
         model = Job
         fields = (
             'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'scheduled', 'interval', 'started',
-            'completed', 'user', 'error', 'job_id',
+            'completed', 'user', 'queue_name', 'log_entries', 'error', 'job_id',
         )
         default_columns = (
             'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',

+ 38 - 1
netbox/core/tests/test_models.py

@@ -1,8 +1,10 @@
+from unittest.mock import patch, MagicMock
+
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ObjectDoesNotExist
 from django.test import TestCase
 
-from core.models import DataSource, ObjectType
+from core.models import DataSource, Job, ObjectType
 from core.choices import ObjectChangeActionChoices
 from dcim.models import Site, Location, Device
 from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
@@ -200,3 +202,38 @@ class ObjectTypeTest(TestCase):
         bookmarks_ots = ObjectType.objects.with_feature('bookmarks')
         self.assertIn(ObjectType.objects.get_by_natural_key('dcim', 'site'), bookmarks_ots)
         self.assertNotIn(ObjectType.objects.get_by_natural_key('dcim', 'cabletermination'), bookmarks_ots)
+
+
+class JobTest(TestCase):
+
+    @patch('core.models.jobs.django_rq.get_queue')
+    def test_delete_cancels_job_from_correct_queue(self, mock_get_queue):
+        """
+        Test that when a job is deleted, it's canceled from the correct queue.
+        """
+        mock_queue = MagicMock()
+        mock_rq_job = MagicMock()
+        mock_queue.fetch_job.return_value = mock_rq_job
+        mock_get_queue.return_value = mock_queue
+
+        def dummy_func(**kwargs):
+            pass
+
+        # Enqueue a job with a custom queue name
+        custom_queue = 'my_custom_queue'
+        job = Job.enqueue(
+            func=dummy_func,
+            name='Test Job',
+            queue_name=custom_queue
+        )
+
+        # Reset mock to clear enqueue call
+        mock_get_queue.reset_mock()
+
+        # Delete the job
+        job.delete()
+
+        # Verify the correct queue was used for cancellation
+        mock_get_queue.assert_called_with(custom_queue)
+        mock_queue.fetch_job.assert_called_with(str(job.job_id))
+        mock_rq_job.cancel.assert_called_once()

+ 4 - 0
netbox/templates/core/job.html

@@ -59,6 +59,10 @@
             <th scope="row">{% trans "Completed" %}</th>
             <td>{{ object.completed|isodatetime|placeholder }}</td>
           </tr>
+          <tr>
+            <th scope="row">{% trans "Queue" %}</th>
+            <td>{{ object.queue_name|placeholder }}</td>
+          </tr>
         </table>
       </div>
     </div>