mirror of
https://github.com/borgbackup/borg.git
synced 2025-02-20 21:27:32 +00:00
repository: truncate segments before unlinking
This commit is contained in:
parent
68b61acffd
commit
95064cd241
1 changed files with 42 additions and 2 deletions
|
@ -83,6 +83,30 @@ class Repository:
|
|||
dir/data/<X // SEGMENTS_PER_DIR>/<X>
|
||||
dir/index.X
|
||||
dir/hints.X
|
||||
|
||||
File system interaction
|
||||
-----------------------
|
||||
|
||||
LoggedIO generally tries to rely on common behaviours across transactional file systems.
|
||||
|
||||
Segments that are deleted are truncated first, which avoids problems if the FS needs to
|
||||
allocate space to delete the dirent of the segment. This mostly affects CoW file systems,
|
||||
traditional journaling file systems have a fairly good grip on this problem.
|
||||
|
||||
Note that deletion, i.e. unlink(2), is atomic on every file system that uses inode reference
|
||||
counts, which includes pretty much all of them. To remove a dirent the inodes refcount has
|
||||
to be decreased, but you can't decrease the refcount before removing the dirent nor can you
|
||||
decrease the refcount after removing the dirent. File systems solve this with a lock,
|
||||
and by ensuring it all stays within the same FS transaction.
|
||||
|
||||
Truncation is generally not atomic in itself, and combining truncate(2) and unlink(2) is of
|
||||
course never guaranteed to be atomic. Truncation in a classic extent-based FS is done in
|
||||
roughly two phases, first the extents are removed then the inode is updated. (In practice
|
||||
this is of course way more complex).
|
||||
|
||||
LoggedIO gracefully handles truncate/unlink splits as long as the truncate resulted in
|
||||
a zero length file. Zero length segments are considered to not exist, while LoggedIO.cleanup()
|
||||
will still get rid of them.
|
||||
"""
|
||||
|
||||
class DoesNotExist(Error):
|
||||
|
@ -1111,6 +1135,8 @@ def segment_iterator(self, segment=None, reverse=False):
|
|||
filenames = [filename for filename in filenames if filename.isdigit() and int(filename) <= segment]
|
||||
filenames = sorted(filenames, key=int, reverse=reverse)
|
||||
for filename in filenames:
|
||||
# Note: Do not filter out logically deleted segments (see "File system interaction" above),
|
||||
# since this is used by cleanup and txn state detection as well.
|
||||
yield int(filename), os.path.join(data_path, dir, filename)
|
||||
|
||||
def get_latest_segment(self):
|
||||
|
@ -1132,6 +1158,9 @@ def cleanup(self, transaction_id):
|
|||
self.segment = transaction_id + 1
|
||||
for segment, filename in self.segment_iterator(reverse=True):
|
||||
if segment > transaction_id:
|
||||
# Truncate segment files before unlink(). This can help a full file system recover.
|
||||
# We can use 'wb', since the segment must exist (just returned by the segment_iterator).
|
||||
open(filename, 'wb').close()
|
||||
os.unlink(filename)
|
||||
else:
|
||||
break
|
||||
|
@ -1207,12 +1236,22 @@ def delete_segment(self, segment):
|
|||
if segment in self.fds:
|
||||
del self.fds[segment]
|
||||
try:
|
||||
os.unlink(self.segment_filename(segment))
|
||||
filename = self.segment_filename(segment)
|
||||
# Truncate segment files before unlink(). This can help a full file system recover.
|
||||
# In this instance (cf. cleanup()) we need to use r+b (=O_RDWR|O_BINARY) and
|
||||
# issue an explicit truncate() to avoid creating a file
|
||||
# if *segment* did not exist in the first place.
|
||||
with open(filename, 'r+b') as fd:
|
||||
fd.truncate()
|
||||
os.unlink(filename)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def segment_exists(self, segment):
|
||||
return os.path.exists(self.segment_filename(segment))
|
||||
filename = self.segment_filename(segment)
|
||||
# When deleting segments, they are first truncated. If truncate(2) and unlink(2) are split
|
||||
# across FS transactions, then logically deleted segments will show up as truncated.
|
||||
return os.path.exists(filename) and os.path.getsize(filename)
|
||||
|
||||
def segment_size(self, segment):
|
||||
return os.path.getsize(self.segment_filename(segment))
|
||||
|
@ -1258,6 +1297,7 @@ def recover_segment(self, segment, filename):
|
|||
if segment in self.fds:
|
||||
del self.fds[segment]
|
||||
with open(filename, 'rb') as fd:
|
||||
# XXX: Rather use mmap.
|
||||
data = memoryview(fd.read())
|
||||
os.rename(filename, filename + '.beforerecover')
|
||||
logger.info('attempting to recover ' + filename)
|
||||
|
|
Loading…
Reference in a new issue