Skip to content

Remove hosted project rows after final purge #1326

Description

@gaborbernat

Problem

A hosted project's display row outlives its files. Deleting every file of a project leaves the root Simple index advertising a project whose detail page answers 404, which breaks the PEP 691 contract instead of just showing stale text.

Reproduced against e64cc8308 through the served HTTP surface: upload one wheel to a hosted index, DELETE /root/pypi/peryxpkg/, then read both views. Detail answers 404 while the root index answers 200 with {"meta":{"api-version":"1.4"},"projects":[{"name":"peryxpkg"}]}.

The cause is that remove_upload_row maintains the per-index counts but never touches the project row, and list_projects returns every project holding a PROJECTS row minus retired tombstones, without consulting whether any live upload remains.

Corrections to the original report

The first version of this issue named delete_upload as the entry point. That is wrong and sends a reader down a dead end, so this corrects it.

delete_upload is test-only. Nothing in production calls it. mutate_uploads is test-only as well, which makes UploadMutation::Delete and the remove_upload_row call behind it unreachable outside tests.

HTTP DELETE soft-deletes. The real path is remove_files_with_webhook to mutate_uploads_and_overrides, whose closure returns a replacement record with trashed set. It removes no row. So the reproduction above is the trash case: the record survives, marked trashed, the detail page stops serving it, and the root listing keeps advertising the project.

No production path removes a hosted upload row from the store. Retention plans and exports decisions and nothing applies them; the ExportHeader doc in peryx-driver/src/retention.rs refers to "a later apply" that does not exist yet. This reads as a boundary nobody has built yet, not a second defect, and it decides what this issue can promise.

Required change

Maintain a persisted live-upload count or visibility marker per hosted project, written in the same transaction as every mutation that can change it: upload, trash, restore, promotion, and the store's delete paths. A second transaction would replace the inconsistency this closes with a new window.

list_projects includes a hosted project when the marker shows at least one untrashed upload and excludes it at zero. The display row stays while trashed records remain, so a restore returns the original spelling. When the final upload record goes, both the display and marker rows go with it, and a later upload writes fresh display spelling.

Acceptance checks

  • Publish one file and trash it. Root and detail views omit the project; restore returns the original display spelling.
  • Delete one file from a multi-file project. Root and detail views retain the project.
  • Purge the trashed file. Root and detail views still omit the project, and the display and visibility rows no longer exist.
  • Upload the project after final purge with different display spelling. Root and detail views use the new spelling.

The first two are reachable today, and a fix answers to them. The last two have no production trigger, because nothing purges. A PR can drive them through the store API so the invariant holds once an apply phase arrives, and should claim exactly that instead of an end-to-end purge it cannot exercise.

Architecture and test boundary

Keep PyPI protocol rules, normalization, metadata semantics, handlers, tooling, and behavioral tests in peryx-ecosystem-pypi. Shared crates may expose ecosystem-neutral data and narrow capability traits; peryx-ecosystem-pypi implements those traits.

Do not add a shared-crate dependency on peryx-ecosystem-pypi or a PyPI branch in generic orchestration. Use peryx-plugin-registry only where the binary composes concrete plugins. The ecosystem crate must build and test independently. System tests may verify composition without taking ownership of PyPI behavior.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:managementIndex, user, ACL, and server managementarea:pypiPython package index behaviorarea:storageBlob stores, object storage, and storage backend worktype:bugIncorrect behavior or missing validation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions