Skip to content

Commit d6a38a8

Browse files
archive_progress: the final object has the final statistics
The progress output is rate limited and the final archive_progress object (finished: true) had no statistics, so nothing reported what was processed after the previous object, or anything at all for a short operation: a transfer of 10000 files ended at "nfiles": 5653, a recreate of 500 files at 0. create and import-tar have the final --json output for that, recreate and transfer have nothing else. The final object had lost its statistics because they were all 0 for create, see #6570: create and import-tar reported the statistics of the archive before adding the ones of the object processors to them. They add them first now. The cockpit uses the statistics of the final object. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 2488911 commit d6a38a8

13 files changed

Lines changed: 65 additions & 31 deletions

File tree

‎docs/changes.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,9 @@ Fixes:
224224
documented), not text progress lines
225225
- import-tar/recreate: count the items by their status, like create does. The counts
226226
were always 0: "Added files" of import-tar --stats, files_stats in the JSON output.
227+
- --log-json: the final archive_progress object (finished: true) has the final statistics.
228+
The progress output is rate limited, so a frontend could not know what was processed
229+
after the previous object (or at all, for a short operation), see also #6570.
227230

228231
Other changes:
229232

‎docs/internals/frontends.rst‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ it is not produced unless ``--progress`` is specified.
9090
archive_progress
9191
Output during operations creating archives (:ref:`borg_create`, :ref:`borg_import-tar`,
9292
:ref:`borg_recreate` and :ref:`borg_transfer`).
93-
The following keys exist, each represents the current progress.
93+
The following keys exist, each represents the current progress. The output is rate limited, so
94+
only the last object (*finished* is *true*) tells about everything that was processed.
9495

9596
original_size
9697
Original size of the data processed so far (before compression and deduplication)
@@ -116,8 +117,8 @@ archive_progress
116117
Unix timestamp (float)
117118
finished
118119
boolean indicating whether the operation has finished, only the last object for an *operation*
119-
can have this property set to *true*. That last object has no keys besides *time*, *type*
120-
and *finished*.
120+
can have this property set to *true*. That last object has the final statistics of the
121+
archive and no *path*.
121122

122123
progress_message
123124
A message-based progress information with no concrete progress information, just a message
@@ -241,7 +242,9 @@ See Prompts_ for the types used by prompts.
241242
{"type": "file_status", "status": "A", "path": "src/linux/file1"}
242243
{"type": "file_status", "status": "d", "path": "src/linux"}
243244
{"type": "file_status", "status": "d", "path": "src"}
244-
{"time": 1787900398.686938, "type": "archive_progress", "finished": true}
245+
{"original_size": 250012, "deduplicated_size": 250012, "nfiles": 3, "hashing_time": 0.002,
246+
"chunking_time": 0.001, "files_stats": {"A": 3, "d": 3}, "store_stats": {}, "time": 1787900398.686938,
247+
"type": "archive_progress", "finished": true}
245248

246249
:ref:`borg_extract` file listing, with ``--exclude src/linux/baz/file3``::
247250

‎src/borg/archive.py‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,11 @@ def show_progress(self, item=None, final=False, stream=None):
208208
stream = stream or sys.stderr
209209
self.last_progress = now
210210
if self.output_json:
211-
if not final:
212-
data = self.as_dict()
213-
if item:
214-
data |= text_to_json("path", item.path)
215-
else:
216-
data = {}
211+
# the progress is rate limited, so the final object must have the final statistics: nothing
212+
# else reports what was processed since the previous object (or all of a short operation).
213+
data = self.as_dict()
214+
if item and not final:
215+
data |= text_to_json("path", item.path)
217216
data |= {"time": time.time(), "type": "archive_progress", "finished": final}
218217
msg = json.dumps(data)
219218
end = "\n"

‎src/borg/archiver/create_cmd.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,9 @@ def create_inner(archive, cache, fso):
291291
self.print_warning_instance(BackupWarning(path, e))
292292
continue
293293
if not dry_run:
294+
archive.stats += fso.stats # before the final progress, it reports the final statistics
294295
if args.progress:
295296
archive.stats.show_progress(final=True)
296-
archive.stats += fso.stats
297297
if sig_int:
298298
# do not save the archive if the user ctrl-c-ed.
299299
raise Error("Got Ctrl-C / SIGINT.")

‎src/borg/archiver/tar_cmds.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,9 +614,9 @@ def path_components(name):
614614
# This does not close the fileobj (tarstream) we passed to it -- a side effect of the | mode.
615615
tar.close()
616616

617+
archive.stats += tfo.stats # before the final progress, it reports the final statistics
617618
if args.progress:
618619
archive.stats.show_progress(final=True)
619-
archive.stats += tfo.stats
620620
archive.save(comment=args.comment, timestamp=args.timestamp)
621621
args.stats |= args.json
622622
if args.stats:

