-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
/
Copy pathutils.py
120 lines (99 loc) · 3.95 KB
/
utils.py
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
"""Locking utilities."""
from __future__ import absolute_import
import errno
import logging
import os
import stat
import time
from builtins import object
log = logging.getLogger(__name__)
class LockTimeout(Exception):
pass
class Lock(object):
"""
A simple file based lock with timeout
On entering the context, it will try to acquire the lock. If timeout passes,
it just gets the lock anyway.
If we're in the same thread as the one holding this lock, ignore the lock.
"""
def __init__(self, project, version, timeout=5, polling_interval=0.1):
self.name = project.slug
self.fpath = os.path.join(project.doc_path, '%s__rtdlock' % version.slug)
self.timeout = timeout
self.polling_interval = polling_interval
def __enter__(self):
start = time.time()
while os.path.exists(self.fpath):
lock_age = time.time() - os.stat(self.fpath)[stat.ST_MTIME]
if lock_age > self.timeout:
log.info("Lock (%s): Force unlock, old lockfile",
self.name)
os.remove(self.fpath)
break
log.info("Lock (%s): Locked, waiting..", self.name)
time.sleep(self.polling_interval)
timesince = time.time() - start
if timesince > self.timeout:
log.info("Lock (%s): Force unlock, timeout reached",
self.name)
os.remove(self.fpath)
break
log.info("%s still locked after %.2f seconds; retry for %.2f"
" seconds", self.name, timesince, self.timeout)
open(self.fpath, 'w').close()
log.info("Lock (%s): Lock acquired", self.name)
def __exit__(self, exc, value, tb):
try:
log.info("Lock (%s): Releasing", self.name)
os.remove(self.fpath)
except OSError as e:
# We want to ignore "No such file or directory" and log any other
# type of error.
if e.errno != errno.ENOENT:
log.exception(
"Lock (%s): Failed to release, ignoring...",
self.name,
)
class NonBlockingLock(object):
"""
Acquire a lock in a non-blocking manner.
Instead of waiting for a lock, depending on the lock file age, either
acquire it immediately or throw LockTimeout
:param project: Project being built
:param version: Version to build
:param max_lock_age: If file lock is older than this, forcibly acquire.
None means never force
"""
def __init__(self, project, version, max_lock_age=None):
self.fpath = os.path.join(project.doc_path, '%s__rtdlock' % version.slug)
self.max_lock_age = max_lock_age
self.name = project.slug
def __enter__(self):
path_exists = os.path.exists(self.fpath)
if path_exists and self.max_lock_age is not None:
lock_age = time.time() - os.stat(self.fpath)[stat.ST_MTIME]
if lock_age > self.max_lock_age:
log.info("Lock (%s): Force unlock, old lockfile",
self.name)
os.remove(self.fpath)
else:
raise LockTimeout(
"Lock ({}): Lock still active".format(self.name))
elif path_exists:
raise LockTimeout(
"Lock ({}): Lock still active".format(self.name))
open(self.fpath, 'w').close()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
try:
log.info("Lock (%s): Releasing", self.name)
os.remove(self.fpath)
except (IOError, OSError) as e:
# We want to ignore "No such file or directory" and log any other
# type of error.
if e.errno != errno.ENOENT:
log.error(
'Lock (%s): Failed to release, ignoring...',
self.name,
exc_info=True,
)