Repository navigation
Conversation
list_directory_blocking()'s ContentArtifact query filtered on content__in=content_repo_ver.content, relative_path__startswith=path, which forces Postgres to materialize the entire repository version's content set before applying the path filter. For a path with no matches (the common case for bot traffic probing nonexistent directories), this meant scanning the whole membership set just to return nothing. Measured directly against prod-scale data: ~428ms per nonexistent-directory lookup. Filter relative_path first (indexed, selective) and check membership with a correlated Exists() subquery against _content_relationships() instead, so Postgres can short-circuit on a path with zero matches without touching membership at all. Measured after the fix: ~13ms for both negative and positive lookups (~30-55x faster), no new index required. This is a draft opened only to validate against pulpcore's CI/test suite. The actual fix will land as a pulp-service patch (see pulp/pulp-service#TBD) per team policy of patching pulp-service rather than pulpcore directly; this PR is not intended to be merged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous version called .filter() on content_repo_ver._content_relationships(), which assumes a Django QuerySet. test_list_directory_metadata mocks that method as a plain list (since the old code only ever iterated it), so CI failed with AttributeError: 'list' object has no attribute 'filter'. Build the same membership query independently via RepositoryContent instead of deriving it from _content_relationships()'s return value, matching that method's own implementation. Also completes the test's repo_version mock with repository_id/number, which the new query needs and the old mock never set. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
I tested the old and proposed query shapes in GABI against the production-shaped dataset. For a nonexistent directory prefix, the SQL plans were: -- Existing shape
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT ca.content_id, ca.relative_path
FROM core_contentartifact ca
WHERE ca.relative_path LIKE '<directory-prefix>%'
AND ca.content_id IN (
SELECT unnest(content_ids)
FROM core_repositoryversion
WHERE pulp_id = '<repository-version-uuid>'::uuid
);
-- Proposed shape
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT ca.content_id, ca.relative_path
FROM core_contentartifact ca
WHERE ca.relative_path LIKE '<directory-prefix>%'
AND EXISTS (
SELECT 1
FROM core_repositorycontent rc
JOIN core_repositoryversion va
ON va.pulp_id = rc.version_added_id
LEFT JOIN core_repositoryversion vr
ON vr.pulp_id = rc.version_removed_id
WHERE rc.repository_id = '<repository-uuid>'::uuid
AND rc.content_id = ca.content_id
AND va.number <= <version-number>
AND (
rc.version_removed_id IS NULL
OR vr.number > <version-number>
)
);Observed GABI results for the same nonexistent path: The proposed plan performed a parallel sequential scan of The PR currently does not add a migration or a Please recheck the index assumption and test the proposed query with the actual production index schema. A suitable |
The directory-listing query now filters on relative_path first. That is only fast with an index that serves LIKE 'prefix%': the existing relative_path btree uses the database collation and cannot, so on a large table the new query scans all of core_contentartifact (about 16.8 s on a 28M-row production table). Add a text_pattern_ops index on ContentArtifact.relative_path, created with AddIndexConcurrently. Replace the changelog text, which wrongly claimed constant time. Measured on a test database with repositories of 1.59M and 108K items, missing path, EXPLAIN ANALYZE: the previous query took 17.4 s and 0.89 s; the new query without the index 7.0 s and 0.58 s; with the index 0.04-0.07 ms. Not improved: broad prefixes, and small repositories with a folder that exists. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
I compared this with Jan's #8262. The indexes are attribute-equivalent: B-tree on I added the shared |
|
Closing this in favor of the combined work:
The perf results for the directory-listing query stay in this PR's description for reference. |
Do not merge
This is a draft opened only to run pulpcore's CI and tests. Per team policy, performance fixes for the hosted Pulp service ship as patches in
pulp-serviceon top of the releasedpulpcore. The maintained fix is patch 0079 plus a migration inpulp-service. This PR exists to get CI signal on the same change.Ticket: PULP-2346 (HEAD requests to the lightwell Java repositories time out at 30 s).
Problem
For a file path, the content handler asks the database three questions, each shaped as "find rows for this path among the repository version's items":
index.html?_match_and_streamx.jar/?list_directory_blocking()_match_and_streamAll three use
content__in=<version content>. For a repository with ~1.6M items, Postgres walks every item before answering "no". Measured on prod (read-onlyEXPLAIN ANALYZE,java/validated, 1,592,450 items, path that does not exist): Q1 13.5 s, Q2 14.0 s, Q3 13.0 s. Together that is over Akamai's 30 s limit.What this PR changes (Q2 only)
list_directory_blocking()filters onrelative_pathfirst and checks membership per matching row with a correlatedEXISTS.text_pattern_opsindex onContentArtifact.relative_path(migration0162,AddIndexConcurrently). The existingrelative_pathbtree uses the database collation and cannot serveLIKE 'prefix%'.Without the index the new query is worse on prod (parallel seq scan of
core_contentartifact, ~16.8 s against ~14 s today). The query change and the index only work together.Results (test database, repositories sized like the ticket's)
What was measured: the time of one database lookup, the folder question (Q2), as reported by
EXPLAIN ANALYZE. Lower is better. Three versions of the code are compared:Tested on a test database with repositories sized like the two in the ticket. Units are shown in each cell (
s= seconds,ms= milliseconds).java/validated(1.59M items)org/codehaus/)java/remediated(108K items)Small test repository (2K items)
Index build: 46 s, 1.79 GB on 28M rows.
Not fixed here
java/validatedstays near the limit. Follow-up PR.Caveats
relative_pathstatistics differ. The numbers show the mechanism, not prod timings.content_ids; no removed-membership rows.Test plan
test_handler.pyunit tests (test_list_directory_metadata,test_publication_listing_uses_repository_content_date) pass with the new query.makemigrations --check), lint, sanity.CREATE INDEX CONCURRENTLYduration andstatement_timeoutbehavior on a prod-sized table.