Repository: borgbackup/borg

Issue:
Prune command delete size is incorrect
<!--
Thank you for reporting an issue.

*IMPORTANT* -  *before* creating a new issue please look around:
 - Borgbackup documentation: http://borgbackup.readthedocs.io/en/stable/index.html
 - FAQ: https://borgbackup.readthedocs.io/en/stable/faq.html
 and
 - open issues in Github tracker: https://github.com/borgbackup/borg/issues
  
If you cannot find a similar problem, then create a new issue.

Please fill in as much of the template as possible.
-->

## Have you checked borgbackup docs, FAQ, and open Github issues?

Yes. May be that #3260 is related to this and was misunderstood? I'm interested in the Deleted Data line, the All archives line is correct.

## Is this a BUG / ISSUE report or a QUESTION?

Bug

## System information. For client/server mode post info for both machines.

#### Your borg version (borg -V).
```
$ borg -V
borg 1.1.9
```

#### Operating system (distribution) and version.
Ubuntu 18.04.1

#### Hardware / network configuration, and filesystems used.
GCE Instance, Ext4 filesystem

#### How much data is handled by borg?
In this instance, 2 x 100GB Google Persistent Disks

#### Full borg commandline that lead to the problem (leave away excludes and passwords)
(Commands are run automatically by a script, below is what the constructed command would look like, some things like the timestamp are just placeholders)

Prune command
```
borgbackup prune --list --stats -P gcs-suq-mariadb-3-1-mysql-backups --keep-last 4 --keep-daily 14 --keep-weekly 2 --keep-monthly 3 --keep-yearly 10 /backups/borg/mysql-1
```

Creation command
```
borgbackup create --read-special -v --stats --timestamp XYZ --comment "src: gcs-suq-mariadb-3-1-mysql-backups" /backups/borg/mysql-1::gcs-suq-mariadb-3-1-mysql-backups-1555126202 /dev/sdd
```

## Describe the problem you're observing.

Our backup routine runs twice a day, automatically. We are backing up full disk images.
On certain borg prune runs, it appears to delete twice the expected data, I believe the stats are incorrect, as they are exactly double of what is expected (The negative original size, and negative compressed sizes. Deduplicated size does appear to be correct).

#### Can you reproduce the problem? If so, describe how. If not, describe troubleshooting steps you took before opening the issue.

Yes. I have it occurring on a number of jobs that are all similar. They all backup a disk image, then prune the repository (with the above prune schedule). Every 2nd prune actually prunes something, and when it does, the prune removes 1 archive, but the removed data appears to be twice what the archive was. I've only proved details from 1 backup job here, but this even occurs on a backup of a 1TB disk, that prune reports as 2TB's pruned, and a backup of a 1.83TB disk that prune reports as 3.65TB's pruned.

#### Include any warning/errors/backtraces from the system logs

Creation of the archive that gets pruned
```
[2019-04-13 15:41:00] Starting Borg Backup
Creating archive at "/backups/borg/mysql-1::gcs-suq-mariadb-3-1-mysql-backups-1555126202"
------------------------------------------------------------------------------
Archive name: gcs-suq-mariadb-3-1-mysql-backups-1555126202
Archive fingerprint: e61ee1cf81f76ca74345a78941aa08aa095fc047d2ca5552b4417975a72527a2
Time (start): Sat, 2019-04-13 06:40:56
Time (end):   Sat, 2019-04-13 07:14:06
Duration: 33 minutes 10.04 seconds
Number of files: 1
Utilization of max. archive size: 0%
------------------------------------------------------------------------------
                       Original size      Compressed size    Deduplicated size
This archive:              107.38 GB            421.60 MB              8.30 kB
All archives:                3.97 TB             15.60 GB              4.04 MB

                       Unique chunks         Total chunks
Chunk index:                     160               473700
------------------------------------------------------------------------------
```

