Skip to content

fix: add MongoDB-based distributed lock to contest sync cron job - #280

Merged
kunalverma2512 merged 3 commits into
kunalverma2512:mainfrom
ida-jemi:fix/contest-sync-distributed-lock
Jul 21, 2026
Merged

fix: add MongoDB-based distributed lock to contest sync cron job#280
kunalverma2512 merged 3 commits into
kunalverma2512:mainfrom
ida-jemi:fix/contest-sync-distributed-lock

Conversation

@ida-jemi

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

Copy link
Copy Markdown
Contributor

📌 Pull Request Summary

🔗 Related Issue

Closes #276


📝 Description

Provide a clear and concise summary of the changes made in this pull request.

Changes Made

  • Added a new SyncLock mongoose model (server/models/SyncLock.js) representing a distributed lock document keyed by jobName, with a lockedUntil expiry timestamp.
  • Updated server/jobs/contestSync.js to atomically acquire the lock via findOneAndUpdate before running syncCodeforcesContests(). Only the instance that successfully acquires the lock runs the sync; all other instances skip that tick and log a message instead.
  • Lock acquisition failures (e.g. DB unreachable, or another instance already holding the lock) degrade gracefully, the tick is skipped with a logged warning instead of the process crashing.
  • No changes were needed to server/server.js - startContestSyncJob() is still called the same way; the guard lives inside the job itself.

Motivation

startContestSyncJob() schedules an hourly node-cron job entirely in-process, with no coordination between multiple running instances. As reported by CodeRabbit during review of PR #269, once this app is horizontally scaled (multiple dynos/replicas/PM2 cluster workers), every instance would independently fire its own timer and run the full Codeforces sync at the same wall-clock minute, hitting the Codeforces API N times instead of once, and multiplying DB write load for zero benefit. This PR introduces a MongoDB-based distributed lock (using an existing dependency, no new infra) so that exactly one instance executes the sync per scheduled tick, regardless of instance count.


🚀 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

Ran two node server.js instances concurrently against the same MongoDB Atlas cluster to simulate horizontal scaling:

  • First run: Terminal 1 logged [Contest Sync] Synced 24 Codeforces contest(s). while Terminal 2 logged [Contest Sync] Skipped - another instance holds the lock., confirming exactly one instance executes the sync per tick.
  • Verified the synclocks collection in MongoDB Atlas contains a single lock document per job (jobName: "contestSync") with a lockedUntil expiry ~5 minutes ahead of createdAt.
  • Also verified lock persistence across process restarts: restarting both instances while a prior lock was still valid correctly resulted in both instances skipping, confirming the lock is DB-backed (not just an in-memory guard) and survives process restarts.

📸 Screenshots / Demo (If Applicable)

Terminal 1:
Screenshot 2026-07-11 224857

Terminal 2:
Screenshot 2026-07-11 224915


Screenshot 2026-07-11 224950

✅ 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

Chose the MongoDB-based leader lock approach (option 1 from the issue) since MongoDB is already a project dependency and findOneAndUpdate provides the atomicity needed to avoid a read-then-write race condition between instances, no new infrastructure (e.g. Redis) required. Also fixed the findOneAndUpdate deprecation warning (new: truereturnDocument: "after") encountered while implementing this.

Summary by CodeRabbit

  • Bug Fixes
    • Prevented duplicate contest synchronization when multiple application instances run concurrently by adding a database-backed distributed lock.
    • Improved scheduled job coordination and startup sequencing so contest sync runs only when exclusive access is available, skipping safely on contention and releasing the lock after completion/failure.

Prevents duplicate execution when multiple server instances run
concurrently (e.g. horizontal scaling). Uses an atomic
findOneAndUpdate on a new SyncLock collection so only one instance
per hourly tick runs syncCodeforcesContests(); lock failures degrade
gracefully instead of crashing.

Fixes kunalverma2512#276
@vercel

vercel Bot commented Jul 11, 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 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 56a4e389-1edf-42b9-9407-dc8f7b391605

📥 Commits

Reviewing files that changed from the base of the PR and between 3836a26 and fd7271d.

📒 Files selected for processing (2)
  • server/jobs/contestSync.js
  • server/models/SyncLock.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/models/SyncLock.js

📝 Walkthrough

Walkthrough

The contest synchronization job now uses a MongoDB-backed expiring lock. Startup initializes the lock model, and each run acquires and releases the lock while preventing overlapping local executions.

Changes

Contest sync distributed locking

Layer / File(s) Summary
Sync lock model
server/models/SyncLock.js
Defines the unique job name, owner, expiration date, timestamps, and exported Mongoose model.
Lock acquisition and release
server/jobs/contestSync.js
Atomically claims expired leases, handles duplicate-key and other acquisition failures, and releases the owned lock after execution.
Startup and scheduled execution
server/jobs/contestSync.js, server/server.js
Initializes lock indexes, adds local and distributed execution guards, awaits the initial sync run, and preserves hourly scheduling.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Server
  participant ContestSyncJob
  participant SyncLock
  participant ContestService
  Server->>ContestSyncJob: Await job initialization
  ContestSyncJob->>SyncLock: Initialize indexes
  ContestSyncJob->>SyncLock: Acquire expired lease
  SyncLock-->>ContestSyncJob: Grant or deny ownership
  ContestSyncJob->>ContestService: Sync contests when granted
  ContestSyncJob->>SyncLock: Release owned lease
Loading

Possibly related issues

  • kunalverma2512/CodeLens#277 — Directly concerns the database-backed distributed lock implemented for contest synchronization.

Possibly related PRs

Suggested labels: enhancement, type:design, type:performance

Suggested reviewers: kunalverma2512

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a MongoDB-backed distributed lock for the contest sync cron job.
Description check ✅ Passed The description follows the template and includes summary, motivation, type, testing, screenshots, checklist, and related issue.
Linked Issues check ✅ Passed The changes implement the requested multi-instance guard, atomic MongoDB locking, graceful failure handling, and expired-lock reacquisition.
Out of Scope Changes check ✅ Passed No clear unrelated code changes stand out beyond the locking fix and supporting startup/model updates.
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: 1

