Skip to content

feat: harden contest sync lease with safe release, overlap guard, tests & docs - #284

Merged
kunalverma2512 merged 2 commits into
kunalverma2512:mainfrom
ida-jemi:feat/contest-sync-lease-hardening
Jul 24, 2026
Merged

feat: harden contest sync lease with safe release, overlap guard, tests & docs#284
kunalverma2512 merged 2 commits into
kunalverma2512:mainfrom
ida-jemi:feat/contest-sync-lease-hardening

Conversation

@ida-jemi

@ida-jemi ida-jemi commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

📌 Pull Request Summary

🔗 Related Issue

Closes #277


📝 Description

Changes Made

  • Added ownerId to the SyncLock model so a lease is tagged with exactly which instance/run acquired it, allowing safe, scoped release that can never delete another instance's active lease.
  • Updated server/jobs/contestSync.js to release the lease immediately after each run completes (success or failure) instead of relying solely on TTL expiry, so the next legitimate run doesn't wait unnecessarily.
  • Added an in-process isRunning guard so a single instance cannot start an overlapping sync if a previous run is still in flight when the next scheduled tick fires.
  • Improved logging to include a run ID and owner ID per attempt, clearly distinguishing acquired / skipped / succeeded / failed outcomes for easier debugging in production.
  • Added automated tests (server/jobs/contestSync.test.js, using Node's built-in node:test runner) covering concurrent lock acquisition, DB-unreachable degradation, lease handoff after expiry, and owner-scoped release.
  • Documented the chosen distributed-lease strategy and its deployment implications in server/jobs/README.md.
  • Wired up npm test (node --test) since the repo had no test runner configured yet.

Motivation

This is a follow-up to #276 covering the full High-severity finding from the review of #269: an in-process cron scheduler has no leader-election or distributed-lock mechanism, so every backend replica independently runs its own schedule. #276 introduced a basic TTL-based lock, but this issue requires the harder production-safety guarantees called out in its acceptance criteria, safe lock release scoped to the correct owner, protection against overlapping runs on the same instance, recovery from a crashed lock holder, and test/documentation coverage, none of which #276 addressed on its own. This PR closes those gaps so the contest sync job is safe to run across any number of replicas without multiplying external API calls, DB writes, or log noise.


🚀 Type of Change

Select all that apply:

  • Bug Fix
  • New Feature
  • Enhancement
  • Documentation Update
  • Refactoring
  • Performance Improvement
  • DevOps / Tooling
  • Other

🧪 Testing

Verification

  • Tested Locally
  • Existing Tests Passed
  • New Tests Added
  • No Testing Required

Test Details

  • Automated: Added 6 unit tests via node --test, covering: successful lock acquisition, concurrent acquirers racing for the same lease (only one wins via duplicate-key handling), graceful false return (no throw) when the DB is unreachable, reacquisition after lease handoff, owner-scoped releaseLock deletion, and releaseLock not throwing on DB failure. All 6 pass.
  • Manual - true concurrency: Temporarily added an artificial delay inside the sync to widen the race window, then started two node server.js instances against the same MongoDB within that window. Instance 1 logged Acquired lease ... starting syncsynced 24 contest(s); Instance 2 logged only Skipped ... another instance holds the lease. with no acquire/sync lines, confirming exclusivity under real concurrency. Delay was removed before committing.
  • Manual - prompt release: Ran two instances sequentially; the second instance acquired a new lease immediately after the first released (not after waiting out the 10-minute TTL), confirming release-on-completion works correctly.
  • Manual - lock document: Verified in MongoDB Atlas that the synclocks collection holds exactly one document for jobName: "contestSync", and that it is removed immediately after a successful run rather than persisting for the full TTL window.

📸 Screenshots / Demo (If Applicable)

Terminal 1 & 2:

image

npm test:

image

✅ Checklist

  • I have read and followed the contribution guidelines.
  • I have self-reviewed my changes.
  • My changes are limited to the scope of this issue.
  • Documentation has been updated where necessary.
  • No unnecessary files or unrelated changes have been included.
  • The related issue has been linked correctly.
  • All applicable testing and validation steps have been completed.

📚 Additional Notes

This PR is based on top of the fix/contest-sync-distributed-lock branch from #276 rather than main, since it extends that work directly, please merge #276 first (or review this PR's diff against that branch) to avoid seeing unrelated changes. The chosen approach is a MongoDB-backed lease (Option 2 from the issue) rather than a dedicated worker process or queue, since it requires no new infrastructure; the tradeoffs and a migration path to a dedicated worker are documented in server/jobs/README.md for future reference.

Summary by CodeRabbit

  • Documentation

    • Added deployment and troubleshooting guidance for reliable contest synchronization across multiple server instances.
    • Included steps for manually verifying lock and failover behavior.
  • Tests

    • Added coverage for lock acquisition, expiration, concurrent execution, release behavior, and database failures.
    • Updated the test command to run the automated test suite.

…ts & docs

- SyncLock now tracks ownerId so releases are scoped to the acquiring
  instance and can never delete another instance's active lease.
- Lease is released immediately after each run instead of relying
  solely on TTL expiry, reducing unnecessary wait time between ticks.
- Added an in-process isRunning guard so a slow sync cannot overlap
  with the next scheduled tick on the same instance.
- Logs now include a run ID and owner ID, and distinguish skipped vs
  failed vs successful runs.
- Added automated tests (node:test) covering concurrent acquisition,
  DB failure, lease handoff, and scoped release.
- Documented the chosen strategy in server/jobs/README.md.

Closes kunalverma2512#277
@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

@ida-jemi is attempting to deploy a commit to the Kunal Verma's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

Copy link
Copy Markdown

🎉 Welcome to CodeLens — Thank You for Your Contribution!

Hey @ida-jemi! 👋

We are genuinely excited to have you here. Every single PR — big or small — makes CodeLens better, and yours is no exception. Take a moment to review the checklist below to help us merge your work quickly and smoothly.

✅ Before Requesting a Review

  • Keep code clean, readable, and consistent with the existing codebase
  • Avoid unrelated or unnecessary file changes
  • Make sure the UI is fully responsive across all device sizes
  • Attach screenshots or a short screen recording for any UI changes
  • Resolve all merge conflicts before marking the PR as ready
  • Do not submit AI-generated, copy-pasted, or low-effort implementations

💬 Join Our Community Channel — This is Mandatory

Being part of our communication channel is compulsory for all contributors, not optional.

📡 Join the CodeLens Matrix Channel

Why join? This is where all important announcements, PR review updates, contribution discussions, and maintainer decisions happen in real time. Contributors who are not in the channel regularly miss critical context and updates, which often leads to duplicated or misaligned work. Staying connected here is what keeps the community strong and your contributions impactful.


We are rooting for you! If you have any questions, drop them in the channel or comment right here on this PR. Let's build something great together. 🚀✨

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ida-jemi, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 44 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 226381a1-a5e6-4401-861c-c7d097a79472

📥 Commits

Reviewing files that changed from the base of the PR and between 1573ec2 and 6ed425b.

📒 Files selected for processing (3)
  • server/jobs/README.md
  • server/jobs/contestSync.js
  • server/jobs/contestSync.test.js
📝 Walkthrough

Walkthrough

Adds Node tests for contestSync distributed locking, documents lease behavior and manual validation steps, and changes the server test script to run Node’s built-in test runner.

Changes

Contest sync lock validation

Layer / File(s) Summary
Lock behavior documentation and validation
server/jobs/contestSync.test.js, server/jobs/README.md, server/package.json
Tests lock acquisition, duplicate-key races, database failures, lease expiry, and scoped release; documents TTL takeover and multi-instance coordination; enables execution through node --test.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

  • #282 — Covers documentation and tests supporting the contest sync distributed lock.
  • #281 — Addresses lease behavior while syncCodeforcesContests() is running.

Possibly related PRs

Suggested labels: backend, type:bug, GSSoC26, gssoc:approved, mentor:kunalverma2512

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: hardening contest sync leasing with safe release, overlap protection, tests, and docs.
Description check ✅ Passed The description matches the template well, covering issue, changes, motivation, type, testing, screenshots, checklist, and notes.
Linked Issues check ✅ Passed The changes satisfy #277 by adding atomic lease acquisition, safe owner-scoped release, overlap prevention, recovery, logs, tests, and docs.
Out of Scope Changes check ✅ Passed No clear out-of-scope changes appear; the added test runner and documentation support the contest sync locking work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/jobs/contestSync.test.js`:
- Around line 51-60: Update the “a new owner can acquire once the previous lease
has expired” test around acquireLock to make the mocked
SyncLock.findOneAndUpdate inspect its filter argument and assert that
lockedUntil uses a $lt predicate with a Date value. Keep the existing successful
acquisition assertion, ensuring the test fails if acquireLock omits the expiry
condition.
- Line 6: Extend the tests around startContestSyncJob to cover successful
synchronization, failed synchronization that still releases the lock, and
skipped execution when the lock is unavailable. Extract or inject the runSync
dependency so each scheduler path can be exercised deterministically, while
retaining the existing acquireLock/releaseLock coverage.

In `@server/jobs/README.md`:
- Around line 15-18: The jobs documentation must not claim safe recovery from
overlap based only on LOCK_TTL_MS and the per-instance isRunning flag. Update
the lease behavior and recovery description to document the implemented
renewal/fencing mechanism, or state and enforce a TTL longer than the maximum
possible run duration before asserting that expired leases can be safely taken
over.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 015c448f-42b3-41c1-b5ca-b978928a1dff

📥 Commits

Reviewing files that changed from the base of the PR and between 4519230 and 1573ec2.

📒 Files selected for processing (3)
  • server/jobs/README.md
  • server/jobs/contestSync.test.js
  • server/package.json

Comment thread server/jobs/contestSync.test.js Outdated
Comment thread server/jobs/contestSync.test.js Outdated
Comment thread server/jobs/README.md Outdated
…s, cover all scheduler outcomes, fix expiry-predicate test

- Extracted runSync into createRunSync() with injectable acquire/
  release/renew/syncFn dependencies, enabling deterministic unit
  tests of the actual scheduling logic (not a reimplementation of it)
  for all four outcomes: synced, failed-with-release, skipped-locked,
  skipped-running.
- Added renewLock(): while a sync is active, the lease is renewed at
  half the TTL, scoped to ownerId. A run that legitimately exceeds
  LOCK_TTL_MS no longer loses its lease to another instance mid-run;
  only a genuinely crashed/hung holder (which stops renewing) lets
  the TTL lapse.
- Fixed the lease-expiry test, which previously always returned
  success regardless of the lockedUntil filter — it now captures and
  asserts the actual query predicate.
- Updated README to document the renewal behavior.

Addresses CodeRabbit review feedback on this PR
@ida-jemi

Copy link
Copy Markdown
Contributor Author

Hi @kunalverma2512,

Pushed a follow-up commit addressing all three review comments:

  • Extracted runSync into createRunSync() with injectable acquire/release/renew/syncFn dependencies, and added tests covering all four scheduler outcomes (synced, failed-with-release, skipped-locked, skipped-running), not just the underlying acquireLock/releaseLock functions in isolation.
  • Fixed the lease-expiry test to actually assert the lockedUntil: { $lt: <Date> } predicate in the query filter, instead of trivially passing regardless of it.
  • Added lease renewal (renewLock), while a sync is active, the lease is renewed at half the TTL, scoped to ownerId. A run that legitimately exceeds LOCK_TTL_MS no longer loses its lease to another instance mid-run; only a genuinely crashed/hung holder (which stops renewing) lets the TTL lapse. Updated README.md to document this.

All 12 tests pass locally (npm test).

@kunalverma2512

Copy link
Copy Markdown
Owner

@ida-jemi Merging. Phenomenal work.

The code quality here is what sets this apart - named constants, explicit CAS validation, scoped error handling, and dependency injection purely for testability. This is not just correct code, it is principled code that the next engineer will understand without a single question.

The lease renewal was the last real gap in PR #280 and you closed it precisely. A crashed holder now stops renewing and lets the TTL lapse naturally that is the textbook-correct solution to the hung-sync problem. Beyond correctness, you also gave this codebase its first automated test suite and its first jobs documentation. Both will outlive this PR by years.

What makes you an indispensable contributor is not just technical depth but the follow-through you did not stop at "it works." You thought about what breaks it, wrote tests for those exact scenarios, and documented it so the next person inheriting this never has to reverse-engineer your intent. That discipline is rarer than the implementation itself.

On more thing I would genuinely love to see your eye applied to the frontend side of CodeLens. Hop onto the Matrix development channel and let us discuss what you could pick up there. You have earned that conversation.

Thank you for being exceptional. This is exactly the bar.

@kunalverma2512
kunalverma2512 merged commit 834da02 into kunalverma2512:main Jul 24, 2026
1 of 2 checks passed
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
codelensx Ready Ready Preview, Comment Jul 24, 2026 6:50pm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

High: Make Codeforces contest sync single-execution safe across multiple backend instances

2 participants