Then the prune that removes that 1 archive.
```
[2019-04-15 16:15:04] Applying retention policy to backup prefix gcs-suq-mariadb-3-1-mysql-backups in repository /backups/borg/mysql-1
Keeping archive: gcs-suq-mariadb-3-1-mysql-backups-1555299001 Mon, 2019-04-15 06:45:19 [816d4fa0d04512aac15df69a4e18a7d875215213d20f6492e3348bc672fc9cb0]
Keeping archive: gcs-suq-mariadb-3-1-mysql-backups-1555255802 Sun, 2019-04-14 18:40:48 [fa30a7733f19aca65560daac46f39afbf17691b16a306a8b2e1697948c2d6e41]
Keeping archive: gcs-suq-mariadb-3-1-mysql-backups-1555212602 Sun, 2019-04-14 06:44:12 [397cad5df5abe3603d6977047ab32a815b5854ef70bd3e37cec1f075bd6d0c73]
Keeping archive: gcs-suq-mariadb-3-1-mysql-backups-1555169402 Sat, 2019-04-13 18:44:23 [9a708ca878e8c1850224a1922f66092358895450dd6b33b3a15eeb6f1d5134e2]
Pruning archive: gcs-suq-mariadb-3-1-mysql-backups-1555126202 Sat, 2019-04-13 06:40:56 [e61ee1cf81f76ca74345a78941aa08aa095fc047d2ca5552b4417975a72527a2] (1/1)
Keeping archive: gcs-suq-mariadb-3-1-mysql-backups-1555083001 Fri, 2019-04-12 18:48:01 [83371f37a189e8d2ae6e47e37404256a415fef2fd83a1feca23bc6a7480e7d77]
Keeping archive: gcs-suq-mariadb-3-1-mysql-backups-1554996601 Thu, 2019-04-11 18:46:36 [690503847ede39675b6024275ea4bfadc1378092f26b0c4cd7b182a15574266b]
Keeping archive: gcs-suq-mariadb-3-1-mysql-backups-1554910202 Wed, 2019-04-10 18:46:28 [2d3722fa81f0d9a7ff18ad6853a42b3e0997074af8caec6c6e78166d6c688390]
Keeping archive: gcs-suq-mariadb-3-1-mysql-backups-1554823801 Tue, 2019-04-09 18:50:59 [e822c51f8fee8d7958c2872e9025f08949ed942483aa34cd5b1622e4177a61dd]
Keeping archive: gcs-suq-mariadb-3-1-mysql-backups-1554737402 Mon, 2019-04-08 18:59:28 [56b325ab7076224519bada02732d6c0a630d318f29b7a660d4f603f5d9a463b7]
Keeping archive: gcs-suq-mariadb-3-1-mysql-backups-1554651002 Sun, 2019-04-07 18:41:50 [c378e3d18836e5ed6eb2b12dc517e5d9b65bf144eca35b58df37f4681900c144]
Keeping archive: gcs-suq-mariadb-3-1-mysql-backups-1554564602 Sat, 2019-04-06 18:56:44 [2ef531bacad772e45fa712aec65f62b0c0f9a6b1c8eb7b6f2afd9d0df45a1c82]
Keeping archive: gcs-suq-mariadb-3-1-mysql-backups-1554478202 Fri, 2019-04-05 18:46:03 [92720263fb2aff194064132209be83250fbb8248a9c11da8eb4f00d782cb64bf]
Keeping archive: gcs-suq-mariadb-3-1-mysql-backups-1554391802 Thu, 2019-04-04 19:00:32 [133ff744641ea97d64668097f97bbfe9448c86fbe8883b40c431f3f2d51cace6]
Keeping archive: gcs-suq-mariadb-3-1-mysql-backups-1554305402 Wed, 2019-04-03 18:49:48 [2f9baf9bd8f6beecf03806fc3085ce061943ebdd43ba058c8c61229c58af603e]
------------------------------------------------------------------------------
                       Original size      Compressed size    Deduplicated size
Deleted data:             -214.75 GB           -843.19 MB             -8.30 kB
All archives:                4.40 TB             17.28 GB              4.05 MB

                       Unique chunks         Total chunks
Chunk index:                     171               524911
------------------------------------------------------------------------------
```

Oddly enough, another disk that is in that archive is added/removed right before that, and it's numbers are correct.

```
Creating archive at "/backups/borg/mysql-1::gcs-suq-mariadb-3-1-mysql-data-1555299001"
------------------------------------------------------------------------------
Archive name: gcs-suq-mariadb-3-1-mysql-data-1555299001
Archive fingerprint: 79e62db6e5ae8536defd0187781179c4de015637f178c2356e82370243eee9d8
Time (start): Mon, 2019-04-15 06:45:57
Time (end):   Mon, 2019-04-15 07:11:07
Duration: 25 minutes 10.11 seconds
Number of files: 1
Utilization of max. archive size: 0%
------------------------------------------------------------------------------
                       Original size      Compressed size    Deduplicated size
This archive:              107.37 GB            421.25 MB              3.21 kB
All archives:                4.51 TB             17.70 GB              4.05 MB

                       Unique chunks         Total chunks
Chunk index:                     174               537714
------------------------------------------------------------------------------

Keeping archive: gcs-suq-mariadb-3-1-mysql-data-1555299001 Mon, 2019-04-15 06:45:57 [79e62db6e5ae8536defd0187781179c4de015637f178c2356e82370243eee9d8]
Keeping archive: gcs-suq-mariadb-3-1-mysql-data-1555255802 Sun, 2019-04-14 18:41:26 [324c8f81e7b632b6496d3e26c582942bbe343a890dd0cf9d08eee6087e741c51]
Keeping archive: gcs-suq-mariadb-3-1-mysql-data-1555212602 Sun, 2019-04-14 06:44:48 [80578b39cbb7d1579ca4cd81de6c66a89c834df9927fd6611c37c68ec43790cc]
Keeping archive: gcs-suq-mariadb-3-1-mysql-data-1555169402 Sat, 2019-04-13 18:45:01 [fdd9e1cb98f5f1f5faab57670ad1fb8c0896b0b2f48aea0f3971145b0505e141]
Pruning archive: gcs-suq-mariadb-3-1-mysql-data-1555126202 Sat, 2019-04-13 06:41:35 [8dad6025739b0b3d82fe58f9afbb281d12ba88a210747db4cd757406d4ad5215] (1/1)
Keeping archive: gcs-suq-mariadb-3-1-mysql-data-1555083001 Fri, 2019-04-12 18:48:37 [fee3ad92d458e69cc3d119d5984e5a08a427fa03c8927c5e76b3230ecd5fadad]
Keeping archive: gcs-suq-mariadb-3-1-mysql-data-1554996601 Thu, 2019-04-11 18:47:13 [b9d33e9345939851dfc894dd8240ce695760367f689a18516f5d3989fb978d11]
Keeping archive: gcs-suq-mariadb-3-1-mysql-data-1554910202 Wed, 2019-04-10 18:47:07 [8c552d18c1c15ee3c338be17d55a11e2c0fedd19e86b809490a67608affe92ad]
Keeping archive: gcs-suq-mariadb-3-1-mysql-data-1554823801 Tue, 2019-04-09 18:51:48 [61b14598495808916cc59036a539ce8450208be172f2b4902a35f16354efdf39]
Keeping archive: gcs-suq-mariadb-3-1-mysql-data-1554737402 Mon, 2019-04-08 19:00:05 [a527384db0454c10f38f4dd589caac9e32b21fe8492f5ab7e07f01d80e6e0a77]
Keeping archive: gcs-suq-mariadb-3-1-mysql-data-1554651002 Sun, 2019-04-07 18:42:27 [145fc8968c86efa3cb32e0fe1ec8bb7ac59652f60b5daf982ff93c75e6070275]
Keeping archive: gcs-suq-mariadb-3-1-mysql-data-1554564602 Sat, 2019-04-06 18:57:22 [7e5bf9efc184743cfa163aa3194ed94c534639f13e689cb1c2bf7125700503f3]
Keeping archive: gcs-suq-mariadb-3-1-mysql-data-1554478202 Fri, 2019-04-05 18:46:39 [138e32bd9a21428bd51929baf7458a5980657037caecb8498c287b64afdcd220]
Keeping archive: gcs-suq-mariadb-3-1-mysql-data-1554391802 Thu, 2019-04-04 19:01:08 [f9f3efb3064ce8f62cdf8ce70e52088929c8dc723aa01914a1575a632a8ad6f0]
------------------------------------------------------------------------------
                       Original size      Compressed size    Deduplicated size
Deleted data:             -107.37 GB           -421.25 MB             -3.21 kB
All archives:                4.40 TB             17.28 GB              4.05 MB

                       Unique chunks         Total chunks
Chunk index:                     171               524911
------------------------------------------------------------------------------
```

<!--

If this complaint relates to borg performance, please include CRUD benchmark
results and any steps you took to troubleshoot.
How to run benchmark: http://borgbackup.readthedocs.io/en/stable/usage/benchmark.html

*IMPORTANT* - Please mark logs and text output from terminal commands 
or else Github will not display them correctly. 
An example is provided below.

