Skip to content

orbital migrate finalize

orbital migrate finalize is the lazy-proxy strategy capstone. Once a remote-mode repository has warmed enough that incoming pulls are predominantly cache hits, finalize flips the row to local-mode: the upstream pointer is dropped and OrbitalReg becomes authoritative for the cached blobs. Run finalize once per migrated repo (or in a single sweep across all of them) when you are ready to decommission the source registry.

The CLI is a thin layer over POST /api/admin/migrate/finalize.

When to run finalize

Finalize only matters for the lazy-proxy strategy. big-bang and blue-green migrations land local-mode repositories from day one and never need a finalize step.

A typical lazy-proxy timeline:

DayAction
0orbital migrate planorbital migrate apply lands remote-mode repos pointing at the source.
1 – 30Consumers pull from OrbitalReg; the cache fills as the repo absorbs traffic.
7orbital migrate verify confirms byte-equivalence on the cached subset.
30+orbital migrate finalize --plan migration-plan.json (preview) — review which repos have warmed.
30+orbital migrate finalize --plan migration-plan.json --convert — flip the warm ones to local.
60+When every migrated repo is local, decommission the source registry.

Run finalize as a recurring task (cron, scheduled CI) without --convert — it is read-only by default and surfaces which repos are still warming.

How the cache-hit rate is computed

OrbitalReg does not stamp a per-pull "served from cache" flag on artifact_pulls. Finalize derives the rate from the pull pattern itself:

text
pulls_total       = COUNT(*)              FROM artifact_pulls (per repo, last N days)
distinct_artifacts = COUNT(DISTINCT artifact_id)
cache_hit_rate    = (pulls_total - distinct_artifacts) / pulls_total * 100

The intuition: each artifact's first pull is a probable cache miss (OrbitalReg fetched it from upstream), and every subsequent pull of the same artifact is a cache hit (served from local storage). A repo where most pulls are repeats has effectively warmed; one with mostly unique artifacts is still actively populating its cache.

This is a deliberate proxy, not a precise instrument. It biases slightly conservative — repos that re-fetch the same artifact via upstream-revalidation paths (e.g. a remote-mode repo with short upstream-cache TTL) under-report their hit rate, which means finalize waits longer than strictly necessary. That is the safer side to err on.

Decisions

Each repo's verdict is one of:

DecisionMeaning
converted(--convert only) repo flipped from remote → local in this run.
qualifiesCache-hit rate cleared the threshold + sample floor; would convert under --convert.
below_thresholdSample size sufficient but cache-hit rate is under the threshold. Wait longer or lower the bar.
insufficient_samplesPull count in the window is below the minimum (--min-samples). Either traffic is light or the migration is too fresh.
already_localRepo is already local-mode (a previous finalize, or an operator manually flipped it). No-op.
repo_missingPlan references this repo but no (project, slug) row exists. Apply was never run, or the repo was skipped.

Defaults

FlagDefaultNotes
--threshold95 (%)Conservative — most lazy-proxy migrations clear this within 30 d. Set lower (e.g. 80) for more aggressive cutover.
--min-samples100Per-repo pull-count floor inside the window. Avoids converting a quiet repo that hasn't seen real traffic.
--window-days30Capped at 365. The window matches the artifact_pulls retention horizon most operators run.
--convertfalseRead-only by default. Add --convert to actually flip qualifying repos.

Quick start

bash
# 1. Apply a lazy-proxy plan (remote-mode repos that proxy back to the source)
orbital migrate plan --source jfrog \
  --endpoint https://jfrog.example.com \
  --token "$JFROG_TOKEN" \
  --output-file migration-plan.json

orbital migrate apply --plan migration-plan.json --strategy lazy-proxy

# 2. Wait. Consumers pull through OrbitalReg; the cache warms up.

# 3. Preview which repos are ready to finalize
orbital migrate finalize --plan migration-plan.json

# 4. Flip the qualifying repos to local-mode
orbital migrate finalize --plan migration-plan.json --convert

Sample preview output:

text
Migration finalize — mode=preview — source=jfrog project=jfrog-import
  Threshold:    95.00% cache-hit-rate (min 100 pull(s) in 30-day window)
  Repos:        12 examined, 0 skipped
  Decisions:
    qualifies               7
    below_threshold         3
    insufficient_samples    2
  Per-repo:
    [qualifies]             maven-prod — pulls=4811 distinct=132 hit-rate=97.26%
    [qualifies]             npm-mirror — pulls=2204 distinct=87 hit-rate=96.05%
    [below_threshold]       docker-hot — pulls=512 distinct=180 hit-rate=64.84%
    [insufficient_samples]  obscure-feed — pulls=12 distinct=12 hit-rate=0.00%


Next steps:
  - Re-run with --convert to flip the qualifying repos to local-mode.

Sample convert output:

text
Migration finalize — mode=convert — source=jfrog project=jfrog-import
  Threshold:    95.00% cache-hit-rate (min 100 pull(s) in 30-day window)
  Repos:        12 examined, 0 skipped
  Decisions:
    converted               7
    below_threshold         3
    insufficient_samples    2


Next steps:
  - Repos flipped to local-mode; their upstream pointer is cleared.
  - When all migrated repos are local, the source registry can be decommissioned.

Idempotency

Finalize is safe to run repeatedly. A --convert run that flips a repo to local-mode will report already_local for that same repo on every subsequent run. The kind='remote' guard inside the SQL UPDATE short-circuits a re-flip even under concurrent operators.

Tuning notes

  • Aggressive cutover (small registries). Drop --threshold to 80 and --min-samples to 25. Keeps decommission tight at the cost of occasional upstream-fetch surprises if a long-tail artifact re-enters circulation.
  • Patient cutover (regulated sectors). Hold the default 95% but bump --min-samples to 500 and --window-days to 90. Finalizes only the genuinely warm core; long tails remain proxied.
  • Per-format finalize. Finalize does not currently filter by format — it walks every repo in the plan. Operators who want to finalize Maven first and Docker later can split the migration plan into two apply runs and finalize them independently.

Exit codes

Same deterministic mapping as the rest of orbital migrate:

ExitMeaning
0Successful run (regardless of how many repos qualified). Finalize is a maintenance command — "no qualifying repos yet" is not an error.
1User error (bad flag, unreadable plan file).
2Authentication failure (401 / 403 from OrbitalReg).
3Server error (500).

Out of scope

  • Per-repo override. --convert is all-or-nothing; partial conversion is achieved by editing the plan file or running finalize multiple times with different --threshold values.
  • Finalize-after-traffic-shaping. A future revision may reweight recent pulls higher than older ones. Today's window is a flat arithmetic average.
  • Source-side health check during convert. Finalize never re-contacts the source; it only reads OrbitalReg-side pull data. The byte-equivalence guarantee is what orbital migrate verify provides.

See also

Released under the Apache-2.0 License.