-
Notifications
You must be signed in to change notification settings - Fork 168
initial commit for multidb #7862
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c91f1b7
4aa3d61
aa0124f
9757426
1c14902
e9c1d26
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import logging | ||
| import random | ||
| from collections import defaultdict | ||
| from gettext import gettext as _ | ||
|
|
@@ -6,8 +7,8 @@ | |
| from django import apps | ||
| from django.conf import settings | ||
| from django.core.exceptions import ImproperlyConfigured | ||
| from django.db import connection, transaction | ||
| from django.db.models.signals import post_migrate, pre_migrate | ||
| from django.db import connection, connections, transaction | ||
| from django.db.models.signals import post_delete, post_migrate, post_save, pre_migrate | ||
| from django.utils.module_loading import module_has_submodule | ||
|
|
||
| from pulpcore.exceptions.plugin import MissingPlugin | ||
|
|
@@ -255,14 +256,39 @@ def ready(self): | |
| post_migrate.connect( | ||
| _populate_system_id, sender=self, dispatch_uid="populate_system_id_identifier" | ||
| ) | ||
| post_migrate.connect( | ||
| _ensure_domains_replicated, | ||
| sender=self, | ||
| dispatch_uid="ensure_domains_replicated_identifier", | ||
| ) | ||
| post_migrate.connect( | ||
| _populate_artifact_serving_distribution, | ||
| sender=self, | ||
| dispatch_uid="populate_artifact_serving_distribution_identifier", | ||
| ) | ||
| from pulpcore.app.domain_sync import on_domain_post_delete, on_domain_post_save | ||
| from pulpcore.app.models import Domain | ||
|
|
||
| post_save.connect( | ||
| on_domain_post_save, sender=Domain, dispatch_uid="replicate_domain_post_save" | ||
| ) | ||
| post_delete.connect( | ||
| on_domain_post_delete, sender=Domain, dispatch_uid="replicate_domain_post_delete" | ||
| ) | ||
|
|
||
| from pulpcore.app.db_router import is_multi_db_routing_active | ||
|
|
||
| if is_multi_db_routing_active(): | ||
| from pulpcore.app.role_util import on_any_model_post_delete | ||
|
|
||
| post_delete.connect( | ||
| on_any_model_post_delete, dispatch_uid="cleanup_cross_plane_roles_post_delete" | ||
|
Comment on lines
+281
to
+285
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this needed? Are UserRoles even cleanedup currently?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Django`s deletion collector will treat a GenericRelation like any other reverse relation for cascade purposes and delete the UserRoles when you delete a Repository. |
||
| ) | ||
|
|
||
|
|
||
| def _clean_app_status(sender, apps, verbosity, **kwargs): | ||
| if kwargs.get("using", "default") != "default": | ||
| return | ||
| from django.contrib.postgres.functions import TransactionNow | ||
| from django.db.models import F | ||
|
|
||
|
|
@@ -276,6 +302,9 @@ def _clean_app_status(sender, apps, verbosity, **kwargs): | |
|
|
||
|
|
||
| def _populate_access_policies(sender, apps, verbosity, **kwargs): | ||
| if kwargs.get("using", "default") != "default": | ||
| return | ||
|
|
||
| from pulpcore.app.util import get_view_urlpattern | ||
| from pulpcore.app.viewsets import LoginViewSet | ||
|
|
||
|
|
@@ -320,12 +349,16 @@ def _populate_access_policies(sender, apps, verbosity, **kwargs): | |
|
|
||
|
|
||
| def _populate_system_id(sender, apps, verbosity, **kwargs): | ||
| if kwargs.get("using", "default") != "default": | ||
| return | ||
| SystemID = apps.get_model("core", "SystemID") | ||
| if not SystemID.objects.exists(): | ||
| SystemID().save() | ||
|
|
||
|
|
||
| def _ensure_default_domain(sender, **kwargs): | ||
| if kwargs.get("using", "default") != "default": | ||
| return | ||
| table_names = connection.introspection.table_names() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is wrong, with multi-db we need to ensure that every domain on a separate DB has that exact domain object in their DB. We might also need to ensure that each DB also has a copy of the default domain.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe that what you are looking for is handled already by _ensure_domains_replicated + reconcile_domains_to_alias() |
||
| if "core_domain" in table_names: | ||
| from pulpcore.app.util import get_default_domain | ||
|
|
@@ -343,7 +376,29 @@ def _ensure_default_domain(sender, **kwargs): | |
| default.save(skip_hooks=True) | ||
|
|
||
|
|
||
| def _ensure_domains_replicated(sender, **kwargs): | ||
| using = kwargs.get("using", "default") | ||
| if using == "default": | ||
| return | ||
| if "core_domain" not in connections[using].introspection.table_names(): | ||
| return | ||
| from pulpcore.app.domain_sync import reconcile_domains_to_alias | ||
|
|
||
| try: | ||
| reconcile_domains_to_alias(using) | ||
| except Exception: | ||
| logging.getLogger(__name__).error( | ||
| "Reconciling Domain rows to alias '%s' failed during migration. Data-plane objects " | ||
| "created on this alias by later migrations/post_migrate hooks that FK to Domain may " | ||
| "fail until 'pulpcore-manager sync-domains' is run.", | ||
| using, | ||
| exc_info=True, | ||
| ) | ||
|
|
||
|
|
||
| def _populate_roles(sender, apps, verbosity, **kwargs): | ||
| if kwargs.get("using", "default") != "default": | ||
| return | ||
| role_prefix = f"{sender.label}." | ||
| # collect all plugin defined roles | ||
| desired_roles = {} | ||
|
|
@@ -403,6 +458,7 @@ def _get_permission(perm): | |
|
|
||
|
|
||
| def _populate_artifact_serving_distribution(sender, apps, verbosity, **kwargs): | ||
| alias = kwargs.get("using", "default") | ||
| if ( | ||
| settings.STORAGES["default"]["BACKEND"] == "pulpcore.app.models.storage.FileSystem" | ||
| or not settings.REDIRECT_TO_OBJECT_STORAGE | ||
|
|
@@ -415,15 +471,17 @@ def _populate_artifact_serving_distribution(sender, apps, verbosity, **kwargs): | |
| print(_("ArtifactDistribution model does not exist. Skipping initialization.")) | ||
| return | ||
| try: | ||
| ArtifactDistribution.objects.get() | ||
| ArtifactDistribution.objects.using(alias).get() | ||
| except ArtifactDistribution.DoesNotExist: | ||
| name = f"{random.getrandbits(256):x}" | ||
| with transaction.atomic(): | ||
| content_guard, _created = ContentRedirectContentGuard.objects.get_or_create( | ||
| with transaction.atomic(using=alias): | ||
| content_guard, _created = ContentRedirectContentGuard.objects.using( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would fail without a default domain in the other DB.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's already covered. |
||
| alias | ||
| ).get_or_create( | ||
| name=name, | ||
| pulp_type="core.content_redirect", | ||
| ) | ||
| _dist, _created = ArtifactDistribution.objects.get_or_create( | ||
| _dist, _created = ArtifactDistribution.objects.using(alias).get_or_create( | ||
| name=name, | ||
| pulp_type="core.artifact", | ||
| defaults={"base_path": name, "content_guard": content_guard}, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import logging | ||
|
|
||
| from django.apps import apps as django_apps | ||
| from django.db import router as django_router | ||
|
|
||
| from pulpcore.app.contexts import _current_migration_alias | ||
| from pulpcore.app.util import get_domain | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| CONTROL_PLANE_LABELS = frozenset( | ||
| { | ||
| "core.domain", | ||
| "core.task", | ||
| "core.taskgroup", | ||
| "core.taskschedule", | ||
| "core.createdresource", | ||
| "core.appstatus", | ||
| "core.systemid", | ||
| "core.accesspolicy", | ||
| "core.role", | ||
| "core.userrole", | ||
| "core.grouprole", | ||
| "core.progressreport", | ||
| "core.groupprogressreport", | ||
| "core.migrationstatus", | ||
| "core.domainmove", | ||
| "core.profileartifact", | ||
| "core.signingservice", | ||
| "core.asciiarmoreddetachedsigningservice", | ||
| "container.manifestsigningservice", | ||
| "rpm.rpmpackagesigningservice", | ||
| } | ||
| ) | ||
|
|
||
| CONTROL_PLANE_APPS = frozenset({"auth", "contenttypes", "admin", "sessions"}) | ||
|
|
||
|
|
||
| def _database_alias(domain): | ||
| if "database_alias" in domain.__dict__: | ||
| return domain.__dict__["database_alias"] | ||
| return "default" | ||
|
|
||
|
|
||
| class PulpDomainRouter: | ||
| def _is_control_plane(self, model): | ||
| label = f"{model._meta.app_label}.{model._meta.model_name}" | ||
| return label in CONTROL_PLANE_LABELS or model._meta.app_label in CONTROL_PLANE_APPS | ||
|
|
||
| def _resolve_db(self, model, **hints): | ||
| if model._meta.apps is not django_apps: | ||
| migration_alias = _current_migration_alias.get() | ||
| if migration_alias is not None: | ||
| return migration_alias | ||
|
|
||
| if self._is_control_plane(model): | ||
| return "default" | ||
|
|
||
| # Use __dict__ / fields_cache, not getattr/hasattr. FK descriptors can | ||
| # recurse into this router during instance construction or issue an extra query. | ||
| instance = hints.get("instance") | ||
| if instance is not None: | ||
| if "pulp_domain_id" in instance.__dict__: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably add a comment for why we are looking at
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
| domain = instance._state.fields_cache.get("pulp_domain") | ||
| if domain is not None: | ||
| return _database_alias(domain) | ||
|
|
||
| domain = get_domain() | ||
| if domain is not None: | ||
| return _database_alias(domain) | ||
|
|
||
| return "default" | ||
|
|
||
| def db_for_read(self, model, **hints): | ||
| return self._resolve_db(model, **hints) | ||
|
|
||
| def db_for_write(self, model, **hints): | ||
| return self._resolve_db(model, **hints) | ||
|
|
||
| def allow_relation(self, obj1, obj2, **hints): | ||
| return True | ||
|
|
||
| def allow_migrate(self, db, app_label, model_name=None, **hints): | ||
| return True | ||
|
|
||
|
|
||
| def is_multi_db_routing_active(): | ||
| return any(isinstance(r, PulpDomainRouter) for r in django_router.routers) | ||
|
Comment on lines
+87
to
+88
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel we should cache this calculation.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's already a very cheap calculation, with djanngo_router.routers already being a cached property. An any/isinstance over something that should already be a very short list is a sub-microsecond as is. |
||
Uh oh!
There was an error while loading. Please reload this page.