🤖 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/models/SyncLock.js`:
- Around line 12-20: Update the startup flow in startContestSyncJob() so it
awaits SyncLock.init() (or syncIndexes()) after connectDB() and before
scheduling the cron, ensuring the unique jobName index exists before any lock
acquisition.
🪄 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

Run ID: de5cddac-43ee-4b68-85ca-6f10e7906652

📥 Commits

Reviewing files that changed from the base of the PR and between 3d0bda7 and 1a4062e.

📒 Files selected for processing (2)
  • server/jobs/contestSync.js
  • server/models/SyncLock.js

Comment thread server/models/SyncLock.js
@kunalverma2512

Copy link
Copy Markdown
Owner

@coderabbitai

Please perform a COMPLETE production-grade review of this PR. This PR fixes a high-severity distributed systems issue related to the contest sync cron job by introducing a MongoDB-based distributed locking mechanism.

This is NOT a feature review. This is a correctness, concurrency, and production reliability review.

Please review every changed file and every changed line of code:

  • server/jobs/contestSync.js
  • server/models/SyncLock.js
  • server/server.js (verify whether leaving it unchanged is actually correct)

Review this as if you are the senior backend engineer responsible for approving a production deployment.

1. Distributed Lock Correctness

Verify whether the locking algorithm is actually correct.

Specifically check:

  • Can two instances ever acquire the lock simultaneously?
  • Is findOneAndUpdate being used atomically?
  • Is there any hidden race condition?
  • Are compare-and-swap semantics implemented correctly?
  • Is there any possibility of duplicate execution?
  • Could stale locks occur?
  • Can lock expiration cause overlapping executions?
  • Can the same sync accidentally execute twice?
  • Is there any split-brain scenario?
  • Is the implementation truly safe under horizontal scaling?

If you find even a theoretical race condition, explain it in detail.


2. Failure Scenarios

Review every failure path.

Examples:

  • MongoDB temporarily unavailable
  • Lock acquisition fails
  • Process crashes while holding lock
  • Sync throws halfway
  • Node process killed
  • Network interruption
  • Slow Mongo response
  • Clock drift
  • Multiple application restarts
  • Simultaneous startup of multiple instances
  • Long-running sync exceeding lock duration

Explain exactly what happens in every case.


3. Lock Lifecycle

Carefully inspect:

  • Lock creation
  • Lock acquisition
  • Lock renewal (if applicable)
  • Lock expiry
  • Lock release
  • Cleanup

Verify there is no path that leaves the system permanently locked or permanently unlocked.


4. Cron Job Review

Review contestSync.js completely.

Check:

  • Scheduling correctness
  • Overlapping executions
  • Re-entrancy
  • Logging
  • Error handling
  • Async handling
  • Promise handling
  • Memory leaks
  • Unhandled exceptions
  • Retry behaviour
  • Graceful degradation

Verify that skipped executions behave exactly as intended.


5. MongoDB Model Review

Review SyncLock.js in depth.

Check:

  • Schema design
  • Indexes
  • Unique constraints
  • Validation
  • TTL strategy (if used)
  • Query efficiency
  • Atomic updates
  • Data consistency
  • Future scalability

Suggest improvements if applicable.


6. Production Readiness

Think beyond the current deployment.

Would this implementation still be correct if the application runs:

  • 2 Node instances
  • 5 Node instances
  • PM2 cluster mode
  • Kubernetes replicas
  • Railway multiple replicas
  • Render multiple instances
  • Docker Swarm
  • Future autoscaling

If not, explain exactly why.


7. Edge Cases

Look specifically for hidden production bugs such as:

  • Timezone issues
  • Date arithmetic
  • Lock expiry timing
  • Clock skew
  • Duplicate cron ticks
  • Long-running sync jobs
  • Process restart during sync
  • Lock starvation
  • Lost updates
  • Deadlocks
  • Infinite lock ownership
  • Partial failures

8. Code Quality

Review whether the implementation follows professional backend engineering standards.

Evaluate:

  • Readability
  • Simplicity
  • Maintainability
  • Naming
  • Separation of concerns
  • Logging quality
  • Error messages
  • Defensive programming

Point out any code smell.


9. Alternative Designs

If there is a more robust production approach, explain it.

Examples:

  • Lease renewal
  • Compare-and-delete release
  • Owner tokens
  • Dedicated worker process
  • External scheduler
  • Redis Redlock
  • Platform cron jobs
  • Leader election

Explain the trade-offs.


10. Testing Review

Determine whether the contributor has tested enough.

Suggest additional tests for:

  • Multiple concurrent instances
  • Simultaneous startup
  • Lock expiry
  • Crash recovery
  • Mongo outage
  • Long-running sync
  • Restart during execution
  • Duplicate execution attempts
  • Race-condition stress testing

Mention any missing integration or concurrency tests.


11. Review Output Format

For EVERY issue found, provide:

  • Severity (Critical / High / Medium / Low)
  • File
  • Function
  • Root cause
  • Production impact
  • How the bug could occur
  • Recommended fix
  • Example implementation if appropriate

Do not limit the review to style or syntax. Focus on correctness, concurrency, distributed systems, production stability, fault tolerance, scalability, and long-term maintainability.

Finally answer these questions explicitly:

  1. Is this distributed locking implementation actually correct?
  2. Is there any remaining race condition?
  3. Is it safe under horizontal scaling?
  4. Could duplicate executions still happen?
  5. Are there any edge cases that remain unsolved?
  6. Is this PR production-ready?
  7. Must anything be fixed before merging?
  8. Overall merge confidence (0–10).
  9. Overall production reliability score (0–10).

Please be extremely critical. Assume this code will run in production for years and look for subtle bugs that are easy to miss during normal code review.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
echo "=== contestSync.js ==="
fd contestSync.js -x cat -n {}
echo "=== SyncLock.js ==="
fd SyncLock.js -x cat -n {}
echo "=== server.js ==="
fd server.js --max-depth 2 -x cat -n {}
echo "=== search for cron usage ==="
rg -n "cron|schedule|setInterval" --type=js -g '!node_modules' 
echo "=== search usages of startContestSyncJob ==="
rg -n "startContestSyncJob" --type=js

Length of output: 4811


🏁 Script executed:

#!/bin/bash
set -e
fd package.json --max-depth 2 -x cat -n {} \;
echo "=== mongoose version specifically ==="
rg -n '"mongoose"' server/package.json 2>/dev/null || fd package.json -x grep -n mongoose {}
echo "=== node-cron version ==="
rg -n '"node-cron"' server/package.json 2>/dev/null || fd package.json -x grep -n node-cron {}
echo "=== ContestService.syncCodeforcesContests ==="
fd service.js -p contests -x cat -n {}

Length of output: 3183


🏁 Script executed:

#!/bin/bash
set -e
fd -a "service.js" server/modules
cat -n server/modules/contests/service.js 2>/dev/null || fd service.js server/modules -x cat -n {}

Length of output: 4405


🏁 Script executed:

#!/bin/bash
fd repository.js server/modules/contests -x cat -n {}

Length of output: 3719


@kunalverma2512 Thanks for the detailed rubric — here's a full production-grade review with line-level references.

1. Distributed Lock Correctness

Can two instances acquire the lock simultaneously? No. The mechanism is sound, but it's implicit rather than explicit: acquireLock (Lines 17-37 of server/jobs/contestSync.js) never inspects the document returned by findOneAndUpdate — it unconditionally return true unless an exception is thrown (Line 25). Mutual exclusion is actually enforced by the unique index on jobName combined with the upsert-conflict pattern:

  • If no doc exists, or the existing doc's lockedUntil is in the past, the filter { jobName, lockedUntil: { $lt: now } } matches (or upsert inserts) and the caller wins.
  • If a valid (unexpired) lock exists, the filter matches nothing. With upsert: true, Mongo attempts to insert a new doc with the same jobName, which collides with the unique index → E11000, caught at Line 29, returns false.

Since MongoDB serializes writes on the same document/unique key at the storage-engine level, this does provide correct mutual exclusion — no split-brain, no simultaneous acquisition. However, this is a fragile way to express a CAS: it depends entirely on error-code sniffing rather than checking the returned document. If a future refactor changes the filter shape, adds retryable-write semantics, or a driver upgrade alters error wrapping, this silently breaks. Recommend an explicit check:

const result = await SyncLock.findOneAndUpdate(
  { jobName, lockedUntil: { $lt: now } },
  { jobName, lockedUntil: new Date(now.getTime() + ttlMs) },
  { upsert: true, returnDocument: "after" }
);
return Boolean(result); // defensive, in addition to the catch block

Severity: Medium — File: server/jobs/contestSync.js, Function: acquireLock. Root cause: correctness relies on an unchecked side-channel (duplicate-key error) instead of an explicit CAS check. Production impact: none today, but brittle to refactors/driver upgrades.

2. Failure Scenarios

Scenario What actually happens
Mongo temporarily unavailable findOneAndUpdate throws a non-11000 error → caught at Line 34, logs warning, acquireLock returns false, tick is skipped. Server does not crash. Correct graceful degradation.
Lock acquisition fails (genuine contention) Returns false via E11000, logs "Skipped — another instance holds the lock." Correct.
Process crashes while holding lock Lock remains until lockedUntil (5 min) passes, then any instance can reacquire. No permanent lockout. Good. But between crash and expiry, the sync is simply not retried (up to 5 min gap, acceptable given hourly cadence).
Sync throws halfway Caught at Line 55-56 (catch (err) { console.error(...) }). Lock is not released early — it stays held until TTL naturally expires. Fine, but means a crash 1 second into the sync still blocks other instances for up to 5 minutes.
Node process killed (SIGKILL) Same as crash — lock self-heals via TTL.
Network interruption to Mongo mid-sync Depends on where in ContestService.syncCodeforcesContests() it fails; bulkWrite({ ordered: false }) (in ContestRepository.bulkUpsertContests) means partial batches can partially succeed — acceptable since the operation is naturally idempotent (keyed on {platform, contestId}), so a retry on the next tick will simply re-upsert.
Slow Mongo response Delays acquireLock/sync but no timeout is configured anywhere — a hung connection could block the runSync promise indefinitely, which matters for point below (re-entrancy).
Clock drift Not handled — see Section 7.
Multiple app restarts Each restart calls SyncLock.init() then attempts runSync() immediately (Line 60) in addition to hourly schedule. If restarts happen frequently, every restart triggers an immediate sync attempt, contending for the lock — correct behavior, but could cause more frequent duplicate-key "noise" in logs under flapping deployments.
Simultaneous startup of multiple instances This is the PR's core use case, and Images 1/2 in the PR description confirm exactly this: one instance synced 24 contests, the other logged the skip message. Validated by manual testing.
Long-running sync exceeding lock TTL Real gap — see Section 6/7.

3. Lock Lifecycle

  • Creation: implicit — the first successful upsert creates the SyncLock doc (confirmed by PR Image 3 showing a single contestSync doc with lockedUntil).
  • Acquisition: atomic upsert/update as analyzed above.
  • Renewal: not implemented. There is no heartbeat/lease-extension while a sync is in-flight; the lock is a fixed 5-minute lease set at acquisition time only.
  • Expiry: purely time-based (lockedUntil < now), no MongoDB TTL index is used to physically delete the doc — the doc persists forever and is just re-stamped. This is fine (and arguably better than a real TTL index, since you don't want the doc auto-deleted mid-comparison).
  • Release: no explicit release step on success or failure. The lock simply expires naturally.
  • Cleanup: none needed since the doc is reused via upsert on jobName.

No path leaves the system permanently locked (TTL bounds it) or permanently unlocked (unique index + filter always converge on a single winner). Verdict: lifecycle is safe but coarse — it trades instant release/renewal for simplicity.

4. Cron Job Review (contestSync.js)

  • Scheduling: cron.schedule("0 * * * *", runSync) — correct hourly-at-:00 cron expression.
  • Overlapping executions across instances: prevented by the lock (validated).
  • Re-entrancy within the same instance: not fully prevented. If a sync from tick N is still running when tick N+1 fires and the lock has expired (TTL 5 min < 60 min tick interval, so unlikely, but possible if the sync hangs on a slow/stuck HTTP call to Codeforces with no timeout configured in cfGetContestList), the same process could call runSync() a second time concurrently, acquire the lock again (since it's expired), and run two syncCodeforcesContests() calls in parallel in-process. Impact is bounded because bulkUpsertContests is idempotent (upsert by {platform, contestId}), so this causes wasted API calls and DB writes, not data corruption. Severity: Low-Medium given the idempotent downstream writes, but still worth a timeout on the Codeforces HTTP call and/or an in-process re-entrancy guard (e.g., a module-level boolean isRunning).
  • Logging: adequate — distinguishes "Synced N contests", "Skipped — another instance holds the lock", and error logs. Could be improved by including jobName/timestamp/instance-id for correlating logs across replicas in aggregated log systems (e.g., Kubernetes/Railway multi-replica logs are hard to distinguish without an instance identifier).
  • Error handling: runSync's try/catch (Lines 52-57) prevents unhandled promise rejections from ContestService.syncCodeforcesContests(). Good.
  • Promise handling: the initial runSync() call (Line 60) and the cron-scheduled callback are both fire-and-forget, which is expected/idiomatic for node-cron (it doesn't await async callbacks). No unhandled rejection risk since all async paths are internally caught.
  • Retry behavior: none — a skipped/failed tick simply waits for the next hourly tick. Acceptable for this use case.
  • Memory leaks: none apparent; no accumulating listeners/timers beyond the single cron.schedule registration.

5. MongoDB Model Review (SyncLock.js)

  • Schema: minimal and appropriate — jobName (unique, required), lockedUntil (required Date), timestamps: true.
  • Indexes: unique: true on jobName creates a background/unique index. This is the entire correctness backbone of the locking scheme (Section 1) — yet the PR relies on SyncLock.init() (Line 43 of contestSync.js) to guarantee it exists before first use. Risk: Model.init() only builds indexes if Mongoose's autoIndex is enabled (default true, but commonly disabled in production for performance on large collections via mongoose.set('autoIndex', false') or autoIndex: false in connectDB). If autoIndex is disabled anywhere in this codebase's Mongo connection config, the unique index silently never gets created, and the entire mutual-exclusion guarantee collapses (two instances could both succeed the upsert since there'd be no unique constraint to conflict on). Please verify connectDB() in server/config/db.js doesn't disable autoIndex, or explicitly call SyncLock.syncIndexes()/createIndexes() as part of a startup migration step regardless of the autoIndex setting.
  • Validation: adequate for the scope (required: true on both fields).
  • TTL strategy: no native Mongo TTL index (expireAfterSeconds) is used — by design, since the doc is reused, not deleted. This is the right choice here.
  • Query efficiency: trivial single-doc lookups on an indexed unique field — no concerns.
  • Atomic updates: correctly implemented via a single findOneAndUpdate.
  • Future scalability: fine for a handful of named jobs (jobName as the natural sharding/partition key). If more scheduled jobs are added later, this same model/pattern generalizes cleanly.

6. Production Readiness

Environment Correct? Notes
2-5 Node instances Yes Confirmed correct via the unique-index CAS mechanism.
PM2 cluster mode Yes, with a caveat Each PM2 worker is a separate process with its own clock/event loop — same guarantees apply.
Kubernetes / Railway / Render replicas Yes, with a caveat The clock-drift issue (Section 7) becomes more relevant in multi-node/multi-AZ deployments where NTP sync isn't guaranteed to sub-second precision.
Docker Swarm Yes Same as above.
Future autoscaling Mostly yes As replica count grows, contention on the single SyncLock doc increases proportionally (more E11000 "losers" per tick), but this is cheap — no bottleneck concern at any realistic scale for an hourly job.

Caveat that applies to all of the above: the correctness of the entire scheme hinges on the unique index on jobName actually existing in the target Mongo deployment (see Section 5) — this is an operational dependency that isn't verified/enforced anywhere beyond SyncLock.init().

7. Edge Cases

  • Clock skew (real, unaddressed): now in acquireLock is each Node process's local Date.now(). If instance A's clock is fast, it may consider a lock expired sooner than it should be (from the setter's perspective), or if the setter's clock was fast when writing lockedUntil, other instances may see the lock as valid longer than the 5-minute intent. This is not catastrophic here (worst case: a slightly early/late reacquisition on an idempotent job), but for a "run for years" system, prefer server-side time via an update pipeline (update: [{ $set: { lockedUntil: { $add: ["$$NOW", ttlMs] } } }]) so all time math happens on the Mongo server's single clock, eliminating app-to-app clock skew entirely.
  • Lock expiry timing / long-running sync: as discussed, TTL (5 min) < tick interval (60 min) normally provides ample margin, but there is no hard timeout on the Codeforces HTTP call (cfGetContestList) — an indefinitely hanging request would hold the lock in a "logically running but not renewing" state past its TTL, allowing another instance (or the same one on a later tick) to start a concurrent run. Recommend adding an explicit axios timeout to cfGetContestList and/or an AbortController for the sync call.
  • Duplicate cron ticks / lost updates / deadlocks / infinite lock ownership: none observed — the TTL bound rules out infinite ownership and deadlocks.
  • Partial failures: bulkWrite({ ordered: false }) in ContestRepository.bulkUpsertContests means one bad doc doesn't block the rest — good; combined with the idempotent upsert key, partial/duplicate runs self-heal on the next tick.

8. Code Quality

  • Naming and comments are good — the docstrings on acquireLock and the SyncLockSchema explain intent clearly.
  • Code smell: the unused/uninspected result of findOneAndUpdate combined with the returnDocument: "after" option that is never read (Lines 20-24) — either use the returned doc for a defensive check or drop the option, since as written it does nothing.
  • Separation of concerns is reasonable: lock logic lives entirely in contestSync.js/SyncLock.js, sync logic stays in ContestService.
  • Logging quality is decent but lacks a per-instance identifier for correlating across replicas in production log aggregators.
  • No defensive timeout on the external HTTP call driving the whole lock-duration assumption.

9. Alternative Designs (trade-offs)

  • Lease renewal (heartbeat): an interval that re-extends lockedUntil every N seconds while the sync runs would eliminate the "TTL expires mid-sync" risk entirely, at the cost of slightly more code and one extra periodic write. Recommended given how cheap it'd be to add.
  • Owner token + compare-and-delete release: store a random ownerId per acquisition and findOneAndDelete({ jobName, ownerId }) on completion to release the lock immediately rather than waiting out the TTL. Improves responsiveness for manual/ad-hoc reruns but is unnecessary complexity for a fixed hourly job.
  • Redis Redlock: explicitly avoided per the linked issue's objective ("avoid requiring new infrastructure such as Redis") — correct call for this scale.
  • Platform-native cron (e.g., Kubernetes CronJob) instead of in-process node-cron: removes the need for app-level locking altogether by only ever running one Job pod, but couples the sync to infra config rather than app code — a bigger architectural change, probably out of scope here.
  • Leader election (e.g., via Mongo-backed leader lease held for the process lifetime rather than per-tick): would let only the leader instance run any scheduled job, generalizing beyond just contest sync, at the cost of more moving parts.

Given the scope (single hourly job, no existing infra), the current MongoDB-upsert-lock approach is a reasonable, low-complexity choice — the main gap is the missing renewal/heartbeat for long-running syncs.

10. Testing Review

Manual two-instance testing (per PR images) validates the primary happy path (one syncs, one skips) and lock persistence across restarts. Missing/recommended tests:

  • Unit test for acquireLock mocking SyncLock.findOneAndUpdate to throw { code: 11000 } → assert false returned, no crash.
  • Unit test for acquireLock mocking a generic Mongo error (e.g., MongoNetworkError) → assert false returned and a warning is logged, not a throw.
  • Integration test using mongodb-memory-server (or similar) with two concurrent calls to acquireLock for the same jobName → assert exactly one resolves "acquired" and the doc's lockedUntil is set once.
  • Test for lock expiry: manually set lockedUntil in the past, call acquireLock again, assert it succeeds (renewal path).
  • Test for SyncLock.init() actually creating the unique index (guards against silent autoIndex: false regressions).
  • Stress/race test: fire N (e.g., 20) concurrent acquireLock calls for the same fresh jobName and assert exactly one true, N-1 false, and no unhandled exceptions.
  • No integration test exists exercising the actual runSync skip-logging path or the immediate-run-on-boot path (Line 60).

Answers to your explicit questions

  1. Is this distributed locking implementation actually correct? Yes, for the core mutual-exclusion guarantee at a given moment in time — verified by manual testing and confirmed algorithmically via MongoDB's per-document write serialization + unique index conflict.
  2. Is there any remaining race condition? Not a simultaneous-acquisition race, but there is a real overlap-on-expiry gap: if a sync outlives the 5-minute TTL (no heartbeat/renewal), a second instance (or the same instance's next tick) can start concurrently. Downstream writes are idempotent, so this degrades to wasted work rather than data corruption.
  3. Is it safe under horizontal scaling? Yes, conditional on the unique index on SyncLock.jobName actually existing in production (verify autoIndex settings).
  4. Could duplicate executions still happen? Yes, in the narrow long-running-sync-exceeds-TTL scenario described above — but "duplicate" here means duplicate work, not duplicate/corrupted data, thanks to ContestRepository.bulkUpsertContests's idempotent upsert key.
  5. Are there any edge cases that remain unsolved? Clock skew across app instances (no server-side time source used), no HTTP timeout bounding the sync duration relative to the lock TTL, and dependence on autoIndex for the unique index.
  6. Is this PR production-ready? Yes, for the stated scope and current scale (single hourly job, small number of replicas) — it correctly solves the duplicate-sync problem described in the linked issue.
  7. Must anything be fixed before merging? Nothing blocking; the following are strongly recommended as fast follow-ups: (a) add an HTTP timeout to cfGetContestList so lock duration assumptions hold, (b) explicitly verify/enforce the unique index exists regardless of autoIndex, (c) optionally add a defensive check on the findOneAndUpdate result.
  8. Overall merge confidence: 8/10.
  9. Overall production reliability score: 7/10 (would be 9/10 with a heartbeat/lease-renewal mechanism and a bounded HTTP timeout on the sync call).
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 1

🤖 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/server.js`:
- Line 11: Update startContestSyncJob() invocation in startServer() to catch
SyncLock.init() failures, log the error, and leave contest synchronization
disabled while allowing app.listen() to continue. Preserve normal job startup
when lock initialization succeeds.
🪄 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

