Skip to content
This repository was archived by the owner on Feb 4, 2020. It is now read-only.

Commit d3bc5c0

Browse files
authored
Merge pull request #217 from frerich/per_section_locking
Per-section locking
2 parents 129561a + 0954a47 commit d3bc5c0

2 files changed

Lines changed: 127 additions & 74 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ clcache changelog
1616
* Improvement: Timeout errors when accessing the cache now generate friendlier
1717
error messages mentioning the possibility to work around the issue using the
1818
`CLCACHE_OBJECT_CACHE_TIMEOUT_MS` environment variable.
19+
* Improvement: Greatly improved concurrency of clcache such that concurrent
20+
invocations of the tool no longer block each other.
1921

2022
## clcache 3.2.0 (2016-07-28)
2123

clcache.py

Lines changed: 125 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import cProfile
1111
import codecs
1212
from collections import defaultdict, namedtuple
13+
import contextlib
1314
import errno
1415
import hashlib
1516
import json
@@ -127,6 +128,7 @@ def __str__(self):
127128
class ManifestSection(object):
128129
def __init__(self, manifestSectionDir):
129130
self.manifestSectionDir = manifestSectionDir
131+
self.lock = CacheLock.forPath(self.manifestSectionDir)
130132

131133
def manifestPath(self, manifestHash):
132134
return os.path.join(self.manifestSectionDir, manifestHash + ".json")
@@ -152,6 +154,18 @@ def getManifest(self, manifestHash):
152154
return None
153155

154156

157+
@contextlib.contextmanager
158+
def allSectionsLocked(repository):
159+
sections = list(repository.sections())
160+
for section in sections:
161+
section.lock.acquire()
162+
try:
163+
yield
164+
finally:
165+
for section in sections:
166+
section.lock.release()
167+
168+
155169
class ManifestRepository(object):
156170
# Bump this counter whenever the current manifest file format changes.
157171
# E.g. changing the file format from {'oldkey': ...} to {'newkey': ...} requires
@@ -270,10 +284,17 @@ def acquire(self):
270284
def release(self):
271285
windll.kernel32.ReleaseMutex(self._mutex)
272286

287+
@staticmethod
288+
def forPath(path):
289+
timeoutMs = int(os.environ.get('CLCACHE_OBJECT_CACHE_TIMEOUT_MS', 10 * 1000))
290+
lockName = path.replace(':', '-').replace('\\', '-')
291+
return CacheLock(lockName, timeoutMs)
292+
273293

274294
class CompilerArtifactsSection(object):
275295
def __init__(self, compilerArtifactsSectionDir):
276296
self.compilerArtifactsSectionDir = compilerArtifactsSectionDir
297+
self.lock = CacheLock.forPath(self.compilerArtifactsSectionDir)
277298

278299
def cacheEntryDir(self, key):
279300
return os.path.join(self.compilerArtifactsSectionDir, key)
@@ -424,13 +445,17 @@ def __init__(self, cacheDirectory=None):
424445
ensureDirectoryExists(compilerArtifactsRootDir)
425446
self.compilerArtifactsRepository = CompilerArtifactsRepository(compilerArtifactsRootDir)
426447

427-
lockName = self.cacheDirectory().replace(':', '-').replace('\\', '-')
428-
timeoutMs = int(os.environ.get('CLCACHE_OBJECT_CACHE_TIMEOUT_MS', 10 * 1000))
429-
self.lock = CacheLock(lockName, timeoutMs)
430-
431448
self.configuration = Configuration(os.path.join(self.dir, "config.txt"))
432449
self.statistics = Statistics(os.path.join(self.dir, "stats.txt"))
433450

451+
@property
452+
@contextlib.contextmanager
453+
def lock(self):
454+
with allSectionsLocked(self.manifestRepository), \
455+
allSectionsLocked(self.compilerArtifactsRepository), \
456+
self.statistics.lock:
457+
yield
458+
434459
def cacheDirectory(self):
435460
return self.dir
436461

@@ -551,6 +576,7 @@ class Statistics(object):
551576
def __init__(self, statsFile):
552577
self._statsFile = statsFile
553578
self._stats = None
579+
self.lock = CacheLock.forPath(self._statsFile)
554580

555581
def __enter__(self):
556582
self._stats = PersistentJSONDict(self._statsFile)
@@ -1316,37 +1342,49 @@ def parseIncludesSet(compilerOutput, sourceFile, strip):
13161342
return includesSet, compilerOutput
13171343

13181344

