Repository: borgbackup/borg

Issue:
Incorrect duration if clock jumps backwards during borg-create
Two bugs actually:

1. The duration becomes negative.
2. Negative deltas aren't correctly calculated, either.

Eg.: Starting around 00:52:00 (on the 6th), then crossing over to 23:57:00 (on the 5th) displays something along the lines of "-1 days 5 minutes xy seconds".

(Borg isn't the only one getting it wrong, though. GNU time says: "243user 18system -55:-28.37elapsed" -- note how the absolute time span is still wrong by about 50 minutes!).

IIRC we already store the duration in seconds in the archive metadata, so we'd only have to use a different clock to calculate that (I'd leave the timestamps as-is, no way knowing which one would be correct!), ie. a monotonic one that's good for at least a couple days (large creates and all).


Discussion:

Resolving patch:
diff --git a/borg/archive.py b/borg/archive.py
--- a/borg/archive.py
+++ b/borg/archive.py
@@ -1,5 +1,5 @@
 from contextlib import contextmanager
-from datetime import datetime, timezone
+from datetime import datetime, timezone, timedelta
 from getpass import getuser
 from itertools import groupby
 import errno
@@ -186,7 +186,7 @@ class IncompatibleFilesystemEncodingError(Error):
 
     def __init__(self, repository, key, manifest, name, cache=None, create=False,
                  checkpoint_interval=300, numeric_owner=False, noatime=False, noctime=False, progress=False,
-                 chunker_params=CHUNKER_PARAMS, start=None, end=None):
+                 chunker_params=CHUNKER_PARAMS, start=None, start_monotonic=None, end=None):
         self.cwd = os.getcwd()
         self.key = key
         self.repository = repository
@@ -200,9 +200,12 @@ def __init__(self, repository, key, manifest, name, cache=None, create=False,
         self.numeric_owner = numeric_owner
         self.noatime = noatime
         self.noctime = noctime
+        assert (start is None) == (start_monotonic is None), 'Logic error: if start is given, start_monotonic must be given as well and vice versa.'
         if start is None:
             start = datetime.utcnow()
+            start_monotonic = time.monotonic()
         self.start = start
+        self.start_monotonic = start_monotonic
         if end is None:
             end = datetime.utcnow()
         self.end = end
@@ -212,7 +215,7 @@ def __init__(self, repository, key, manifest, name, cache=None, create=False,
             self.chunker = Chunker(self.key.chunk_seed, *chunker_params)
             if name in manifest.archives:
                 raise self.AlreadyExists(name)
-            self.last_checkpoint = time.time()
+            self.last_checkpoint = time.monotonic()
             i = 0
             while True:
                 self.checkpoint_name = '%s.checkpoint%s' % (name, i and ('.%d' % i) or '')
@@ -287,9 +290,9 @@ def add_item(self, item):
         if self.show_progress:
             self.stats.show_progress(item=item, dt=0.2)
         self.items_buffer.add(item)
-        if time.time() - self.last_checkpoint > self.checkpoint_interval:
+        if time.monotonic() - self.last_checkpoint > self.checkpoint_interval:
             self.write_checkpoint()
-            self.last_checkpoint = time.time()
+            self.last_checkpoint = time.monotonic()
 
     def write_checkpoint(self):
         self.save(self.checkpoint_name)
@@ -301,14 +304,17 @@ def save(self, name=None, timestamp=None):
         if name in self.manifest.archives:
             raise self.AlreadyExists(name)
         self.items_buffer.flush(flush=True)
+        duration = timedelta(seconds=time.monotonic() - self.start_monotonic)
         if timestamp is None:
             self.end = datetime.utcnow()
+            self.start = self.end - duration
             start = self.start
             end = self.end
         else:
             self.end = timestamp
-            start = timestamp
-            end = timestamp  # we only have 1 value
+            self.start = timestamp - duration
+            end = timestamp
+            start = self.start
         metadata = StableDict({
             'version': 1,
             'name': name,
diff --git a/borg/archiver.py b/borg/archiver.py
--- a/borg/archiver.py
+++ b/borg/archiver.py
@@ -5,7 +5,6 @@
 import argparse
 import functools
 import inspect
-import io
 import os
 import re
 import shlex
@@ -13,6 +12,7 @@
 import stat
 import sys
 import textwrap
+import time
 import traceback
 import collections
 
@@ -263,7 +263,6 @@ def create_inner(archive, cache):
                 if args.progress:
                     archive.stats.show_progress(final=True)
                 if args.stats:
-                    archive.end = datetime.utcnow()
                     log_multi(DASHES,
                               str(archive),
                               DASHES,
@@ -276,6 +275,7 @@ def create_inner(archive, cache):
         self.ignore_inode = args.ignore_inode
         dry_run = args.dry_run
         t0 = datetime.utcnow()
+        t0_monotonic = time.monotonic()
         if not dry_run:
             key.compressor = Compressor(**args.compression)
             with Cache(repository, key, manifest, do_files=args.cache_files, lock_wait=self.lock_wait) as cache:
@@ -283,7 +283,7 @@ def create_inner(archive, cache):
                                   create=True, checkpoint_interval=args.checkpoint_interval,
                                   numeric_owner=args.numeric_owner, noatime=args.noatime, noctime=args.noctime,
                                   progress=args.progress,
-                                  chunker_params=args.chunker_params, start=t0)
+                                  chunker_params=args.chunker_params, start=t0, start_monotonic=t0_monotonic)
                 create_inner(archive, cache)
         else:
             create_inner(None, None)
diff --git a/borg/helpers.py b/borg/helpers.py
--- a/borg/helpers.py
+++ b/borg/helpers.py
@@ -219,7 +219,7 @@ def csize_fmt(self):
         return format_file_size(self.csize)
 
     def show_progress(self, item=None, final=False, stream=None, dt=None):
-        now = time.time()
+        now = time.monotonic()
         if dt is None or now - self.last_progress > dt:
             self.last_progress = now
             columns, lines = get_terminal_size()


Test patch: