rqworker.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import logging
  2. from django_rq.queues import get_connection
  3. from rq import Retry, Worker
  4. from rq.worker_registration import REDIS_WORKER_KEYS
  5. from rq.worker_registration import register as register_worker
  6. from netbox.config import get_config
  7. from netbox.constants import RQ_QUEUE_DEFAULT
  8. __all__ = (
  9. 'NetBoxRQWorker',
  10. 'any_workers_for_queue',
  11. 'get_all_workers',
  12. 'get_queue_for_model',
  13. 'get_rq_retry',
  14. 'get_workers_for_queue',
  15. )
  16. logger = logging.getLogger('netbox.rqworker')
  17. class NetBoxRQWorker(Worker):
  18. """
  19. RQ worker subclass which self-heals its registration. If the worker's
  20. registration is missing from Redis (e.g. because the tasks Redis database
  21. was lost and rebuilt while the worker was running), the next heartbeat
  22. will re-register the worker so that Worker.all() / Worker.find_by_key()
  23. can locate it again.
  24. """
  25. def heartbeat(self, *args, **kwargs):
  26. try:
  27. if not self.connection.sismember(REDIS_WORKER_KEYS, self.key):
  28. logger.warning(f"Worker {self.name} not found in registry; re-registering.")
  29. # If the worker hash still exists (partial Redis data loss),
  30. # register_birth() would raise because rq treats an existing,
  31. # non-dead hash as an active worker. Re-add to the registry
  32. # sets directly in that case; the heartbeat below will refresh
  33. # the hash TTL.
  34. if self.connection.exists(self.key) and not self.connection.hexists(self.key, 'death'):
  35. register_worker(self, self.connection)
  36. else:
  37. self.register_birth()
  38. except Exception:
  39. logger.exception("Failed to verify worker registration.")
  40. super().heartbeat(*args, **kwargs)
  41. def get_queue_for_model(model):
  42. """
  43. Return the configured queue name for jobs associated with the given model.
  44. """
  45. return get_config().QUEUE_MAPPINGS.get(model, RQ_QUEUE_DEFAULT)
  46. def _is_live_worker(worker, queue_name):
  47. """
  48. Return True if the given Worker is currently servicing queue_name.
  49. Liveness itself is enforced by RQ: Worker.all() / Worker.find_by_key()
  50. only return workers whose Redis hash still exists, and RQ resets that
  51. hash's expiry to (worker_ttl + 60s) on every heartbeat. So any worker
  52. returned by RQ has heartbeat'd within its configured TTL -- we only need
  53. to confirm it's listening on the requested queue. (Reconstructing
  54. worker_ttl ourselves would be unsafe: RQ does not persist worker_ttl in
  55. the hash, so a worker started with a non-default --worker-ttl is
  56. reconstructed with DEFAULT_WORKER_TTL regardless of its real TTL.)
  57. """
  58. return queue_name in worker.queue_names()
  59. def get_workers_for_queue(queue_name):
  60. """
  61. Return the number of live workers currently servicing the given queue.
  62. """
  63. connection = get_connection(queue_name)
  64. return sum(
  65. 1 for worker in Worker.all(connection=connection)
  66. if _is_live_worker(worker, queue_name)
  67. )
  68. def get_all_workers():
  69. """
  70. Return the set of worker names currently registered on the tasks Redis
  71. connection, regardless of which queue(s) each worker is servicing. Stale
  72. registrations (workers whose Redis hash has expired) are filtered out by
  73. RQ via Worker.all() -- see _is_live_worker() for details.
  74. Used for system-wide worker counts (dashboard, status API), where the
  75. intent is "are any RQ workers running" rather than "are workers handling
  76. a specific queue."
  77. """
  78. connection = get_connection(RQ_QUEUE_DEFAULT)
  79. return {worker.name for worker in Worker.all(connection=connection)}
  80. def any_workers_for_queue(queue_name):
  81. """
  82. Return True if at least one live worker is currently servicing the given
  83. queue. Cheaper than get_workers_for_queue() when only a liveness check is
  84. needed: workers are fetched one at a time and iteration stops at the first
  85. live match.
  86. """
  87. connection = get_connection(queue_name)
  88. for key in Worker.all_keys(connection=connection):
  89. worker = Worker.find_by_key(key, connection=connection)
  90. if worker is None:
  91. continue
  92. if _is_live_worker(worker, queue_name):
  93. return True
  94. return False
  95. def get_rq_retry():
  96. """
  97. If RQ_RETRY_MAX is defined and greater than zero, instantiate and return a Retry object to be
  98. used when queuing a job. Otherwise, return None.
  99. """
  100. retry_max = get_config().RQ_RETRY_MAX
  101. retry_interval = get_config().RQ_RETRY_INTERVAL
  102. if retry_max:
  103. return Retry(max=retry_max, interval=retry_interval)
  104. return None