Skip to content

Commit c5111f3

Browse files
authored
Merge pull request #16 from cronitorio/celerybeat-schedule-error
Move celerybeat-schedule to new tempfile to avoid startup errors
2 parents 84cff34 + adcfc9f commit c5111f3

File tree

3 files changed

+45
-14
lines changed

3 files changed

+45
-14
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,13 @@ creating monitors for them and sending pings when tasks run, succeed, or fail. Y
2626

2727
Requires Celery 4.0 or higher. Celery auto-discover utilizes the Celery [message protocol version 2](https://docs.celeryproject.org/en/stable/internals/protocol.html#version-2).
2828

29-
> Note: tasks on [solar schedules](https://docs.celeryproject.org/en/stable/userguide/periodic-tasks.html#solar-schedules) are not supported and will be ignored.
29+
<details>
30+
<summary>Some important notes on support</summary>
31+
32+
* Tasks on [solar schedules](https://docs.celeryproject.org/en/stable/userguide/periodic-tasks.html#solar-schedules) are not supported and will be ignored.
33+
* [`django-celery-beat`](https://docs.celeryproject.org/en/stable/userguide/periodic-tasks.html#using-custom-scheduler-classes) is not yet supported, but is in the works.
34+
* If you use the default `PersistentScheduler`, the celerybeat integration overrides the celerybeat local task run database (as referenced [here](https://docs.celeryproject.org/en/stable/userguide/periodic-tasks.html#starting-the-scheduler) in the docs), named `celerybeat-schedule` by default. If you currently specify a custom location for this database, this integration will override it. **Very** few people require setting custom locations for this database. If you fall into this group and want to use `cronitor-python`'s celerybeat integration, please reach out to Cronitor support.
35+
</details>
3036

3137
```python
3238
import cronitor.celery

cronitor/celery.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import logging
55
from cronitor import State, Monitor
66
import cronitor
7+
import functools
8+
import shutil
9+
import tempfile
710
import sys
811

912
logger = logging.getLogger(__name__)
@@ -51,10 +54,17 @@ def initialize(app, celerybeat_only=False, api_key=None): # type: (celery.Celer
5154
global ping_monitor_on_retry
5255

5356
def celerybeat_startup(sender, **kwargs): # type: (celery.beat.Service, Dict) -> None
54-
scheduler = sender.get_scheduler() # type: celery.beat.Scheduler
55-
schedules = scheduler.get_schedule()
57+
# To avoid recursion, since restarting celerybeat will result in this
58+
# signal being called again, we disconnect the signal.
59+
beat_init.disconnect(celerybeat_startup, dispatch_uid=1)
60+
61+
# Must use the cached_property from scheduler so as not to re-open the shelve database
62+
scheduler = sender.scheduler # type: celery.beat.Scheduler
63+
# Also need to use the property here, including for django-celery-beat
64+
schedules = scheduler.schedule
5665
monitors = [] # type: List[Dict[str, str]]
5766

67+
add_periodic_task_deferred = []
5868
for name in schedules:
5969
if name.startswith('celery.'):
6070
continue
@@ -95,21 +105,37 @@ def celerybeat_startup(sender, **kwargs): # type: (celery.beat.Service, Dict) -
95105
'x-cronitor-celerybeat-name': name,
96106
})
97107

98-
app.add_periodic_task(entry.schedule,
108+
add_periodic_task_deferred.append(
109+
functools.partial(app.add_periodic_task,
110+
entry.schedule,
99111
# Setting headers in the signature
100-
# works better then in periodic task options
112+
# works better than in periodic task options
101113
app.tasks.get(entry.task).s().set(headers=headers),
102114
args=entry.args, kwargs=entry.kwargs,
103115
name=entry.name, **(entry.options or {}))
116+
)
117+
118+
if isinstance(sender.scheduler, celery.beat.PersistentScheduler):
119+
# The celerybeat-schedule file with shelve gets corrupted really easily, so we need
120+
# to set up a tempfile instead.
121+
new_schedule = tempfile.NamedTemporaryFile()
122+
with open(sender.schedule_filename, 'rb') as current_schedule:
123+
shutil.copyfileobj(current_schedule, new_schedule)
124+
# We need to stop and restart celerybeat to get the task updates in place.
125+
# This isn't ideal, but seems to work.
126+
127+
sender.stop()
128+
# Now, actually add all the periodic tasks to overwrite beat with the headers
129+
for task in add_periodic_task_deferred:
130+
task()
131+
# Then, restart celerybeat, on the new schedule file (copied from the old one)
132+
app.Beat(schedule=new_schedule.name).run()
104133

105-
# To avoid recursion, since restarting celerybeat will result in this
106-
# signal being called again, we disconnect the signal.
107-
beat_init.disconnect(celerybeat_startup, dispatch_uid=1)
134+
else:
135+
# For django-celery, etc., we don't need to stop and restart celerybeat
136+
for task in add_periodic_task_deferred:
137+
task()
108138

109-
# We need to stop and restart celerybeat to get the task updates in place.
110-
# This isn't ideal, but seems to work.
111-
sender.stop()
112-
app.Beat().run()
113139
logger.debug("[Cronitor] creating monitors: %s", [m['key'] for m in monitors])
114140
Monitor.put(monitors)
115141

@@ -175,4 +201,3 @@ def ping_monitor_on_retry(sender, # type: celery.Task
175201
return
176202

177203
monitor.ping(state=State.FAIL, series=sender.request.id, message=str(reason))
178-

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setup(
77
name='cronitor',
8-
version='4.4.3',
8+
version='4.4.4',
99
packages=find_packages(),
1010
url='https://github.com/cronitorio/cronitor-python',
1111
license='MIT License',

0 commit comments

Comments
 (0)