Gentoo Archives: gentoo-commits

From: Zac Medico <zmedico@g.o>
To: gentoo-commits@l.g.o
Subject: [gentoo-commits] proj/portage:master commit in: lib/portage/tests/sync/, man/, lib/portage/repository/storage/, ...
Date: Mon, 24 Sep 2018 06:12:17
Message-Id: 1537768372.36f50e3b64756179758a8e3a11a3c6c666550cf5.zmedico@gentoo
1 commit: 36f50e3b64756179758a8e3a11a3c6c666550cf5
2 Author: Zac Medico <zmedico <AT> gentoo <DOT> org>
3 AuthorDate: Tue Jul 31 07:28:45 2018 +0000
4 Commit: Zac Medico <zmedico <AT> gentoo <DOT> org>
5 CommitDate: Mon Sep 24 05:52:52 2018 +0000
6 URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=36f50e3b
7
8 Add sync-rcu support for rsync (bug 662070)
9
10 Add a boolean sync-rcu repos.conf setting that behaves as follows:
11
12 Enable read-copy-update (RCU) behavior for sync operations. The
13 current latest immutable version of a repository will be referenced
14 by a symlink found where the repository would normally be located
15 (see the location setting). Repository consumers should resolve
16 the cannonical path of this symlink before attempt to access
17 the repository, and all operations should be read-only, since
18 the repository is considered immutable. Updates occur by atomic
19 replacement of the symlink, which causes new consumers to use the
20 new immutable version, while any earlier consumers continue to
21 use the cannonical path that was resolved earlier. This option
22 requires sync-allow-hardlinks and sync-rcu-store-dir options to
23 be enabled, and currently also requires that sync-type is set
24 to rsync. This option is disabled by default, since the symlink
25 usage would require special handling for scenarios involving bind
26 mounts and chroots.
27
28 Bug: https://bugs.gentoo.org/662070
29 Reviewed-by: Brian Dolbec <dolsen <AT> gentoo.org>
30 Signed-off-by: Zac Medico <zmedico <AT> gentoo.org>
31
32 lib/portage/repository/config.py | 36 +++-
33 lib/portage/repository/storage/hardlink_rcu.py | 251 +++++++++++++++++++++++++
34 lib/portage/sync/syncbase.py | 4 +-
35 lib/portage/tests/sync/test_sync_local.py | 40 +++-
36 man/portage.5 | 35 ++++
37 5 files changed, 360 insertions(+), 6 deletions(-)
38
39 diff --git a/lib/portage/repository/config.py b/lib/portage/repository/config.py
40 index f790f9392..8cdc2a696 100644
41 --- a/lib/portage/repository/config.py
42 +++ b/lib/portage/repository/config.py
43 @@ -84,7 +84,7 @@ class RepoConfig(object):
44 'profile_formats', 'sign_commit', 'sign_manifest', 'strict_misc_digests',
45 'sync_depth', 'sync_hooks_only_on_change',
46 'sync_type', 'sync_umask', 'sync_uri', 'sync_user', 'thin_manifest',
47 - 'update_changelog', '_eapis_banned', '_eapis_deprecated',
48 + 'update_changelog', 'user_location', '_eapis_banned', '_eapis_deprecated',
49 '_masters_orig', 'module_specific_options', 'manifest_required_hashes',
50 'sync_allow_hardlinks',
51 'sync_openpgp_key_path',
52 @@ -93,6 +93,10 @@ class RepoConfig(object):
53 'sync_openpgp_key_refresh_retry_delay_exp_base',
54 'sync_openpgp_key_refresh_retry_delay_mult',
55 'sync_openpgp_key_refresh_retry_overall_timeout',
56 + 'sync_rcu',
57 + 'sync_rcu_store_dir',
58 + 'sync_rcu_spare_snapshots',
59 + 'sync_rcu_ttl_days',
60 )
61
62 def __init__(self, name, repo_opts, local_config=True):
63 @@ -198,6 +202,22 @@ class RepoConfig(object):
64 'sync_openpgp_key_refresh_retry_overall_timeout'):
65 setattr(self, k, repo_opts.get(k.replace('_', '-'), None))
66
67 + self.sync_rcu = repo_opts.get(
68 + 'sync-rcu', 'false').lower() in ('true', 'yes')
69 +
70 + self.sync_rcu_store_dir = repo_opts.get('sync-rcu-store-dir')
71 +
72 + for k in ('sync-rcu-spare-snapshots', 'sync-rcu-ttl-days'):
73 + v = repo_opts.get(k, '').strip() or None
74 + if v:
75 + try:
76 + v = int(v)
77 + except (OverflowError, ValueError):
78 + writemsg(_("!!! Invalid %s setting for repo"
79 + " %s: %s\n") % (k, name, v), noiselevel=-1)
80 + v = None
81 + setattr(self, k.replace('-', '_'), v)
82 +
83 self.module_specific_options = {}
84
85 # Not implemented.
86 @@ -206,9 +226,14 @@ class RepoConfig(object):
87 format = format.strip()
88 self.format = format
89
90 + self.user_location = None
91 location = repo_opts.get('location')
92 if location is not None and location.strip():
93 if os.path.isdir(location) or portage._sync_mode:
94 + # The user_location is required for sync-rcu support,
95 + # since it manages a symlink which resides at that
96 + # location (and realpath is irreversible).
97 + self.user_location = location
98 location = os.path.realpath(location)
99 else:
100 location = None
101 @@ -542,6 +567,10 @@ class RepoConfigLoader(object):
102 'sync_openpgp_key_refresh_retry_delay_exp_base',
103 'sync_openpgp_key_refresh_retry_delay_mult',
104 'sync_openpgp_key_refresh_retry_overall_timeout',
105 + 'sync_rcu',
106 + 'sync_rcu_store_dir',
107 + 'sync_rcu_spare_snapshots',
108 + 'sync_rcu_ttl_days',
109 'sync_type', 'sync_umask', 'sync_uri', 'sync_user',
110 'module_specific_options'):
111 v = getattr(repos_conf_opts, k, None)
112 @@ -962,7 +991,7 @@ class RepoConfigLoader(object):
113 return repo_name in self.prepos
114
115 def config_string(self):
116 - bool_keys = ("strict_misc_digests", "sync_allow_hardlinks")
117 + bool_keys = ("strict_misc_digests", "sync_allow_hardlinks", "sync_rcu")
118 str_or_int_keys = ("auto_sync", "clone_depth", "format", "location",
119 "main_repo", "priority", "sync_depth", "sync_openpgp_key_path",
120 "sync_openpgp_key_refresh_retry_count",
121 @@ -970,6 +999,9 @@ class RepoConfigLoader(object):
122 "sync_openpgp_key_refresh_retry_delay_exp_base",
123 "sync_openpgp_key_refresh_retry_delay_mult",
124 "sync_openpgp_key_refresh_retry_overall_timeout",
125 + "sync_rcu_store_dir",
126 + "sync_rcu_spare_snapshots",
127 + "sync_rcu_ttl_days",
128 "sync_type", "sync_umask", "sync_uri", 'sync_user')
129 str_tuple_keys = ("aliases", "eclass_overrides", "force")
130 repo_config_tuple_keys = ("masters",)
131
132 diff --git a/lib/portage/repository/storage/hardlink_rcu.py b/lib/portage/repository/storage/hardlink_rcu.py
133 new file mode 100644
134 index 000000000..80cdbb0d7
135 --- /dev/null
136 +++ b/lib/portage/repository/storage/hardlink_rcu.py
137 @@ -0,0 +1,251 @@
138 +# Copyright 2018 Gentoo Foundation
139 +# Distributed under the terms of the GNU General Public License v2
140 +
141 +import datetime
142 +
143 +import portage
144 +from portage import os
145 +from portage.repository.storage.interface import (
146 + RepoStorageException,
147 + RepoStorageInterface,
148 +)
149 +from portage.util.futures import asyncio
150 +from portage.util.futures.compat_coroutine import (
151 + coroutine,
152 + coroutine_return,
153 +)
154 +
155 +from _emerge.SpawnProcess import SpawnProcess
156 +
157 +
158 +class HardlinkRcuRepoStorage(RepoStorageInterface):
159 + """
160 + Enable read-copy-update (RCU) behavior for sync operations. The
161 + current latest immutable version of a repository will be
162 + reference by a symlink found where the repository would normally
163 + be located. Repository consumers should resolve the cannonical
164 + path of this symlink before attempt to access the repository,
165 + and all operations should be read-only, since the repository
166 + is considered immutable. Updates occur by atomic replacement
167 + of the symlink, which causes new consumers to use the new
168 + immutable version, while any earlier consumers continue to use
169 + the cannonical path that was resolved earlier.
170 +
171 + Performance is better than HardlinkQuarantineRepoStorage,
172 + since commit involves atomic replacement of a symlink. Since
173 + the symlink usage would require special handling for scenarios
174 + involving bind mounts and chroots, this module is not enabled
175 + by default.
176 +
177 + repos.conf parameters:
178 +
179 + sync-rcu-store-dir
180 +
181 + Directory path reserved for sync-rcu storage. This
182 + directory must have a unique value for each repository
183 + (do not set it in the DEFAULT section). This directory
184 + must not contain any other files or directories aside
185 + from those that are created automatically when sync-rcu
186 + is enabled.
187 +
188 + sync-rcu-spare-snapshots = 1
189 +
190 + Number of spare snapshots for sync-rcu to retain with
191 + expired ttl. This protects the previous latest snapshot
192 + from being removed immediately after a new version
193 + becomes available, since it might still be used by
194 + running processes.
195 +
196 + sync-rcu-ttl-days = 7
197 +
198 + Number of days for sync-rcu to retain previous immutable
199 + snapshots of a repository. After the ttl of a particular
200 + snapshot has expired, it will be remove automatically (the
201 + latest snapshot is exempt, and sync-rcu-spare-snapshots
202 + configures the number of previous snapshots that are
203 + exempt). If the ttl is set too low, then a snapshot could
204 + expire while it is in use by a running process.
205 +
206 + """
207 + def __init__(self, repo, spawn_kwargs):
208 + # Note that repo.location cannot substitute for repo.user_location here,
209 + # since we manage a symlink that resides at repo.user_location, and
210 + # repo.location is the irreversible result of realpath(repo.user_location).
211 + self._user_location = repo.user_location
212 + self._spawn_kwargs = spawn_kwargs
213 +
214 + if not repo.sync_allow_hardlinks:
215 + raise RepoStorageException("repos.conf sync-rcu setting"
216 + " for repo '%s' requires that sync-allow-hardlinks be enabled" % repo.name)
217 +
218 + # Raise an exception if repo.sync_rcu_store_dir is unset, since the
219 + # user needs to be aware of this location for bind mount and chroot
220 + # scenarios
221 + if not repo.sync_rcu_store_dir:
222 + raise RepoStorageException("repos.conf sync-rcu setting"
223 + " for repo '%s' requires that sync-rcu-store-dir be set" % repo.name)
224 +
225 + self._storage_location = repo.sync_rcu_store_dir
226 + if repo.sync_rcu_spare_snapshots is None or repo.sync_rcu_spare_snapshots < 0:
227 + self._spare_snapshots = 1
228 + else:
229 + self._spare_snapshots = repo.sync_rcu_spare_snapshots
230 + if self._spare_snapshots < 0:
231 + self._spare_snapshots = 0
232 + if repo.sync_rcu_ttl_days is None or repo.sync_rcu_ttl_days < 0:
233 + self._ttl_days = 1
234 + else:
235 + self._ttl_days = repo.sync_rcu_ttl_days
236 + self._update_location = None
237 + self._latest_symlink = os.path.join(self._storage_location, 'latest')
238 + self._latest_canonical = os.path.realpath(self._latest_symlink)
239 + if not os.path.exists(self._latest_canonical) or os.path.islink(self._latest_canonical):
240 + # It doesn't exist, or it's a broken symlink.
241 + self._latest_canonical = None
242 + self._snapshots_dir = os.path.join(self._storage_location, 'snapshots')
243 +
244 + @coroutine
245 + def _check_call(self, cmd, privileged=False):
246 + """
247 + Run cmd and raise RepoStorageException on failure.
248 +
249 + @param cmd: command to executre
250 + @type cmd: list
251 + @param privileged: run with maximum privileges
252 + @type privileged: bool
253 + """
254 + if privileged:
255 + kwargs = dict(fd_pipes=self._spawn_kwargs.get('fd_pipes'))
256 + else:
257 + kwargs = self._spawn_kwargs
258 + p = SpawnProcess(args=cmd, scheduler=asyncio._wrap_loop(), **kwargs)
259 + p.start()
260 + if (yield p.async_wait()) != os.EX_OK:
261 + raise RepoStorageException('command exited with status {}: {}'.\
262 + format(p.returncode, ' '.join(cmd)))
263 +
264 + @coroutine
265 + def init_update(self):
266 + update_location = os.path.join(self._storage_location, 'update')
267 + yield self._check_call(['rm', '-rf', update_location])
268 +
269 + # This assumes normal umask permissions if it doesn't exist yet.
270 + portage.util.ensure_dirs(self._storage_location)
271 +
272 + if self._latest_canonical is not None:
273 + portage.util.ensure_dirs(update_location)
274 + portage.util.apply_stat_permissions(update_location,
275 + os.stat(self._user_location))
276 + # Use rsync --link-dest to hardlink a files into update_location,
277 + # since cp -l is not portable.
278 + yield self._check_call(['rsync', '-a', '--link-dest', self._latest_canonical,
279 + self._latest_canonical + '/', update_location + '/'])
280 +
281 + elif not os.path.islink(self._user_location):
282 + yield self._migrate(update_location)
283 + update_location = (yield self.init_update())
284 +
285 + self._update_location = update_location
286 +
287 + coroutine_return(self._update_location)
288 +
289 + @coroutine
290 + def _migrate(self, update_location):
291 + """
292 + When repo.user_location is a normal directory, migrate it to
293 + storage so that it can be replaced with a symlink. After migration,
294 + commit the content as the latest snapshot.
295 + """
296 + try:
297 + os.rename(self._user_location, update_location)
298 + except OSError:
299 + portage.util.ensure_dirs(update_location)
300 + portage.util.apply_stat_permissions(update_location,
301 + os.stat(self._user_location))
302 + # It's probably on a different device, so copy it.
303 + yield self._check_call(['rsync', '-a',
304 + self._user_location + '/', update_location + '/'])
305 +
306 + # Remove the old copy so that symlink can be created. Run with
307 + # maximum privileges, since removal requires write access to
308 + # the parent directory.
309 + yield self._check_call(['rm', '-rf', user_location], privileged=True)
310 +
311 + self._update_location = update_location
312 +
313 + # Make this copy the latest snapshot
314 + yield self.commit_update()
315 +
316 + @property
317 + def current_update(self):
318 + if self._update_location is None:
319 + raise RepoStorageException('current update does not exist')
320 + return self._update_location
321 +
322 + @coroutine
323 + def commit_update(self):
324 + update_location = self.current_update
325 + self._update_location = None
326 + try:
327 + snapshots = [int(name) for name in os.listdir(self._snapshots_dir)]
328 + except OSError:
329 + snapshots = []
330 + portage.util.ensure_dirs(self._snapshots_dir)
331 + portage.util.apply_stat_permissions(self._snapshots_dir,
332 + os.stat(self._storage_location))
333 + if snapshots:
334 + new_id = max(snapshots) + 1
335 + else:
336 + new_id = 1
337 + os.rename(update_location, os.path.join(self._snapshots_dir, str(new_id)))
338 + new_symlink = self._latest_symlink + '.new'
339 + try:
340 + os.unlink(new_symlink)
341 + except OSError:
342 + pass
343 + os.symlink('snapshots/{}'.format(new_id), new_symlink)
344 + os.rename(new_symlink, self._latest_symlink)
345 +
346 + try:
347 + user_location_correct = os.path.samefile(self._user_location, self._latest_symlink)
348 + except OSError:
349 + user_location_correct = False
350 +
351 + if not user_location_correct:
352 + new_symlink = self._user_location + '.new'
353 + try:
354 + os.unlink(new_symlink)
355 + except OSError:
356 + pass
357 + os.symlink(self._latest_symlink, new_symlink)
358 + os.rename(new_symlink, self._user_location)
359 +
360 + coroutine_return()
361 + yield None
362 +
363 + @coroutine
364 + def abort_update(self):
365 + if self._update_location is not None:
366 + update_location = self._update_location
367 + self._update_location = None
368 + yield self._check_call(['rm', '-rf', update_location])
369 +
370 + @coroutine
371 + def garbage_collection(self):
372 + snap_ttl = datetime.timedelta(days=self._ttl_days)
373 + snapshots = sorted(int(name) for name in os.listdir(self._snapshots_dir))
374 + # always preserve the latest snapshot
375 + protect_count = self._spare_snapshots + 1
376 + while snapshots and protect_count:
377 + protect_count -= 1
378 + snapshots.pop()
379 + for snap_id in snapshots:
380 + snap_path = os.path.join(self._snapshots_dir, str(snap_id))
381 + try:
382 + st = os.stat(snap_path)
383 + except OSError:
384 + continue
385 + snap_timestamp = datetime.datetime.utcfromtimestamp(st.st_mtime)
386 + if (datetime.datetime.utcnow() - snap_timestamp) < snap_ttl:
387 + continue
388 + yield self._check_call(['rm', '-rf', snap_path])
389
390 diff --git a/lib/portage/sync/syncbase.py b/lib/portage/sync/syncbase.py
391 index e9b6ede4e..83b35c667 100644
392 --- a/lib/portage/sync/syncbase.py
393 +++ b/lib/portage/sync/syncbase.py
394 @@ -93,7 +93,9 @@ class SyncBase(object):
395 @rtype: str
396 @return: name of the selected repo storage constructor
397 '''
398 - if self.repo.sync_allow_hardlinks:
399 + if self.repo.sync_rcu:
400 + mod_name = 'portage.repository.storage.hardlink_rcu.HardlinkRcuRepoStorage'
401 + elif self.repo.sync_allow_hardlinks:
402 mod_name = 'portage.repository.storage.hardlink_quarantine.HardlinkQuarantineRepoStorage'
403 else:
404 mod_name = 'portage.repository.storage.inplace.InplaceRepoStorage'
405
406 diff --git a/lib/portage/tests/sync/test_sync_local.py b/lib/portage/tests/sync/test_sync_local.py
407 index 17ff6f200..49c7a992d 100644
408 --- a/lib/portage/tests/sync/test_sync_local.py
409 +++ b/lib/portage/tests/sync/test_sync_local.py
410 @@ -1,6 +1,7 @@
411 # Copyright 2014-2015 Gentoo Foundation
412 # Distributed under the terms of the GNU General Public License v2
413
414 +import datetime
415 import subprocess
416 import sys
417 import textwrap
418 @@ -42,6 +43,8 @@ class SyncLocalTestCase(TestCase):
419 location = %(EPREFIX)s/var/repositories/test_repo
420 sync-type = %(sync-type)s
421 sync-uri = file://%(EPREFIX)s/var/repositories/test_repo_sync
422 + sync-rcu = %(sync-rcu)s
423 + sync-rcu-store-dir = %(EPREFIX)s/var/repositories/test_repo_rcu_storedir
424 auto-sync = %(auto-sync)s
425 %(repo_extra_keys)s
426 """)
427 @@ -88,9 +91,10 @@ class SyncLocalTestCase(TestCase):
428 committer_email = "gentoo-dev@g.o"
429
430 def repos_set_conf(sync_type, dflt_keys=None, xtra_keys=None,
431 - auto_sync="yes"):
432 + auto_sync="yes", sync_rcu=False):
433 env["PORTAGE_REPOSITORIES"] = repos_conf % {\
434 "EPREFIX": eprefix, "sync-type": sync_type,
435 + "sync-rcu": "yes" if sync_rcu else "no",
436 "auto-sync": auto_sync,
437 "default_keys": "" if dflt_keys is None else dflt_keys,
438 "repo_extra_keys": "" if xtra_keys is None else xtra_keys}
439 @@ -99,7 +103,18 @@ class SyncLocalTestCase(TestCase):
440 with open(os.path.join(repo.location + "_sync",
441 "dev-libs", "A", "A-0.ebuild"), "a") as f:
442 f.write("\n")
443 - os.unlink(os.path.join(metadata_dir, 'timestamp.chk'))
444 + bump_timestamp()
445 +
446 + def bump_timestamp():
447 + bump_timestamp.timestamp += datetime.timedelta(seconds=1)
448 + with open(os.path.join(repo.location + '_sync', 'metadata', 'timestamp.chk'), 'w') as f:
449 + f.write(bump_timestamp.timestamp.strftime('%s\n' % TIMESTAMP_FORMAT,))
450 +
451 + bump_timestamp.timestamp = datetime.datetime.utcnow()
452 +
453 + bump_timestamp_cmds = (
454 + (homedir, bump_timestamp),
455 + )
456
457 sync_cmds = (
458 (homedir, cmds["emerge"] + ("--sync",)),
459 @@ -170,6 +185,18 @@ class SyncLocalTestCase(TestCase):
460 (homedir, lambda: repos_set_conf("rsync")),
461 )
462
463 + delete_repo_location = (
464 + (homedir, lambda: shutil.rmtree(repo.user_location)),
465 + (homedir, lambda: os.mkdir(repo.user_location)),
466 + )
467 +
468 + revert_rcu_layout = (
469 + (homedir, lambda: os.rename(repo.user_location, repo.user_location + '.bak')),
470 + (homedir, lambda: os.rename(os.path.realpath(repo.user_location + '.bak'), repo.user_location)),
471 + (homedir, lambda: os.unlink(repo.user_location + '.bak')),
472 + (homedir, lambda: shutil.rmtree(repo.user_location + '_rcu_storedir')),
473 + )
474 +
475 delete_sync_repo = (
476 (homedir, lambda: shutil.rmtree(
477 repo.location + "_sync")),
478 @@ -190,6 +217,10 @@ class SyncLocalTestCase(TestCase):
479 (homedir, lambda: repos_set_conf("git")),
480 )
481
482 + sync_rsync_rcu = (
483 + (homedir, lambda: repos_set_conf("rsync", sync_rcu=True)),
484 + )
485 +
486 pythonpath = os.environ.get("PYTHONPATH")
487 if pythonpath is not None and not pythonpath.strip():
488 pythonpath = None
489 @@ -228,7 +259,7 @@ class SyncLocalTestCase(TestCase):
490
491 timestamp_path = os.path.join(metadata_dir, 'timestamp.chk')
492 with open(timestamp_path, 'w') as f:
493 - f.write(time.strftime('%s\n' % TIMESTAMP_FORMAT, time.gmtime()))
494 + f.write(bump_timestamp.timestamp.strftime('%s\n' % TIMESTAMP_FORMAT,))
495
496 if debug:
497 # The subprocess inherits both stdout and stderr, for
498 @@ -242,6 +273,9 @@ class SyncLocalTestCase(TestCase):
499 for cwd, cmd in rename_repo + sync_cmds_auto_sync + sync_cmds + \
500 rsync_opts_repos + rsync_opts_repos_default + \
501 rsync_opts_repos_default_ovr + rsync_opts_repos_default_cancel + \
502 + bump_timestamp_cmds + sync_rsync_rcu + sync_cmds + revert_rcu_layout + \
503 + delete_repo_location + sync_cmds + sync_cmds + \
504 + bump_timestamp_cmds + sync_cmds + revert_rcu_layout + \
505 delete_sync_repo + git_repo_create + sync_type_git + \
506 rename_repo + sync_cmds:
507
508
509 diff --git a/man/portage.5 b/man/portage.5
510 index c3c610a6c..62943fb76 100644
511 --- a/man/portage.5
512 +++ b/man/portage.5
513 @@ -1025,6 +1025,41 @@ If set to true, then sync of a given repository will not trigger postsync
514 hooks unless hooks would have executed for a master repository or the
515 repository has changed since the previous sync operation.
516 .TP
517 +.B sync\-rcu = yes|no
518 +Enable read\-copy\-update (RCU) behavior for sync operations. The current
519 +latest immutable version of a repository will be referenced by a symlink
520 +found where the repository would normally be located (see the \fBlocation\fR
521 +setting). Repository consumers should resolve the cannonical path of this
522 +symlink before attempt to access the repository, and all operations should
523 +be read\-only, since the repository is considered immutable. Updates occur
524 +by atomic replacement of the symlink, which causes new consumers to use the
525 +new immutable version, while any earlier consumers continue to use the
526 +cannonical path that was resolved earlier. This option requires
527 +sync\-allow\-hardlinks and sync\-rcu\-store\-dir options to be enabled, and
528 +currently also requires that sync\-type is set to rsync. This option is
529 +disabled by default, since the symlink usage would require special handling
530 +for scenarios involving bind mounts and chroots.
531 +.TP
532 +.B sync\-rcu\-store\-dir
533 +Directory path reserved for sync\-rcu storage. This directory must have a
534 +unique value for each repository (do not set it in the DEFAULT section).
535 +This directory must not contain any other files or directories aside from
536 +those that are created automatically when sync\-rcu is enabled.
537 +.TP
538 +.B sync\-rcu\-spare\-snapshots = 1
539 +Number of spare snapshots for sync\-rcu to retain with expired ttl. This
540 +protects the previous latest snapshot from being removed immediately after
541 +a new version becomes available, since it might still be used by running
542 +processes.
543 +.TP
544 +.B sync\-rcu\-ttl\-days = 7
545 +Number of days for sync\-rcu to retain previous immutable snapshots of
546 +a repository. After the ttl of a particular snapshot has expired, it
547 +will be remove automatically (the latest snapshot is exempt, and
548 +sync\-rcu\-spare\-snapshots configures the number of previous snapshots
549 +that are exempt). If the ttl is set too low, then a snapshot could
550 +expire while it is in use by a running process.
551 +.TP
552 .B sync\-type
553 Specifies type of synchronization performed by `emerge \-\-sync`.
554 .br