Skip to content

Commit 0701ec0

Browse files
Dantemssmwvolo
andauthored
CORE-753 Make FindOrCreateApplicationGroups and FindOrCreateApplicationUser more resilient to concurrent calls (#1294)
* Attempt to fix some FindOrCreate routines * Added a bunch of preloading * Attempt to prevent the id sequence from autoincrementing too much * Added some early returns for FindOrCreateApplicationGroups --------- Co-authored-by: Michael Volo <[email protected]>
1 parent 78f8c44 commit 0701ec0

11 files changed

+134
-48
lines changed

app/representers/api/v1/application_group_representer.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module Api::V1
22
class ApplicationGroupRepresenter < Roar::Decorator
33
include Roar::JSON
44

5-
property :id,
5+
property :id,
66
type: Integer,
77
readable: true,
88
writeable: false,
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
require 'representable/json/collection'
22

33
module Api::V1
4-
module ApplicationGroupsRepresenter
4+
class ApplicationGroupsRepresenter < Roar::Decorator
55
include Representable::JSON::Collection
66

77
items class: ApplicationGroup, decorator: ApplicationGroupRepresenter
8+
9+
def to_hash(options = {})
10+
# Avoid N+1 load on application_groups.group
11+
ActiveRecord::Associations::Preloader.new.preload represented, group: [
12+
{ group_members: :user },
13+
{ group_owners: :user },
14+
:member_group_nestings
15+
]
16+
17+
super(options)
18+
end
819
end
920
end

app/representers/api/v1/application_user_application_representer.rb

+7-7
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ class ApplicationUserApplicationRepresenter < Roar::Decorator
2323
}
2424

2525
collection :roles,
26-
type: String,
27-
readable: true,
28-
writeable: false,
29-
schema_info: {
30-
required: true,
31-
description: "The User's roles in this application"
32-
}
26+
type: String,
27+
readable: true,
28+
writeable: false,
29+
schema_info: {
30+
required: true,
31+
description: "The User's roles in this application"
32+
}
3333
end
3434
end
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
require 'representable/json/collection'
22

33
module Api::V1
4-
module ApplicationUsersRepresenter
4+
class ApplicationUsersRepresenter < Roar::Decorator
55
include Representable::JSON::Collection
66

77
items class: ApplicationUser, decorator: ApplicationUserRepresenter
8+
9+
def to_hash(options = {})
10+
# Avoid N+1 load on application_users.user
11+
ActiveRecord::Associations::Preloader.new.preload(
12+
represented.to_a, user: { application_users: :application }
13+
)
14+
15+
super(options)
16+
end
817
end
918
end
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
require 'representable/json/collection'
22

33
module Api::V1
4-
module GroupMembersRepresenter
4+
class GroupMembersRepresenter < Roar::Decorator
55
include Representable::JSON::Collection
66

77
items class: GroupMember, decorator: GroupMemberRepresenter
8+
9+
def to_hash(options = {})
10+
# Avoid N+1 load on group_members.group
11+
ActiveRecord::Associations::Preloader.new.preload(
12+
represented.to_a, group: [
13+
{ group_members: { user: { application_users: :application } } },
14+
{ group_owners: { user: { application_users: :application } } },
15+
:member_group_nestings
16+
]
17+
)
18+
19+
super(options)
20+
end
821
end
922
end
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
require 'representable/json/collection'
22

33
module Api::V1
4-
module GroupOwnersRepresenter
4+
class GroupOwnersRepresenter < Roar::Decorator
55
include Representable::JSON::Collection
66

77
items class: GroupOwner, decorator: GroupOwnerRepresenter
8+
9+
def to_hash(options = {})
10+
# Avoid N+1 load on group_members.group
11+
ActiveRecord::Associations::Preloader.new.preload(
12+
represented.to_a, group: [
13+
{ group_members: { user: { application_users: :application } } },
14+
{ group_owners: { user: { application_users: :application } } },
15+
:member_group_nestings
16+
]
17+
)
18+
19+
super(options)
20+
end
821
end
922
end

app/representers/api/v1/group_representer.rb

+13-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module Api::V1
22
class GroupRepresenter < Roar::Decorator
33
include Roar::JSON
44

5-
property :id,
5+
property :id,
66
type: Integer,
77
readable: true,
88
writeable: false,
@@ -78,5 +78,17 @@ class GroupRepresenter < Roar::Decorator
7878
description: "The ID's of all members of groups nested in this group's subtree, including this one; For membership checking purposes"
7979
}
8080

81+
def to_hash(options = {})
82+
# Avoid N+1 load on group_members.user and group_owners.user
83+
ActiveRecord::Associations::Preloader.new.preload(
84+
represented.group_members.to_a, user: { application_users: :application }
85+
)
86+
ActiveRecord::Associations::Preloader.new.preload(
87+
represented.group_owners.to_a, user: { application_users: :application }
88+
)
89+
90+
super(options)
91+
end
92+
8193
end
8294
end
+14-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
require 'representable/json/collection'
22

33
module Api::V1
4-
module GroupsRepresenter
4+
class GroupsRepresenter < Roar::Decorator
55
include Representable::JSON::Collection
66

77
items class: Group, decorator: GroupRepresenter
8+
9+
def to_hash(options = {})
10+
# Avoid N+1 load on groups
11+
ActiveRecord::Associations::Preloader.new.preload(
12+
represented.to_a, [
13+
{ group_members: { user: { application_users: :application } } },
14+
{ group_owners: { user: { application_users: :application } } },
15+
:member_group_nestings
16+
]
17+
)
18+
19+
super(options)
20+
end
821
end
922
end

app/representers/api/v1/user_search_representer.rb

+7
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,12 @@ class UserSearchRepresenter < OpenStax::Api::V1::AbstractSearchRepresenter
1919
description: "The Users matching the query or a subset thereof when paginating"
2020
}
2121

22+
def to_hash(options = {})
23+
# Avoid N+1 load on items
24+
ActiveRecord::Associations::Preloader.new.preload represented.items.to_a, application_users: :application
25+
26+
super(options)
27+
end
28+
2229
end
2330
end
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,36 @@
11
# Finds or creates ApplicationGroups for the given application and group_ids.
22
class FindOrCreateApplicationGroups
3-
4-
lev_routine
3+
lev_routine transaction: :read_committed
54

65
protected
76

87
def exec(application_id, group_ids)
9-
application_groups = ApplicationGroup.where(application_id: application_id,
10-
group_id: group_ids)
11-
.preload(:group).to_a
12-
ag_group_ids = application_groups.collect{|ag| ag.group_id}
8+
outputs.application_groups = ApplicationGroup.where(
9+
application_id: application_id, group_id: group_ids
10+
).to_a
1311

14-
# There might be a way to make this more efficient
15-
group_ids.each do |group_id|
16-
unless ag_group_ids.include?(group_id)
17-
application_group = ApplicationGroup.create do |app_group|
18-
app_group.application_id = application_id
19-
app_group.group_id = group_id
20-
app_group.save!
21-
end
22-
application_groups << application_group
23-
end
24-
end
12+
missing_group_ids = group_ids - outputs.application_groups.map(&:group_id)
13+
return if missing_group_ids.empty?
2514

26-
outputs[:application_groups] = application_groups
27-
end
15+
# Insert missing records
16+
# Returns newly-inserted records only
17+
# We could run this as the first query, but it would cause the id sequence to autoincrement
18+
# by the number of group_ids every time this routine is called
19+
# We can still do that if we switch the primary key to a uuid column
20+
new_records = ApplicationGroup.import(
21+
missing_group_ids.map do |group_id|
22+
ApplicationGroup.new(application_id: application_id, group_id: group_id)
23+
end, on_duplicate_key_ignore: true
24+
).results
25+
outputs.application_groups += new_records
2826

27+
existing_group_ids = missing_group_ids - new_records.map(&:group_id)
28+
return if existing_group_ids.empty?
29+
30+
# Run the first query again in case another process or thread
31+
# inserted the missing records before us
32+
outputs.application_groups += ApplicationGroup.where(
33+
application_id: application_id, group_id: existing_group_ids
34+
).to_a
35+
end
2936
end
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
# Finds or creates an ApplicationUser for the given application and user.
22
class FindOrCreateApplicationUser
3-
4-
lev_routine
3+
lev_routine transaction: :read_committed
54

65
protected
76

87
def exec(application_id, user_id)
9-
application_user = ApplicationUser.where(:application_id => application_id,
10-
:user_id => user_id).first
11-
unless application_user
12-
application_user = ApplicationUser.create do |app_user|
13-
app_user.application_id = application_id
14-
app_user.user_id = user_id
15-
app_user.save!
16-
end
17-
end
18-
19-
outputs[:application_user] = application_user
8+
# First attempt to query the record
9+
# If no record found, attempt to insert it record, ignoring conflicts
10+
# If no record is inserted, query the existing record again
11+
# The first query is necessary to prevent the id sequence from autoincrementing
12+
# every time this routine is called
13+
# We can remove it if we switch the primary key to a uuid column
14+
outputs.application_user = ApplicationUser.find_by(
15+
application_id: application_id, user_id: user_id
16+
) || ApplicationUser.import(
17+
[ ApplicationUser.new(application_id: application_id, user_id: user_id) ],
18+
on_duplicate_key_ignore: true
19+
).results.first || ApplicationUser.find_by(
20+
application_id: application_id, user_id: user_id
21+
)
2022
end
21-
22-
end
23+
end

0 commit comments

Comments
 (0)