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
startContestSyncJob() acquires the contestSync MongoDB lease with a fixed five-minute TTL.
- The job invokes
cfGetContestList to fetch Codeforces contests.
- 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).
- While the original process is still awaiting that request, the five-minute lease expires.
- A different replica sees the lease as expired, successfully acquires it, and starts a second
syncCodeforcesContests() execution.
- 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
References
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
startContestSyncJob()acquires thecontestSyncMongoDB lease with a fixed five-minute TTL.cfGetContestListto fetch Codeforces contests.syncCodeforcesContests()execution.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
isRunningdoes not prevent thisisRunningis process-local. It blocks a secondrunSynccall 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'sisRunningstate and can legally acquire the lock.Affected areas
server/modules/contests/service.js—cfGetContestList/ Codeforces HTTP request configurationserver/jobs/contestSync.js— the existing fixed five-minute distributed-lock lease relies on sync work being boundedRequired changes
finally.Acceptance criteria
References