Skip to main content

max / makenotwork

Send storage_used_bytes on the synckit subscription status The field existed on the response, the client parsed it, and the server hardcoded None at every site, so audiofiles' usage bar read zero for everyone. The number was already being computed: confirm_internal_blob sums sync_blobs to enforce the quota on every upload. Lift that sum into sum_blob_bytes, generic over the executor so the confirm path keeps asking inside its own locked transaction, and expose storage_used_bytes for the status and change-cap routes. Summed live rather than cached. The same query runs per upload already and idx_sync_blobs_lookup covers (app_id, user_id) as a prefix.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-21 18:29 UTC
Signed with PGP, not checked
Commit: f8e74ec7655be9d937718a538c37642d7dffee5e
Parent: d59028e
2 files changed, +35 insertions, -10 deletions
@@ -30,6 +30,34 @@
30 30 Ok(blob)
31 31 }
32 32
33 + /// Sum the bytes a user currently stores for an app. `sync_blobs` is the
34 + /// authoritative record, so this is the same number the quota gate enforces
35 + /// against; there is no counter to drift. Generic over the executor so the
36 + /// status route can ask the pool and `confirm_internal_blob` can ask inside
37 + /// its own locked transaction.
38 + async fn sum_blob_bytes<'e, E>(executor: E, app_id: SyncAppId, user_id: UserId) -> Result<i64>
39 + where
40 + E: sqlx::PgExecutor<'e>,
41 + {
42 + let used: i64 = sqlx::query_scalar(
43 + "SELECT COALESCE(SUM(size_bytes), 0)::BIGINT FROM sync_blobs
44 + WHERE app_id = $1 AND user_id = $2",
45 + )
46 + .bind(app_id)
47 + .bind(user_id)
48 + .fetch_one(executor)
49 + .await?;
50 + Ok(used)
51 + }
52 +
53 + /// Bytes a user currently stores for an app, for the subscription-status
54 + /// route. Summed live rather than cached: the same query already runs on every
55 + /// blob confirm, and `sync_blobs` is indexed on `(app_id, user_id)`.
56 + #[tracing::instrument(skip_all)]
57 + pub async fn storage_used_bytes(pool: &PgPool, app_id: SyncAppId, user_id: UserId) -> Result<i64> {
58 + sum_blob_bytes(pool, app_id, user_id).await
59 + }
60 +
33 61 /// Result of an atomic blob-confirm. Every variant is terminal for one confirm
34 62 /// call; the route handler maps it to a 204 or a 402 with the right reason.
35 63 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -130,14 +158,7 @@
130 158 return Ok(BlobConfirm::AlreadyStored);
131 159 }
132 160
133 - let used: i64 = sqlx::query_scalar(
134 - "SELECT COALESCE(SUM(size_bytes), 0)::BIGINT FROM sync_blobs
135 - WHERE app_id = $1 AND user_id = $2",
136 - )
137 - .bind(app_id)
138 - .bind(user_id)
139 - .fetch_one(&mut *tx)
140 - .await?;
161 + let used = sum_blob_bytes(&mut *tx, app_id, user_id).await?;
141 162 let limit = limit.unwrap_or(0);
142 163 if used.saturating_add(size_bytes) > limit {
143 164 return Ok(BlobConfirm::QuotaExceeded {
@@ -265,7 +265,9 @@
265 265 status: Some(s.status),
266 266 storage_limit_bytes: s.storage_limit_bytes,
267 267 pending_storage_limit_bytes: s.pending_storage_limit_bytes,
268 - storage_used_bytes: None,
268 + storage_used_bytes: Some(
269 + db::synckit::storage_used_bytes(&db, sync_user.app_id, sync_user.user_id).await?,
270 + ),
269 271 current_period_end: s.current_period_end.map(|t| t.to_rfc3339()),
270 272 },
271 273 None => SyncSubscriptionStatusResponse {
@@ -460,7 +462,9 @@
460 462 status: Some(sub.status),
461 463 storage_limit_bytes: sub.storage_limit_bytes,
462 464 pending_storage_limit_bytes: Some(req.cap_bytes),
463 - storage_used_bytes: None,
465 + storage_used_bytes: Some(
466 + db::synckit::storage_used_bytes(&db, sync_user.app_id, sync_user.user_id).await?,
467 + ),
464 468 current_period_end: sub.current_period_end.map(|t| t.to_rfc3339()),
465 469 }))
466 470 }