Skip to content

Commit f9ff5ea

Browse files
authored
Merge branch 'master' into save_safety
2 parents c4bba22 + 3646dd5 commit f9ff5ea

File tree

11 files changed

+84
-39
lines changed

11 files changed

+84
-39
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Test workflow
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
environment:
7+
description: 'Environment to run the workflow in'
8+
required: true
9+
type: environment
10+
reason:
11+
description: 'Reason for running the workflow'
12+
required: true
13+
type: string
14+
environment-optional:
15+
description: 'Environment to run the workflow in'
16+
required: false
17+
type: environment
18+
choices-input-optional:
19+
type: choice
20+
required: false
21+
options:
22+
- option 1
23+
- option 2
24+
description: "Choice input"
25+
number-input-optional:
26+
type: number
27+
required: false
28+
description: "Number input"
29+
30+
jobs:
31+
ruff:
32+
runs-on: ubuntu-latest
33+
name: "ruff on code"
34+
permissions:
35+
contents: read
36+
steps:
37+
- uses: actions/checkout@v5
38+
with:
39+
persist-credentials: false
40+
- name: print all inputs
41+
run: |
42+
echo "environment: ${{ github.event.inputs.environment }}"
43+
echo "reason: ${{ github.event.inputs.reason }}"
44+
echo "environment-optional: ${{ github.event.inputs.environment-optional }}"
45+
echo "choices-input-optional: ${{ github.event.inputs.choices-input-optional }}"
46+
echo "number-input-optional: ${{ github.event.inputs.number-input-optional }}"

docs/changelog.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
# Changelog
22

3-
## v4.0.8 🌈
3+
## v4.0.7 🌈
44

55
### 🐛 Bug Fixes
66

77
- Fix: Scheduled jobs fail to execute: {'scheduled_time': ['Scheduled time must be in the future']} #297
88
- Fix Error with deserialize JobModel due to multi-processing #291
99

10-
## v4.0.7 🌈
11-
1210
### 🧰 Maintenance
1311

1412
- Improve GitHub workflow performance using ruff action @DhavalGojiya #294

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "django-tasks-scheduler"
7-
version = "4.0.8"
7+
version = "4.0.7"
88
description = "An async job scheduler for django using redis/valkey brokers"
99
authors = [{ name = "Daniel Moran", email = "[email protected]" }]
1010
requires-python = ">=3.10"

scheduler/helpers/queues/queue_logic.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,10 @@ def enqueue_job(
397397
) -> JobModel:
398398
"""Enqueues a job for delayed execution without checking dependencies.
399399
400-
If Queue is instantiated with is_async=False, job is executed immediately.
400+
If Queue is instantiated with is_async=False, the job is executed immediately.
401401
:param job_model: The job redis model
402402
:param pipeline: The Broker Pipeline
403-
:param at_front: Whether to enqueue the job at the front
403+
:param at_front: Should the job be enqueued at the front
404404
405405
:returns: The enqueued JobModel
406406
"""

scheduler/redis_models/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ def _deserialize(value: str, _type: Type) -> Any:
7676
class BaseModel:
7777
name: str
7878
_element_key_template: ClassVar[str] = ":element:{}"
79-
# fields that are not serializable using method above and should be dealt with in the subclass
80-
# e.g. args/kwargs for a job
79+
# fields that are not serializable using the method above and should be dealt with in the subclass
80+
# e.g., args/kwargs for a job
8181
_non_serializable_fields: ClassVar[Set[str]] = set()
8282

8383
@classmethod

scheduler/redis_models/job.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def prepare_for_execution(self, worker_name: str, registry: JobNamesRegistry, co
128128
self.last_heartbeat = utils.utcnow()
129129
self.started_at = self.last_heartbeat
130130
self.status = JobStatus.STARTED
131-
registry.add(connection, self.name, self.last_heartbeat.timestamp())
131+
registry.add(connection, self.name, self.last_heartbeat.timestamp() + self.timeout)
132132
self.save(connection=connection)
133133

134134
def after_execution(

scheduler/redis_models/registry/base_registry.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class DequeueTimeout(Exception):
1515
@dataclasses.dataclass(slots=True, kw_only=True)
1616
class ZSetModel(BaseModel):
1717
def cleanup(self, connection: ConnectionType, timestamp: Optional[float] = None) -> None:
18-
"""Remove expired jobs from registry."""
18+
"""Remove expired jobs from the registry."""
1919
score = timestamp or current_timestamp()
2020
connection.zremrangebyscore(self._key, 0, score)
2121

@@ -45,23 +45,23 @@ def __contains__(self, item: str) -> bool:
4545
return self.connection.zrank(self._key, item) is not None
4646

4747
def all(self, start: int = 0, end: int = -1) -> List[str]:
48-
"""Returns list of all job names.
48+
"""Returns a list of all job names.
4949
5050
:param start: Start score/timestamp, default to 0.
5151
:param end: End score/timestamp, default to -1 (i.e., no max score).
52-
:returns: Returns list of all job names with timestamp from start to end
52+
:returns: Returns a list of all job names with timestamp from start to end
5353
"""
5454
self.cleanup(self.connection)
5555
res = [as_str(job_name) for job_name in self.connection.zrange(self._key, start, end)]
5656
logger.debug(f"Getting jobs for registry {self._key}: {len(res)} found.")
5757
return res
5858

5959
def all_with_timestamps(self, start: int = 0, end: int = -1) -> List[Tuple[str, float]]:
60-
"""Returns list of all job names with their timestamps.
60+
"""Returns a list of all job names with their timestamps.
6161
6262
:param start: Start score/timestamp, default to 0.
6363
:param end: End score/timestamp, default to -1 (i.e., no max score).
64-
:returns: Returns list of all job names with timestamp from start to end
64+
:returns: Returns a list of all job names with timestamp from start to end
6565
"""
6666
self.cleanup(self.connection)
6767
res = self.connection.zrange(self._key, start, end, withscores=True)

scheduler/redis_models/worker.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,13 @@ def set_current_job_working_time(self, job_execution_time: float, connection: Co
8484
self.set_field("current_job_working_time", job_execution_time, connection=connection)
8585

8686
def heartbeat(self, connection: ConnectionType, timeout: Optional[int] = None) -> None:
87-
timeout = timeout or DEFAULT_WORKER_TTL + 60
88-
connection.expire(self._key, timeout)
89-
now = utcnow()
90-
self.set_field("last_heartbeat", now, connection=connection)
91-
logger.debug(f"Next heartbeat for worker {self._key} should arrive in {timeout} seconds.")
87+
with connection.pipeline() as pipeline:
88+
timeout = timeout or DEFAULT_WORKER_TTL + 60
89+
pipeline.expire(self._key, timeout)
90+
now = utcnow()
91+
self.set_field("last_heartbeat", now, connection=pipeline)
92+
pipeline.execute()
93+
logger.debug(f"Next heartbeat for worker {self._key} should arrive in {timeout} seconds.")
9294

9395
@classmethod
9496
def cleanup(cls, connection: ConnectionType, queue_name: Optional[str] = None):

scheduler/tests/test_worker/test_worker_commands.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
import json
22
from threading import Thread
33
from time import sleep
4+
from unittest import mock
45

56
from scheduler.helpers.queues import get_queue
67
from scheduler.tests.jobs import test_job, two_seconds_job
78
from ..test_views.base import BaseTestCase
9+
from ...helpers.callback import Callback
810
from ...redis_models import JobModel, JobStatus, WorkerModel
911
from ...worker import create_worker
1012
from ...worker.commands import send_command, StopJobCommand
1113
from ...worker.commands.suspend_worker import SuspendWorkCommand
1214

1315

16+
def _callback_func():
17+
pass
18+
19+
20+
def callback_func():
21+
pass
22+
23+
1424
class WorkerCommandsTest(BaseTestCase):
1525
def test_stop_worker_command__green(self):
1626
# Arrange
@@ -45,11 +55,12 @@ def test_stop_worker_command__bad_worker_name(self):
4555
job = JobModel.get(job.name, connection=queue.connection)
4656
self.assertFalse(job.is_queued)
4757

48-
def test_stop_job_command__success(self):
58+
@mock.patch("scheduler.redis_models.job.JobModel.call_stopped_callback")
59+
def test_stop_job_command__success(self, mock_stopped_callback):
4960
# Arrange
5061
worker_name = "test"
5162
queue = get_queue("default")
52-
job = queue.create_and_enqueue_job(two_seconds_job)
63+
job = queue.create_and_enqueue_job(two_seconds_job, on_stopped=Callback(callback_func))
5364
self.assertTrue(job.is_queued)
5465
worker = create_worker("default", name=worker_name, burst=True, with_scheduler=False)
5566
worker.bootstrap()
@@ -70,3 +81,4 @@ def test_stop_job_command__success(self):
7081
self.assertIsNone(worker.current_job_name)
7182
self.assertEqual(job.status, JobStatus.STOPPED)
7283
t.join()
84+
mock_stopped_callback.assert_called()

scheduler/worker/worker.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636
from scheduler.helpers.queues import Queue, perform_job
3737
from scheduler.helpers.timeouts import JobExecutionMonitorTimeoutException, JobTimeoutException
38-
from scheduler.helpers.utils import utcnow, current_timestamp
38+
from scheduler.helpers.utils import utcnow
3939

4040
try:
4141
from setproctitle import setproctitle as setprocname
@@ -625,7 +625,7 @@ def monitor_job_execution_process(self, job: JobModel, queue: Queue) -> None:
625625
self.wait_for_job_execution_process()
626626
break
627627

628-
self.maintain_heartbeats(job, queue)
628+
self._model.heartbeat(self.connection, self.job_monitoring_interval + 60)
629629

630630
except OSError as e:
631631
# In case we encountered an OSError due to EINTR (which is
@@ -685,17 +685,6 @@ def execute_job(self, job: JobModel, queue: Queue) -> None:
685685
self.perform_job(job, queue)
686686
self._model.set_field("state", WorkerStatus.IDLE, connection=self.connection)
687687

688-
def maintain_heartbeats(self, job: JobModel, queue: Queue) -> None:
689-
"""Updates worker and job's last heartbeat field."""
690-
with self.connection.pipeline() as pipeline:
691-
self._model.heartbeat(pipeline, self.job_monitoring_interval + 60)
692-
ttl = self.get_heartbeat_ttl(job)
693-
694-
queue.active_job_registry.add(pipeline, self.name, current_timestamp() + ttl, update_existing_only=False)
695-
results = pipeline.execute()
696-
if results[2] == 1:
697-
job.delete(self.connection)
698-
699688
def execute_in_separate_process(self, job: JobModel, queue: Queue) -> None:
700689
"""This is the entry point of the newly spawned job execution process.
701690
After fork()'ing, assure we are generating random sequences that are different from the worker.
@@ -785,10 +774,8 @@ def perform_job(self, job: JobModel, queue: Queue) -> bool:
785774
logger.debug(f"[Worker {self.name}/{self._pid}]: Performing {job.name} code.")
786775

787776
try:
788-
with self.connection.pipeline() as pipeline:
789-
self.worker_before_execution(job, connection=pipeline)
790-
job.prepare_for_execution(self.name, queue.active_job_registry, connection=pipeline)
791-
pipeline.execute()
777+
self.worker_before_execution(job, connection=queue.connection)
778+
job.prepare_for_execution(self.name, queue.active_job_registry, connection=queue.connection)
792779
timeout = job.timeout or SCHEDULER_CONFIG.DEFAULT_JOB_TIMEOUT
793780
with SCHEDULER_CONFIG.DEATH_PENALTY_CLASS(timeout, JobTimeoutException, job_name=job.name):
794781
logger.debug(f"[Worker {self.name}/{self._pid}]: Performing job `{job.name}`...")

0 commit comments

Comments
 (0)