mirror of
https://github.com/django-q2/django-q2.git
synced 2026-09-25 20:38:11 +08:00
Remove arrow dependency, update packages, set up testing with compose (#17)
This commit is contained in:
@@ -11,8 +11,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [ "3.7", "3.8", "3.9", "3.10" ]
|
||||
django: [ "2.2", "3.2" ]
|
||||
python-version: [ "3.8", "3.9", "3.10" ]
|
||||
django: [ "3.2", "4.1" ]
|
||||
|
||||
services:
|
||||
disque:
|
||||
image: efrecon/disque:1.0-rc1
|
||||
@@ -52,6 +53,9 @@ jobs:
|
||||
- name: Run Tests
|
||||
run: |
|
||||
poetry run pytest --cov=./django_q --cov-report=xml
|
||||
env:
|
||||
MONGO_HOST: "127.0.0.1"
|
||||
REDIS_HOST: "127.0.0.1"
|
||||
- name: Upload to coveralls
|
||||
run: |
|
||||
python -m pip install coveralls
|
||||
|
||||
+8
-24
@@ -7,34 +7,18 @@ ENV PYTHONUNBUFFERED 1
|
||||
# Sets the default shell to bash
|
||||
ENV SHELL /bin/bash
|
||||
|
||||
# Creates a non-root user
|
||||
RUN adduser --disabled-password docker
|
||||
|
||||
# Upgrades pip
|
||||
RUN pip install --upgrade pip
|
||||
RUN pip install --upgrade pip
|
||||
|
||||
# Poetry project setup for development
|
||||
# Copies poetry requirements files
|
||||
COPY --chown=docker Dockerfile.dev requirements.txt* setup.py* ./
|
||||
# Install poetry
|
||||
RUN pip install poetry
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip install pytest pytest-django codecov poetry
|
||||
COPY . .
|
||||
|
||||
# Clean up
|
||||
RUN apt-get autoremove -y \
|
||||
&& apt-get clean -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN poetry lock --no-update
|
||||
|
||||
WORKDIR /home/docker
|
||||
RUN poetry config virtualenvs.create false
|
||||
|
||||
# Sets the binaries to path
|
||||
ENV PATH="/home/docker/.local/bin:${PATH}"
|
||||
|
||||
# Copy in as non-root user, so permissions match what we need
|
||||
COPY --chown=docker:docker . .
|
||||
|
||||
RUN python setup.py develop
|
||||
|
||||
# Applies the container user to be non-root
|
||||
USER docker
|
||||
RUN poetry install -E testing
|
||||
|
||||
+8
-40
@@ -19,23 +19,21 @@ Features
|
||||
- Django Admin integration
|
||||
- PaaS compatible with multiple instances
|
||||
- Multi cluster monitor
|
||||
- Redis, Disque, IronMQ, SQS, MongoDB or ORM
|
||||
- Redis, IronMQ, SQS, MongoDB or ORM
|
||||
- Rollbar and Sentry support
|
||||
|
||||
Requirements
|
||||
~~~~~~~~~~~~
|
||||
|
||||
- `Django <https://www.djangoproject.com>`__ > = 2.2
|
||||
- `Django <https://www.djangoproject.com>`__ > = 3.2
|
||||
- `Django-picklefield <https://github.com/gintas/django-picklefield>`__
|
||||
- `Arrow <https://github.com/crsmithdev/arrow>`__
|
||||
- `Blessed <https://github.com/jquast/blessed>`__
|
||||
|
||||
Tested with: Python 3.7, 3.8, 3.9, 3.10 Django 2.2.X and 3.2.X
|
||||
Tested with: Python 3.7, 3.8, 3.9, 3.10 Django 3.2.X and 4.1.X
|
||||
|
||||
Brokers
|
||||
~~~~~~~
|
||||
- `Redis <https://django-q2.readthedocs.org/en/latest/brokers.html#redis>`__
|
||||
- `Disque <https://django-q2.readthedocs.org/en/latest/brokers.html#disque>`__
|
||||
- `IronMQ <https://django-q2.readthedocs.org/en/latest/brokers.html#ironmq>`__
|
||||
- `Amazon SQS <https://django-q2.readthedocs.org/en/latest/brokers.html#amazon-sqs>`__
|
||||
- `MongoDB <https://django-q2.readthedocs.org/en/latest/brokers.html#mongodb>`__
|
||||
@@ -173,14 +171,14 @@ Admin page or directly from your code:
|
||||
|
||||
# Run a task every 5 minutes, starting at 6 today
|
||||
# for 2 hours
|
||||
import arrow
|
||||
from datetime import datetime
|
||||
|
||||
schedule('math.hypot',
|
||||
3, 4,
|
||||
schedule_type=Schedule.MINUTES,
|
||||
minutes=5,
|
||||
repeats=24,
|
||||
next_run=arrow.utcnow().replace(hour=18, minute=0))
|
||||
next_run=datetime.utcnow().replace(hour=18, minute=0))
|
||||
|
||||
# Use a cron expression
|
||||
schedule('math.hypot',
|
||||
@@ -194,45 +192,15 @@ For more info check the `Schedules <https://django-q2.readthedocs.org/en/latest/
|
||||
Testing
|
||||
~~~~~~~
|
||||
|
||||
To run the tests you will need the following in addition to install requirements:
|
||||
|
||||
* `py.test <http://pytest.org/latest/>`__
|
||||
* `pytest-django <https://github.com/pytest-dev/pytest-django>`__
|
||||
* Disque from https://github.com/antirez/disque.git
|
||||
* Redis
|
||||
* MongoDB
|
||||
|
||||
Or you can use the included Docker Compose file.
|
||||
|
||||
The following commands can be used to run the tests:
|
||||
Running tests is easy with docker compose, it will also start the necessary databases. Just run:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
# Create virtual environment
|
||||
python -m venv venv
|
||||
|
||||
# Install requirements
|
||||
venv/bin/pip install -r requirements.txt
|
||||
|
||||
# Install test dependencies
|
||||
venv/bin/pip install pytest pytest-django
|
||||
|
||||
# Install django-q
|
||||
venv/bin/python setup.py develop
|
||||
|
||||
# Run required services (you need to have docker-compose installed)
|
||||
docker-compose -f test-services-docker-compose.yaml up -d
|
||||
|
||||
# Run tests
|
||||
venv/bin/pytest
|
||||
|
||||
# Stop the services required by tests (when you no longer plan to run tests)
|
||||
docker-compose -f test-services-docker-compose.yaml down
|
||||
docker-compose -f test-services-docker-compose.yaml run --rm django-q2 poetry run pytest
|
||||
|
||||
Locale
|
||||
~~~~~~
|
||||
|
||||
Currently available in English, German and French.
|
||||
Currently available in English, German, Turkish, and French.
|
||||
Translation pull requests are always welcome.
|
||||
|
||||
Todo
|
||||
|
||||
@@ -173,11 +173,6 @@ def get_broker(list_key: str = Conf.PREFIX) -> Broker:
|
||||
m = importlib.import_module(module)
|
||||
broker = getattr(m, func)
|
||||
return broker(list_key=list_key)
|
||||
# disque
|
||||
elif Conf.DISQUE_NODES:
|
||||
from django_q.brokers import disque
|
||||
|
||||
return disque.Disque(list_key=list_key)
|
||||
# Iron MQ
|
||||
elif Conf.IRON_MQ:
|
||||
from django_q.brokers import ironmq
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import random
|
||||
|
||||
# External
|
||||
import redis
|
||||
|
||||
# Django
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from redis import Redis
|
||||
|
||||
from django_q.brokers import Broker
|
||||
from django_q.conf import Conf
|
||||
|
||||
|
||||
class Disque(Broker):
|
||||
def enqueue(self, task):
|
||||
retry = Conf.RETRY if Conf.RETRY > 0 else f"{Conf.RETRY} REPLICATE 1"
|
||||
return self.connection.execute_command(
|
||||
f"ADDJOB {self.list_key} {task} 500 RETRY {retry}"
|
||||
).decode()
|
||||
|
||||
def dequeue(self):
|
||||
tasks = self.connection.execute_command(
|
||||
f"GETJOB COUNT {Conf.BULK} TIMEOUT 1000 FROM {self.list_key}"
|
||||
)
|
||||
if tasks:
|
||||
return [(t[1].decode(), t[2].decode()) for t in tasks]
|
||||
|
||||
def queue_size(self):
|
||||
return self.connection.execute_command(f"QLEN {self.list_key}")
|
||||
|
||||
def acknowledge(self, task_id):
|
||||
command = "FASTACK" if Conf.DISQUE_FASTACK else "ACKJOB"
|
||||
return self.connection.execute_command(f"{command} {task_id}")
|
||||
|
||||
def ping(self) -> bool:
|
||||
return self.connection.execute_command("HELLO")[0] > 0
|
||||
|
||||
def delete(self, task_id):
|
||||
return self.connection.execute_command(f"DELJOB {task_id}")
|
||||
|
||||
def fail(self, task_id):
|
||||
return self.delete(task_id)
|
||||
|
||||
def delete_queue(self) -> int:
|
||||
jobs = self.connection.execute_command(f"JSCAN QUEUE {self.list_key}")[1]
|
||||
if jobs:
|
||||
job_ids = " ".join(jid.decode() for jid in jobs)
|
||||
self.connection.execute_command(f"DELJOB {job_ids}")
|
||||
return len(jobs)
|
||||
|
||||
def info(self) -> str:
|
||||
if not self._info:
|
||||
info = self.connection.info("server")
|
||||
self._info = f'Disque {info["disque_version"]}'
|
||||
return self._info
|
||||
|
||||
@staticmethod
|
||||
def get_connection(list_key: str = Conf.PREFIX) -> Redis:
|
||||
if not Conf.DISQUE_NODES:
|
||||
raise redis.exceptions.ConnectionError(_("No Disque nodes configured"))
|
||||
# randomize nodes
|
||||
random.shuffle(Conf.DISQUE_NODES)
|
||||
# find one that works
|
||||
for node in Conf.DISQUE_NODES:
|
||||
host, port = node.split(":")
|
||||
kwargs = {"host": host, "port": port}
|
||||
if Conf.DISQUE_AUTH:
|
||||
kwargs["password"] = Conf.DISQUE_AUTH
|
||||
redis_client = redis.Redis(**kwargs)
|
||||
redis_client.decode_responses = True
|
||||
try:
|
||||
redis_client.execute_command("HELLO")
|
||||
return redis_client
|
||||
except redis.exceptions.ConnectionError:
|
||||
continue
|
||||
raise redis.exceptions.ConnectionError(
|
||||
_("Could not connect to any Disque nodes")
|
||||
)
|
||||
+15
-23
@@ -6,13 +6,10 @@ import signal
|
||||
import socket
|
||||
import traceback
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from multiprocessing import Event, Process, Value, current_process
|
||||
from time import sleep
|
||||
|
||||
# External
|
||||
import arrow
|
||||
|
||||
# Django
|
||||
from django import core, db
|
||||
from django.apps.registry import apps
|
||||
@@ -47,6 +44,8 @@ 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
|
||||
|
||||
|
||||
class Cluster:
|
||||
def __init__(self, broker: Broker = None):
|
||||
@@ -635,22 +634,22 @@ def scheduler(broker: Broker = None):
|
||||
q_options["hook"] = s.hook
|
||||
# set up the next run time
|
||||
if s.schedule_type != s.ONCE:
|
||||
next_run = arrow.get(s.next_run)
|
||||
next_run = s.next_run
|
||||
while True:
|
||||
if s.schedule_type == s.MINUTES:
|
||||
next_run = next_run.shift(minutes=+(s.minutes or 1))
|
||||
next_run = next_run + timedelta(minutes=(s.minutes or 1))
|
||||
elif s.schedule_type == s.HOURLY:
|
||||
next_run = next_run.shift(hours=+1)
|
||||
next_run = next_run + timedelta(hours=1)
|
||||
elif s.schedule_type == s.DAILY:
|
||||
next_run = next_run.shift(days=+1)
|
||||
next_run = next_run + timedelta(days=1)
|
||||
elif s.schedule_type == s.WEEKLY:
|
||||
next_run = next_run.shift(weeks=+1)
|
||||
next_run = next_run + timedelta(weeks=1)
|
||||
elif s.schedule_type == s.MONTHLY:
|
||||
next_run = next_run.shift(months=+1)
|
||||
next_run = add_months(next_run, 1)
|
||||
elif s.schedule_type == s.QUARTERLY:
|
||||
next_run = next_run.shift(months=+3)
|
||||
next_run = add_months(next_run, 3)
|
||||
elif s.schedule_type == s.YEARLY:
|
||||
next_run = next_run.shift(years=+1)
|
||||
next_run = add_years(next_run, 1)
|
||||
elif s.schedule_type == s.CRON:
|
||||
if not croniter:
|
||||
raise ImportError(
|
||||
@@ -658,18 +657,11 @@ def scheduler(broker: Broker = None):
|
||||
"Please install croniter to enable cron expressions"
|
||||
)
|
||||
)
|
||||
next_run = arrow.get(
|
||||
croniter(s.cron, localtime()).get_next()
|
||||
)
|
||||
if Conf.CATCH_UP or next_run > arrow.utcnow():
|
||||
next_run = croniter(s.cron, localtime()).get_next(datetime)
|
||||
if Conf.CATCH_UP or next_run > localtime():
|
||||
break
|
||||
# arrow always returns a tz aware datetime, and we don't want
|
||||
# this when we explicitly configured django with USE_TZ=False
|
||||
s.next_run = (
|
||||
next_run.datetime
|
||||
if settings.USE_TZ
|
||||
else next_run.datetime.replace(tzinfo=None)
|
||||
)
|
||||
|
||||
s.next_run = next_run
|
||||
s.repeats += -1
|
||||
# send it to the cluster
|
||||
scheduled_broker = broker
|
||||
|
||||
@@ -45,15 +45,6 @@ class Conf:
|
||||
|
||||
DJANGO_REDIS = conf.get("django_redis", None)
|
||||
|
||||
# Disque broker
|
||||
DISQUE_NODES = conf.get("disque_nodes", None)
|
||||
|
||||
# Optional Authentication
|
||||
DISQUE_AUTH = conf.get("disque_auth", None)
|
||||
|
||||
# Optional Fast acknowledge
|
||||
DISQUE_FASTACK = conf.get("disque_fastack", False)
|
||||
|
||||
# IronMQ broker
|
||||
IRON_MQ = conf.get("iron_mq", None)
|
||||
|
||||
|
||||
@@ -106,11 +106,16 @@ LOGGING = {
|
||||
|
||||
STATIC_URL = "/static/"
|
||||
|
||||
REDIS_HOST = os.environ.get("REDIS_HOST", "redis")
|
||||
|
||||
MONGO_HOST = os.environ.get("MONGO_HOST", "mongo")
|
||||
|
||||
|
||||
# Django Redis
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": "redis://127.0.0.1:6379/0",
|
||||
"LOCATION": f"redis://{REDIS_HOST}:6379/0",
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"PARSER_CLASS": "redis.connection.HiredisParser",
|
||||
@@ -125,4 +130,5 @@ Q_CLUSTER = {
|
||||
"testing": True,
|
||||
"log_level": "DEBUG",
|
||||
"django_redis": "default",
|
||||
"redis": f"redis://{REDIS_HOST}:6379/0"
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import redis
|
||||
from django_q.brokers import Broker, get_broker
|
||||
from django_q.conf import Conf
|
||||
from django_q.humanhash import uuid
|
||||
from django_q.tests.settings import REDIS_HOST, MONGO_HOST
|
||||
|
||||
|
||||
def test_broker(monkeypatch):
|
||||
@@ -40,11 +41,11 @@ def test_redis(monkeypatch):
|
||||
broker = get_broker()
|
||||
assert broker.ping() is True
|
||||
assert broker.info() is not None
|
||||
monkeypatch.setattr(Conf, "REDIS", {"host": "127.0.0.1", "port": 7799})
|
||||
monkeypatch.setattr(Conf, "REDIS", {"host": REDIS_HOST, "port": 7799})
|
||||
broker = get_broker()
|
||||
with pytest.raises(Exception):
|
||||
broker.ping()
|
||||
monkeypatch.setattr(Conf, "REDIS", "redis://127.0.0.1:7799")
|
||||
monkeypatch.setattr(Conf, "REDIS", f"redis://{REDIS_HOST}:7799")
|
||||
broker = get_broker()
|
||||
with pytest.raises(Exception):
|
||||
broker.ping()
|
||||
@@ -58,68 +59,6 @@ def test_custom(monkeypatch):
|
||||
assert broker.__class__.__name__ == "Redis"
|
||||
|
||||
|
||||
def test_disque(monkeypatch):
|
||||
monkeypatch.setattr(Conf, "DISQUE_NODES", ["127.0.0.1:7711"])
|
||||
# check broker
|
||||
broker = get_broker(list_key="disque_test")
|
||||
assert broker.ping() is True
|
||||
assert broker.info() is not None
|
||||
# clear before we start
|
||||
broker.delete_queue()
|
||||
# async_task
|
||||
broker.enqueue("test")
|
||||
assert broker.queue_size() == 1
|
||||
# dequeue
|
||||
task = broker.dequeue()[0]
|
||||
assert task[1] == "test"
|
||||
broker.acknowledge(task[0])
|
||||
assert broker.queue_size() == 0
|
||||
# Retry test
|
||||
monkeypatch.setattr(Conf, "RETRY", 1)
|
||||
broker.enqueue("test")
|
||||
assert broker.queue_size() == 1
|
||||
broker.dequeue()
|
||||
assert broker.queue_size() == 0
|
||||
sleep(1.5)
|
||||
assert broker.queue_size() == 1
|
||||
task = broker.dequeue()[0]
|
||||
assert broker.queue_size() == 0
|
||||
broker.acknowledge(task[0])
|
||||
sleep(1.5)
|
||||
assert broker.queue_size() == 0
|
||||
# delete job
|
||||
task_id = broker.enqueue("test")
|
||||
broker.delete(task_id)
|
||||
assert broker.dequeue() is None
|
||||
# fail
|
||||
task_id = broker.enqueue("test")
|
||||
broker.fail(task_id)
|
||||
# bulk test
|
||||
for _ in range(5):
|
||||
broker.enqueue("test")
|
||||
monkeypatch.setattr(Conf, "BULK", 5)
|
||||
monkeypatch.setattr(Conf, "DISQUE_FASTACK", True)
|
||||
tasks = broker.dequeue()
|
||||
for task in tasks:
|
||||
assert task is not None
|
||||
broker.acknowledge(task[0])
|
||||
# test duplicate acknowledge
|
||||
broker.acknowledge(task[0])
|
||||
# delete queue
|
||||
broker.enqueue("test")
|
||||
broker.enqueue("test")
|
||||
broker.delete_queue()
|
||||
assert broker.queue_size() == 0
|
||||
# connection test
|
||||
monkeypatch.setattr(Conf, "DISQUE_NODES", ["127.0.0.1:7798", "127.0.0.1:7799"])
|
||||
with pytest.raises(redis.exceptions.ConnectionError):
|
||||
broker.get_connection()
|
||||
# connection test with no nodes
|
||||
monkeypatch.setattr(Conf, "DISQUE_NODES", None)
|
||||
with pytest.raises(redis.exceptions.ConnectionError):
|
||||
broker.get_connection()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.getenv("IRON_MQ_TOKEN"), reason="requires IronMQ credentials"
|
||||
)
|
||||
@@ -312,7 +251,7 @@ def test_orm(monkeypatch):
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_mongo(monkeypatch):
|
||||
monkeypatch.setattr(Conf, "MONGO", {"host": "127.0.0.1", "port": 27017})
|
||||
monkeypatch.setattr(Conf, "MONGO", {"host": MONGO_HOST, "port": 27017})
|
||||
# check broker
|
||||
broker = get_broker(list_key="mongo_test")
|
||||
assert broker.ping() is True
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from datetime import datetime
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
@@ -32,6 +33,7 @@ from django_q.tasks import (
|
||||
result_group,
|
||||
)
|
||||
from django_q.tests.tasks import TaskError, multiply
|
||||
from django_q.utils import add_months, add_years
|
||||
|
||||
|
||||
class WordClass:
|
||||
@@ -743,3 +745,43 @@ def assert_result(task):
|
||||
def assert_bad_result(task):
|
||||
assert task is not None
|
||||
assert task.success is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_add_months():
|
||||
# add some months
|
||||
initial_date = datetime(2020, 2, 2)
|
||||
new_date = add_months(initial_date, 3)
|
||||
assert new_date.year == 2020
|
||||
assert new_date.month == 5
|
||||
assert new_date.day == 2
|
||||
|
||||
# push to next year
|
||||
initial_date = datetime(2020, 11, 2)
|
||||
new_date = add_months(initial_date, 3)
|
||||
assert new_date.year == 2021
|
||||
assert new_date.month == 2
|
||||
assert new_date.day == 2
|
||||
|
||||
# last day of the month
|
||||
initial_date = datetime(2020, 1, 31)
|
||||
new_date = add_months(initial_date, 1)
|
||||
assert new_date.year == 2020
|
||||
assert new_date.month == 2
|
||||
assert new_date.day == 29
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_add_years():
|
||||
# add some months
|
||||
initial_date = datetime(2020, 2, 2)
|
||||
new_date = add_years(initial_date, 1)
|
||||
assert new_date.year == 2021
|
||||
assert new_date.month == 2
|
||||
assert new_date.day == 2
|
||||
|
||||
# test leap year
|
||||
initial_date = datetime(2020, 2, 29)
|
||||
new_date = add_years(initial_date, 1)
|
||||
assert new_date.year == 2021
|
||||
assert new_date.month == 2
|
||||
assert new_date.day == 28
|
||||
|
||||
@@ -3,7 +3,6 @@ from datetime import timedelta
|
||||
from multiprocessing import Event, Value
|
||||
from unittest import mock
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import IntegrityError
|
||||
@@ -128,7 +127,7 @@ def test_scheduler(broker, monkeypatch):
|
||||
assert schedule.repeats == 0
|
||||
assert schedule.last_run() is not None
|
||||
assert schedule.success() is True
|
||||
assert schedule.next_run < arrow.get(timezone.now()).shift(hours=+1)
|
||||
assert schedule.next_run < timezone.now() + timedelta(hours=1)
|
||||
task = fetch(schedule.task)
|
||||
assert task is not None
|
||||
assert task.success is True
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import datetime
|
||||
from datetime import date
|
||||
from django.utils.timezone import make_aware
|
||||
import calendar
|
||||
|
||||
# credits: https://stackoverflow.com/a/4131114
|
||||
# Made them aware of timezone
|
||||
def add_months(d, months):
|
||||
month = d.month - 1 + months
|
||||
year = d.year + month // 12
|
||||
month = month % 12 + 1
|
||||
day = min(d.day, calendar.monthrange(year,month)[1])
|
||||
return d.replace(year=year, month=month, day=day)
|
||||
|
||||
# credits: https://stackoverflow.com/a/15743908
|
||||
# Changed the last line to make it a little easier to read and changed it to move February 29 to 28 next year
|
||||
# Also made them aware of timezone
|
||||
def add_years(d, years):
|
||||
"""Return a date that's `years` years after the date (or datetime)
|
||||
object `d`. Return the same calendar date (month and day) in the
|
||||
destination year, if it exists, otherwise use the previous day
|
||||
(thus changing February 29 to February 28).
|
||||
|
||||
"""
|
||||
try:
|
||||
return d.replace(year = d.year + years)
|
||||
except ValueError:
|
||||
new_date = d + (date(d.year + years, 3, 1) - date(d.year, 3, 1))
|
||||
return d.replace(year=new_date.year, month=new_date.month, day=new_date.day)
|
||||
|
||||
+1
-17
@@ -2,7 +2,7 @@ Brokers
|
||||
=======
|
||||
|
||||
The broker sits between your Django instances and your Django Q2 cluster instances; accepting, saving and delivering task packages.
|
||||
Currently we support a variety of brokers from the default Redis, bleeding edge Disque to the convenient ORM and fast MongoDB.
|
||||
Currently we support a variety of brokers.
|
||||
|
||||
The default Redis broker does not support message receipts.
|
||||
This means that in case of a catastrophic failure of the cluster server or worker timeouts, tasks that were being executed get lost.
|
||||
@@ -38,22 +38,6 @@ The default broker for Django Q2 clusters.
|
||||
* Can use existing :ref:`django_redis` connections.
|
||||
* Configure with :ref:`redis_configuration`-py compatible configuration
|
||||
|
||||
Disque
|
||||
------
|
||||
Unlike Redis, Disque supports message receipts which make delivery to the cluster workers guaranteed.
|
||||
In our tests it is as fast or faster than the Redis broker.
|
||||
You can control the amount of time Disque should wait for completion of a task by configuring the :ref:`retry` setting.
|
||||
Bulk task retrieval is supported via the :ref:`bulk` option.
|
||||
|
||||
* Delivery receipts
|
||||
* Atomic
|
||||
* Needs Django's `Cache framework <https://docs.djangoproject.com/en/4.0/topics/cache/#setting-up-the-cache>`__ configured for monitoring
|
||||
* Compatible with `Tynd <https://disque.tynd.co/>`__ Disque addon on `Heroku <https://heroku.com>`__
|
||||
* Still considered Alpha software
|
||||
* Supports bulk dequeue
|
||||
* Requires `Redis-py <https://github.com/andymccurdy/redis-py>`__ client library: ``pip install redis``
|
||||
* See the :ref:`disque_configuration` configuration section for more info.
|
||||
|
||||
IronMQ
|
||||
------
|
||||
This HTTP based queue service is both available directly via `Iron.io <http://www.iron.io/mq/>`__ and as an add-on on Heroku.
|
||||
|
||||
@@ -235,43 +235,6 @@ of the cache connection you want to use instead of a direct Redis connection::
|
||||
.. tip::
|
||||
Django Q2 uses your ``SECRET_KEY`` to sign task packages and prevent task crossover. So make sure you have it set up in your Django settings.
|
||||
|
||||
.. _disque_configuration:
|
||||
|
||||
disque_nodes
|
||||
~~~~~~~~~~~~
|
||||
If you want to use Disque as your broker, set this to a list of available Disque nodes and each cluster will randomly try to connect to them::
|
||||
|
||||
# example disque connection
|
||||
Q_CLUSTER = {
|
||||
'name': 'DisqueBroker',
|
||||
'workers': 4,
|
||||
'timeout': 60,
|
||||
'retry': 60,
|
||||
'disque_nodes': ['127.0.0.1:7711', '127.0.0.1:7712']
|
||||
}
|
||||
|
||||
|
||||
Django Q2 is also compatible with the `Tynd Disque <https://disque.tynd.co/>`__ addon on `Heroku <https://heroku.com>`__::
|
||||
|
||||
# example Tynd Disque connection
|
||||
import os
|
||||
|
||||
Q_CLUSTER = {
|
||||
'name': 'TyndBroker',
|
||||
'workers': 8,
|
||||
'timeout': 30,
|
||||
'retry': 60,
|
||||
'bulk': 10,
|
||||
'disque_nodes': os.environ['TYND_DISQUE_NODES'].split(','),
|
||||
'disque_auth': os.environ['TYND_DISQUE_AUTH']
|
||||
}
|
||||
|
||||
|
||||
disque_auth
|
||||
~~~~~~~~~~~
|
||||
|
||||
Optional Disque password for servers that require authentication.
|
||||
|
||||
.. _ironmq_configuration:
|
||||
|
||||
iron_mq
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ Features
|
||||
- Django Admin integration
|
||||
- PaaS compatible with multiple instances
|
||||
- Multi cluster monitor
|
||||
- Redis, Disque, IronMQ, SQS, MongoDB or ORM
|
||||
- Redis, IronMQ, SQS, MongoDB or ORM
|
||||
- Rollbar and Sentry support
|
||||
|
||||
|
||||
|
||||
+3
-9
@@ -32,16 +32,12 @@ Django Q2 is tested for Python 3.7, 3.8, 3.9 and 3.10
|
||||
- `Django <https://www.djangoproject.com>`__
|
||||
|
||||
Django Q2 aims to use as much of Django's standard offerings as possible.
|
||||
The code is tested against Django versions `2.2.x` and `3.2.x`.
|
||||
The code is tested against Django versions `3.2.x` and `4.1.x`.
|
||||
|
||||
- `Django-picklefield <https://github.com/gintas/django-picklefield>`__
|
||||
|
||||
Used to store args, kwargs and result objects in the database.
|
||||
|
||||
- `Arrow <https://github.com/crsmithdev/arrow>`__
|
||||
|
||||
The scheduler uses Chris Smith's wonderful project to determine correct dates in the future.
|
||||
|
||||
- `Blessed <https://github.com/jquast/blessed>`__
|
||||
|
||||
This feature-filled fork of Erik Rose's blessings project provides the terminal layout of the monitor.
|
||||
@@ -49,7 +45,7 @@ Django Q2 is tested for Python 3.7, 3.8, 3.9 and 3.10
|
||||
|
||||
Optional
|
||||
~~~~~~~~
|
||||
- `Redis-py <https://github.com/andymccurdy/redis-py>`__ client by Andy McCurdy is used to interface with both the Redis and Disque brokers::
|
||||
- `Redis-py <https://github.com/andymccurdy/redis-py>`__ client by Andy McCurdy is used to interface with both the Redis::
|
||||
|
||||
$ pip install redis
|
||||
|
||||
@@ -77,8 +73,6 @@ Optional
|
||||
|
||||
- `Redis <http://redis.io/>`__ server is the default broker for Django Q2. It provides the best performance and does not require Django's cache framework for monitoring.
|
||||
|
||||
- `Disque <https://github.com/antirez/disque>`__ server is based on Redis by the same author, but focuses on reliable queues. Currently in Alpha, but highly recommended. You can either build it from source or use it on Heroku through the `Tynd <https://disque.tynd.co/>`__ beta.
|
||||
|
||||
- `MongoDB <https://www.mongodb.org/>`__ is a highly scalable NoSQL database which makes for a very fast and reliably persistent at-least-once message broker. Usually available on most PaaS providers.
|
||||
|
||||
- `Pyrollbar <https://github.com/rollbar/pyrollbar>`__ is an error notifier for `Rollbar <https://rollbar.com/>`__ which lets you manage your worker errors in one place. Needs a `Rollbar <https://rollbar.com/>`__ account and access key::
|
||||
@@ -142,7 +136,7 @@ You can reference the `requirements <https://github.com/GDay/django-q2/blob/mast
|
||||
Django
|
||||
~~~~~~
|
||||
We strive to be compatible with last two major version of Django.
|
||||
At the moment this means we support the 2.2.x and 3.2.x releases.
|
||||
At the moment this means we support the 3.2.x and 4.1.x releases.
|
||||
|
||||
Since we are now no longer supporting Python 2, we can also not support older versions of Django that do not support Python >= 3.6
|
||||
For this you can always use older releases, but they are no longer maintained.
|
||||
|
||||
+2
-2
@@ -37,14 +37,14 @@ You can manage them through the :ref:`admin_page` or directly from your code wit
|
||||
|
||||
# Run a schedule every 5 minutes, starting at 6 today
|
||||
# for 2 hours
|
||||
import arrow
|
||||
from datetime import datetime
|
||||
|
||||
schedule('math.hypot',
|
||||
3, 4,
|
||||
schedule_type=Schedule.MINUTES,
|
||||
minutes=5,
|
||||
repeats=24,
|
||||
next_run=arrow.utcnow().replace(hour=18, minute=0))
|
||||
next_run=datetime.utcnow().replace(hour=18, minute=0))
|
||||
|
||||
# Use a cron expression
|
||||
schedule('math.hypot',
|
||||
|
||||
Generated
+539
-482
File diff suppressed because it is too large
Load Diff
+13
-14
@@ -44,20 +44,19 @@ include = ['CHANGELOG.md']
|
||||
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.7.13, <4"
|
||||
django = ">=2.2"
|
||||
blessed = "^1.17.6"
|
||||
arrow = "^1.1.0"
|
||||
django-picklefield = "^3.0.1"
|
||||
python = ">=3.8.14, <4"
|
||||
django = ">=3.2"
|
||||
blessed = "^1.19.1"
|
||||
django-picklefield = "^3.1"
|
||||
|
||||
hiredis = { version = "^1.0.1", optional = true }
|
||||
redis = { version = "^3.5.3", optional = true }
|
||||
psutil = { version = "^5.7.0", optional = true }
|
||||
django-redis = { version = "^4.12.1", optional = true }
|
||||
hiredis = { version = "^2.0.0", optional = true }
|
||||
redis = { version = "^4.3.4", optional = true }
|
||||
psutil = { version = "^5.9.2", optional = true }
|
||||
django-redis = { version = "^5.2.0", optional = true }
|
||||
iron-mq = { version = "^0.9", optional = true }
|
||||
boto3 = { version = "^1.14.12", optional = true }
|
||||
pymongo = { version = "^3.10.1", optional = true }
|
||||
croniter = { version = "^0.3.34", optional = true }
|
||||
boto3 = { version = "^1.24.92", optional = true }
|
||||
pymongo = { version = "^4.2.0", optional = true }
|
||||
croniter = { version = "^1.3.7", optional = true }
|
||||
django-q-rollbar = {version = ">=0.1", optional = true}
|
||||
django-q-sentry = {version = ">=0.1", optional = true}
|
||||
|
||||
@@ -66,8 +65,8 @@ django-q-sentry = {version = ">=0.1", optional = true}
|
||||
pytest = "^7.1.3"
|
||||
pytest-django = "^4.5.2"
|
||||
Sphinx = "^4.0.2"
|
||||
pytest-cov = "^3.0.0"
|
||||
black = "^22.8.0"
|
||||
pytest-cov = "^4.0.0"
|
||||
black = "^22.10.0"
|
||||
isort = {extras = ["requirements_deprecated_finder"], version = "^5.10.1"}
|
||||
|
||||
[tool.poetry.extras]
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
version: '2.2'
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
disque:
|
||||
image: efrecon/disque:1.0-rc1
|
||||
ports:
|
||||
- '7711:7711/tcp'
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
ports:
|
||||
- '6379:6379/tcp'
|
||||
expose:
|
||||
- '6379/tcp'
|
||||
networks:
|
||||
- main
|
||||
|
||||
mongo:
|
||||
image: mongo:4
|
||||
ports:
|
||||
- '27017:27017/tcp'
|
||||
image: mongo:6
|
||||
expose:
|
||||
- '27017/tcp'
|
||||
networks:
|
||||
- main
|
||||
|
||||
django-q2:
|
||||
build:
|
||||
dockerfile: ./Dockerfile.dev
|
||||
context: .
|
||||
volumes:
|
||||
- .:/app
|
||||
depends_on:
|
||||
- redis
|
||||
- mongo
|
||||
networks:
|
||||
- main
|
||||
|
||||
networks:
|
||||
main:
|
||||
|
||||
Reference in New Issue
Block a user