Run ID: 2db160d1-2d5e-4098-8bc0-04a1c3b5e575

📥 Commits

Reviewing files that changed from the base of the PR and between 1a4062e and 3836a26.

📒 Files selected for processing (2)
  • server/jobs/contestSync.js
  • server/server.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/jobs/contestSync.js

Comment thread server/server.js
@kunalverma2512

kunalverma2512 commented Jul 12, 2026

Copy link
Copy Markdown
Owner

@ida-jemi Please see coderabbitai comment
coderabbitai has said lot of things using which you can learn and please do a proper reasoning of what coderabbitai is saying i.e what everything good work you done with proper reasoning and what you more to do as per coderabbitai comment and why to do and what it improves after doing those things

please do a proper reasoning and confirm me ones done that things are finally ready to merge or not

NOTE : please provide a report for the reasoning you will do about any further changes you will be doing and why and how it improves the code and overall work

Thanks

…e, and handle SyncLock init failure gracefully

- Fixes a syntax error from a bad merge (duplicate const runSync).
- acquireLock/releaseLock now use an ownerId per acquisition so only
  the owning instance can release its own lease, and release happens
  immediately after each run instead of waiting out the full TTL.
- startContestSyncJob() now catches SyncLock.init() failures so a
  background job init problem can't prevent app.listen() from running.

