Skip to content

Payroll scheduler has no row locking — duplicate payment execution risk across pod replicas #448

Description

@Wilfred007

What

Add row-level locking (FOR UPDATE SKIP LOCKED or equivalent) to the due-schedule query in the payroll scheduler so concurrent pods can't pick up and execute the same schedule at the same time.

Why

processDueSchedules() in backend/src/services/scheduleExecutor.ts (L44-150) runs on a node-cron('* * * * *') job initialized unconditionally in backend/src/index.ts (L30) — meaning every backend pod runs this job independently. The query selecting due schedules (SELECT ... WHERE next_run_timestamp <= NOW() AND status='active') has no row locking. k8s/base/backend-hpa.yaml sets minReplicas: 2, so at least two pods are always running this cron. The code's own comment (L95-97) acknowledges there's no real idempotency check on this path — and idempotencyMiddleware is applied to HTTP schedule-creation routes but never reaches this background executor. In production this can mean two pods executing the same due payroll schedule simultaneously — real duplicate Stellar payments to employees.

Scope

In scope:

  • Add FOR UPDATE SKIP LOCKED (or a Postgres advisory lock / claim-row pattern) to the schedule-selection query so only one pod claims a given due schedule
  • Ensure the claim + status transition happens atomically (single transaction)
  • Test coverage simulating two concurrent "pods" processing the same due schedule

Out of scope:

  • Changes to how schedules are created or their cron cadence
  • The separate backend/src/jobs/part49Jobs.ts daily jobs (tracked in a different issue)

Acceptance Criteria

  • Two concurrent calls to processDueSchedules() against the same due schedule result in exactly one execution
  • New test demonstrates the race is closed (e.g. two parallel invocations, assert single execution)
  • No regression in normal single-pod scheduling behavior

Technical Context

  • backend/src/services/scheduleExecutor.tsprocessDueSchedules(), L44-150, comment at L95-97
  • backend/src/index.ts — L30, unconditional cron init
  • k8s/base/backend-hpa.yamlminReplicas: 2
  • idempotencyMiddleware — currently only wired to HTTP routes, not this background path

Activity

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

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third CampaignbackendBackend developmenthardComplex taskssecurityIssues related to application security and audits

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions