Skip to content

Fix race condition in ViewCount with atomic SQL updates#193

Closed
Copilot wants to merge 3 commits intomainfrom
copilot/fix-views-counter-race-condition
Closed

Fix race condition in ViewCount with atomic SQL updates#193
Copilot wants to merge 3 commits intomainfrom
copilot/fix-views-counter-race-condition

Conversation

Copy link

Copilot AI commented Jan 18, 2026

The ViewCount module's read-modify-write pattern causes lost updates when multiple users view the same content simultaneously. An editor modifying content while others view it can also lose changes.

Changes

Modified ViewCountService:

  • Inject ISession to access database connection
  • Implement IncrementViewCountAtomicallyAsync() using database-specific JSON functions
  • Support SQL Server (JSON_MODIFY), SQLite (json_set), MySQL (JSON_SET), and PostgreSQL (jsonb_set)

Dependencies:

  • Add Dapper.StrongName for parameterized SQL execution

Implementation

// Before: Race condition
contentItem.Content.ViewCountPart.Count = ++count;
await contentManager.UpdateAsync(contentItem);

// After: Atomic increment at database level
sql = $@"UPDATE {table}
         SET {column} = JSON_MODIFY({column}, '$.ViewCountPart.Count',
             CAST(ISNULL(JSON_VALUE({column}, '$.ViewCountPart.Count'), '0') AS INT) + 1)
         WHERE JSON_VALUE({column}, '$.ContentItemId') = @ContentItemId";
         
await connection.ExecuteAsync(commandDefinition);

The increment now executes atomically within YesSql's transaction context, eliminating concurrent update conflicts.

Original prompt

This section details on the original issue you should resolve

<issue_title>Views Counter raise condition</issue_title>
<issue_description>@hishamco I came across this on LinkedIn and took a look at the code out of curiosity. One thing to be aware of is that this approach can fail when more than one user is viewing the same article at the same time. There’s a race condition between updates.

This becomes an even bigger issue when you have many concurrent readers and someone editing the article at the same time—an editor could potentially lose their changes while others are viewing the content due to the same race condition.

In my own implementation, I also track views, but I handle it differently by using a ShapeTableProvider and executing a raw SQL update to increment the counter atomically, which avoids these concurrency issues.

Just wanted to share this in case it’s helpful.</issue_description>

Comments on the Issue (you are @copilot in this section)

@hishamco Thanks for your feedback @MikeAlhayek I might prefer to go without `ShapeTableProvider`. I will try to create a unit test to simulate the case, if it's possible, then I will do a fix

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits January 18, 2026 09:10
Co-authored-by: hishamco <3237266+hishamco@users.noreply.github.com>
…syntax

Co-authored-by: hishamco <3237266+hishamco@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix views counter raise condition in article viewing Fix race condition in ViewCount with atomic SQL updates Jan 18, 2026
Copilot AI requested a review from hishamco January 18, 2026 09:21
@MikeAlhayek
Copy link
Contributor

@hishamco This PR is even worse. Which AI model was used to generate it? Because this is not a good PR.

The PR directly updates the Document record by scanning the JSON document, which is slow, doesn’t solve the underlying problem, and ends up updating every version of the document on every view.

The only approach I see working efficiently here is to introduce a dedicated table for tracking views. Every time a user clicks on a content item, insert a new record into this table (e.g., ContentItemViewTracker with ContentItemId, UserId, DateTime).

Then, add a background service that runs every 10 minutes to aggregate the data and update a stats table (e.g., ContentItemViewTrackerStats with ContentItemId, Total). This way, view tracking is non-blocking since it’s just an insert, and the aggregation happens asynchronously in the background, avoiding any impact on production traffic. Yes, this means the counter won’t update immediately, but it’s a safe and scalable approach for a production application.

If an immediate update is required, an alternative is to skip the raw tracking table and upsert directly into ContentItemViewTrackerStats: insert a new record when a ContentItemId is viewed for the first time, or increment the Total column if it already exists.

@hishamco
Copy link
Member

hishamco commented Jan 18, 2026

This PR is even worse. Which AI model was used to generate it? Because this is not a good PR.

Don't care, I thought he would give me a clue :)

The only approach I see working efficiently here is to introduce a dedicated table for tracking views. Every time a user clicks on a content item, insert a new record into this table (e.g., ContentItemViewTracker with ContentItemId, UserId, DateTime).

This is HUGE!!

If an immediate update is required, an alternative is to skip the raw tracking table and upsert directly into ContentItemViewTrackerStats: insert a new record when a ContentItemId is viewed for the first time, or increment the Total column if it already exists.

This makes sense, but again, how does the OC avoid a race condition while editing a content item, which is a common use case while editing by multiple authors, @sebastienros? I think you talked about this a long time back, right?

@sebastienros
Copy link

What is the goal? Prevent multiple editors (humans) from editing the same content item concurrently?

@hishamco
Copy link
Member

Yes, in my case, avoid multiple users accessing the same content item, which modifies the ViewCountPart behind the scene

@sebastienros
Copy link

So I think like @MikeAlhayek said this should use a dedicated table (or document, though not easier at all). This would represent actual locks, who owns it, and when it was acquired (so it can be timed out). Then the UI would just show that the editor is acquired in exclusive mode, or for others that it can't be updated for now, ideally with polling (or server-side events) so it can detect when it's free again. Or when the lock has timed out. The same way and actual editor should ping the server to keep the session alive, to show it's still in edit mode. If not the user loses its lock (if there is someone else requesting it, no need if there is not other editor waiting).

@hishamco
Copy link
Member

Thanks both for your input, I will check what I do in my case because of introducing a table, there's no need for the ContentPart

@hishamco
Copy link
Member

Closing this and updating the other PR

@hishamco hishamco closed this Jan 18, 2026
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.

Views Counter raise condition

4 participants