Skip to content

Commit 00b8bc8

Browse files
committed
Use an advisory lock to prevent concurrent saves
1 parent aaee875 commit 00b8bc8

5 files changed

+57
-62
lines changed

Gemfile

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ gem "rack-cors", require: "rack/cors"
2323
gem "rails", "~> 6.0"
2424
gem "sass-rails", "~> 6.0"
2525
gem "secure_headers", ">= 3.0"
26+
gem "with_advisory_lock"
2627

2728
group :production, :staging do
2829
gem "net-pop"

Gemfile.lock

+4
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,9 @@ GEM
446446
websocket-driver (0.7.6)
447447
websocket-extensions (>= 0.1.0)
448448
websocket-extensions (0.1.5)
449+
with_advisory_lock (5.1.0)
450+
activerecord (>= 6.1)
451+
zeitwerk (>= 2.6)
449452
xpath (3.2.0)
450453
nokogiri (~> 1.8)
451454
zeitwerk (2.6.13)
@@ -502,6 +505,7 @@ DEPENDENCIES
502505
standard
503506
timecop
504507
web-console (~> 4.1)
508+
with_advisory_lock
505509

506510
RUBY VERSION
507511
ruby 3.3.1p55

app/models/measure_category.rb

+21-25
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,18 @@ class MeasureCategory < VersionedRecord
1414
# replacing "save": before creating the record we need to remove all
1515
# previous relationships (there should only be one) between the measure
1616
# and other categories of the same taxonomy
17-
# also we need to lock the measure to ensure that no other category is associated
17+
# also we need an advisory lock to ensure that no other category is associated during the transaction
1818
def save_with_cleanup
19-
with_locked_measure do
19+
if multiple_categories_allowed_for_taxonomy?
20+
return save!
21+
end
22+
23+
self.class.with_advisory_lock("MeasureCategory-measure_id_#{measure_id}-taxonomy_id_#{category.taxonomy_id}") do
2024
transaction do
21-
if category && category.taxonomy&.allow_multiple == false
22-
self.class.where(
23-
category_id: category.taxonomy.categories.where.not(id: category.id).pluck(:id),
24-
measure_id: measure_id
25-
).destroy_all
26-
end
25+
self.class.where(
26+
category_id: category.taxonomy.categories.where.not(id: category.id).pluck(:id),
27+
measure_id: measure_id
28+
).destroy_all
2729

2830
save!
2931
end
@@ -32,6 +34,10 @@ def save_with_cleanup
3234

3335
private
3436

37+
def multiple_categories_allowed_for_taxonomy?
38+
!category&.taxonomy || category&.taxonomy&.allow_multiple
39+
end
40+
3541
def set_relationship_updated
3642
if measure && !measure.destroyed?
3743
measure.update_column(:relationship_updated_at, Time.zone.now)
@@ -40,25 +46,15 @@ def set_relationship_updated
4046
end
4147

4248
def single_category_per_taxonomy
43-
if category&.taxonomy && !category.taxonomy.allow_multiple
44-
existing_categories = self.class.where(
45-
category_id: category.taxonomy.categories.pluck(:id), # Ensure you're using IDs here
46-
measure_id: measure_id
47-
)
49+
return if multiple_categories_allowed_for_taxonomy?
4850

49-
if existing_categories.count >= 1
50-
errors.add(:category, "This measure already has a category in the same taxonomy. Multiple categories are not allowed for the taxonomy.")
51-
end
52-
end
53-
end
51+
existing_categories = self.class.where(
52+
category_id: category.taxonomy.categories.pluck(:id), # Ensure you're using IDs here
53+
measure_id: measure_id
54+
)
5455

55-
def with_locked_measure
56-
if measure
57-
measure.with_lock do
58-
yield
59-
end
60-
else
61-
yield
56+
if existing_categories.count >= 1
57+
errors.add(:category, "This measure already has a category in the same taxonomy. Multiple categories are not allowed for the taxonomy.")
6258
end
6359
end
6460
end

app/models/recommendation_category.rb