Addresses CodeRabbit review feedback on PR kunalverma2512#280
@ida-jemi

Copy link
Copy Markdown
Contributor Author

@kunalverma2512 I've reviewed the comments and made a few changes:

(a) Startup could crash on a background-job failure

startContestSyncJob() awaited SyncLock.init() with no try/catch anywhere above it. If MongoDB's index build failed for any reason, that rejection would propagate through startServer() and prevent app.listen() from ever running. This meant a failure in a background cron job could take down the entire HTTP API. This was a real, traced bu, not a theoretical concern.

Fix:

  • Wrapped SyncLock.init() in a try/catch inside startContestSyncJob().
  • If initialization fails:
    • The error is logged.
    • Contest sync is disabled for that process instance.
    • The function returns normally instead of rejecting.
  • This ensures app.listen() is never at risk from a background job initialization failure.
  • This also satisfies CodeRabbit's suggestion of handling the failure either at the caller or within the job itself. We chose the latter since it keeps the failure contained where it originates.

(b) acquireLock didn't explicitly validate its own result

The original implementation returned true unconditionally unless an exception was thrown, relying entirely on MongoDB's duplicate-key error as a side effect. While this works today, it's fragile, if a future MongoDB driver or Mongoose update changes how conflicts are surfaced, the lock acquisition could silently fail without detection.

