Skip to content

Scope dav recursive list and delete to the requested prefix#130

Open
wayneeseguin wants to merge 1 commit into
cloudfoundry:mainfrom
fivetwenty-io:fix/dav-delete-recursive-prefix-scope
Open

Scope dav recursive list and delete to the requested prefix#130
wayneeseguin wants to merge 1 commit into
cloudfoundry:mainfrom
fivetwenty-io:fix/dav-delete-recursive-prefix-scope

Conversation

@wayneeseguin

Copy link
Copy Markdown

Fixes #129

Problem

The dav backend's List(prefix) starts its Depth: 1 PROPFIND walk at the blobstore endpoint root and applies the prefix only when it reaches a leaf, so DeleteRecursive traverses the entire blobstore to delete one prefix. Against a production-sized WebDAV blobstore, where keys are partitioned into two-character directories, that is thousands of PROPFIND round trips per delete. Cloud Controller calls delete-recursive to clear an app's buildpack cache on every cf delete -r, and the walk does not finish before the operation times out. The s3 backend already scopes the same operation by passing the prefix to the server.

Fix

Two changes in dav/client/storage_client.go, both affecting only which requests are made, not what is returned:

  • List now roots the walk at the deepest directory the prefix fully names — the portion of the prefix up to its last /, computed by the new prefixDir helper. Blob IDs are matched as string prefixes rather than directory paths, so only that portion is guaranteed to be a directory; the final segment may match both a file (ab/cd/guid-file) and a directory (ab/cd/guid/), and starting one level up keeps both reachable. For Cloud Controller's partitioned keys (ab/cd/<guid>), the walk starts at ab/cd and a recursive delete becomes two PROPFINDs instead of a whole-store traversal.

  • listRecursive now prunes subcollections that cannot contain matching blob IDs, via the new collectionMayContainPrefix helper: every blob under a collection with relative path rel has an ID starting with rel + "/", so the walk descends only when rel + "/" and the prefix are string prefixes of one another. This bounds single-segment prefixes (which name no directory) and keeps the walk narrow below the starting point.

Two details of the starting URL:

  • prefixDir rejects directories that would escape the endpoint through .. segments (path.Dir cleans the path, so ../etc and a/../../b are caught). Such prefixes fall back to the endpoint-root walk, exactly as before — they can never match a blob ID, so the result stays empty, and no request is ever issued outside the configured endpoint path.
  • The starting URL is slash-terminated, marking it as a collection. That matches the slash-terminated hrefs the recursion already follows, and avoids directory redirects from servers that insist on the canonical form (Go's HTTP client would replay a redirected PROPFIND as a GET).

Observable behavior is unchanged: the leaf filter is untouched, List("") still walks the whole store, and a prefix whose directory does not exist still yields an empty list (the existing 404-on-PROPFIND handling), which DeleteRecursive already treats as a no-op.

Testing

  • New unit tests in dav/client/storage_client_list_test.go run List and DeleteRecursive against an httptest WebDAV fake that records every PROPFIND and DELETE path, and assert both the returned/deleted blob sets and the exact set of directories the walk touched — e.g. deleting ab/cd/<guid> PROPFINDs only ab/cd and ab/cd/<guid>, and never enters sibling partitions.
  • A regression test pins the empty-prefix case to the full-store walk and the complete blob list.
  • Table tests cover prefixDir and collectionMayContainPrefix edge cases (empty, single-segment, trailing-slash, leading-slash, and ..-containing prefixes), and a dedicated test asserts that escaping prefixes never produce a PROPFIND outside the endpoint path.
  • A new dav integration spec exercises the scoped walk against the real Dockerized nginx WebDAV server with multi-segment partitioned keys (ab/cd/<id>): it puts blobs under the prefix plus bystanders in sibling partitions, then verifies list and delete-recursive touch only the prefixed blobs. The existing integration prefixes are all single-segment, which never exercise the deeper starting URL.
  • go build ./..., go vet ./..., go test ./dav/... (unit and Docker integration, 8 of 8 specs), and golangci-lint run ./dav/... all pass. The gcs/azurebs/s3 integration suites need live cloud credentials and were not run.

The dav backend's List started its PROPFIND walk at the blobstore
endpoint root and filtered by prefix only at the leaves, so
DeleteRecursive traversed every directory in the store to delete one
prefix. Against a large partitioned WebDAV blobstore this issues
thousands of PROPFIND round trips per delete and the operation times
out.

Bound the walk to the prefix's subtree:

- List now starts the PROPFIND at the deepest directory the prefix
  fully names (the portion up to its last slash), matching how the s3
  backend pushes the prefix to the server. Directories that would
  escape the endpoint through dot-dot segments are rejected and fall
  back to the endpoint-root walk, and the starting URL is
  slash-terminated so servers that redirect directory URLs to their
  canonical form are not triggered.
- listRecursive now skips collections whose paths cannot contain blob
  IDs matching the prefix, which prunes sibling partitions when the
  prefix does not name a directory.

Blob IDs are still matched as string prefixes at the leaves, so the
observable results of List and DeleteRecursive are unchanged; only the
traversal is narrower. A missing prefix directory still yields an empty
list via the existing 404 handling.

Add a dav integration spec covering multi-segment prefixes, which the
existing single-segment specs never exercised.

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

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

dav DeleteRecursive walks the entire blobstore instead of the target prefix

1 participant