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
This commit is contained in:
Arthur Hanson
2026-01-29 12:29:33 -08:00
committed by GitHub
parent 8e620ef325
commit c44e8606f7
8 changed files with 90 additions and 8 deletions
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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,
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>