Skip to content

initial commit for multidb - #7862

Open
YasenT wants to merge 10 commits into
pulp:mainfrom
YasenT:multidb-implementation
Open

initial commit for multidb#7862
YasenT wants to merge 10 commits into
pulp:mainfrom
YasenT:multidb-implementation

Conversation

@YasenT

@YasenT YasenT commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Initial PR for the multidb implementation. To test github actions. etc

@YasenT

YasenT commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

/retest

YasenT and others added 4 commits July 15, 2026 15:13
getattr(value, 'pulp_domain_id', None) silently returns None for models
that reach their domain transitively rather than through their own field
(RepositoryVersion.repository, RepositoryContent.repository,
ContentArtifact.content, PublishedArtifact.publication). RepositoryVersion
is the single most common CreatedResource target in pulpcore (every
sync/publish creates one), so this meant most real-world post-move
CreatedResource rows for satellite-hosted domains silently resolved
content_object on the wrong (control-plane) alias instead of raising or
logging -- the exact KI-18 landmine, reached via an uncovered path.

Found via live validation against a real two-Postgres environment: a
genuine post-move CreatedResource pointing at a satellite RepositoryVersion
resolved to a stale pre-cleanup copy on 'default' instead of the live row
on the satellite alias.

Fixes by walking the target's concrete FK/O2O fields (bounded depth,
cycle-guarded) to find any related object exposing pulp_domain_id, rather
than a hardcoded model list -- covers all four known cases generically and
any future/plugin model with the same shape.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ntext

test_content_object_domain_id_set_for_repository_version created the
CreatedResource outside with_task_context(task), so CreatedResource.task
(a required NOT NULL FK) never got auto-populated -- IntegrityError on
insert. The aborted transaction from that failure can poison the
connection state for whatever DB-touching test runs next in the same
session, which is why this surfaced downstream (test_reconciliation.py)
in a full-suite run rather than pointing at the actual buggy test.

Verified clean: full pulpcore.tests.unit suite (363 passed, 2 skipped, 0
failed) against two real local Postgres instances mirroring the CI
'multi_db' matrix leg, plus a separate single-DB (no data_1) run
confirming the skip path still works.

Co-authored-by: Cursor <cursoragent@cursor.com>
@YasenT

YasenT commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@gerrod3 What do you think? Can I get a review?

@YasenT
YasenT marked this pull request as ready for review July 24, 2026 10:30

@gerrod3 gerrod3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 1 of reviews. This is honestly quite unreviewable in its current state. The AI comments are a nightmare and make references to docs and comments I don't have access to. The commits are not logically structured either. I would probably have ordered them something like:

  1. The initial adding of the database-alias and db-router
  2. Fixing management commands and other models (GenericReleation)
  3. Adding the database domain migration command
  4. CI work and tests

I would like to set expectations now that this will require major changes and many iterations before we are close to a state that might be mergeable.

Comment thread pulpcore/app/db_router.py Outdated
Comment on lines +159 to +160
if domain is not None:
return getattr(domain, "database_alias", "default")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this getattr not trigger something crazy too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't be crazy, but it might add extra queries, so makes sense to change it.

Comment thread pulpcore/app/settings.py
Comment on lines +318 to +323
CROSS_PLANE_RECONCILIATION_GRACE_MINUTES = 60

# KI-11: how long, in days, a confirmed-orphaned cross-plane row is kept (logged/alerted on every
# sweep) before the reconciliation sweep deletes it outright. 0 disables purging entirely --
# orphans are only ever logged, never deleted, which is the safe default.
CROSS_PLANE_RECONCILIATION_PURGE_AFTER_DAYS = 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need these two different settings?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first one makes sure we don't "flag" objects in "flight" as orphans. For example in the case of a migration of an active domain to a new DB, we need some grace period before we start flagging as orphans

And than how regularly we purge is a different setting, the 2x can be very different values. Multiple days vs multiple hours. Makes sense to be 2 phased in a way.

Comment thread pulpcore/app/settings.py Outdated
Comment on lines +722 to +724
if len(settings.DATABASES) > 1:
DATABASE_ROUTERS = ["pulpcore.app.db_router.PulpDomainRouter"]
settings.set("DATABASE_ROUTERS", DATABASE_ROUTERS)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this. Routers must be explicitly set by the user.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, it's removed

Comment thread pulpcore/app/queryset.py Outdated
Comment on lines +37 to +38
if len(settings.DATABASES) <= 1:
return super().filter(*args, **kwargs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check should be if the router is the special PulpMultiDBRouter, not if there are more than one 1 database.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, it's a bit extra-safe. Will change it.

_DOMAIN_WALK_MAX_DEPTH = 2


def _resolve_domain_id(value, _depth=0, _seen=None):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this? Yeah it's probably the safest way to get the domain of the object, but I would expect that get_domain would always return the correct domain that the object is in. Maybe it doesn't matter since creating GenericRelationships typically never happen in a hot path

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see it more as a protection for the future, just playing safe.

Comment thread pulpcore/app/domain_sync.py Outdated
Comment on lines +5 to +6
read-only copy of every `Domain` row must also exist on every other configured `DATABASES` alias
so that per-process code (the router, `for_each_domain()`, `Domain.get_storage()`, etc.) can

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should this be true? It doesn't seem like it should be. The only thing that I would expect that needs to exist on the satellite dbs is the default domain and the satellite domain.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was more related to how the sync would work. Changed that, so now it's not true/needed.
As i was keeping the domain metadata synced between dbs.

YasenT and others added 2 commits August 5, 2026 11:45
Addresses gerrod3's review comment on _resolve_db's Domain-hint branches
("Does this getattr not trigger something crazy too?"): getattr(domain,
"database_alias", "default") goes through the same DeferredAttribute.__get__
as pulp_domain_id does one branch up, so a Domain instance reaching here
with database_alias deferred would call refresh_from_db() for it.

It can't actually recurse the way the pulp_domain_id case (KI-27) does --
Domain is control-plane, so the re-entrant db_for_read(Domain, ...) call
hits the _is_control_plane short-circuit and returns "default" before ever
reaching the instance-hint branch again. So the real failure mode here is
a silent extra query, not a RecursionError. No call site defers the field
today (DomainMiddleware's plain .get(), and select_related("pulp_domain")
with no field restriction on every task-fetch path), so this isn't an
active bug, but it's the same class of "assume it's always loaded"
fragility KI-27 already burned us on once. Added _database_alias() to
close it out unconditionally, plus a regression test that asserts zero
queries when the field is deferred.

Co-authored-by: Cursor <cursoragent@cursor.com>
…cit, and drop external doc references

Domain metadata now only replicates to `default` (everywhere) and to a
domain's own current `database_alias`, instead of every domain onto every
satellite; move-domain explicitly seeds the destination alias before
copying data, and cleanup-moved-domain/sync-domains prune the stale
replica left behind on the source alias after a move. Also stops
auto-activating DATABASE_ROUTERS from DATABASES (must be set explicitly
now, via the new is_multi_db_routing_active() check), and removes
lingering comments/messages that referenced an external design doc not
available to reviewers.

Fixes two bugs found via live end-to-end validation against a local
multi-database setup: cleanup-moved-domain deleting the authoritative
Domain row when a domain's original alias was "default", and a
DATABASE_ROUTERS unit test relying on override_settings, which silently
never reached the real router registry under this project's dynaconf
settings integration.

Co-authored-by: Cursor <cursoragent@cursor.com>
@dralley

dralley commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Rebase this please

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants