Repository navigation
Conversation
Each uploaded file is added by a task that holds the repository exclusively. The task was dispatched as immediate only, so an upload arriving while another one held the repository was canceled and answered with 429. Maven clients do not retry that, so two builds publishing into the same repository at once failed. The task may now wait in the queue for a worker, and the request waits for it for up to MAVEN_DEPLOY_TASK_TIMEOUT seconds (30 by default) before answering 429, as the container registry API does for its uploads.
Every repository version regenerates maven-metadata.xml, prefixes.txt and the directory index pages of what it changed. Most of that cost did not depend on the size of the change: - Packages and metadata were added and removed in up to seven add_content / remove_content calls, and each rewrites the version's whole list of content ids. They are now collected and applied in one add and one remove. - Deciding whether prefixes.txt changed scanned the group ids of every artifact in the repository, twice. Only the affected prefixes are looked up now. - The index pages of the affected directories were found through the version's whole content list; they are now checked for membership one by one. - Metadata files and index pages were written to storage one after another. They are now written in parallel, by a pool of threads that lives as long as the process so its storage connections stay open. A forked child, where pulpcore runs most tasks, starts its own pool, since it inherits the pool but not its threads. The resulting repository content is unchanged.
The deploy API turns every uploaded file into its own repository version. A Maven, Gradle or sbt publish uploads about fifteen files per module, the checksum files among them, so a module costs fifteen versions, each of which regenerates metadata and index pages and stores the full list of the repository's content ids. Publishing is slow and the versions table grows with the square of the number of deployed files. With MAVEN_DEPLOY_BATCH on, a deploy stores the file and its content unit, records it as pending for the repository (MavenPendingDeploy) and answers 201 at once. A commit task adds everything pending in one version once uploads pause for MAVEN_DEPLOY_BATCH_LINGER seconds, or at the latest MAVEN_DEPLOY_BATCH_MAX_WAIT seconds after the oldest pending file. This follows the pending blobs of container pushes and the grouped uploads of the Python plugin. What a client sees stays the same: - A pending file is served by the distribution until its batch is committed, and recording it invalidates the content cache, so a build can resolve what it has just published. - Within a batch the newest file deployed to a path wins. - A maven-metadata.xml the client deployed within a batch is kept for its artifact or SNAPSHOT version instead of being regenerated, which is what happens when it arrives last in a version of its own. It matters for SNAPSHOTs deployed under timestamped names, which the generated version-level metadata does not resolve to. Uploads arriving together dispatch one commit task between them, under a Postgres advisory lock per repository. Off by default; without it the deploy API behaves as before.
This branch has not been deployed
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Problem
The deploy API (
PUT /pulp/maven/<repo>/<path>) turns every uploaded file into its ownrepository version. That has three consequences that get worse as a repository is used:
immediate=True, deferred=Falseand the repository as an exclusive resource. If anotherdeploy holds the repository at that moment, pulpcore cancels the task ("Resources
temporarily unavailable.") and the API answers 429. Maven, Gradle and sbt do not retry a
refused upload, so two pipelines publishing into one repository at the same time fail.
With 2 parallel uploads one gets 429; with 16, most of them do.
finalize_new_versionregeneratesmaven-metadata.xml(two levels for SNAPSHOTs),
prefixes.txtand the affected index pages, and its costgrows with the repository: up to seven
add_content/remove_contentcalls each rewritethe version's whole
content_ids,prefixes.txtscans every artifact's group id twice,the index-page lookup expands the whole content list, and the generated files are
written to storage one after another. On a test deployment adding one file took
0.8 s on a repository of about 600 units and 2.5 s on one of about 8,500.
sources, javadoc, pom, each with
.md5and.sha1), so a module costs 15 versions.Since every version stores the full
content_idsarray, the versions table grows withthe square of the number of deployed files: a test repository with 1,330 versions held
24 MB of id arrays for 3,900 units.
Changes
Three commits, each with its own changelog entry:
Let concurrent deploys to one repository wait for their turn. The add task may now
be deferred, and the request waits for it for up to
MAVEN_DEPLOY_TASK_TIMEOUTseconds(default 30) before answering 429, as the container registry API does for its uploads.
A new functional test deploys 8 files at once; it fails with 429 without the change.
Make finalizing a repository version cheaper. Packages and metadata are collected
and applied with one
add_contentand oneremove_content; whetherprefixes.txtchanged is decided from the affected prefixes only; affected index pages are found by
membership (
Existson_content_relationships(), the same pattern as the contenthandler); metadata and index pages are written to storage in parallel by a
per-process thread pool (reset in forked task processes, which inherit the pool but
not its threads). The resulting repository content is unchanged; on its own this saves
about 10% per deploy.
Add deployed files to the repository in batches (opt-in). With
MAVEN_DEPLOY_BATCH = True, a deploy stores the file and its content unit, records itin a new
MavenPendingDeploytable and answers 201 at once. Acommit_pending_deploystask adds everything pending in one version once uploads pause for
MAVEN_DEPLOY_BATCH_LINGERseconds (default 1), at the latestMAVEN_DEPLOY_BATCH_MAX_WAITseconds (default 10) after the oldest pending file. Thisfollows the pending blobs of container pushes and
PYTHON_GROUP_UPLOADS. What clientssee stays the same:
invalidates the content cache, so a build can resolve what it has just published;
maven-metadata.xmldeployed by the client within a batch is kept for its artifactor SNAPSHOT version, as it is today when it arrives last in a version of its own.
Uploads arriving together dispatch a single commit task (a Postgres advisory lock per
repository around the check and the dispatch). Off by default; without the setting the
deploy API behaves as after commit 1.
Results on a test deployment
A scenario covering releases, a SNAPSHOT, redeploys of a release and a SNAPSHOT, a new
prefix, a one-segment group id and a client-uploaded
maven-metadata.xmlends in the samerepository content (all units with their fields, metadata, index pages,
prefixes.txt,timestamps aside) with and without batching: 73 versions without it, 2 with it.
Tests
test_concurrent_deploy.py: parallel deploys all succeed (commit 1).test_deploy_batch.py, run when the server hasMAVEN_DEPLOY_BATCHon: sequentialdeploys share versions, parallel deploys all land, a file is served before its batch is
committed, and a client's timestamped SNAPSHOT metadata is kept. A test that a redeploy
within one batch serves the newer file needs Replace superseded content when redeploying the same coordinates #552 (a redeploy with different bytes keeps
serving the first file today, batched or not); I will add it once Replace superseded content when redeploying the same coordinates #552 is merged.
test_mvn_deploy.pywaits for the batch instead of expecting 12 versions when batchingis on.
Related finding, not changed here
The generated version-level
maven-metadata.xmllists<value>1.1.0-SNAPSHOT</value>for SNAPSHOTs deployed under timestamped file names, so Maven cannot resolve them from the
generated file. Today this is masked because the client's own metadata arrives last and is
not regenerated; batching keeps that behaviour explicitly.
I can open a separate issue for it, and split this PR into three if that is easier to
review.