Compare commits

...
24 Commits
Author SHA1 Message Date
Stan 386e04171b Release v1.9.0 2025-12-04 23:04:52 +01:00
Luke Plant 0054535624 Django 6.0 support (#307)
* Remove upper bound on Django version.

This allows it to be installed alongside Django 6 and later.

See https://iscinumpy.dev/post/bound-version-constraints/

* Run tests on Django 6.0 / Python 3.12
2025-12-04 17:15:18 +01:00
Salvo 'LtWorf' Tomaselli 09e65da5b3 Fix to make tests work with redis-py > 5 (#282) 2025-07-04 02:13:46 +02:00
Stan 65d2548e98 Release v1.8.0 2025-04-25 16:30:51 +02:00
Stan Triepels cbe51f995c Move timeout function from ORM broker into class to allow easy customization (#274)
* move timeout function to class to allow customization

* format

* fix format
2025-04-23 15:44:37 +02:00
Adam JohnsonandStan Triepels 0090a6f411 Update tested versions, add python 3.13 support and django 5.2 support. Drop python 3.8 support (#271)
* Update tested versions

* Remove f string

* Fix second `format_html`

---------

Co-authored-by: Stan Triepels <1939656+GDay@users.noreply.github.com>
2025-04-16 01:48:51 +02:00
Fernando Karchiloff a154fd658a Avoid creating task run on Scheduler creation (#268) 2025-03-12 23:31:00 +01:00
Adam Johnson 81f2531e3e Remove disque from CI (#270) 2025-03-12 23:28:59 +01:00
Stan Triepels c464751ae9 Delete deprecated imports and unwrapping (#261)
* delete old code

* remove imports
2025-01-12 03:40:18 +01:00
Stan 144536cbe4 Release v1.7.6 2025-01-12 02:25:29 +01:00
Stan Triepels 7e72b68c7f Make poetry version fixed in CI (#260) 2025-01-12 02:22:14 +01:00
Stan 404932117e Release v1.7.5 2025-01-12 01:57:13 +01:00
Anthony Hivert ddc1aa2de1 perf: avoid checking success tasks when save limit is disabled (#255)
- avoid extra queries when checking the count of success in db
2025-01-04 02:31:30 +01:00
mhaehnel 351bf66d71 Fix install path for CHANGELOG.md (#258)
The file should not be installed directly in site-packages. Use the
correct include syntax to include them into the source distribution
package only.
2025-01-02 01:52:07 +01:00
Stan ccd760a69f Release v1.7.4 2024-11-03 15:26:11 +01:00
Alberto fb23b6b825 Fix 'receive_message_wait_time_seconds' SQS broker management (#243)
* Fix 'receive_message_wait_time_seconds' SQS broker management

* Remove socket volume from localstack container
2024-11-03 15:04:35 +01:00
P-EB 35c911080b Fix BROKER_CLASS monkeypatch in test_brokers (#239) 2024-10-21 17:51:27 +02:00
P-EB 81058fc1cf Decrease the MAX_RSS set in test_cluster::test_max_rss (#240) 2024-10-21 17:50:30 +02:00
Stan 505eb8d537 Release v1.7.3 2024-10-15 04:17:28 +02:00
blockisecandStan Triepels dcfe5b6650 Fix missing ack_id when finishing task (#224)
* Update monitor.py

* format

---------

Co-authored-by: Stan Triepels <1939656+GDay@users.noreply.github.com>
2024-10-11 15:03:18 +02:00
Paulo CoutinhoandStan Triepels c3b49cca25 Only trigger prometheus if configured (#231)
* fix prometheus integration

* fix prometheus integration

* Add also lowercase option

As they seem to support that too: https://github.com/prometheus/client_python/blob/d7c9cd88c7f50097cd86869974301df7615bc9c0/prometheus_client/multiprocess.py#L166-L167

* format

---------

Co-authored-by: Stan Triepels <1939656+GDay@users.noreply.github.com>
2024-10-11 04:09:17 +02:00
Knuth c1e00b09cb Fix type check for args in scheduler.py E721 (#233) 2024-10-10 16:10:43 +02:00
Knuth 8e403d0136 Refactor timeout handling to handle AttributeError and ValueError for Windows users (#234) 2024-10-10 16:09:57 +02:00
Stan Triepels 337a5782d9 Catch missing SEGALRM with AttributeError instead of ValueError (#223) 2024-09-22 04:12:38 +02:00
22 changed files with 242 additions and 81 deletions
+29 -15
View File
@@ -26,25 +26,39 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ] python-version:
django: [ "4.2", "5.0", "5.1"] - "3.9"
- "3.10"
- "3.11"
- "3.12"
django:
- "4.2"
- "5.0"
- "5.1"
- "5.2"
- "6.0"
exclude: exclude:
# django 5.1 does not support 3.8 and 3.9 # django 5.2 does not support 3.9
- python-version: "3.8" - python-version: "3.9"
django: "5.1" django: "5.2"
# django 5.1 does not support 3.9
- python-version: "3.9" - python-version: "3.9"
django: "5.1" django: "5.1"
# django 5.0 does not support 3.8 and 3.9 # django 5.0 does not support 3.9
- python-version: "3.8"
django: "5.0"
- python-version: "3.9" - python-version: "3.9"
django: "5.0" django: "5.0"
# django 4.2 does not support 3.13
- python-version: "3.13"
django: "4.2"
# django 6.0 does not support earlier than 3.12
- python-version: "3.9"
django: "6.0"
- python-version: "3.10"
django: "6.0"
- python-version: "3.11"
django: "6.0"
services: services:
disque:
image: efrecon/disque:1.0-rc1
ports:
- '7711:7711/tcp'
mongodb: mongodb:
image: mongo image: mongo
ports: ports:
@@ -73,8 +87,8 @@ jobs:
- name: Install dependencies with Django ${{ matrix.django }} - name: Install dependencies with Django ${{ matrix.django }}
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install poetry pip install poetry==1.8.5
poetry add "django==${{ matrix.django }}" --python=${{ matrix.python-version }} poetry add "django~=${{ matrix.django }}a1" --python=${{ matrix.python-version }}
poetry install -E testing poetry install -E testing
- name: Run Tests - name: Run Tests
run: | run: |
+37
View File
@@ -1,5 +1,42 @@
# Changelog # Changelog
## [v1.9.0](https://github.com/django-q2/django-q2/tree/v1.9.0) (2025-12-04)
- Django 6.0 support https://github.com/django-q2/django-q2/pull/307
- Fix to make tests work with redis-py > 5 https://github.com/django-q2/django-q2/pull/282
## [v1.8.0](https://github.com/django-q2/django-q2/tree/v1.8.0) (2025-04-25)
- Delete deprecated imports and unwrapping https://github.com/django-q2/django-q2/pull/261
- Remove disque from CI https://github.com/django-q2/django-q2/pull/270
- Avoid creating task run on Scheduler creation https://github.com/django-q2/django-q2/pull/268
- Update tested versions, add python 3.13 support and django 5.2 support. Drop python 3.8 support https://github.com/django-q2/django-q2/pull/271
- Move timeout function from ORM broker into class to allow easy customization https://github.com/django-q2/django-q2/pull/274
## [v1.7.6](https://github.com/django-q2/django-q2/tree/v1.7.6) (2025-01-12)
- Make poetry version fixed in CI https://github.com/django-q2/django-q2/pull/260
## [v1.7.5](https://github.com/django-q2/django-q2/tree/v1.7.5) (2025-01-12)
- perf: avoid checking success tasks when save limit is disabled https://github.com/django-q2/django-q2/pull/255
- Fix install path for CHANGELOG.md https://github.com/django-q2/django-q2/pull/258
## [v1.7.4](https://github.com/django-q2/django-q2/tree/v1.7.4) (2024-11-03)
- Decrease the MAX_RSS set in test_cluster::test_max_rss https://github.com/django-q2/django-q2/pull/240
- Fix BROKER_CLASS monkeypatch in test_brokers https://github.com/django-q2/django-q2/pull/239
- Fix 'receive_message_wait_time_seconds' SQS broker management https://github.com/django-q2/django-q2/pull/243
## [v1.7.3](https://github.com/django-q2/django-q2/tree/v1.7.3) (2024-10-15)
- Catch missing SEGALRM with AttributeError instead of ValueError https://github.com/django-q2/django-q2/pull/223
- Refactor timeout handling to handle AttributeError and ValueError for Windows users https://github.com/django-q2/django-q2/pull/234
- Fix type check for args in scheduler.py E721 https://github.com/django-q2/django-q2/pull/233
- Only trigger prometheus if configured https://github.com/django-q2/django-q2/pull/231
- Fix missing ack_id when finishing task https://github.com/django-q2/django-q2/pull/224
## [v1.7.2](https://github.com/django-q2/django-q2/tree/v1.7.2) (2024-09-09) ## [v1.7.2](https://github.com/django-q2/django-q2/tree/v1.7.2) (2024-09-09)
- Fix twine check - Fix twine check
+4
View File
@@ -7,6 +7,10 @@ ENV PYTHONUNBUFFERED 1
# Sets the default shell to bash # Sets the default shell to bash
ENV SHELL /bin/bash ENV SHELL /bin/bash
RUN set -ex \
&& apt update \
&& apt-get install gcc python3-dev --yes
# Upgrades pip # Upgrades pip
RUN pip install -U pip setuptools RUN pip install -U pip setuptools
+4 -4
View File
@@ -2,16 +2,16 @@ dev:
docker compose -f web-docker-compose.yaml up docker compose -f web-docker-compose.yaml up
test: test:
docker-compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run pytest docker compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run pytest
shell: shell:
docker-compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run python manage.py shell docker compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run python manage.py shell
makemigrations: makemigrations:
docker-compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run python manage.py makemigrations docker compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run python manage.py makemigrations
migrate: migrate:
docker-compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run python manage.py migrate docker compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run python manage.py migrate
createsuperuser: createsuperuser:
docker compose -f web-docker-compose.yaml run --rm web python manage.py createsuperuser docker compose -f web-docker-compose.yaml run --rm web python manage.py createsuperuser
+4 -1
View File
@@ -38,7 +38,10 @@ Requirements
- `Django <https://www.djangoproject.com>`__ > = 4.2 - `Django <https://www.djangoproject.com>`__ > = 4.2
- `Django-picklefield <https://github.com/gintas/django-picklefield>`__ - `Django-picklefield <https://github.com/gintas/django-picklefield>`__
Tested with: Python 3.8, 3.9, 3.10, 3.11 and 3.12. Works with Django 4.2.X and 5.0.X Tested with:
* Python 3.9 to 3.13.
* Django 4.2 to 6.0.
Brokers Brokers
~~~~~~~ ~~~~~~~
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# Note that this file needs to have the executable bit set for it to work with later localstack implementations.
export DEFAULT_REGION=us-west-2
create_sqs() {
QUEUE_NAME="$1"
TIMEOUT=${2:-60}
DL_QUEUE_URL=$(awslocal sqs create-queue --queue-name "dl-$QUEUE_NAME" --query QueueUrl --output text)
echo ">>> Created $DL_QUEUE_URL queue!"
DL_QUEUE_ARN=$(awslocal sqs get-queue-attributes --queue-url "$DL_QUEUE_URL" --attribute-names QueueArn --query Attributes.QueueArn --output text)
awslocal sqs create-queue --queue-name "$QUEUE_NAME" --attributes '{
"RedrivePolicy": "{\"deadLetterTargetArn\": \"'"$DL_QUEUE_ARN"'\",\"maxReceiveCount\":\"3\"}",
"VisibilityTimeout": "'"$TIMEOUT"'"
}'
}
# Create SQS queues
create_sqs testing
+1 -6
View File
@@ -1,8 +1,3 @@
import django VERSION = (1, 9, 0)
VERSION = (1, 7, 2)
if django.VERSION < (3, 2):
default_app_config = "django_q.apps.DjangoQConfig"
__all__ = ["conf", "cluster", "models", "tasks"] __all__ = ["conf", "cluster", "models", "tasks"]
+1 -1
View File
@@ -138,7 +138,7 @@ class ScheduleAdmin(admin.ModelAdmin):
url = reverse("admin:django_q_success_change", args=(obj.task_id,)) url = reverse("admin:django_q_success_change", args=(obj.task_id,))
else: else:
url = reverse("admin:django_q_failure_change", args=(obj.task_id,)) url = reverse("admin:django_q_failure_change", args=(obj.task_id,))
return format_html(f'<a href="{url}">[{obj.task_name}]</a>') return format_html('<a href="{}">[{}]</a>', url, obj.task_name)
return None return None
get_last_run.allow_tags = True get_last_run.allow_tags = True
+9 -7
View File
@@ -1,3 +1,5 @@
import copy
from boto3 import Session from boto3 import Session
from botocore.client import ClientError from botocore.client import ClientError
@@ -78,15 +80,15 @@ class Sqs(Broker):
@staticmethod @staticmethod
def get_connection(list_key: str = None) -> Session: def get_connection(list_key: str = None) -> Session:
config = Conf.SQS config_cloned = copy.deepcopy(Conf.SQS)
if "aws_region" in config: if "aws_region" in config_cloned:
config["region_name"] = config["aws_region"] config_cloned["region_name"] = config_cloned["aws_region"]
del config["aws_region"] del config_cloned["aws_region"]
if "receive_message_wait_time_seconds" in config: if "receive_message_wait_time_seconds" in config_cloned:
del config["receive_message_wait_time_seconds"] del config_cloned["receive_message_wait_time_seconds"]
return Session(**config) return Session(**config_cloned)
def get_queue(self): def get_queue(self):
self.sqs = self.connection.resource("sqs") self.sqs = self.connection.resource("sqs")
+4 -5
View File
@@ -10,10 +10,6 @@ from django_q.conf import Conf, logger
from django_q.models import OrmQ from django_q.models import OrmQ
def _timeout():
return timezone.now() + timedelta(seconds=Conf.RETRY)
class ORM(Broker): class ORM(Broker):
@staticmethod @staticmethod
def get_connection(list_key: str = None): def get_connection(list_key: str = None):
@@ -28,6 +24,9 @@ class ORM(Broker):
logger.debug("Broker in an atomic transaction") logger.debug("Broker in an atomic transaction")
return OrmQ.objects.using(Conf.ORM) return OrmQ.objects.using(Conf.ORM)
def timeout(self, task):
return timezone.now() + timedelta(seconds=Conf.RETRY)
def queue_size(self) -> int: def queue_size(self) -> int:
return ( return (
self.get_connection() self.get_connection()
@@ -75,7 +74,7 @@ class ORM(Broker):
if ( if (
self.get_connection() self.get_connection()
.filter(id=task.id, lock=task.lock) .filter(id=task.id, lock=task.lock)
.update(lock=_timeout()) .update(lock=self.timeout(task))
): ):
task_list.append((task.pk, task.payload)) task_list.append((task.pk, task.payload))
# else don't process, as another cluster has been faster than us on # else don't process, as another cluster has been faster than us on
+8 -1
View File
@@ -1,4 +1,5 @@
# Standard # Standard
import os
import signal import signal
import socket import socket
import uuid import uuid
@@ -230,8 +231,14 @@ class Sentinel:
% {"name": process.name} % {"name": process.name}
) )
else: else:
if prometheus_multiprocess: # check if prometheus is proper configurated
prometheus_path = os.getenv(
"PROMETHEUS_MULTIPROC_DIR", os.getenv("prometheus_multiproc_dir")
)
if prometheus_multiprocess and prometheus_path:
prometheus_multiprocess.mark_process_dead(process.pid) prometheus_multiprocess.mark_process_dead(process.pid)
self.pool.remove(process) self.pool.remove(process)
self.spawn_worker() self.spawn_worker()
if process.timer.value == 0: if process.timer.value == 0:
+8 -12
View File
@@ -2,7 +2,6 @@ from datetime import datetime, timedelta
from keyword import iskeyword from keyword import iskeyword
# Django # Django
from django import get_version
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
@@ -15,7 +14,6 @@ from django.utils.translation import gettext_lazy as _
# External # External
from picklefield import PickledObjectField from picklefield import PickledObjectField
from picklefield.fields import dbsafe_decode
# Local # Local
from django_q.conf import croniter from django_q.conf import croniter
@@ -59,7 +57,7 @@ class Task(models.Model):
.exclude(success=False) .exclude(success=False)
.values_list("result", flat=True) .values_list("result", flat=True)
) )
return decode_results(values) return values
def group_result(self, failures=False): def group_result(self, failures=False):
if self.group: if self.group:
@@ -298,12 +296,18 @@ class Schedule(models.Model):
url = reverse("admin:django_q_success_change", args=(task.id,)) url = reverse("admin:django_q_success_change", args=(task.id,))
else: else:
url = reverse("admin:django_q_failure_change", args=(task.id,)) url = reverse("admin:django_q_failure_change", args=(task.id,))
return format_html(f'<a href="{url}">[{task.name}]</a>') return format_html('<a href="{}">[{}]</a>', url, task.name)
return None return None
def __str__(self): def __str__(self):
return self.func return self.func
def save(self, *args, **kwargs):
if self.pk is None and self.schedule_type == self.CRON:
self.next_run = self.calculate_next_run()
return super().save(*args, **kwargs)
success.boolean = True success.boolean = True
success.short_description = _("success") success.short_description = _("success")
last_run.allow_tags = True last_run.allow_tags = True
@@ -356,11 +360,3 @@ class OrmQ(models.Model):
app_label = "django_q" app_label = "django_q"
verbose_name = _("Queued task") verbose_name = _("Queued task")
verbose_name_plural = _("Queued tasks") verbose_name_plural = _("Queued tasks")
# Backwards compatibility for Django 1.7
def decode_results(values):
if get_version().split(".")[1] == "7":
# decode values in 1.7
return [dbsafe_decode(v) for v in values]
return values
+15 -8
View File
@@ -107,13 +107,16 @@ def save_task(task, broker: Broker):
value = get_func_repr(value) value = get_func_repr(value)
filters[Conf.SAVE_LIMIT_PER] = value filters[Conf.SAVE_LIMIT_PER] = value
with db.transaction.atomic(using=db.router.db_for_write(Success)): # check if we should clean the success tasks
list(Success.objects.filter(**filters).select_for_update()) if Conf.SAVE_LIMIT > 0:
if ( with db.transaction.atomic(using=db.router.db_for_write(Success)):
task["success"] success_tasks_qs = Success.objects.filter(**filters)
and 0 < Conf.SAVE_LIMIT <= Success.objects.filter(**filters).count() success_tasks_pks = [
): success_task.pk
Success.objects.filter(**filters).last().delete() for success_task in success_tasks_qs.select_for_update()
]
if task["success"] and len(success_tasks_pks) >= Conf.SAVE_LIMIT:
success_tasks_qs.last().delete()
# check if this task has previous results # check if this task has previous results
try: try:
@@ -145,7 +148,11 @@ def save_task(task, broker: Broker):
attempt_count=1, attempt_count=1,
) )
if Conf.MAX_ATTEMPTS > 0 and task_obj.attempt_count >= Conf.MAX_ATTEMPTS: if (
Conf.MAX_ATTEMPTS > 0
and task_obj.attempt_count >= Conf.MAX_ATTEMPTS
and task.get("ack_id")
):
broker.acknowledge(task["ack_id"]) broker.acknowledge(task["ack_id"])
except Exception: except Exception:
+1 -1
View File
@@ -65,7 +65,7 @@ def scheduler(broker: Broker = None):
if s.args: if s.args:
args = ast.literal_eval(s.args) args = ast.literal_eval(s.args)
# single value won't eval to tuple, so: # single value won't eval to tuple, so:
if type(args) != tuple: if type(args) is not tuple:
args = (args,) args = (args,)
q_options = kwargs.get("q_options", {}) q_options = kwargs.get("q_options", {})
if s.intended_date_kwarg: if s.intended_date_kwarg:
+1 -1
View File
@@ -116,7 +116,7 @@ CACHES = {
"LOCATION": f"redis://{REDIS_HOST}:6379/0", "LOCATION": f"redis://{REDIS_HOST}:6379/0",
"OPTIONS": { "OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient", "CLIENT_CLASS": "django_redis.client.DefaultClient",
"PARSER_CLASS": "redis.connection.HiredisParser", "PARSER_CLASS": "redis.connection.DefaultParser",
}, },
} }
} }
+7 -5
View File
@@ -51,7 +51,7 @@ def test_redis(monkeypatch):
def test_custom(monkeypatch): def test_custom(monkeypatch):
monkeypatch.setattr(Conf, "BROKER_CLASS", "brokers.redis_broker.Redis") monkeypatch.setattr(Conf, "BROKER_CLASS", "django_q.brokers.redis_broker.Redis")
broker = get_broker() broker = get_broker()
assert broker.ping() is True assert broker.ping() is True
assert broker.info() is not None assert broker.info() is not None
@@ -124,7 +124,7 @@ def test_ironmq(monkeypatch):
@pytest.mark.skipif( @pytest.mark.skipif(
not os.getenv("AWS_ACCESS_KEY_ID"), reason="requires AWS credentials" not os.getenv("AWS_ACCESS_KEY_ID"), reason="requires AWS credentials"
) )
def canceled_sqs(monkeypatch): def test_sqs(monkeypatch):
monkeypatch.setattr( monkeypatch.setattr(
Conf, Conf,
"SQS", "SQS",
@@ -132,11 +132,13 @@ def canceled_sqs(monkeypatch):
"aws_region": os.getenv("AWS_REGION"), "aws_region": os.getenv("AWS_REGION"),
"aws_access_key_id": os.getenv("AWS_ACCESS_KEY_ID"), "aws_access_key_id": os.getenv("AWS_ACCESS_KEY_ID"),
"aws_secret_access_key": os.getenv("AWS_SECRET_ACCESS_KEY"), "aws_secret_access_key": os.getenv("AWS_SECRET_ACCESS_KEY"),
"receive_message_wait_time_seconds": 20, "receive_message_wait_time_seconds": 5,
}, },
) )
# check broker # check broker
broker = get_broker(list_key=uuid()[0]) broker = get_broker(list_key="testing")
assert "receive_message_wait_time_seconds" in Conf.SQS
assert "aws_region" in Conf.SQS
assert broker.ping() is True assert broker.ping() is True
assert broker.info() is not None assert broker.info() is not None
assert broker.queue_size() == 0 assert broker.queue_size() == 0
@@ -173,7 +175,7 @@ def canceled_sqs(monkeypatch):
broker.enqueue("test") broker.enqueue("test")
while task is None: while task is None:
task = broker.dequeue()[0] task = broker.dequeue()[0]
broker.fail(task[0]) broker.fail(task[0][0])
# bulk test # bulk test
for _ in range(10): for _ in range(10):
broker.enqueue("test") broker.enqueue("test")
+4 -3
View File
@@ -399,7 +399,7 @@ def test_timeout_task_finishes(broker, cluster_config_timeout, async_task_kwargs
@pytest.mark.django_db @pytest.mark.django_db
def test_recycle(broker, monkeypatch): def test_recycle(broker, monkeypatch, django_assert_num_queries):
# set up the Sentinel # set up the Sentinel
broker.list_key = "test_recycle_test:q" broker.list_key = "test_recycle_test:q"
async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker) async_task("django_q.tests.tasks.multiply", 2, 2, broker=broker)
@@ -432,7 +432,8 @@ def test_recycle(broker, monkeypatch):
monkeypatch.setattr(Conf, "SAVE_LIMIT", 1) monkeypatch.setattr(Conf, "SAVE_LIMIT", 1)
result_queue.put("STOP") result_queue.put("STOP")
# run monitor # run monitor
monitor(result_queue) with django_assert_num_queries(12):
monitor(result_queue)
assert Success.objects.count() == Conf.SAVE_LIMIT assert Success.objects.count() == Conf.SAVE_LIMIT
broker.delete_queue() broker.delete_queue()
@@ -487,7 +488,7 @@ def test_max_rss(broker, monkeypatch):
stop_event = Event() stop_event = Event()
cluster_id = uuidlib.uuid4() cluster_id = uuidlib.uuid4()
# override settings # override settings
monkeypatch.setattr(Conf, "MAX_RSS", 40000) monkeypatch.setattr(Conf, "MAX_RSS", 20000)
monkeypatch.setattr(Conf, "WORKERS", 1) monkeypatch.setattr(Conf, "WORKERS", 1)
# set a timer to stop the Sentinel # set a timer to stop the Sentinel
threading.Timer(3, stop_event.set).start() threading.Timer(3, stop_event.set).start()
+41
View File
@@ -473,6 +473,47 @@ def test_scheduler_atomic_must_specify_the_database_based_on_router_redirection(
mocked_db.atomic.assert_called_with(using="default") mocked_db.atomic.assert_called_with(using="default")
@pytest.mark.django_db
def test_schedule_save_sets_next_run_for_cron():
"""Ensure Schedule.save() sets next_run correctly for CRON schedules."""
cron_expression = "0 12 * * *" # Executes dialy at 12pm
schedule = Schedule(
func="math.sqrt",
schedule_type=Schedule.CRON,
cron=cron_expression,
)
assert schedule.next_run is not None
assert schedule.pk is None
initial_next_run = schedule.next_run
schedule.save()
# After save, next_run must be recalculated based on the CRON expression
assert schedule.next_run > initial_next_run
@pytest.mark.django_db
def test_schedule_save_direct_db(broker):
"""Ensure Schedule.save() updates next_run correctly when created directly in DB."""
cron_expression = "0 12 * * *" # Executes dialy at 12pm
# Creating schedule directly in database
schedule = Schedule.objects.create(
func="math.sqrt",
schedule_type=Schedule.CRON,
cron=cron_expression,
)
# Next run must be defined after execution time
assert schedule.next_run is not None
assert schedule.next_run > timezone.now()
scheduler(broker)
assert broker.queue_size() == 0
def test_localtime(): def test_localtime():
assert not is_naive(localtime()) assert not is_naive(localtime())
+9 -4
View File
@@ -22,11 +22,13 @@ class TimeoutHandler:
return return
try: try:
signal.signal(signal.SIGALRM, self.raise_timeout_exception) signal.signal(signal.SIGALRM, self.raise_timeout_exception)
except ValueError: # ValueError is raised for Windows users signal.alarm(self._timeout)
except (
ValueError,
AttributeError,
): # AttributeError or ValueError might be raised for Windows users
logger.debug(_("SIGALARM is not available on your platform")) logger.debug(_("SIGALARM is not available on your platform"))
signal.alarm(self._timeout)
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, exc_type, exc_value, traceback):
if self._timeout == -1: if self._timeout == -1:
return return
@@ -34,5 +36,8 @@ class TimeoutHandler:
try: try:
signal.alarm(0) signal.alarm(0)
signal.signal(signal.SIGALRM, signal.SIG_DFL) signal.signal(signal.SIGALRM, signal.SIG_DFL)
except ValueError: # ValueError is raised for Windows users except (
ValueError,
AttributeError,
): # AttributeError or ValueError might be raised for Windows users
logger.debug(_("SIGALARM is not available on your platform")) logger.debug(_("SIGALARM is not available on your platform"))
+2 -2
View File
@@ -73,9 +73,9 @@ author = "Ilan Steemers, Stan Triepels"
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = "1.7" version = "1.9"
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = "1.7.2" release = "1.9.0"
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
+8 -5
View File
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry] [tool.poetry]
name = "django-q2" name = "django-q2"
version = "1.7.2" version = "1.9.0"
packages = [ packages = [
{ include = "django_q" }, { include = "django_q" },
] ]
@@ -30,16 +30,19 @@ classifiers = [
'Operating System :: MacOS', 'Operating System :: MacOS',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP',
'Topic :: System :: Distributed Computing', 'Topic :: System :: Distributed Computing',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',
] ]
include = ['CHANGELOG.md']
include = [
{ path = "CHANGELOG.md", format = "sdist" },
]
[tool.poetry.plugins] # Optional super table [tool.poetry.plugins] # Optional super table
@@ -49,8 +52,8 @@ include = ['CHANGELOG.md']
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.8,<4" python = ">=3.9,<4"
django = ">=4.2, <6" django = ">=4.2"
django-picklefield = "^3.1" django-picklefield = "^3.1"
blessed = { version = "^1.19.1", optional = true } blessed = { version = "^1.19.1", optional = true }
+26
View File
@@ -13,15 +13,41 @@ services:
networks: networks:
- main - main
aws:
container_name: aws
image: localstack/localstack:3.4.0
ports:
- "127.0.0.1:4566:4566" # LocalStack Gateway
- "127.0.0.1:4510-4559:4510-4559" # External services port range
environment:
AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-west-2}
DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-west-2}
SQS_ENDPOINT_STRATEGY: path
SERVICES: sqs
LOCALSTACK_HOST: aws
DEBUG: 1
LS_LOG: trace
volumes:
- ./containers/localstack:/etc/localstack/init/ready.d
networks:
- main
django-q2: django-q2:
build: build:
dockerfile: ./Dockerfile.dev dockerfile: ./Dockerfile.dev
context: . context: .
environment:
AWS_ENDPOINT_URL: http://aws:4566
AWS_REGION: ${AWS_REGION:-us-west-2}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-test}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-test}
AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-west-2}
volumes: volumes:
- .:/app - .:/app
depends_on: depends_on:
- redis - redis
- mongo - mongo
- aws
networks: networks:
- main - main