-
Notifications
You must be signed in to change notification settings - Fork 8
Optimize DOI event performance with bulk-preloading #1449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
670694f
d0d6ed3
3ebf15f
ef1dd09
f98c939
b74d788
25eebba
24a6019
8988206
df399da
6b36137
c280e5d
aa5a5e8
3f4c605
0fdba2e
17e3ccf
7890125
19049fd
fd0f4a1
f779c32
9cdf989
74061b2
40b17ed
05ade79
fbdfbdf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| # Wrapper class to make preloaded event arrays compatible with ActiveRecord::Relation API | ||
| # This allows existing code that calls methods like `pluck`, `map`, `select` to work | ||
| # with in-memory arrays without modification. | ||
| class PreloadedEventRelation | ||
| include Enumerable | ||
|
|
||
| def initialize(events) | ||
| @events = Array(events) | ||
| end | ||
|
|
||
| # Delegate Enumerable methods to the underlying array | ||
| def each(&block) | ||
| @events.each(&block) | ||
| end | ||
|
|
||
| # Delegate common Enumerable methods explicitly | ||
| def first | ||
| @events.first | ||
| end | ||
|
|
||
| def last | ||
| @events.last | ||
| end | ||
|
|
||
| def count | ||
| @events.count | ||
| end | ||
|
|
||
| # Implement pluck to match ActiveRecord::Relation#pluck behavior | ||
| # Supports single or multiple column names | ||
| def pluck(*column_names) | ||
| if column_names.length == 1 | ||
| column_name = column_names.first | ||
| @events.map { |event| event.public_send(column_name) } | ||
| else | ||
| @events.map { |event| column_names.map { |col| event.public_send(col) } } | ||
| end | ||
| end | ||
|
|
||
| # Delegate map to the underlying array | ||
| def map(&block) | ||
| @events.map(&block) | ||
| end | ||
|
|
||
| # Delegate select to the underlying array | ||
| def select(&block) | ||
| PreloadedEventRelation.new(@events.select(&block)) | ||
| end | ||
|
|
||
| # Delegate other common Enumerable methods | ||
| def compact | ||
| PreloadedEventRelation.new(@events.compact) | ||
| end | ||
|
|
||
| def uniq | ||
| PreloadedEventRelation.new(@events.uniq) | ||
| end | ||
|
|
||
| def sort_by(&block) | ||
| PreloadedEventRelation.new(@events.sort_by(&block)) | ||
| end | ||
|
|
||
| def group_by(&block) | ||
| @events.group_by(&block) | ||
| end | ||
|
|
||
| def inject(*args, &block) | ||
| @events.inject(*args, &block) | ||
| end | ||
|
|
||
| def length | ||
| @events.length | ||
| end | ||
|
|
||
| def empty? | ||
| @events.empty? | ||
| end | ||
|
|
||
| def present? | ||
| @events.present? | ||
| end | ||
|
|
||
| def blank? | ||
| @events.blank? | ||
| end | ||
|
|
||
| # Allow direct access to the underlying array | ||
| def to_a | ||
| @events | ||
| end | ||
|
|
||
| def to_ary | ||
| @events | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,62 @@ | ||||||||||||||||||||||||
| # frozen_string_literal: true | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Service class to preload events for a batch of DOIs in a single query | ||||||||||||||||||||||||
| # This dramatically reduces database queries from N*M (DOIs * Relationship Types) to 1-2 queries total | ||||||||||||||||||||||||
| class EventsPreloader | ||||||||||||||||||||||||
| # Maximum number of DOIs to query at once to avoid database parameter limits | ||||||||||||||||||||||||
| CHUNK_SIZE = 1000 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def initialize(dois) | ||||||||||||||||||||||||
| @dois = Array(dois) | ||||||||||||||||||||||||
| @doi_map = {} | ||||||||||||||||||||||||
| @dois.each do |doi| | ||||||||||||||||||||||||
| next if doi.doi.blank? | ||||||||||||||||||||||||
| @doi_map[doi.doi.upcase] = doi | ||||||||||||||||||||||||
| # Initialize preloaded_events array if not already set | ||||||||||||||||||||||||
| doi.preloaded_events ||= [] | ||||||||||||||||||||||||
| end | ||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||
| end | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Preload all events for the batch of DOIs | ||||||||||||||||||||||||
| def preload! | ||||||||||||||||||||||||
| return if @dois.empty? | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| doi_identifiers = @dois.filter_map { |doi| doi.doi&.upcase }.uniq | ||||||||||||||||||||||||
| return if doi_identifiers.empty? | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
Comment on lines
+22
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep Line 22 and Line 25 return 🛠️ Proposed fix def preload!
- return if `@dois.empty`?
+ return self if `@dois.empty`?
doi_identifiers = `@dois.filter_map` { |doi| doi.doi&.upcase }.uniq
- return if doi_identifiers.empty?
+ if doi_identifiers.empty?
+ `@dois.each` { |doi| doi.preloaded_events ||= [] }
+ return self
+ end📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| # Fetch events in chunks to avoid database parameter limits | ||||||||||||||||||||||||
| all_events = [] | ||||||||||||||||||||||||
| doi_identifiers.each_slice(CHUNK_SIZE) do |chunk| | ||||||||||||||||||||||||
| events = Event.where( | ||||||||||||||||||||||||
| "source_doi IN (?) OR target_doi IN (?)", | ||||||||||||||||||||||||
| chunk, chunk | ||||||||||||||||||||||||
| ).order(updated_at: :desc).to_a | ||||||||||||||||||||||||
| all_events.concat(events) | ||||||||||||||||||||||||
| end | ||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||
| all_events.uniq! | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Group events by DOI and assign to each Doi object | ||||||||||||||||||||||||
| all_events.each do |event| | ||||||||||||||||||||||||
| # Add event to source DOI's preloaded_events if it matches | ||||||||||||||||||||||||
| if event.source_doi.present? | ||||||||||||||||||||||||
| source_doi_obj = @doi_map[event.source_doi.upcase] | ||||||||||||||||||||||||
| source_doi_obj.preloaded_events << event if source_doi_obj | ||||||||||||||||||||||||
| end | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Add event to target DOI's preloaded_events if it matches | ||||||||||||||||||||||||
| if event.target_doi.present? | ||||||||||||||||||||||||
| target_doi_obj = @doi_map[event.target_doi.upcase] | ||||||||||||||||||||||||
| if target_doi_obj && target_doi_obj != source_doi_obj | ||||||||||||||||||||||||
| target_doi_obj.preloaded_events << event | ||||||||||||||||||||||||
| end | ||||||||||||||||||||||||
| end | ||||||||||||||||||||||||
| end | ||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Ensure all DOIs have an array (even if empty) | ||||||||||||||||||||||||
| @dois.each do |doi| | ||||||||||||||||||||||||
| doi.preloaded_events ||= [] | ||||||||||||||||||||||||
| end | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| self | ||||||||||||||||||||||||
| end | ||||||||||||||||||||||||
| end | ||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.