Class: Smith::PersistenceAdapters::ActiveRecordStore

Inherits:
Object
  • Object
show all
Defined in:
lib/smith/persistence_adapters/active_record_store.rb

Constant Summary collapse

TRANSIENT_ERROR_NAMES =

AR transient errors resolved via class-name guard so Smith doesn’t require activerecord at load time. Hosts that use this adapter already have activerecord in their dep tree.

%w[
  ActiveRecord::ConnectionNotEstablished
  ActiveRecord::StatementInvalid
  ActiveRecord::TransactionIsolationConflict
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model:, key_column: :key, payload_column: :payload, version_column: :lock_version) ⇒ ActiveRecordStore

Returns a new instance of ActiveRecordStore.



23
24
25
26
27
28
# File 'lib/smith/persistence_adapters/active_record_store.rb', line 23

def initialize(model:, key_column: :key, payload_column: :payload, version_column: :lock_version)
  @model_source = model
  @key_column = key_column
  @payload_column = payload_column
  @version_column = version_column
end

Class Method Details

.transient_errorsObject



15
16
17
18
19
20
21
# File 'lib/smith/persistence_adapters/active_record_store.rb', line 15

def self.transient_errors
  TRANSIENT_ERROR_NAMES.filter_map do |name|
    Object.const_get(name)
  rescue NameError
    nil
  end
end

Instance Method Details

#delete(key) ⇒ Object



47
48
49
50
51
# File 'lib/smith/persistence_adapters/active_record_store.rb', line 47

def delete(key)
  Retry.with_retries(operation: :delete, transient: self.class.transient_errors) do
    model_class.where(@key_column => key).delete_all
  end
end

#fetch(key) ⇒ Object



41
42
43
44
45
# File 'lib/smith/persistence_adapters/active_record_store.rb', line 41

def fetch(key)
  Retry.with_retries(operation: :fetch, transient: self.class.transient_errors) do
    model_class.find_by(@key_column => key)&.public_send(@payload_column)
  end
end

#store(key, payload, ttl: nil) ⇒ Object

rubocop:disable Lint/UnusedMethodArgument



30
31
32
33
34
35
36
37
38
39
# File 'lib/smith/persistence_adapters/active_record_store.rb', line 30

def store(key, payload, ttl: nil) # rubocop:disable Lint/UnusedMethodArgument
  # TTL is deferred for ActiveRecordStore — would require an
  # `expires_at` column + a periodic sweeper job. Ignored here;
  # documented as a known limitation.
  Retry.with_retries(operation: :store, transient: self.class.transient_errors) do
    record = model_class.find_or_initialize_by(@key_column => key)
    record.public_send(:"#{@payload_column}=", payload)
    record.save!
  end
end

#store_versioned(key, payload, expected_version:, ttl: nil) ⇒ Object

Optimistic locking via Rails’ built-in optimistic locking on the ‘lock_version` column. Requires the AR model to have a `lock_version` (or configured) integer column with default 0. If absent, raises ArgumentError directing the host to migrate.



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/smith/persistence_adapters/active_record_store.rb', line 57

def store_versioned(key, payload, expected_version:, ttl: nil) # rubocop:disable Lint/UnusedMethodArgument
  unless model_class.column_names.include?(@version_column.to_s)
    raise ArgumentError,
          "ActiveRecordStore#store_versioned requires a #{@version_column} column on " \
          "#{model_class.name}. Add via: " \
          "add_column :#{model_class.table_name}, :#{@version_column}, :integer, default: 0"
  end

  Retry.with_retries(operation: :store_versioned, transient: self.class.transient_errors) do
    record = model_class.find_or_initialize_by(@key_column => key)
    if record.persisted? && record.public_send(@version_column) != expected_version
      raise Smith::PersistenceVersionConflict.new(
        key: key, expected: expected_version, actual: record.public_send(@version_column)
      )
    end
    record.public_send(:"#{@payload_column}=", payload)
    record.save!
  rescue defined?(::ActiveRecord::StaleObjectError) ? ::ActiveRecord::StaleObjectError : StandardError => e
    raise unless defined?(::ActiveRecord::StaleObjectError) && e.is_a?(::ActiveRecord::StaleObjectError)

    raise Smith::PersistenceVersionConflict.new(
      key: key, expected: expected_version, actual: :concurrent
    )
  end
end