-
Notifications
You must be signed in to change notification settings - Fork 82
adds lockfile based locking strategy #82
base: master
Are you sure you want to change the base?
Changes from 1 commit
e5f1e10
a23081a
0b6a697
4de8217
49a0d95
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -51,6 +51,9 @@ | |||
| import sys | ||||
| import multiprocessing | ||||
| import re | ||||
| import time | ||||
| import msvcrt | ||||
| import random | ||||
|
|
||||
| VERSION = "3.0.3-dev" | ||||
|
|
||||
|
|
@@ -84,10 +87,9 @@ def __str__(self): | |||
| return repr(self.message) | ||||
|
|
||||
|
|
||||
| class ObjectCacheLock: | ||||
| class ObjectCacheLockMutex: | ||||
| """ Implements a lock for the object cache which | ||||
| can be used in 'with' statements. """ | ||||
| INFINITE = 0xFFFFFFFF | ||||
|
|
||||
| def __init__(self, mutexName, timeoutMs): | ||||
| mutexName = 'Local\\' + mutexName | ||||
|
|
@@ -126,6 +128,80 @@ def release(self): | |||
| self._acquired = False | ||||
|
|
||||
|
|
||||
| class ObjectCacheLockFile: | ||||
| """ Implements a lock file lock for the object cache which | ||||
| can be used in 'with' statements. | ||||
| Inspired by | ||||
| https://github.com/harlowja/fasteners | ||||
| https://github.com/benediktschmitt/py-filelock""" | ||||
|
|
||||
| def __init__(self, lockfileName, timeoutMs): | ||||
| self._lockfileName = lockfileName | ||||
| self._lockfile = None | ||||
| self._timeoutMs = timeoutMs | ||||
|
|
||||
| def __enter__(self): | ||||
| self.acquire() | ||||
|
|
||||
| def __exit__(self, typ, value, traceback): | ||||
| self.release() | ||||
|
|
||||
| def __del__(self): | ||||
| self.release() | ||||
|
|
||||
| def _acquire(self): | ||||
| try: | ||||
| lockfile = open(self._lockfileName, 'a') | ||||
| except OSError: | ||||
| lockfile = None | ||||
| else: | ||||
| try: | ||||
| msvcrt.locking(lockfile.fileno(), msvcrt.LK_NBLCK, 1) | ||||
| except (IOError, OSError): | ||||
| lockfile.close() | ||||
| else: | ||||
| self._lockfile = lockfile | ||||
| return None | ||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For stylistic reason, I think it would be fine to just emit the |
||||
|
|
||||
| def _release(self): | ||||
| if self._lockfile is not None: | ||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This |
||||
| lockfile = self._lockfile | ||||
| self._lockfile = None | ||||
| msvcrt.locking(lockfile.fileno(), msvcrt.LK_UNLCK, 1) | ||||
| lockfile.close() | ||||
|
|
||||
| #The following might fail because another instance already has locked the file. | ||||
| #This is no problem because the existence of the file does not provide the | ||||
| #locking but win32 api base file locking mechanism. | ||||
| try: | ||||
| os.remove(self._lockfileName) | ||||
| except OSError: | ||||
| pass | ||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that the existence of the file doesn't matter, I guess it could be argued that there's no point in removing the file - how about just dropping this code completely? |
||||
|
|
||||
| return None | ||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same remarks regarding |
||||
|
|
||||
| def is_locked(self): | ||||
| return self._lockfile is not None | ||||
|
|
||||
| def acquire(self): | ||||
| start_time = time.time() | ||||
| while True: | ||||
| if not self.is_locked(): | ||||
| self._acquire() | ||||
|
|
||||
| if self.is_locked(): | ||||
| break | ||||
| elif self._timeoutMs >= 0 and time.time() - start_time > self._timeoutMs/1000: | ||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be nicer to enforce that |
||||
| raise ObjectCacheLockException("Timeout waiting for file lock") | ||||
| else: | ||||
| poll_intervall = random.uniform(0.01, 0.1) | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. camelCase; is this really an interval? I think pollDelay would be a better name indicating that the delay before the next poll – a single float number – is stored. |
||||
| time.sleep(poll_intervall) | ||||
|
|
||||
| def release(self): | ||||
| if self.is_locked(): | ||||
| self._release() | ||||
|
|
||||
|
|
||||
| class ObjectCache: | ||||
| def __init__(self): | ||||
| try: | ||||
|
|
@@ -142,7 +218,13 @@ def __init__(self): | |||
| os.makedirs(self.objectsDir) | ||||
| lockName = self.cacheDirectory().replace(':', '-').replace('\\', '-') | ||||
| timeout_ms = int(os.environ.get('CLCACHE_OBJECT_CACHE_TIMEOUT_MS', 10 * 1000)) | ||||
| self.lock = ObjectCacheLock(lockName, timeout_ms) | ||||
|
|
||||
| cfg = Configuration(self) | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one is tricky ;) We need to hold a lock before we're allowed to access the configuration. At least this is what the current code suggests: Line 1104 in dc63c22
So this might be the point where moving to environment variables makes life easier.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch! There's a hen and egg problem here: the Quite frankly, given that file-based locking doesn't seem to have any downsides so far (at least not timing-wise), I think we could just as well make it the default for the next version and see how it goes. |
||||
| if cfg.lockingStrategy() == "File": | ||||
| lockfileName = os.path.join(self.cacheDirectory(), "cache.lock") | ||||
| self.lock = ObjectCacheLockFile(lockfileName, timeout_ms) | ||||
| else: | ||||
| self.lock = ObjectCacheLockMutex(lockName, timeout_ms) | ||||
|
|
||||
| def cacheDirectory(self): | ||||
| return self.dir | ||||
|
|
@@ -342,7 +424,8 @@ def __contains__(self, key): | |||
|
|
||||
|
|
||||
| class Configuration: | ||||
| _defaultValues = {"MaximumCacheSize": 1073741824} # 1 GiB | ||||
| _defaultValues = {"MaximumCacheSize": 1073741824, # 1 GiB | ||||
| "LockingStrategy": "Mutex"} | ||||
|
|
||||
| def __init__(self, objectCache): | ||||
| self._objectCache = objectCache | ||||
|
|
@@ -358,6 +441,9 @@ def maximumCacheSize(self): | |||
| def setMaximumCacheSize(self, size): | ||||
| self._cfg["MaximumCacheSize"] = size | ||||
|
|
||||
| def lockingStrategy(self): | ||||
| return self._cfg["LockingStrategy"] | ||||
|
|
||||
| def save(self): | ||||
| self._cfg.save() | ||||
|
|
||||
|
|
@@ -888,6 +974,7 @@ def printStatistics(cache): | |||
| current cache dir : {} | ||||
| cache size : {:,} bytes | ||||
| maximum cache size : {:,} bytes | ||||
| locking strategy : {} | ||||
| cache entries : {} | ||||
| cache hits : {} | ||||
| cache misses | ||||
|
|
@@ -905,6 +992,7 @@ def printStatistics(cache): | |||
| cache.cacheDirectory(), | ||||
| stats.currentCacheSize(), | ||||
| cfg.maximumCacheSize(), | ||||
| cfg.lockingStrategy(), | ||||
| stats.numCacheEntries(), | ||||
| stats.numCacheHits(), | ||||
| stats.numCacheMisses(), | ||||
|
|
@@ -1165,7 +1253,11 @@ def processCompileRequest(cache, compiler, args): | |||
|
|
||||
| def processDirect(cache, outputFile, compiler, cmdLine, sourceFile): | ||||
| manifestHash = ObjectCache.getManifestHash(compiler, cmdLine, sourceFile) | ||||
| with cache.lock: | ||||
| postProcessing = None | ||||
|
|
||||
| try: | ||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure I get this change: why couldn't you go with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry some temporary change. I revert it. I wanted to try if it is possible to fall back to just invoke the compiler in case we hit a timeout waiting for the lockfile instead of aborting the build. |
||||
| cache.lock.acquire() | ||||
|
|
||||
| manifest = cache.getManifest(manifestHash) | ||||
| baseDir = os.environ.get('CLCACHE_BASEDIR') | ||||
| if baseDir and not baseDir.endswith(os.path.sep): | ||||
|
|
@@ -1196,9 +1288,18 @@ def processDirect(cache, outputFile, compiler, cmdLine, sourceFile): | |||
| stripIncludes = True | ||||
| postProcessing = lambda compilerResult: postprocessNoManifestMiss(cache, outputFile, manifestHash, baseDir, compiler, origCmdLine, sourceFile, compilerResult, stripIncludes) | ||||
|
|
||||
| except ObjectCacheLockException: | ||||
| printTraceStatement("Timeout waiting for lock") | ||||
|
|
||||
| finally: | ||||
| cache.lock.release() | ||||
|
|
||||
| compilerResult = invokeRealCompiler(compiler, cmdLine, captureOutput=True) | ||||
| compilerResult = postProcessing(compilerResult) | ||||
| printTraceStatement("Finished. Exit code %d" % compilerResult[0]) | ||||
|
|
||||
| if postProcessing is not None: | ||||
| compilerResult = postProcessing(compilerResult) | ||||
| printTraceStatement("Finished. Exit code %d" % compilerResult[0]) | ||||
|
|
||||
| return compilerResult | ||||
|
|
||||
|
|
||||
|
|
||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this assignment is unneeded:
_acquireis only called ifself.lockfile == Noneholds, which is stillTruein caseopenraises an exception. I'd say you can justreturnhere. Doing so would permit dropping the explicitelsebranch and thus lowering the indentation.