Skip to content

Commit c44e860

Browse files
authored
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
1 parent 8e620ef commit c44e860

File tree

8 files changed

+90
-8
lines changed

8 files changed

+90
-8
lines changed

netbox/core/api/serializers_/jobs.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ class Meta:
3131
model = Job
3232
fields = [
3333
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'object', 'name', 'status', 'created',
34-
'scheduled', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'log_entries',
34+
'scheduled', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'queue_name',
35+
'log_entries',
3536
]
3637
brief_fields = ('url', 'created', 'completed', 'user', 'status')
3738

netbox/core/filtersets.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,14 @@ class JobFilterSet(BaseFilterSet):
129129
choices=JobStatusChoices,
130130
null_value=None
131131
)
132+
queue_name = django_filters.CharFilter()
132133

133134
class Meta:
134135
model = Job
135-
fields = ('id', 'object_type', 'object_type_id', 'object_id', 'name', 'interval', 'status', 'user', 'job_id')
136+
fields = (
137+
'id', 'object_type', 'object_type_id', 'object_id', 'name', 'interval', 'status', 'user', 'job_id',
138+
'queue_name',
139+
)
136140

137141
def search(self, queryset, name, value):
138142
if not value.strip():

netbox/core/forms/filtersets.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
7272
model = Job
7373
fieldsets = (
7474
FieldSet('q', 'filter_id'),
75-
FieldSet('object_type_id', 'status', name=_('Attributes')),
75+
FieldSet('object_type_id', 'status', 'queue_name', name=_('Attributes')),
7676
FieldSet(
7777
'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
7878
'started__after', 'completed__before', 'completed__after', 'user', name=_('Creation')
@@ -88,6 +88,10 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
8888
choices=JobStatusChoices,
8989
required=False
9090
)
91+
queue_name = forms.CharField(
92+
label=_('Queue'),
93+
required=False
94+
)
9195
created__after = forms.DateTimeField(
9296
label=_('Created after'),
9397
required=False,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.9 on 2026-01-27 00:39
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('core', '0020_owner'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='job',
15+
name='queue_name',
16+
field=models.CharField(blank=True, max_length=100),
17+
),
18+
]

netbox/core/models/jobs.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ class Job(models.Model):
112112
verbose_name=_('job ID'),
113113
unique=True
114114
)
115+
queue_name = models.CharField(
116+
verbose_name=_('queue name'),
117+
max_length=100,
118+
blank=True,
119+
help_text=_('Name of the queue in which this job was enqueued')
120+
)
115121
log_entries = ArrayField(
116122
verbose_name=_('log entries'),
117123
base_field=models.JSONField(
@@ -179,11 +185,15 @@ def duration(self):
179185
return f"{int(minutes)} minutes, {seconds:.2f} seconds"
180186

181187
def delete(self, *args, **kwargs):
188+
# Use the stored queue name, or fall back to get_queue_for_model for legacy jobs
189+
rq_queue_name = self.queue_name or get_queue_for_model(self.object_type.model if self.object_type else None)
190+
rq_job_id = str(self.job_id)
191+
182192
super().delete(*args, **kwargs)
183193

184-
rq_queue_name = get_queue_for_model(self.object_type.model if self.object_type else None)
194+
# Cancel the RQ job using the stored queue name
185195
queue = django_rq.get_queue(rq_queue_name)
186-
job = queue.fetch_job(str(self.job_id))
196+
job = queue.fetch_job(rq_job_id)
187197

188198
if job:
189199
try:
@@ -288,7 +298,8 @@ def enqueue(
288298
scheduled=schedule_at,
289299
interval=interval,
290300
user=user,
291-
job_id=uuid.uuid4()
301+
job_id=uuid.uuid4(),
302+
queue_name=rq_queue_name
292303
)
293304
job.full_clean()
294305
job.save()

netbox/core/tables/jobs.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ class JobTable(NetBoxTable):
4242
completed = columns.DateTimeColumn(
4343
verbose_name=_('Completed'),
4444
)
45+
queue_name = tables.Column(
46+
verbose_name=_('Queue'),
47+
)
4548
log_entries = tables.Column(
4649
verbose_name=_('Log Entries'),
4750
)
@@ -53,7 +56,7 @@ class Meta(NetBoxTable.Meta):
5356
model = Job
5457
fields = (
5558
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'scheduled', 'interval', 'started',
56-
'completed', 'user', 'error', 'job_id',
59+
'completed', 'user', 'queue_name', 'log_entries', 'error', 'job_id',
5760
)
5861
default_columns = (
5962
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',

netbox/core/tests/test_models.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
from unittest.mock import patch, MagicMock
2+
13
from django.contrib.contenttypes.models import ContentType
24
from django.core.exceptions import ObjectDoesNotExist
35
from django.test import TestCase
46

5-
from core.models import DataSource, ObjectType
7+
from core.models import DataSource, Job, ObjectType
68
from core.choices import ObjectChangeActionChoices
79
from dcim.models import Site, Location, Device
810
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
@@ -200,3 +202,38 @@ def test_with_feature(self):
200202
bookmarks_ots = ObjectType.objects.with_feature('bookmarks')
201203
self.assertIn(ObjectType.objects.get_by_natural_key('dcim', 'site'), bookmarks_ots)
202204
self.assertNotIn(ObjectType.objects.get_by_natural_key('dcim', 'cabletermination'), bookmarks_ots)
205+
206+
207+
class JobTest(TestCase):
208+
209+
@patch('core.models.jobs.django_rq.get_queue')
210+
def test_delete_cancels_job_from_correct_queue(self, mock_get_queue):
211+
"""
212+
Test that when a job is deleted, it's canceled from the correct queue.
213+
"""
214+
mock_queue = MagicMock()
215+
mock_rq_job = MagicMock()
216+
mock_queue.fetch_job.return_value = mock_rq_job
217+
mock_get_queue.return_value = mock_queue
218+
219+
def dummy_func(**kwargs):
220+
pass
221+
222+
# Enqueue a job with a custom queue name
223+
custom_queue = 'my_custom_queue'
224+
job = Job.enqueue(
225+
func=dummy_func,
226+
name='Test Job',
227+
queue_name=custom_queue
228+
)
229+
230+
# Reset mock to clear enqueue call
231+
mock_get_queue.reset_mock()
232+
233+
# Delete the job
234+
job.delete()
235+
236+
# Verify the correct queue was used for cancellation
237+
mock_get_queue.assert_called_with(custom_queue)
238+
mock_queue.fetch_job.assert_called_with(str(job.job_id))
239+
mock_rq_job.cancel.assert_called_once()

netbox/templates/core/job.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ <h2 class="card-header">{% trans "Scheduling" %}</h2>
5959
<th scope="row">{% trans "Completed" %}</th>
6060
<td>{{ object.completed|isodatetime|placeholder }}</td>
6161
</tr>
62+
<tr>
63+
<th scope="row">{% trans "Queue" %}</th>
64+
<td>{{ object.queue_name|placeholder }}</td>
65+
</tr>
6266
</table>
6367
</div>
6468
</div>

0 commit comments

Comments
 (0)