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
59 lines
1.7 KiB
Ruby
59 lines
1.7 KiB
Ruby
require "test_helper"
|
|
|
|
class Storage::MaterializeJobTest < ActiveJob::TestCase
|
|
setup do
|
|
@account = accounts("37s")
|
|
@board = boards(:writebook)
|
|
end
|
|
|
|
test "calls materialize_storage on account" do
|
|
Storage::Entry.record(account: @account, delta: 1024, operation: "attach")
|
|
|
|
Storage::MaterializeJob.perform_now(@account)
|
|
|
|
assert_not_nil @account.storage_total
|
|
assert_equal 1024, @account.bytes_used
|
|
end
|
|
|
|
test "calls materialize_storage on board" do
|
|
Storage::Entry.record(account: @account, board: @board, delta: 2048, operation: "attach")
|
|
|
|
Storage::MaterializeJob.perform_now(@board)
|
|
|
|
assert_not_nil @board.storage_total
|
|
assert_equal 2048, @board.bytes_used
|
|
end
|
|
|
|
test "job is idempotent" do
|
|
Storage::Entry.record(account: @account, delta: 1024, operation: "attach")
|
|
|
|
3.times { Storage::MaterializeJob.perform_now(@account) }
|
|
|
|
assert_equal 1024, @account.bytes_used
|
|
end
|
|
|
|
test "job processes entries added between runs" do
|
|
Storage::Entry.record(account: @account, delta: 1000, operation: "attach")
|
|
Storage::MaterializeJob.perform_now(@account)
|
|
|
|
# Small delay to ensure UUIDv7 timestamp advances
|
|
travel 1.second
|
|
|
|
Storage::Entry.record(account: @account, delta: 500, operation: "attach")
|
|
Storage::MaterializeJob.perform_now(@account)
|
|
|
|
assert_equal 1500, @account.bytes_used
|
|
end
|
|
|
|
test "job queued to backend queue" do
|
|
assert_equal "backend", Storage::MaterializeJob.new.queue_name
|
|
end
|
|
|
|
test "job has concurrency limit by owner" do
|
|
job = Storage::MaterializeJob.new(@account)
|
|
# limits_concurrency is a Solid Queue feature
|
|
# Just verify the job can be instantiated and has the correct queue
|
|
assert_equal "backend", job.queue_name
|
|
end
|
|
end
|