Skip to content

Commit

Permalink
ensure timestamp updates are atomic and applied in order
Browse files Browse the repository at this point in the history
  • Loading branch information
timcowlishaw committed Oct 10, 2024
1 parent 250807a commit dc89967
Show file tree
Hide file tree
Showing 6 changed files with 35 additions and 15 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ gem 'workflow-activerecord'
gem 'em-mqtt'

group :test do
gem "database_cleaner-active_record"
gem 'simplecov', require: false
gem 'timecop'
gem 'vcr'
Expand Down
7 changes: 6 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ GEM
css_parser (1.14.0)
addressable
dalli (3.2.4)
database_cleaner-active_record (2.2.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.3.3)
date_validator (0.12.0)
activemodel (>= 3)
Expand Down Expand Up @@ -545,6 +549,7 @@ DEPENDENCIES
cane
countries
dalli
database_cleaner-active_record
date_validator
diffy
doorkeeper (~> 5)
Expand Down Expand Up @@ -616,4 +621,4 @@ RUBY VERSION
ruby 3.0.6p216

BUNDLED WITH
2.5.6
2.5.17
4 changes: 2 additions & 2 deletions app/models/device.rb
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,8 @@ def remove_mac_address_for_newly_registered_device!

def update_component_timestamps(timestamp, sensor_ids)
components.select {|c| sensor_ids.include?(c.sensor_id) }.each do |component|
component.transaction do
component.lock!
component.lock! if self.class.connection.transaction_open?
if !component.reload.last_reading_at || timestamp > component.last_reading_at
component.update_column(:last_reading_at, timestamp)
end
end
Expand Down
22 changes: 12 additions & 10 deletions app/models/storer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,20 @@ def store device, reading, do_update = true

def update_device(device, parsed_ts, sql_data)
return if parsed_ts <= Time.at(0)
device.transaction(isolation: :serializable) do
device.lock!
if device.reload.last_reading_at.present?
# Comparison errors if device.last_reading_at is nil (new devices).
# Devices can post multiple readings, in a non-sorted order.
# Do not update data with an older timestamp.
return if parsed_ts < device.last_reading_at
end

if device.last_reading_at.present?
# Comparison errors if device.last_reading_at is nil (new devices).
# Devices can post multiple readings, in a non-sorted order.
# Do not update data with an older timestamp.
return if parsed_ts < device.last_reading_at
sql_data = device.data.present? ? device.data.merge(sql_data) : sql_data
device.update_columns(last_reading_at: parsed_ts, data: sql_data, state: 'has_published')
sensor_ids = sql_data.select { |k, v| k.is_a?(Integer) }.keys.compact.uniq
device.update_component_timestamps(parsed_ts, sensor_ids)
end

sql_data = device.data.present? ? device.data.merge(sql_data) : sql_data
device.update_columns(last_reading_at: parsed_ts, data: sql_data, state: 'has_published')
sensor_ids = sql_data.select { |k, v| k.is_a?(Integer) }.keys.compact.uniq
device.update_component_timestamps(parsed_ts, sensor_ids)
end

def kairos_publish(reading_data)
Expand Down
2 changes: 1 addition & 1 deletion spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
# instead of true.
config.use_transactional_fixtures = true
config.use_transactional_fixtures = false

# RSpec Rails can automatically mix in different behaviours to your tests
# based on their file location, for example enabling you to call `get` and
Expand Down
14 changes: 13 additions & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# users commonly want.
#
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration

require "database_cleaner/active_record"
RSpec.configure do |config|
# rspec-expectations config goes here. You can use an alternate
# assertion/expectation library such as wrong or the stdlib/minitest
Expand All @@ -40,6 +40,18 @@
mocks.verify_partial_doubles = true
end

DatabaseCleaner.strategy = :truncation

config.before(:suite) do
DatabaseCleaner.clean
end

config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end

# The settings below are suggested to provide a good initial experience
# with RSpec, but feel free to customize to your heart's content.

Expand Down

0 comments on commit dc89967

Please sign in to comment.