Example:
```
this is an example how log text should be marked (wrap it with ```)
```
-->



Discussion:
Guess this might be a duplicate to an already existing ticket (maybe not for prune, but for info).

The double size comes from not considering part files correctly (== not at all, except if --consider-part-files is given) and is a cosmetic problem.
I noticed that part-file support is missing for archive deletion (and thus for prune). Working on this now.

Resolving patch:
diff --git a/src/borg/archive.py b/src/borg/archive.py
--- a/src/borg/archive.py
+++ b/src/borg/archive.py
@@ -845,9 +845,9 @@ def fetch_async_response(wait=True):
                 error = True
                 return exception_ignored  # must not return None here
 
-        def chunk_decref(id, stats):
+        def chunk_decref(id, stats, part=False):
             try:
-                self.cache.chunk_decref(id, stats, wait=False)
+                self.cache.chunk_decref(id, stats, wait=False, part=part)
             except KeyError:
                 cid = bin_to_hex(id)
                 raise ChunksIndexError(cid)
@@ -869,8 +869,9 @@ def chunk_decref(id, stats):
                     for item in unpacker:
                         item = Item(internal_dict=item)
                         if 'chunks' in item:
+                            part = not self.consider_part_files and 'part' in item
                             for chunk_id, size, csize in item.chunks:
-                                chunk_decref(chunk_id, stats)
+                                chunk_decref(chunk_id, stats, part=part)
                 except (TypeError, ValueError):
                     # if items metadata spans multiple chunks and one chunk got dropped somehow,
                     # it could be that unpacker yields bad types
diff --git a/src/borg/archiver.py b/src/borg/archiver.py
--- a/src/borg/archiver.py
+++ b/src/borg/archiver.py
@@ -1132,8 +1132,9 @@ def _delete_archives(self, args, repository):
                 msg = 'Would delete archive: {} ({}/{})' if dry_run else 'Deleting archive: {} ({}/{})'
                 logger.info(msg.format(format_archive(manifest.archives[archive_name]), i, len(archive_names)))
                 if not dry_run:
-                    Archive(repository, key, manifest, archive_name, cache=cache).delete(
-                        stats, progress=args.progress, forced=args.forced)
+                    archive = Archive(repository, key, manifest, archive_name, cache=cache,
+                                      consider_part_files=args.consider_part_files)
+                    archive.delete(stats, progress=args.progress, forced=args.forced)
             if not dry_run:
                 manifest.write()
                 repository.commit(compact=False, save_space=args.save_space)
@@ -1430,7 +1431,9 @@ def do_prune(self, args, repository, manifest, key):
                     else:
                         archives_deleted += 1
                         log_message = 'Pruning archive (%d/%d):' % (archives_deleted, to_delete_len)
-                        Archive(repository, key, manifest, archive.name, cache).delete(stats, forced=args.forced)
+                        archive = Archive(repository, key, manifest, archive.name, cache,
+                                          consider_part_files=args.consider_part_files)
+                        archive.delete(stats, forced=args.forced)
                 else:
                     if is_checkpoint(archive.name):
                         log_message = 'Keeping checkpoint archive:'
diff --git a/src/borg/cache.py b/src/borg/cache.py
--- a/src/borg/cache.py
+++ b/src/borg/cache.py
@@ -909,16 +909,16 @@ def chunk_incref(self, id, stats, size=None, part=False):
         stats.update(_size, csize, False, part=part)
         return ChunkListEntry(id, _size, csize)
 
-    def chunk_decref(self, id, stats, wait=True):
+    def chunk_decref(self, id, stats, wait=True, part=False):
         if not self.txn_active:
             self.begin_txn()
         count, size, csize = self.chunks.decref(id)
         if count == 0:
             del self.chunks[id]
             self.repository.delete(id, wait=wait)
-            stats.update(-size, -csize, True)
+            stats.update(-size, -csize, True, part=part)
         else:
-            stats.update(-size, -csize, False)
+            stats.update(-size, -csize, False, part=part)
 
     def file_known_and_unchanged(self, path_hash, st):
         """
@@ -1057,16 +1057,16 @@ def chunk_incref(self, id, stats, size=None, part=False):
         stats.update(size, csize, False, part=part)
         return ChunkListEntry(id, size, csize)
 
-    def chunk_decref(self, id, stats, wait=True):
+    def chunk_decref(self, id, stats, wait=True, part=False):
         if not self._txn_active:
             self.begin_txn()
         count, size, csize = self.chunks.decref(id)
         if count == 0:
             del self.chunks[id]
             self.repository.delete(id, wait=wait)
-            stats.update(-size, -csize, True)
+            stats.update(-size, -csize, True, part=part)
         else:
-            stats.update(-size, -csize, False)
+            stats.update(-size, -csize, False, part=part)
 
     def commit(self):
         if not self._txn_active:


Test patch: