Skip to content

Commit 69feea2

Browse files
authored
env: synchronize updates to environments (spack#14621)
Updates to environments were not multi-process safe, which prevented them from taking advantage of parallel builds as implemented in spack#13100. This is a minimal set of changes to enable `spack install` in an environment to be parallelized: - [x] add an internal lock, stored in the `.spack-env` directory, to synchronize updates to `spack.yaml` and `spack.lock` - [x] add `Environment.write_transaction` interface for this lock - [x] makes use of `Environment.write_transaction` in `install`, `add`, and `remove` commands - `uninstall` is not synchronized yet; that is left for a future PR.
1 parent e710656 commit 69feea2

File tree

5 files changed

+153
-93
lines changed

5 files changed

+153
-93
lines changed

lib/spack/spack/cmd/add.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ def setup_parser(subparser):
2525
def add(parser, args):
2626
env = ev.get_env(args, 'add', required=True)
2727

28-
for spec in spack.cmd.parse_specs(args.specs):
29-
if not env.add(spec, args.list_name):
30-
tty.msg("Package {0} was already added to {1}"
31-
.format(spec.name, env.name))
32-
else:
33-
tty.msg('Adding %s to environment %s' % (spec, env.name))
34-
env.write()
28+
with env.write_transaction():
29+
for spec in spack.cmd.parse_specs(args.specs):
30+
if not env.add(spec, args.list_name):
31+
tty.msg("Package {0} was already added to {1}"
32+
.format(spec.name, env.name))
33+
else:
34+
tty.msg('Adding %s to environment %s' % (spec, env.name))
35+
env.write()

lib/spack/spack/cmd/concretize.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def setup_parser(subparser):
1818

1919
def concretize(parser, args):
2020
env = ev.get_env(args, 'concretize', required=True)
21-
concretized_specs = env.concretize(force=args.force)
22-
ev.display_specs(concretized_specs)
23-
env.write()
21+
with env.write_transaction():
22+
concretized_specs = env.concretize(force=args.force)
23+
ev.display_specs(concretized_specs)
24+
env.write()

lib/spack/spack/cmd/install.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,13 @@ def install_spec(cli_args, kwargs, abstract_spec, spec):
223223
# handle active environment, if any
224224
env = ev.get_env(cli_args, 'install')
225225
if env:
226-
env.install(abstract_spec, spec, **kwargs)
227-
env.write()
226+
with env.write_transaction():
227+
concrete = env.concretize_and_add(
228+
abstract_spec, spec)
229+
env.write(regenerate_views=False)
230+
env._install(concrete, **kwargs)
231+
with env.write_transaction():
232+
env.regenerate_views()
228233
else:
229234
spec.package.do_install(**kwargs)
230235

@@ -259,16 +264,20 @@ def install(parser, args, **kwargs):
259264
env = ev.get_env(args, 'install')
260265
if env:
261266
if not args.only_concrete:
262-
concretized_specs = env.concretize()
263-
ev.display_specs(concretized_specs)
267+
with env.write_transaction():
268+
concretized_specs = env.concretize()
269+
ev.display_specs(concretized_specs)
264270

265-
# save view regeneration for later, so that we only do it
266-
# once, as it can be slow.
267-
env.write(regenerate_views=False)
271+
# save view regeneration for later, so that we only do it
272+
# once, as it can be slow.
273+
env.write(regenerate_views=False)
268274

269275
tty.msg("Installing environment %s" % env.name)
270276
env.install_all(args)
271-
env.regenerate_views()
277+
with env.write_transaction():
278+
# It is not strictly required to synchronize view regeneration
279+
# but doing so can prevent redundant work in the filesystem.
280+
env.regenerate_views()
272281
return
273282
else:
274283
tty.die("install requires a package argument or a spack.yaml file")

lib/spack/spack/cmd/remove.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ def setup_parser(subparser):
3131
def remove(parser, args):
3232
env = ev.get_env(args, 'remove', required=True)
3333

34-
if args.all:
35-
env.clear()
36-
else:
37-
for spec in spack.cmd.parse_specs(args.specs):
38-
tty.msg('Removing %s from environment %s' % (spec, env.name))
39-
env.remove(spec, args.list_name, force=args.force)
40-
env.write()
34+
with env.write_transaction():
35+
if args.all:
36+
env.clear()
37+
else:
38+
for spec in spack.cmd.parse_specs(args.specs):
39+
tty.msg('Removing %s from environment %s' % (spec, env.name))
40+
env.remove(spec, args.list_name, force=args.force)
41+
env.write()

lib/spack/spack/environment.py

Lines changed: 116 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from spack.spec import Spec
3737
from spack.spec_list import SpecList, InvalidSpecConstraintError
3838
from spack.variant import UnknownVariantError
39+
import spack.util.lock as lk
3940

4041
#: environment variable used to indicate the active environment
4142
spack_env_var = 'SPACK_ENV'
@@ -557,6 +558,9 @@ def __init__(self, path, init_file=None, with_view=None):
557558
path to the view.
558559
"""
559560
self.path = os.path.abspath(path)
561+
562+
self.txlock = lk.Lock(self._transaction_lock_path)
563+
560564
# This attribute will be set properly from configuration
561565
# during concretization
562566
self.concretization = None
@@ -571,26 +575,7 @@ def __init__(self, path, init_file=None, with_view=None):
571575
else:
572576
self._read_manifest(f, raw_yaml=default_manifest_yaml)
573577
else:
574-
default_manifest = not os.path.exists(self.manifest_path)
575-
if default_manifest:
576-
# No manifest, use default yaml
577-
self._read_manifest(default_manifest_yaml)
578-
else:
579-
with open(self.manifest_path) as f:
580-
self._read_manifest(f)
581-
582-
if os.path.exists(self.lock_path):
583-
with open(self.lock_path) as f:
584-
read_lock_version = self._read_lockfile(f)
585-
if default_manifest:
586-
# No manifest, set user specs from lockfile
587-
self._set_user_specs_from_lockfile()
588-
589-
if read_lock_version == 1:
590-
tty.debug(
591-
"Storing backup of old lockfile {0} at {1}".format(
592-
self.lock_path, self._lock_backup_v1_path))
593-
shutil.copy(self.lock_path, self._lock_backup_v1_path)
578+
self._read()
594579

595580
if with_view is False:
596581
self.views = {}
@@ -602,6 +587,42 @@ def __init__(self, path, init_file=None, with_view=None):
602587
# If with_view is None, then defer to the view settings determined by
603588
# the manifest file
604589

590+
def _re_read(self):
591+
"""Reinitialize the environment object if it has been written (this
592+
may not be true if the environment was just created in this running
593+
instance of Spack)."""
594+
if not os.path.exists(self.manifest_path):
595+
return
596+
597+
self.clear()
598+
self._read()
599+
600+
def _read(self):
601+
default_manifest = not os.path.exists(self.manifest_path)
602+
if default_manifest:
603+
# No manifest, use default yaml
604+
self._read_manifest(default_manifest_yaml)
605+
else:
606+
with open(self.manifest_path) as f:
607+
self._read_manifest(f)
608+
609+
if os.path.exists(self.lock_path):
610+
with open(self.lock_path) as f:
611+
read_lock_version = self._read_lockfile(f)
612+
if default_manifest:
613+
# No manifest, set user specs from lockfile
614+
self._set_user_specs_from_lockfile()
615+
616+
if read_lock_version == 1:
617+
tty.debug(
618+
"Storing backup of old lockfile {0} at {1}".format(
619+
self.lock_path, self._lock_backup_v1_path))
620+
shutil.copy(self.lock_path, self._lock_backup_v1_path)
621+
622+
def write_transaction(self):
623+
"""Get a write lock context manager for use in a `with` block."""
624+
return lk.WriteTransaction(self.txlock, acquire=self._re_read)
625+
605626
def _read_manifest(self, f, raw_yaml=None):
606627
"""Read manifest file and set up user specs."""
607628
if raw_yaml:
@@ -694,6 +715,13 @@ def manifest_path(self):
694715
"""Path to spack.yaml file in this environment."""
695716
return os.path.join(self.path, manifest_name)
696717

718+
@property
719+
def _transaction_lock_path(self):
720+
"""The location of the lock file used to synchronize multiple
721+
processes updating the same environment.
722+
"""
723+
return os.path.join(self.path, 'transaction_lock')
724+
697725
@property
698726
def lock_path(self):
699727
"""Path to spack.lock file in this environment."""
@@ -986,11 +1014,18 @@ def _concretize_separately(self):
9861014
concretized_specs.append((uspec, concrete))
9871015
return concretized_specs
9881016

989-
def install(self, user_spec, concrete_spec=None, **install_args):
990-
"""Install a single spec into an environment.
1017+
def concretize_and_add(self, user_spec, concrete_spec=None):
1018+
"""Concretize and add a single spec to the environment.
9911019
992-
This will automatically concretize the single spec, but it won't
993-
affect other as-yet unconcretized specs.
1020+
Concretize the provided ``user_spec`` and add it along with the
1021+
concretized result to the environment. If the given ``user_spec`` was
1022+
already present in the environment, this does not add a duplicate.
1023+
The concretized spec will be added unless the ``user_spec`` was
1024+
already present and an associated concrete spec was already present.
1025+
1026+
Args:
1027+
concrete_spec: if provided, then it is assumed that it is the
1028+
result of concretizing the provided ``user_spec``
9941029
"""
9951030
if self.concretization == 'together':
9961031
msg = 'cannot install a single spec in an environment that is ' \
@@ -1001,37 +1036,21 @@ def install(self, user_spec, concrete_spec=None, **install_args):
10011036

10021037
spec = Spec(user_spec)
10031038

1004-
with spack.store.db.read_transaction():
1005-
if self.add(spec):
1006-
concrete = concrete_spec or spec.concretized()
1039+
if self.add(spec):
1040+
concrete = concrete_spec or spec.concretized()
1041+
self._add_concrete_spec(spec, concrete)
1042+
else:
1043+
# spec might be in the user_specs, but not installed.
1044+
# TODO: Redo name-based comparison for old style envs
1045+
spec = next(
1046+
s for s in self.user_specs if s.satisfies(user_spec)
1047+
)
1048+
concrete = self.specs_by_hash.get(spec.build_hash())
1049+
if not concrete:
1050+
concrete = spec.concretized()
10071051
self._add_concrete_spec(spec, concrete)
1008-
else:
1009-
# spec might be in the user_specs, but not installed.
1010-
# TODO: Redo name-based comparison for old style envs
1011-
spec = next(
1012-
s for s in self.user_specs if s.satisfies(user_spec)
1013-
)
1014-
concrete = self.specs_by_hash.get(spec.build_hash())
1015-
if not concrete:
1016-
concrete = spec.concretized()
1017-
self._add_concrete_spec(spec, concrete)
10181052

1019-
self._install(concrete, **install_args)
1020-
1021-
def _install(self, spec, **install_args):
1022-
spec.package.do_install(**install_args)
1023-
1024-
# Make sure log directory exists
1025-
log_path = self.log_path
1026-
fs.mkdirp(log_path)
1027-
1028-
with fs.working_dir(self.path):
1029-
# Link the resulting log file into logs dir
1030-
build_log_link = os.path.join(
1031-
log_path, '%s-%s.log' % (spec.name, spec.dag_hash(7)))
1032-
if os.path.lexists(build_log_link):
1033-
os.remove(build_log_link)
1034-
os.symlink(spec.package.build_log_path, build_log_link)
1053+
return concrete
10351054

10361055
@property
10371056
def default_view(self):
@@ -1131,32 +1150,61 @@ def _add_concrete_spec(self, spec, concrete, new=True):
11311150
self.concretized_order.append(h)
11321151
self.specs_by_hash[h] = concrete
11331152

1153+
def install(self, user_spec, concrete_spec=None, **install_args):
1154+
"""Install a single spec into an environment.
1155+
1156+
This will automatically concretize the single spec, but it won't
1157+
affect other as-yet unconcretized specs.
1158+
"""
1159+
concrete = self.concretize_and_add(user_spec, concrete_spec)
1160+
1161+
self._install(concrete, **install_args)
1162+
1163+
def _install(self, spec, **install_args):
1164+
# "spec" must be concrete
1165+
spec.package.do_install(**install_args)
1166+
1167+
if not spec.external:
1168+
# Make sure log directory exists
1169+
log_path = self.log_path
1170+
fs.mkdirp(log_path)
1171+
1172+
with fs.working_dir(self.path):
1173+
# Link the resulting log file into logs dir
1174+
build_log_link = os.path.join(
1175+
log_path, '%s-%s.log' % (spec.name, spec.dag_hash(7)))
1176+
if os.path.lexists(build_log_link):
1177+
os.remove(build_log_link)
1178+
os.symlink(spec.package.build_log_path, build_log_link)
1179+
11341180
def install_all(self, args=None):
11351181
"""Install all concretized specs in an environment.
11361182
11371183
Note: this does not regenerate the views for the environment;
11381184
that needs to be done separately with a call to write().
11391185
11401186
"""
1187+
1188+
# If "spack install" is invoked repeatedly for a large environment
1189+
# where all specs are already installed, the operation can take
1190+
# a large amount of time due to repeatedly acquiring and releasing
1191+
# locks, this does an initial check across all specs within a single
1192+
# DB read transaction to reduce time spent in this case.
1193+
uninstalled_specs = []
11411194
with spack.store.db.read_transaction():
11421195
for concretized_hash in self.concretized_order:
11431196
spec = self.specs_by_hash[concretized_hash]
1197+
if not spec.package.installed:
1198+
uninstalled_specs.append(spec)
1199+
1200+
for spec in uninstalled_specs:
1201+
# Parse cli arguments and construct a dictionary
1202+
# that will be passed to Package.do_install API
1203+
kwargs = dict()
1204+
if args:
1205+
spack.cmd.install.update_kwargs_from_args(args, kwargs)
11441206

1145-
# Parse cli arguments and construct a dictionary
1146-
# that will be passed to Package.do_install API
1147-
kwargs = dict()
1148-
if args:
1149-
spack.cmd.install.update_kwargs_from_args(args, kwargs)
1150-
1151-
self._install(spec, **kwargs)
1152-
1153-
if not spec.external:
1154-
# Link the resulting log file into logs dir
1155-
log_name = '%s-%s' % (spec.name, spec.dag_hash(7))
1156-
build_log_link = os.path.join(self.log_path, log_name)
1157-
if os.path.lexists(build_log_link):
1158-
os.remove(build_log_link)
1159-
os.symlink(spec.package.build_log_path, build_log_link)
1207+
self._install(spec, **kwargs)
11601208

11611209
def all_specs_by_hash(self):
11621210
"""Map of hashes to spec for all specs in this environment."""

0 commit comments

Comments
 (0)