Skip to content

Commit 6612cd8

Browse files
authored
ref(backup): Split dependency function into own file (#54323)
We will re-use this functionality in multiple places, so its best to move it now. Issue: getsentry/team-ospo#171
1 parent b73e7e8 commit 6612cd8

File tree

2 files changed

+103
-99
lines changed

2 files changed

+103
-99
lines changed

src/sentry/backup/dependencies.py

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from __future__ import annotations
2+
3+
from django.db.models.fields.related import ManyToManyField
4+
5+
from sentry.backup.helpers import EXCLUDED_APPS
6+
7+
8+
def sort_dependencies():
9+
"""
10+
Similar to Django's except that we discard the important of natural keys
11+
when sorting dependencies (i.e. it works without them).
12+
"""
13+
from django.apps import apps
14+
15+
from sentry.models.actor import Actor
16+
from sentry.models.team import Team
17+
from sentry.models.user import User
18+
19+
# Process the list of models, and get the list of dependencies
20+
model_dependencies = []
21+
models = set()
22+
for app_config in apps.get_app_configs():
23+
if app_config.label in EXCLUDED_APPS:
24+
continue
25+
26+
model_iterator = app_config.get_models()
27+
28+
for model in model_iterator:
29+
models.add(model)
30+
# Add any explicitly defined dependencies
31+
if hasattr(model, "natural_key"):
32+
deps = getattr(model.natural_key, "dependencies", [])
33+
if deps:
34+
deps = [apps.get_model(*d.split(".")) for d in deps]
35+
else:
36+
deps = []
37+
38+
# Now add a dependency for any FK relation with a model that
39+
# defines a natural key
40+
for field in model._meta.fields:
41+
rel_model = getattr(field.remote_field, "model", None)
42+
if rel_model is not None and rel_model != model:
43+
# TODO(hybrid-cloud): actor refactor.
44+
# Add cludgy conditional preventing walking actor.team_id, actor.user_id
45+
# Which avoids circular imports
46+
if model == Actor and (rel_model == Team or rel_model == User):
47+
continue
48+
49+
deps.append(rel_model)
50+
51+
# Also add a dependency for any simple M2M relation with a model
52+
# that defines a natural key. M2M relations with explicit through
53+
# models don't count as dependencies.
54+
many_to_many_fields = [
55+
field for field in model._meta.get_fields() if isinstance(field, ManyToManyField)
56+
]
57+
for field in many_to_many_fields:
58+
rel_model = getattr(field.remote_field, "model", None)
59+
if rel_model is not None and rel_model != model:
60+
deps.append(rel_model)
61+
62+
model_dependencies.append((model, deps))
63+
64+
model_dependencies.reverse()
65+
# Now sort the models to ensure that dependencies are met. This
66+
# is done by repeatedly iterating over the input list of models.
67+
# If all the dependencies of a given model are in the final list,
68+
# that model is promoted to the end of the final list. This process
69+
# continues until the input list is empty, or we do a full iteration
70+
# over the input models without promoting a model to the final list.
71+
# If we do a full iteration without a promotion, that means there are
72+
# circular dependencies in the list.
73+
model_list = []
74+
while model_dependencies:
75+
skipped = []
76+
changed = False
77+
while model_dependencies:
78+
model, deps = model_dependencies.pop()
79+
80+
# If all of the models in the dependency list are either already
81+
# on the final model list, or not on the original serialization list,
82+
# then we've found another model with all it's dependencies satisfied.
83+
found = True
84+
for candidate in ((d not in models or d in model_list) for d in deps):
85+
if not candidate:
86+
found = False
87+
if found:
88+
model_list.append(model)
89+
changed = True
90+
else:
91+
skipped.append((model, deps))
92+
if not changed:
93+
raise RuntimeError(
94+
"Can't resolve dependencies for %s in serialized app list."
95+
% ", ".join(
96+
f"{model._meta.app_label}.{model._meta.object_name}"
97+
for model, deps in sorted(skipped, key=lambda obj: obj[0].__name__)
98+
)
99+
)
100+
model_dependencies = skipped
101+
102+
return model_list

src/sentry/backup/exports.py

+1-99
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66
import click
77
from django.core.serializers import serialize
88
from django.core.serializers.json import DjangoJSONEncoder
9-
from django.db.models.fields.related import ManyToManyField
109

11-
from sentry.backup.helpers import EXCLUDED_APPS
10+
from sentry.backup.dependencies import sort_dependencies
1211

1312
UTC_0 = timezone(timedelta(hours=0))
1413

@@ -24,103 +23,6 @@ def default(self, obj):
2423
return super().default(obj)
2524

2625

27-
def sort_dependencies():
28-
"""
29-
Similar to Django's except that we discard the important of natural keys
30-
when sorting dependencies (i.e. it works without them).
31-
"""
32-
from django.apps import apps
33-
34-
from sentry.models.actor import Actor
35-
from sentry.models.team import Team
36-
from sentry.models.user import User
37-
38-
# Process the list of models, and get the list of dependencies
39-
model_dependencies = []
40-
models = set()
41-
for app_config in apps.get_app_configs():
42-
if app_config.label in EXCLUDED_APPS:
43-
continue
44-
45-
model_iterator = app_config.get_models()
46-
47-
for model in model_iterator:
48-
models.add(model)
49-
# Add any explicitly defined dependencies
50-
if hasattr(model, "natural_key"):
51-
deps = getattr(model.natural_key, "dependencies", [])
52-
if deps:
53-
deps = [apps.get_model(*d.split(".")) for d in deps]
54-
else:
55-
deps = []
56-
57-
# Now add a dependency for any FK relation with a model that
58-
# defines a natural key
59-
for field in model._meta.fields:
60-
rel_model = getattr(field.remote_field, "model", None)
61-
if rel_model is not None and rel_model != model:
62-
# TODO(hybrid-cloud): actor refactor.
63-
# Add cludgy conditional preventing walking actor.team_id, actor.user_id
64-
# Which avoids circular imports
65-
if model == Actor and (rel_model == Team or rel_model == User):
66-
continue
67-
68-
deps.append(rel_model)
69-
70-
# Also add a dependency for any simple M2M relation with a model
71-
# that defines a natural key. M2M relations with explicit through
72-
# models don't count as dependencies.
73-
many_to_many_fields = [
74-
field for field in model._meta.get_fields() if isinstance(field, ManyToManyField)
75-
]
76-
for field in many_to_many_fields:
77-
rel_model = getattr(field.remote_field, "model", None)
78-
if rel_model is not None and rel_model != model:
79-
deps.append(rel_model)
80-
81-
model_dependencies.append((model, deps))
82-
83-
model_dependencies.reverse()
84-
# Now sort the models to ensure that dependencies are met. This
85-
# is done by repeatedly iterating over the input list of models.
86-
# If all the dependencies of a given model are in the final list,
87-
# that model is promoted to the end of the final list. This process
88-
# continues until the input list is empty, or we do a full iteration
89-
# over the input models without promoting a model to the final list.
90-
# If we do a full iteration without a promotion, that means there are
91-
# circular dependencies in the list.
92-
model_list = []
93-
while model_dependencies:
94-
skipped = []
95-
changed = False
96-
while model_dependencies:
97-
model, deps = model_dependencies.pop()
98-
99-
# If all of the models in the dependency list are either already
100-
# on the final model list, or not on the original serialization list,
101-
# then we've found another model with all it's dependencies satisfied.
102-
found = True
103-
for candidate in ((d not in models or d in model_list) for d in deps):
104-
if not candidate:
105-
found = False
106-
if found:
107-
model_list.append(model)
108-
changed = True
109-
else:
110-
skipped.append((model, deps))
111-
if not changed:
112-
raise RuntimeError(
113-
"Can't resolve dependencies for %s in serialized app list."
114-
% ", ".join(
115-
f"{model._meta.app_label}.{model._meta.object_name}"
116-
for model, deps in sorted(skipped, key=lambda obj: obj[0].__name__)
117-
)
118-
)
119-
model_dependencies = skipped
120-
121-
return model_list
122-
123-
12426
class OldExportConfig(NamedTuple):
12527
"""While we are migrating to the new backup system, we need to take care not to break the old
12628
and relatively untested workflows. This model allows us to stub in the old configs."""

0 commit comments

Comments
 (0)