Fix concurrent activity summary insert conflicts #26

Closed
McPringle wants to merge 1 commit from fix-race-condition into main
McPringle commented 2026-04-28 16:14:36 +02:00 (Migrated from github.com)

Problem

When multiple activities were processed in quick succession, asynchronous updates of ActivitySummary entries could run into a race condition.

The sequence was:

  1. Two activities for the same user were processed almost at the same time.
  2. Both async tasks tried to update the same weekly/monthly/yearly summary.
  3. Both tasks initially found no existing ActivitySummary row.
  4. Both created a new entity for the same combination of:
    • user_id
    • period_type
    • period_start
  5. The first insert succeeded, and the second one failed on the unique constraint.

This resulted in errors such as:

  • duplicate key value violates unique constraint "activity_summaries_user_id_period_type_period_start_key"
  • DataIntegrityViolationException in updateSummariesForActivity(...)

Root cause

The summary update logic was not robust against concurrent inserts for the same period summary.

The code followed a pattern like:

  • findByUserIdAndPeriodTypeAndPeriodStart(...)
  • if not found: build a new ActivitySummary
  • save(...)

This "check then insert" flow is not atomic under parallel execution.

Impact

  • The activity itself was still imported or saved successfully.
  • The asynchronous summary update could fail afterward.
  • This caused noisy log output and could leave weekly/monthly/yearly summaries temporarily inconsistent or delayed.

Solution

The update flow now handles concurrent insert conflicts explicitly.

When saving a newly created summary fails with a DataIntegrityViolationException, the service assumes that another concurrent task has already created the row. It then reloads the existing ActivitySummary, recalculates the summary values for the same period, and saves the entity again as a normal update.

This keeps the asynchronous workflow intact while making summary updates resilient to concurrent activity processing.

Expected behavior

If another concurrent task creates the same summary row first, the second task should not fail with an uncaught exception. It should reload the existing summary and continue with a normal update.

## Problem When multiple activities were processed in quick succession, asynchronous updates of `ActivitySummary` entries could run into a race condition. The sequence was: 1. Two activities for the same user were processed almost at the same time. 2. Both async tasks tried to update the same weekly/monthly/yearly summary. 3. Both tasks initially found no existing `ActivitySummary` row. 4. Both created a new entity for the same combination of: - `user_id` - `period_type` - `period_start` 5. The first insert succeeded, and the second one failed on the unique constraint. This resulted in errors such as: - `duplicate key value violates unique constraint "activity_summaries_user_id_period_type_period_start_key"` - `DataIntegrityViolationException` in `updateSummariesForActivity(...)` ## Root cause The summary update logic was not robust against concurrent inserts for the same period summary. The code followed a pattern like: - `findByUserIdAndPeriodTypeAndPeriodStart(...)` - if not found: build a new `ActivitySummary` - `save(...)` This "check then insert" flow is not atomic under parallel execution. ## Impact - The activity itself was still imported or saved successfully. - The asynchronous summary update could fail afterward. - This caused noisy log output and could leave weekly/monthly/yearly summaries temporarily inconsistent or delayed. ## Solution The update flow now handles concurrent insert conflicts explicitly. When saving a newly created summary fails with a `DataIntegrityViolationException`, the service assumes that another concurrent task has already created the row. It then reloads the existing `ActivitySummary`, recalculates the summary values for the same period, and saves the entity again as a normal update. This keeps the asynchronous workflow intact while making summary updates resilient to concurrent activity processing. ## Expected behavior If another concurrent task creates the same summary row first, the second task should not fail with an uncaught exception. It should reload the existing summary and continue with a normal update.
McPringle commented 2026-04-30 08:48:28 +02:00 (Migrated from github.com)

please wait with a merge of this PR - last night I got an idea of a possibly way easier solution.

please wait with a merge of this PR - last night I got an idea of a possibly way easier solution.
McPringle commented 2026-04-30 11:41:28 +02:00 (Migrated from github.com)

The current implementation is unnecessarily complicated. I'm withdrawing my PR and would rather work on a simpler solution. See Issue #30.

The current implementation is unnecessarily complicated. I'm withdrawing my PR and would rather work on a simpler solution. See Issue #30.

Pull request closed

Sign in to join this conversation.
No description provided.