Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com> Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
This commit was merged in pull request #34.
This commit is contained in:
+32
-13
@@ -1,7 +1,7 @@
|
||||
# SQLite → Postgres cutover runbook
|
||||
|
||||
One-way, one-time. Moves the owner's reading history out of the retired SQLite
|
||||
volume (`bookmarkmanager_bookmarks-data`, holding `/data/bookmarks.db`) and into
|
||||
volume (`<compose project>_bookmarks-data`, holding `/data/bookmarks.db`) and into
|
||||
the Postgres schema the migration runner builds. There is no dual-write period:
|
||||
the old database is read once, at cutover, from a **fresh export** — anything
|
||||
written to SQLite after the export is lost, so the old API must already be down.
|
||||
@@ -32,10 +32,10 @@ is dropped) — writing it from the spec below costs less than maintaining it
|
||||
would.
|
||||
|
||||
Beyond `DEPLOY.md`'s prerequisites (Docker and Compose), this runbook needs
|
||||
`python3` on the machine running §3 — its stdlib `sqlite3` module is the whole
|
||||
SQLite dependency — and `jq` for the one read-path check in §5. Neither has to
|
||||
be the server: §3 only reads the snapshot copy, so it can run on a laptop and
|
||||
the resulting `import.sql` be copied over.
|
||||
`python3`: its stdlib `sqlite3` module is the whole SQLite dependency, and §5's
|
||||
read-path check uses it in place of `jq`, which the server does not have. It
|
||||
does not have to run on the server — §3 only reads the snapshot copy, so it can
|
||||
run on a laptop and the resulting `import.sql` be copied over.
|
||||
|
||||
---
|
||||
|
||||
@@ -45,21 +45,37 @@ the resulting `import.sql` be copied over.
|
||||
already stale.
|
||||
|
||||
```bash
|
||||
cd /opt/bookmarkmanager
|
||||
cd ~/mangaBookmark # wherever the checkout lives
|
||||
COMPOSE="docker compose -f docker-compose.yml -f docker-compose.prod.yml"
|
||||
BACKUP_DIR="$(cd .. && pwd)/bookmarkmanager-backups"; mkdir -p "$BACKUP_DIR"
|
||||
BACKUP_DIR="$(cd .. && pwd)/$(basename "$PWD")-backups"; mkdir -p "$BACKUP_DIR"
|
||||
STAMP=$(date -u +%Y%m%d-%H%M%S)
|
||||
|
||||
# The volume is <compose project>_bookmarks-data, and the project name defaults
|
||||
# to the lowercased *directory* name, not the repo name — on this host the
|
||||
# checkout is ~/mangaBookmark, so the volume is mangabookmark_bookmarks-data.
|
||||
# Derive it exactly rather than with a `--filter name=` substring match, which
|
||||
# would return every volume whose name merely contains the string.
|
||||
VOL="$(basename "$PWD" | tr '[:upper:]' '[:lower:]')_bookmarks-data"
|
||||
docker volume inspect "$VOL" >/dev/null && echo "$VOL"
|
||||
|
||||
$COMPOSE stop bookmark-api
|
||||
|
||||
# Copy the file straight out of the retired volume. Nothing is writing to it,
|
||||
# so a plain copy is consistent — no -wal to worry about after a clean stop.
|
||||
docker run --rm -v bookmarkmanager_bookmarks-data:/from:ro -v "$BACKUP_DIR":/to \
|
||||
# A clean SIGTERM closes the store, which checkpoints and unlinks the -wal, so
|
||||
# bookmarks.db alone is then the whole database. But `compose stop` SIGKILLs
|
||||
# after 10s, and a surviving -wal holds writes the main file does not — assert
|
||||
# it is gone rather than assuming the shutdown was clean.
|
||||
docker run --rm -v "$VOL":/d:ro alpine ls -l /d # -> bookmarks.db, alone
|
||||
|
||||
docker run --rm -v "$VOL":/from:ro -v "$BACKUP_DIR":/to \
|
||||
alpine cp /from/bookmarks.db "/to/bookmarks-$STAMP.db"
|
||||
|
||||
ls -lh "$BACKUP_DIR/bookmarks-$STAMP.db"
|
||||
```
|
||||
|
||||
If `-wal` and `-shm` are still there, the container was killed mid-write. Copy
|
||||
all three under the same basename and let SQLite replay the log when §3 opens
|
||||
it — copying only `bookmarks.db` silently drops whatever the log still holds.
|
||||
|
||||
Work on a **copy** of that file for the rest of this runbook. The export is the
|
||||
last line of retreat; nothing below should be able to write to it.
|
||||
|
||||
@@ -256,7 +272,8 @@ would catch a correct import behind a broken join:
|
||||
```bash
|
||||
API=https://bookmark-api.violetcrown.my.id
|
||||
TOKEN=$(grep -E '^API_TOKEN=' .env | cut -d= -f2) # grace-window credential
|
||||
curl -s -H "Authorization: Bearer $TOKEN" $API/bookmarks | jq 'length' # -> 29
|
||||
curl -s -H "Authorization: Bearer $TOKEN" $API/bookmarks |
|
||||
python3 -c 'import json,sys; print(len(json.load(sys.stdin)))' # -> 29
|
||||
```
|
||||
|
||||
---
|
||||
@@ -265,8 +282,10 @@ curl -s -H "Authorization: Bearer $TOKEN" $API/bookmarks | jq 'length' # -> 29
|
||||
|
||||
- **Keep the old SQLite volume for a month.** It is already undeclared in
|
||||
compose, so `docker compose down -v` cannot take it. Remove it by hand once
|
||||
the Postgres data has been trusted for a while:
|
||||
`docker volume rm bookmarkmanager_bookmarks-data` (see `REDEPLOY.md` §1).
|
||||
the Postgres data has been trusted for a while. That happens in a shell where
|
||||
`$VOL` from §1 is long gone, so re-derive it:
|
||||
`docker volume rm "$(basename ~/mangaBookmark | tr '[:upper:]' '[:lower:]')_bookmarks-data"`
|
||||
(see `REDEPLOY.md` §1).
|
||||
- **Delete the generator and the working copies:** `rm -rf /tmp/cutover`. The
|
||||
timestamped export in `$BACKUP_DIR` is the copy that is kept.
|
||||
- **Take the first Postgres dump immediately** — `REDEPLOY.md` §1. Until that
|
||||
|
||||
Reference in New Issue
Block a user