Fix:
This was addressed as part of a broader locking improvement:

  • acquireLock and the new releaseLock now use a unique ownerId for every lock acquisition.

  • Instead of assuming success, lock acquisition now explicitly validates the result:

    return result?.ownerId === ownerId;

    This replaces an implicit assumption with an explicit correctness check.

Additionally, this change provides two improvements beyond CodeRabbit's original suggestion:

  • Immediate lock release

    • The lock is now released immediately after a sync completes (whether it succeeds or fails), rather than waiting for the TTL to expire.
    • This significantly narrows the "long-running sync exceeds TTL" window that CodeRabbit highlighted in Sections 6/7 of its review.
  • In-process overlap protection

    • An isRunning guard prevents the same process from starting a second sync while a previous one is still executing.
    • This avoids overlapping sync runs within the same application instance.

What CodeRabbit flagged but I'm intentionally not addressing in this PR:

Item CodeRabbit's verdict Why it's reasonable to leave for a follow-up
No HTTP timeout on the Codeforces API call Recommended fast-follow This belongs in ContestService/cfGetContestList, not the locking implementation. It's a separate concern in a different file and is better handled in its own focused PR rather than expanding the scope of this one.
Clock drift across instances "Not catastrophic" (explicitly marked as non-blocking) The worst-case outcome is a slightly early or late lock reacquisition on an already-idempotent job. A more robust solution, using MongoDB server time (e.g., via an aggregation pipeline update), is a reasonable future enhancement, but it's not urgent.
No lease renewal / heartbeat "Real gap" but degrades to wasted work rather than data corruption Since the sync operation is idempotent, the impact is limited. This PR addresses the concern differently by releasing the lock immediately after each run and adding an in-process isRunning guard, which substantially reduces the overlap window without introducing heartbeat complexity.
autoIndex might be disabled, undermining the unique index "Please verify" Verified directly by checking server/config/db.js. The configuration never modifies autoIndex, so Mongoose's default behavior (autoIndex: true) remains in effect. Confirmed to be a non-issue.

