Skip to content

Commit 41e03a6

Browse files
authored
Merge pull request #654 from meeseeksmachine/auto-backport-of-pr-564-on-0.11.x
Backport PR #564 on branch 0.11.x (Fix race condition issues on slower FS)
2 parents da5ee12 + b2001c8 commit 41e03a6

File tree

2 files changed

+68
-15
lines changed

2 files changed

+68
-15
lines changed

jupyterlab_git/git.py

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import pexpect
1010
import tornado
11+
import tornado.locks
12+
import datetime
1113

1214

1315
# Git configuration options exposed through the REST API
@@ -16,11 +18,18 @@
1618
# See https://git-scm.com/docs/git-config#_syntax for git var syntax
1719
CONFIG_PATTERN = re.compile(r"(?:^|\n)([\w\-\.]+)\=")
1820
DEFAULT_REMOTE_NAME = "origin"
21+
# How long to wait to be executed or finished your execution before timing out
22+
MAX_WAIT_FOR_EXECUTE_S = 20
23+
# Ensure on NFS or similar, that we give the .git/index.lock time to be removed
24+
MAX_WAIT_FOR_LOCK_S = 5
25+
# How often should we check for the lock above to be free? This comes up more on things like NFS
26+
CHECK_LOCK_INTERVAL_S = 0.1
1927

28+
execution_lock = tornado.locks.Lock()
2029

2130
async def execute(
2231
cmdline: "List[str]",
23-
cwd: "Optional[str]" = None,
32+
cwd: "str",
2433
env: "Optional[Dict[str, str]]" = None,
2534
username: "Optional[str]" = None,
2635
password: "Optional[str]" = None,
@@ -85,19 +94,36 @@ def call_subprocess(
8594
output, error = process.communicate()
8695
return (process.returncode, output.decode("utf-8"), error.decode("utf-8"))
8796

88-
if username is not None and password is not None:
89-
code, output, error = await call_subprocess_with_authentication(
90-
cmdline,
91-
username,
92-
password,
93-
cwd,
94-
env,
95-
)
96-
else:
97-
current_loop = tornado.ioloop.IOLoop.current()
98-
code, output, error = await current_loop.run_in_executor(
99-
None, call_subprocess, cmdline, cwd, env
100-
)
97+
try:
98+
await execution_lock.acquire(timeout=datetime.timedelta(seconds=MAX_WAIT_FOR_EXECUTE_S))
99+
except tornado.util.TimeoutError:
100+
return (1, "", "Unable to get the lock on the directory")
101+
102+
try:
103+
# Ensure our execution operation will succeed by first checking and waiting for the lock to be removed
104+
time_slept = 0
105+
lockfile = os.path.join(cwd, '.git', 'index.lock')
106+
while os.path.exists(lockfile) and time_slept < MAX_WAIT_FOR_LOCK_S:
107+
await tornado.gen.sleep(CHECK_LOCK_INTERVAL_S)
108+
time_slept += CHECK_LOCK_INTERVAL_S
109+
110+
# If the lock still exists at this point, we will likely fail anyway, but let's try anyway
111+
112+
if username is not None and password is not None:
113+
code, output, error = await call_subprocess_with_authentication(
114+
cmdline,
115+
username,
116+
password,
117+
cwd,
118+
env,
119+
)
120+
else:
121+
current_loop = tornado.ioloop.IOLoop.current()
122+
code, output, error = await current_loop.run_in_executor(
123+
None, call_subprocess, cmdline, cwd, env
124+
)
125+
finally:
126+
execution_lock.release()
101127

102128
return code, output, error
103129

@@ -1003,7 +1029,7 @@ async def diff_content(self, filename, prev_ref, curr_ref, top_repo_path):
10031029
is_binary = await self._is_binary(filename, "INDEX", top_repo_path)
10041030
if is_binary:
10051031
raise tornado.web.HTTPError(log_message="Error occurred while executing command to retrieve plaintext diff as file is not UTF-8.")
1006-
1032+
10071033
curr_content = await self.show(filename, "", top_repo_path)
10081034
else:
10091035
raise tornado.web.HTTPError(

jupyterlab_git/tests/test_execute.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import pytest
2+
from unittest.mock import patch
3+
4+
from jupyterlab_git.git import execute, execution_lock
5+
6+
7+
@pytest.mark.asyncio
8+
async def test_execute_waits_on_index_lock(tmp_path):
9+
lock_file = tmp_path / ".git/index.lock"
10+
lock_file.parent.mkdir(parents=True, exist_ok=True)
11+
lock_file.write_text("")
12+
13+
async def remove_lock_file(*args):
14+
assert "unlocked" not in repr(execution_lock) # Check that the lock is working
15+
lock_file.unlink() # Raise an error for missing file
16+
17+
with patch("tornado.gen.sleep") as sleep:
18+
sleep.side_effect = remove_lock_file # Remove the lock file instead of sleeping
19+
20+
assert "unlock" in repr(execution_lock)
21+
cmd = ["git", "dummy"]
22+
kwargs = {"cwd": f"{tmp_path!s}"}
23+
await execute(cmd, **kwargs)
24+
assert "unlock" in repr(execution_lock)
25+
26+
assert not lock_file.exists()
27+
sleep.assert_called_once()

0 commit comments

Comments
 (0)