Skip to content
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

Lms/updated parallel write responses #9393

Merged
merged 10 commits into from
Jul 25, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,12 @@ def destroy
concept_results_to_save = @concept_results.map { |c| concept_results_hash(c) }.reject(&:empty?)
return if concept_results_to_save.empty?

OldConceptResult.bulk_insert(values: concept_results_to_save)
bulk_inserter = OldConceptResult.bulk_insert(values: concept_results_to_save, return_primary_keys: true)
old_concept_result_ids = bulk_inserter.result_sets.first.map { |result| result['id'] }
# Note that this will need to be overhauled when we stop writing OldConceptResult
# records, but we have to have this here now because we have to have the id value
# from the OldConceptResult to record the new ConceptResult
SaveActivitySessionConceptResultsWorker.perform_async(old_concept_result_ids)
end

private def concept_results_hash(concept_result)
Expand Down
1 change: 1 addition & 0 deletions services/QuillLMS/app/models/activity_session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class LongTimeTrackingError < StandardError; end
has_many :user_activity_classifications, through: :classification
has_one :classroom, through: :classroom_unit
has_one :unit, through: :classroom_unit
has_many :concept_results
has_many :old_concept_results
has_many :teachers, through: :classroom
has_many :concepts, -> { distinct }, through: :old_concept_results
Expand Down
6 changes: 5 additions & 1 deletion services/QuillLMS/app/models/concept_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def self.init_from_json(data_hash)
concept_id: data_hash[:concept_id],
attempt_number: metadata[:attemptNumber],
correct: ActiveModel::Type::Boolean.new.cast(metadata[:correct]),
old_concept_result_id: data_hash[:old_concept_result_id],
anathomical marked this conversation as resolved.
Show resolved Hide resolved
question_number: metadata[:questionNumber],
question_score: metadata[:questionScore]
)
Expand All @@ -87,7 +88,10 @@ def self.bulk_create_from_json(data_hash_array)
def self.find_or_create_from_old_concept_result(old_concept_result)
return old_concept_result.concept_result if old_concept_result.concept_result

create_from_json(old_concept_result.as_json)
json = old_concept_result.as_json
anathomical marked this conversation as resolved.
Show resolved Hide resolved
json['old_concept_result_id'] = old_concept_result.id

create_from_json(json)
end

def self.parse_extra_metadata(metadata)
Expand Down
23 changes: 23 additions & 0 deletions services/QuillLMS/app/models/old_concept_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,27 @@ def correct?
def concept_uid=(concept_uid)
self.concept = Concept.where(uid: concept_uid).first
end

# Over-riding the `bulk_insert` classmethod from the bulk_insert gem because the
# behavior documented in the gem doesn't actually work. This change makes it behave
# as documented when passed `return_primary_keys: true`. Minimal modifications from
# the original code found here, specifically around return values in if blocks:
# https://github.com/jamis/bulk_insert/blob/master/lib/bulk_insert.rb
anathomical marked this conversation as resolved.
Show resolved Hide resolved
def self.bulk_insert(*columns, values: nil, set_size: 500, ignore: false, update_duplicates: false, return_primary_keys: false)
anathomical marked this conversation as resolved.
Show resolved Hide resolved
columns = default_bulk_columns if columns.empty?
worker = BulkInsert::Worker.new(connection, table_name, primary_key, columns, set_size, ignore, update_duplicates, return_primary_keys)

if values.present?
transaction do
worker.add_all(values)
worker.save!
end
elsif block_given?
transaction do
yield worker
worker.save!
end
end
worker
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

class SaveActivitySessionConceptResultsWorker
include Sidekiq::Worker

def perform(old_concept_result_ids)
OldConceptResult.where(id: old_concept_result_ids).each do |old_concept_result|
ConceptResult.find_or_create_from_old_concept_result(old_concept_result)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,25 @@
create(:old_concept_result,
activity_session_id: activity_session.id,
concept: writing_concept,
metadata: { foo: 'bar' }
metadata: { foo: 'bar', correct: true }
anathomical marked this conversation as resolved.
Show resolved Hide resolved
)
end

let(:concept_result2) do
create(:old_concept_result,
activity_session_id: activity_session.id,
metadata: { baz: 'foo' }
metadata: { baz: 'foo', correct: true }
)
end

let(:concept_result3) do
create(:old_concept_result,
activity_session_id: activity_session.id
activity_session_id: activity_session.id,
metadata: { correct: true }
)
end

let(:concept_results) do
let!(:concept_results) do
results = JSON.parse([concept_result1, concept_result2, concept_result3].to_json)

results[0] = results[0].merge('concept_uid' => concept_result1.concept.uid)
Expand All @@ -74,23 +75,29 @@
results
end

before { put :update, params: { id: activity_session.uid, concept_results: concept_results }, as: :json }

it 'succeeds' do
put :update, params: { id: activity_session.uid, concept_results: concept_results }, as: :json
expect(response.status).to eq(200)
end

it 'stores the concept results' do
activity_session.reload
expect(activity_session.old_concept_results.size).to eq 7
# Run Sidekiq jobs immediately instead of queuing them
Sidekiq::Testing.inline! do
expect do
put :update, params: { id: activity_session.uid, concept_results: concept_results }, as: :json
end.to change { activity_session.reload.old_concept_results.size }.by(3)
.and change { activity_session.reload.concept_results.size }.by(3)
end
Comment on lines -84 to +90
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original version of those code with expect ... to eq 7 was counting the items that were created as part of setup as well as the ones created by the function call. This cleans things up slightly to only count items new from the call.

end

it 'saves the arbitrary metadata for the results' do
put :update, params: { id: activity_session.uid, concept_results: concept_results }, as: :json
activity_session.reload
expect(activity_session.old_concept_results.find{|x| x.metadata == {"foo"=>"bar"}}).to be
expect(activity_session.old_concept_results.find{|x| x.metadata['foo'] == "bar"}).to be
end

it 'saves the concept tag relationship (ID) in the result' do
put :update, params: { id: activity_session.uid, concept_results: concept_results }, as: :json
expect(OldConceptResult.where(activity_session_id: activity_session, concept_id: writing_concept.id).count).to eq 2
end
end
Expand Down
1 change: 1 addition & 0 deletions services/QuillLMS/spec/models/activity_session_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
it { should have_many(:feedback_histories).through(:feedback_sessions) }
it { should have_one(:classroom).through(:classroom_unit) }
it { should have_one(:unit).through(:classroom_unit) }
it { should have_many(:concept_results) }
it { should have_many(:concepts).through(:old_concept_results) }
it { should have_many(:teachers).through(:classroom) }
it { should belong_to(:user) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

require 'rails_helper'

describe SaveActivitySessionConceptResultsWorker, type: :worker do

context '#perform' do
let(:old_concept_result) { create(:old_concept_result) }

it 'should save new ConceptResult records based on an array of OldConceptResult ids' do
expect { subject.perform(old_concept_result.id) }
.to change(ConceptResult, :count).by(1)
expect(old_concept_result.reload.concept_result).to be
anathomical marked this conversation as resolved.
Show resolved Hide resolved
end

it 'should create new ConceptResults that are related to the same ActivitySession as the original OldConceptResult' do
subject.perform(old_concept_result.id)

expect(old_concept_result.concept_result.activity_session).to eq(old_concept_result.activity_session)
end
end
end