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 |