forked from treeverse/dvc
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlock.py
More file actions
201 lines (154 loc) 路 5.25 KB
/
Copy pathlock.py
File metadata and controls
201 lines (154 loc) 路 5.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
"""Manages dvc lock file."""
import hashlib
import os
from abc import ABC, abstractmethod
from datetime import timedelta
import flufl.lock
import zc.lockfile
from funcy import retry
from dvc.exceptions import DvcException
from dvc.progress import Tqdm
from dvc.utils import format_link
DEFAULT_TIMEOUT = 3
FAILED_TO_LOCK_MESSAGE = (
"Unable to acquire lock. Most likely another DVC process is running or "
"was terminated abruptly. Check the page {} for other possible reasons "
"and to learn how to resolve this."
).format(
format_link("https://dvc.org/doc/user-guide/troubleshooting#lock-issue")
)
class LockError(DvcException):
"""Thrown when unable to acquire the lock for DVC repo."""
class LockBase(ABC):
@abstractmethod
def __init__(self, lockfile):
self._lockfile = lockfile
@property
def lockfile(self):
return self._lockfile
@abstractmethod
def lock(self):
pass
@abstractmethod
def unlock(self):
pass
@property
@abstractmethod
def is_locked(self):
pass
@abstractmethod
def __enter__(self):
pass
@abstractmethod
def __exit__(self, typ, value, tbck):
pass
class LockNoop(LockBase):
def __init__(
self, *args, **kwargs
): # pylint: disable=super-init-not-called
self._lock = False
def lock(self):
self._lock = True
def unlock(self):
self._lock = False
@property
def is_locked(self):
return self._lock
def __enter__(self):
self.lock()
def __exit__(self, typ, value, tbck):
self.unlock()
class Lock(LockBase):
"""Class for DVC repo lock.
Uses zc.lockfile as backend.
"""
def __init__(self, lockfile, friendly=False, **kwargs):
super().__init__(lockfile)
self._friendly = friendly
self._lock = None
@property
def files(self):
return [self._lockfile]
def _do_lock(self):
try:
with Tqdm(
bar_format="{desc}",
disable=not self._friendly,
desc=(
"If DVC froze, see `hardlink_lock` in {}".format(
format_link("https://man.dvc.org/config#core")
)
),
):
self._lock = zc.lockfile.LockFile(self._lockfile)
except zc.lockfile.LockError:
raise LockError(FAILED_TO_LOCK_MESSAGE)
def lock(self):
retries = 6
delay = DEFAULT_TIMEOUT / retries
lock_retry = retry(retries, LockError, timeout=delay)(self._do_lock)
lock_retry()
def unlock(self):
self._lock.close()
self._lock = None
@property
def is_locked(self):
return bool(self._lock)
def __enter__(self):
self.lock()
def __exit__(self, typ, value, tbck):
self.unlock()
class HardlinkLock(flufl.lock.Lock, LockBase):
"""Class for DVC repo lock.
Args:
lockfile (str): the lock filename
in.
tmp_dir (str): a directory to store claim files.
"""
def __init__(
self, lockfile, tmp_dir=None, **kwargs
): # pylint: disable=super-init-not-called
import socket
self._tmp_dir = tmp_dir
super().__init__(lockfile)
# NOTE: this is basically Lock.__init__ copy-paste, except that
# instead of using `socket.getfqdn()` we use `socket.gethostname()`
# to speed this up. We've seen [1] `getfqdn()` take ~5sec to return
# anything, which is way too slow. `gethostname()` is actually a
# fallback for `getfqdn()` when it is not able to resolve a
# canonical hostname through network. The claimfile that uses
# `self._hostname` is still usable, as it uses `pid` and random
# number to generate the resulting lock file name, which is unique
# enough for our application.
#
# [1] https://github.com/iterative/dvc/issues/2582
self._hostname = socket.gethostname()
self._lifetime = timedelta(days=365) # Lock for good by default
self._separator = flufl.lock.SEP
self._set_claimfile()
self._owned = True
self._retry_errnos = []
def lock(self): # pylint: disable=arguments-differ
try:
super().lock(timedelta(seconds=DEFAULT_TIMEOUT))
except flufl.lock.TimeOutError:
raise LockError(FAILED_TO_LOCK_MESSAGE)
def _set_claimfile(self, pid=None):
super()._set_claimfile(pid)
if self._tmp_dir is not None:
# Under Windows file path length is limited so we hash it
filename = hashlib.md5(self._claimfile.encode()).hexdigest()
self._claimfile = os.path.join(self._tmp_dir, filename + ".lock")
# Fix for __del__ bug in flufl.lock [1] which is causing errors on
# Python shutdown [2].
# [1] https://gitlab.com/warsaw/flufl.lock/issues/7
# [2] https://github.com/iterative/dvc/issues/2573
def __del__(self):
try:
if self._owned:
self.finalize()
except ImportError:
pass
def make_lock(lockfile, tmp_dir=None, friendly=False, hardlink_lock=False):
cls = HardlinkLock if hardlink_lock else Lock
return cls(lockfile, tmp_dir=tmp_dir, friendly=friendly)