]> source.dussan.org Git - sonarqube.git/commitdiff
Decouple cache invalidation criteria
authorDuarte Meneses <duarte.meneses@sonarsource.com>
Wed, 30 Sep 2015 13:16:56 +0000 (15:16 +0200)
committerDuarte Meneses <duarte.meneses@sonarsource.com>
Thu, 1 Oct 2015 12:34:34 +0000 (14:34 +0200)
sonar-batch/src/test/java/org/sonar/batch/cache/WSLoaderTestWithServer.java
sonar-home/src/main/java/org/sonar/home/cache/PersistentCache.java
sonar-home/src/main/java/org/sonar/home/cache/PersistentCacheBuilder.java
sonar-home/src/main/java/org/sonar/home/cache/PersistentCacheInvalidation.java [new file with mode: 0644]
sonar-home/src/main/java/org/sonar/home/cache/TTLCacheInvalidation.java [new file with mode: 0644]
sonar-home/src/main/java/org/sonar/home/cache/UnlockOnCloseInputStream.java [deleted file]
sonar-home/src/test/java/org/sonar/home/cache/PersistentCacheTest.java
sonar-home/src/test/java/org/sonar/home/cache/TTLCacheInvalidationTest.java [new file with mode: 0644]

index 1781a143f8955f48c5ed75a4a7111d7cc45d6417..def5566c2743d250f47c7a7aa160dc4d62867898 100644 (file)
@@ -21,11 +21,12 @@ package org.sonar.batch.cache;
 
 import static org.mockito.Mockito.mock;
 
+import org.sonar.home.cache.TTLCacheInvalidation;
+
 import org.sonar.batch.bootstrap.GlobalProperties;
 import org.sonar.batch.bootstrap.MockHttpServer;
 import org.sonar.batch.bootstrap.ServerClient;
 import org.sonar.batch.bootstrap.Slf4jLogger;
-
 import org.sonar.batch.cache.WSLoader;
 import org.sonar.batch.cache.WSLoader.LoadStrategy;
 import org.junit.Rule;
@@ -57,7 +58,7 @@ public class WSLoaderTestWithServer {
     when(bootstrapProps.property("sonar.host.url")).thenReturn("http://localhost:" + server.getPort());
 
     client = new ServerClient(bootstrapProps, new EnvironmentInformation("Junit", "4"));
-    cache = new PersistentCache(temp.getRoot().toPath(), 1000 * 60, new Slf4jLogger(), null);
+    cache = new PersistentCache(temp.getRoot().toPath(), new TTLCacheInvalidation(100_000L), new Slf4jLogger(), null);
   }
 
   @After