1319-
def addObjectToCache(stats, cache, cachekey, artifacts):
1345+
def addObjectToCache(stats, cache, section, cachekey, artifacts):
1346+
# This function asserts that the caller locked 'section' and 'stats'
1347+
# already and also saves them
13201348
printTraceStatement("Adding file {} to cache using key {}".format(artifacts.objectFilePath, cachekey))
1321-
cache.compilerArtifactsRepository.section(cachekey).setEntry(cachekey, artifacts)
1349+
1350+
section.setEntry(cachekey, artifacts)
13221351
stats.registerCacheEntry(os.path.getsize(artifacts.objectFilePath))
1352+
13231353
with cache.configuration as cfg:
1324-
cache.clean(stats, cfg.maximumCacheSize())
1354+
return stats.currentCacheSize() >= cfg.maximumCacheSize()
13251355

13261356

13271357
def processCacheHit(cache, objectFile, cachekey):
1328-
with cache.statistics as stats:
1329-
stats.registerCacheHit()
13301358
printTraceStatement("Reusing cached object for key {} for object file {}".format(cachekey, objectFile))
1331-
if os.path.exists(objectFile):
1332-
os.remove(objectFile)
1359+
13331360
section = cache.compilerArtifactsRepository.section(cachekey)
1334-
cachedArtifacts = section.getEntry(cachekey)
1335-
copyOrLink(cachedArtifacts.objectFilePath, objectFile)
1336-
printTraceStatement("Finished. Exit code 0")
1337-
return 0, cachedArtifacts.stdout, cachedArtifacts.stderr
1361+
with section.lock:
1362+
with cache.statistics.lock, cache.statistics as stats:
1363+
stats.registerCacheHit()
1364+
1365+
if os.path.exists(objectFile):
1366+
os.remove(objectFile)
1367+
1368+
cachedArtifacts = section.getEntry(cachekey)
1369+
copyOrLink(cachedArtifacts.objectFilePath, objectFile)
1370+
printTraceStatement("Finished. Exit code 0")
1371+
return 0, cachedArtifacts.stdout, cachedArtifacts.stderr, False
13381372

13391373

13401374
def postprocessObjectEvicted(cache, objectFile, cachekey, compilerResult):
13411375
printTraceStatement("Cached object already evicted for key {} for object {}".format(cachekey, objectFile))
13421376
returnCode, compilerOutput, compilerStderr = compilerResult
13431377

1344-
with cache.lock, cache.statistics as stats:
1378+
cleanupRequired = False
1379+
1380+
section = cache.compilerArtifactsRepository.section(cachekey)
1381+
with section.lock, cache.statistics.lock, cache.statistics as stats:
13451382
stats.registerEvictedMiss()
13461383
if returnCode == 0 and os.path.exists(objectFile):
1347-
addObjectToCache(stats, cache, cachekey, CompilerArtifacts(objectFile, compilerOutput, compilerStderr))
1384+
artifacts = CompilerArtifacts(objectFile, compilerOutput, compilerStderr)
1385+
cleanupRequired = addObjectToCache(stats, cache, section, cachekey, artifacts)
13481386

1349-
return compilerResult
1387+
return compilerResult + (cleanupRequired,)
13501388

13511389

13521390
def createManifest(manifestHash, includePaths):
@@ -1377,13 +1415,16 @@ def postprocessHeaderChangedMiss(
13771415
if returnCode == 0 and os.path.exists(objectFile):
13781416
manifest, cachekey = createManifest(manifestHash, includePaths)
13791417

1380-
with cache.lock, cache.statistics as stats:
1418+
cleanupRequired = False
1419+
section = cache.compilerArtifactsRepository.section(cachekey)
1420+
with section.lock, cache.statistics.lock, cache.statistics as stats:
13811421
stats.registerHeaderChangedMiss()
13821422
if returnCode == 0 and os.path.exists(objectFile):
1383-
addObjectToCache(stats, cache, cachekey, CompilerArtifacts(objectFile, compilerOutput, compilerStderr))
1423+
artifacts = CompilerArtifacts(objectFile, compilerOutput, compilerStderr)
1424+
cleanupRequired = addObjectToCache(stats, cache, section, cachekey, artifacts)
13841425
manifestSection.setManifest(manifestHash, manifest)
13851426

1386-
return returnCode, compilerOutput, compilerStderr
1427+
return returnCode, compilerOutput, compilerStderr, cleanupRequired
13871428

13881429

13891430
def postprocessNoManifestMiss(
@@ -1397,14 +1438,17 @@ def postprocessNoManifestMiss(
13971438
if returnCode == 0 and os.path.exists(objectFile):
13981439
manifest, cachekey = createManifest(manifestHash, includePaths)
13991440

1400-
with cache.lock, cache.statistics as stats:
1441+
cleanupRequired = False
1442+
section = cache.compilerArtifactsRepository.section(cachekey)
1443+
with section.lock, cache.statistics.lock, cache.statistics as stats:
14011444
stats.registerSourceChangedMiss()
14021445
if returnCode == 0 and os.path.exists(objectFile):
1446+
artifacts = CompilerArtifacts(objectFile, compilerOutput, compilerStderr)
14031447
# Store compile output and manifest
1404-
addObjectToCache(stats, cache, cachekey, CompilerArtifacts(objectFile, compilerOutput, compilerStderr))
1448+
cleanupRequired = addObjectToCache(stats, cache, section, cachekey, artifacts)
14051449
manifestSection.setManifest(manifestHash, manifest)
14061450

1407-
return returnCode, compilerOutput, compilerStderr
1451+
return returnCode, compilerOutput, compilerStderr, cleanupRequired
14081452

14091453

14101454
def main():
@@ -1481,7 +1525,7 @@ def main():
14811525

14821526

14831527
def updateCacheStatistics(cache, method):
1484-
with cache.lock, cache.statistics as stats:
1528+
with cache.statistics.lock, cache.statistics as stats:
14851529
method(stats)
14861530

14871531

@@ -1500,11 +1544,18 @@ def processCompileRequest(cache, compiler, args):
15001544
else:
15011545
assert objectFile is not None
15021546
if 'CLCACHE_NODIRECT' in os.environ:
1503-
compilerResult = processNoDirect(cache, objectFile, compiler, cmdLine, environment)
1547+
returnCode, compilerOutput, compilerStderr, cleanupRequired = \
1548+
processNoDirect(cache, objectFile, compiler, cmdLine, environment)
15041549
else:
1505-
compilerResult = processDirect(cache, objectFile, compiler, cmdLine, sourceFiles[0])
1506-
printTraceStatement("Finished. Exit code {0:d}".format(compilerResult[0]))
1507-
return compilerResult
1550+
returnCode, compilerOutput, compilerStderr, cleanupRequired = \
1551+
processDirect(cache, objectFile, compiler, cmdLine, sourceFiles[0])
1552+
printTraceStatement("Finished. Exit code {0:d}".format(returnCode))
1553+
1554+
if cleanupRequired:
1555+
with cache.lock:
1556+
cleanCache(cache)
1557+
1558+
return returnCode, compilerOutput, compilerStderr
15081559
except InvalidArgumentError:
15091560
printTraceStatement("Cannot cache invocation as {}: invalid argument".format(cmdLine))
15101561
updateCacheStatistics(cache, Statistics.registerCallWithInvalidArgument)
@@ -1528,6 +1579,8 @@ def processCompileRequest(cache, compiler, args):
15281579
except CalledForPreprocessingError:
15291580
printTraceStatement("Cannot cache invocation as {}: called for preprocessing".format(cmdLine))
15301581
updateCacheStatistics(cache, Statistics.registerCallForPreprocessing)
1582+
except IncludeNotFoundException:
1583+
pass
15311584

15321585
return invokeRealCompiler(compiler, args[1:])
15331586

@@ -1536,62 +1589,60 @@ def processDirect(cache, objectFile, compiler, cmdLine, sourceFile):
15361589
baseDir = normalizeBaseDir(os.environ.get('CLCACHE_BASEDIR'))
15371590
manifestHash = ManifestRepository.getManifestHash(compiler, cmdLine, sourceFile)
15381591
manifestSection = cache.manifestRepository.section(manifestHash)
1539-
with cache.lock:
1540-
createNewManifest = False
1592+
with manifestSection.lock:
15411593
manifest = manifestSection.getManifest(manifestHash)
1542-
if manifest is not None:
1543-
# NOTE: command line options already included in hash for manifest name
1544-
try:
1545-
includesContentHash = ManifestRepository.getIncludesContentHashForFiles({
1546-
expandBasedirPlaceholder(path, baseDir):contentHash
1547-
for path, contentHash in manifest.includeFiles.items()
1548-
})
1549-
1550-
cachekey = manifest.includesContentToObjectMap.get(includesContentHash)
1551-
assert cachekey is not None
1552-
if cache.compilerArtifactsRepository.section(cachekey).hasEntry(cachekey):
1553-
return processCacheHit(cache, objectFile, cachekey)
1554-
else:
1555-
postProcessing = lambda compilerResult: postprocessObjectEvicted(
1556-
cache, objectFile, cachekey, compilerResult)
1557-
except IncludeChangedException:
1558-
createNewManifest = True
1559-
postProcessing = lambda compilerResult: postprocessHeaderChangedMiss(
1560-
cache, objectFile, manifestSection, manifestHash, sourceFile, compilerResult, stripIncludes)
1561-
except IncludeNotFoundException:
1562-
# register nothing. This is probably just a compile error
1563-
postProcessing = None
1564-
else:
1565-
createNewManifest = True
1566-
postProcessing = lambda compilerResult: postprocessNoManifestMiss(
1594+
if manifest is None:
1595+
stripIncludes = False
1596+
if '/showIncludes' not in cmdLine:
1597+
cmdLine.insert(0, '/showIncludes')
1598+
stripIncludes = True
1599+
compilerResult = invokeRealCompiler(compiler, cmdLine, captureOutput=True)
1600+
return postprocessNoManifestMiss(
15671601
cache, objectFile, manifestSection, manifestHash, sourceFile, compilerResult, stripIncludes)
15681602

1569-
if createNewManifest:
1570-
stripIncludes = False
1571-
if '/showIncludes' not in cmdLine:
1572-
cmdLine.insert(0, '/showIncludes')
1573-
stripIncludes = True
1603+
# NOTE: command line options already included in hash for manifest name
1604+
try:
1605+
includesContentHash = ManifestRepository.getIncludesContentHashForFiles({
1606+
expandBasedirPlaceholder(path, baseDir):contentHash
1607+
for path, contentHash in manifest.includeFiles.items()
1608+
})
1609+
except IncludeChangedException:
1610+
stripIncludes = False
1611+
if '/showIncludes' not in cmdLine:
1612+
cmdLine.insert(0, '/showIncludes')
1613+
stripIncludes = True
1614+
compilerResult = invokeRealCompiler(compiler, cmdLine, captureOutput=True)
1615+
return postprocessHeaderChangedMiss(
1616+
cache, objectFile, manifestSection, manifestHash, sourceFile, compilerResult, stripIncludes)
15741617

1575-
compilerResult = invokeRealCompiler(compiler, cmdLine, captureOutput=True)
1576-
if postProcessing:
1577-
compilerResult = postProcessing(compilerResult)
1578-
return compilerResult
1618+
cachekey = manifest.includesContentToObjectMap.get(includesContentHash)
1619+
assert cachekey is not None
1620+
1621+
artifactSection = cache.compilerArtifactsRepository.section(cachekey)
1622+
with artifactSection.lock:
1623+
if artifactSection.hasEntry(cachekey):
1624+
return processCacheHit(cache, objectFile, cachekey)
1625+
compilerResult = invokeRealCompiler(compiler, cmdLine, captureOutput=True)
1626+
return postprocessObjectEvicted(cache, objectFile, cachekey, compilerResult)
15791627

15801628

15811629
def processNoDirect(cache, objectFile, compiler, cmdLine, environment):
15821630
cachekey = CompilerArtifactsRepository.computeKeyNodirect(compiler, cmdLine, environment)
1583-
with cache.lock:
1584-
if cache.compilerArtifactsRepository.section(cachekey).hasEntry(cachekey):
1631+
section = cache.compilerArtifactsRepository.section(cachekey)
1632+
cleanupRequired = False
1633+
with section.lock:
1634+
if section.hasEntry(cachekey):
15851635
return processCacheHit(cache, objectFile, cachekey)
15861636

1587-
compilerResult = invokeRealCompiler(compiler, cmdLine, captureOutput=True, environment=environment)
1588-
returnCode, compilerStdout, compilerStderr = compilerResult
1589-
with cache.lock, cache.statistics as stats:
1590-
stats.registerCacheMiss()
1591-
if returnCode == 0 and os.path.exists(objectFile):
1592-
addObjectToCache(stats, cache, cachekey, CompilerArtifacts(objectFile, compilerStdout, compilerStderr))
1637+
compilerResult = invokeRealCompiler(compiler, cmdLine, captureOutput=True, environment=environment)
1638+
returnCode, compilerStdout, compilerStderr = compilerResult
1639+
with cache.statistics.lock, cache.statistics as stats:
1640+
stats.registerCacheMiss()
1641+
if returnCode == 0 and os.path.exists(objectFile):
1642+
artifacts = CompilerArtifacts(objectFile, compilerStdout, compilerStderr)
1643+
cleanupRequired = addObjectToCache(stats, cache, section, cachekey, artifacts)
15931644

1594-
return returnCode, compilerStdout, compilerStderr
1645+
return returnCode, compilerStdout, compilerStderr, cleanupRequired
15951646

15961647
if __name__ == '__main__':
15971648
if 'CLCACHE_PROFILE' in os.environ:

0 commit comments

Comments
 (0)