mirror of
https://github.com/django-q2/django-q2.git
synced 2026-09-15 21:47:53 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf33275d92 | ||
|
|
e78e473be3 | ||
|
|
4d454e4001 | ||
|
|
cf33891291 | ||
|
|
134a54dbeb |
37
.github/release-drafter.yml
vendored
37
.github/release-drafter.yml
vendored
@@ -1,37 +0,0 @@
|
||||
categories:
|
||||
-
|
||||
label: breaking
|
||||
title: Breaking
|
||||
-
|
||||
label: feature
|
||||
title: New
|
||||
-
|
||||
label: bug
|
||||
title: "Bug Fixes"
|
||||
-
|
||||
label: dependencies
|
||||
title: "Dependency Updates"
|
||||
-
|
||||
label: security
|
||||
title: Security
|
||||
name-template: v$NEXT_PATCH_VERSION
|
||||
tag-template: v$NEXT_PATCH_VERSION
|
||||
template: |
|
||||
# Changes
|
||||
$CHANGES
|
||||
version-resolver:
|
||||
major:
|
||||
labels:
|
||||
- breaking
|
||||
- major
|
||||
minor:
|
||||
labels:
|
||||
- feature
|
||||
- minor
|
||||
patch:
|
||||
labels:
|
||||
- bug
|
||||
- dependencies
|
||||
- security
|
||||
- patch
|
||||
default: patch
|
||||
14
.github/workflows/release_log.yml
vendored
14
.github/workflows/release_log.yml
vendored
@@ -1,14 +0,0 @@
|
||||
name: Update release draft
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
update_release_draft:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
with:
|
||||
config-name: release-drafter.yml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
## [Unreleased](https://github.com/GDay/django-q2/tree/HEAD)
|
||||
|
||||
## [v1.4.7](https://github.com/GDay/django-q2/tree/v1.4.7) (2022-12-21)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix: handling exceptions inside job function https://github.com/GDay/django-q2/pull/51
|
||||
- Fix: Daylight saving time issue with scheduler https://github.com/GDay/django-q2/pull/47
|
||||
- Chore: Fix badge and add download badge https://github.com/GDay/django-q2/pull/52
|
||||
- Chore: Remove release drafter https://github.com/GDay/django-q2/pull/53
|
||||
|
||||
## [v1.4.6](https://github.com/GDay/django-q2/tree/v1.4.6) (2022-11-30)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
A multiprocessing distributed task queue for Django
|
||||
---------------------------------------------------
|
||||
|
||||
|image0| |image1| |docs|
|
||||
|image0| |image1| |docs| |downloads|
|
||||
|
||||
::
|
||||
|
||||
@@ -245,4 +245,6 @@ Acknowledgements
|
||||
.. |docs| image:: https://readthedocs.org/projects/docs/badge/?version=latest
|
||||
:alt: Documentation Status
|
||||
:scale: 100
|
||||
:target: https://django-q.readthedocs.org/
|
||||
:target: https://django-q2.readthedocs.org/
|
||||
.. |downloads| image:: https://img.shields.io/pypi/dm/django-q2
|
||||
:target: https://img.shields.io/pypi/dm/django-q2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import django
|
||||
|
||||
VERSION = (1, 4, 6)
|
||||
VERSION = (1, 4, 7)
|
||||
|
||||
if django.VERSION < (3, 2):
|
||||
default_app_config = "django_q.apps.DjangoQConfig"
|
||||
|
||||
@@ -6,6 +6,7 @@ import socket
|
||||
import traceback
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from pytz import timezone as pytz_timezone
|
||||
from multiprocessing import Event, Process, Value, current_process
|
||||
from time import sleep
|
||||
|
||||
@@ -43,7 +44,7 @@ from django_q.signals import post_execute, pre_execute
|
||||
from django_q.signing import BadSignature, SignedPackage
|
||||
from django_q.status import Stat, Status
|
||||
|
||||
from .utils import add_months, add_years, get_func_repr
|
||||
from .utils import get_func_repr, localtime
|
||||
|
||||
|
||||
class Cluster:
|
||||
@@ -484,19 +485,12 @@ def worker(
|
||||
try:
|
||||
res = f(*task["args"], **task["kwargs"])
|
||||
result = (res, True)
|
||||
except Exception:
|
||||
result = (
|
||||
_(
|
||||
"Could not process '%(func_name)s'. Check the location of the "
|
||||
"function and the args/kwargs."
|
||||
)
|
||||
% {"func_name": func_name},
|
||||
False,
|
||||
)
|
||||
except Exception as e:
|
||||
result = (f"{e} : {traceback.format_exc()}", False)
|
||||
if error_reporter:
|
||||
error_reporter.report()
|
||||
if task.get("sync", False):
|
||||
raise Exception(result)
|
||||
raise
|
||||
with timer.get_lock():
|
||||
# Process result
|
||||
task["result"] = result[0]
|
||||
@@ -700,33 +694,7 @@ def scheduler(broker: Broker = None):
|
||||
if s.schedule_type != s.ONCE:
|
||||
next_run = s.next_run
|
||||
while True:
|
||||
if s.schedule_type == s.MINUTES:
|
||||
next_run = next_run + timedelta(minutes=(s.minutes or 1))
|
||||
elif s.schedule_type == s.HOURLY:
|
||||
next_run = next_run + timedelta(hours=1)
|
||||
elif s.schedule_type == s.DAILY:
|
||||
next_run = next_run + timedelta(days=1)
|
||||
elif s.schedule_type == s.WEEKLY:
|
||||
next_run = next_run + timedelta(weeks=1)
|
||||
elif s.schedule_type == s.BIWEEKLY:
|
||||
next_run = next_run + timedelta(weeks=2)
|
||||
elif s.schedule_type == s.MONTHLY:
|
||||
next_run = add_months(next_run, 1)
|
||||
elif s.schedule_type == s.BIMONTHLY:
|
||||
next_run = add_months(next_run, 2)
|
||||
elif s.schedule_type == s.QUARTERLY:
|
||||
next_run = add_months(next_run, 3)
|
||||
elif s.schedule_type == s.YEARLY:
|
||||
next_run = add_years(next_run, 1)
|
||||
elif s.schedule_type == s.CRON:
|
||||
if not croniter:
|
||||
raise ImportError(
|
||||
_(
|
||||
"Please install croniter to enable cron "
|
||||
"expressions"
|
||||
)
|
||||
)
|
||||
next_run = croniter(s.cron, localtime()).get_next(datetime)
|
||||
next_run = s.calculate_next_run(next_run)
|
||||
if Conf.CATCH_UP or next_run > localtime():
|
||||
break
|
||||
|
||||
@@ -842,10 +810,3 @@ def rss_check():
|
||||
elif psutil:
|
||||
return psutil.Process().memory_info().rss >= Conf.MAX_RSS * 1024
|
||||
return False
|
||||
|
||||
|
||||
def localtime() -> datetime:
|
||||
"""Override for timezone.localtime to deal with naive times and local times"""
|
||||
if settings.USE_TZ:
|
||||
return timezone.localtime()
|
||||
return datetime.now()
|
||||
|
||||
@@ -211,6 +211,11 @@ class Conf:
|
||||
# to manage workarounds during testing
|
||||
TESTING = conf.get("testing", False)
|
||||
|
||||
# Timezone for next_run, overrules Django timezone
|
||||
TIME_ZONE = None
|
||||
if settings.USE_TZ:
|
||||
TIME_ZONE = conf.get("time_zone", settings.TIME_ZONE)
|
||||
|
||||
|
||||
# logger
|
||||
logger = logging.getLogger("django-q")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Django
|
||||
from django import get_version
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -5,6 +7,7 @@ from django.db import models
|
||||
from django.template.defaultfilters import truncatechars
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import is_aware
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -13,8 +16,9 @@ from picklefield import PickledObjectField
|
||||
from picklefield.fields import dbsafe_decode
|
||||
|
||||
# Local
|
||||
from django_q.conf import croniter
|
||||
from django_q.conf import croniter, Conf
|
||||
from django_q.signing import SignedPackage
|
||||
from django_q.utils import localtime, add_months, add_years
|
||||
|
||||
from .utils import get_func_repr
|
||||
|
||||
@@ -208,6 +212,59 @@ class Schedule(models.Model):
|
||||
task = models.CharField(max_length=100, null=True, editable=False)
|
||||
cluster = models.CharField(max_length=100, default=None, null=True, blank=True)
|
||||
|
||||
def calculate_next_run(self, next_run=None):
|
||||
# next run is always in UTC
|
||||
next_run = next_run or self.next_run
|
||||
|
||||
if self.schedule_type == self.CRON:
|
||||
if not croniter:
|
||||
raise ImportError(
|
||||
_("Please install croniter to enable cron expressions")
|
||||
)
|
||||
return croniter(self.cron, localtime()).get_next(datetime)
|
||||
|
||||
if self.schedule_type == self.MINUTES:
|
||||
add = timedelta(minutes=(self.minutes or 1))
|
||||
elif self.schedule_type == self.HOURLY:
|
||||
add = timedelta(hours=1)
|
||||
elif self.schedule_type == self.DAILY:
|
||||
add = timedelta(days=1)
|
||||
elif self.schedule_type == self.WEEKLY:
|
||||
add = timedelta(weeks=1)
|
||||
elif self.schedule_type == self.BIWEEKLY:
|
||||
add = timedelta(weeks=2)
|
||||
elif self.schedule_type == self.MONTHLY:
|
||||
add = timedelta(days=(add_months(next_run, 1) - next_run).days)
|
||||
elif self.schedule_type == self.BIMONTHLY:
|
||||
add = timedelta(days=(add_months(next_run, 2) - next_run).days)
|
||||
elif self.schedule_type == self.QUARTERLY:
|
||||
add = timedelta(days=(add_months(next_run, 3) - next_run).days)
|
||||
elif self.schedule_type == self.YEARLY:
|
||||
add = timedelta(days=(add_years(next_run, 1) - next_run).days)
|
||||
|
||||
# add normal timedelta, we will correct this later based on timezone
|
||||
next_run += add
|
||||
|
||||
# DST differencers don't matter with minutes, hourly or yearly, so skip those
|
||||
if self.schedule_type not in [self.MINUTES, self.HOURLY, self.YEARLY]:
|
||||
# Get localtimes and then remove the tzinfo, so we can get the actual difference
|
||||
current_next_run = localtime(next_run - add).replace(tzinfo=None)
|
||||
new_next_run = localtime(next_run).replace(tzinfo=None)
|
||||
|
||||
# get the difference between them, this should be (-)1 or (-)0.5 hour
|
||||
# based on DST active or not
|
||||
extra_diff = (new_next_run - current_next_run) - add
|
||||
|
||||
# if we have one positive hour difference, then subtract it, so we are even
|
||||
# and vice versa. In most cases, this will be 0, as there won't be a
|
||||
# timezone diff
|
||||
if extra_diff > timedelta(hours=0):
|
||||
next_run -= extra_diff
|
||||
else:
|
||||
next_run += extra_diff
|
||||
|
||||
return next_run
|
||||
|
||||
def success(self):
|
||||
if self.task and Task.objects.filter(id=self.task):
|
||||
return Task.objects.get(id=self.task).success
|
||||
|
||||
@@ -75,7 +75,7 @@ DATABASES = {
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
TIME_ZONE = "Europe/Amsterdam"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ from django_q.tasks import (
|
||||
result,
|
||||
result_group,
|
||||
)
|
||||
from django_q.tests.tasks import multiply
|
||||
from django_q.tests.tasks import multiply, TaskError
|
||||
from django_q.utils import add_months, add_years
|
||||
|
||||
myPath = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -64,7 +64,7 @@ def test_sync(broker):
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_sync_raise_exception(broker):
|
||||
with pytest.raises(Exception):
|
||||
with pytest.raises(TaskError):
|
||||
async_task("django_q.tests.tasks.raise_exception", broker=broker, sync=True)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
from datetime import timedelta
|
||||
import pytz
|
||||
from datetime import datetime, timedelta
|
||||
from multiprocessing import Event, Value
|
||||
from unittest import mock
|
||||
|
||||
@@ -83,6 +84,63 @@ MULTIPLE_APPS_DATABASES = {
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_scheduler_daylight_saving_time_daily(broker, monkeypatch):
|
||||
# Set up a startdate in the Amsterdam timezone (without dst 1 hour ahead). The
|
||||
# 28th of March 2021 is the day when sunlight saving starts (at 2 am)
|
||||
|
||||
monkeypatch.setattr(Conf, "TIME_ZONE", "Europe/Amsterdam")
|
||||
tz = pytz.timezone('Europe/Amsterdam')
|
||||
broker.list_key = "scheduler_test:q"
|
||||
# Let's start a schedule at 1 am on the 27th of March. This is in AMS timezone.
|
||||
# So, 2021-03-27 00:00:00 when saved (due to TZ being Amsterdam and saved in UTC)
|
||||
start_date = datetime(2021, 3, 27, 1, 0, 0)
|
||||
|
||||
# Create schedule with the next run date on the start date. It will move one day
|
||||
# forward when we run the scheduler
|
||||
schedule = create_schedule(
|
||||
"math.copysign",
|
||||
1,
|
||||
-1,
|
||||
name="test math",
|
||||
schedule_type=Schedule.DAILY,
|
||||
next_run=start_date,
|
||||
)
|
||||
|
||||
# Run scheduler so we get the next run date
|
||||
scheduler(broker=broker)
|
||||
schedule.refresh_from_db()
|
||||
|
||||
# It's now the day after exactly at midnight UTC
|
||||
next_run = schedule.next_run
|
||||
assert str(next_run) == "2021-03-28 00:00:00+00:00"
|
||||
|
||||
# In the Amsterdam timezone, it's 1 hour over midnight (+01)
|
||||
next_run = next_run.astimezone(tz)
|
||||
assert str(next_run) == "2021-03-28 01:00:00+01:00"
|
||||
|
||||
# Run scheduler so we get the next run date
|
||||
scheduler(broker=broker)
|
||||
schedule.refresh_from_db()
|
||||
|
||||
next_run = schedule.next_run
|
||||
|
||||
assert str(next_run) == "2021-03-28 23:00:00+00:00"
|
||||
next_run = next_run.astimezone(tz)
|
||||
# In the Amsterdam timezone, it's 1 hour over midnight (+02)
|
||||
assert str(next_run) == "2021-03-29 01:00:00+02:00"
|
||||
|
||||
# Run scheduler so we get the next run date
|
||||
scheduler(broker=broker)
|
||||
schedule.refresh_from_db()
|
||||
|
||||
next_run = schedule.next_run
|
||||
|
||||
assert str(next_run) == "2021-03-29 23:00:00+00:00"
|
||||
next_run = next_run.astimezone(tz)
|
||||
assert str(next_run) == "2021-03-30 01:00:00+02:00"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_scheduler(broker, monkeypatch):
|
||||
broker.list_key = "scheduler_test:q"
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
import calendar
|
||||
import inspect
|
||||
from datetime import date
|
||||
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
from django_q.conf import Conf
|
||||
|
||||
# credits: https://stackoverflow.com/a/4131114
|
||||
# Made them aware of timezone
|
||||
@@ -39,3 +45,13 @@ def get_func_repr(func):
|
||||
)
|
||||
else:
|
||||
return str(func)
|
||||
|
||||
|
||||
def localtime(value=None) -> datetime:
|
||||
"""Override for timezone.localtime to deal with naive times and local times"""
|
||||
if settings.USE_TZ:
|
||||
return timezone.localtime(value=value, timezone=pytz.timezone(Conf.TIME_ZONE))
|
||||
if value is None:
|
||||
return datetime.now()
|
||||
else:
|
||||
return value
|
||||
|
||||
@@ -75,7 +75,7 @@ author = "Ilan Steemers, Stan Triepels"
|
||||
# The short X.Y version.
|
||||
version = "1.4"
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = "1.4.6"
|
||||
release = "1.4.7"
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
||||
@@ -70,6 +70,13 @@ Set this to something that makes sense for your project. Can be overridden for i
|
||||
|
||||
See :ref:`retry` for details how to set values for timeout and retry.
|
||||
|
||||
.. _time_zone:
|
||||
|
||||
time_zone
|
||||
~~~~~~~
|
||||
|
||||
The timezone that is used for task scheduling. Use this if you are having issue with DST. The scheduler uses UTC to calculate the next date and will therefore ignore any DST changes. This will cause 1 hour or 0.5 hour changes in the schedule when time is moved one hour ahead or back. Defaults to `settings.TIME_ZONE` if `USE_TZ` is enabled.
|
||||
|
||||
.. _ack_failures:
|
||||
|
||||
ack_failures
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "django-q2"
|
||||
version = "1.4.6"
|
||||
version = "1.4.7"
|
||||
packages = [
|
||||
{ include = "django_q" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user