Skip to content
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

Provide atomicity of state dir copying #7055

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 44 additions & 5 deletions src/tribler/core/upgrade/version_manager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import json
import logging
import os.path
import shutil
import time
Expand Down Expand Up @@ -66,6 +67,9 @@ class VersionError(Exception):
pass


logger = logging.getLogger(__name__)


# pylint: disable=too-many-instance-attributes
class TriblerVersion:
version_str: str
Expand All @@ -90,6 +94,7 @@ def __init__(self, root_state_dir: Path, version_str: str, last_launched_at: Opt
self.last_launched_at = last_launched_at
self.root_state_dir = root_state_dir
self.directory = self.get_directory()
self.tmp_copy_directory = self.get_tmp_copy_directory()
self.prev_version_by_time = None
self.prev_version_by_number = None
self.can_be_copied_from = None
Expand All @@ -103,6 +108,9 @@ def __repr__(self):
def get_directory(self):
return self.root_state_dir / ('%d.%d' % self.major_minor)

def get_tmp_copy_directory(self):
return self.root_state_dir / ('%d.%d_tmp_copy' % self.major_minor)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Maybe it is a bit safer to generate some random postfix for this temporary directory

Suggested change
return self.root_state_dir / ('%d.%d_tmp_copy' % self.major_minor)
major = self.major_minor[0]
minor = self.major_minor[1]
temporary_dir_name = f'{major}.{minor}_tmp_copy_{random.randrange(100)}'
return self.root_state_dir / temporary_dir_name


def state_exists(self):
# For ancient versions that use root directory for state storage
# we additionally check the existence of the `triblerd.conf` file
Expand Down Expand Up @@ -131,6 +139,8 @@ def delete_state(self) -> Optional[Path]:
if self.deleted:
return None

logger.info(f"Delete state directory for version {self.version_str}")

self.deleted = True
for filename in STATE_FILES_TO_COPY:
try:
Expand All @@ -150,29 +160,42 @@ def delete_state(self) -> Optional[Path]:
return None

def copy_state_from(self, other: TriblerVersion, overwrite=False):

logger.info(f"Copy state directory from version {other.version_str} to version {self.version_str}")

if self.directory.exists():
if not overwrite:
raise VersionError(f'Directory for version {self.version_str} already exists')
self.delete_state()

self.directory.mkdir()
if self.tmp_copy_directory.exists():
logger.info("Remove the previous unfinished temporary copy of the state directory")
shutil.rmtree(self.tmp_copy_directory)

self.tmp_copy_directory.mkdir()

for dirname in STATE_DIRS_TO_COPY:
src = other.directory / dirname
if src.exists():
dst = self.directory / dirname
dst = self.tmp_copy_directory / dirname
shutil.copytree(src, dst)

for filename in STATE_FILES_TO_COPY:
src = other.directory / filename
if src.exists():
dst = self.directory / filename
dst = self.tmp_copy_directory / filename
shutil.copy(src, dst)

self.tmp_copy_directory.rename(self.directory)
logger.info(f"State directory is copied from version {other.version_str} to version {self.version_str}")

def rename_directory(self, prefix='unused_v'):
if self.directory == self.root_state_dir:
raise VersionError('Cannot rename root directory')
timestamp_str = datetime.now().strftime("%Y-%m-%d_%Hh%Mm%Ss")
dirname = prefix + '%d.%d' % self.major_minor + '_' + timestamp_str

logger.info(f"Rename state directory for version {self.version_str} to {dirname}")
return self.directory.rename(self.root_state_dir / dirname)


Expand Down Expand Up @@ -215,13 +238,15 @@ def __init__(self, root_state_dir: Path, code_version_id: Optional[str] = None):

if not last_run_version:
# No previous versions found
pass
logger.info(f"No previous version found, current Tribler version is {code_version.version_str}")
elif last_run_version.version_str == code_version.version_str:
# Previously we started the same version, nothing to upgrade
code_version = last_run_version
logger.info(f"The previously started version is the same as the current one: {code_version.version_str}")
elif last_run_version.major_minor == code_version.major_minor:
# Previously we started version from the same directory and can continue use this directory
pass
logger.info(f"The previous version {last_run_version.version_str} "
f"used the same state directory as the current version {code_version.version_str}")
else:
# Previously we started version from the different directory
for v in versions_by_time:
Expand All @@ -231,14 +256,26 @@ def __init__(self, root_state_dir: Path, code_version_id: Optional[str] = None):

if code_version.can_be_copied_from:
if not code_version.directory.exists():
logger.info(f"The state directory for the current version {code_version.version_str} "
f"does not exists and can be copied from {code_version.can_be_copied_from.version_str}")
code_version.should_be_copied = True

elif code_version.major_minor in versions:
# We already used version with this major.minor number, but not the last time.
# We need to upgrade from the latest version if possible (see description at the top of the file).
# Probably we should ask user, should we copy data again from the previous version or not
logger.info(f"The state directory for the current version {code_version.version_str} "
f"exists but is not the last used version "
f"and can be copied from {code_version.can_be_copied_from.version_str}")
code_version.should_be_copied = True
code_version.should_recreate_directory = True
else:
logger.info(f"The state directory for the current version {code_version.version_str} "
f"is present, but the version does not listed in version history. Will not copy state "
f"from a previous version {code_version.can_be_copied_from.version_str}")

else:
logger.info("Cannot find the previous suitable version to copy state directory")

self.versions_by_number = sorted(versions.values(), key=attrgetter('major_minor'))
self.versions_by_time = versions_by_time
Expand Down Expand Up @@ -271,6 +308,7 @@ def save_if_necessary(self) -> bool:
"""Returns True if state was saved"""
should_save = self.code_version != self.last_run_version
if should_save:
logger.info("Save version history")
self.save()
return should_save

Expand Down Expand Up @@ -333,5 +371,6 @@ def get_disposable_state_directories(

def remove_state_dirs(root_state_dir: str, state_dirs: List[str]):
for state_dir in state_dirs:
logger.info(f"Remove state directory {state_dir}")
state_dir = os.path.join(root_state_dir, state_dir)
shutil.rmtree(state_dir, ignore_errors=True)