Skip to content

Commit c5ad4ef

Browse files
jobselkodralley
authored andcommitted
Optimize re-sync with content caching
Assisted By: Claude Opus 4.6
1 parent 5bc362f commit c5ad4ef

4 files changed

Lines changed: 74 additions & 15 deletions

File tree

CHANGES/+optimize_resync.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improved sync performance by caching content during re-syncs.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`DeclarativeVersion` now accepts `deferred_fields` to exclude fields not needed during sync, reducing memory usage and producing lighter queries.

pulpcore/plugin/stages/content_stages.py

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,38 @@ class QueryExistingContents(Stage):
3030
3131
This stage drains all available items from `self._in_q` and batches everything into one large
3232
call to the db for efficiency.
33+
34+
When `repo_version` is provided, content from that version is cached in memory on first
35+
access (per content type) so that repeat syncs can resolve most items without per-batch
36+
database queries.
37+
38+
Plugins with expensive fields that are not needed during sync can pass
39+
`deferred_fields` - a mapping of content model class to a list of field names
40+
to exclude from queries via Django's `defer()`.
3341
"""
3442

43+
def __init__(self, repo_version=None, deferred_fields=None, *args, **kwargs):
44+
super().__init__(*args, **kwargs)
45+
self._repo_version = repo_version
46+
self._deferred_fields = deferred_fields or {}
47+
self._content_cache = {}
48+
49+
def _ensure_type_cache(self, model_type):
50+
"""
51+
Populate the cache for model_type from repo_version on first access.
52+
53+
Args:
54+
model_type: The content model class to cache.
55+
"""
56+
if model_type not in self._content_cache and self._repo_version is not None:
57+
deferred = self._deferred_fields.get(model_type, ())
58+
cache = {}
59+
for content in self._repo_version.get_content(
60+
model_type.objects.defer(*deferred)
61+
).iterator():
62+
cache[content.natural_key()] = content
63+
self._content_cache[model_type] = cache
64+
3565
async def run(self):
3666
"""
3767
The coroutine for this stage.
@@ -42,25 +72,46 @@ async def run(self):
4272
async for batch in self.batches():
4373
content_q_by_type = defaultdict(lambda: Q(pk__in=[]))
4474
d_content_by_nat_key = defaultdict(list)
75+
cache_hits_by_type = defaultdict(set)
76+
4577
for d_content in batch:
4678
if d_content.content._state.adding:
4779
model_type = type(d_content.content)
48-
unit_q = d_content.content.q()
49-
content_q_by_type[model_type] = content_q_by_type[model_type] | unit_q
50-
d_content_by_nat_key[d_content.content.natural_key()].append(d_content)
51-
80+
nat_key = d_content.content.natural_key()
81+
await sync_to_async(self._ensure_type_cache)(model_type)
82+
cached = self._content_cache.get(model_type, {}).get(nat_key)
83+
if cached is not None:
84+
d_content.content = cached
85+
cache_hits_by_type[model_type].add(cached.pk)
86+
else:
87+
unit_q = d_content.content.q()
88+
content_q_by_type[model_type] = content_q_by_type[model_type] | unit_q
89+
d_content_by_nat_key[nat_key].append(d_content)
90+
91+
db_results_by_type = defaultdict(list)
5292
for model_type, content_q in content_q_by_type.items():
53-
try:
54-
await sync_to_async(model_type.objects.filter(content_q).touch)()
55-
except AttributeError:
56-
raise TypeError(
57-
"Plugins which declare custom ORM managers on their content classes "
58-
"should have those managers inherit from "
59-
"pulpcore.plugin.models.ContentManager."
60-
)
93+
deferred = self._deferred_fields.get(model_type, ())
6194
async for result in sync_to_async_iterable(
62-
model_type.objects.filter(content_q).iterator()
95+
model_type.objects.filter(content_q).defer(*deferred).iterator()
6396
):
97+
db_results_by_type[model_type].append(result)
98+
99+
all_types = set(cache_hits_by_type.keys()) | set(db_results_by_type.keys())
100+
for model_type in all_types:
101+
pks = cache_hits_by_type.get(model_type, set())
102+
pks |= {r.pk for r in db_results_by_type.get(model_type, [])}
103+
if pks:
104+
try:
105+
await sync_to_async(model_type.objects.filter(pk__in=pks).touch)()
106+
except AttributeError:
107+
raise TypeError(
108+
"Plugins which declare custom ORM managers on their content classes "
109+
"should have those managers inherit from "
110+
"pulpcore.plugin.models.ContentManager."
111+
)
112+
113+
for model_type, results in db_results_by_type.items():
114+
for result in results:
64115
for d_content in d_content_by_nat_key[result.natural_key()]:
65116
d_content.content = result
66117

pulpcore/plugin/stages/declarative_version.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222

2323
class DeclarativeVersion:
24-
def __init__(self, first_stage, repository, mirror=False, acs=False):
24+
def __init__(self, first_stage, repository, mirror=False, acs=False, deferred_fields=None):
2525
"""
2626
A pipeline that creates a new [pulpcore.plugin.models.RepositoryVersion][] from a
2727
stream of [pulpcore.plugin.stages.DeclarativeContent][] objects.
@@ -107,12 +107,16 @@ async def run(self):
107107
'False' is the default.
108108
acs (bool): When set to 'True' a new stage is added to look for
109109
Alternate Content Sources.
110+
deferred_fields (dict): A mapping of content model class to a list of
111+
field names to exclude from queries via Django's `defer()`.
112+
Passed through to [pulpcore.plugin.stages.QueryExistingContents][].
110113
111114
"""
112115
self.first_stage = first_stage
113116
self.repository = repository
114117
self.mirror = mirror
115118
self.acs = acs
119+
self.deferred_fields = deferred_fields
116120

117121
def pipeline_stages(self, new_version):
118122
"""
@@ -142,7 +146,9 @@ def pipeline_stages(self, new_version):
142146
[
143147
ArtifactDownloader(resource_budget=resource_budget),
144148
ArtifactSaver(resource_budget=resource_budget),
145-
QueryExistingContents(),
149+
QueryExistingContents(
150+
repo_version=new_version, deferred_fields=self.deferred_fields
151+
),
146152
ContentSaver(),
147153
RemoteArtifactSaver(),
148154
ResolveContentFutures(),

0 commit comments

Comments
 (0)