Skip to content

fix: Fix race conditions in schedule_task API#46

Merged
tibordp merged 11 commits into
masterfrom
fix-schedule-task-race-conditions
Oct 19, 2025
Merged

fix: Fix race conditions in schedule_task API#46
tibordp merged 11 commits into
masterfrom
fix-schedule-task-race-conditions

Conversation

@tibordp
Copy link
Copy Markdown
Owner

@tibordp tibordp commented Oct 19, 2025

Add force parameter to schedule_task/register_task to prevent race conditions and schedule starvation when updating dynamic task instances.

Changes:

  • Add force parameter (default False) to register_task across all backends
  • When force=False: fail if task is locked, preserve sooner schedule (MIN logic)
  • When force=True: override locks and use new schedule
  • Update task_spec while preserving execution timing (unless forced)

This prevents two issues:

  1. Race condition: Calling schedule_task while task is executing would clear locks, allowing duplicate execution
  2. Schedule starvation: Repeated schedule_task calls would push execution time into the future indefinitely

Add force parameter to schedule_task/register_task to prevent race conditions
and schedule starvation when updating dynamic task instances.

Changes:
- Add force parameter (default False) to register_task across all backends
- When force=False: fail if task is locked, preserve sooner schedule (MIN logic)
- When force=True: override locks and use new schedule
- Update task_spec while preserving execution timing (unless forced)

This prevents two issues:
1. Race condition: Calling schedule_task while task is executing would clear
   locks, allowing duplicate execution
2. Schedule starvation: Repeated schedule_task calls would push execution
   time into the future indefinitely

Add comprehensive integration tests covering all scenarios across all backends.
@codecov
Copy link
Copy Markdown

codecov Bot commented Oct 19, 2025

Codecov Report

❌ Patch coverage is 95.47325% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.98%. Comparing base (053bf10) to head (d49a42c).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/pyncette/dynamodb.py 86.20% 1 Missing and 3 partials ⚠️
src/pyncette/mysql.py 89.47% 1 Missing and 1 partial ⚠️
src/pyncette/postgres.py 88.23% 1 Missing and 1 partial ⚠️
src/pyncette/sqlite.py 89.47% 1 Missing and 1 partial ⚠️
tests/test_pyncette_integration.py 99.29% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #46      +/-   ##
==========================================
- Coverage   98.20%   97.98%   -0.23%     
==========================================
  Files          26       26              
  Lines        2905     3124     +219     
  Branches      159      180      +21     
==========================================
+ Hits         2853     3061     +208     
- Misses         29       34       +5     
- Partials       23       29       +6     
Flag Coverage Δ
integration 97.95% <95.47%> (-0.23%) ⬇️
py3.10 79.03% <72.42%> (-0.35%) ⬇️
py3.11 79.03% <72.42%> (-0.35%) ⬇️
py3.12 79.03% <72.42%> (-0.35%) ⬇️
py3.13 79.03% <72.42%> (-0.35%) ⬇️
py3.14 78.93% <72.42%> (-0.35%) ⬇️
py3.9 79.01% <72.42%> (-0.35%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Document the new force parameter and safe mode behavior for updating
dynamic tasks, including race condition prevention and schedule starvation
protection.
- Introduce TaskLockedException for clearer error handling when tasks are locked
- Update all backends to use TaskLockedException instead of generic PyncetteException
- Simplify documentation to match existing style and remove overstated language
- Mention MIN logic briefly as preventing postponement rather than emphasizing starvation
@tibordp tibordp requested a review from Copilot October 19, 2025 16:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Adds a force flag to scheduling APIs to eliminate race conditions and schedule starvation when updating dynamic task instances. Key changes:

  • Introduce force parameter to schedule_task and repository.register_task across all backends.
  • Implement lock-aware updates: when force=False, fail if locked and preserve the earlier execute_after; when force=True, override locks and use the new schedule.
  • Add integration tests and docs; update CI and dev setup.

Reviewed Changes

Copilot reviewed 19 out of 20 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
tests/utils/fakerepository.py Updates test fake repository interface to accept force.
tests/test_pyncette_integration.py Adds comprehensive tests for lock handling, force updates, and schedule preservation.
tests/conftest.py Isolates SQLite DB per pytest-xdist worker to avoid cross-test interference.
src/pyncette/sqlite.py Implements lock-aware register_task with MIN schedule logic and optional force override.
src/pyncette/repository.py Updates abstract interface and docs for register_task(force).
src/pyncette/redis/manage.lua Adds REGISTER handling for force and locked cases; preserves earlier schedule when not forced.
src/pyncette/redis/init.py Passes force and utc_now to Lua script; raises TaskLockedException on lock.
src/pyncette/pyncette.py Exposes force in schedule_task and forwards to repository.
src/pyncette/prometheus.py Plumbs force through the metrics wrapper.
src/pyncette/postgres.py Implements lock-aware updates; removes upsert in favor of checked update/insert.
src/pyncette/mysql.py Implements lock-aware updates; splits update vs. insert paths.
src/pyncette/errors.py Introduces TaskLockedException with informative messages.
src/pyncette/dynamodb.py Adds lock-aware register_task with CAS; extends _update_item options.
pyproject.toml Moves dev deps from extras to dependency-groups.
docs/usage.md Documents updating dynamic tasks, force behavior, and starvation prevention.
docs/contributing.md Updates dev instructions; adds parallel test execution note.
README.md Updates dev setup instructions.
.github/workflows/python-package.yml Adjusts uv sync commands.
.github/workflows/docs.yml Adjusts uv sync commands.

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread .github/workflows/python-package.yml
Comment thread .github/workflows/python-package.yml
Comment thread .github/workflows/python-package.yml
Comment thread .github/workflows/docs.yml
Comment thread README.md
Comment thread docs/contributing.md
Comment thread docs/contributing.md
Comment thread src/pyncette/pyncette.py
Comment thread src/pyncette/pyncette.py
Comment thread src/pyncette/repository.py Outdated
force: If False, fails if task is locked. If True, overwrites everything including locks.

Raises:
PyncetteException: If force=False and task is currently locked
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The raised exception is specifically TaskLockedException (a PyncetteException subclass). Update the docstring to name TaskLockedException for clarity and discoverability.

Suggested change
PyncetteException: If force=False and task is currently locked
TaskLockedException: If force=False and task is currently locked

Copilot uses AI. Check for mistakes.
Prevent race conditions by locking the row during the check-then-update
sequence in register_task. This ensures atomicity between checking the
lock state and performing the update.
Simplify register_task by using INSERT ON CONFLICT/ON DUPLICATE KEY UPDATE
for force=True, eliminating the need to SELECT first. This makes force mode
truly unconditional and clarifies the two different code paths.
Reorganize REGISTER logic to have clearer separation:
1. Create if not exists
2. Force mode: unconditional overwrite
3. Safe mode: check locks, compute MIN, preserve locks

This makes the three cases explicit and easier to understand.
Use READ COMMITTED instead of REPEATABLE READ (MySQL default) to eliminate
gap lock deadlocks when using SELECT FOR UPDATE on non-existent rows.

Benefits:
- Eliminates gap lock-related deadlocks from concurrent inserts
- Better concurrency and throughput for dynamic task registration
- Consistent with Postgres behavior (READ COMMITTED default)
- No phantom read prevention needed (each operation is independent)

Set via init_command on connection pool to avoid per-transaction overhead.
Document force parameter, race condition fixes, performance improvements,
and new TaskLockedException in unreleased section.
@tibordp tibordp merged commit a29c291 into master Oct 19, 2025
12 checks passed
@tibordp tibordp deleted the fix-schedule-task-race-conditions branch October 19, 2025 17:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants