ADR 010: Registry backends are plugins, found by name¶
Status: accepted, 0.9.0
Context¶
The model registry had a protocol (RegistryBackend) and an entry point group
(ubunye.registry_backends, with filesystem and mlflow), but no running
code used them. ubunye models * and the model transform built
ModelRegistry(store) directly, so a third party registry could be installed
and never reached. A private auto detect picked mlflow when it saw
Databricks, but only tests called it. ADR 001 fixed the same problem for
execution backends.
The protocol is also narrower than what users call: it has register, get, list_versions, promote, demote and delete, while the CLI and the Python API also roll back, archive, compare and check promotion gates.
Decision¶
Registry backends are found by name in ubunye.registry_backends, the shipped
ones exactly like a third party one (ubunye.models.backends). A registry
backend is chosen in one order, shared with lineage backends:
- Asked for by name:
--backend/-bonubunye models *,registry.backendin a model transform,backend=onModelRegistryorresolve_registry. - The environment:
UBUNYE_REGISTRY_BACKEND. - The default:
filesystem. It needs no cloud and always works.
The store string (--store, registry.store) goes to the backend's
constructor unchanged. An unknown name is an error that lists the installed
names and says how to add one. The host is never sniffed: Databricks alone no
longer means mlflow.
A backend implements the six protocol methods. Everything else is done once in
core, on top of them (ubunye.models.lifecycle), so every backend gets it for
free: version numbers when none is given, refusing a duplicate version,
promotion gates (stored with the model as the promotion_gates metadata key,
checked before promote, force skips them), one production version at a
time, rollback, archive and compare. A backend may keep gates its own way by
offering promotion_gates(use_case, model_name).
The stages development, staging, production, archived are the public
contract. A backend may call them something else inside and maps them at its
edge.
ModelRegistry stays the public class. It delegates to the lifecycle layer on
every backend, filesystem included, so the rules live in one place. (Until
0.9.1 the filesystem kept its own copy of them, and the two drifted.) The
filesystem store keeps the same files and format on disk; a store written by
0.9.0 is pinned by a test (tests/fixtures/registry_0_9_0).
ubunye.testing.registry_conformance.RegistryConformance is the suite a
backend runs to prove itself. The filesystem, MLflow and a test-only third
party backend (installed through a real entry point) all run it.
Consequences¶
- A new registry (a real MLflow Model Registry, SageMaker, Vertex, a database) is a package with one entry point and six methods. No edit to Ubunye.
ModelRegistry(store)with no backend and no env var is unchanged, so existing user code and stores keep working.- If
UBUNYE_REGISTRY_BACKENDis set,ModelRegistry(store)in user code follows it too. That is the point of the order, and it is the one way to change backend without editing code. _internal.auto_detect.detect_registry_backendfollows the new order and is deprecated. It no longer returnsmlflowbecause ofDATABRICKS_RUNTIME_VERSION. Nothing called it; the private module was removed in 0.9.1.- Gates and "one production version" are checked by core with a read before a write, not atomically. Two promotions racing on one model can both pass. The filesystem registry had the same gap.