| 1 | # Storage holds |
| 2 | |
| 3 | Owner: whoever answers a workspace whose public pushes are refused for room |
| 4 | it does not use, or a fork whose source refuses pushes. The page is sudo → |
| 5 | Platform → **Storage holds** (`/storage`); the design is in |
| 6 | `services/repos/src/storage_holds.rs` and the docs' architecture page |
| 7 | [Git](https://docs.g1t.sh/architecture/git/#public-storage-reservations). |
| 8 | |
| 9 | A write reserves public storage before it reaches the git store, and a |
| 10 | native fork locks its source while it copies. One that is interrupted keeps |
| 11 | its hold: a reservation counts against the workspace's 5 GB public cap, and |
| 12 | a copy lock refuses every write to the source, private or public (each |
| 13 | told "This repository is being copied into a fork. Try again in a few |
| 14 | minutes; if it lasts more than an hour, g1t staff release it.", never that |
| 15 | storage is full; a push is told the same in git's `remote:` lines; once the |
| 16 | copy has taken longer than it should or did not finish cleanly, that it can |
| 17 | take a few hours, below). A pull |
| 18 | request g1t merges by itself stalls with that message, and a person's |
| 19 | automatic merge turns off with it: neither is tried again by itself once the lock |
| 20 | is released. Nothing expires a hold on a timer, on |
| 21 | purpose: releasing one that is still in flight would let two writes take the |
| 22 | same room. |
| 23 | |
| 24 | ## A source that takes no writes |
| 25 | |
| 26 | A fork whose call to the store timed out or met a server error may still be |
| 27 | copying, so it keeps its source's lock, and the source refuses every write |
| 28 | until staff release it: at the soonest 75 minutes after the copy began (the |
| 29 | lease, an hour, and 15 minutes' grace). The repos service reads a lock as |
| 30 | stalled when its fork's reservation ended, when its fork holds no live |
| 31 | reservation a minute after it locked, or once its lease has passed. Its |
| 32 | writers are then told the copy has taken longer than it should, or did not |
| 33 | finish cleanly, and that it can take a few hours, so do not leave it. |
| 34 | |
| 35 | - **Where it shows.** sudo's Overview has **Writes blocked** under Needs |
| 36 | attention: the repositories a copy lock has held longer than a copy |
| 37 | ordinarily takes (15 minutes), red once one did not finish cleanly. The |
| 38 | Storage holds page lists them under **Writes blocked**, with how long each |
| 39 | has been blocked, how the fork's call ended, and when its lock becomes |
| 40 | releasable. The repos service also logs `copy_lock_kept` (an error, with |
| 41 | the source, the fork and the store's error) when it keeps a lock this |
| 42 | way. |
| 43 | - **What to do.** Inspect the lock: the fork's refs in the store say whether |
| 44 | the copy landed. Once it is releasable, release the **lock first**, with |
| 45 | what you saw; the source takes writes again at once. Then release the |
| 46 | fork's ended reservation the same way. In that order because the ended |
| 47 | reservation is releasable at once while the lock waits for its lease and |
| 48 | grace, and the reservation is what tells sudo how the fork's call ended: |
| 49 | released first, the Writes blocked row loses it (the lock still reads as |
| 50 | stalled, as one whose fork holds no live reservation). Whether the fork |
| 51 | kept the copy, its `stored_bytes` stays as it was: note it in the reason, |
| 52 | and use Drift. |
| 53 | - **A fork that outlived its lock.** If the store's call returns after staff |
| 54 | released the lock, the fork is counted at its source's measured size, a |
| 55 | lower bound that never overcharges: it is the source's `stored_bytes` |
| 56 | read right after the lock, which only grows, and a fork counts in full by |
| 57 | policy, even of its default branch only. The fork is counted before the |
| 58 | lock is let go, so an unlock that fails (logged as `copy_unlock_failed`, |
| 59 | the lock left for staff, reading as stalled) never leaves it uncounted. |
| 60 | The repos service logs |
| 61 | `fork_outlived_copy_lock` (a warning, with the fork and the bytes |
| 62 | counted): the source may have taken writes the fork holds and the count |
| 63 | misses. Drift measures how far it is out. |
| 64 | |
| 65 | ## Provably abandoned |
| 66 | |
| 67 | The repos service decides, from what the write recorded: |
| 68 | |
| 69 | | Hold | Abandoned when | |
| 70 | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | |
| 71 | | A reservation marked `ended` | At once: the write stopped without knowing whether the store kept it, and sends nothing after | |
| 72 | | A reservation still `held` | 15 minutes after its lease, an hour from when it was taken. A push or a branch copied chunk by chunk is cut short at its lease; a write of one pack, and a native fork's one call to the store, are not, and are taken as answered by then | |
| 73 | | Unledgered bytes (reserved before migration 0023, with no row) | 24 hours after the ledger began (`storage_ledger_epoch`): older code had no lease | |
| 74 | | A copy lock with a lease | 15 minutes after its lease. A fork whose call timed out or met a server error keeps its lock until then: the store may still be copying | |
| 75 | | A copy lock without one (older code) | 24 hours after the ledger began, and at least its lease and grace after it was taken | |
| 76 | |
| 77 | Release is offered only then, and the repos service checks again, in the |
| 78 | same batch that releases. |
| 79 | |
| 80 | ## Checklist |
| 81 | |
| 82 | 1. **Find the hold.** On Storage holds, the workspace's repository, oldest |
| 83 | first. A copy lock names the fork it was copying into. |
| 84 | 2. **Inspect it.** Read what the store lists for each repository (refs, the |
| 85 | default branch's commit) and what g1t counts (stored, reserved, how much |
| 86 | of it the ledger accounts for). For a push that ended, each ref it asked |
| 87 | for says **landed** when the store has it: the push may have been stored |
| 88 | though its answer was an error (a 500, or a 200 with a fatal error, |
| 89 | whose outcome quotes it), or a later push may have set the same ref. |
| 90 | 3. **Release it**, with a reason that says what you saw ("ended with a 500; |
| 91 | main is not at the commit it asked for"). The reason and the inspection |
| 92 | are kept in the audit row, listed under **Released**. The release is |
| 93 | also in sudo's audit log (**Storage hold released**, under the |
| 94 | repository's workspace, or `storage` if its row is gone), naming the |
| 95 | row. The repos service keeps that line in the release's own batch |
| 96 | (`storage_recovery_audits`) and sends it to billing right after, keyed by |
| 97 | the row, then every hour until billing confirms it, so it is written |
| 98 | once even if an answer is lost or the page stops waiting. If billing has |
| 99 | not confirmed it yet, the page and the row under **Released** say so, |
| 100 | with billing's last answer: the release still stands, so do not release |
| 101 | it again. A line that stays unconfirmed past a few hours means billing |
| 102 | keeps refusing it: the repos logs have `storage_release_unaudited` with |
| 103 | the error. |
| 104 | 4. **If it is not provably abandoned yet**, the page says when it will be. |
| 105 | Wait: a write that may still be sending is never released by hand. A |
| 106 | workspace that needs room sooner can delete what it does not need. |
| 107 | 5. **If a push ended but landed**, its bytes are not in `stored_bytes`, |
| 108 | which is a lower bound by design and what private storage is billed |
| 109 | from. A release never changes it, and neither does anything here: note |
| 110 | it in the reason, and use Drift to see how far a repository is out. |
| 111 | |
| 112 | ## Drift |
| 113 | |
| 114 | **Measure** streams a small random sample of repositories (1 to 20, those |
| 115 | over 200 MB passed over) from the store: the pack it sends for every ref, |
| 116 | plus what each pull request's working copy holds beyond it, beside |
| 117 | `stored_bytes`. It reads only. Above zero, g1t counts less than the store |
| 118 | holds: pushes made with a credential straight to the store (sandboxes' |
| 119 | `git_access`), ended pushes that landed, g1t's own refs (a pull |
| 120 | request's test merge, `refs/pull/N/merge`), and a pull request's head |
| 121 | copied in from someone's fork are not counted. Below zero, |
| 122 | g1t counted objects that a later push made unreachable (a force push), or |
| 123 | packs that sent objects the store already had: g1t asks for whole packs. |
| 124 | Before #307, g1t also counted writes the store refused (a merge or update |
| 125 | whose branch had moved, an `ng`, a failed unpack); since, a refused write |
| 126 | gives its reservation back and counts nothing. Large or growing |
| 127 | drift is for the owner of billing to decide on; nothing corrects it |
| 128 | automatically. |