Skip to content

Fix PRIMARY KEY violation in WorkloadDetails when interval fires multiple times#166

Merged
spaghettidba merged 5 commits intomasterfrom
copilot/fix-primary-key-constraint-violation
Apr 10, 2026
Merged

Fix PRIMARY KEY violation in WorkloadDetails when interval fires multiple times#166
spaghettidba merged 5 commits intomasterfrom
copilot/fix-primary-key-constraint-violation

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 9, 2026

  • Understand root cause: CloseInterval condition 0 >= 0 is always true when Interval=0 (default); after the first write sets lastDump=lastEventTime, the next loop iteration fires for the same interval_id → PRIMARY KEY violation
  • Proper fix: track lastWrittenIntervalId (volatile). CloseInterval skips the write if the prospective interval_id matches the last written one. WriteToServer records lastWrittenIntervalId after a successful commit.
  • Extracted ComputeIntervalId() helper used by both CloseInterval and CreateInterval
  • Removed static from ComputeIntervalId for consistency with class style
  • Moved rawData.Clear() inside lock(rawData) to prevent a race between write and clear
  • Reverted staging table + MERGE approach (replaced with the proper race condition fix)
  • Parallel validation passed (CodeQL: 0 alerts)

Copilot AI and others added 2 commits April 9, 2026 16:14
…e and MERGE

Instead of bulk-inserting directly into WorkloadDetails (which fails with a PRIMARY
KEY violation when CloseInterval fires multiple times for the same interval_id, e.g.
when events share identical timestamps), the new approach:

1. Bulk-copies aggregated rows into a temporary staging table (#WorkloadDetails_Staging)
2. Uses a MERGE statement to either INSERT new rows or UPDATE existing rows by
   properly combining aggregate statistics (sums add, min/max recalculate, counts add)

Also moves rawData.Clear() inside the lock(rawData) block to fix a race condition
where new events could be added to rawData between the write and the clear."

Agent-Logs-Url: https://github.com/spaghettidba/WorkloadTools/sessions/df9cb5f7-e87d-43bd-aa92-5c81d6039a5c

Co-authored-by: spaghettidba <7592822+spaghettidba@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix violation of PRIMARY KEY constraint 'PK_WorkloadDetails' Fix PRIMARY KEY violation in WorkloadDetails when interval fires multiple times Apr 9, 2026
@spaghettidba
Copy link
Copy Markdown
Owner

@copilot I very much dislike this implementation. Why use a MERGE to patch a race condition rather than fixing it?

…writes

Root cause: when Interval=0 (default), CloseInterval's condition
`duration.TotalMinutes >= Interval` is `0 >= 0` (always true). After
the first write sets lastDump=lastEventTime, the next loop iteration
fires immediately with the same lastEventTime -> same interval_id ->
PRIMARY KEY violation on WorkloadDetails.

Fix: track lastWrittenIntervalId (volatile for cross-thread visibility).
CloseInterval computes the prospective interval_id before writing; if it
matches lastWrittenIntervalId it skips the write and advances lastDump.
WriteToServer records lastWrittenIntervalId after a successful commit.

Additional changes:
- Extract ComputeIntervalId() helper shared by CloseInterval and CreateInterval
- Move rawData.Clear() inside lock(rawData) to close a race window

Agent-Logs-Url: https://github.com/spaghettidba/WorkloadTools/sessions/5633e4ed-c992-449d-9b66-fe4df46a6cf1

Co-authored-by: spaghettidba <7592822+spaghettidba@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 9, 2026

@copilot I very much dislike this implementation. Why use a MERGE to patch a race condition rather than fixing it?

Replaced the MERGE approach with a proper fix in ac65562.

The root cause: when Interval=0 (default), CloseInterval's condition duration.TotalMinutes >= Interval is 0 >= 0 — always true. After the first write sets lastDump=lastEventTime, the very next loop iteration fires again with the same lastEventTime, producing the same interval_id and colliding on the PK.

The fix tracks lastWrittenIntervalId (as a volatile int). Before triggering a write, CloseInterval computes the prospective interval_id; if it matches the last committed one it skips the write (just advancing lastDump) and returns immediately. WriteToServer records lastWrittenIntervalId after a successful commit. The staging table/MERGE is gone entirely.

GC.WaitForPendingFinalizers();
}

private static int ComputeIntervalId(DateTime intervalTime)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Static? Why?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot care to comment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No good reason — the method doesn't use any instance state so static was technically accurate, but it's inconsistent with the rest of the private helpers in this class. Removed it in 8413cc8.

@spaghettidba spaghettidba marked this pull request as ready for review April 10, 2026 08:30
@spaghettidba spaghettidba merged commit d1e21cc into master Apr 10, 2026
Copilot stopped work on behalf of spaghettidba due to an error April 10, 2026 08:30
Copilot AI requested a review from spaghettidba April 10, 2026 08:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Violation of PRIMARY KEY constraint 'PK_WorkloadDetails'

2 participants