Merge pull request #1170 from tcely/patch-2

Use `TaskHistory` for reporting about tasks
This commit is contained in:
meeb authored and GitHub committed 2025-07-04 17:08:33 +10:00
commit f456c22b30
9 files changed
+208 -65

No files matched your search

+9
View File
@@ -5,6 +5,7 @@ from huey import (
CancelExecution, SqliteHuey as huey_SqliteHuey, CancelExecution, SqliteHuey as huey_SqliteHuey,
signals, utils, signals, utils,
) )
from .timestamp import datetime_to_timestamp, timestamp_to_datetime
class SqliteHuey(huey_SqliteHuey): class SqliteHuey(huey_SqliteHuey):
@@ -277,7 +278,15 @@ def historical_task(signal_name, task_obj, exception_obj=None, /, *, huey=None):
th.verbose_name = f'{th.name} with: {model_instance.key}' th.verbose_name = f'{th.name} with: {model_instance.key}'
if hasattr(model_instance, 'name'): if hasattr(model_instance, 'name'):
th.verbose_name += f' / {model_instance.name}' th.verbose_name += f' / {model_instance.name}'
elif signal_name == signals.SIGNAL_SCHEDULED:
if huey.utc:
th.scheduled_at = task_obj.eta.replace(tzinfo=datetime.UTC)
else: # this path is unlikely
th.scheduled_at = timestamp_to_datetime(
datetime_to_timestamp(task_obj.eta, integer=False),
).astimezone(tz=datetime.UTC)
th.end_at = signal_dt th.end_at = signal_dt
th.elapsed = history['elapsed']
th.save() th.save()
# Registration of shared signal handlers # Registration of shared signal handlers
@@ -0,0 +1,35 @@
# Generated by Django 5.2.4 on 2025-07-03 21:51
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='taskhistory',
name='elapsed',
field=models.FloatField(default=float),
),
migrations.AddField(
model_name='taskhistory',
name='scheduled_at',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now),
),
migrations.AlterField(
model_name='taskhistory',
name='attempts',
field=models.IntegerField(db_index=True, default=int),
),
migrations.AlterField(
model_name='taskhistory',
name='repeat',
field=models.BigIntegerField(default=int),
),
]
+5 -2
View File
@@ -50,6 +50,8 @@ class TaskHistory(models.Model):
verbose_name = models.CharField(max_length=255, null=True, blank=True) verbose_name = models.CharField(max_length=255, null=True, blank=True)
start_at = models.DateTimeField(db_index=True, null=True, blank=True) start_at = models.DateTimeField(db_index=True, null=True, blank=True)
# when the task was scheduled to run
scheduled_at = models.DateTimeField(default=timezone.now, db_index=True)
# when the task was completed # when the task was completed
end_at = models.DateTimeField(default=timezone.now, db_index=True) end_at = models.DateTimeField(default=timezone.now, db_index=True)
@@ -59,13 +61,14 @@ class TaskHistory(models.Model):
queue = models.CharField(max_length=190, db_index=True, null=True, blank=True) queue = models.CharField(max_length=190, db_index=True, null=True, blank=True)
# how many times the task has been tried # how many times the task has been tried
attempts = models.IntegerField(default=0, db_index=True) attempts = models.IntegerField(default=int, db_index=True)
# when the task last failed # when the task last failed
failed_at = models.DateTimeField(db_index=True, null=True, blank=True) failed_at = models.DateTimeField(db_index=True, null=True, blank=True)
# details of the error that occurred # details of the error that occurred
last_error = models.TextField(blank=True) last_error = models.TextField(blank=True)
repeat = models.BigIntegerField(default=0) elapsed = models.FloatField(default=float)
repeat = models.BigIntegerField(default=int)
repeat_until = models.DateTimeField(null=True, blank=True) repeat_until = models.DateTimeField(null=True, blank=True)
objects = TaskHistoryQuerySet.as_manager() objects = TaskHistoryQuerySet.as_manager()
+6 -2
View File
@@ -1,8 +1,9 @@
import datetime import datetime
import math
utc_tz = datetime.timezone.utc utc_tz = datetime.timezone.utc
posix_epoch = datetime.datetime.fromtimestamp(0, utc_tz) posix_epoch = datetime.datetime.fromtimestamp(0, tz=utc_tz)
def add_epoch(seconds): def add_epoch(seconds):
@@ -13,6 +14,9 @@ def add_epoch(seconds):
def subtract_epoch(arg_dt, /): def subtract_epoch(arg_dt, /):
assert isinstance(arg_dt, datetime.datetime) assert isinstance(arg_dt, datetime.datetime)
if arg_dt.utcoffset() is None: # naive
return arg_dt - datetime.datetime.fromtimestamp(0, tz=None)
utc_dt = arg_dt.astimezone(utc_tz) utc_dt = arg_dt.astimezone(utc_tz)
return utc_dt - posix_epoch return utc_dt - posix_epoch
@@ -22,7 +26,7 @@ def datetime_to_timestamp(arg_dt, /, *, integer=True):
if not integer: if not integer:
return timestamp return timestamp
return round(timestamp) return math.ceil(timestamp)
def timestamp_to_datetime(seconds, /): def timestamp_to_datetime(seconds, /):
return add_epoch(seconds=seconds).astimezone(utc_tz) return add_epoch(seconds=seconds).astimezone(utc_tz)
+52 -3
View File
@@ -8,7 +8,9 @@ from django.db.transaction import atomic, on_commit
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from background_task.signals import task_started, task_successful, task_failed from background_task.signals import (
task_created, task_started, task_successful, task_rescheduled, task_failed,
)
from background_task.models import Task from background_task.models import Task
from common.logger import log from common.logger import log
from common.models import TaskHistory from common.models import TaskHistory
@@ -168,6 +170,28 @@ def source_post_delete(sender, instance, **kwargs):
delete_task_by_source('sync.tasks.save_all_media_for_source', instance.pk) delete_task_by_source('sync.tasks.save_all_media_for_source', instance.pk)
@receiver(task_created, dispatch_uid='sync.signals.task_task_created')
@atomic(durable=False)
def task_task_created(sender, task=None, **kwargs):
if task is None:
return
task_obj = task
th, created = TaskHistory.objects.get_or_create(
task_id=str(task_obj.pk),
name=task_obj.task_name,
queue=task_obj.queue,
)
th.scheduled_at = task_obj.run_at
th.priority = (100 - task_obj.priority)
th.repeat = task_obj.repeat
th.repeat_until = task_obj.repeat_until
th.task_params = list(task_obj.params())
th.verbose_name = task_obj.verbose_name
th.save()
if created:
log.debug(f'Created a new task history record: {th.pk}: {th.verbose_name}')
@receiver(task_started, dispatch_uid='sync.signals.task_task_started') @receiver(task_started, dispatch_uid='sync.signals.task_task_started')
@atomic(durable=False) @atomic(durable=False)
def task_task_started(sender, **kwargs): def task_task_started(sender, **kwargs):
@@ -188,15 +212,40 @@ def task_task_started(sender, **kwargs):
th.verbose_name = task_obj.verbose_name th.verbose_name = task_obj.verbose_name
th.save() th.save()
if created: if created:
log.debug(f'Created a new task history record: {th.pk}: {th.verbose_name}') log.debug(f'Started a new task history record: {th.pk}: {th.verbose_name}')
@receiver(task_rescheduled, dispatch_uid='sync.signals.task_task_rescheduled')
@atomic(durable=False)
def task_task_rescheduled(sender, task=None, **kwargs):
if task is None:
return
now_dt = timezone.now()
task_obj = task
th, created = TaskHistory.objects.get_or_create(
task_id=str(task_obj.pk),
name=task_obj.task_name,
queue=task_obj.queue,
)
th.elapsed += (
now_dt - task_obj.locked_at
).total_seconds()
th.end_at = now_dt
th.scheduled_at = task_obj.run_at
th.start_at = task_obj.locked_at
th.save()
if created:
log.debug(f'Rescheduled a new task history record: {th.pk}: {th.verbose_name}')
def merge_completed_task_into_history(task_id, task_obj): def merge_completed_task_into_history(task_id, task_obj):
th, created = TaskHistory.objects.get_or_create( th, created = TaskHistory.objects.get_or_create(
task_id=str(task_id), task_id=str(task_id),
name=task_obj.task_name, name=task_obj.task_name,
queue=task_obj.queue, queue=task_obj.queue,
) )
th.elapsed += (
(task_obj.failed_at or task_obj.run_at) - task_obj.locked_at
).total_seconds()
th.end_at = task_obj.run_at th.end_at = task_obj.run_at
th.failed_at = task_obj.failed_at th.failed_at = task_obj.failed_at
th.last_error = task_obj.last_error th.last_error = task_obj.last_error
@@ -218,7 +267,7 @@ def task_task_successful(sender, task_id, completed_task, **kwargs):
def task_task_failed(sender, task_id, completed_task, **kwargs): def task_task_failed(sender, task_id, completed_task, **kwargs):
merge_completed_task_into_history(task_id, completed_task) merge_completed_task_into_history(task_id, completed_task)
# Triggered after a task fails by reaching its max retry attempts # Triggered after a task fails by reaching its max retry attempts
obj, url = map_task_to_instance(completed_task) obj, url = map_task_to_instance(completed_task, using_history=False)
if isinstance(obj, Source): if isinstance(obj, Source):
log.error(f'Permanent failure for source: {obj} task: {completed_task}') log.error(f'Permanent failure for source: {obj} task: {completed_task}')
obj.has_failed = True obj.has_failed = True
+8 -3
View File
@@ -51,7 +51,7 @@ def get_hash(task_name, pk):
return sha1(f'{task_name}{task_params}'.encode('utf-8')).hexdigest() return sha1(f'{task_name}{task_params}'.encode('utf-8')).hexdigest()
def map_task_to_instance(task): def map_task_to_instance(task, using_history=True):
''' '''
Reverse-maps a scheduled backgrond task to an instance. Requires the task name Reverse-maps a scheduled backgrond task to an instance. Requires the task name
to be a known task function and the first argument to be a UUID. This is used to be a known task function and the first argument to be a UUID. This is used
@@ -72,7 +72,12 @@ def map_task_to_instance(task):
Media: 'sync:media-item', Media: 'sync:media-item',
} }
# Unpack # Unpack
task_func, task_args_str = task.task_name, task.task_params task_args = None
if using_history:
task_func = task.name
task_args = task.task_params
else:
task_func, task_args_str = task.task_name, task.task_params
model = TASK_MAP.get(task_func, None) model = TASK_MAP.get(task_func, None)
if not model: if not model:
return None, None return None, None
@@ -80,7 +85,7 @@ def map_task_to_instance(task):
if not url: if not url:
return None, None return None, None
try: try:
task_args = json.loads(task_args_str) task_args = task_args or json.loads(task_args_str)
except (TypeError, ValueError, AttributeError): except (TypeError, ValueError, AttributeError):
return None, None return None, None
if len(task_args) != 2: if len(task_args) != 2:
@@ -16,16 +16,16 @@
<span class="collection-item"> <span class="collection-item">
{% if task.has_error %} {% if task.has_error %}
<i class="fas fa-exclamation-triangle"></i> <strong>{{ task.verbose_name }}</strong><br> <i class="fas fa-exclamation-triangle"></i> <strong>{% if task.verbose_name %}{{ task.verbose_name }}{% else %}{{ task.name }}{% endif %}</strong><br>
Queue: &quot;{{ task.queue }}&quot;<br> Queue: &quot;{{ task.queue }}&quot;<br>
Error: &quot;{{ task.error_message }}&quot;<br> Error: &quot;{{ task.error_message }}&quot;<br>
{% else %} {% else %}
<i class="fas fa-check"></i> <strong>{{ task.verbose_name }}</strong><br> <i class="fas fa-check"></i> <strong>{% if task.verbose_name %}{{ task.verbose_name }}{% else %}{{ task.name }}{% endif %}</strong><br>
Queue: &quot;{{ task.queue }}&quot;<br> Queue: &quot;{{ task.queue }}&quot;<br>
{% endif %} {% endif %}
Task locked for: {{ task.run_at|sub:task.locked_at|timedelta }}<br> Task locked for: {{ task.end_at|sub:task.start_at|timedelta }}<br>
<i class="far fa-clock"></i> Task locked at <strong>{{ task.locked_at|date:'Y-m-d H:i:s' }}</strong><br> <i class="far fa-clock"></i> Task locked at <strong>{{ task.start_at|date:'Y-m-d H:i:s' }}</strong><br>
<i class="fas fa-hourglass-end"></i> Task ended at <strong>{{ task.run_at|date:'Y-m-d H:i:s' }}</strong> <i class="fas fa-hourglass-end"></i> Task ended at <strong>{{ task.end_at|date:'Y-m-d H:i:s' }}</strong>
</span> </span>
{% empty %} {% empty %}
<span class="collection-item no-items"><i class="fas fa-info-circle"></i> There have been no completed tasks{% if source %} that match the specified source filter{% endif %}.</span> <span class="collection-item no-items"><i class="fas fa-info-circle"></i> There have been no completed tasks{% if source %} that match the specified source filter{% endif %}.</span>
@@ -33,5 +33,5 @@
</div> </div>
</div> </div>
</div> </div>
{% include 'pagination.html' with pagination=sources.paginator filter=source.pk %} {% include 'pagination.html' with filter=source.pk %}
{% endblock %} {% endblock %}
+11 -10
View File
@@ -23,9 +23,9 @@
</p> </p>
<div class="collection"> <div class="collection">
{% for task in running %} {% for task in running %}
<a href="{% url task.url pk=task.instance.pk %}" class="collection-item"> <a href="{%if task.instance.pk %}{% url task.url pk=task.instance.pk %}{% else %}#{{ task.task_id }}{% endif %}" class="collection-item">
<i class="fas fa-running"></i> <strong>{{ task }}</strong><br> <i class="fas fa-running"></i> <strong>{{ task }}</strong><br>
<i class="far fa-clock"></i> Task started at <strong>{{ task.locked_at|date:'Y-m-d H:i:s' }}</strong> <i class="far fa-clock"></i> Task started at <strong>{{ task.start_at|date:'Y-m-d H:i:s' }}</strong>
</a> </a>
{% empty %} {% empty %}
<span class="collection-item no-items"><i class="fas fa-info-circle"></i> There are no running tasks.</span> <span class="collection-item no-items"><i class="fas fa-info-circle"></i> There are no running tasks.</span>
@@ -54,8 +54,8 @@
<i class="fas fa-exclamation-triangle"></i> <strong>{{ task }}</strong>, attempted {{ task.attempts }} time{{ task.attempts|pluralize }}<br> <i class="fas fa-exclamation-triangle"></i> <strong>{{ task }}</strong>, attempted {{ task.attempts }} time{{ task.attempts|pluralize }}<br>
Error: &quot;{{ task.error_message }}&quot;<br> Error: &quot;{{ task.error_message }}&quot;<br>
</a> </a>
<i class="fas fa-history"></i> Task will be retried at <strong>{{ task.run_at|date:'Y-m-d H:i:s' }}</strong> <i class="fas fa-history"></i> Task will be retried at <strong>{{ task.scheduled_at|date:'Y-m-d H:i:s' }}</strong>
<a href="{% url 'sync:run-task' pk=task.pk %}" class="error-text"> <a href="{% url 'sync:run-task' pk=task.task_id %}" class="error-text">
<i class="fas fa-undo"></i> <i class="fas fa-undo"></i>
</a> </a>
</div> </div>
@@ -73,19 +73,20 @@
<p> <p>
Tasks which are scheduled to run in the future or are waiting in a queue to be Tasks which are scheduled to run in the future or are waiting in a queue to be
processed. They can be waiting for an available worker to run immediately, or processed. They can be waiting for an available worker to run immediately, or
run in the future at the specified &quot;run at&quot; time. run in the future at the specified &quot;scheduled at&quot; time.
</p> </p>
<div class="collection"> <div class="collection">
{% for task in scheduled %} {% for task in scheduled %}
<div class="collection-item"> <div class="collection-item">
<a href="{% url task.url pk=task.instance.pk %}"> <a href="{% url task.url pk=task.instance.pk %}">
<i class="fas fa-hourglass-start"></i> <strong>{{ task }}</strong><br> <i class="fas fa-hourglass-start"></i> <strong>{{ task }}</strong><br>
{% if task.instance.index_schedule and task.repeat > 0 %}Scheduled to run {{ task.instance.get_index_schedule_display|lower }}.<br>{% endif %} {% if task.instance.is_active and 'once' not in task.verbose_name %}Scheduled to run {{ task.instance.get_index_schedule_display|lower }}.<br>{% endif %}
<i class="far fa-clock"></i> Task will run {% if task.run_now %}<strong>immediately</strong>{% else %}at <strong>{{ task.run_at|date:'Y-m-d H:i:s' }}</strong> <i class="far fa-clock"></i> Task will run {% if task.run_now %}<strong>immediately</strong>{% else %}at <strong>{{ task.scheduled_at|date:'Y-m-d H:i:s' }}</strong>
{% if '-' not in task.task_id %}
</a> </a>
<a href="{% url 'sync:run-task' pk=task.pk %}"> <a href="{% url 'sync:run-task' pk=task.task_id %}">
<i class="far fa-play-circle"></i> <i class="far fa-play-circle"></i>
{% endif %} {% endif %}{% endif %}
</a> </a>
</div> </div>
{% empty %} {% empty %}
@@ -94,7 +95,7 @@
</div> </div>
</div> </div>
</div> </div>
{% include 'pagination.html' with pagination=sources.paginator filter=source.pk %} {% include 'pagination.html' with filter=source.pk %}
<div class="row"> <div class="row">
<div class="col s12"> <div class="col s12">
<h2>Completed</h2> <h2>Completed</h2>
+76 -39
View File
@@ -13,16 +13,17 @@ from django.core.exceptions import SuspiciousFileOperation
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.db import connection, IntegrityError from django.db import connection, IntegrityError
from django.db.models import Q, Count, Sum, When, Case from django.db.models import F, Q, Count, Sum, When, Case
from django.forms import Form, ValidationError from django.forms import Form, ValidationError
from django.utils.text import slugify from django.utils.text import slugify
from django.utils._os import safe_join from django.utils._os import safe_join
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from common.models import TaskHistory
from common.timestamp import timestamp_to_datetime from common.timestamp import timestamp_to_datetime
from common.utils import append_uri_params, mkdir_p, multi_key_sort from common.utils import append_uri_params, mkdir_p, multi_key_sort
from background_task.models import Task, CompletedTask from background_task.models import Task
from django_huey import DJANGO_HUEY from django_huey import DJANGO_HUEY, get_queue
from common.huey import h_q_reset_tasks from common.huey import h_q_reset_tasks
from .models import Source, Media, MediaServer from .models import Source, Media, MediaServer
from .forms import (ValidateSourceForm, ConfirmDeleteSourceForm, RedownloadMediaForm, from .forms import (ValidateSourceForm, ConfirmDeleteSourceForm, RedownloadMediaForm,
@@ -41,6 +42,24 @@ from . import signals # noqa
from . import youtube from . import youtube
def get_waiting_tasks():
background_task_ids = {
str(t.pk) for t in Task.objects.all()
}
huey_queue_names = (DJANGO_HUEY or {}).get('queues', {})
huey_queues = list(map(get_queue, huey_queue_names))
huey_task_ids = {
str(t.id) for q in huey_queues for t in set(
q.pending()
).union(
q.scheduled()
)
}
return TaskHistory.objects.filter(
task_id__in=huey_task_ids.union(background_task_ids),
)
class DashboardView(TemplateView): class DashboardView(TemplateView):
''' '''
The dashboard shows non-interactive totals and summaries. The dashboard shows non-interactive totals and summaries.
@@ -62,8 +81,13 @@ class DashboardView(TemplateView):
data['num_media'] = Media.objects.all().count() data['num_media'] = Media.objects.all().count()
data['num_downloaded_media'] = Media.objects.filter(downloaded=True).count() data['num_downloaded_media'] = Media.objects.filter(downloaded=True).count()
# Tasks # Tasks
data['num_tasks'] = Task.objects.all().count() completed_qs = TaskHistory.objects.filter(
data['num_completed_tasks'] = CompletedTask.objects.all().count() start_at__isnull=False,
end_at__gt=F('start_at'),
)
waiting_qs = get_waiting_tasks()
data['num_tasks'] = waiting_qs.count()
data['num_completed_tasks'] = completed_qs.count()
# Disk usage # Disk usage
disk_usage = Media.objects.filter( disk_usage = Media.objects.filter(
downloaded=True, downloaded_filesize__isnull=False downloaded=True, downloaded_filesize__isnull=False
@@ -830,27 +854,23 @@ class TasksView(ListView):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
qs = Task.objects.all() qs = get_waiting_tasks()
if self.filter_source: if self.filter_source:
params_prefix=f'[["{self.filter_source.pk}"' params_prefix=f'[["{self.filter_source.pk}"'
qs = qs.filter(task_params__istartswith=params_prefix) qs = qs.filter(task_params__istartswith=params_prefix)
order = getattr(settings,
'BACKGROUND_TASK_PRIORITY_ORDERING',
'DESC'
)
prefix = '-' if 'ASC' != order else ''
_priority = f'{prefix}priority'
return qs.order_by( return qs.order_by(
_priority, 'priority',
'run_at', 'end_at',
) )
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
data = super().get_context_data(*args, **kwargs) data = super().get_context_data(*args, **kwargs)
now = timezone.now() now_dt = timezone.now()
qs = Task.objects.all() scheduled_qs = get_waiting_tasks()
running_qs = qs.filter(locked_by__isnull=False) running_qs = scheduled_qs.filter(
scheduled_qs = qs.filter(locked_by__isnull=True) start_at__isnull=False,
end_at__lte=F('start_at'),
)
errors_qs = scheduled_qs.filter( errors_qs = scheduled_qs.filter(
attempts__gt=0 attempts__gt=0
).exclude(last_error__exact='') ).exclude(last_error__exact='')
@@ -867,19 +887,19 @@ class TasksView(ListView):
data['wait_for_database_queue'] = False data['wait_for_database_queue'] = False
def add_to_task(task): def add_to_task(task):
setattr(task, 'run_now', task.scheduled_at < now_dt)
obj, url = map_task_to_instance(task) obj, url = map_task_to_instance(task)
if not obj: if obj:
return False setattr(task, 'instance', obj)
setattr(task, 'instance', obj) setattr(task, 'url', url)
setattr(task, 'url', url)
setattr(task, 'run_now', task.run_at < now)
if task.has_error(): if task.has_error():
error_message = get_error_message(task) error_message = get_error_message(task)
setattr(task, 'error_message', error_message) setattr(task, 'error_message', error_message)
return 'error' return 'error'
return True return True and obj
for task in running_qs: verbose_names = dict()
for task in Task.objects.filter(locked_by__isnull=False):
# There was broken logic in `Task.objects.locked()`, work around it. # There was broken logic in `Task.objects.locked()`, work around it.
# With that broken logic, the tasks never resume properly. # With that broken logic, the tasks never resume properly.
# This check unlocks the tasks without a running process. # This check unlocks the tasks without a running process.
@@ -893,11 +913,28 @@ class TasksView(ListView):
# do not wait for the task to expire # do not wait for the task to expire
task.locked_at = None task.locked_at = None
task.save() task.save()
if locked_by_pid_running and add_to_task(task): task_id = str(task.pk)
data['running'].append(task) verbose_names[task_id] = task.verbose_name
elif locked_by_pid_running and 'wait_for_database_queue' in task.task_name: try:
data['wait_for_database_queue'] = True task = TaskHistory.objects.get(task_id=task_id)
except TaskHistory.DoesNotExist:
# possibly create a new instance?
pass
else:
if locked_by_pid_running and add_to_task(task):
# Use the status if it is available
task.verbose_name = verbose_names.get(task_id) or task.verbose_name
data['running'].append(task)
elif locked_by_pid_running and 'wait_for_database_queue' in task.name:
data['wait_for_database_queue'] = True
verbose_names = None
for task in running_qs:
if task in data['running']:
continue
add_to_task(task)
data['running'].append(task)
# show all the errors when they fit on one page # show all the errors when they fit on one page
if (data['total_errors'] + len(data['running'])) < self.paginate_by: if (data['total_errors'] + len(data['running'])) < self.paginate_by:
for task in errors_qs: for task in errors_qs:
@@ -923,14 +960,10 @@ class TasksView(ListView):
elif mapped: elif mapped:
data['scheduled'].append(task) data['scheduled'].append(task)
order = getattr(settings,
'BACKGROUND_TASK_PRIORITY_ORDERING',
'DESC'
)
sort_keys = ( sort_keys = (
# key, reverse # key, reverse
('run_at', False), ('scheduled_at', False),
('priority', 'ASC' != order), ('priority', True),
('run_now', True), ('run_now', True),
) )
data['errors'] = multi_key_sort(data['errors'], sort_keys, attr=True) data['errors'] = multi_key_sort(data['errors'], sort_keys, attr=True)
@@ -965,11 +998,14 @@ class CompletedTasksView(ListView):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
qs = CompletedTask.objects.all() qs = TaskHistory.objects.filter(
start_at__isnull=False,
end_at__gt=F('start_at'),
)
if self.filter_source: if self.filter_source:
params_prefix=f'[["{self.filter_source.pk}"' params_prefix=f'[["{self.filter_source.pk}"'
qs = qs.filter(task_params__istartswith=params_prefix) qs = qs.filter(task_params__istartswith=params_prefix)
return qs.order_by('-run_at') return qs.order_by('-end_at')
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
data = super().get_context_data(*args, **kwargs) data = super().get_context_data(*args, **kwargs)
@@ -998,7 +1034,8 @@ class ResetTasks(FormView):
def form_valid(self, form): def form_valid(self, form):
# Delete all tasks # Delete all tasks
Task.objects.all().delete() Task.objects.all().delete()
for queue_name in (DJANGO_HUEY or {}).get('queues', {}): huey_queue_names = (DJANGO_HUEY or {}).get('queues', {})
for queue_name in huey_queue_names:
h_q_reset_tasks(queue_name) h_q_reset_tasks(queue_name)
# Iter all tasks # Iter all tasks
for source in Source.objects.all(): for source in Source.objects.all():