Skip to content

Commit 65ccd6b

Browse files
authored
Avoid parallel execution of a specific schenario (#2638)
Adds a lock and waits for it in order to avoid running in parallel using the same ephemeral directory.
1 parent 0172637 commit 65ccd6b

File tree

3 files changed

+49
-17
lines changed

3 files changed

+49
-17
lines changed

.pre-commit-config.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,4 @@ repos:
5757
- id: pylint
5858
additional_dependencies:
5959
- ansible-base
60+
- testinfra

molecule/constants.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Constants used by molecule."""
2+
3+
4+
RC_SUCCESS = 0
5+
RC_TIMEOUT = 3

molecule/scenario.py

+43-17
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@
1919
# DEALINGS IN THE SOFTWARE.
2020
"""Molecule Scenario Module."""
2121

22+
import fcntl
2223
import fnmatch
2324
import os
2425
import shutil
2526
from pathlib import Path
27+
from time import sleep
2628

2729
from molecule import logger, scenarios, util
30+
from molecule.constants import RC_TIMEOUT
2831

2932
LOG = logger.get_logger(__name__)
3033

@@ -91,6 +94,7 @@ def __init__(self, config):
9194
:param config: An instance of a Molecule config.
9295
:return: None
9396
"""
97+
self._lock = None
9498
self.config = config
9599
self._setup()
96100

@@ -143,22 +147,42 @@ def directory(self):
143147

144148
@property
145149
def ephemeral_directory(self):
146-
_ephemeral_directory = os.getenv("MOLECULE_EPHEMERAL_DIRECTORY")
147-
if _ephemeral_directory:
148-
return _ephemeral_directory
149-
150-
project_directory = os.path.basename(self.config.project_directory)
151-
152-
if self.config.is_parallel:
153-
project_directory = "{}-{}".format(project_directory, self.config._run_uuid)
154-
155-
project_scenario_directory = os.path.join(
156-
self.config.cache_directory, project_directory, self.name
157-
)
158-
159-
path = ephemeral_directory(project_scenario_directory)
160-
161-
return ephemeral_directory(path)
150+
path = os.getenv("MOLECULE_EPHEMERAL_DIRECTORY", None)
151+
if not path:
152+
153+
project_directory = os.path.basename(self.config.project_directory)
154+
155+
if self.config.is_parallel:
156+
project_directory = "{}-{}".format(
157+
project_directory, self.config._run_uuid
158+
)
159+
160+
project_scenario_directory = os.path.join(
161+
self.config.cache_directory, project_directory, self.name
162+
)
163+
164+
path = ephemeral_directory(project_scenario_directory)
165+
166+
# TODO(ssbarnea): Tune or make the retry logic configurable once we have enough data
167+
if not self._lock:
168+
self._lock = open(os.path.join(path, ".lock"), "w")
169+
for i in range(1, 5):
170+
try:
171+
fcntl.lockf(self._lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
172+
break
173+
except OSError:
174+
delay = 30 * i
175+
LOG.warning(
176+
"Retrying to acquire lock on %s, waiting for %s seconds",
177+
path,
178+
delay,
179+
)
180+
sleep(delay)
181+
else:
182+
LOG.warning("Timedout trying to acquire lock on %s", path)
183+
raise SystemExit(RC_TIMEOUT)
184+
185+
return path
162186

163187
@property
164188
def inventory_directory(self):
@@ -246,7 +270,7 @@ def _setup(self):
246270
os.makedirs(self.inventory_directory)
247271

248272

249-
def ephemeral_directory(path=None):
273+
def ephemeral_directory(path: str = None) -> str:
250274
"""
251275
Return temporary directory to be used by molecule.
252276
@@ -256,6 +280,8 @@ def ephemeral_directory(path=None):
256280
d = os.getenv("MOLECULE_EPHEMERAL_DIRECTORY")
257281
if not d:
258282
d = os.getenv("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))
283+
if not d:
284+
raise RuntimeError("Unable to determine ephemeral directory to use.")
259285
d = os.path.abspath(os.path.join(d, path if path else "molecule"))
260286

261287
if not os.path.isdir(d):

0 commit comments

Comments
 (0)