Throttle the number of UPDATEs triggered by touch
This throttles the number of UPDATE queries that can be triggered by calling "touch" on a Note, Issue, or MergeRequest. For Note objects we also take care of updating the associated "noteable" relation in a smarter way than Rails does by default.
This commit is contained in:
parent
483b5f1bfa
commit
856447ccd3
10 changed files with 82 additions and 3 deletions
10
app/models/concerns/throttled_touch.rb
Normal file
10
app/models/concerns/throttled_touch.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# ThrottledTouch can be used to throttle the number of updates triggered by
|
||||||
|
# calling "touch" on an ActiveRecord model.
|
||||||
|
module ThrottledTouch
|
||||||
|
# The amount of time to wait before "touch" can update a record again.
|
||||||
|
TOUCH_INTERVAL = 1.minute
|
||||||
|
|
||||||
|
def touch(*args)
|
||||||
|
super if (Time.zone.now - updated_at) > TOUCH_INTERVAL
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,6 +9,7 @@ class Issue < ActiveRecord::Base
|
||||||
include FasterCacheKeys
|
include FasterCacheKeys
|
||||||
include RelativePositioning
|
include RelativePositioning
|
||||||
include TimeTrackable
|
include TimeTrackable
|
||||||
|
include ThrottledTouch
|
||||||
|
|
||||||
DueDateStruct = Struct.new(:title, :name).freeze
|
DueDateStruct = Struct.new(:title, :name).freeze
|
||||||
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
|
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
|
||||||
|
|
|
@ -7,6 +7,7 @@ class MergeRequest < ActiveRecord::Base
|
||||||
include TimeTrackable
|
include TimeTrackable
|
||||||
include ManualInverseAssociation
|
include ManualInverseAssociation
|
||||||
include EachBatch
|
include EachBatch
|
||||||
|
include ThrottledTouch
|
||||||
|
|
||||||
ignore_column :locked_at,
|
ignore_column :locked_at,
|
||||||
:ref_fetched
|
:ref_fetched
|
||||||
|
|
|
@ -15,6 +15,7 @@ class Note < ActiveRecord::Base
|
||||||
include IgnorableColumn
|
include IgnorableColumn
|
||||||
include Editable
|
include Editable
|
||||||
include Gitlab::SQL::Pattern
|
include Gitlab::SQL::Pattern
|
||||||
|
include ThrottledTouch
|
||||||
|
|
||||||
module SpecialRole
|
module SpecialRole
|
||||||
FIRST_TIME_CONTRIBUTOR = :first_time_contributor
|
FIRST_TIME_CONTRIBUTOR = :first_time_contributor
|
||||||
|
@ -55,7 +56,7 @@ class Note < ActiveRecord::Base
|
||||||
participant :author
|
participant :author
|
||||||
|
|
||||||
belongs_to :project
|
belongs_to :project
|
||||||
belongs_to :noteable, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
|
belongs_to :noteable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
|
||||||
belongs_to :author, class_name: "User"
|
belongs_to :author, class_name: "User"
|
||||||
belongs_to :updated_by, class_name: "User"
|
belongs_to :updated_by, class_name: "User"
|
||||||
belongs_to :last_edited_by, class_name: 'User'
|
belongs_to :last_edited_by, class_name: 'User'
|
||||||
|
@ -118,6 +119,7 @@ class Note < ActiveRecord::Base
|
||||||
before_validation :set_discussion_id, on: :create
|
before_validation :set_discussion_id, on: :create
|
||||||
after_save :keep_around_commit, if: :for_project_noteable?
|
after_save :keep_around_commit, if: :for_project_noteable?
|
||||||
after_save :expire_etag_cache
|
after_save :expire_etag_cache
|
||||||
|
after_save :touch_noteable
|
||||||
after_destroy :expire_etag_cache
|
after_destroy :expire_etag_cache
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
@ -367,6 +369,38 @@ class Note < ActiveRecord::Base
|
||||||
Gitlab::EtagCaching::Store.new.touch(key)
|
Gitlab::EtagCaching::Store.new.touch(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def touch(*args)
|
||||||
|
# We're not using an explicit transaction here because this would in all
|
||||||
|
# cases result in all future queries going to the primary, even if no writes
|
||||||
|
# are performed.
|
||||||
|
#
|
||||||
|
# We touch the noteable first so its SELECT query can run before our writes,
|
||||||
|
# ensuring it runs on a secondary (if no prior write took place).
|
||||||
|
touch_noteable
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
# By default Rails will issue an "SELECT *" for the relation, which is
|
||||||
|
# overkill for just updating the timestamps. To work around this we manually
|
||||||
|
# touch the data so we can SELECT only the columns we need.
|
||||||
|
def touch_noteable
|
||||||
|
# Commits are not stored in the DB so we can't touch them.
|
||||||
|
return if for_commit?
|
||||||
|
|
||||||
|
assoc = association(:noteable)
|
||||||
|
|
||||||
|
noteable_object =
|
||||||
|
if assoc.loaded?
|
||||||
|
noteable
|
||||||
|
else
|
||||||
|
# If the object is not loaded (e.g. when notes are loaded async) we
|
||||||
|
# _only_ want the data we actually need.
|
||||||
|
assoc.scope.select(:id, :updated_at).take
|
||||||
|
end
|
||||||
|
|
||||||
|
noteable_object&.touch
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def keep_around_commit
|
def keep_around_commit
|
||||||
|
|
5
changelogs/unreleased/throttle-touching-of-objects.yml
Normal file
5
changelogs/unreleased/throttle-touching-of-objects.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Throttle the number of UPDATEs triggered by touch
|
||||||
|
merge_request:
|
||||||
|
author:
|
||||||
|
type: performance
|
|
@ -171,7 +171,7 @@ describe Issuable do
|
||||||
|
|
||||||
it "returns false when record has been updated" do
|
it "returns false when record has been updated" do
|
||||||
allow(issue).to receive(:today?).and_return(true)
|
allow(issue).to receive(:today?).and_return(true)
|
||||||
issue.touch
|
issue.update_attribute(:updated_at, 1.hour.ago)
|
||||||
expect(issue.new?).to be_falsey
|
expect(issue.new?).to be_falsey
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -765,4 +765,8 @@ describe Issue do
|
||||||
expect(described_class.public_only).to eq([public_issue])
|
expect(described_class.public_only).to eq([public_issue])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'throttled touch' do
|
||||||
|
subject { create(:issue, updated_at: 1.hour.ago) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1851,4 +1851,8 @@ describe MergeRequest do
|
||||||
.to change { project.open_merge_requests_count }.from(1).to(0)
|
.to change { project.open_merge_requests_count }.from(1).to(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'throttled touch' do
|
||||||
|
subject { create(:merge_request, updated_at: 1.hour.ago) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@ describe Note do
|
||||||
|
|
||||||
describe 'associations' do
|
describe 'associations' do
|
||||||
it { is_expected.to belong_to(:project) }
|
it { is_expected.to belong_to(:project) }
|
||||||
it { is_expected.to belong_to(:noteable).touch(true) }
|
it { is_expected.to belong_to(:noteable).touch(false) }
|
||||||
it { is_expected.to belong_to(:author).class_name('User') }
|
it { is_expected.to belong_to(:author).class_name('User') }
|
||||||
|
|
||||||
it { is_expected.to have_many(:todos).dependent(:destroy) }
|
it { is_expected.to have_many(:todos).dependent(:destroy) }
|
||||||
|
|
20
spec/support/shared_examples/throttled_touch.rb
Normal file
20
spec/support/shared_examples/throttled_touch.rb
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
shared_examples_for 'throttled touch' do
|
||||||
|
describe '#touch' do
|
||||||
|
it 'updates the updated_at timestamp' do
|
||||||
|
Timecop.freeze do
|
||||||
|
subject.touch
|
||||||
|
expect(subject.updated_at).to eq(Time.zone.now)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the object at most once per minute' do
|
||||||
|
first_updated_at = Time.zone.now - (ThrottledTouch::TOUCH_INTERVAL * 2)
|
||||||
|
second_updated_at = Time.zone.now - (ThrottledTouch::TOUCH_INTERVAL * 1.5)
|
||||||
|
|
||||||
|
Timecop.freeze(first_updated_at) { subject.touch }
|
||||||
|
Timecop.freeze(second_updated_at) { subject.touch }
|
||||||
|
|
||||||
|
expect(subject.updated_at).to eq(first_updated_at)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue