-
-
Notifications
You must be signed in to change notification settings - Fork 859
repository: fix Repository.delete edge cases (#9815) #9820
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -798,48 +798,46 @@ def put(self, id, data): | |
| # PackWriter shares this repository's index, so add() triggers the lazy build itself. | ||
| return self._pack_writer.add(id, data) | ||
|
|
||
| def delete(self, id): | ||
| def delete(self, id, *, persist=True): | ||
| """Delete a single repo object by rewriting its pack without it. | ||
|
|
||
| A pack holds several objects, so we can not just drop the whole pack: that would take the | ||
| target's innocent neighbours with it. Route through compact_pack, which copies the survivors | ||
| into a new pack and repoints them in the chunk index. Rewriting a whole pack to remove one | ||
| object is slow, but delete is only used by special-purpose callers (tests, the debug command, | ||
| check --repair), never on a hot path, so the cost does not matter. | ||
| A pack holds several objects, so we rewrite it via compact_pack(complete=False) rather than | ||
| dropping the whole pack. With persist=True the full chunk index is written back so the next | ||
|
Comment on lines
+804
to
+805
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please read these AI-made comments and make sure they don't talk about the bad hacks of the past? Dropping a whole pack instead of a defect chunk is nothing we need to discuss or even mention. |
||
| borg process sees the deletion; bulk callers that rebuild the index themselves (check --repair) | ||
| pass persist=False to avoid a full index rewrite per deleted object. | ||
| """ | ||
| self._lock_refresh() | ||
| from .cache import write_chunkindex_to_repo | ||
|
|
||
| entry = self.chunks.get(id) | ||
| if entry is None: | ||
| raise self.ObjectNotFound(id, str(self._location)) | ||
| pack_id = entry.pack_id | ||
| # keep every other object in the same pack; compact_pack rewrites the pack without the target. | ||
| # work off the live index (each entry already carries its offset/size after flush): do not | ||
| # rebuild from the packs here, that could not tell the target apart from stale duplicate copies | ||
| # of the same id left in other packs by re-puts (build keeps an arbitrary one of them). | ||
| # build keep_ids from the live index (offsets are filled in after flush), not from the packs: | ||
| # a rebuild could not tell the target from stale duplicates that re-puts left in other packs. | ||
| keep_ids = {cid for cid, e in self.chunks.iteritems() if e.pack_id == pack_id} | ||
| keep_ids.discard(id) | ||
| self.compact_pack(pack_id, keep_ids=keep_ids, drop_ids={id}) | ||
| # persist a full index so a following borg process sees the deletion; close() only writes new | ||
| # entries incrementally and would not record the removal. | ||
| write_chunkindex_to_repo(self, self.chunks, incremental=False, force_write=True, delete_other=True) | ||
| self.compact_pack(pack_id, keep_ids=keep_ids, drop_ids={id}, complete=False) | ||
| if persist: | ||
| # close() only persists new entries incrementally, so write the full index here to record | ||
| # the removal for the next borg process. | ||
| from .cache import write_chunkindex_to_repo | ||
|
|
||
| write_chunkindex_to_repo(self, self.chunks, incremental=False, force_write=True, delete_other=True) | ||
|
|
||
| def compact_pack(self, pack_id, *, keep_ids: set, drop_ids: set): | ||
| def compact_pack(self, pack_id, *, keep_ids: set, drop_ids: set, complete: bool = True): | ||
| """Rewrite pack <pack_id>, keeping <keep_ids> and dropping <drop_ids>, then delete the old pack. | ||
|
|
||
| keep_ids and drop_ids are sets of chunk ids that must together be every object of the pack, as | ||
| recorded in the chunk index. The caller guarantees that completeness (both callers build the two | ||
| sets by iterating the index for this pack_id); here we only assert their ranges tile contiguously | ||
| from offset 0 with no gap or overlap, and that their intersection is empty. Kept objects are | ||
| copied into a new pack via store.defrag and repointed in the chunk index; dropped objects' index | ||
| entries are removed. | ||
| keep_ids and drop_ids come from the caller iterating the chunk index for this pack_id; they | ||
| must not overlap. With complete=True (compact) they are the pack's whole object set, so their | ||
| ranges must tile it from offset 0 with no gap or overlap. With complete=False (delete) the index | ||
| view may be partial (a re-put can repoint an earlier object to another pack while its bytes stay | ||
| here), so we copy only the known survivors at their real offsets and allow gaps. Kept objects are | ||
| copied into a new pack via store.defrag and repointed; dropped objects' index entries are removed. | ||
|
Comment on lines
+829
to
+834
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rewrite this to be more clear. also remove crap like "come from the caller iterating chunk index for this pack id". a description of parameters needs to tell what they are and what we do here with them, not where a call comes from or what the caller is doing. |
||
|
|
||
| Returns the new pack_id, None if nothing is kept (pack dropped), or <pack_id> unchanged if the | ||
| kept objects reproduce the old pack (same sha256 name, nothing to delete). | ||
|
|
||
| Updates the in-memory chunk index only. The caller holds the exclusive lock and owns index | ||
| durability: invalidate the cached index before calling, write it back after, as compact does. | ||
| Updates the in-memory chunk index only; the caller holds the exclusive lock and owns index | ||
| durability (invalidate before, write back after, as compact does). | ||
| """ | ||
| self._lock_refresh() | ||
| pack_key = "packs/" + bin_to_hex(pack_id) | ||
|
|
@@ -855,13 +853,12 @@ def compact_pack(self, pack_id, *, keep_ids: set, drop_ids: set): | |
| located.append((entry.obj_offset, obj_id, entry.obj_size, keep)) | ||
| located.sort() | ||
|
|
||
| # keep + drop tile the pack contiguously from offset 0; collect the objects to keep in the same | ||
| # pass. we do not cross-check against the pack's on-disk size: the caller already guarantees the | ||
| # two sets are the pack's complete object set. | ||
| # walk objects in offset order, collecting the survivors. | ||
| kept = [] # (obj_offset, obj_id, obj_size), offset-ordered | ||
| covered = 0 | ||
| for offset, obj_id, size, keep in located: | ||
| assert offset == covered, f"gap or overlap in pack {bin_to_hex(pack_id)} at offset {covered}" | ||
| if complete: # keep+drop are the whole pack: they must tile it with no gap or overlap | ||
| assert offset == covered, f"gap or overlap in pack {bin_to_hex(pack_id)} at offset {covered}" | ||
| covered += size | ||
| if keep: | ||
| kept.append((offset, obj_id, size)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -374,7 +374,6 @@ def test_extra_chunks(archivers, request): | |
| cmd(archiver, "check", "-v", exit_code=0) # check does not deal with orphans anymore | ||
|
|
||
|
|
||
| @pytest.mark.skip(reason="TODO: test broken due to packs refactoring") | ||
| @pytest.mark.parametrize("init_args", [["--encryption=aes256-ocb"], ["--encryption", "none"]]) | ||
| def test_verify_data(archivers, request, init_args): | ||
| archiver = request.getfixturevalue(archivers) | ||
|
|
@@ -390,10 +389,14 @@ def test_verify_data(archivers, request, init_args): | |
| for item in archive.iter_items(): | ||
| if item.path.endswith(src_file): | ||
| chunk = item.chunks[-1] | ||
| data = repository.get(chunk.id) | ||
| data = corrupt(data, 123) | ||
| data = corrupt(repository.get(chunk.id), 123) | ||
| # re-putting the same id would not repoint the index: close() persists new index | ||
| # entries incrementally, not updates to existing ones, so the next process would still | ||
| # read the old, good copy. delete first, then put the corrupted bytes as a fresh entry. | ||
| repository.delete(chunk.id) | ||
| repository.put(chunk.id, data) | ||
|
Comment on lines
+392
to
397
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, that should work better. even better than that would be to simulate reality of bit corruption more closely: use some low-level code that directly corrupts the file on disk without going through delete/put and without modifying the index. |
||
| break | ||
| repository.flush() # make the corrupted put durable before close() | ||
|
|
||
| # the normal archives check does not read file content data. | ||
| cmd(archiver, "check", "--archives-only", exit_code=0) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
persistis a bad name, maybeupdate_indexor so?