Skip to content

Commit a7b2ef3

Browse files
committed
Add optimistic lock for concurrent insertion
1 parent 921babf commit a7b2ef3

11 files changed

+130
-80
lines changed

Gemfile.lock

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
GIT
22
remote: https://github.com/rails/rails.git
3-
revision: 2ab0094ab6e94d30a5a424dc93c2aeb1066cc76b
3+
revision: 95deab7b439abba23fdc4bd659116dab5dbe2606
44
branch: main
55
specs:
66
actioncable (8.1.0.alpha)
@@ -127,7 +127,7 @@ GEM
127127
concurrent-ruby (1.3.4)
128128
connection_pool (2.4.1)
129129
crass (1.0.6)
130-
date (3.4.0)
130+
date (3.4.1)
131131
debug (1.9.2)
132132
irb (~> 1.10)
133133
reline (>= 0.3.8)
@@ -144,11 +144,11 @@ GEM
144144
activesupport (>= 6.1)
145145
i18n (1.14.6)
146146
concurrent-ruby (~> 1.0)
147-
io-console (0.7.2)
147+
io-console (0.8.0)
148148
irb (1.14.1)
149149
rdoc (>= 4.0.0)
150150
reline (>= 0.4.2)
151-
json (2.8.2)
151+
json (2.9.0)
152152
kamal (2.3.0)
153153
activesupport (>= 7.0)
154154
base64 (~> 0.2)
@@ -162,7 +162,7 @@ GEM
162162
zeitwerk (>= 2.6.18, < 3.0)
163163
keccak (1.3.1)
164164
language_server-protocol (3.17.0.3)
165-
logger (1.6.1)
165+
logger (1.6.2)
166166
loofah (2.23.1)
167167
crass (~> 1.0.2)
168168
nokogiri (>= 1.12.0)
@@ -175,7 +175,7 @@ GEM
175175
matrix (0.4.2)
176176
mini_mime (1.1.5)
177177
mini_portile2 (2.8.8)
178-
minitest (5.25.2)
178+
minitest (5.25.4)
179179
msgpack (1.7.5)
180180
net-imap (0.5.1)
181181
date
@@ -192,27 +192,28 @@ GEM
192192
net-protocol
193193
net-ssh (7.3.0)
194194
nio4r (2.7.4)
195-
nokogiri (1.16.7-aarch64-linux)
195+
nokogiri (1.16.8-aarch64-linux)
196196
racc (~> 1.4)
197-
nokogiri (1.16.7-arm-linux)
197+
nokogiri (1.16.8-arm-linux)
198198
racc (~> 1.4)
199-
nokogiri (1.16.7-arm64-darwin)
199+
nokogiri (1.16.8-arm64-darwin)
200200
racc (~> 1.4)
201-
nokogiri (1.16.7-x86-linux)
201+
nokogiri (1.16.8-x86-linux)
202202
racc (~> 1.4)
203-
nokogiri (1.16.7-x86_64-darwin)
203+
nokogiri (1.16.8-x86_64-darwin)
204204
racc (~> 1.4)
205-
nokogiri (1.16.7-x86_64-linux)
205+
nokogiri (1.16.8-x86_64-linux)
206206
racc (~> 1.4)
207207
ostruct (0.6.1)
208-
pagy (9.3.1)
208+
pagy (9.3.2)
209209
parallel (1.26.3)
210210
parser (3.3.6.0)
211211
ast (~> 2.4.1)
212212
racc
213213
pg (1.5.9)
214214
pkg-config (1.5.8)
215-
psych (5.2.0)
215+
psych (5.2.1)
216+
date
216217
stringio
217218
public_suffix (6.0.1)
218219
puma (6.5.0)
@@ -230,9 +231,9 @@ GEM
230231
activesupport (>= 5.0.0)
231232
minitest
232233
nokogiri (>= 1.6)
233-
rails-html-sanitizer (1.6.0)
234+
rails-html-sanitizer (1.6.1)
234235
loofah (~> 2.21)
235-
nokogiri (~> 1.14)
236+
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
236237
rainbow (3.1.1)
237238
rake (13.2.1)
238239
rbsecp256k1 (6.0.0)
@@ -241,18 +242,18 @@ GEM
241242
rubyzip (~> 2.3)
242243
rdoc (6.8.1)
243244
psych (>= 4.0.0)
244-
regexp_parser (2.9.2)
245-
reline (0.5.11)
245+
regexp_parser (2.9.3)
246+
reline (0.5.12)
246247
io-console (~> 0.5)
247248
rexml (3.3.9)
248-
rubocop (1.69.0)
249+
rubocop (1.69.1)
249250
json (~> 2.3)
250251
language_server-protocol (>= 3.17.0)
251252
parallel (~> 1.10)
252253
parser (>= 3.3.0.2)
253254
rainbow (>= 2.2.2, < 4.0)
254-
regexp_parser (>= 2.4, < 3.0)
255-
rubocop-ast (>= 1.36.1, < 2.0)
255+
regexp_parser (>= 2.9.3, < 3.0)
256+
rubocop-ast (>= 1.36.2, < 2.0)
256257
ruby-progressbar (~> 1.7)
257258
unicode-display_width (>= 2.4.0, < 4.0)
258259
rubocop-ast (1.36.2)
@@ -275,7 +276,7 @@ GEM
275276
rubocop-rails
276277
ruby-progressbar (1.13.0)
277278
rubyzip (2.3.2)
278-
securerandom (0.3.2)
279+
securerandom (0.4.0)
279280
selenium-webdriver (4.27.0)
280281
base64 (~> 0.2)
281282
logger (~> 1.4)
@@ -318,7 +319,7 @@ GEM
318319
unicode-emoji (~> 4.0, >= 4.0.4)
319320
unicode-emoji (4.0.4)
320321
uri (1.0.2)
321-
useragent (0.16.10)
322+
useragent (0.16.11)
322323
web-console (4.2.1)
323324
actionview (>= 6.0.0)
324325
activemodel (>= 6.0.0)

