Skip to content

Commit 08cd256

Browse files
Fix: Refactor Hardcover auto-fetch scheduling to use schedule_task() method
Conforms to PR #918 fix for race condition in scheduled tasks. Changes: - Moved inline scheduling code to dedicated _schedule_hardcover_auto_fetch() function - Changed from scheduler.schedule() to scheduler.schedule_task() method - Uses lambda pattern that returns TaskAutoHardcoverID instance - Sets worker_task.scheduled = True automatically via schedule_task() - Follows same pattern as _schedule_duplicate_scan() for consistency - Prevents race conditions and database session corruption - Ensures proper thread safety with BackgroundScheduler lock
1 parent 646544b commit 08cd256

1 file changed

Lines changed: 83 additions & 111 deletions

File tree

cps/schedule.py

Lines changed: 83 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -69,117 +69,7 @@ def register_scheduled_tasks(reconnect=True):
6969
timezone=timezone_info),
7070
name="end scheduled task")
7171

72-
# Register Hardcover auto-fetch task if enabled
73-
try:
74-
import sys as _sys
75-
if '/app/calibre-web-automated/scripts/' not in _sys.path:
76-
_sys.path.insert(1, '/app/calibre-web-automated/scripts/')
77-
from cwa_db import CWA_DB
78-
from os import getenv
79-
80-
cwa_db = CWA_DB()
81-
cwa_settings = cwa_db.get_cwa_settings()
82-
83-
# Check if enabled and token available
84-
enabled = bool(cwa_settings.get('hardcover_auto_fetch_enabled', False))
85-
token_available = bool(
86-
getattr(config, "config_hardcover_token", None) or
87-
getenv("HARDCOVER_TOKEN")
88-
)
89-
90-
if enabled and token_available:
91-
schedule_type = cwa_settings.get('hardcover_auto_fetch_schedule', 'weekly')
92-
schedule_day = cwa_settings.get('hardcover_auto_fetch_schedule_day', 'sunday')
93-
schedule_hour = int(cwa_settings.get('hardcover_auto_fetch_schedule_hour', 2))
94-
min_confidence = float(cwa_settings.get('hardcover_auto_fetch_min_confidence', 0.85))
95-
batch_size = int(cwa_settings.get('hardcover_auto_fetch_batch_size', 50))
96-
rate_limit = float(cwa_settings.get('hardcover_auto_fetch_rate_limit', 5.0))
97-
98-
def run_hardcover_fetch():
99-
"""Wrapper to enqueue Hardcover auto-fetch task"""
100-
task = TaskAutoHardcoverID(
101-
min_confidence=min_confidence,
102-
batch_size=batch_size,
103-
rate_limit_delay=rate_limit
104-
)
105-
WorkerThread.add('System', task, hidden=False)
106-
107-
# Map day names to APScheduler format
108-
day_map = {
109-
'monday': 'mon', 'tuesday': 'tue', 'wednesday': 'wed',
110-
'thursday': 'thu', 'friday': 'fri', 'saturday': 'sat', 'sunday': 'sun'
111-
}
112-
113-
if schedule_type == '15min':
114-
scheduler.schedule(
115-
func=run_hardcover_fetch,
116-
trigger=IntervalTrigger(minutes=15, timezone=timezone_info),
117-
name="hardcover_auto_fetch_15min"
118-
)
119-
elif schedule_type == '30min':
120-
scheduler.schedule(
121-
func=run_hardcover_fetch,
122-
trigger=IntervalTrigger(minutes=30, timezone=timezone_info),
123-
name="hardcover_auto_fetch_30min"
124-
)
125-
elif schedule_type == '1hour':
126-
scheduler.schedule(
127-
func=run_hardcover_fetch,
128-
trigger=IntervalTrigger(hours=1, timezone=timezone_info),
129-
name="hardcover_auto_fetch_1hour"
130-
)
131-
elif schedule_type == '2hours':
132-
scheduler.schedule(
133-
func=run_hardcover_fetch,
134-
trigger=IntervalTrigger(hours=2, timezone=timezone_info),
135-
name="hardcover_auto_fetch_2hours"
136-
)
137-
elif schedule_type == '4hours':
138-
scheduler.schedule(
139-
func=run_hardcover_fetch,
140-
trigger=IntervalTrigger(hours=4, timezone=timezone_info),
141-
name="hardcover_auto_fetch_4hours"
142-
)
143-
elif schedule_type == '6hours':
144-
scheduler.schedule(
145-
func=run_hardcover_fetch,
146-
trigger=IntervalTrigger(hours=6, timezone=timezone_info),
147-
name="hardcover_auto_fetch_6hours"
148-
)
149-
elif schedule_type == '12hours':
150-
scheduler.schedule(
151-
func=run_hardcover_fetch,
152-
trigger=IntervalTrigger(hours=12, timezone=timezone_info),
153-
name="hardcover_auto_fetch_12hours"
154-
)
155-
elif schedule_type == 'daily':
156-
scheduler.schedule(
157-
func=run_hardcover_fetch,
158-
trigger=CronTrigger(hour=schedule_hour, minute=0, timezone=timezone_info),
159-
name="hardcover_auto_fetch_daily"
160-
)
161-
elif schedule_type == 'weekly':
162-
day_abbr = day_map.get(schedule_day.lower(), 'sun')
163-
scheduler.schedule(
164-
func=run_hardcover_fetch,
165-
trigger=CronTrigger(day_of_week=day_abbr, hour=schedule_hour, minute=0, timezone=timezone_info),
166-
name="hardcover_auto_fetch_weekly"
167-
)
168-
elif schedule_type == 'monthly':
169-
# For monthly, schedule_day contains day of month (1-28)
170-
try:
171-
day_of_month = int(schedule_day) if str(schedule_day).isdigit() else 1
172-
day_of_month = max(1, min(28, day_of_month)) # Clamp to 1-28
173-
except (ValueError, TypeError):
174-
day_of_month = 1
175-
scheduler.schedule(
176-
func=run_hardcover_fetch,
177-
trigger=CronTrigger(day=day_of_month, hour=schedule_hour, minute=0, timezone=timezone_info),
178-
name="hardcover_auto_fetch_monthly"
179-
)
180-
except Exception as ex:
181-
# Don't break scheduling if Hardcover setup fails
182-
pass
72+
_schedule_hardcover_auto_fetch(scheduler, timezone_info)
18373

