Skip to content

Commit caf83a2

Browse files
committed
Batch parent relationship removal for improved performance
Previously, bulk parent removal processed all relationships in a single job, which could timeout for large imports. Now the form enqueues multiple jobs, each processing a batch of 100 parent relationships. Each batch job: - Identifies parent relationships to remove - Invalidates associated consents (if remove_option is "all") - Destroys the identified relationships - Cleans up orphaned parents - Checks if all jobs are complete and updates import status
1 parent 58fbc79 commit caf83a2

6 files changed

Lines changed: 106 additions & 97 deletions

File tree

app/controllers/imports/bulk_remove_parents_controller.rb

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,7 @@ def set_import
5151
end
5252

5353
def set_consents
54-
@consents =
55-
Consent
56-
.includes(patient: { parent_relationships: :parent })
57-
.joins(patient: :parent_relationships)
58-
.merge(@import.patients)
59-
.merge(@import.parent_relationships)
60-
.where("consents.parent_id = parent_relationships.parent_id")
61-
.not_invalidated
54+
@consents = @import.parent_relationship_consents
6255
end
6356

6457
def success_flash_text

app/forms/bulk_remove_parents_form.rb

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ class BulkRemoveParentsForm
44
include ActiveModel::Model
55
include ActiveModel::Attributes
66

7+
BATCH_SIZE = 100
8+
79
attr_accessor :current_user
810
attr_reader :import, :consents
911

@@ -25,12 +27,18 @@ def initialize(import:, consents:, current_user:, **attributes)
2527
def save!
2628
return false unless valid?
2729
import.update!(status: :removing_parent_relationships)
28-
BulkRemoveParentRelationshipsJob.perform_later(
29-
import.to_global_id.to_s,
30-
consents.map(&:id),
31-
current_user.id,
32-
remove_option
33-
)
30+
31+
import
32+
.parent_relationship_ids
33+
.each_slice(BATCH_SIZE) do |batch_ids|
34+
BulkRemoveParentRelationshipsJob.perform_later(
35+
import.to_global_id.to_s,
36+
batch_ids,
37+
current_user.id,
38+
remove_option
39+
)
40+
end
41+
3442
true
3543
end
3644

app/jobs/bulk_remove_parent_relationships_job.rb

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,71 @@
33
class BulkRemoveParentRelationshipsJob < ApplicationJob
44
queue_as :imports
55

6-
def perform(import_global_id, consent_ids, user_id, remove_option)
6+
def perform(
7+
import_global_id,
8+
parent_relationship_ids_batch,
9+
user_id,
10+
remove_option
11+
)
712
import = GlobalID::Locator.locate(import_global_id)
8-
consents = Consent.where(id: consent_ids)
9-
10-
if remove_option == "unconsented_only"
11-
import.destroy_parent_relationships_without_consent!(consents)
12-
else
13-
import.destroy_parent_relationships_and_invalidate_consents!(
14-
User.find(user_id),
15-
consents
16-
)
13+
user = User.find(user_id)
14+
15+
ActiveRecord::Base.transaction do
16+
parent_relationships =
17+
import
18+
.parent_relationships
19+
.where(id: parent_relationship_ids_batch)
20+
.includes(:parent, :patient)
21+
22+
consents =
23+
import.parent_relationship_consents(scope: parent_relationships)
24+
25+
parent_relationships_to_remove =
26+
if remove_option == "unconsented_only"
27+
consented_pairs = consents.pluck(:patient_id, :parent_id).to_set
28+
29+
parent_relationships.reject do |pr|
30+
consented_pairs.include?([pr.patient_id, pr.parent_id])
31+
end
32+
else
33+
parent_relationships
34+
end
35+
36+
invalidate_consents!(user, consents) if remove_option == "all"
37+
38+
parents_to_check = parent_relationships_to_remove.map(&:parent)
39+
patient_ids = parent_relationships_to_remove.map(&:patient_id)
40+
41+
parent_relationships_to_remove.each(&:destroy!)
42+
43+
parents_to_check.each do |parent|
44+
next if parent.destroyed?
45+
next if parent.parent_relationships.exists?
46+
next if parent.consents.exists?
47+
48+
parent.destroy!
49+
end
50+
51+
StatusUpdaterJob.perform_bulk(patient_ids.zip)
1752
end
1853

54+
mark_complete_if_finished(import, remove_option)
55+
end
56+
57+
private
58+
59+
def invalidate_consents!(user, consents)
60+
timestamp = Time.current.to_fs(:long)
61+
invalidation_note =
62+
"Consent invalidated on #{timestamp} " \
63+
"because #{user.full_name} removed all parent-child relationships from an import."
64+
65+
consents.update_all(notes: invalidation_note, invalidated_at: Time.current)
66+
end
67+
68+
def mark_complete_if_finished(import, remove_option)
69+
return unless import.remaining_parent_relationships(remove_option:).empty?
70+
1971
import.update!(status: :processed)
2072
end
2173
end

app/models/patient_import.rb

Lines changed: 14 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -128,61 +128,23 @@ def commit_changesets(changesets)
128128
end
129129
end
130130

131-
def destroy_parent_relationships_and_invalidate_consents!(
132-
current_user,
133-
consents
134-
)
135-
ActiveRecord::Base.transaction do
136-
parent_relationships =
137-
self.parent_relationships.includes(:parent, :patient)
138-
139-
timestamp = Time.current.to_fs(:long)
140-
user_name = current_user&.full_name || "system"
141-
invalidation_note =
142-
"Consent invalidated on #{timestamp} " \
143-
"because #{user_name} removed all parent-child relationships from an import."
144-
145-
# Invalidate consents first before destroying parent relationships, which
146-
# will make consents return an empty set.
147-
consents.update_all(
148-
notes: invalidation_note,
149-
invalidated_at: Time.current
150-
)
151-
152-
patient_ids_to_update = consents.map(&:patient_id).uniq
153-
154-
parents_to_check = parent_relationships.map(&:parent)
155-
156-
parent_relationships.destroy_all
157-
158-
parents_to_check.each do |parent|
159-
if !parent.destroyed? && parent.parent_relationships.empty? &&
160-
parent.consents.empty?
161-
parent.destroy!
162-
end
163-
end
164-
165-
StatusUpdaterJob.perform_bulk(patient_ids_to_update.zip)
131+
def remaining_parent_relationships(remove_option:)
132+
if remove_option == "unconsented_only"
133+
parent_relationships -
134+
parent_relationship_consents.map(&:parent_relationship)
135+
else
136+
parent_relationships
166137
end
167138
end
168139

169-
def destroy_parent_relationships_without_consent!(consents)
170-
ActiveRecord::Base.transaction do
171-
parent_relationships_without_consents =
172-
parent_relationships.includes(:parent, :patient) -
173-
consents.map(&:parent_relationship).uniq
174-
175-
parents_to_check = parent_relationships_without_consents.map(&:parent)
176-
177-
parent_relationships_without_consents.each(&:destroy!)
178-
179-
parents_to_check.each do |parent|
180-
if !parent.destroyed? && parent.parent_relationships.empty? &&
181-
parent.consents.empty?
182-
parent.destroy!
183-
end
184-
end
185-
end
140+
def parent_relationship_consents(scope: parent_relationships)
141+
Consent
142+
.includes(patient: { parent_relationships: :parent })
143+
.joins(patient: :parent_relationships)
144+
.merge(patients)
145+
.merge(scope)
146+
.where("consents.parent_id = parent_relationships.parent_id")
147+
.not_invalidated
186148
end
187149

188150
private

spec/features/bulk_remove_parents_spec.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,9 @@ def then_i_should_see_the_parent_relationships_without_associated_consents_are_b
181181
end
182182

183183
def and_the_bulk_remove_job_should_be_enqueued_with_remove_option(option)
184-
consent_ids = @consent ? [@consent.id] : []
185184
expect(BulkRemoveParentRelationshipsJob).to have_been_enqueued.with(
186185
@class_import.to_global_id.to_s,
187-
consent_ids,
186+
@class_import.parent_relationship_ids,
188187
@user.id,
189188
option
190189
)

spec/jobs/bulk_remove_parent_relationships_job_spec.rb

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
subject(:perform_job) do
55
described_class.new.perform(
66
import.to_global_id.to_s,
7-
consents.map(&:id),
7+
import.parent_relationship_ids,
88
user.id,
99
remove_option
1010
)
@@ -17,26 +17,21 @@
1717

1818
let(:user) { create(:user, team:) }
1919

20-
let(:consents) do
21-
[
22-
create(
23-
:consent,
24-
:given,
25-
parent: import.parent_relationships.includes(:parent).first.parent,
26-
patient: import.parent_relationships.includes(:patient).first.patient
27-
),
28-
create(
29-
:consent,
30-
:refused,
31-
parent: import.parent_relationships.includes(:parent).second.parent,
32-
patient: import.parent_relationships.includes(:patient).second.patient
33-
)
34-
]
35-
end
36-
3720
before do
3821
import.process!
3922
CommitImportJob.drain
23+
create(
24+
:consent,
25+
:given,
26+
parent: import.parent_relationships.includes(:parent).first.parent,
27+
patient: import.parent_relationships.includes(:patient).first.patient
28+
)
29+
create(
30+
:consent,
31+
:refused,
32+
parent: import.parent_relationships.includes(:parent).second.parent,
33+
patient: import.parent_relationships.includes(:patient).second.patient
34+
)
4035
end
4136

4237
describe "#perform" do
@@ -68,7 +63,7 @@
6863

6964
it "invalidates consents" do
7065
perform_job
71-
consents.each { |consent| expect(consent.reload).to be_invalidated }
66+
Consent.all.find_each { expect(it.reload).to be_invalidated }
7267
end
7368

7469
it "updates import status" do

0 commit comments

Comments
 (0)