app/controllers/api/events_controller.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ def create
4444
}
4545
}, status: :unprocessable_content
4646
end
47+
rescue ActiveRecord::StaleObjectError
48+
render json: {
49+
status: "error",
50+
error: {
51+
message: "Event not saved",
52+
data: "Too fast requests."
53+
}
54+
}, status: :unprocessable_content
4755
end
4856

4957
def batch_create
@@ -60,6 +68,8 @@ def batch_create
6068
errored = event_params
6169
break
6270
end
71+
rescue ActiveRecord::StaleObjectError
72+
errored = event_params
6373
end
6474

6575
render json: {

app/models/conversation.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
class Conversation < ApplicationRecord
4+
self.primary_key = %i[pubkey session]
5+
self.lock_optimistically = true
6+
7+
has_many :events,
8+
foreign_key: %i[pubkey session],
9+
dependent: :restrict_with_exception
10+
11+
def latest_event
12+
events.order(id: :desc).first
13+
end
14+
end

app/models/event.rb

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,17 @@ class Event < ApplicationRecord
88
scope :of_topic, ->(topic) { where(topic:) }
99
scope :of_recipient, ->(recipient) { where(recipient:) }
1010

11+
belongs_to :conversation, foreign_key: %i[pubkey session], required: true, autosave: true
1112
has_one :merkle_node, dependent: :restrict_with_exception
1213

13-
after_create :add_to_merkle_tree
14+
# after_create :add_to_merkle_tree
1415

1516
# A publisher must not send events in the same time which makes harder to sort them.
1617
validates :created_at,
17-
uniqueness: {
18-
scope: :pubkey
19-
},
18+
presence: true,
2019
comparison: {
2120
greater_than_or_equal_to: ->(current) {
22-
current.latest&.created_at || 0
21+
current.conversation&.latest_event_created_at || 0
2322
}
2423
}
2524

@@ -37,14 +36,20 @@ class Event < ApplicationRecord
3736
format: { with: /\A\h+\z/ },
3837
allow_nil: true
3938

40-
before_validation do
39+
before_validation on: :create do
4140
self.session = tags.find { |tag| tag[0] == "s" }&.[](1)
4241
self.topic = tags.find { |tag| tag[0] == "t" }&.[](1)
4342
self.recipient = tags.find { |tag| tag[0] == "p" }&.[](1)
43+
44+
self.conversation ||= Conversation.find_or_create_by(pubkey: pubkey, session: session) do |c|
45+
c.latest_event_created_at = created_at
46+
c.events_count = 0
47+
end
4448
end
4549

46-
def readonly?
47-
persisted?
50+
after_validation on: :create do
51+
self.conversation.latest_event_created_at = created_at
52+
self.conversation.events_count += 1
4853
end
4954

5055
def merkle_tree_hash
@@ -63,19 +68,8 @@ def inclusion_proof
6368
merkle_node&.inclusion_proof
6469
end
6570

66-
def latest
67-
@latest ||=
68-
Event
69-
.of_topic(topic)
70-
.of_pubkey(pubkey)
71-
.of_session(session)
72-
.order(id: :desc)
73-
.first
74-
end
75-
76-
def reload(options = nil)
77-
@latest = nil
78-
super
71+
def readonly?
72+
persisted?
7973
end
8074

8175
class << self

config/application.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,12 @@ class Application < Rails::Application
3737
#
3838
# config.time_zone = "Central Time (US & Canada)"
3939
# config.eager_load_paths << Rails.root.join("extras")
40+
41+
config.i18n.load_path += Dir[Rails.root.join("config", "locales", "**", "*.{rb,yml}")]
42+
43+
config.generators do |g|
44+
g.helper false
45+
g.assets false
46+
end
4047
end
4148
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
class CreateConversations < ActiveRecord::Migration[8.1]
4+
def change
5+
create_table :conversations, primary_key: %i[pubkey session] do |t|
6+
t.string :pubkey, null: false
7+
t.string :session, null: false
8+
9+
t.integer :lock_version
10+
t.datetime :latest_event_created_at
11+
t.integer :events_count
12+
13+
t.timestamps
14+
end
15+
end
16+
end
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
class CreateEvents < ActiveRecord::Migration[8.0]
3+
class CreateEvents < ActiveRecord::Migration[8.1]
44
def change
55
create_table :events do |t|
66
t.string :eid, null: false, index: { unique: true }
@@ -10,9 +10,7 @@ def change
1010
t.string :content, null: false
1111
t.string :sig, null: false
1212

13-
t.datetime :created_at, null: false
14-
15-
t.index %i[pubkey created_at], unique: true
13+
t.datetime :created_at, null: false, index: true
1614
end
1715
end
1816
end

db/migrate/20240325200102_add_extended_fields_to_events.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# frozen_string_literal: true
22

3-
class AddExtendedFieldsToEvents < ActiveRecord::Migration[8.0]
3+
class AddExtendedFieldsToEvents < ActiveRecord::Migration[8.1]
44
def change
5-
change_table :events, id: :string do |t|
5+
change_table :events do |t|
66
t.string :session, null: false
77

88
t.string :topic, null: true, index: true

db/migrate/20240325200103_create_merkle_nodes.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
class CreateMerkleNodes < ActiveRecord::Migration[8.0]
3+
class CreateMerkleNodes < ActiveRecord::Migration[8.1]
44
def change
55
# 创建顺序:parent to children
66
create_table :merkle_nodes do |t|

db/schema.rb

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)