18474
# Kick-off tasks, if they should currently be running
18575
if should_task_be_running(start, duration):
@@ -342,3 +232,85 @@ def _schedule_duplicate_scan(scheduler, timezone_info):
342232
except Exception:
343233
# Scheduling is best-effort; never block startup
344234
pass
235+
236+
237+
def _schedule_hardcover_auto_fetch(scheduler, timezone_info):
238+
"""Schedule background Hardcover auto-fetch based on CWA settings."""
239+
try:
240+
import sys as _sys
241+
if '/app/calibre-web-automated/scripts/' not in _sys.path:
242+
_sys.path.insert(1, '/app/calibre-web-automated/scripts/')
243+
from cwa_db import CWA_DB
244+
from .tasks.auto_hardcover_id import TaskAutoHardcoverID
245+
from os import getenv
246+
247+
db = CWA_DB()
248+
cwa_settings = db.get_cwa_settings()
249+
250+
# Check if enabled and token available
251+
enabled = bool(cwa_settings.get('hardcover_auto_fetch_enabled', False))
252+
token_available = bool(
253+
getattr(config, "config_hardcover_token", None) or
254+
getenv("HARDCOVER_TOKEN")
255+
)
256+
257+
if not enabled or not token_available:
258+
return
259+
260+
schedule_type = cwa_settings.get('hardcover_auto_fetch_schedule', 'weekly')
261+
schedule_day = cwa_settings.get('hardcover_auto_fetch_schedule_day', 'sunday')
262+
schedule_hour = int(cwa_settings.get('hardcover_auto_fetch_schedule_hour', 2))
263+
min_confidence = float(cwa_settings.get('hardcover_auto_fetch_min_confidence', 0.85))
264+
batch_size = int(cwa_settings.get('hardcover_auto_fetch_batch_size', 50))
265+
rate_limit = float(cwa_settings.get('hardcover_auto_fetch_rate_limit', 5.0))
266+
267+
# Create lambda that returns task instance with configured settings
268+
task_lambda = lambda: TaskAutoHardcoverID(
269+
min_confidence=min_confidence,
270+
batch_size=batch_size,
271+
rate_limit_delay=rate_limit
272+
)
273+
274+
# Map day names to APScheduler format
275+
day_map = {
276+
'monday': 'mon', 'tuesday': 'tue', 'wednesday': 'wed',
277+
'thursday': 'thu', 'friday': 'fri', 'saturday': 'sat', 'sunday': 'sun'
278+
}
279+
280+
# Determine trigger based on schedule type
281+
trigger = None
282+
name = "hardcover auto-fetch"
283+
284+
if schedule_type == '15min':
285+
trigger = IntervalTrigger(minutes=15, timezone=timezone_info)
286+
elif schedule_type == '30min':
287+
trigger = IntervalTrigger(minutes=30, timezone=timezone_info)
288+
elif schedule_type == '1hour':
289+
trigger = IntervalTrigger(hours=1, timezone=timezone_info)
290+
elif schedule_type == '2hours':
291+
trigger = IntervalTrigger(hours=2, timezone=timezone_info)
292+
elif schedule_type == '4hours':
293+
trigger = IntervalTrigger(hours=4, timezone=timezone_info)
294+
elif schedule_type == '6hours':
295+
trigger = IntervalTrigger(hours=6, timezone=timezone_info)
296+
elif schedule_type == '12hours':
297+
trigger = IntervalTrigger(hours=12, timezone=timezone_info)
298+
elif schedule_type == 'daily':
299+
trigger = CronTrigger(hour=schedule_hour, minute=0, timezone=timezone_info)
300+
elif schedule_type == 'weekly':
301+
day_abbr = day_map.get(schedule_day.lower(), 'sun')
302+
trigger = CronTrigger(day_of_week=day_abbr, hour=schedule_hour, minute=0, timezone=timezone_info)
303+
elif schedule_type == 'monthly':
304+
# For monthly, schedule_day contains day of month (1-28)
305+
try:
306+
day_of_month = int(schedule_day) if str(schedule_day).isdigit() else 1
307+
day_of_month = max(1, min(28, day_of_month)) # Clamp to 1-28
308+
except (ValueError, TypeError):
309+
day_of_month = 1
310+
trigger = CronTrigger(day=day_of_month, hour=schedule_hour, minute=0, timezone=timezone_info)
311+
312+
if trigger:
313+
scheduler.schedule_task(task_lambda, user='System', trigger=trigger, name=name, hidden=False)
314+
except Exception:
315+
# Scheduling is best-effort; never block startup
316+
pass

0 commit comments

Comments
 (0)