761d0b4a76
An append-only storage ledger replaces deadlock-prone synchronous counter updates and drift-prone async updates. All content storage (card images, card/comment/board description embeds) is tracked. Account exports (which expire) and avatars are not tracked. Storage is accounted for by Account and Board. Both consume the same event stream independently: no bubble-up storage bumps triggering deadlocks due to lock sequencing. Each calculates its own total from the underlying ledger with independent cursors and materialization. * Storage::Entry: Append-only ledger recording attach/detach/transfer events with delta bytes. Single event stream indexed for both Account and Board cursor queries. * Storage::Total: Polymorphic snapshot cache with cursor (last_entry_id) tracking which entries have been materialized. * Storage::Totaled concern: Provides bytes_used (fast snapshot) and bytes_used_exact (snapshot + pending) query modes, plus materialize_storage! to roll up pending entries. * Storage::Tracked concern: For models owning attachments (Card, Comment, Board). Provides board_for_storage_tracking for models where board is determined differently (Board returns self). Handles board transfers by recording transfer_out/transfer_in entries. * Storage::AttachmentTracking: Hooks ActiveStorage::Attachment lifecycle to record attach/detach entries. Handles ActionText::RichText embeds by traversing to the actual model. Snapshots context in before_destroy to handle cascading deletes where parent record may be gone by after_destroy_commit. * MaterializeJob: Rolls up pending entries into snapshot. Concurrency limited per owner to prevent duplicate work. * ReconcileJob: On-demand reconciliation against actual attachment storage for support/debugging. Compares ledger total to real bytes from card images, card embeds, comment embeds, and board embeds. Usage: * account.bytes_used / board.bytes_used: fast, slightly stale bytesize * account.bytes_used_exact / board.bytes_used_exact: real-time bytesize * Storage::Entry: audit trail for debugging and point-in-time queries
58 lines
1.4 KiB
Ruby
58 lines
1.4 KiB
Ruby
module Storage::Tracked
|
|
extend ActiveSupport::Concern
|
|
|
|
included do
|
|
before_update :track_board_transfer, if: :board_transfer?
|
|
end
|
|
|
|
# Return self as the trackable record for storage entries
|
|
def storage_tracked_record
|
|
self
|
|
end
|
|
|
|
# Override in models where board is determined differently (e.g., Board itself)
|
|
def board_for_storage_tracking
|
|
board
|
|
end
|
|
|
|
# Total bytes for all attachments on this record
|
|
def storage_bytes
|
|
attachments_for_storage.sum { |a| a.blob.byte_size }
|
|
end
|
|
|
|
private
|
|
def board_transfer?
|
|
respond_to?(:board_id_changed?) && board_id_changed?
|
|
end
|
|
|
|
def track_board_transfer
|
|
old_board_id = board_id_was
|
|
current_bytes = storage_bytes
|
|
|
|
if current_bytes.positive?
|
|
# Debit old board
|
|
if old_board_id
|
|
Storage::Entry.record \
|
|
account: account,
|
|
board_id: old_board_id,
|
|
recordable: self,
|
|
delta: -current_bytes,
|
|
operation: "transfer_out"
|
|
end
|
|
|
|
# Credit new board
|
|
Storage::Entry.record \
|
|
account: account,
|
|
board: board,
|
|
recordable: self,
|
|
delta: current_bytes,
|
|
operation: "transfer_in"
|
|
end
|
|
end
|
|
|
|
# Override if needed. Default = all direct attachments
|
|
def attachments_for_storage
|
|
ActiveStorage::Attachment.where(record: self)
|
|
end
|
|
end
|