Class: Dragonfly::CascadeDataStore

Inherits:
Object
  • Object
show all
Defined in:
lib/dragonfly/cascade_data_store.rb

Overview

Dragonfly datastore that reads LOCAL-first and falls back to a CHAIN of
read-only datastores on a miss, writing the result back through
(self-healing cache).

Use case: development. Dev writes to secure_assets/development in the
Wasabi usc1 bucket, but blobs live in three eras/places:

  1. secure_assets/development (usc1) — dev-written files
  2. secure_assets/production (usc1) — prod-written since the migration
  3. secure_assets/production in the OLD us-east-1 heatwave-assets
    bucket — everything uploaded before the bucket migration
    A plain miss used to raise Dragonfly::Job::Fetch::NotFound and break every
    dev flow touching those attachments (publication pages, studio seeding,
    docling/translation pipelines). With the cascade, the first read pulls the
    blob into the development prefix — subsequent reads are local.

Writes and destroys always go to the primary only: every fallback is
strictly read-through, never mutated.

Datastore contract (same as Dragonfly::S3DataStore / FileDataStore):
write(content, opts) → uid; read(uid) → [data, meta] or nil; destroy(uid);
url_for(uid, opts) — presigned URLs sign against the first prefix that
actually holds the blob.

Instance Attribute Summary collapse

Delegated Instance Attributes collapse

Instance Method Summary collapse

Constructor Details

#initialize(primary:, fallback: nil, fallbacks: nil) ⇒ CascadeDataStore

Returns a new instance of CascadeDataStore.

Parameters:

  • fallback (Object) (defaults to: nil)

    single fallback (back-compat shorthand)

  • fallbacks (Array<Object>) (defaults to: nil)

    ordered fallbacks — tried in order



31
32
33
34
35
# File 'lib/dragonfly/cascade_data_store.rb', line 31

def initialize(primary:, fallback: nil, fallbacks: nil)
  @primary = primary
  @fallbacks = Array(fallbacks || fallback)
  @head_cache = {}
end

Instance Attribute Details

#primaryObject (readonly)

Returns the value of attribute primary.



27
28
29
# File 'lib/dragonfly/cascade_data_store.rb', line 27

def primary
  @primary
end

Instance Method Details

#destroyObject

Alias for @primary#destroy

Returns:

  • (Object)

    @primary#destroy

See Also:



71
# File 'lib/dragonfly/cascade_data_store.rb', line 71

delegate :destroy, to: :@primary

#exists?(uid) ⇒ Boolean

Cheap existence check (head request, no download) against the PRIMARY
store — use instead of read(uid) when only "already synced locally?"
matters; read downloads the full blob just to answer that.

Returns:

  • (Boolean)


67
68
69
# File 'lib/dragonfly/cascade_data_store.rb', line 67

def exists?(uid)
  head?(@primary, uid)
end

#fallbackObject?

First fallback — kept for callers written against the single-fallback API.

Returns:

  • (Object, nil)

    the first fallback store



39
# File 'lib/dragonfly/cascade_data_store.rb', line 39

def fallback = @fallbacks.first

#read(uid) ⇒ Array?

Returns the (data, meta) tuple, or nil when no store has the uid.

Returns:

  • (Array, nil)

    the (data, meta) tuple, or nil when no store has the uid



50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/dragonfly/cascade_data_store.rb', line 50

def read(uid)
  result = @primary.read(uid)
  return result if result

  @fallbacks.each do |store|
    result = store.read(uid)
    next unless result

    write_through(uid, result)
    return result
  end
  nil
end

#url_for(uid, opts = {}) ⇒ String

Returns presigned URL for the blob.

Parameters:

  • uid (String)

    stored content uid

  • opts (Hash) (defaults to: {})

    url options forwarded to the signing store

Options Hash (opts):

  • all (Object)

    keys are forwarded unchanged

Returns:

  • (String)

    presigned URL for the blob



77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/dragonfly/cascade_data_store.rb', line 77

def url_for(uid, opts = {})
  # Presigned URLs point straight at the bucket — cascade them too: sign
  # against the first prefix that actually has the blob (local dev prefix,
  # then each fallback in order). Head results are memoized per store+uid
  # (see head? below) — one round-trip per blob per process, not per link.
  return @primary.url_for(uid, opts) if head?(@primary, uid)

  store = @fallbacks.find { |s| head?(s, uid) }
  # No store holds the blob — sign against the PRIMARY (the canonical home
  # for new writes), not an arbitrary fallback.
  (store || @primary).url_for(uid, opts)
end

#write(content, opts = {}) ⇒ String

Returns the assigned uid.

Parameters:

  • content (Dragonfly::Content)

    content to write

  • opts (Hash) (defaults to: {})

    write options forwarded to the primary store

Options Hash (opts):

  • all (Object)

    keys are forwarded unchanged

Returns:

  • (String)

    the assigned uid



45
46
47
# File 'lib/dragonfly/cascade_data_store.rb', line 45

def write(content, opts = {})
  @primary.write(content, opts)
end