You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PersistentCache.java 8.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. /*
  2. * SonarQube, open source software quality management tool.
  3. * Copyright (C) 2008-2014 SonarSource
  4. * mailto:contact AT sonarsource DOT com
  5. *
  6. * SonarQube is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * SonarQube is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.home.cache;
  21. import java.io.IOException;
  22. import java.io.RandomAccessFile;
  23. import java.nio.channels.FileChannel;
  24. import java.nio.channels.FileLock;
  25. import java.nio.charset.Charset;
  26. import java.nio.charset.StandardCharsets;
  27. import java.nio.file.DirectoryStream;
  28. import java.nio.file.Files;
  29. import java.nio.file.Path;
  30. import java.nio.file.attribute.BasicFileAttributes;
  31. import java.security.MessageDigest;
  32. import java.security.NoSuchAlgorithmException;
  33. import javax.annotation.CheckForNull;
  34. import javax.annotation.Nonnull;
  35. import javax.annotation.Nullable;
  36. import static java.nio.file.StandardOpenOption.CREATE;
  37. import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
  38. import static java.nio.file.StandardOpenOption.WRITE;
  39. public class PersistentCache {
  40. private static final Charset ENCODING = StandardCharsets.UTF_8;
  41. private static final String DIGEST_ALGO = "MD5";
  42. private static final String LOCK_FNAME = ".lock";
  43. private Path baseDir;
  44. // eviction strategy is to expire entries after modification once a time duration has elapsed
  45. private final long defaultDurationToExpireMs;
  46. private final Logger logger;
  47. private final String version;
  48. public PersistentCache(Path baseDir, long defaultDurationToExpireMs, Logger logger, String version) {
  49. this.baseDir = baseDir;
  50. this.defaultDurationToExpireMs = defaultDurationToExpireMs;
  51. this.logger = logger;
  52. this.version = version;
  53. reconfigure();
  54. logger.debug("cache: " + baseDir + ", default expiration time (ms): " + defaultDurationToExpireMs);
  55. }
  56. public void reconfigure() {
  57. try {
  58. Files.createDirectories(baseDir);
  59. } catch (IOException e) {
  60. throw new IllegalStateException("failed to create cache dir", e);
  61. }
  62. }
  63. public Path getBaseDirectory() {
  64. return baseDir;
  65. }
  66. @CheckForNull
  67. public synchronized String getString(@Nonnull String obj, @Nullable final PersistentCacheLoader<String> valueLoader) throws IOException {
  68. byte[] cached = get(obj, new ValueLoaderDecoder(valueLoader));
  69. if (cached == null) {
  70. return null;
  71. }
  72. return new String(cached, ENCODING);
  73. }
  74. @CheckForNull
  75. public synchronized byte[] get(@Nonnull String obj, @Nullable PersistentCacheLoader<byte[]> valueLoader) throws IOException {
  76. String key = getKey(obj);
  77. try {
  78. lock();
  79. byte[] cached = getCache(key);
  80. if (cached != null) {
  81. logger.debug("cache hit for " + obj + " -> " + key);
  82. return cached;
  83. }
  84. logger.debug("cache miss for " + obj + " -> " + key);
  85. if (valueLoader != null) {
  86. byte[] value = valueLoader.get();
  87. if (value != null) {
  88. putCache(key, value);
  89. }
  90. return value;
  91. }
  92. } finally {
  93. unlock();
  94. }
  95. return null;
  96. }
  97. public synchronized void put(@Nonnull String obj, @Nonnull byte[] value) throws IOException {
  98. String key = getKey(obj);
  99. try {
  100. lock();
  101. putCache(key, value);
  102. } finally {
  103. unlock();
  104. }
  105. }
  106. /**
  107. * Deletes all cache entries
  108. */
  109. public synchronized void clear() {
  110. logger.info("cache: clearing");
  111. try {
  112. lock();
  113. deleteCacheEntries(new DirectoryClearFilter());
  114. } catch (IOException e) {
  115. logger.error("Error clearing cache", e);
  116. } finally {
  117. unlock();
  118. }
  119. }
  120. /**
  121. * Deletes cache entries that are no longer valid according to the default expiration time period.
  122. */
  123. public synchronized void clean() {
  124. logger.info("cache: cleaning");
  125. try {
  126. lock();
  127. deleteCacheEntries(new DirectoryCleanFilter(defaultDurationToExpireMs));
  128. } catch (IOException e) {
  129. logger.error("Error cleaning cache", e);
  130. } finally {
  131. unlock();
  132. }
  133. }
  134. private void lock() throws IOException {
  135. lockRandomAccessFile = new RandomAccessFile(getLockPath().toFile(), "rw");
  136. lockChannel = lockRandomAccessFile.getChannel();
  137. lockFile = lockChannel.lock();
  138. }
  139. private RandomAccessFile lockRandomAccessFile;
  140. private FileChannel lockChannel;
  141. private FileLock lockFile;
  142. private void unlock() {
  143. if (lockFile != null) {
  144. try {
  145. lockFile.release();
  146. } catch (IOException e) {
  147. logger.error("Error releasing lock", e);
  148. }
  149. }
  150. if (lockChannel != null) {
  151. try {
  152. lockChannel.close();
  153. } catch (IOException e) {
  154. logger.error("Error closing file channel", e);
  155. }
  156. }
  157. if (lockRandomAccessFile != null) {
  158. try {
  159. lockRandomAccessFile.close();
  160. } catch (IOException e) {
  161. logger.error("Error closing file", e);
  162. }
  163. }
  164. lockFile = null;
  165. lockRandomAccessFile = null;
  166. lockChannel = null;
  167. }
  168. private String getKey(String uri) {
  169. try {
  170. String key = uri;
  171. if (version != null) {
  172. key += version;
  173. }
  174. MessageDigest digest = MessageDigest.getInstance(DIGEST_ALGO);
  175. digest.update(key.getBytes(StandardCharsets.UTF_8));
  176. return byteArrayToHex(digest.digest());
  177. } catch (NoSuchAlgorithmException e) {
  178. throw new IllegalStateException("Couldn't create hash", e);
  179. }
  180. }
  181. private void deleteCacheEntries(DirectoryStream.Filter<Path> filter) throws IOException {
  182. try (DirectoryStream<Path> stream = Files.newDirectoryStream(baseDir, filter)) {
  183. for (Path p : stream) {
  184. try {
  185. Files.delete(p);
  186. } catch (Exception e) {
  187. logger.error("Error deleting " + p, e);
  188. }
  189. }
  190. }
  191. }
  192. private static class ValueLoaderDecoder implements PersistentCacheLoader<byte[]> {
  193. PersistentCacheLoader<String> valueLoader;
  194. ValueLoaderDecoder(PersistentCacheLoader<String> valueLoader) {
  195. this.valueLoader = valueLoader;
  196. }
  197. @Override
  198. public byte[] get() throws IOException {
  199. String s = valueLoader.get();
  200. if (s != null) {
  201. return s.getBytes(ENCODING);
  202. }
  203. return null;
  204. }
  205. }
  206. private static class DirectoryClearFilter implements DirectoryStream.Filter<Path> {
  207. @Override
  208. public boolean accept(Path entry) throws IOException {
  209. return !LOCK_FNAME.equals(entry.getFileName().toString());
  210. }
  211. }
  212. private static class DirectoryCleanFilter implements DirectoryStream.Filter<Path> {
  213. private long defaultDurationToExpireMs;
  214. DirectoryCleanFilter(long defaultDurationToExpireMs) {
  215. this.defaultDurationToExpireMs = defaultDurationToExpireMs;
  216. }
  217. @Override
  218. public boolean accept(Path entry) throws IOException {
  219. if (LOCK_FNAME.equals(entry.getFileName().toString())) {
  220. return false;
  221. }
  222. return isCacheEntryExpired(entry, defaultDurationToExpireMs);
  223. }
  224. }
  225. private void putCache(String key, byte[] value) throws IOException {
  226. Path cachePath = getCacheEntryPath(key);
  227. Files.write(cachePath, value, CREATE, WRITE, TRUNCATE_EXISTING);
  228. }
  229. private byte[] getCache(String key) throws IOException {
  230. Path cachePath = getCacheEntryPath(key);
  231. if (!validateCacheEntry(cachePath, this.defaultDurationToExpireMs)) {
  232. return null;
  233. }
  234. return Files.readAllBytes(cachePath);
  235. }
  236. private boolean validateCacheEntry(Path cacheEntryPath, long durationToExpireMs) throws IOException {
  237. if (!Files.exists(cacheEntryPath)) {
  238. return false;
  239. }
  240. if (isCacheEntryExpired(cacheEntryPath, durationToExpireMs)) {
  241. logger.debug("cache: expiring entry");
  242. Files.delete(cacheEntryPath);
  243. return false;
  244. }
  245. return true;
  246. }
  247. private static boolean isCacheEntryExpired(Path cacheEntryPath, long durationToExpireMs) throws IOException {
  248. BasicFileAttributes attr = Files.readAttributes(cacheEntryPath, BasicFileAttributes.class);
  249. long modTime = attr.lastModifiedTime().toMillis();
  250. long age = System.currentTimeMillis() - modTime;
  251. if (age > durationToExpireMs) {
  252. return true;
  253. }
  254. return false;
  255. }
  256. private Path getLockPath() {
  257. return baseDir.resolve(LOCK_FNAME);
  258. }
  259. private Path getCacheEntryPath(String key) {
  260. return baseDir.resolve(key);
  261. }
  262. private static String byteArrayToHex(byte[] a) {
  263. StringBuilder sb = new StringBuilder(a.length * 2);
  264. for (byte b : a) {
  265. sb.append(String.format("%02x", b & 0xff));
  266. }
  267. return sb.toString();
  268. }
  269. }