Periodically clean outdated session cache files When a user logs out, the corresponding web session cache file is deleted. When the web session expires, however, the file remains in the cache directory. In a busy Gerrit server using this plugin, the size of the cache directory can reach hundreds of megabytes within a few months. Remove outdated session cache files in a periodic way. Cache files are considered as outdated when its expiration date has passed. Change-Id: Id195f2b04ef4b323ce5c5a1c024e62ebeea39720
diff --git a/BUCK b/BUCK index 7f1487c..2260abb 100644 --- a/BUCK +++ b/BUCK
@@ -14,6 +14,7 @@ resources = RESOURCES, manifest_entries = [ 'Gerrit-PluginName: websession-flatfile', + 'Gerrit-Module: com.googlesource.gerrit.plugins.websession.flatfile.Module', 'Gerrit-HttpModule: com.googlesource.gerrit.plugins.websession.flatfile.FlatFileWebSession$Module', 'Implementation-Title: Flat file WebSession', 'Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/websession-flatfile',
diff --git a/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/CleanupInterval.java b/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/CleanupInterval.java new file mode 100644 index 0000000..9874881 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/CleanupInterval.java
@@ -0,0 +1,26 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.websession.flatfile; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.BindingAnnotation; + +import java.lang.annotation.Retention; + +@Retention(RUNTIME) +@BindingAnnotation +public @interface CleanupInterval { +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/FlatFileWebSession.java b/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/FlatFileWebSession.java index 75716eb..e9a2b70 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/FlatFileWebSession.java +++ b/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/FlatFileWebSession.java
@@ -14,7 +14,6 @@ package com.googlesource.gerrit.plugins.websession.flatfile; -import com.google.gerrit.extensions.annotations.PluginName; import com.google.gerrit.extensions.annotations.RootRelative; import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.httpd.CacheBasedWebSession; @@ -23,19 +22,12 @@ import com.google.gerrit.server.AnonymousUser; import com.google.gerrit.server.IdentifiedUser.RequestFactory; import com.google.gerrit.server.config.AuthConfig; -import com.google.gerrit.server.config.PluginConfigFactory; -import com.google.gerrit.server.config.SitePaths; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Provider; -import com.google.inject.Provides; -import com.google.inject.Singleton; import com.google.inject.servlet.RequestScoped; import com.google.inject.servlet.ServletScopes; -import java.nio.file.Path; -import java.nio.file.Paths; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -49,15 +41,6 @@ DynamicItem.bind(binder(), WebSession.class) .to(FlatFileWebSession.class).in(RequestScoped.class); } - - @Provides - @Singleton - @WebSessionDir - Path getWebSessionDir(SitePaths site, PluginConfigFactory cfg, - @PluginName String pluginName) { - return Paths.get(cfg.getFromGerritConfig(pluginName).getString( - "directory", site.site_path + "/websessions")); - } } @Inject
diff --git a/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/FlatFileWebSessionCache.java b/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/FlatFileWebSessionCache.java index 8d12bb2..50acdf0 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/FlatFileWebSessionCache.java +++ b/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/FlatFileWebSessionCache.java
@@ -23,6 +23,7 @@ import com.google.inject.Inject; import com.google.inject.Singleton; +import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -80,7 +81,13 @@ @Override public void cleanUp() { - // do nothing + for (Path path : listFiles()) { + Val val = readFile(path); + DateTime expires = new DateTime(val.getExpiresAt()); + if (expires.isBefore(new DateTime())) { + deleteFile(path); + } + } } @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/FlatFileWebSessionCacheCleaner.java b/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/FlatFileWebSessionCacheCleaner.java new file mode 100644 index 0000000..c6c89a6 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/FlatFileWebSessionCacheCleaner.java
@@ -0,0 +1,68 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.websession.flatfile; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.server.git.WorkQueue; +import com.google.inject.Inject; + +class FlatFileWebSessionCacheCleaner implements Runnable { + + static class CleanerLifecycle implements LifecycleListener { + private static final int INITIAL_DELAY_MS = 1000; + private final WorkQueue queue; + private final FlatFileWebSessionCacheCleaner cleaner; + private final long cleanupInterval; + + @Inject + CleanerLifecycle( + WorkQueue queue, + FlatFileWebSessionCacheCleaner cleaner, + @CleanupInterval long cleanupInterval) { + this.queue = queue; + this.cleaner = cleaner; + this.cleanupInterval = cleanupInterval; + } + + @Override + public void start() { + queue.getDefaultQueue().scheduleAtFixedRate(cleaner, INITIAL_DELAY_MS, + cleanupInterval, MILLISECONDS); + } + + @Override + public void stop() { + } + } + + private FlatFileWebSessionCache flatFileWebSessionCache; + + @Inject + FlatFileWebSessionCacheCleaner(FlatFileWebSessionCache flatFileWebSessionCache) { + this.flatFileWebSessionCache = flatFileWebSessionCache; + } + + @Override + public void run() { + flatFileWebSessionCache.cleanUp(); + } + + @Override + public String toString() { + return "FlatFile WebSession Cleaner"; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/Module.java b/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/Module.java new file mode 100644 index 0000000..a5ea0c3 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/websession/flatfile/Module.java
@@ -0,0 +1,61 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.websession.flatfile; + +import static java.util.concurrent.TimeUnit.HOURS; + +import com.google.common.base.Strings; +import com.google.gerrit.extensions.annotations.PluginName; +import com.google.gerrit.lifecycle.LifecycleModule; +import com.google.gerrit.server.config.ConfigUtil; +import com.google.gerrit.server.config.PluginConfigFactory; +import com.google.gerrit.server.config.SitePaths; +import com.google.inject.Provides; +import com.google.inject.Singleton; + +import com.googlesource.gerrit.plugins.websession.flatfile.FlatFileWebSessionCacheCleaner.CleanerLifecycle; + +import java.nio.file.Path; +import java.nio.file.Paths; + +public class Module extends LifecycleModule { + + private static final int DEFAULT_CLEANUP_INTERVAL = 24; + + @Override + protected void configure() { + listener().to(CleanerLifecycle.class); + } + + @Provides + @Singleton + @WebSessionDir + Path getWebSessionDir(SitePaths site, PluginConfigFactory cfg, + @PluginName String pluginName) { + return Paths.get(cfg.getFromGerritConfig(pluginName).getString("directory", + site.site_path + "/websessions")); + } + + @Provides + @Singleton + @CleanupInterval + Long getCleanupInterval(PluginConfigFactory cfg, @PluginName String pluginName) { + String fromConfig = + Strings.nullToEmpty(cfg.getFromGerritConfig(pluginName).getString( + "cleanupInterval")); + return HOURS.toMillis(ConfigUtil.getTimeUnit(fromConfig, + DEFAULT_CLEANUP_INTERVAL, HOURS)); + } +}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md index 09a49d5..cc5e71f 100644 --- a/src/main/resources/Documentation/config.md +++ b/src/main/resources/Documentation/config.md
@@ -8,8 +8,10 @@ main Gerrit config file: $site_dir/etc/gerrit.config. This location defaults to $site_dir/websessions. +``` [plugin "@PLUGIN@"] directory = <disk_cache_directory> + # NOTE: <disk_cache_directory> can be any location on the # shared filesystem that can be accessed by all servers, # and in which rename operations are atomic and allow @@ -19,6 +21,33 @@ Reload the plugin on each master for the changes to take effect. +The plugin periodically cleans up the cache directory, deleting +files corresponding to expired sessions. The frequency of this +operation can be specified in the configuration. For example: + +``` + [plugin "@PLUGIN@"] + cleanupInterval = 1h +``` + +indicates the cleanup operation to be triggered every hour. + +Values should use common time unit suffixes to express their setting: + +* s, sec, second, seconds +* m, min, minute, minutes +* h, hr, hour, hours +* d, day, days +* w, week, weeks (`1 week` is treated as `7 days`) +* mon, month, months (`1 month` is treated as `30 days`) +* y, year, years (`1 year` is treated as `365 days`) + +If a time unit suffix is not specified, `hours` is assumed. + +If 'cleanupInterval' is not present in the configuration, the +cleanup operation is triggered every 24 hours. + + SEE ALSO --------
diff --git a/src/test/java/com/googlesource/gerrit/plugins/websession/flatfile/FlatFileWebSessionCacheTest.java b/src/test/java/com/googlesource/gerrit/plugins/websession/flatfile/FlatFileWebSessionCacheTest.java index 1eb7200..038135a 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/websession/flatfile/FlatFileWebSessionCacheTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/websession/flatfile/FlatFileWebSessionCacheTest.java
@@ -84,11 +84,19 @@ } @Test + public void cleanUpTest() throws Exception { + loadExistingKeyToCacheDir(); + flatFileWebSessionCache.cleanUp(); + assertThat(isDirEmpty(dir)).isTrue(); + } + + @Test public void getAllPresentTest() throws Exception { Files.createFile(dir.resolve(key)); loadExistingKeyToCacheDir(); List<String> keys = Arrays.asList(new String[] {key, existingKey}); - assertThat(flatFileWebSessionCache.getAllPresent(keys)).containsKey(existingKey); + assertThat(flatFileWebSessionCache.getAllPresent(keys)) + .containsKey(existingKey); } @Test @@ -116,13 +124,15 @@ return null; } } - assertThat(flatFileWebSessionCache.get(existingKey, new ValueLoader())).isNull(); + assertThat(flatFileWebSessionCache.get(existingKey, new ValueLoader())) + .isNull(); loadExistingKeyToCacheDir(); - assertThat(flatFileWebSessionCache.get(existingKey, new ValueLoader())).isNotNull(); + assertThat(flatFileWebSessionCache.get(existingKey, new ValueLoader())) + .isNotNull(); } - @Test(expected=ExecutionException.class) + @Test(expected = ExecutionException.class) public void getTestCallableThrowsException() throws Exception { class ValueLoader implements Callable<Val> { @Override @@ -130,7 +140,8 @@ throw new Exception(); } } - assertThat(flatFileWebSessionCache.get(existingKey, new ValueLoader())).isNull(); + assertThat(flatFileWebSessionCache.get(existingKey, new ValueLoader())) + .isNull(); } @Test @@ -166,10 +177,10 @@ @Test public void putTest() throws Exception { loadExistingKeyToCacheDir(); - Val val = flatFileWebSessionCache.getIfPresent(existingKey); - String newKey = "abcde12345"; - flatFileWebSessionCache.put(newKey, val); - assertThat(flatFileWebSessionCache.getIfPresent(newKey)).isNotNull(); + Val val = flatFileWebSessionCache.getIfPresent(existingKey); + String newKey = "abcde12345"; + flatFileWebSessionCache.put(newKey, val); + assertThat(flatFileWebSessionCache.getIfPresent(newKey)).isNotNull(); } @Test