Files
fizzy/test/models/storage/entry_test.rb
T
Jeremy Daer 761d0b4a76 Speedy, auditable, deadlock-resistant storage tracking (#2026)
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
2025-12-10 16:04:30 -08:00

169 lines
4.4 KiB
Ruby

require "test_helper"
class Storage::EntryTest < ActiveSupport::TestCase
setup do
@account = accounts("37s")
@board = boards(:writebook)
@card = cards(:logo)
end
test "record! creates entry with positive delta" do
assert_difference "Storage::Entry.count", +1 do
entry = Storage::Entry.record \
account: @account,
board: @board,
recordable: @card,
delta: 1024,
operation: "attach"
assert_equal @account.id, entry.account_id
assert_equal @board.id, entry.board_id
assert_equal @card.class.name, entry.recordable_type
assert_equal @card.id, entry.recordable_id
assert_equal 1024, entry.delta
assert_equal "attach", entry.operation
end
end
test "record! creates entry with negative delta" do
entry = Storage::Entry.record \
account: @account,
board: @board,
recordable: @card,
delta: -512,
operation: "detach"
assert_equal -512, entry.delta
assert_equal "detach", entry.operation
end
test "record! returns nil and creates no entry when delta is zero" do
assert_no_difference "Storage::Entry.count" do
result = Storage::Entry.record \
account: @account,
board: @board,
recordable: @card,
delta: 0,
operation: "attach"
assert_nil result
end
end
test "record! accepts _id params for after_destroy_commit snapshots" do
entry = Storage::Entry.record \
account_id: @account.id,
board_id: @board.id,
recordable_type: "Card",
recordable_id: @card.id,
delta: 2048,
operation: "detach"
assert_equal @account.id, entry.account_id
assert_equal @board.id, entry.board_id
assert_equal "Card", entry.recordable_type
assert_equal @card.id, entry.recordable_id
end
test "record! creates entry without board" do
entry = Storage::Entry.record \
account: @account,
board: nil,
recordable: @card,
delta: 1024,
operation: "attach"
assert_nil entry.board_id
end
test "record! creates entry without recordable" do
entry = Storage::Entry.record \
account: @account,
board: @board,
recordable: nil,
delta: 1024,
operation: "reconcile"
assert_nil entry.recordable_type
assert_nil entry.recordable_id
end
test "record! enqueues MaterializeJob for account" do
assert_enqueued_with job: Storage::MaterializeJob, args: [ @account ] do
Storage::Entry.record \
account: @account,
board: nil,
recordable: nil,
delta: 1024,
operation: "attach"
end
end
test "record! enqueues MaterializeJob for board when board_id present" do
assert_enqueued_with job: Storage::MaterializeJob, args: [ @board ] do
Storage::Entry.record \
account: @account,
board: @board,
recordable: nil,
delta: 1024,
operation: "attach"
end
end
test "record! does not enqueue job when account is deleted" do
# The graceful handling is that find_by returns nil, so no job is enqueued
# for a non-existent account. We can't test with a fake ID due to FK constraints,
# but we can verify the find_by behavior by stubbing.
Account.stubs(:find_by).returns(nil)
assert_no_enqueued_jobs only: Storage::MaterializeJob do
Storage::Entry.record \
account: @account,
delta: 1024,
operation: "attach"
end
end
test "record! does not enqueue board job when board is deleted" do
Board.stubs(:find_by).returns(nil)
# Account job still enqueued, but board job is not
entry = Storage::Entry.record \
account: @account,
board: @board,
delta: 1024,
operation: "attach"
assert_not_nil entry
end
test "entries belong to account" do
entry = Storage::Entry.record \
account: @account,
delta: 1024,
operation: "attach"
assert_equal @account, entry.account
end
test "entries belong to board (optional)" do
entry = Storage::Entry.record \
account: @account,
board: @board,
delta: 1024,
operation: "attach"
assert_equal @board, entry.board
end
test "entries belong to recordable (polymorphic, optional)" do
entry = Storage::Entry.record \
account: @account,
recordable: @card,
delta: 1024,
operation: "attach"
assert_equal @card, entry.recordable
end
end