Please do review the changes and if there are any more changes or improvements to be made pls do let me know

@ida-jemi

Copy link
Copy Markdown
Contributor Author

Hi @kunalverma2512, just following up since it's been about a week. Whenever you get a chance, could you take a look at the fixes I pushed addressing CodeRabbit's review (startup-crash handling on SyncLock.init() failure, and the explicit lock-ownership check)? Happy to make any further changes if you have feedback. Just wanted to bump this in case it got buried. Thanks!

@kunalverma2512

Copy link
Copy Markdown
Owner

@ida-jemi Hi
I can understand
I'll review it asap
currently a lot busy in other stuffs

Thanks for keeping hope and I'll comeback soon

Till then you can work on thinking that what more you can grow and make its issue and directly come up with PR
just make sure it doesnt be boring feature or any kind of spamming feature which is already their and must be a well good idea and full research before making it eligible for making issue and hence its PR

Currently I am occupied a lot so I hope you'll understand

@kunalverma2512

Copy link
Copy Markdown
Owner

@ida-jemi — Merging this. Genuinely well done. 🎉

I want to take a moment to actually say this properly: this was not a
simple PR. You tackled a real distributed systems problem — something
that most contributors shy away from — and you saw it through completely.
Three commits, full reasoning for every CodeRabbit point, manual testing
with two live terminal instances and Atlas screenshots, and a clear,
honest explanation of what you deferred and why. That is exactly the
standard of work we want to build CodeLens on. Thank you.


The core locking logic is correct and production-safe for our current
scale. The ownerId-scoped CAS check, immediate releaseLock(), the
isRunning in-process guard, and the startup crash protection together
make this a solid, well-reasoned implementation.


I am going to keep two things out of this PR intentionally and open
them as dedicated follow-up issues instead:

Follow-up 1 — HTTP timeout on the Codeforces API call
Follow-up 2 — Per-instance identifier in sync logs

Why these matter even though they are not blockers today:

Follow-up 1 is the only scenario where duplicate execution can still
technically happen. If the Codeforces API ever hangs with an open but
silent connection, the lock TTL can expire while Instance A is still
waiting, allowing Instance B to acquire the lock and start a second sync
concurrently. Data won't corrupt (bulkUpsertContests is idempotent) but
this is precisely the problem this PR was built to prevent. A single
axios timeout: 15000 in cfGetContestList closes that gap permanently.

Follow-up 2 matters the moment we run more than one replica. Logs from
multiple instances all saying "[Contest Sync] Skipped" with no instance
identifier makes it nearly impossible to debug which node did what during
a production incident. It is a tiny change with a large operational
payoff.

Both follow-ups are small and well-scoped. I will open them as labelled
issues shortly so anyone can pick them up.


One small tip before you go: your written reasoning in the contributor
comment was genuinely impressive — you did not just list what you changed,
you explained why each change improves the system and what trade-off you
accepted by deferring something. Keep that habit. It is rarer than good
code and it is what separates a contributor from a collaborator.

Looking forward to seeing what you build next.

@kunalverma2512

Copy link
Copy Markdown
Owner

@coderabbitai create new issue

This is a follow-up to PR #280 which introduced a MongoDB-based distributed
lock for the contest sync cron job.

The lock uses a fixed 5-minute TTL. The problem is there is no HTTP timeout
configured on the Codeforces API call inside cfGetContestList. If the
Codeforces API establishes a TCP connection but never sends a response,
Node.js will hang indefinitely waiting on the OS TCP keepalive timer which
can be 10–30 minutes depending on the host. During that window the lock TTL
will expire, another instance will see the lock as stale, acquire it, and
begin a second concurrent sync — which is exactly the duplicate execution
scenario PR #280 was built to eliminate.

Please create a well-structured GitHub issue covering the full failure chain,
why the isRunning guard does not protect against this cross-process scenario,
what the downstream impact is, and what needs to be done to resolve it.
Mark it as a follow-up to PR #280 and label it appropriately.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

gh label list --repo kunalverma2512/CodeLens --limit 100

Length of output: 1843


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="kunalverma2512/CodeLens"
body_file="$(mktemp)"

cat >"$body_file" <<'EOF'
## Summary

Follow-up to `#280`: bound the Codeforces API request used by contest synchronization so a hung HTTP response cannot outlive the MongoDB distributed-lock lease and permit a concurrent sync on another application instance.

## Failure chain

1. `startContestSyncJob()` acquires the `contestSync` MongoDB lease with a fixed five-minute TTL.
2. The job invokes `cfGetContestList` to fetch Codeforces contests.
3. If Codeforces accepts the TCP connection but never sends a response, the request has no application-level timeout and can remain pending until the host/OS network timeout (potentially 10–30 minutes).
4. While the original process is still awaiting that request, the five-minute lease expires.
5. A different replica sees the lease as expired, successfully acquires it, and starts a second `syncCodeforcesContests()` execution.
6. Both processes can then perform the same sync concurrently.

The database upserts are idempotent, so this should not corrupt contest records. However, it defeats the PR `#280` guarantee of one synchronization execution per scheduled interval and causes duplicate external API traffic and avoidable database work.

## Why `isRunning` does not prevent this

`isRunning` is process-local. It blocks a second `runSync` call only within the instance that is already waiting on the hung request. It is not stored in MongoDB and is not shared with PM2 workers, containers, Kubernetes pods, or other application replicas. Once the distributed lease expires, another process has no visibility into the first process's `isRunning` state and can legally acquire the lock.

## Affected areas

- `server/modules/contests/service.js` — `cfGetContestList` / Codeforces HTTP request configuration
- `server/jobs/contestSync.js` — the existing fixed five-minute distributed-lock lease relies on sync work being bounded

## Required changes

- Configure an explicit, finite HTTP timeout for the Codeforces request (15 seconds is the initial target proposed during review).
- Ensure timeout failures reject through the existing sync error-handling path, are logged with actionable context, and do not crash the server.
- Preserve the existing behavior that releases the owner-scoped lock in `finally`.
- Document the timeout choice and ensure it is configurable if project conventions support environment-based HTTP settings.

## Acceptance criteria

- [ ] A Codeforces request that does not return within the configured timeout fails predictably rather than waiting for OS-level TCP timeouts.
- [ ] The failed sync is logged and the owner-scoped distributed lock is released through the existing cleanup path.
- [ ] A hung/simulated slow request cannot remain active beyond the five-minute lock lease; the configured timeout must remain safely below that lease.
- [ ] Add or update tests covering the timeout/failure path and verifying that a later sync attempt can proceed.
- [ ] Verify normal successful Codeforces synchronization remains unchanged.

## References

- Follow-up to PR `#280`: https://github.com/kunalverma2512/CodeLens/pull/280
- Requested in CodeRabbit review comment: https://github.com/kunalverma2512/CodeLens/pull/280#issuecomment-4952346075
- Requested by `@kunalverma2512`
EOF

issue_url="$(gh issue create \
  --repo "$repo" \
  --title "fix(contest-sync): add a timeout to Codeforces API requests" \
  --body-file "$body_file" \
  --label "bug" \
  --label "backend" \
  --label "type:bug" \
  --label "level:advanced" \
  --assignee "kunalverma2512")"

printf '%s\n' "$issue_url"

Length of output: 3836


The Comment image 1 shows a server instance completing the Codeforces contest sync; the Comment image 2 shows another instance correctly skipping because the distributed lock is held; the Comment image 3 shows the persisted contestSync MongoDB lease document with a five-minute lockedUntil timestamp.

@kunalverma2512 Created and assigned the follow-up issue: #281 — fix(contest-sync): add a timeout to Codeforces API requests.

It includes the cross-process failure chain, why isRunning is insufficient, downstream impact, required remediation, and testable acceptance criteria. It is linked back to PR #280 and labeled bug, backend, type:bug, and level:advanced.

@kunalverma2512

Copy link
Copy Markdown
Owner

@coderabbitai create new issue

This is a follow-up to PR #280 which introduced multi-instance awareness
at the execution level for the contest sync cron job.

The logging in contestSync.js still uses a static prefix with no per-instance
identifier. Once this application runs more than one replica — on Render,
Railway, Kubernetes, or PM2 cluster — every log line from every instance is
completely indistinguishable in an aggregated log stream. You cannot tell
which instance ran the sync, which instance skipped, or which instance to
inspect during a production incident.

Please create a well-structured GitHub issue explaining the observability gap
this creates in a multi-replica deployment, the production debugging scenarios
that become painful without it, and what needs to be done. Mark it as a
follow-up to PR #280 and label it appropriately.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

gh label list --repo kunalverma2512/CodeLens --limit 100

Length of output: 1843


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="kunalverma2512/CodeLens"
body_file="$(mktemp)"

cat >"$body_file" <<'EOF'
## Summary

Follow-up to `#280`: add a stable per-instance identifier to contest-sync logs so activity from multiple application replicas can be correlated in a shared log stream.

PR `#280` made the contest sync execution-safe across instances using a MongoDB-backed distributed lock. Its logs still use a static prefix such as `[Contest Sync]`, however. In a multi-replica deployment, this does not identify which process acquired the lock, completed a sync, skipped because another replica owns the lease, or emitted an error.

## Observability gap

When multiple instances run concurrently—such as PM2 cluster workers, Render/Railway replicas, Kubernetes pods, or Docker Swarm tasks—their stdout/stderr is commonly aggregated. Without an instance identifier, log lines are indistinguishable across processes.

For example, an operator can see:

- `[Contest Sync] Synced 24 Codeforces contest(s).`
- `[Contest Sync] Skipped - another instance holds the lock.`

but cannot determine which replica performed the sync and which replica skipped. This prevents reconstructing ownership and execution flow during an incident.

## Production impact

The lack of instance-level context makes the following scenarios unnecessarily difficult to debug:

- Investigating whether the distributed lock is consistently selecting one executor across replicas.
- Identifying the replica that produced sync failures, slow requests, or database errors.
- Correlating application logs with a specific Kubernetes pod, container, PM2 worker, host, or platform instance.
- Diagnosing restart loops or deployments where a newly started replica repeatedly attempts the immediate startup sync.
- Determining whether unexpected duplicate work came from the same process or separate replicas.

The lock remains correct, but incident response and post-incident diagnosis are impaired as horizontal scaling increases.

## Affected areas

- `server/jobs/contestSync.js` — contest-sync success, skip, lock-acquisition-failure, and error logs.
- Server configuration/environment handling — source or derive a stable identifier for the running process/replica.

## Required changes

- Add an instance identifier to every contest-sync log line.
- Prefer a configurable environment value when the deployment platform provides one; otherwise use a safe generated process-local fallback established at startup.
- Use a consistent log format for successful runs, lock contention/skips, acquisition failures, release failures, and sync errors.
- Ensure the identifier is safe to expose in logs and does not include secrets.
- Keep the locking behavior and existing sync semantics unchanged.

## Acceptance criteria

- [ ] Every contest-sync log emitted by `server/jobs/contestSync.js` includes a non-empty instance identifier.
- [ ] Two concurrently running instances produce distinguishable sync and skip log entries.
- [ ] The identifier remains consistent for the lifetime of a process.
- [ ] An environment-provided identifier can be used when available, with a documented fallback when it is absent.
- [ ] No credentials, connection strings, or other sensitive configuration values are logged.
- [ ] Existing contest-sync scheduling, distributed-lock acquisition, owner-scoped release, and error handling remain unchanged.
- [ ] Add or update tests for identifier selection and representative log output where the project's test conventions support it.

## References

- Follow-up to PR `#280`: https://github.com/kunalverma2512/CodeLens/pull/280
- Requesting comment: https://github.com/kunalverma2512/CodeLens/pull/280#issuecomment-4952346075
- Requested by `@kunalverma2512`
EOF

gh issue create \
  --repo "$repo" \
  --title "enhancement(contest-sync): add per-instance identifiers to distributed sync logs" \
  --body-file "$body_file" \
  --label "enhancement" \
  --label "backend" \
  --label "code-quality" \
  --label "level:advanced" \
  --assignee "kunalverma2512"

Length of output: 4241


The PR description image 1 shows one server instance successfully completing the contest sync: [Contest Sync] Synced 24 Codeforces contest(s). The PR description image 2 shows the second instance correctly skipping while the distributed lock is held: [Contest Sync] Skipped - another instance holds the lock. The PR description image 3 shows the persisted MongoDB lease document for jobName: "contestSync".

@kunalverma2512 Created and assigned #282 — enhancement(contest-sync): add per-instance identifiers to distributed sync logs.

It is marked as a follow-up to #280 and covers the multi-replica observability gap, incident-debugging impact, affected areas, required implementation approach, and concrete acceptance criteria.

Labels applied: enhancement, backend, code-quality, and level:advanced.

@kunalverma2512
kunalverma2512 merged commit 4519230 into kunalverma2512:main Jul 21, 2026
1 of 2 checks passed
@kunalverma2512

Copy link
Copy Markdown
Owner

@ida-jemi!

Quick reminder - please keep the development channel updated
whenever you are working on an issue or PR. A brief note like
"picked up issue #X, working on Y" is all it takes.

This channel exists specifically for this so the whole community
knows who is working on what, avoids duplicate efforts, and
stays in sync. It is mandatory for all contributors, not optional.

Looking forward to seeing your updates here! 🙌

Click here

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] Contest sync cron job lacks multi-instance execution guard (duplicate execution risk)

2 participants