SnowflakeService::next() read the sequence with Cache::get() and, on the
non-initial branch, incremented the cache store but never refreshed the
local $seq. As a result the first two IDs in a fresh sequence both used
seq=1 (values ran 1, 1, 2, 3, ...). Two IDs generated in the same
millisecond with the same (randomly chosen) datacenter/worker then
produced identical snowflakes, causing intermittent
'UNIQUE constraint failed: statuses.id' errors — e.g. the flaky
SeasonalAggregationTest which rapidly creates several statuses.
Use Cache::increment()'s atomic post-increment return value as the
sequence so every call gets a distinct seq (1, 2, 3, ...), reseeding
when the store is empty/non-numeric and preserving the 4095 wraparound.
Laravel 11 exposes MariaDB as a dedicated 'mariadb' driver, so
config('database.default') === 'mysql' checks silently misclassified
MariaDB as the non-mysql (postgres) branch.
- Add App\Util\Database\DatabaseDriver with isMysqlLike()/isPgsql()
plus db_is_mysql_like()/db_is_pgsql() global helpers.
- Route all database.default driver checks through the helpers so
MySQL and MariaDB are treated as one group.
- Use '' (not null) for share/compose caption+rendered, valid whether
the column is nullable or NOT NULL (it is NOT NULL on MySQL/MariaDB).
- Guard pgsql strtolower() in registration against missing fields.
- Scope CustomEmoji::duplicateShortcodes to the grouped column for
Postgres GROUP BY validity.
- Remove stale Postgres guard in status:dedup; use havingRaw for
cross-driver HAVING.
queueDelivery() runs synchronously from the v1 follow/unfollow endpoints
(via Helpers::sendSignedObject), which commit local state before delivery
and have no try/catch. After the Http::send() rewrite, a ConnectionException
from a momentarily-unreachable remote was rethrown out of queueDelivery(),
turning a best-effort delivery into a 500 for the user after the follow/
unfollow was already persisted. For unfollows, a retry then hit the
isFollowing==false branch and never re-sent the Undo, diverging state.
Treat transport failures (ConnectionException) as best-effort on this
single-delivery path: log, record host health, and return without
propagating. Other exception types (invalid sender/destination, signing,
serialization) still throw, matching pre-rewrite precondition behavior.
Also widen SendUpdateActor's per-user catch from HttpException to Throwable
so a single bad host no longer aborts a fleet-wide actor update (the old
HttpException catch is dead for ConnectionException/invalid-destination).
Notification status hydration compared item_type strictly against
Status::class (App\Models\Status). Rows created before the App\ ->
App\Models\ namespace migration store the legacy 'App\Status' morph-map
alias, so the comparison failed and favourite/comment/mention
notifications came back with no attached status. The web UI filters those
out client-side but keeps paginating (response never empty), leaving the
infinite-scroll loader spinning forever.
- NotificationTransformer + Mastodon NotificationTransformer: match both
the legacy alias and the current FQCN when hydrating status.
- NotificationService::buildNotification: same alias-aware deleted-item guard.
- NotificationService::getMaxPage/getMinPage: filter out unrenderable
notifications (status-type without a hydrated status) so the endpoint
never returns rows the UI discards, fixing pagination termination; warn
on unexpected notification types.
- Tests for transformer hydration (legacy + current), renderable filtering,
the unexpected-type warning, and pagination termination.
Aligns with the app's dominant convention (171 static Str:: calls vs
24 Str::of() chains). Uses Str::afterLast() for the repeated
"segment after last slash" pattern, Str::matchAll() where a
Collection return is needed, and native explode()/substr() where a
plain array/string suffices.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Aligns with the app's dominant convention (413 now()/today() call
sites vs 12 Carbon::now()). Carbon::parse() calls are untouched since
they parse arbitrary date strings, not current-time access.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
UserStorageService::get() now recalculates from source when the cached
counter is missing or older than STALE_AFTER_HOURS, instead of returning a
possibly-inflated cached value. This is what unblocks a user stuck at the
account size limit: the limit check on their next upload attempt reads the
freshly recalculated real usage rather than the drifted value (#7169).
The upload flow reads get() and enforces the limit BEFORE the write-path
heal runs, so a blocked user could never self-heal via upload/delete alone.
Healing on read closes that gap and makes the scheduled reconciler a
belt-and-suspenders safety net rather than a requirement.
A fresh counter is still trusted as-is (no per-read SUM). Adds tests for the
stale-get recompute and fresh-get trust paths.
Make increaseStorageUsed/decrementStorageUsed recalculate from source when
the cached counter is older than STALE_AFTER_HOURS (168h) or never
calculated, so an affected user is corrected the next time they upload or
delete without waiting for the nightly reconciler. Callers save/delete the
media row before calling these, so the from-source recalc already reflects
the change and the incremental delta is skipped on the recalc path.
- Add UserStorageService::STALE_AFTER_HOURS and isStale() helper (no extra
query: reads the already-loaded model), with defensive Carbon parsing
- Cast users.storage_used_updated_at to datetime so freshness comparisons
work on a Carbon instance
- Add tests for stale/fresh/never-calculated increase and decrement paths
users.storage_used only ever grew: uploads incremented it but no deletion
path decremented it, so users hit the account size limit even when their
real media usage was well below it.
- Decrement storage_used in MediaDeletePipeline when media is removed
- Add UserStorageService::increaseStorageUsed / decrementStorageUsed as the
fast, symmetric hot-path counter updates (floor-based, clamped at zero)
- Refactor the 6 upload call sites to use increaseStorageUsed instead of
duplicated inline writes (also fixes ceil/floor drift vs the reconciler)
- Add (user_id, size) covering index so per-user SUM(size) is not a full
table scan (INPLACE/LOCK=NONE, skipped on sqlite)
- Add user:storage:recalculate command to repair affected accounts, with a
daily --stale=168 scheduled reconciler to correct any drift
- Add regression tests for the pipeline and UserStorageService