diff --git a/setup.py b/setup.py index 7d0526d..8693e81 100644 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ def read_version(): 'pyyaml~=6.0', 'rich>=14', 'requests==2.31.0', # Docker SDK breaks on 2.32.0 + 'filelock==3.29.7', 'repo_library @ git+https://github.com/rdkcentral/sc-repo-library.git@master', 'git_flow_library @ git+https://github.com/rdkcentral/sc-git-flow-library.git@master', 'sc_manifest_parser @ git+https://github.com/rdkcentral/sc-manifest-parser.git@main' diff --git a/src/sc/clone/cloners/repo_cloner.py b/src/sc/clone/cloners/repo_cloner.py index f531212..5f85416 100644 --- a/src/sc/clone/cloners/repo_cloner.py +++ b/src/sc/clone/cloners/repo_cloner.py @@ -18,7 +18,9 @@ import shutil import subprocess import sys +import time +from filelock import SoftFileLock, Timeout from pydantic import BaseModel from .cloner import Cloner, RefType @@ -29,6 +31,8 @@ logger = logging.getLogger(__name__) REPO_CACHE_DIR = Path.home() / ".caches" +CACHE_LOCK_PATH = REPO_CACHE_DIR / ".sc_lock" +CACHE_MAX_WAIT = 300 class RepoClonerConfig(BaseModel): """ @@ -74,8 +78,24 @@ def clone(self, directory: Path): - Parses the manifest to retrieve projects. - Initializes GitFlow for all unlocked projects. """ - reference = self._cache() if self.config.cache else None + if self.config.cache: + self._clone_with_cache(directory) + else: + self._clone(directory) + + def _clone_with_cache(self, directory: Path): + REPO_CACHE_DIR.mkdir(exist_ok=True) + + lock = SoftFileLock(CACHE_LOCK_PATH) + self._acquire_cache_lock(lock) + try: + reference = self._cache() + self._clone(directory, reference) + finally: + lock.release() + + def _clone(self, directory: Path, reference: Path | None = None): self._init_repo(directory=directory, reference=reference) RepoLibrary.sync( directory, @@ -95,7 +115,6 @@ def _cache(self) -> Path: Returns: Path: The directory of the mirrored cache. """ - REPO_CACHE_DIR.mkdir(exist_ok=True) manifest_hostname = self._get_manifest_hostname(self.config.uri) host_cache_dir = Path(REPO_CACHE_DIR / manifest_hostname) host_cache_dir.mkdir(exist_ok=True) @@ -139,6 +158,40 @@ def _init_repo(self, directory: Path, mirror: bool = False, reference: Path | No logger.error(f"repo init error: {e}") sys.exit(1) + def _acquire_cache_lock(self, lock: SoftFileLock): + """Try to acquire cache lock. After a certain time bypass it.""" + started = time.monotonic() + first_warning = False + + logger.info("Acquiring cache lock.") + while True: + try: + lock.acquire(timeout=20) + logger.info("Cache lock acquired.") + return + + except Timeout: + waited = int(time.monotonic() - started) + if waited < CACHE_MAX_WAIT: + logger.info( + f"Cache is in use by another process, waited {waited} seconds..." + ) + + if first_warning == False: + logger.info(f"SC will wait for {CACHE_MAX_WAIT} seconds before bypassing.") + logger.info( + "The cache is user specific, if you believe there is no way your cache " + f"should be in use you could try deleting the lock: {CACHE_LOCK_PATH}") + first_warning = True + else: + logger.warning( + f"Cache remained locked past the set wait time of {CACHE_MAX_WAIT} seconds. " + "Force proceeding without acquiring cache lock..." + ) + lock.break_lock() + lock.acquire() + return + def _get_manifest_hostname(self, url: str) -> str: """Extracts the hostname from a given URL.