Skip to content

Commit

Permalink
Compatibility with RQ 2.0 (#675)
Browse files Browse the repository at this point in the history
* rqworker command can now run

* Make job_detail.html display better

* Properly show currently running executions

* Minor job detail cleanup

* Fixed a few tests

* Made stop job command work again

* Fixed tests

* Failed job registry should show jobs in reverse chronological order.

* Make failed jobs sortable

* Make finished jobs sortable

* Require RQ >= 2

* Temporarily skip rq-scheduler tests

* Comment out scheduler test

* Comment out invalid test

* Updated changelog
  • Loading branch information
selwin authored Oct 28, 2024
1 parent a953e6a commit fbb916c
Show file tree
Hide file tree
Showing 13 changed files with 696 additions and 144 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install django==${{ matrix.django-version }} \
redis django-redis pyyaml rq sentry-sdk rq-scheduler
redis django-redis pyyaml rq sentry-sdk
- name: Run Test
run: |
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
### Version 3.0 (2024-10-28)
* Added support for RQ 2.0. Thanks @selwin!
* Many typing improvements. Thanks @SpecLad and @terencehonles!
* Added management command to suspend and resume workers. Thanks @jackkinsella!
* Better support for Redis Sentinel. Thanks @alaouimehdi1995!

### Version 2.10.2 (2024-03-23)
* Added support for Django 5.0. Thanks @selwin!
* Fixed an error in Python 3.12. Thanks @selwin!
Expand Down
18 changes: 7 additions & 11 deletions django_rq/management/commands/rqworker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import sys

from redis.exceptions import ConnectionError
from rq import Connection
from rq.logutils import setup_loghandlers

from django.core.management.base import BaseCommand
Expand Down Expand Up @@ -84,21 +83,18 @@ def handle(self, *args, **options):
'queue_class': options['queue_class'],
'job_class': options['job_class'],
'name': options['name'],
'default_worker_ttl': options['worker_ttl'],
'worker_ttl': options['worker_ttl'],
'serializer': options['serializer']
}
w = get_worker(*args, **worker_kwargs)

# Call Connection context manager to push the redis connection into LocalStack
# without this, jobs using RQ's get_current_job() will fail
with Connection(w.connection):
# Close any opened DB connection before any fork
reset_db_connections()
# Close any opened DB connection before any fork
reset_db_connections()

w.work(
burst=options.get('burst', False), with_scheduler=options.get('with_scheduler', False),
logging_level=level, max_jobs=options['max_jobs']
)
w.work(
burst=options.get('burst', False), with_scheduler=options.get('with_scheduler', False),
logging_level=level, max_jobs=options['max_jobs']
)
except ConnectionError as e:
self.stderr.write(str(e))
sys.exit(1)
170 changes: 170 additions & 0 deletions django_rq/templates/django_rq/failed_jobs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
{% extends "admin/base_site.html" %}

{% load static jquery_path django_rq %}

{% block title %}Failed Jobs in {{ queue.name }} {{ block.super }}{% endblock %}

{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/changelists.css" %}">
{% endblock %}

{% block extrahead %}
{{ block.super }}
<script type="text/javascript" src="{% get_jquery_path as jquery_path %}{% static jquery_path %}"></script>
<script type="text/javascript" src="{% static "admin/js/jquery.init.js" %}"></script>
<script type="text/javascript" src="{% static "admin/js/actions.js" %}"></script>
<script type="text/javascript">
(function($) {
$(document).ready(function($) {
$("tr input.action-select").actions();
});
})(django.jQuery);
</script>
{% endblock %}


{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">Home</a> &rsaquo;
<a href="{% url 'rq_home' %}">Django RQ</a> &rsaquo;
<a href="{% url 'rq_jobs' queue_index %}">{{ queue.name }}</a>
</div>
{% endblock %}

{% block content_title %}<h1>{{ job_status }} jobs in {{ queue.name }}</h1>{% endblock %}

{% block content %}

<div id="content-main">
<ul class="object-tools">
<li><a href="{% url 'rq_requeue_all' queue_index %}" class="requeuelink">Requeue All</a></li>
<li><a href="{% url 'rq_delete_failed_jobs' queue_index %}" class="requeuelink">Delete All</a></li>
</ul>
<div class="module" id="changelist">
<div class="changelist-form-container">
<form id="changelist-form" action="{% url 'rq_confirm_action' queue_index %}" method="post">
{% csrf_token %}
<div class="actions">
<label>Actions:
<select name="action" required>
<option value="" selected>---------</option>
<option value="delete">Delete</option>
<option value="requeue">Requeue</option>
</select>
</label>
<button type="submit" class="button" title="Execute selected action" name="index" value="0">Go</button>
</div>
<div class="results">
<table id="result_list">
<thead>
<tr>
<th scope="col" class="action-checkbox-column">
<div class="text">
<span><input type="checkbox" id="action-toggle"></span>
</div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable">
<div class='text'><span>ID</span></div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable">
<div class='text'><span>Created</span></div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable">
<div class='text'><span>Enqueued</span></div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable sorted {{ sort_direction }}">
<div class="sortoptions">
{% if sort_direction == 'ascending' %}
<a href="?desc=1" class="toggle ascending" title="Toggle sorting"></a>
{% else %}
<a href="?desc=0" class="toggle descending" title="Toggle sorting"></a>
{% endif %}
</div>
<div class='text'>
{% if sort_direction == 'ascending' %}
<a href="?desc=1">
{% else %}
<a href="?desc=0">
{% endif %}
Ended
</a>
</div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable">
<div class='text'><span>Status</span></div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable">
<div class='text'><span>Callable</span></div>
<div class="clear"></div>
</th>
{% block extra_columns %}
{% endblock extra_columns %}
</tr>
</thead>
<tbody>
{% for job in jobs %}
<tr>
<td class="action-checkbox">
<input class="action-select" name="_selected_action" type="checkbox" value="{{ job.id }}">
</td>
<th>
<a href="{% url 'rq_job_detail' queue_index job.id %}">
{{ job.id }}
</a>
</th>
<td>
{% if job.created_at %}
{{ job.created_at|to_localtime|date:"Y-m-d, H:i:s" }}
{% endif %}
</td>
{% if job_status == 'Scheduled' %}
<td>
{% if job.scheduled_at %}
{{ job.scheduled_at|to_localtime|date:"Y-m-d, H:i:s" }}
{% endif %}
</td>
{% endif %}
<td>
{% if job.enqueued_at %}
{{ job.enqueued_at|to_localtime|date:"Y-m-d, H:i:s" }}
{% endif %}
</td>
<td>
{% if job.ended_at %}
{{ job.ended_at|to_localtime|date:"Y-m-d, H:i:s" }}
{% endif %}
</td>
<td>{{ job.get_status.value }}</td>
<td>{{ job|show_func_name }}</td>
{% block extra_columns_values %}
{% endblock extra_columns_values %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="paginator">
{% for p in page_range %}
{% if p == page %}
<span class="this-page">{{ p }}</span>
{% elif forloop.last %}
<a href="?page={{ p }}" class="end">{{ p }}</a>
{% else %}
<a href="?page={{ p }}">{{ p }}</a>
{% endif %}
{% endfor %}
{{ num_jobs }} jobs
</p>
</form>
</div>
</div>
</div>

{% endblock %}
170 changes: 170 additions & 0 deletions django_rq/templates/django_rq/finished_jobs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
{% extends "admin/base_site.html" %}

{% load static jquery_path django_rq %}

{% block title %}Finished Jobs in {{ queue.name }} {{ block.super }}{% endblock %}

{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/changelists.css" %}">
{% endblock %}

{% block extrahead %}
{{ block.super }}
<script type="text/javascript" src="{% get_jquery_path as jquery_path %}{% static jquery_path %}"></script>
<script type="text/javascript" src="{% static "admin/js/jquery.init.js" %}"></script>
<script type="text/javascript" src="{% static "admin/js/actions.js" %}"></script>
<script type="text/javascript">
(function($) {
$(document).ready(function($) {
$("tr input.action-select").actions();
});
})(django.jQuery);
</script>
{% endblock %}


{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">Home</a> &rsaquo;
<a href="{% url 'rq_home' %}">Django RQ</a> &rsaquo;
<a href="{% url 'rq_jobs' queue_index %}">{{ queue.name }}</a>
</div>
{% endblock %}

{% block content_title %}<h1>{{ job_status }} jobs in {{ queue.name }}</h1>{% endblock %}

{% block content %}

<div id="content-main">
<ul class="object-tools">
<li><a href="{% url 'rq_requeue_all' queue_index %}" class="requeuelink">Requeue All</a></li>
<li><a href="{% url 'rq_delete_failed_jobs' queue_index %}" class="requeuelink">Delete All</a></li>
</ul>
<div class="module" id="changelist">
<div class="changelist-form-container">
<form id="changelist-form" action="{% url 'rq_confirm_action' queue_index %}" method="post">
{% csrf_token %}
<div class="actions">
<label>Actions:
<select name="action" required>
<option value="" selected>---------</option>
<option value="delete">Delete</option>
<option value="requeue">Requeue</option>
</select>
</label>
<button type="submit" class="button" title="Execute selected action" name="index" value="0">Go</button>
</div>
<div class="results">
<table id="result_list">
<thead>
<tr>
<th scope="col" class="action-checkbox-column">
<div class="text">
<span><input type="checkbox" id="action-toggle"></span>
</div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable">
<div class='text'><span>ID</span></div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable">
<div class='text'><span>Created</span></div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable">
<div class='text'><span>Enqueued</span></div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable sorted {{ sort_direction }}">
<div class="sortoptions">
{% if sort_direction == 'ascending' %}
<a href="?desc=1" class="toggle ascending" title="Toggle sorting"></a>
{% else %}
<a href="?desc=0" class="toggle descending" title="Toggle sorting"></a>
{% endif %}
</div>
<div class='text'>
{% if sort_direction == 'ascending' %}
<a href="?desc=1">
{% else %}
<a href="?desc=0">
{% endif %}
Ended
</a>
</div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable">
<div class='text'><span>Status</span></div>
<div class="clear"></div>
</th>
<th scope="col" class="sortable">
<div class='text'><span>Callable</span></div>
<div class="clear"></div>
</th>
{% block extra_columns %}
{% endblock extra_columns %}
</tr>
</thead>
<tbody>
{% for job in jobs %}
<tr>
<td class="action-checkbox">
<input class="action-select" name="_selected_action" type="checkbox" value="{{ job.id }}">
</td>
<th>
<a href="{% url 'rq_job_detail' queue_index job.id %}">
{{ job.id }}
</a>
</th>
<td>
{% if job.created_at %}
{{ job.created_at|to_localtime|date:"Y-m-d, H:i:s" }}
{% endif %}
</td>
{% if job_status == 'Scheduled' %}
<td>
{% if job.scheduled_at %}
{{ job.scheduled_at|to_localtime|date:"Y-m-d, H:i:s" }}
{% endif %}
</td>
{% endif %}
<td>
{% if job.enqueued_at %}
{{ job.enqueued_at|to_localtime|date:"Y-m-d, H:i:s" }}
{% endif %}
</td>
<td>
{% if job.ended_at %}
{{ job.ended_at|to_localtime|date:"Y-m-d, H:i:s" }}
{% endif %}
</td>
<td>{{ job.get_status.value }}</td>
<td>{{ job|show_func_name }}</td>
{% block extra_columns_values %}
{% endblock extra_columns_values %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="paginator">
{% for p in page_range %}
{% if p == page %}
<span class="this-page">{{ p }}</span>
{% elif forloop.last %}
<a href="?page={{ p }}" class="end">{{ p }}</a>
{% else %}
<a href="?page={{ p }}">{{ p }}</a>
{% endif %}
{% endfor %}
{{ num_jobs }} jobs
</p>
</form>
</div>
</div>
</div>

{% endblock %}
Loading

0 comments on commit fbb916c

Please sign in to comment.