‎src/borg/cockpit/session.py‎

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def __init__(self, command=None, capture_stdout=False):
8484
self.finished_at = None
8585
self.rc = None # exit code of borg, None while it runs
8686
self.error = None # why borg could not be run, if so
87-
self.archive_progress = None # the latest ArchiveProgress carrying statistics
87+
self.archive_progress = None # the latest ArchiveProgress
8888
self.archive_finished = False
8989
# status char -> count of the --list lines. They only tell about the listing (which --filter reduces),
9090
# the statistics of an archive being created never come from here, see nfiles and files_stats.
@@ -147,10 +147,9 @@ def _final_stat(self, key, types=int):
147147
value = stats.get(key) if stats else None
148148
return value if isinstance(value, types) and not isinstance(value, bool) else None
149149

150-
# The statistics come from borg's statistics only: the final ones (exact) are preferred over the latest
151-
# archive_progress. That one is rate limited and its final object carries no statistics, so without final
152-
# statistics the numbers can be a little behind at the end of a run (and stay at zero for a run shorter
153-
# than the update interval).
150+
# The statistics come from borg's statistics only: the ones of the final --json output (they also have
151+
# the store statistics) are preferred over the latest archive_progress. That one is rate limited, so its
152+
# numbers can be a little behind while borg runs; its final object has the final statistics.
154153
# The --list lines are no source for them: they only exist if the user asked for the listing, and --filter
155154
# reduces them to some status characters, so counting them would give wrong numbers.
156155
# What borg does not tell is unknown: there is no archive_progress for a dry-run, and transfer has no
@@ -217,13 +216,9 @@ def feed(self, event):
217216
self.archive_counts[event.status] += 1
218217
self._add_line(Line(event.message, "archive", event.status))
219218
case ArchiveProgress():
220-
if event.finished:
221-
# the final object carries no statistics, keep the previous ones.
222-
self.archive_finished = True
223-
self.progress_text = ""
224-
else:
225-
self.archive_progress = event
226-
self.progress_text = event.path or ""
219+
self.archive_progress = event # the final object has the final statistics (and no path)
220+
self.archive_finished = event.finished
221+
self.progress_text = event.path or ""
227222
case ProgressPercent() | ProgressMessage():
228223
self._feed_phase(event)
229224
case Question():

‎src/borg/testsuite/archive_test.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,10 @@ def test_stats_progress_json(stats):
170170
assert isinstance(result["time"], float)
171171
assert result["finished"] is True # see #6570
172172
assert "path" not in result
173-
assert "original_size" not in result
174-
assert "nfiles" not in result
173+
# the final object has the final statistics
174+
assert result["original_size"] == 20
175+
assert result["deduplicated_size"] == 20
176+
assert result["nfiles"] == 1
175177

176178

177179
def test_stats_as_dict(stats):

‎src/borg/testsuite/archiver/create_cmd_test.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,20 @@ def test_progress_on(archivers, request):
739739
assert "0 B O 0 B U 0 N" in output
740740

741741

742+
def test_progress_json_final_statistics(archivers, request):
743+
archiver = request.getfixturevalue(archivers)
744+
create_regular_file(archiver.input_path, "file1", size=1024 * 80)
745+
create_regular_file(archiver.input_path, "file2", size=1024)
746+
cmd(archiver, "repo-create", RK_ENCRYPTION)
747+
output = cmd(archiver, "create", "test", "input", "--progress", "--log-json")
748+
messages = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
749+
final = [msg for msg in messages if msg["type"] == "archive_progress"][-1]
750+
# the progress is rate limited, so only the final object tells about everything that was processed.
751+
assert final["finished"] and "path" not in final
752+
assert final["nfiles"] == 2 and final["original_size"] == 1024 * 81
753+
assert final["files_stats"] == {"A": 2, "d": 1}
754+
755+
742756
def test_progress_off(archivers, request):
743757
archiver = request.getfixturevalue(archivers)
744758
create_regular_file(archiver.input_path, "file1", size=1024 * 80)

‎src/borg/testsuite/archiver/recreate_cmd_test.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,8 @@ def test_recreate_progress_json(archivers, request):
345345
assert len(progress) >= 2
346346
assert not progress[0]["finished"] and progress[-1]["finished"]
347347
assert {"nfiles", "original_size", "deduplicated_size", "path"} <= set(progress[0])
348+
# the final object has the final statistics: file2 was excluded.
349+
assert progress[-1]["nfiles"] == 1 and progress[-1]["files_stats"] == {"A": 1, "d": 1}
348350
# ... and not of text lines like "1.02 kB O 0 B U 1 N input/file1".
349351
assert not any(re.search(r" O .* U \d+ N ", line) for line in lines if not line.startswith("{"))
350352

‎src/borg/testsuite/archiver/tar_cmds_test.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,12 @@ def test_import_tar_files_stats(archivers, request):
214214
assert stats["files_stats"] == {"A": 2, "h": 1, "d": 1, "s": 1}
215215
output = cmd(archiver, "import-tar", "--stats", "dst2", "input.tar")
216216
assert "Added files: 2" in output
217+
# the final archive_progress object has the final statistics
218+
output = cmd(archiver, "import-tar", "--log-json", "--progress", "dst3", "input.tar")
219+
messages = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
220+
final = [msg for msg in messages if msg["type"] == "archive_progress"][-1]
221+
assert final["finished"] and final["nfiles"] == 3
222+
assert final["files_stats"] == {"A": 2, "h": 1, "d": 1, "s": 1}
217223

218224

219225
def test_import_tar_json(archivers, request):

0 commit comments

Comments
 (0)