Class: ActiveStorage::AwsRecord::VariantRecord

Inherits:
Object
  • Object
show all
Includes:
Item, Owner
Defined in:
lib/active_storage/aws_record/variant_record.rb

Overview

Tracks a processed variant of a blob when config.active_storage.track_variants is on. It is co-located in its blob’s partition (+ns#Blob#<blob_id>+) with a sort key of ns#VariantRecord#<variation_digest>, so the (blob_id, variation_digest) pair is unique by construction and the blob and all its variants form one item collection. It is itself an attachment owner (+has_one_attached :image+); its owner id is a reversible, #-free encoding of the natural key so find(id) resolves it.

Defined Under Namespace

Classes: CreateConflict

Constant Summary collapse

ID_DELIMITER =
' '

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Owner

#destroy

Methods included from Item

#ns_key, #physical_key, #schema, #stamp_physical_keys!

Methods included from Persistence

#==, #changed?, #dynamodb_client, #hash, #mark_destroyed!, #mark_persisted!, #read_attribute, #write_attribute

Class Method Details

.active_storage_find(id) ⇒ Object

Owner resolution for Active Storage (VariantRecord is an attachment owner via image). Delegate to the reversible-id #find: Attachable‘s default find_with_opts(hash_key => id) adapter cannot address the composite key.



48
49
50
# File 'lib/active_storage/aws_record/variant_record.rb', line 48

def active_storage_find(id)
  find(id)
end

.create_or_find_by!(blob_id:, variation_digest:) ⇒ Object

Race-safe creation: a conditional put on the variant’s own key, paired with a check that the source blob still exists, so a variant can never be created against a just-purged blob. A condition failure (the variant already exists, or the blob is gone) falls back to find. The block (used by VariantWithRecord to image.attach the processed file) runs before save so the queued image attachment is flushed by the save callbacks.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/active_storage/aws_record/variant_record.rb', line 58

def create_or_find_by!(blob_id:, variation_digest:)
  record = new(blob_id: blob_id.to_s, variation_digest: variation_digest)
  yield record if block_given?
  record.save!
  record
rescue CreateConflict
  # Only a genuine create conflict (the row already exists) falls back to
  # find — a post-write failure (image attachment, blob gone) propagates.
  # NOTE: the marker row commits just before its image attachment is
  # flushed, so a *concurrent* processor that loses the race here can
  # briefly observe the record before its image is attached (a transient
  # that resolves once the winner finishes; like Active Storage's
  # create-then-upload, but without AR's single enclosing transaction).
  find_by(blob_id: blob_id, variation_digest: variation_digest) ||
    raise(ActiveStorage::RecordNotSaved.new('Failed to create or find variant record', record))
end

.decode_id(id) ⇒ Object



108
109
110
# File 'lib/active_storage/aws_record/variant_record.rb', line 108

def decode_id(id)
  Base64.urlsafe_decode64(id).split(ID_DELIMITER, 2)
end

.encode_id(blob_id, variation_digest) ⇒ Object



104
105
106
# File 'lib/active_storage/aws_record/variant_record.rb', line 104

def encode_id(blob_id, variation_digest)
  Base64.urlsafe_encode64("#{blob_id}#{ID_DELIMITER}#{variation_digest}", padding: false)
end

.find(id) ⇒ Object

Contract find(id): decode the reversible id back to the natural key.



32
33
34
35
36
37
38
# File 'lib/active_storage/aws_record/variant_record.rb', line 32

def find(id)
  blob_id, variation_digest = decode_id(id)
  find_by(blob_id: blob_id, variation_digest: variation_digest) ||
    raise(ActiveStorage::RecordNotFound, "Couldn't find #{name} with id=#{id.inspect}")
rescue ArgumentError
  raise ActiveStorage::RecordNotFound, "Couldn't find #{name} with id=#{id.inspect}"
end

.find_by(blob_id:, variation_digest:) ⇒ Object



40
41
42
43
# File 'lib/active_storage/aws_record/variant_record.rb', line 40

def find_by(blob_id:, variation_digest:)
  keys = logical_keys_for(blob_id.to_s, variation_digest)
  get_item(**keys)
end

.logical_keys_for(blob_id, variation_digest) ⇒ Object

Logical keys for a (blob_id, variation_digest) pair without an instance.



96
97
98
99
100
101
102
# File 'lib/active_storage/aws_record/variant_record.rb', line 96

def logical_keys_for(blob_id, variation_digest)
  {
    h: ns_key('Blob', blob_id),
    r: ns_key('VariantRecord', variation_digest),
    item_id: ns_key('Blob', blob_id, 'VariantRecord', variation_digest),
  }
end

.where_blob(blob_id) ⇒ Object

All variant records for a blob (used by Blob#destroy’s variant sweep). Mode A: strong base-table query under the blob partition. Mode B: GSI.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/active_storage/aws_record/variant_record.rb', line 77

def where_blob(blob_id)
  schema = ActiveStorage::AwsRecord.schema
  partition = ns_key('Blob', blob_id)
  prefix = "#{ns_key('VariantRecord')}#{ActiveStorage::AwsRecord.config.separator}"
  opts = {
    key_condition_expression: '#h = :h AND begins_with(#r, :r)',
    expression_attribute_values: { ':h' => partition, ':r' => prefix },
  }
  if schema.range_mode?
    opts[:expression_attribute_names] = { '#h' => schema.partition_attr, '#r' => schema.sort_attr }
    opts[:consistent_read] = true
  else
    opts[:index_name] = schema.index_name
    opts[:expression_attribute_names] = { '#h' => schema.index_partition_attr, '#r' => schema.index_sort_attr }
  end
  query(opts).to_a
end

Instance Method Details

#idObject

The owner id used as record_id for the image attachment; reversible so VariantRecord.find(id) resolves the natural key.



153
154
155
156
157
# File 'lib/active_storage/aws_record/variant_record.rb', line 153

def id
  return nil if blob_id.nil? || variation_digest.nil?

  self.class.encode_id(blob_id, variation_digest)
end

#logical_keysObject



113
114
115
# File 'lib/active_storage/aws_record/variant_record.rb', line 113

def logical_keys
  self.class.logical_keys_for(blob_id, variation_digest)
end

#save(opts = {}) ⇒ Object



144
145
146
147
148
149
# File 'lib/active_storage/aws_record/variant_record.rb', line 144

def save(opts = {})
  save!(opts)
  true
rescue ActiveStorage::RecordNotSaved, CreateConflict
  false
end

#save!(opts = {}) ⇒ Object

Persist the variant row transactionally and run the owner save/commit callbacks, so a queued image attachment (from create_or_find_by!‘s block) is saved and uploaded. Overrides Owner#save! to substitute the transactional create for aws-record’s plain put. If a step after the row write fails (e.g. the image attachment), the half-written row is removed so it does not become a markerless “poison” record.



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/active_storage/aws_record/variant_record.rb', line 123

def save!(opts = {})
  marker_written = false
  completed = run_callbacks(:save) do
    stamp_physical_keys!
    if new_record?
      transactional_create!
      marker_written = true
    end
    true
  end
  raise ActiveStorage::RecordNotSaved.new('Save halted by a before_save callback', self) unless completed

  run_callbacks(:commit)
  self
rescue CreateConflict
  raise
rescue StandardError
  destroy_marker! if marker_written
  raise
end