Repository navigation
Run the read-only content queries as a PULP-2346 A/B experiment #1655
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
Merged
Merged
Changes from 3 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
479e33c
Run the read-only content queries as a PULP-2346 A/B experiment
larrymou9 8050475
Enable the PULP-2346 content query experiment by default
larrymou9 27c9e12
Always run the PULP-2346 content query experiment
larrymou9 60dabc2
Preserve index probes for dotted directories
kdelee 123a89f
Fix patch 0080 after dropping the index probe skip; address review
larrymou9 d5d62c6
Update the PULP-2346 changelog fragment: the index probe is no longer…
larrymou9 7d037be
Preserve PULP-2346 markers on lazy queries
kdelee 0325ffd
Merge pull request #3 from kdelee/kdelee/pulp-2346-lazy-marker
larrymou9 473fefa
Keep one PULP-2346 marker per statement
larrymou9 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Add the PULP-2346 A/B experiment for the content app's read-only content queries (the ``index.html`` probe, the directory listing and the exact file lookup). Variant B checks repository membership per matching row instead of walking the whole repository version's content set and skips the probe for file paths. The experiment is always on: half of the calls run variant B, and each call logs its variant, query, repository and duration. The queries rely on the ``core_ca_relpath_pattern_idx`` index added by migration 0024. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
images/assets/patches/0080-Optimize-read-only-content-queries.patch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| --- a/pulpcore/content/handler.py | ||
| +++ b/pulpcore/content/handler.py | ||
| @@ -50,7 +50,11 @@ | ||
| ) | ||
| from jinja2 import Template # noqa: E402 | ||
|
|
||
| -from pulp_service.app.experiments import directory_membership_dates # noqa: E402 | ||
| +from pulp_service.app.experiments import ( # noqa: E402 | ||
| + content_query_experiment, | ||
| + directory_membership_dates, | ||
| + single_or_raise, | ||
| +) | ||
|
|
||
| from pulpcore.app import mime_types # noqa: E402 | ||
| from pulpcore.app.models import ( # noqa: E402 | ||
| @@ -785,11 +789,14 @@ | ||
| artifacts_to_find[pa.content_artifact.pk] = name | ||
|
|
||
| if repo_version or publication.pass_through: | ||
| - cas = ContentArtifact.objects.select_related("artifact").filter( | ||
| - content__in=content_repo_ver.content, relative_path__startswith=path | ||
| + ca_rows, cas = content_query_experiment( | ||
| + "directory_listing", | ||
| + content_repo_ver, | ||
| + ContentArtifact.objects.select_related("artifact"), | ||
| + filters={"relative_path__startswith": path}, | ||
| ) | ||
| content_sources.append((cas, "content_id")) | ||
| - for ca in cas: | ||
| + for ca in ca_rows: | ||
| name = file_or_directory_name(path, ca.relative_path) | ||
| directory_list.add(name) | ||
| update_content_date(content_dates, ca.content_id, ca.pulp_created) | ||
| @@ -977,15 +984,18 @@ | ||
| # pass-through | ||
| if publication.pass_through: | ||
| try: | ||
| - ca = await self._retry_content_artifact_query( | ||
| - lambda: ContentArtifact.objects.select_related( | ||
| - "artifact", "artifact__pulp_domain" | ||
| - ) | ||
| - .filter( | ||
| - content__in=publication.repository_version.content, | ||
| + rows, _ = await self._retry_content_artifact_query( | ||
| + lambda: sync_to_async(content_query_experiment)( | ||
| + "pass_through_file_lookup", | ||
| + publication.repository_version, | ||
| + ContentArtifact.objects.select_related( | ||
| + "artifact", "artifact__pulp_domain" | ||
| + ), | ||
| + filters={"relative_path": original_rel_path}, | ||
| + limit=2, | ||
| ) | ||
| - .aget(relative_path=original_rel_path) | ||
| ) | ||
| + ca = single_or_raise(ContentArtifact, rows) | ||
|
|
||
| except MultipleObjectsReturned: | ||
| log.error( | ||
| @@ -1022,11 +1032,19 @@ | ||
| # Look for index.html or list the directory | ||
| index_path = "{}index.html".format(rel_path) | ||
|
|
||
| - contentartifact_exists = await self._retry_content_artifact_query( | ||
| - lambda: ContentArtifact.objects.filter( | ||
| - content__in=repo_version.content, relative_path=index_path | ||
| - ).aexists() | ||
| + # File-ish paths (no trailing slash, dotted last segment) have no index.html to find. | ||
| + last_segment = rel_path.rstrip("/").rsplit("/", 1)[-1] | ||
| + probe_rows, _ = await self._retry_content_artifact_query( | ||
| + lambda: sync_to_async(content_query_experiment)( | ||
| + "index_probe", | ||
| + repo_version, | ||
| + ContentArtifact.objects.all(), | ||
| + filters={"relative_path": index_path}, | ||
| + limit=1, | ||
| + skip_candidate=not ends_in_slash and "." in last_segment, | ||
| + ) | ||
| ) | ||
| + contentartifact_exists = bool(probe_rows) | ||
| if contentartifact_exists: | ||
| original_rel_path = index_path | ||
| headers = self.response_headers(original_rel_path, distro) | ||
| @@ -1047,11 +1065,16 @@ | ||
| ) | ||
|
|
||
| try: | ||
| - ca = await self._retry_content_artifact_query( | ||
| - lambda: ContentArtifact.objects.select_related( | ||
| - "artifact", "artifact__pulp_domain" | ||
| - ).aget(content__in=repo_version.content, relative_path=original_rel_path) | ||
| + rows, _ = await self._retry_content_artifact_query( | ||
| + lambda: sync_to_async(content_query_experiment)( | ||
| + "file_lookup", | ||
| + repo_version, | ||
| + ContentArtifact.objects.select_related("artifact", "artifact__pulp_domain"), | ||
| + filters={"relative_path": original_rel_path}, | ||
| + limit=2, | ||
| + ) | ||
| ) | ||
| + ca = single_or_raise(ContentArtifact, rows) | ||
|
|
||
| except MultipleObjectsReturned: | ||
| log.error( |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 118 additions & 0 deletions
118
pulp_service/pulp_service/tests/unit/test_content_query_experiment.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| """Database-backed checks for the PULP-2346 content query experiment.""" | ||
|
|
||
| import json | ||
| from uuid import uuid4 | ||
|
|
||
| import pytest | ||
| from django.db import connection | ||
| from django.test.utils import CaptureQueriesContext | ||
|
|
||
| from pulpcore.app.models import Content, ContentArtifact, Repository | ||
|
|
||
| from pulp_service.app.experiments import CONTENT_QUERY_EXPERIMENT, content_query_experiment, single_or_raise | ||
|
|
||
| MEMBER_PATHS = ["org/a/a-1.0.jar", "org/a/a-1.0.pom", "org/b/index.html", "top.txt"] | ||
| OTHER_PATHS = ["org/b/b-2.0.jar", "org/a/not-a-member.jar"] | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def version(db): # noqa: ARG001 | ||
| """A repository version holding MEMBER_PATHS, with OTHER_PATHS existing outside of it.""" | ||
| repository = Repository.objects.create(name=uuid4()) | ||
| repository.CONTENT_TYPES = [Content] | ||
| members = [] | ||
| for path in [*MEMBER_PATHS, *OTHER_PATHS]: | ||
| content = Content.objects.create(pulp_type="core.content") | ||
| ContentArtifact.objects.create(content=content, relative_path=path) | ||
| if path in MEMBER_PATHS: | ||
| members.append(content.pk) | ||
| with repository.new_version() as new_version: | ||
| new_version.add_content(Content.objects.filter(pk__in=members)) | ||
| return new_version | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def force_variant(monkeypatch): | ||
| def _force(variant): | ||
| # random() < p_candidate (0.5) selects B, otherwise A. | ||
| monkeypatch.setattr("pulp_service.app.experiments.random.random", lambda: 0.1 if variant == "B" else 0.9) | ||
|
|
||
| return _force | ||
|
|
||
|
|
||
| LOOKUPS = [ | ||
| pytest.param({"relative_path__startswith": "org/"}, None, False, id="directory-listing"), | ||
| pytest.param({"relative_path__startswith": "org/a/"}, None, False, id="specific-folder"), | ||
| pytest.param({"relative_path__startswith": "nothing/"}, None, False, id="missing-folder"), | ||
| pytest.param({"relative_path": "org/b/index.html"}, 1, False, id="index-probe-hit"), | ||
| pytest.param({"relative_path": "org/a/index.html"}, 1, False, id="index-probe-miss"), | ||
| pytest.param({"relative_path": "org/a/a-1.0.jar"}, 2, False, id="file-lookup-hit"), | ||
| pytest.param({"relative_path": "org/b/b-2.0.jar"}, 2, False, id="file-lookup-not-in-version"), | ||
| ] | ||
|
|
||
|
|
||
| @pytest.mark.parametrize(("filters", "limit", "skip"), LOOKUPS) | ||
| def test_variants_return_same_rows(version, force_variant, filters, limit, skip): # noqa: ARG001 | ||
| """Variant A (stock) and variant B (EXISTS) must return identical content artifacts.""" | ||
| results = {} | ||
| for variant in ("A", "B"): | ||
| force_variant(variant) | ||
| rows, _ = content_query_experiment( | ||
| "test", version, ContentArtifact.objects.select_related("artifact"), filters=filters, limit=limit | ||
| ) | ||
| results[variant] = sorted(row.relative_path for row in rows) | ||
| assert results["A"] == results["B"] | ||
|
sourcery-ai[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
|
|
||
| def test_skip_candidate_returns_no_rows_without_querying(version, force_variant): | ||
| force_variant("B") | ||
| with CaptureQueriesContext(connection) as ctx: | ||
| rows, _ = content_query_experiment( | ||
| "index_probe", | ||
| version, | ||
| ContentArtifact.objects.all(), | ||
| filters={"relative_path": "org/a/a-1.0.jarindex.html"}, | ||
| limit=1, | ||
| skip_candidate=True, | ||
| ) | ||
| assert rows == [] | ||
| assert not ctx.captured_queries | ||
|
|
||
|
|
||
| def test_logs_variant_query_and_repository(version, caplog, force_variant): | ||
| caplog.set_level("INFO", logger="pulp.experiment") | ||
| for variant in ("B", "A"): | ||
| force_variant(variant) | ||
| content_query_experiment( | ||
| "directory_listing", version, ContentArtifact.objects.all(), filters={"relative_path__startswith": "org/"} | ||
| ) | ||
| records = [json.loads(r.getMessage()) for r in caplog.records if r.name == "pulp.experiment"] | ||
| assert [record["variant"] for record in records] == ["B", "A"] | ||
| for record in records: | ||
| assert record["event"] == "ab_experiment" | ||
| assert record["exp_id"] == CONTENT_QUERY_EXPERIMENT | ||
| assert record["query"] == "directory_listing" | ||
| assert record["repository_id"] == str(version.repository_id) | ||
| assert record["outcome"] == "success" | ||
| assert record["result_count"] == 3 | ||
| assert record["duration_ms"] >= 0 | ||
|
|
||
|
|
||
| def test_sql_carries_variant_and_repository_markers(version, force_variant): | ||
| for variant in ("A", "B"): | ||
| force_variant(variant) | ||
| with CaptureQueriesContext(connection) as ctx: | ||
| content_query_experiment( | ||
| "test", version, ContentArtifact.objects.all(), filters={"relative_path__startswith": "org/"} | ||
| ) | ||
| marker = f"/* pexp={CONTENT_QUERY_EXPERIMENT} v={variant} */ /* repo={version.repository_id} */" | ||
| assert any(marker in query["sql"] for query in ctx.captured_queries), marker | ||
|
|
||
|
|
||
| def test_single_or_raise_matches_get_semantics(version): # noqa: ARG001 | ||
| rows = list(ContentArtifact.objects.filter(relative_path="top.txt")) | ||
| assert single_or_raise(ContentArtifact, rows) == rows[0] | ||
| with pytest.raises(ContentArtifact.DoesNotExist): | ||
| single_or_raise(ContentArtifact, []) | ||
| with pytest.raises(ContentArtifact.MultipleObjectsReturned): | ||
| single_or_raise(ContentArtifact, list(ContentArtifact.objects.all()[:2])) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.