+13-17
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ class RecommendationCategory < VersionedRecord
1616
# and other categories of the same taxonomy
1717
# also we need to lock the recommendation to ensure that no other category is associated
1818
def save_with_cleanup
19-
with_locked_recommendation do
19+
if multiple_categories_allowed_for_taxonomy?
20+
return save!
21+
end
22+
23+
self.class.with_advisory_lock("RecommendationCategory-recommendation_id_#{recommendation_id}-taxonomy_id_#{category.taxonomy_id}") do
2024
transaction do
21-
if category && category.taxonomy&.allow_multiple == false
22-
self.class.where(
23-
category_id: category.taxonomy.categories.where.not(id: category.id).pluck(:id),
24-
recommendation_id: recommendation_id
25-
).destroy_all
26-
end
25+
self.class.where(
26+
category_id: category.taxonomy.categories.where.not(id: category.id).pluck(:id),
27+
recommendation_id: recommendation_id
28+
).destroy_all
2729

2830
save!
2931
end
@@ -32,6 +34,10 @@ def save_with_cleanup
3234

3335
private
3436

37+
def multiple_categories_allowed_for_taxonomy?
38+
!category&.taxonomy || category&.taxonomy&.allow_multiple
39+
end
40+
3541
def set_relationship_updated
3642
if recommendation && !recommendation.destroyed?
3743
recommendation.update_column(:relationship_updated_at, Time.zone.now)
@@ -51,14 +57,4 @@ def single_category_per_taxonomy
5157
end
5258
end
5359
end
54-
55-
def with_locked_recommendation
56-
if recommendation
57-
recommendation.with_lock do
58-
yield
59-
end
60-
else
61-
yield
62-
end
63-
end
6460
end

app/models/user_category.rb

+18-20
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ class UserCategory < ApplicationRecord
1212
# replacing "save": before creating the record we need to remove all
1313
# previous relationships (there should only be one) between the user
1414
# and other categories of the same taxonomy
15-
# also we need to lock the user to ensure that no other category is associated
15+
# also we need an advisory lock to ensure that no other category is associated during the transaction
1616
def save_with_cleanup
17-
with_locked_user do
17+
if multiple_categories_allowed_for_taxonomy?
18+
return save!
19+
end
20+
21+
self.class.with_advisory_lock("UserCategory-user_id_#{user_id}-taxonomy_id_#{category.taxonomy_id}") do
1822
transaction do
1923
if category && category.taxonomy&.allow_multiple == false
2024
self.class.where(
@@ -30,6 +34,10 @@ def save_with_cleanup
3034

3135
private
3236

37+
def multiple_categories_allowed_for_taxonomy?
38+
!category&.taxonomy || category&.taxonomy&.allow_multiple
39+
end
40+
3341
def set_relationship_updated
3442
if user && !user.destroyed?
3543
user.update_column(:relationship_updated_at, Time.zone.now)
@@ -38,25 +46,15 @@ def set_relationship_updated
3846
end
3947

4048
def single_category_per_taxonomy
41-
if category&.taxonomy && !category.taxonomy.allow_multiple
42-
existing_categories = self.class.where(
43-
category_id: category.taxonomy.categories.pluck(:id), # Ensure you're using IDs here
44-
user_id: user_id
45-
)
46-
47-
if existing_categories.count >= 1
48-
errors.add(:category, "This user already has a category in the same taxonomy. Multiple categories are not allowed for the taxonomy.")
49-
end
50-
end
51-
end
49+
return if multiple_categories_allowed_for_taxonomy?
5250

53-
def with_locked_user
54-
if user
55-
user.with_lock do
56-
yield
57-
end
58-
else
59-
yield
51+
existing_categories = self.class.where(
52+
category_id: category.taxonomy.categories.pluck(:id), # Ensure you're using IDs here
53+
user_id: user_id
54+
)
55+
56+
if existing_categories.count >= 1
57+
errors.add(:category, "This user already has a category in the same taxonomy. Multiple categories are not allowed for the taxonomy.")
6058
end
6159
end
6260
end

0 commit comments

Comments
 (0)