FIX: saving drafts unconditionally increases sequence

Previously we only changed sequence on ownership change, this
cause a race condition between tabs where user could type for a
long time without being warned of an out of date draft.

This change is a radical change and we should watch closely.

Code was already in place to track sequence on the client so no
changes are needed there.
This commit is contained in:
Sam Saffron 2020-05-12 16:55:24 +10:00
parent 451e9c4bb9
commit a29ae17d3a
No known key found for this signature in database
GPG Key ID: B9606168D2FFD9F5
5 changed files with 38 additions and 23 deletions

View File

@ -43,17 +43,17 @@ class Draft < ActiveRecord::Base
raise Draft::OutOfSequence
end
if owner && current_owner && current_owner != owner
sequence += 1
sequence += 1
DraftSequence.upsert({
sequence: sequence,
draft_key: key,
user_id: user.id,
},
unique_by: [:user_id, :draft_key]
)
end
# we need to keep upping our sequence on every save
# if we do not do that there are bad race conditions
DraftSequence.upsert({
sequence: sequence,
draft_key: key,
user_id: user.id,
},
unique_by: [:user_id, :draft_key]
)
DB.exec(<<~SQL, id: draft_id, sequence: sequence, data: data, owner: owner || current_owner)
UPDATE drafts
@ -337,7 +337,7 @@ end
# data :text not null
# created_at :datetime not null
# updated_at :datetime not null
# sequence :integer default(0), not null
# sequence :bigint default(0), not null
# revisions :integer default(1), not null
# owner :string
#

View File

@ -48,7 +48,7 @@ end
# id :integer not null, primary key
# user_id :integer not null
# draft_key :string not null
# sequence :integer not null
# sequence :bigint not null
#
# Indexes
#

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
class ChangeDraftSequenceToBigint < ActiveRecord::Migration[6.0]
def change
change_column :drafts, :sequence, :bigint, default: 0, null: false
change_column :draft_sequences, :sequence, :bigint, null: false
end
end

View File

@ -21,13 +21,13 @@ describe Draft do
random_key: "random"
}
Draft.set(user, "xyz", 0, draft.to_json)
seq = Draft.set(user, "xyz", 0, draft.to_json)
draft["reply"] = "test" * 100
half_grace = (SiteSetting.editing_grace_period / 2 + 1).seconds
freeze_time half_grace.from_now
Draft.set(user, "xyz", 0, draft.to_json)
seq = Draft.set(user, "xyz", seq, draft.to_json)
draft_post = BackupDraftPost.find_by(user_id: user.id, key: "xyz").post
@ -37,7 +37,7 @@ describe Draft do
# this should trigger a post revision as 10 minutes have passed
draft["reply"] = "hello"
Draft.set(user, "xyz", 0, draft.to_json)
Draft.set(user, "xyz", seq, draft.to_json)
draft_topic = BackupDraftTopic.find_by(user_id: user.id)
expect(draft_topic.topic.posts_count).to eq(2)
@ -58,9 +58,16 @@ describe Draft do
end
it "should overwrite draft data correctly" do
Draft.set(user, "test", 0, "data")
Draft.set(user, "test", 0, "new data")
expect(Draft.get(user, "test", 0)).to eq "new data"
seq = Draft.set(user, "test", 0, "data")
seq = Draft.set(user, "test", seq, "new data")
expect(Draft.get(user, "test", seq)).to eq "new data"
end
it "should increase the sequence on every save" do
seq = Draft.set(user, "test", 0, "data")
expect(seq).to eq(0)
seq = Draft.set(user, "test", 0, "data")
expect(seq).to eq(1)
end
it "should clear drafts on request" do
@ -227,7 +234,7 @@ describe Draft do
expect(draft_seq).to eq(1)
draft_seq = Draft.set(user, 'new_topic', 1, 'hello world', _owner = 'HIJKL')
expect(draft_seq).to eq(1)
expect(draft_seq).to eq(2)
end
it 'can correctly preload drafts' do

View File

@ -22,7 +22,7 @@ describe DraftController do
end
it "returns 404 when the key is missing" do
user = sign_in(Fabricate(:user))
_user = sign_in(Fabricate(:user))
post "/draft.json", params: { data: { my: "data" }.to_json, sequence: 0 }
expect(response.status).to eq(404)
end
@ -135,18 +135,18 @@ describe DraftController do
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["draft_sequence"]).to eq(1)
expect(json["draft_sequence"]).to eq(2)
post "/draft.json", params: {
draft_key: "abc",
sequence: 1,
sequence: 2,
data: { c: "test" }.to_json,
owner: "abc"
}
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["draft_sequence"]).to eq(2)
expect(json["draft_sequence"]).to eq(3)
end
it 'raises an error for out-of-sequence draft setting' do