index 991fadf8cdc7ec093fe0554fd066553147f7f122..210bb20e4bdafa398bb3346a89e0c955cb0e374f 100644 (file)
@@ -28,7 +28,6 @@ import java.nio.file.DirectoryStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
-import java.nio.file.attribute.BasicFileAttributes;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 
@@ -44,20 +43,19 @@ public class PersistentCache {
   private static final Charset ENCODING = StandardCharsets.UTF_8;
   private static final String DIGEST_ALGO = "MD5";
 
-  // eviction strategy is to expire entries after modification once a time duration has elapsed
-  private final long defaultDurationToExpireMs;
+  private final PersistentCacheInvalidation invalidation;
   private final Logger logger;
   private final Path dir;
   private DirectoryLock lock;
 
-  public PersistentCache(Path dir, long defaultDurationToExpireMs, Logger logger, DirectoryLock lock) {
+  public PersistentCache(Path dir, PersistentCacheInvalidation invalidation, Logger logger, DirectoryLock lock) {
     this.dir = dir;
-    this.defaultDurationToExpireMs = defaultDurationToExpireMs;
+    this.invalidation = invalidation;
     this.logger = logger;
     this.lock = lock;
 
     reconfigure();
-    logger.debug("cache: " + dir + ", default expiration time (ms): " + defaultDurationToExpireMs);
+    logger.debug("cache: " + dir);
   }
 
   public synchronized void reconfigure() {
@@ -149,7 +147,7 @@ public class PersistentCache {
     logger.info("cache: clearing");
     try {
       lock();
-      deleteCacheEntries(new DirectoryClearFilter(lock.getFileLockName()));
+      deleteCacheEntries(new DirectoryClearFilter());
     } catch (IOException e) {
       logger.error("Error clearing cache", e);
     } finally {
@@ -164,7 +162,7 @@ public class PersistentCache {
     logger.info("cache: cleaning");
     try {
       lock();
-      deleteCacheEntries(new DirectoryCleanFilter(defaultDurationToExpireMs, lock.getFileLockName()));
+      deleteCacheEntries(new DirectoryCleanFilter());
     } catch (IOException e) {
       logger.error("Error cleaning cache", e);
     } finally {
@@ -203,35 +201,21 @@ public class PersistentCache {
     }
   }
 
-  private static class DirectoryClearFilter implements DirectoryStream.Filter<Path> {
-    private String lockFileName;
-
-    DirectoryClearFilter(String lockFileName) {
-      this.lockFileName = lockFileName;
-    }
-
+  private class DirectoryClearFilter implements DirectoryStream.Filter<Path> {
     @Override
     public boolean accept(Path entry) throws IOException {
-      return !lockFileName.equals(entry.getFileName().toString());
+      return !lock.getFileLockName().equals(entry.getFileName().toString());
     }
   }
 
-  private static class DirectoryCleanFilter implements DirectoryStream.Filter<Path> {
-    private long defaultDurationToExpireMs;
-    private String lockFileName;
-
-    DirectoryCleanFilter(long defaultDurationToExpireMs, String lockFileName) {
-      this.defaultDurationToExpireMs = defaultDurationToExpireMs;
-      this.lockFileName = lockFileName;
-    }
-
+  private class DirectoryCleanFilter implements DirectoryStream.Filter<Path> {
     @Override
     public boolean accept(Path entry) throws IOException {
-      if (lockFileName.equals(entry.getFileName().toString())) {
+      if (lock.getFileLockName().equals(entry.getFileName().toString())) {
         return false;
       }
 
-      return isCacheEntryExpired(entry, defaultDurationToExpireMs);
+      return invalidation.test(entry);
     }
   }
 
@@ -248,7 +232,7 @@ public class PersistentCache {
   private byte[] getCache(String key) throws IOException {
     Path cachePath = getCacheEntryPath(key);
 
-    if (!validateCacheEntry(cachePath, this.defaultDurationToExpireMs)) {
+    if (!validateCacheEntry(cachePath)) {
       return null;
     }
 
@@ -258,7 +242,7 @@ public class PersistentCache {
   private Path getCacheCopy(String key) throws IOException {
     Path cachePath = getCacheEntryPath(key);
 
-    if (!validateCacheEntry(cachePath, this.defaultDurationToExpireMs)) {
+    if (!validateCacheEntry(cachePath)) {
       return null;
     }
 
@@ -267,13 +251,13 @@ public class PersistentCache {
     return temp;
   }
 
-  private boolean validateCacheEntry(Path cacheEntryPath, long durationToExpireMs) throws IOException {
+  private boolean validateCacheEntry(Path cacheEntryPath) throws IOException {
     if (!Files.exists(cacheEntryPath)) {
       return false;
     }
 
-    if (isCacheEntryExpired(cacheEntryPath, durationToExpireMs)) {
-      logger.debug("cache: expiring entry");
+    if (invalidation.test(cacheEntryPath)) {
+      logger.debug("cache: evicting entry");
       Files.delete(cacheEntryPath);
       return false;
     }
@@ -281,19 +265,6 @@ public class PersistentCache {
     return true;
   }
 
-  private static boolean isCacheEntryExpired(Path cacheEntryPath, long durationToExpireMs) throws IOException {
-    BasicFileAttributes attr = Files.readAttributes(cacheEntryPath, BasicFileAttributes.class);
-    long modTime = attr.lastModifiedTime().toMillis();
-
-    long age = System.currentTimeMillis() - modTime;
-
-    if (age > durationToExpireMs) {
-      return true;
-    }
-
-    return false;
-  }
-
   private Path getCacheEntryPath(String key) {
     return dir.resolve(key);
   }
index 51a05b7bf9cc5add6ab887d16d11c1bad441b9be..152f17fc40204088f1ff81b49875044de7afaf6c 100644 (file)
@@ -55,20 +55,20 @@ public class PersistentCacheBuilder {
       .resolve(sanitizeFilename(projectKey));
     return this;
   }
-  
+
   public PersistentCacheBuilder setAreaForGlobal(String serverUrl) {
     relativePath = Paths.get(sanitizeFilename(serverUrl))
       .resolve("global");
     return this;
   }
-  
+
   public PersistentCacheBuilder setAreaForLocalProject(String serverUrl, String serverVersion) {
     relativePath = Paths.get(sanitizeFilename(serverUrl))
       .resolve(sanitizeFilename(serverVersion))
       .resolve("local");
     return this;
   }
-  
+
   public PersistentCacheBuilder setSonarHome(@Nullable Path p) {
     if (p != null) {
       this.cacheBasePath = p.resolve(DIR_NAME);
@@ -77,7 +77,7 @@ public class PersistentCacheBuilder {
   }
 
   public PersistentCache build() {
-    if(relativePath == null) {
+    if (relativePath == null) {
       throw new IllegalStateException("area must be set before building");
     }
     if (cacheBasePath == null) {
@@ -85,7 +85,8 @@ public class PersistentCacheBuilder {
     }
     Path cachePath = cacheBasePath.resolve(relativePath);
     DirectoryLock lock = new DirectoryLock(cacheBasePath, logger);
-    return new PersistentCache(cachePath, DEFAULT_EXPIRE_DURATION, logger, lock);
+    PersistentCacheInvalidation criteria = new TTLCacheInvalidation(DEFAULT_EXPIRE_DURATION);
+    return new PersistentCache(cachePath, criteria, logger, lock);
   }
 
   private static Path findHome() {
diff --git a/sonar-home/src/main/java/org/sonar/home/cache/PersistentCacheInvalidation.java b/sonar-home/src/main/java/org/sonar/home/cache/PersistentCacheInvalidation.java
new file mode 100644 (file)
index 0000000..b2ca92a
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.home.cache;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+public interface PersistentCacheInvalidation {
+  boolean test(Path cacheEntryPath) throws IOException;
+}
diff --git a/sonar-home/src/main/java/org/sonar/home/cache/TTLCacheInvalidation.java b/sonar-home/src/main/java/org/sonar/home/cache/TTLCacheInvalidation.java
new file mode 100644 (file)
index 0000000..bc2ef2d
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.home.cache;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+
+public class TTLCacheInvalidation implements PersistentCacheInvalidation {
+  private final long durationToExpireMs;
+
+  public TTLCacheInvalidation(long durationToExpireMs) {
+    this.durationToExpireMs = durationToExpireMs;
+  }
+  
+  @Override
+  public boolean test(Path cacheEntryPath) throws IOException {
+    BasicFileAttributes attr = Files.readAttributes(cacheEntryPath, BasicFileAttributes.class);
+    long modTime = attr.lastModifiedTime().toMillis();
+
+    long age = System.currentTimeMillis() - modTime;
+
+    if (age > durationToExpireMs) {
+      return true;
+    }
+
+    return false;
+  }
+
+}
diff --git a/sonar-home/src/main/java/org/sonar/home/cache/UnlockOnCloseInputStream.java b/sonar-home/src/main/java/org/sonar/home/cache/UnlockOnCloseInputStream.java
deleted file mode 100644 (file)
index fdfac82..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2014 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * SonarQube is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.home.cache;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-public class UnlockOnCloseInputStream extends InputStream {
-  private final DirectoryLock lock;
-  private final InputStream is;
-
-  public UnlockOnCloseInputStream(InputStream stream, DirectoryLock lock) {
-    this.is = stream;
-    this.lock = lock;
-  }
-
-  @Override
-  public int read() throws IOException {
-    return is.read();
-  }
-
-  @Override
-  public int read(byte[] b) throws IOException {
-    return is.read(b);
-  }
-
-  @Override
-  public int read(byte[] b, int off, int len) throws IOException {
-    return is.read(b, off, len);
-  }
-
-  @Override
-  public long skip(long n) throws IOException {
-    return is.skip(n);
-  }
-
-  @Override
-  public synchronized void mark(int readlimit) {
-    is.mark(readlimit);
-  }
-
-  @Override
-  public synchronized void reset() throws IOException {
-    is.reset();
-  }
-
-  @Override
-  public int available() throws IOException {
-    return is.available();
-  }
-
-  @Override
-  public boolean markSupported() {
-    return is.markSupported();
-  }
-
-  @Override
-  public void close() throws IOException {
-    try {
-      super.close();
-    } finally {
-      lock.unlock();
-    }
-  }
-}
index 5063fbdb22f3357a576d572365fcb75fc80285b8..5bb52298bb6822ba6667c08d13c1cfece525a1a2 100644 (file)
@@ -28,6 +28,8 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 
+import static org.mockito.Matchers.any;
+
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.verify;
@@ -44,15 +46,18 @@ public class PersistentCacheTest {
   private final static String VALUE = "cache content";
   private PersistentCache cache = null;
   private DirectoryLock lock = null;
+  private PersistentCacheInvalidation invalidation = null;
 
   @Rule
   public TemporaryFolder tmp = new TemporaryFolder();
 
   @Before
-  public void setUp() {
+  public void setUp() throws IOException {
+    invalidation = mock(PersistentCacheInvalidation.class);
+    when(invalidation.test(any(Path.class))).thenReturn(false);
     lock = mock(DirectoryLock.class);
     when(lock.getFileLockName()).thenReturn("lock");
-    cache = new PersistentCache(tmp.getRoot().toPath(), Long.MAX_VALUE, mock(Logger.class), lock);
+    cache = new PersistentCache(tmp.getRoot().toPath(), invalidation, mock(Logger.class), lock);
   }
 
   @Test
@@ -67,9 +72,9 @@ public class PersistentCacheTest {
     cache.put(URI, VALUE.getBytes(StandardCharsets.UTF_8));
     Files.write(lockFile, "test".getBytes(StandardCharsets.UTF_8));
     assertCacheHit(true);
-    // negative time to make sure it is expired
-    cache = new PersistentCache(tmp.getRoot().toPath(), -100, mock(Logger.class), lock);
+    when(invalidation.test(any(Path.class))).thenReturn(true);
     cache.clean();
+    when(invalidation.test(any(Path.class))).thenReturn(false);
     assertCacheHit(false);
     // lock file should not get deleted
     assertThat(new String(Files.readAllBytes(lockFile), StandardCharsets.UTF_8)).isEqualTo("test");
@@ -104,7 +109,6 @@ public class PersistentCacheTest {
 
   @Test
   public void testReconfigure() throws Exception {
-    cache = new PersistentCache(tmp.getRoot().toPath(), Long.MAX_VALUE, mock(Logger.class), lock);
     assertCacheHit(false);
     cache.put(URI, VALUE.getBytes(StandardCharsets.UTF_8));
     assertCacheHit(true);
@@ -123,8 +127,7 @@ public class PersistentCacheTest {
 
   @Test
   public void testExpiration() throws Exception {
-    // negative time to make sure it is expired
-    cache = new PersistentCache(tmp.getRoot().toPath(), -100, mock(Logger.class), lock);
+    when(invalidation.test(any(Path.class))).thenReturn(true);
     cache.put(URI, VALUE.getBytes(StandardCharsets.UTF_8));
     assertCacheHit(false);
   }
diff --git a/sonar-home/src/test/java/org/sonar/home/cache/TTLCacheInvalidationTest.java b/sonar-home/src/test/java/org/sonar/home/cache/TTLCacheInvalidationTest.java
new file mode 100644 (file)
index 0000000..92c7e51
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.home.cache;
+
+import org.junit.Before;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Test;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+
+public class TTLCacheInvalidationTest {
+  private Path testFile;
+
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
+
+  @Before
+  public void setUp() throws IOException {
+    testFile = temp.newFile().toPath();
+  }
+
+  @Test
+  public void testExpired() throws IOException {
+    TTLCacheInvalidation invalidation = new TTLCacheInvalidation(-100);
+    assertThat(invalidation.test(testFile)).isEqualTo(true);
+  }
+
+  @Test
+  public void testValid() throws IOException {
+    TTLCacheInvalidation invalidation = new TTLCacheInvalidation(100_000);
+    assertThat(invalidation.test(testFile)).isEqualTo(false);
+  }
+}