Commit Graph

123 Commits

Author SHA1 Message Date
Jeremy Daer bd6c1cf34f Storage: harden reconcile for concurrent writes and fix board transfer (#2096)
* Storage: harden reconcile for concurrent writes and fix board transfer

Reconcile now uses two-cursor approach: captures cursor before and after
the storage scan, aborting if they differ (entries added during scan).
Job retries 3x with 1-minute waits and limits concurrency to 1 per owner.

Board transfer now correctly moves storage for card description embeds
and comment embeds, not just direct attachments. Uses batched queries
to handle cards with thousands of comments efficiently.

Also fixes N+1 queries in attachment grouping via lookup maps.

* Storage: fix per-attachment reconcile and enforce no blob reuse

The storage ledger tracks per-attachment (not per-blob) as a business
abstraction for quotas. This fixes reconcile to match that model and
adds enforcement to prevent blob reuse in tracked contexts.

* Reconcile now uses joins(:blob).sum() for per-attachment counting
* New validation prevents reusing blobs across tracked attachments
* Race-safe storage_total creation with create_or_find_by
* Job efficiency: skip find_by when object already available
* Backfill script checks per-attachment, not just per-blob
2025-12-19 13:07:32 -08:00
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
Jorge Manrubia cb0e9b9962 Immediate avatar and embed variants (#2002)
Reverts #2001
Restores #1955
2025-12-08 14:17:06 -08:00
Jorge Manrubia c8c91259c7 Revert "Immediate avatar and embed variants" 2025-12-07 12:06:03 +01:00
Jeremy Daer 21f3f72647 Immediate avatar and embed variants
Process variants synchronously on attachment to close the window between
image upload and variant availability, guaranteeing that we won't have
lazy variant processing attempts in GET requests.

Tradeoff is that we do variant processing in upload requests, which is
actually desirable. We're working with images that should take
milliseconds to resize given that we'll already have the file on hand.

References https://github.com/rails/rails/pull/51951
2025-12-04 23:54:37 -08:00
Karl Entwistle 59d7229500 Move Eventable concern to app/models/concerns
Aligns with Rails conventions for organizing concerns in a dedicated
concerns directory alongside existing concerns like Attachments,
Mentions, Searchable, etc.
2025-12-03 16:36:15 +00:00
Donal McBreen 9c86205510 Enforce SQLite column limits via CHECK constraints
Patch the sqlite adapter to add CHECK constraints for string and text
column limits. We'll do them inline, so that any column changes
automatically update the constraints.
2025-11-26 09:50:29 +00:00
Donal McBreen 97bcdf1853 Enforce column limits via concern
SQLite columns lengths are purely informational, so we'll enforce the
limits via a concern that checks the lengths from the schema.
2025-11-25 15:32:34 +00:00
Donal McBreen b571303385 Don't use Current.account for search records
It doesn't actually work, and even if we could make it work reliably
we are better off if the records always know to go to the right shard.

It does make the interface a bit more complicated as we need to select
the right shard class with `for(account_id)`.
2025-11-25 11:34:41 +00:00
Donal McBreen aaf512875e Add search_record has_one 2025-11-21 14:34:41 +00:00
Donal McBreen 01d16f96d4 Remove Search::Record.for_account
Instead we'll compute the table name dynamically based on
Current.account where needed. Also we'll prevent searchable records
from being saved if Current.account is not set, otherwise the after
commit callbacks will fail.
2025-11-21 12:09:21 +00:00
Donal McBreen b92aba383e account_key is MySQL specific 2025-11-21 11:47:28 +00:00
Donal McBreen 366dbd393c Merge branch 'main' into sqlite 2025-11-21 11:39:47 +00:00
Donal McBreen 70dd754cf7 Account key for search records
Add an account key field to improve search performance. This field
allows us to filter the records by account directly in the fulltext
index, so we only need to examine rows belonging to the relevant account.
2025-11-21 09:38:35 +00:00
Donal McBreen ec06b7085b No need to default created_at 2025-11-21 09:15:19 +00:00
Donal McBreen a4850fbe73 Remove adapter specific logic from searchable 2025-11-21 09:15:19 +00:00
Donal McBreen bb36b4846f Support SQLite searching 2025-11-21 09:15:19 +00:00
Mike Dalessio 2c765fa03f rubocop: style fixes 2025-11-17 09:12:41 -05:00
Donal McBreen 28efe28f24 Replace Search::Index with Search::Records
Lean on ActiveRecord models for searching and strip out the raw SQL.
Replaces the search_index_* tables with sharded search_records_* tables
as that allows us to use a Search::Record model name.

A Class is dynamically created for each record table shard so that we
and we can access it via the Search::Record.for_account(account_id)
method.
2025-11-17 09:12:40 -05:00
Mike Dalessio 6df681991f rubocop: fix style warnings 2025-11-17 09:12:36 -05:00
Donal McBreen b8baeadf66 Use a deterministic string hash 2025-11-17 09:12:36 -05:00
Donal McBreen f4729577b6 Use uuids in the search_index tables too 2025-11-17 09:12:35 -05:00
Donal McBreen fc56ad9d7c Combine uuid-old and uuid branch changes
- Switch to binary 16 for UUID keys
- Remove AccountScopedRecord base class, all model use binary uuids now
- Fix the search sql to serialize uuids properly
- Patch the MySQL schema dumper to output binary lengths
2025-11-17 09:12:34 -05:00
Donal McBreen 05a7ec84e2 Make search tables work with UUID references 2025-11-17 09:12:31 -05:00
Kevin McConnell a81986816f Move test-related UUID helper 2025-11-17 09:12:31 -05:00
Kevin McConnell e9e68646e3 Fix this test without changing app code 2025-11-17 09:12:31 -05:00
Kevin McConnell 63f8a9af78 Deterministic fixture UUIDs 2025-11-17 09:12:31 -05:00
Kevin McConnell 6c04b56f4b Compact the UUIDs to base36 2025-11-17 09:12:31 -05:00
Mike Dalessio e4011ef211 Update primary keys on customer data to UUIDs
- schema changes to primary and foreign keys
- fixture changes
- customer data models subclass AccountScopedRecord
- import script updated
2025-11-17 09:12:30 -05:00
Donal McBreen 3aa4a1a562 Shard the search index into 16 tables
Create search_index_0 to search_index_15 tables and shard each index by
account id. MySQL has no ability to pre-filter fulltext indexes by
another field so this is the best bet for improving performance.

Each fulltest index internally creates 11 sub tables (see
https://dev.mysql.com/doc/refman/8.4/en/innodb-fulltext-index.html) so
actually we have 192 tables in total here.

The search_index table name is generated dynamically based on the
account_id.
2025-11-17 09:12:17 -05:00
Donal McBreen ddd7fe082a Ensure the mentioning scope search works
Include the filter by accessible board ids to avoid large joins from the
search_index table. Drop the unused search scope.
2025-11-17 09:12:17 -05:00
Donal McBreen e8abc66eba MySQL search
Add a single search_index table for full-text search of cards and
comments.

For the search there is a full-text index on the title and content
columns. The board_ids is also included in the table and accessible
board ids are pre-loaded and included in the search query. This allows
us to filter out inaccessible records before joining with other tables.

Right now the search is just using boolean search. This would give us
a bunch of syntax options
(see https://dev.mysql.com/doc/refman/8.4/en/fulltext-boolean.html)
except the search query filters those out.

I've removed the searchable_by method for now - everything is built
on the assumption that there's a single search index table and all data
must fit into it.

Queries are written in SQL, we don't have a SearchIndex ActiveRecord
model. That's because we'll likely want to shard the table and it will
be simpler to just keep with the raw SQL for that.

There's no stemming, highlighting or snippet extraction yet - we are
dumping the full description in the search results.

Data can be reindexed with the search:reindex rake task.
2025-11-17 09:12:17 -05:00
Mike Dalessio 4ad1718d86 Stub searchable functionality and start to restore login/auth 2025-11-17 09:11:42 -05:00
Jorge Manrubia 56a85c261b Remove plain text mentions
They can cause duplicates

https://app.fizzy.do/5986089/cards/2874

Closes #https://github.com/basecamp/fizzy/pull/1549
2025-11-11 10:01:59 +01:00
Jorge Manrubia b686936230 Fix: extract mentions when publishing cards
This wasn't working when creation and publishing happened on separated instances
of the same record, which is what happens when you edit a draft and then you
publish it

https://app.fizzy.do/5986089/cards/2835/edit
2025-11-10 13:08:29 +01:00
Jorge Manrubia 03a345609e Baseline replacing collection with board across code 2025-11-05 13:31:54 +01:00
Jorge Manrubia 825184ab1b Memoize since these are invoked several times 2025-11-03 07:30:23 +01:00
Jorge Manrubia 2ec5d37947 Add support for remote videos too 2025-11-02 09:38:48 +01:00
Jorge Manrubia ac507aa754 Add support for remote images in the seeding system 2025-11-02 09:24:38 +01:00
Jorge Manrubia aa1ffb3357 Remove AI summaries and semantic searches
We were not using either. We can restore if we revisit.
2025-10-29 11:55:32 +01:00
Jorge Manrubia 1c6adf54a8 Restore semantic searches 2025-07-24 16:47:57 +02:00
Jason Zimdars 0afc738d19 Whitespace 2025-07-21 20:11:03 -05:00
Jason Zimdars a6e9e51071 Merge branch 'main' into web-push
* main: (28 commits)
  Clean up the /scripts directory
  Prevent recursion when invoking AI
  Bump sqlite3 from 2.7.2 to 2.7.3
  Port over Jeremy's fix for slow uploads
  .gitignore log files and portfolio's local storage
  Drop Current.account and just use Account.sole
  Use the utf8-patched mysql driver
  Call blob.preview for previewable attachments
  Make sure the image variants we use are preprocessed
  Add /user command
  Make sure it doesn't infer custom abbreviations when matching user names
  Script to load prod db in development
  Update Signup to use the queenbee_id for the tenant name
  Bump cache version
  Make sure it uses latest bundler to prevent error due to mismatch
  Invalidate cache
  Update ruby
  Update bundler to prevent error on lower version
  Update bundler to prevent error on lower version
  Resolve stages
  ...
2025-07-21 11:03:56 -05:00
Mike Dalessio ef6198d394 Make sure the image variants we use are preprocessed
to avoid implicitly creating a variant during a GET to a read replica,
which will raise an exception and cause the image to fail to load.

This feels pretty brittle, to be honest, and we'll need to be careful
in the future about creating and using new variants; we will probably
need to backfill the new variant for existing images because we can't
guarantee that they will be created implicitly/lazily if the GET lands
on the replica.

ref: https://fizzy.37signals.com/5986089/collections/7/cards/942
2025-07-18 15:43:31 -04:00
Jorge Manrubia a1e3835eb8 Prevent 500 when running /search with empty string 2025-07-18 10:55:36 +02:00
Jason Zimdars 38d13f78a3 Add pusher models and job 2025-07-15 13:16:18 -05:00
Jason Zimdars 97e20c5cd7 Make nil safe 2025-07-08 16:57:48 -05:00
Jorge Manrubia b8b4ef5463 Extract common concern to examine attachments 2025-07-04 17:53:37 +02:00
Jorge Manrubia 17f9df8df3 Don't create mentions from drafts until those are published
https://fizzy.37signals.com/5986089/collections/2/cards/982
2025-07-04 17:25:39 +02:00
Jorge Manrubia 9e3dbb0858 Remove semantic searches
We may revisit in the future but we'll go with keyword search only for launch
2025-06-25 09:36:14 +02:00