소스 검색

Fixes #22598: Patch get_scheduler_pid() to resolve RQ discrepancy

Jeremy Stretch 1 일 전
부모
커밋
9199cefbdc
3개의 변경된 파일54개의 추가작업 그리고 1개의 파일을 삭제
  1. 8 0
      netbox/core/apps.py
  2. 17 0
      netbox/core/tests/test_views.py
  3. 29 1
      netbox/core/utils.py

+ 8 - 0
netbox/core/apps.py

@@ -21,8 +21,11 @@ class CoreConfig(AppConfig):
     name = "core"
     name = "core"
 
 
     def ready(self):
     def ready(self):
+        import django_rq.utils
+
         from core.api import schema  # noqa: F401
         from core.api import schema  # noqa: F401
         from core.checks import check_duplicate_indexes, check_postgresql_version, check_redis_version  # noqa: F401
         from core.checks import check_duplicate_indexes, check_postgresql_version, check_redis_version  # noqa: F401
+        from core.utils import get_scheduler_pid
         from netbox import context_managers  # noqa: F401
         from netbox import context_managers  # noqa: F401
         from netbox.models.features import register_models
         from netbox.models.features import register_models
 
 
@@ -31,6 +34,11 @@ class CoreConfig(AppConfig):
         # Register models
         # Register models
         register_models(*self.get_models())
         register_models(*self.get_models())
 
 
+        # Patch django-rq's get_scheduler_pid() to fix scheduler PID resolution under RQ 2.10+, where
+        # the scheduler lock stores a hex name rather than an integer PID (see #22598).
+        # TODO: Remove once django-rq ships a release incorporating the upstream fix.
+        django_rq.utils.get_scheduler_pid = get_scheduler_pid
+
         # Register core events
         # Register core events
         EventType(OBJECT_CREATED, _('Object created')).register()
         EventType(OBJECT_CREATED, _('Object created')).register()
         EventType(OBJECT_UPDATED, _('Object updated')).register()
         EventType(OBJECT_UPDATED, _('Object updated')).register()

+ 17 - 0
netbox/core/tests/test_views.py

@@ -12,9 +12,11 @@ from django_rq.workers import get_worker
 from rq.job import Job as RQ_Job
 from rq.job import Job as RQ_Job
 from rq.job import JobStatus
 from rq.job import JobStatus
 from rq.registry import DeferredJobRegistry, FailedJobRegistry, FinishedJobRegistry, StartedJobRegistry
 from rq.registry import DeferredJobRegistry, FailedJobRegistry, FinishedJobRegistry, StartedJobRegistry
+from rq.scheduler import SCHEDULER_KEY_TEMPLATE, RQScheduler
 
 
 from core.choices import ObjectChangeActionChoices
 from core.choices import ObjectChangeActionChoices
 from core.models import *
 from core.models import *
+from core.utils import get_scheduler_pid
 from dcim.models import Site
 from dcim.models import Site
 from users.models import User
 from users.models import User
 from utilities.testing import TestCase, ViewTestCases, create_tags, disable_logging
 from utilities.testing import TestCase, ViewTestCases, create_tags, disable_logging
@@ -300,6 +302,21 @@ class BackgroundTaskTestCase(RQQueueTestMixin, TestCase):
         self.assertIn('high', str(response.content))
         self.assertIn('high', str(response.content))
         self.assertIn('low', str(response.content))
         self.assertIn('low', str(response.content))
 
 
+    def test_background_queue_list_with_scheduler_lock(self):
+        """
+        The queue list view must not crash when an RQ 2.10+ scheduler lock is present. RQ 2.10 stores
+        the scheduler's hex name (rather than an integer PID) under the lock key, which the previous
+        PID resolution attempted to cast to an int. See #22598.
+        """
+        queue = get_queue('default')
+        scheduler_name = uuid.uuid4().hex
+        queue.connection.set(RQScheduler.get_locking_key(queue.name), scheduler_name)
+        queue.connection.hset(SCHEDULER_KEY_TEMPLATE % scheduler_name, 'pid', 12345)
+
+        response = self.client.get(reverse('core:background_queue_list'))
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(get_scheduler_pid(queue), 12345)
+
     def test_background_tasks_list_default(self):
     def test_background_tasks_list_default(self):
         queue = get_queue('default')
         queue = get_queue('default')
         queue.enqueue(self.dummy_job_default)
         queue.enqueue(self.dummy_job_default)

+ 29 - 1
netbox/core/utils.py

@@ -1,7 +1,8 @@
+from django.core.exceptions import ImproperlyConfigured
 from django.db import DatabaseError, connection
 from django.db import DatabaseError, connection
 from django.http import Http404
 from django.http import Http404
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
-from django_rq.queues import get_queue, get_queue_by_index, get_redis_connection
+from django_rq.queues import get_queue, get_queue_by_index, get_redis_connection, get_scheduler
 from django_rq.settings import get_queues_list, get_queues_map
 from django_rq.settings import get_queues_list, get_queues_map
 from django_rq.utils import get_jobs, stop_jobs
 from django_rq.utils import get_jobs, stop_jobs
 from rq import requeue_job
 from rq import requeue_job
@@ -22,11 +23,38 @@ __all__ = (
     'get_db_schema',
     'get_db_schema',
     'get_rq_jobs',
     'get_rq_jobs',
     'get_rq_jobs_from_status',
     'get_rq_jobs_from_status',
+    'get_scheduler_pid',
     'requeue_rq_job',
     'requeue_rq_job',
     'stop_rq_job',
     'stop_rq_job',
 )
 )
 
 
 
 
+def get_scheduler_pid(queue):
+    """
+    Return the PID of the RQ scheduler holding the lock on the given queue, False if the external
+    rq-scheduler is in use, or None if no scheduler lock is present.
+
+    This is a patched replacement for django_rq.utils.get_scheduler_pid() which resolves the PID via
+    RQ's Queue.scheduler_pid property. The implementation shipped in django-rq 4.1.0 casts the
+    scheduler lock value directly to an integer, which fails under RQ 2.10+ where the lock stores the
+    scheduler's (hex) name rather than its PID. See #22598.
+    """
+    try:
+        # Succeeds only when the external rq-scheduler is installed, in which case the PID cannot be
+        # determined without a costly Redis keys() scan
+        get_scheduler(queue.name)
+        return False
+    except ImproperlyConfigured:
+        # The external rq-scheduler is not installed; resolve the PID via RQ's built-in scheduler.
+        # Guard against Redis errors (e.g. ConnectionError) so they don't propagate to the caller.
+        try:
+            return queue.scheduler_pid
+        except Exception:
+            return None
+    except Exception:
+        return None
+
+
 def get_rq_jobs():
 def get_rq_jobs():
     """
     """
     Return a list of all RQ jobs.
     Return a list of all RQ jobs.