[Notes] [Git][BuildStream/buildstream][bschubert/fix-atomic-move-git-repo] 5 commits: using_config.rst: Add documentation to showing how to impose quotas on the local cache



Title: GitLab

Benjamin Schubert pushed to branch bschubert/fix-atomic-move-git-repo at BuildStream / buildstream

Commits:

7 changed files:

Changes:

  • buildstream/_artifactcache/artifactcache.py
    ... ... @@ -937,15 +937,22 @@ class ArtifactCache():
    937 937
                                 "Invalid cache quota ({}): ".format(utils._pretty_size(cache_quota)) +
    
    938 938
                                 "BuildStream requires a minimum cache quota of 2G.")
    
    939 939
             elif cache_quota > cache_size + available_space:  # Check maximum
    
    940
    +            if '%' in self.context.config_cache_quota:
    
    941
    +                available = (available_space / (stat.f_blocks * stat.f_bsize)) * 100
    
    942
    +                available = '{}% of total disk space'.format(round(available, 1))
    
    943
    +            else:
    
    944
    +                available = utils._pretty_size(available_space)
    
    945
    +
    
    940 946
                 raise LoadError(LoadErrorReason.INVALID_DATA,
    
    941 947
                                 ("Your system does not have enough available " +
    
    942 948
                                  "space to support the cache quota specified.\n" +
    
    943
    -                             "You currently have:\n" +
    
    944
    -                             "- {used} of cache in use at {local_cache_path}\n" +
    
    945
    -                             "- {available} of available system storage").format(
    
    946
    -                                 used=utils._pretty_size(cache_size),
    
    947
    -                                 local_cache_path=self.context.artifactdir,
    
    948
    -                                 available=utils._pretty_size(available_space)))
    
    949
    +                             "\nYou have specified a quota of {quota} total disk space.\n" +
    
    950
    +                             "- The filesystem containing {local_cache_path} only " +
    
    951
    +                             "has: {available_size} available.")
    
    952
    +                            .format(
    
    953
    +                                quota=self.context.config_cache_quota,
    
    954
    +                                local_cache_path=self.context.artifactdir,
    
    955
    +                                available_size=available))
    
    949 956
     
    
    950 957
             # Place a slight headroom (2e9 (2GB) on the cache_quota) into
    
    951 958
             # cache_quota to try and avoid exceptions.
    

  • buildstream/plugins/sources/git.py
    ... ... @@ -97,6 +97,7 @@ from configparser import RawConfigParser
    97 97
     from buildstream import Source, SourceError, Consistency, SourceFetcher
    
    98 98
     from buildstream import utils
    
    99 99
     from buildstream.plugin import CoreWarnings
    
    100
    +from buildstream.utils import move_atomic, DirectoryExistsError
    
    100 101
     
    
    101 102
     GIT_MODULES = '.gitmodules'
    
    102 103
     
    
    ... ... @@ -141,21 +142,16 @@ class GitMirror(SourceFetcher):
    141 142
                                      fail="Failed to clone git repository {}".format(url),
    
    142 143
                                      fail_temporarily=True)
    
    143 144
     
    
    144
    -                # Attempt atomic rename into destination, this will fail if
    
    145
    -                # another process beat us to the punch
    
    146 145
                     try:
    
    147
    -                    os.rename(tmpdir, self.mirror)
    
    148
    -                except OSError as e:
    
    149
    -
    
    150
    -                    # When renaming and the destination repo already exists, os.rename()
    
    151
    -                    # will fail with ENOTEMPTY, since an empty directory will be silently
    
    152
    -                    # replaced
    
    153
    -                    if e.errno == errno.ENOTEMPTY:
    
    154
    -                        self.source.status("{}: Discarding duplicate clone of {}"
    
    155
    -                                           .format(self.source, url))
    
    156
    -                    else:
    
    157
    -                        raise SourceError("{}: Failed to move cloned git repository {} from '{}' to '{}': {}"
    
    158
    -                                          .format(self.source, url, tmpdir, self.mirror, e)) from e
    
    146
    +                    move_atomic(tmpdir, self.mirror)
    
    147
    +                except DirectoryExistsError:
    
    148
    +                    # Another process was quicker to download this repository.
    
    149
    +                    # Let's discard our own
    
    150
    +                    self.source.status("{}: Discarding duplicate clone of {}"
    
    151
    +                                    .format(self.source, url))
    
    152
    +                except OSError:
    
    153
    +                    raise SourceError("{}: Failed to move cloned git repository {} from '{}' to '{}': {}"
    
    154
    +                                      .format(self.source, url, tmpdir, self.mirror, e)) from e
    
    159 155
     
    
    160 156
         def _fetch(self, alias_override=None):
    
    161 157
             url = self.source.translate_url(self.url,
    

  • buildstream/utils.py
    ... ... @@ -72,6 +72,11 @@ class ProgramNotFoundError(BstError):
    72 72
             super().__init__(message, domain=ErrorDomain.PROG_NOT_FOUND, reason=reason)
    
    73 73
     
    
    74 74
     
    
    75
    +class DirectoryExistsError(OSError):
    
    76
    +    """Raised when a `os.rename` is attempted but the destination is an existing directory.
    
    77
    +    """
    
    78
    +
    
    79
    +
    
    75 80
     class FileListResult():
    
    76 81
         """An object which stores the result of one of the operations
    
    77 82
         which run on a list of files.
    
    ... ... @@ -500,6 +505,32 @@ def get_bst_version():
    500 505
                             .format(__version__))
    
    501 506
     
    
    502 507
     
    
    508
    +def move_atomic(source, destination, ensure_parents=True):
    
    509
    +    """Move the source to the destination using atomic primitives.
    
    510
    +
    
    511
    +    This uses `os.rename` to move a file or directory to a new destination.
    
    512
    +    It wraps some `OSError` thrown errors to ensure their handling is correct.
    
    513
    +
    
    514
    +    The main reason for this to exist is that rename can throw different errors
    
    515
    +    for the same symptom (https://www.unix.com/man-page/POSIX/3posix/rename/).
    
    516
    +
    
    517
    +    We are especially interested here in the case when the destination already
    
    518
    +    exists. In this case, either EEXIST or ENOTEMPTY are thrown.
    
    519
    +
    
    520
    +    In order to ensure consistent handling of these exceptions, this function
    
    521
    +    should be used instead of `os.rename`
    
    522
    +    """
    
    523
    +    if ensure_parents:
    
    524
    +        os.makedirs(os.path.dirname(destination), exist_ok=True)
    
    525
    +
    
    526
    +    try:
    
    527
    +        os.rename(source, destination)
    
    528
    +    except OSError as exc:
    
    529
    +        if exc.errno in (errno.EEXIST, errno.ENOTEMPTY):
    
    530
    +            raise DirectoryExistsError(*exc.args) from exc
    
    531
    +        raise
    
    532
    +
    
    533
    +
    
    503 534
     @contextmanager
    
    504 535
     def save_file_atomic(filename, mode='w', *, buffering=-1, encoding=None,
    
    505 536
                          errors=None, newline=None, closefd=True, opener=None, tempdir=None):
    

  • doc/source/using_config.rst
    ... ... @@ -147,6 +147,44 @@ The default mirror is defined by its name, e.g.
    147 147
        ``--default-mirror`` command-line option.
    
    148 148
     
    
    149 149
     
    
    150
    +Local cache expiry
    
    151
    +~~~~~~~~~~~~~~~~~~
    
    152
    +BuildStream locally caches artifacts, build trees, log files and sources within a
    
    153
    +cache located at ``~/.cache/buildstream`` (unless a $XDG_CACHE_HOME environment
    
    154
    +variable exists). When building large projects, this cache can get very large,
    
    155
    +thus BuildStream will attempt to clean up the cache automatically by expiring the least
    
    156
    +recently *used* artifacts.
    
    157
    +
    
    158
    +By default, cache expiry will begin once the file system which contains the cache
    
    159
    +approaches maximum usage. However, it is also possible to impose a quota on the local
    
    160
    +cache in the user configuration. This can be done in two ways:
    
    161
    +
    
    162
    +1. By restricting the maximum size of the cache directory itself.
    
    163
    +
    
    164
    +For example, to ensure that BuildStream's cache does not grow beyond 100 GB,
    
    165
    +simply declare the following in your user configuration (``~/.config/buildstream.conf``):
    
    166
    +
    
    167
    +.. code:: yaml
    
    168
    +
    
    169
    +  cache:
    
    170
    +    quota: 100G
    
    171
    +
    
    172
    +This quota defines the maximum size of the artifact cache in bytes.
    
    173
    +Other accepted values are: K, M, G or T (or you can simply declare the value in bytes, without the suffix).
    
    174
    +This uses the same format as systemd's
    
    175
    +`resource-control <https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html>`_.
    
    176
    +
    
    177
    +2. By expiring artifacts once the file system which contains the cache exceeds a specified usage.
    
    178
    +
    
    179
    +To ensure that we start cleaning the cache once we've used 80% of local disk space (on the file system
    
    180
    +which mounts the cache):
    
    181
    +
    
    182
    +.. code:: yaml
    
    183
    +
    
    184
    +  cache:
    
    185
    +    quota: 80%
    
    186
    +
    
    187
    +
    
    150 188
     Default configuration
    
    151 189
     ---------------------
    
    152 190
     The default BuildStream configuration is specified here for reference:
    

  • tests/frontend/buildtrack.py
    ... ... @@ -115,6 +115,7 @@ def test_build_track(cli, datafiles, tmpdir, ref_storage,
    115 115
         args += ['0.bst']
    
    116 116
     
    
    117 117
         result = cli.run(project=project, silent=True, args=args)
    
    118
    +    result.assert_success()
    
    118 119
         tracked_elements = result.get_tracked_elements()
    
    119 120
     
    
    120 121
         assert set(tracked_elements) == set(tracked)
    

  • tests/utils/misc.py
    ... ... @@ -27,4 +27,5 @@ def test_parse_size_over_1024T(cli, tmpdir):
    27 27
         patched_statvfs = mock_os.mock_statvfs(f_bavail=bavail, f_bsize=BLOCK_SIZE)
    
    28 28
         with mock_os.monkey_patch("statvfs", patched_statvfs):
    
    29 29
             result = cli.run(project, args=["build", "file.bst"])
    
    30
    -        assert "1025T of available system storage" in result.stderr
    30
    +        failure_msg = 'Your system does not have enough available space to support the cache quota specified.'
    
    31
    +        assert failure_msg in result.stderr

  • tests/utils/movedirectory.py
    1
    +import pytest
    
    2
    +
    
    3
    +from buildstream.utils import move_atomic, DirectoryExistsError
    
    4
    +
    
    5
    +
    
    6
    +@pytest.fixture
    
    7
    +def src(tmp_path):
    
    8
    +    src = tmp_path.joinpath("src")
    
    9
    +    src.mkdir()
    
    10
    +
    
    11
    +    with src.joinpath("test").open("w") as fp:
    
    12
    +        fp.write("test")
    
    13
    +
    
    14
    +    return src
    
    15
    +
    
    16
    +
    
    17
    +def test_move_to_empty_dir(src, tmp_path):
    
    18
    +    dst = tmp_path.joinpath("dst")
    
    19
    +
    
    20
    +    move_atomic(src, dst)
    
    21
    +
    
    22
    +    assert dst.joinpath("test").exists()
    
    23
    +
    
    24
    +
    
    25
    +def test_move_to_empty_dir_create_parents(src, tmp_path):
    
    26
    +    dst = tmp_path.joinpath("nested/dst")
    
    27
    +
    
    28
    +    move_atomic(src, dst)
    
    29
    +    assert dst.joinpath("test").exists()
    
    30
    +
    
    31
    +
    
    32
    +def test_move_to_empty_dir_no_create_parents(src, tmp_path):
    
    33
    +    dst = tmp_path.joinpath("nested/dst")
    
    34
    +
    
    35
    +    with pytest.raises(FileNotFoundError):
    
    36
    +        move_atomic(src, dst, ensure_parents=False)
    
    37
    +
    
    38
    +
    
    39
    +def test_move_non_existing_dir(tmp_path):
    
    40
    +    dst = tmp_path.joinpath("dst")
    
    41
    +    src = tmp_path.joinpath("src")
    
    42
    +
    
    43
    +    with pytest.raises(FileNotFoundError):
    
    44
    +        move_atomic(src, dst)
    
    45
    +
    
    46
    +
    
    47
    +def test_move_to_existing_empty_dir(src, tmp_path):
    
    48
    +    dst = tmp_path.joinpath("dst")
    
    49
    +    dst.mkdir()
    
    50
    +
    
    51
    +    move_atomic(src, dst)
    
    52
    +    assert dst.joinpath("test").exists()
    
    53
    +
    
    54
    +
    
    55
    +def test_move_to_existing_file(src, tmp_path):
    
    56
    +    dst = tmp_path.joinpath("dst")
    
    57
    +
    
    58
    +    with dst.open("w") as fp:
    
    59
    +        fp.write("error")
    
    60
    +
    
    61
    +    with pytest.raises(NotADirectoryError):
    
    62
    +        move_atomic(src, dst)
    
    63
    +
    
    64
    +
    
    65
    +def test_move_file_to_existing_file(tmp_path):
    
    66
    +    dst = tmp_path.joinpath("dst")
    
    67
    +    src = tmp_path.joinpath("src")
    
    68
    +
    
    69
    +    with src.open("w") as fp:
    
    70
    +        fp.write("src")
    
    71
    +
    
    72
    +    with dst.open("w") as fp:
    
    73
    +        fp.write("dst")
    
    74
    +
    
    75
    +    move_atomic(src, dst)
    
    76
    +    with dst.open() as fp:
    
    77
    +        assert fp.read() == "src"
    
    78
    +
    
    79
    +
    
    80
    +def test_move_to_existing_non_empty_dir(src, tmp_path):
    
    81
    +    dst = tmp_path.joinpath("dst")
    
    82
    +    dst.mkdir()
    
    83
    +
    
    84
    +    with dst.joinpath("existing").open("w") as fp:
    
    85
    +        fp.write("already there")
    
    86
    +
    
    87
    +    with pytest.raises(DirectoryExistsError):
    
    88
    +        move_atomic(src, dst)



  • [Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]