Fix occasional 500 when reading unread forum topics #6714
Merged
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Closes #6691.
This has been a long-standing problem when reading unread forum topics. Occasionally, reading an unread forum topic will crash the browser because the server responds 500. Refreshing the tab will show the forum topic and correctly mark it as read.
The error comes from
topics_controller.rb:20
when@topic.mark_as_read!
. The raised error is anActiveRecord::RecordNotUnique
, and the full error is as follows.This error came about after ledermann/unread adds support for maintaining unique read marks in 2016 by ledermann/unread#78. A
unique: true
constraint is added to their migration, and that explains why the error above is originally raised by PostgreSQL.It is an issue for us now because there are 3 calls made to
topics_controller#show
at roughly the same time. Even though GET calls are supposed to be idempotent,topics_controller#show
is notably not idempotent because@topic.mark_as_read!
can fail. It fails because each request hasn't seen the read mark for the@topic
, and so attempts to create a new read mark for it. Since the database only allows synchronous operations, the first request's read mark creation succeeds, but the following requests' creations fail because the same read mark already exists, hence raising thePG::UniqueViolation
error. Sometimes it doesn't raise this error because the following requests take place strictly after the first request actually responds. Hence, occasional.You can replay this error by usurping ledermann/unread's
mark_as_read!
and removingif unread?(reader)
and changingrm
to onlyread_marks.build
so that it always tries to create a new read mark. Runningmark_as_read!
on a read forum topic now will always raiseActiveRecord::RecordNotUnique
.Solution
Notably, we need to ensure that GET calls are idempotent no matter what. This also means that
mark_as_read!
(with or without the bang) must always be idempotent. There's no reason whymark_as_read!
should fail if the read mark exists. It simply means that the readable is read.This PR approaches the problem in a non-disruptive manner by creating a new
SafeMarkAsReadConcern
that includes asafely_mark_as_read!
method.safely_mark_as_read!
will just runmark_as_read!
, but if it catchesActiveRecord::RecordNotUnique
, it will double-check if the item is indeed read already in the database (in 1-2 SQL calls). If not, it will re-raise the error, because that is unexpected.I'm not including
SafeMarkAsReadConcern
everywhere for now because this is rather a hack, and we haven't had complaints in other places so far. If we so believe that this concern is the right way to go, we can easily include it respectively in the future. That's why I wrote it this way, i.e., a model concern.