Permalink
Browse files

Back in-memory caches with Guava, disk caches with H2

Instead of using Ehcache for in-memory caches, use Guava. The Guava
cache code has been more completely tested by Google in high load
production environments, and it tends to have fewer bugs. It enables
caches to be built at any time, rather than only at server startup.

By creating a Guava cache as soon as it is declared, rather than
during the LifecycleListener.start() for the CachePool, we can promise
any downstream consumer of the cache that the cache is ready to
execute requests the moment it is supplied by Guice. This fixes a
startup ordering problem in the GroupCache and the ProjectCache, where
code wants to use one of these caches during startup to resolve a
group or project by name.

Tracking the Gauva backend caches with a DynamicMap makes it possible
for plugins to define their own in-memory caches using CacheModule's
cache() function to declare the cache. It allows the core server to
make the cache available to administrators over SSH with the gerrit
show-caches and gerrit flush-caches commands.

Persistent caches store in a private H2 database per cache, with a
simple one-table schema that stores each entry in a table row as a
pair of serialized objects (key and value). Database reads are gated
by a BloomFilter, to reduce the number of calls made to H2 during
cache misses. In theory less than 3% of cache misses will reach H2 and
find nothing. Stores happen on a background thread quickly after the
put is made to the cache, reducing the risk that a diff or web_session
record is lost during an ungraceful shutdown.

Cache databases are capped around 128M worth of stored data by running
a prune cycle each day at 1 AM local server time. Records are removed
from the database by ordering on the last access time, where last
accessed is the last time the record was moved from disk to memory.

Change-Id: Ia82d056796b5af9bcb1f219fe06d905c9c0fbc84
  • Loading branch information...
1 parent 34d4d19 commit 2e1cb2b8495761a7e9abc09ebca24ca6bbf2afbe @spearce spearce committed May 24, 2012
Showing with 2,420 additions and 1,662 deletions.
  1. +45 −45 Documentation/config-gerrit.txt
  2. +1 −1 Documentation/licenses.txt
  3. +39 −0 ReleaseNotes/ReleaseNotes-2.5.txt
  4. +2 −2 {gerrit-ehcache → gerrit-cache-h2}/.gitignore
  5. +2 −1 {gerrit-ehcache → gerrit-cache-h2}/.settings/org.eclipse.core.resources.prefs
  6. 0 {gerrit-ehcache → gerrit-cache-h2}/.settings/org.eclipse.core.runtime.prefs
  7. +1 −1 {gerrit-ehcache → gerrit-cache-h2}/.settings/org.eclipse.jdt.core.prefs
  8. 0 {gerrit-ehcache → gerrit-cache-h2}/.settings/org.eclipse.jdt.ui.prefs
  9. +14 −9 {gerrit-ehcache → gerrit-cache-h2}/pom.xml
  10. +120 −0 gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
  11. +198 −0 gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
  12. +709 −0 gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
  13. +0 −272 gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java
  14. +0 −114 gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java
  15. +0 −81 gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java
  16. +3 −10 gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
  17. +9 −9 gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
  18. +10 −20 gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
  19. +12 −0 gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
  20. +10 −115 gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
  21. +40 −0 gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java
  22. +45 −0 gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.java
  23. +8 −8 ...tionPolicy.java → gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java
  24. +66 −0 gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java
  25. +13 −10 gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
  26. +8 −6 gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
  27. +2 −2 gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
  28. +2 −3 gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
  29. +0 −2 gerrit-plugin-api/pom.xml
  30. +5 −0 gerrit-server/pom.xml
  31. +5 −2 gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
  32. +29 −34 gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
  33. +62 −37 gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
  34. +139 −147 gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
  35. +33 −22 gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
  36. +11 −10 gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
  37. +39 −32 gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
  38. +0 −35 gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java
  39. +46 −0 gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java
  40. +121 −60 gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
  41. +108 −82 gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
  42. +0 −48 gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java
  43. +0 −40 gerrit-server/src/main/java/com/google/gerrit/server/cache/EntryCreator.java
  44. +10 −5 ...src/main/java/com/google/gerrit/server/cache/{UnnamedCacheBinding.java → MemoryCacheFactory.java}
  45. +0 −35 gerrit-server/src/main/java/com/google/gerrit/server/cache/NamedCacheBinding.java
  46. +11 −3 ...erver/src/main/java/com/google/gerrit/server/cache/{CachePool.java → PersistentCacheFactory.java}
  47. +0 −40 gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyCache.java
  48. +4 −1 gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
  49. +13 −9 gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
  50. +11 −19 gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
  51. +9 −44 gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
  52. +9 −0 gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
  53. +3 −3 gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
  54. +9 −1 gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
  55. +3 −0 gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
  56. +5 −6 gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
  57. +28 −0 gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWeigher.java
  58. +3 −2 gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
  59. +32 −30 gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
  60. +16 −0 gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
  61. +8 −4 gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
  62. +8 −4 .../gerrit/server/{git/IncompleteUserInfoException.java → patch/PatchListNotAvailableException.java}
  63. +30 −0 gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java
  64. +64 −42 gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
  65. +3 −6 gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
  66. +9 −1 gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
  67. +5 −5 gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
  68. +1 −1 gerrit-sshd/pom.xml
  69. +21 −18 gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
  70. +0 −3 gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
  71. +14 −18 gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
  72. +13 −9 gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
  73. +112 −80 gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
  74. +2 −2 gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
  75. +0 −4 gerrit-war/src/main/resources/log4j.properties
  76. +7 −7 pom.xml
View
90 Documentation/config-gerrit.txt
@@ -354,8 +354,8 @@ Default is unset, no disk cache.
[[cache.name.maxAge]]cache.<name>.maxAge::
+
-Maximum age to keep an entry in the cache. If an entry has not
-been accessed in this period of time, it is removed from the cache.
+Maximum age to keep an entry in the cache. Entries are removed from
+the cache and refreshed from source data every maxAge interval.
Values should use common unit suffixes to express their setting:
+
* s, sec, second, seconds
@@ -371,41 +371,50 @@ If a unit suffix is not specified, `minutes` is assumed. If 0 is
supplied, the maximum age is infinite and items are never purged
except when the cache is full.
+
-Default is `90 days` for most caches, except:
+Default is `0`, meaning store forever with no expire, except:
+
* `"adv_bases"`: default is `10 minutes`
* `"ldap_groups"`: default is `1 hour`
* `"web_sessions"`: default is `12 hours`
[[cache.name.memoryLimit]]cache.<name>.memoryLimit::
+
-Maximum number of cache items to retain in memory. Keep in mind
-this is total number of items, not bytes of heap used.
+The total cost of entries to retain in memory. The cost computation
+varies by the cache. For most caches where the in-memory size of each
+entry is relatively the same, memoryLimit is currently defined to be
+the number of entries held by the cache (each entry costs 1).
++
+For caches where the size of an entry can vary significantly between
+individual entries (notably `"diff"`, `"diff_intraline"`), memoryLimit
+is an approximation of the total number of bytes stored by the cache.
+Larger entries that represent bigger patch sets or longer source files
+will consume a bigger portion of the memoryLimit. For these caches the
+memoryLimit should be set to roughly the amount of RAM (in bytes) the
+administrator can dedicate to the cache.
+
Default is 1024 for most caches, except:
+
* `"adv_bases"`: default is `4096`
-* `"diff"`: default is `128`
-* `"diff_intraline"`: default is `128`
+* `"diff"`: default is `10m` (10 MiB of memory)
+* `"diff_intraline"`: default is `10m` (10 MiB of memory)
+* `"plugin_resources"`: default is 2m (2 MiB of memory)
-[[cache.name.diskLimit]]cache.<name>.diskLimit::
-+
-Maximum number of cache items to retain on disk, if this cache
-supports storing its items to disk. Like memoryLimit, this is
-total number of items, not bytes of disk used. If 0, disk storage
-for this cache is disabled.
+
-Default is 16384.
+If set to 0 the cache is disabled. Entries are removed immediately
+after being stored by the cache. This is primarily useful for testing.
-[[cache.name.diskBuffer]]cache.<name>.diskBuffer::
+[[cache.name.diskLimit]]cache.<name>.diskLimit::
+
-Number of bytes to buffer in memory before writing less frequently
-accessed cache items to disk, if this cache supports storing its
-items to disk.
+Total size in bytes of the keys and values stored on disk. Caches that
+have grown bigger than this size are scanned daily at 1 AM local
+server time to trim the cache. Entries are removed in least recently
+accessed order until the cache fits within this limit. Caches may
+grow larger than this during the day, as the size check is only
+performed once every 24 hours.
+
-Default is 5 MiB.
+Default is 128 MiB per cache.
+
-Common unit suffixes of 'k', 'm', or 'g' are supported.
+If 0, disk storage for the cache is disabled.
[[cache_names]]Standard Caches
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -447,29 +456,21 @@ Each item caches the differences between two commits, at both the
directory and file levels. Gerrit uses this cache to accelerate
the display of affected file names, as well as file contents.
+
-Entries in this cache are relatively large, so the memory limit
-should not be set incredibly high. Administrators should try to
-target cache.diff.memoryLimit to be roughly the number of changes
-which their users will process in a 1 or 2 day span.
-+
-Keeping entries for 90 days gives sufficient time for most changes
-to be submitted or abandoned before their relevant difference items
-expire out.
+Entries in this cache are relatively large, so memoryLimit is an
+estimate in bytes of memory used. Administrators should try to target
+cache.diff.memoryLimit to fit all changes users will view in a 1 or 2
+day span.
cache `"diff_intraline"`::
+
Each item caches the intraline difference of one file, when compared
between two commits. Gerrit uses this cache to accelerate display of
intraline differences when viewing a file.
+
-Entries in this cache are relatively large, so the memory limit
-should not be set incredibly high. Administrators should try to
-target cache.diff.memoryLimit to be roughly the number of changes
-which their users will process in a 1 or 2 day span.
-+
-Keeping entries for 90 days gives sufficient time for most changes
-to be submitted or abandoned before their relevant difference items
-expire out.
+Entries in this cache are relatively large, so memoryLimit is an
+estimate in bytes of memory used. Administrators should try to target
+cache.diff.memoryLimit to fit all files users will view in a 1 or 2
+day span.
cache `"git_tags"`::
+
@@ -517,6 +518,12 @@ reference. Sorting the sections can be expensive when regular
expressions are used, so this cache remembers the ordering for
each branch.
+cache `"plugin_resources"`::
++
+Caches formatted plugin resources, such as plugin documentation that
+has been converted from Markdown to HTML. The memoryLimit refers to
+the bytes of memory dedicated to storing the documentation.
+
cache `"projects"`::
+
Caches the project description records, from the `projects` table
@@ -550,8 +557,8 @@ and need to sign-in again after the restart, as the cache was
unable to persist the session information. Enabling a disk cache
is strongly recommended.
+
-Session storage is relatively inexpensive, the average entry in
-this cache is approximately 248 bytes, depending on the JVM.
+Session storage is relatively inexpensive. The average entry in
+this cache is approximately 346 bytes.
See also link:cmd-flush-caches.html[gerrit flush-caches].
@@ -598,13 +605,6 @@ configuration.
+
Default is true, enabled.
-cache.plugin_resources.memoryLimit::
-+
-Number of bytes of memory to use to cache formatted plugin resources,
-such as plugin documentation that has been converted from Markdown to
-HTML. Default is 2 MiB. Common unit suffixes of 'k', 'm', or 'g' are
-supported.
-
cache.projects.checkFrequency::
+
How often project configuration should be checked for update from Git.
View
2 Documentation/licenses.txt
@@ -18,6 +18,7 @@ Included Components
|Google Gson | <<apache2,Apache License 2.0>>
|Google Web Toolkit | <<apache2,Apache License 2.0>>
|Guice | <<apache2,Apache License 2.0>>
+|Guava Libraries | <<apache2,Apache License 2.0>>
|Apache Commons Codec | <<apache2,Apache License 2.0>>
|Apache Commons DBCP | <<apache2,Apache License 2.0>>
|Apache Commons Http Client | <<apache2,Apache License 2.0>>
@@ -33,7 +34,6 @@ Included Components
|Apache Xerces | <<apache2,Apache License 2.0>>
|OpenID4Java | <<apache2,Apache License 2.0>>
|Neko HTML | <<apache2,Apache License 2.0>>
-|Ehcache | <<apache2,Apache License 2.0>>
|mime-util | <<apache2,Apache License 2.0>>
|Jetty | <<apache2,Apache License 2.0>>, or link:http://www.eclipse.org/legal/epl-v10.html[EPL]
|Prolog Cafe | <<prolog_cafe,EPL or GPL>>
View
39 ReleaseNotes/ReleaseNotes-2.5.txt
@@ -14,3 +14,42 @@ Replication
Gerrit 2.5 no longer includes replication support out of the box.
Servers that reply upon `replication.config` to copy Git repository
data to other locations must also install the replication plugin.
+
+Cache Configuration
+~~~~~~~~~~~~~~~~~~~
+
+Disk caches are now backed by individual H2 databases, rather than
+Ehcache's own private format. Administrators are encouraged to clear
+the `'$site_path'/cache` directory before starting the new server.
+
+The `cache.NAME.diskLimit` configuration variable is now expressed in
+bytes of disk used. This is a change from previous versions of Gerrit,
+which expressed the limit as the number of entries rather than bytes.
+Bytes of disk is a more accurate way to size what is held. Admins that
+set this variable must update their configurations, as the old values
+are too small. For example a setting of `diskLimit = 65535` will only
+store 64 KiB worth of data on disk and can no longer hold 65,000 patch
+sets. It is recommended to delete the diskLimit variable (if set) and
+rely on the built-in default of `128m`.
+
+The `cache.diff.memoryLimit` and `cache.diff_intraline.memoryLimit`
+configuration variables are now expressed in bytes of memory used,
+rather than number of entries in the cache. This is a change from
+previous versions of Gerrit and gives administrators more control over
+how memory is partioned within a server. Admins that set this variable
+must update their configurations, as the old values are too small.
+For example a setting of `memoryLimit = 1024` now means only 1 KiB of
+data (which may not even hold 1 patch set), not 1024 patch sets. It
+is recommended to set these to `10m` for 10 MiB of memory, and
+increase as necessary.
+
+The `cache.NAME.maxAge` variable now means the maximum amount of time
+that can elapse between reads of the source data into the cache, no
+matter how often it is being accessed. In prior versions it meant how
+long an item could be held without being requested by a client before
+it was discarded. The new meaning of elapsed time before consulting
+the source data is more useful, as it enables a strict bound on how
+stale the cached data can be. This is especially useful for slave
+servers account and permission data, or the `ldap_groups` cache, where
+updates are often made to the source without telling Gerrit to reload
+the cache.
View
4 gerrit-ehcache/.gitignore → gerrit-cache-h2/.gitignore
@@ -1,6 +1,6 @@
/target
/.classpath
/.project
-/.settings/org.eclipse.m2e.core.prefs
/.settings/org.maven.ide.eclipse.prefs
-/gerrit-ehcache.iml
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-cache-h2.iml
View
3 ...settings/org.eclipse.core.resources.prefs → ...settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,5 @@
-#Tue May 15 09:21:09 PDT 2012
+#Thu Jul 28 11:02:36 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
+encoding//src/test/java=UTF-8
encoding/<project>=UTF-8
View
0 .../.settings/org.eclipse.core.runtime.prefs → .../.settings/org.eclipse.core.runtime.prefs
File renamed without changes.
View
2 ...ache/.settings/org.eclipse.jdt.core.prefs → ...e-h2/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Thu Jan 19 12:55:44 PST 2012
+#Thu Jul 28 11:02:36 PDT 2011
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
View
0 ...hcache/.settings/org.eclipse.jdt.ui.prefs → ...che-h2/.settings/org.eclipse.jdt.ui.prefs
File renamed without changes.
View
23 gerrit-ehcache/pom.xml → gerrit-cache-h2/pom.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
-Copyright (C) 2010 The Android Open Source Project
+Copyright (C) 2012 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.
@@ -25,23 +25,28 @@ limitations under the License.
<version>2.5-SNAPSHOT</version>
</parent>
- <artifactId>gerrit-ehcache</artifactId>
- <name>Gerrit Code Review - Ehcache Bindings</name>
+ <artifactId>gerrit-cache-h2</artifactId>
+ <name>Gerrit Code Review - Guava + H2 caching</name>
<description>
- Bindings to Ehcache
+ Implementation of caching backed by Guava and H2
</description>
<dependencies>
<dependency>
- <groupId>net.sf.ehcache</groupId>
- <artifactId>ehcache-core</artifactId>
- </dependency>
-
- <dependency>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-server</artifactId>
<version>${project.version}</version>
</dependency>
+
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.h2database</groupId>
+ <artifactId>h2</artifactId>
+ </dependency>
</dependencies>
</project>
View
120 gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
@@ -0,0 +1,120 @@
+// Copyright (C) 2012 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.google.gerrit.server.cache.h2;
+
+import com.google.common.base.Strings;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Weigher;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.cache.CacheBinding;
+import com.google.gerrit.server.cache.MemoryCacheFactory;
+import com.google.gerrit.server.cache.PersistentCacheFactory;
+import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.concurrent.TimeUnit;
+
+public class DefaultCacheFactory implements MemoryCacheFactory {
+ public static class Module extends LifecycleModule {
+ @Override
+ protected void configure() {
+ bind(DefaultCacheFactory.class);
+ bind(MemoryCacheFactory.class).to(DefaultCacheFactory.class);
+ bind(PersistentCacheFactory.class).to(H2CacheFactory.class);
+ listener().to(H2CacheFactory.class);
+ }
+ }
+
+ private final Config cfg;
+
+ @Inject
+ public DefaultCacheFactory(@GerritServerConfig Config config) {
+ this.cfg = config;
+ }
+
+ @Override
+ public <K, V> Cache<K, V> build(CacheBinding<K, V> def) {
+ return create(def, false).build();
+ }
+
+ @Override
+ public <K, V> LoadingCache<K, V> build(
+ CacheBinding<K, V> def,
+ CacheLoader<K, V> loader) {
+ return create(def, false).build(loader);
+ }
+
+ @SuppressWarnings("unchecked")
+ <K, V> CacheBuilder<K, V> create(
+ CacheBinding<K, V> def,
+ boolean unwrapValueHolder) {
+ CacheBuilder<K,V> builder = newCacheBuilder();
+ builder.maximumWeight(cfg.getLong(
+ "cache", def.name(), "memoryLimit",
+ def.maximumWeight()));
+
+ Weigher<K, V> weigher = def.weigher();
+ if (weigher != null && unwrapValueHolder) {
+ final Weigher<K, V> impl = weigher;
+ weigher = (Weigher<K, V>) new Weigher<K, ValueHolder<V>> () {
+ @Override
+ public int weigh(K key, ValueHolder<V> value) {
+ return impl.weigh(key, value.value);
+ }
+ };
+ } else if (weigher == null) {
+ weigher = unitWeight();
+ }
+ builder.weigher(weigher);
+
+ Long age = def.expireAfterWrite(TimeUnit.SECONDS);
+ if (has(def.name(), "maxAge")) {
+ builder.expireAfterWrite(ConfigUtil.getTimeUnit(cfg,
+ "cache", def.name(), "maxAge",
+ age != null ? age : 0,
+ TimeUnit.SECONDS), TimeUnit.SECONDS);
+ } else if (age != null) {
+ builder.expireAfterWrite(age, TimeUnit.SECONDS);
+ }
+
+ return builder;
+ }
+
+ private boolean has(String name, String var) {
+ return !Strings.isNullOrEmpty(cfg.getString("cache", name, var));
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private static <K, V> CacheBuilder<K, V> newCacheBuilder() {
+ CacheBuilder builder = CacheBuilder.newBuilder();
+ return builder;
+ }
+
+ private static <K, V> Weigher<K, V> unitWeight() {
+ return new Weigher<K, V>() {
+ @Override
+ public int weigh(K key, V value) {
+ return 1;
+ }
+ };
+ }
+}
View
198 gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -0,0 +1,198 @@
+// Copyright (C) 2012 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.google.gerrit.server.cache.h2;
+
+import com.google.common.base.Preconditions;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.cache.CacheBinding;
+import com.google.gerrit.server.cache.PersistentCacheFactory;
+import com.google.gerrit.server.cache.h2.H2CacheImpl.SqlStore;
+import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+class H2CacheFactory implements PersistentCacheFactory, LifecycleListener {
+ static final Logger log = LoggerFactory.getLogger(H2CacheFactory.class);
+
+ private final DefaultCacheFactory defaultFactory;
+ private final Config config;
+ private final File cacheDir;
+ private final List<H2CacheImpl<?, ?>> caches;
+ private final ExecutorService executor;
+ private final ScheduledExecutorService cleanup;
+ private volatile boolean started;
+
+ @Inject
+ H2CacheFactory(
+ DefaultCacheFactory defaultCacheFactory,
+ @GerritServerConfig Config cfg,
+ SitePaths site) {
+ defaultFactory = defaultCacheFactory;
+ config = cfg;
+
+ File loc = site.resolve(cfg.getString("cache", null, "directory"));
+ if (loc == null) {
+ cacheDir = null;
+ } else if (loc.exists() || loc.mkdirs()) {
+ if (loc.canWrite()) {
+ log.info("Enabling disk cache " + loc.getAbsolutePath());
+ cacheDir = loc;
+ } else {
+ log.warn("Can't write to disk cache: " + loc.getAbsolutePath());
+ cacheDir = null;
+ }
+ } else {
+ log.warn("Can't create disk cache: " + loc.getAbsolutePath());
+ cacheDir = null;
+ }
+
+ caches = Lists.newLinkedList();
+
+ if (cacheDir != null) {
+ executor = Executors.newFixedThreadPool(
+ 1,
+ new ThreadFactoryBuilder()
+ .setNameFormat("DiskCache-Store-%d")
+ .build());
+ cleanup = Executors.newScheduledThreadPool(
+ 1,
+ new ThreadFactoryBuilder()
+ .setNameFormat("DiskCache-Prune-%d")
+ .setDaemon(true)
+ .build());
+ } else {
+ executor = null;
+ cleanup = null;
+ }
+ }
+
+ @Override
+ public void start() {
+ started = true;
+ if (executor != null) {
+ for (final H2CacheImpl<?, ?> cache : caches) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ cache.start();
+ }
+ });
+
+ cleanup.schedule(new Runnable() {
+ @Override
+ public void run() {
+ cache.prune(cleanup);
+ }
+ }, 30, TimeUnit.SECONDS);
+ }
+ }
+ }
+
+ @Override
+ public void stop() {
+ if (executor != null) {
+ try {
+ cleanup.shutdownNow();
+
+ List<Runnable> pending = executor.shutdownNow();
+ if (executor.awaitTermination(15, TimeUnit.MINUTES)) {
+ if (pending != null && !pending.isEmpty()) {
+ log.info(String.format("Finishing %d disk cache updates", pending.size()));
+ for (Runnable update : pending) {
+ update.run();
+ }
+ }
+ } else {
+ log.info("Timeout waiting for disk cache to close");
+ }
+ } catch (InterruptedException e) {
+ log.warn("Interrupted waiting for disk cache to shutdown");
+ }
+ }
+ for (H2CacheImpl<?, ?> cache : caches) {
+ cache.stop();
+ }
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes", "cast"})
+ @Override
+ public <K, V> Cache<K, V> build(CacheBinding<K, V> def) {
+ Preconditions.checkState(!started, "cache must be built before start");
+ long limit = config.getLong("cache", def.name(), "diskLimit", 128 << 20);
+
+ if (cacheDir == null || limit <= 0) {
+ return defaultFactory.build(def);
+ }
+
+ SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit);
+ H2CacheImpl<K, V> cache = new H2CacheImpl<K, V>(
+ executor, store, def.keyType(),
+ (Cache<K, ValueHolder<V>>) defaultFactory.create(def, true).build());
+ caches.add(cache);
+ return cache;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <K, V> LoadingCache<K, V> build(
+ CacheBinding<K, V> def,
+ CacheLoader<K, V> loader) {
+ Preconditions.checkState(!started, "cache must be built before start");
+ long limit = config.getLong("cache", def.name(), "diskLimit", 128 << 20);
+
+ if (cacheDir == null || limit <= 0) {
+ return defaultFactory.build(def, loader);
+ }
+
+ SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit);
+ Cache<K, ValueHolder<V>> mem = (Cache<K, ValueHolder<V>>)
+ defaultFactory.create(def, true)
+ .build((CacheLoader<K, V>) new H2CacheImpl.Loader<K, V>(
+ executor, store, loader));
+ H2CacheImpl<K, V> cache = new H2CacheImpl<K, V>(
+ executor, store, def.keyType(), mem);
+ caches.add(cache);
+ return cache;
+ }
+
+ private <V, K> SqlStore<K, V> newSqlStore(
+ String name,
+ TypeLiteral<K> keyType,
+ long maxSize) {
+ File db = new File(cacheDir, name).getAbsoluteFile();
+ String url = "jdbc:h2:" + db.toURI().toString();
+ return new SqlStore<K, V>(url, keyType, maxSize);
+ }
+}
View
709 gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -0,0 +1,709 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.gerrit.server.cache.h2;
+
+import com.google.common.cache.AbstractLoadingCache;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.CacheStats;
+import com.google.common.cache.LoadingCache;
+import com.google.common.hash.BloomFilter;
+import com.google.common.hash.Funnel;
+import com.google.common.hash.Funnels;
+import com.google.common.hash.PrimitiveSink;
+import com.google.inject.TypeLiteral;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Map;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Hybrid in-memory and database backed cache built on H2.
+ * <p>
+ * This cache can be used as either a recall cache, or a loading cache if a
+ * CacheLoader was supplied to its constructor at build time. Before creating an
+ * entry the in-memory cache is checked for the item, then the database is
+ * checked, and finally the CacheLoader is used to construct the item. This is
+ * mostly useful for CacheLoaders that are computationally intensive, such as
+ * the PatchListCache.
+ * <p>
+ * Cache stores and invalidations are performed on a background thread, hiding
+ * the latency associated with serializing the key and value pairs and writing
+ * them to the database log.
+ * <p>
+ * A BloomFilter is used around the database to reduce the number of SELECTs
+ * issued against the database for new cache items that have not been seen
+ * before, a common operation for the PatchListCache. The BloomFilter is sized
+ * when the cache starts to be 64,000 entries or double the number of items
+ * currently in the database table.
+ * <p>
+ * This cache does not export its items as a ConcurrentMap.
+ *
+ * @see H2CacheFactory
+ */
+public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> {
+ private static final Logger log = LoggerFactory.getLogger(H2CacheImpl.class);
+
+ private final Executor executor;
+ private final SqlStore<K, V> store;
+ private final TypeLiteral<K> keyType;
+ private final Cache<K, ValueHolder<V>> mem;
+
+ H2CacheImpl(Executor executor,
+ SqlStore<K, V> store,
+ TypeLiteral<K> keyType,
+ Cache<K, ValueHolder<V>> mem) {
+ this.executor = executor;
+ this.store = store;
+ this.keyType = keyType;
+ this.mem = mem;
+ }
+
+ @Override
+ public V getIfPresent(Object objKey) {
+ if (!keyType.getRawType().isInstance(objKey)) {
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ K key = (K) objKey;
+
+ ValueHolder<V> h = mem.getIfPresent(key);
+ if (h != null) {
+ return h.value;
+ }
+
+ if (store.mightContain(key)) {
+ h = store.getIfPresent(key);
+ if (h != null) {
+ mem.put(key, h);
+ return h.value;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public V get(K key) throws ExecutionException {
+ if (mem instanceof LoadingCache) {
+ return ((LoadingCache<K, ValueHolder<V>>) mem).get(key).value;
+ }
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void put(final K key, V val) {
+ final ValueHolder<V> h = new ValueHolder<V>(val);
+ h.created = System.currentTimeMillis();
+ mem.put(key, h);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ store.put(key, h);
+ }
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void invalidate(final Object key) {
+ if (keyType.getRawType().isInstance(key) && store.mightContain((K) key)) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ store.invalidate((K) key);
+ }
+ });
+ }
+ mem.invalidate(key);
+ }
+
+ @Override
+ public void invalidateAll() {
+ store.invalidateAll();
+ mem.invalidateAll();
+ }
+
+ @Override
+ public long size() {
+ return mem.size();
+ }
+
+ @Override
+ public CacheStats stats() {
+ return mem.stats();
+ }
+
+ public DiskStats diskStats() {
+ return store.diskStats();
+ }
+
+ void start() {
+ store.open();
+ }
+
+ void stop() {
+ for (Map.Entry<K, ValueHolder<V>> e : mem.asMap().entrySet()) {
+ ValueHolder<V> h = e.getValue();
+ if (!h.clean) {
+ store.put(e.getKey(), h);
+ }
+ }
+ store.close();
+ }
+
+ void prune(final ScheduledExecutorService service) {
+ store.prune(mem);
+
+ Calendar cal = Calendar.getInstance();
+ cal.set(Calendar.HOUR_OF_DAY, 01);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ cal.add(Calendar.DAY_OF_MONTH, 1);
+
+ long delay = cal.getTimeInMillis() - System.currentTimeMillis();
+ service.schedule(new Runnable() {
+ @Override
+ public void run() {
+ prune(service);
+ }
+ }, delay, TimeUnit.MILLISECONDS);
+ }
+
+ public static class DiskStats {
+ long size;
+ long space;
+ long hitCount;
+ long missCount;
+
+ public long size() {
+ return size;
+ }
+
+ public long space() {
+ return space;
+ }
+
+ public long hitCount() {
+ return hitCount;
+ }
+
+ public long requestCount() {
+ return hitCount + missCount;
+ }
+ }
+
+ static class ValueHolder<V> {
+ final V value;
+ long created;
+ volatile boolean clean;
+
+ ValueHolder(V value) {
+ this.value = value;
+ }
+ }
+
+ static class Loader<K, V> extends CacheLoader<K, ValueHolder<V>> {
+ private final Executor executor;
+ private final SqlStore<K, V> store;
+ private final CacheLoader<K, V> loader;
+
+ Loader(Executor executor, SqlStore<K, V> store, CacheLoader<K, V> loader) {
+ this.executor = executor;
+ this.store = store;
+ this.loader = loader;
+ }
+
+ @Override
+ public ValueHolder<V> load(final K key) throws Exception {
+ if (store.mightContain(key)) {
+ ValueHolder<V> h = store.getIfPresent(key);
+ if (h != null) {
+ return h;
+ }
+ }
+
+ final ValueHolder<V> h = new ValueHolder<V>(loader.load(key));
+ h.created = System.currentTimeMillis();
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ store.put(key, h);
+ }
+ });
+ return h;
+ }
+ }
+
+ private static class KeyType<K> {
+ String columnType() {
+ return "OTHER";
+ }
+
+ @SuppressWarnings("unchecked")
+ K get(ResultSet rs, int col) throws SQLException {
+ return (K) rs.getObject(col);
+ }
+
+ void set(PreparedStatement ps, int col, K value) throws SQLException {
+ ps.setObject(col, value);
+ }
+
+ Funnel<K> funnel() {
+ return new Funnel<K>() {
+ @Override
+ public void funnel(K from, PrimitiveSink into) {
+ try {
+ ObjectOutputStream ser =
+ new ObjectOutputStream(new SinkOutputStream(into));
+ ser.writeObject(from);
+ ser.flush();
+ } catch (IOException err) {
+ throw new RuntimeException("Cannot hash as Serializable", err);
+ }
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ static <K> KeyType<K> create(TypeLiteral<K> type) {
+ if (type.getRawType() == String.class) {
+ return (KeyType<K>) STRING;
+ }
+ return (KeyType<K>) OTHER;
+ }
+
+ static final KeyType<?> OTHER = new KeyType<Object>();
+ static final KeyType<String> STRING = new KeyType<String>() {
+ @Override
+ String columnType() {
+ return "VARCHAR(4096)";
+ }
+
+ @Override
+ String get(ResultSet rs, int col) throws SQLException {
+ return rs.getString(col);
+ }
+
+ @Override
+ void set(PreparedStatement ps, int col, String value)
+ throws SQLException {
+ ps.setString(col, value);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ Funnel<String> funnel() {
+ Funnel<?> s = Funnels.stringFunnel();
+ return (Funnel<String>) s;
+ }
+ };
+ }
+
+ static class SqlStore<K, V> {
+ private final String url;
+ private final KeyType<K> keyType;
+ private final long maxSize;
+ private final BlockingQueue<SqlHandle> handles;
+ private final AtomicLong hitCount = new AtomicLong();
+ private final AtomicLong missCount = new AtomicLong();
+ private volatile BloomFilter<K> bloomFilter;
+ private int estimatedSize;
+
+ SqlStore(String jdbcUrl, TypeLiteral<K> keyType, long maxSize) {
+ this.url = jdbcUrl;
+ this.keyType = KeyType.create(keyType);
+ this.maxSize = maxSize;
+
+ int cores = Runtime.getRuntime().availableProcessors();
+ int keep = Math.min(cores, 16);
+ this.handles = new ArrayBlockingQueue<SqlHandle>(keep);
+ }
+
+ synchronized void open() {
+ if (bloomFilter == null) {
+ bloomFilter = buildBloomFilter();
+ }
+ }
+
+ void close() {
+ SqlHandle h;
+ while ((h = handles.poll()) != null) {
+ h.close();
+ }
+ }
+
+ boolean mightContain(K key) {
+ BloomFilter<K> b = bloomFilter;
+ if (b == null) {
+ synchronized (this) {
+ b = bloomFilter;
+ if (b == null) {
+ b = buildBloomFilter();
+ bloomFilter = b;
+ }
+ }
+ }
+ return b == null || b.mightContain(key);
+ }
+
+ private BloomFilter<K> buildBloomFilter() {
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ Statement s = c.conn.createStatement();
+ try {
+ ResultSet r;
+ if (estimatedSize <= 0) {
+ r = s.executeQuery("SELECT COUNT(*) FROM data");
+ try {
+ estimatedSize = r.next() ? r.getInt(1) : 0;
+ } finally {
+ r.close();
+ }
+ }
+
+ BloomFilter<K> b = newBloomFilter();
+ r = s.executeQuery("SELECT k FROM data");
+ try {
+ while (r.next()) {
+ b.put(keyType.get(r, 1));
+ }
+ } finally {
+ r.close();
+ }
+ return b;
+ } finally {
+ s.close();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot build BloomFilter for " + url, e);
+ c = close(c);
+ return null;
+ } finally {
+ release(c);
+ }
+ }
+
+ ValueHolder<V> getIfPresent(K key) {
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ if (c.get == null) {
+ c.get = c.conn.prepareStatement("SELECT v FROM data WHERE k=?");
+ }
+ keyType.set(c.get, 1, key);
+ ResultSet r = c.get.executeQuery();
+ try {
+ if (!r.next()) {
+ missCount.incrementAndGet();
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ V val = (V) r.getObject(1);
+ ValueHolder<V> h = new ValueHolder<V>(val);
+ h.clean = true;
+ hitCount.incrementAndGet();
+ touch(c, key);
+ return h;
+ } finally {
+ r.close();
+ c.get.clearParameters();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot read cache " + url + " for " + key, e);
+ c = close(c);
+ return null;
+ } finally {
+ release(c);
+ }
+ }
+
+ private void touch(SqlHandle c, K key) throws SQLException {
+ if (c.touch == null) {
+ c.touch =c.conn.prepareStatement("UPDATE data SET accessed=? WHERE k=?");
+ }
+ try {
+ c.touch.setTimestamp(1, new Timestamp(System.currentTimeMillis()));
+ keyType.set(c.touch, 2, key);
+ c.touch.executeUpdate();
+ } finally {
+ c.touch.clearParameters();
+ }
+ }
+
+ void put(K key, ValueHolder<V> holder) {
+ if (holder.clean) {
+ return;
+ }
+
+ BloomFilter<K> b = bloomFilter;
+ if (b != null) {
+ b.put(key);
+ bloomFilter = b;
+ }
+
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ if (c.put == null) {
+ c.put = c.conn.prepareStatement("MERGE INTO data VALUES(?,?,?,?)");
+ }
+ try {
+ keyType.set(c.put, 1, key);
+ c.put.setObject(2, holder.value);
+ c.put.setTimestamp(3, new Timestamp(holder.created));
+ c.put.setTimestamp(4, new Timestamp(System.currentTimeMillis()));
+ c.put.executeUpdate();
+ holder.clean = true;
+ } finally {
+ c.put.clearParameters();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot put into cache " + url, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ }
+
+ void invalidate(K key) {
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ invalidate(c, key);
+ } catch (SQLException e) {
+ log.warn("Cannot invalidate cache " + url, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ }
+
+ private void invalidate(SqlHandle c, K key) throws SQLException {
+ if (c.invalidate == null) {
+ c.invalidate = c.conn.prepareStatement("DELETE FROM data WHERE k=?");
+ }
+ try {
+ keyType.set(c.invalidate, 1, key);
+ c.invalidate.executeUpdate();
+ } finally {
+ c.invalidate.clearParameters();
+ }
+ }
+
+ void invalidateAll() {
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ Statement s = c.conn.createStatement();
+ try {
+ s.executeUpdate("DELETE FROM data");
+ } finally {
+ s.close();
+ }
+ bloomFilter = newBloomFilter();
+ } catch (SQLException e) {
+ log.warn("Cannot invalidate cache " + url, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ }
+
+ void prune(Cache<K, ?> mem) {
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ Statement s = c.conn.createStatement();
+ try {
+ long used = 0;
+ ResultSet r = s.executeQuery("SELECT"
+ + " SUM(OCTET_LENGTH(k) + OCTET_LENGTH(v))"
+ + " FROM data");
+ try {
+ used = r.next() ? r.getLong(1) : 0;
+ } finally {
+ r.close();
+ }
+ if (used <= maxSize) {
+ return;
+ }
+
+ r = s.executeQuery("SELECT"
+ + " k"
+ + ",OCTET_LENGTH(k) + OCTET_LENGTH(v)"
+ + " FROM data"
+ + " ORDER BY accessed");
+ try {
+ while (maxSize < used && r.next()) {
+ K key = keyType.get(r, 1);
+ if (mem.getIfPresent(key) != null) {
+ touch(c, key);
+ } else {
+ invalidate(c, key);
+ used -= r.getLong(2);
+ }
+ }
+ } finally {
+ r.close();
+ }
+ } finally {
+ s.close();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot prune cache " + url, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ }
+
+ DiskStats diskStats() {
+ DiskStats d = new DiskStats();
+ d.hitCount = hitCount.get();
+ d.missCount = missCount.get();
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ Statement s = c.conn.createStatement();
+ try {
+ ResultSet r = s.executeQuery("SELECT"
+ + " COUNT(*)"
+ + ",SUM(OCTET_LENGTH(k) + OCTET_LENGTH(v))"
+ + " FROM data");
+ try {
+ if (r.next()) {
+ d.size = r.getLong(1);
+ d.space = r.getLong(2);
+ }
+ } finally {
+ r.close();
+ }
+ } finally {
+ s.close();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot get DiskStats for " + url, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ return d;
+ }
+
+ private SqlHandle acquire() throws SQLException {
+ SqlHandle h = handles.poll();
+ return h != null ? h : new SqlHandle(url, keyType);
+ }
+
+ private void release(SqlHandle h) {
+ if (h != null && !handles.offer(h)) {
+ h.close();
+ }
+ }
+
+ private SqlHandle close(SqlHandle h) {
+ if (h != null) {
+ h.close();
+ }
+ return null;
+ }
+
+ private BloomFilter<K> newBloomFilter() {
+ int cnt = Math.max(64 * 1024, 2 * estimatedSize);
+ return BloomFilter.create(keyType.funnel(), cnt);
+ }
+ }
+
+ static class SqlHandle {
+ private final String url;
+ Connection conn;
+ PreparedStatement get;
+ PreparedStatement put;
+ PreparedStatement touch;
+ PreparedStatement invalidate;
+
+ SqlHandle(String url, KeyType<?> type) throws SQLException {
+ this.url = url;
+ this.conn = org.h2.Driver.load().connect(url, null);
+ Statement stmt = conn.createStatement();
+ try {
+ stmt.execute("CREATE TABLE IF NOT EXISTS data"
+ + "(k " + type.columnType() + " NOT NULL PRIMARY KEY HASH"
+ + ",v OTHER NOT NULL"
+ + ",created TIMESTAMP NOT NULL"
+ + ",accessed TIMESTAMP NOT NULL"
+ + ")");
+ } finally {
+ stmt.close();
+ }
+ }
+
+ void close() {
+ get = closeStatement(get);
+ put = closeStatement(put);
+ touch = closeStatement(touch);
+ invalidate = closeStatement(invalidate);
+
+ if (conn != null) {
+ try {
+ conn.close();
+ } catch (SQLException e) {
+ log.warn("Cannot close connection to " + url, e);
+ } finally {
+ conn = null;
+ }
+ }
+ }
+
+ private PreparedStatement closeStatement(PreparedStatement ps) {
+ if (ps != null) {
+ try {
+ ps.close();
+ } catch (SQLException e) {
+ log.warn("Cannot close statement for " + url, e);
+ }
+ }
+ return null;
+ }
+ }
+
+ private static class SinkOutputStream extends OutputStream {
+ private final PrimitiveSink sink;
+
+ SinkOutputStream(PrimitiveSink sink) {
+ this.sink = sink;
+ }
+
+ @Override
+ public void write(int b) {
+ sink.putByte((byte)b);
+ }
+
+ @Override
+ public void write(byte[] b, int p, int n) {
+ sink.putBytes(b, p, n);
+ }
+ }
+}
View
272 gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java
@@ -1,272 +0,0 @@
-// Copyright (C) 2009 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.google.gerrit.ehcache;
-
-import static java.util.concurrent.TimeUnit.MINUTES;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.CachePool;
-import com.google.gerrit.server.cache.CacheProvider;
-import com.google.gerrit.server.cache.EntryCreator;
-import com.google.gerrit.server.cache.EvictionPolicy;
-import com.google.gerrit.server.cache.ProxyCache;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import net.sf.ehcache.CacheManager;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.config.CacheConfiguration;
-import net.sf.ehcache.config.Configuration;
-import net.sf.ehcache.config.DiskStoreConfiguration;
-import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
-
-import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Pool of all declared caches created by {@link CacheModule}s. */
-@Singleton
-public class EhcachePoolImpl implements CachePool {
- private static final Logger log =
- LoggerFactory.getLogger(EhcachePoolImpl.class);
-
- public static class Module extends LifecycleModule {
- @Override
- protected void configure() {
- bind(CachePool.class).to(EhcachePoolImpl.class);
- bind(EhcachePoolImpl.class);
- listener().to(EhcachePoolImpl.Lifecycle.class);
- }
- }
-
- public static class Lifecycle implements LifecycleListener {
- private final EhcachePoolImpl cachePool;
-
- @Inject
- Lifecycle(final EhcachePoolImpl cachePool) {
- this.cachePool = cachePool;
- }
-
- @Override
- public void start() {
- cachePool.start();
- }
-
- @Override
- public void stop() {
- cachePool.stop();
- }
- }
-
- private final Config config;
- private final SitePaths site;
-
- private final Object lock = new Object();
- private final Map<String, CacheProvider<?, ?>> caches;
- private CacheManager manager;
-
- @Inject
- EhcachePoolImpl(@GerritServerConfig final Config cfg, final SitePaths site) {
- this.config = cfg;
- this.site = site;
- this.caches = new HashMap<String, CacheProvider<?, ?>>();
- }
-
- @SuppressWarnings({"rawtypes", "unchecked"})
- private void start() {
- synchronized (lock) {
- if (manager != null) {
- throw new IllegalStateException("Cache pool has already been started");
- }
-
- try {
- System.setProperty("net.sf.ehcache.skipUpdateCheck", "" + true);
- } catch (SecurityException e) {
- // Ignore it, the system is just going to ping some external page
- // using a background thread and there's not much we can do about
- // it now.
- }
-
- manager = new CacheManager(new Factory().toConfiguration());
- for (CacheProvider<?, ?> p : caches.values()) {
- Ehcache eh = manager.getEhcache(p.getName());
- EntryCreator<?, ?> c = p.getEntryCreator();
- if (c != null) {
- p.bind(new PopulatingCache(eh, c));
- } else {
- p.bind(new SimpleCache(eh));
- }
- }
- }
- }
-
- private void stop() {
- synchronized (lock) {
- if (manager != null) {
- manager.shutdown();
- }
- }
- }
-
- /** <i>Discouraged</i> Get the underlying cache descriptions, for statistics. */
- public CacheManager getCacheManager() {
- synchronized (lock) {
- return manager;
- }
- }
-
- public <K, V> ProxyCache<K, V> register(final CacheProvider<K, V> provider) {
- synchronized (lock) {
- if (manager != null) {
- throw new IllegalStateException("Cache pool has already been started");
- }
-
- final String n = provider.getName();
- if (caches.containsKey(n) && caches.get(n) != provider) {
- throw new IllegalStateException("Cache \"" + n + "\" already defined");
- }
- caches.put(n, provider);
- return new ProxyCache<K, V>();
- }
- }
-
- private class Factory {
- private static final int MB = 1024 * 1024;
- private final Configuration mgr = new Configuration();
-
- Configuration toConfiguration() {
- configureDiskStore();
- configureDefaultCache();
-
- for (CacheProvider<?, ?> p : caches.values()) {
- final String name = p.getName();
- final CacheConfiguration c = newCache(name);
- c.setMemoryStoreEvictionPolicyFromObject(toPolicy(p.evictionPolicy()));
-
- c.setMaxElementsInMemory(getInt(name, "memorylimit", p.memoryLimit()));
-
- c.setTimeToIdleSeconds(0);
- c.setTimeToLiveSeconds(getSeconds(name, "maxage", p.maxAge()));
- c.setEternal(c.getTimeToLiveSeconds() == 0);
-
- if (p.disk() && mgr.getDiskStoreConfiguration() != null) {
- c.setMaxElementsOnDisk(getInt(name, "disklimit", p.diskLimit()));
-
- int v = c.getDiskSpoolBufferSizeMB() * MB;
- v = getInt(name, "diskbuffer", v) / MB;
- c.setDiskSpoolBufferSizeMB(Math.max(1, v));
- c.setOverflowToDisk(c.getMaxElementsOnDisk() > 0);
- c.setDiskPersistent(c.getMaxElementsOnDisk() > 0);
- }
-
- mgr.addCache(c);
- }
-
- return mgr;
- }
-
- private MemoryStoreEvictionPolicy toPolicy(final EvictionPolicy policy) {
- switch (policy) {
- case LFU:
- return MemoryStoreEvictionPolicy.LFU;
-
- case LRU:
- return MemoryStoreEvictionPolicy.LRU;
-
- default:
- throw new IllegalArgumentException("Unsupported " + policy);
- }
- }
-
- private int getInt(String n, String s, int d) {
- return config.getInt("cache", n, s, d);
- }
-
- private long getSeconds(String n, String s, long d) {
- d = MINUTES.convert(d, SECONDS);
- long m = ConfigUtil.getTimeUnit(config, "cache", n, s, d, MINUTES);
- return SECONDS.convert(m, MINUTES);
- }
-
- private void configureDiskStore() {
- boolean needDisk = false;
- for (CacheProvider<?, ?> p : caches.values()) {
- if (p.disk()) {
- needDisk = true;
- break;
- }
- }
- if (!needDisk) {
- return;
- }
-
- File loc = site.resolve(config.getString("cache", null, "directory"));
- if (loc == null) {
- } else if (loc.exists() || loc.mkdirs()) {
- if (loc.canWrite()) {
- final DiskStoreConfiguration c = new DiskStoreConfiguration();
- c.setPath(loc.getAbsolutePath());
- mgr.addDiskStore(c);
- log.info("Enabling disk cache " + loc.getAbsolutePath());
- } else {
- log.warn("Can't write to disk cache: " + loc.getAbsolutePath());
- }
- } else {
- log.warn("Can't create disk cache: " + loc.getAbsolutePath());
- }
- }
-
- private CacheConfiguration newConfiguration() {
- CacheConfiguration c = new CacheConfiguration();
-
- c.setMaxElementsInMemory(1024);
- c.setMemoryStoreEvictionPolicyFromObject(MemoryStoreEvictionPolicy.LFU);
-
- c.setTimeToIdleSeconds(0);
- c.setTimeToLiveSeconds(0 /* infinite */);
- c.setEternal(true);
-
- if (mgr.getDiskStoreConfiguration() != null) {
- c.setMaxElementsOnDisk(16384);
- c.setOverflowToDisk(false);
- c.setDiskPersistent(false);
-
- c.setDiskSpoolBufferSizeMB(5);
- c.setDiskExpiryThreadIntervalSeconds(60 * 60);
- }
- return c;
- }
-
- private void configureDefaultCache() {
- mgr.setDefaultCacheConfiguration(newConfiguration());
- }
-
- private CacheConfiguration newCache(final String name) {
- CacheConfiguration c = newConfiguration();
- c.setName(name);
- return c;
- }
- }
-}
View
114 gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java
@@ -1,114 +0,0 @@
-// Copyright (C) 2008 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.google.gerrit.ehcache;
-
-import com.google.gerrit.server.cache.Cache;
-import com.google.gerrit.server.cache.EntryCreator;
-
-import net.sf.ehcache.CacheException;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.Element;
-import net.sf.ehcache.constructs.blocking.CacheEntryFactory;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A decorator for {@link Cache} which automatically constructs missing entries.
- * <p>
- * On a cache miss {@link EntryCreator#createEntry(Object)} is invoked, allowing
- * the application specific subclass to compute the entry and return it for
- * caching. During a miss the cache takes a lock related to the missing key,
- * ensuring that at most one thread performs the creation work, and other
- * threads wait for the result. Concurrent creations are possible if two
- * different keys miss and hash to different locks in the internal lock table.
- *
- * @param <K> type of key used to name cache entries.
- * @param <V> type of value stored within a cache entry.
- */
-class PopulatingCache<K, V> implements Cache<K, V> {
- private static final Logger log =
- LoggerFactory.getLogger(PopulatingCache.class);
-
- private final net.sf.ehcache.constructs.blocking.SelfPopulatingCache self;
- private final EntryCreator<K, V> creator;
-
- PopulatingCache(Ehcache s, EntryCreator<K, V> entryCreator) {
- creator = entryCreator;
- final CacheEntryFactory f = new CacheEntryFactory() {
- @SuppressWarnings("unchecked")
- @Override
- public Object createEntry(Object key) throws Exception {
- return creator.createEntry((K) key);
- }
- };
- self = new net.sf.ehcache.constructs.blocking.SelfPopulatingCache(s, f);
- }
-
- /**
- * Get the element from the cache, or {@link EntryCreator#missing(Object)} if not found.
- * <p>
- * The {@link EntryCreator#missing(Object)} method is only invoked if:
- * <ul>
- * <li>{@code key == null}, in which case the application should return a
- * suitable return value that callers can accept, or throw a RuntimeException.
- * <li>{@code createEntry(key)} threw an exception, in which case the entry
- * was not stored in the cache. An entry was recorded in the application log,
- * but a return value is still required.
- * <li>The cache has been shutdown, and access is forbidden.
- * </ul>
- *
- * @param key key to locate.
- * @return either the cached entry, or {@code missing(key)} if not found.
- */
- @SuppressWarnings("unchecked")
- public V get(final K key) {
- if (key == null) {
- return creator.missing(key);
- }
-
- final Element m;
- try {
- m = self.get(key);
- } catch (IllegalStateException err) {
- log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
- return creator.missing(key);
- } catch (CacheException err) {
- log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
- return creator.missing(key);
- }
- return m != null ? (V) m.getObjectValue() : creator.missing(key);
- }
-
- public void remove(final K key) {
- if (key != null) {
- self.remove(key);
- }
- }
-
- /** Remove all cached items, forcing them to be created again on demand. */
- public void removeAll() {
- self.removeAll();
- }
-
- public void put(K key, V value) {
- self.put(new Element(key, value));
- }
-
- @Override
- public String toString() {
- return "Cache[" + self.getName() + "]";
- }
-}
View
81 gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java
@@ -1,81 +0,0 @@
-// Copyright (C) 2009 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.google.gerrit.ehcache;
-
-import com.google.gerrit.server.cache.Cache;
-
-import net.sf.ehcache.CacheException;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.Element;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A fast in-memory and/or on-disk based cache.
- *
- * @type <K> type of key used to lookup entries in the cache.
- * @type <V> type of value stored within each cache entry.
- */
-final class SimpleCache<K, V> implements Cache<K, V> {
- private static final Logger log = LoggerFactory.getLogger(SimpleCache.class);
-
- private final Ehcache self;
-
- SimpleCache(final Ehcache self) {
- this.self = self;
- }
-
- Ehcache getEhcache() {
- return self;
- }
-
- @SuppressWarnings("unchecked")
- public V get(final K key) {
- if (key == null) {
- return null;
- }
- final Element m;
- try {
- m = self.get(key);
- } catch (IllegalStateException err) {
- log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
- return null;
- } catch (CacheException err) {
- log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
- return null;
- }
- return m != null ? (V) m.getObjectValue() : null;
- }
-
- public void put(final K key, final V value) {
- self.put(new Element(key, value));
- }
-
- public void remove(final K key) {
- if (key != null) {
- self.remove(key);
- }
- }
-
- public void removeAll() {
- self.removeAll();
- }
-
- @Override
- public String toString() {
- return "Cache[" + self.getName() + "]";
- }
-}
View
13 gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -26,14 +26,11 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EvictionPolicy;
import com.google.gerrit.server.config.AuthConfig;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Provider;
-import com.google.inject.TypeLiteral;
import com.google.inject.servlet.RequestScoped;
import javax.servlet.http.Cookie;
@@ -49,13 +46,9 @@ public static Module module() {
return new CacheModule() {
@Override
protected void configure() {
- final String cacheName = WebSessionManager.CACHE_NAME;
- final TypeLiteral<Cache<Key, Val>> type =
- new TypeLiteral<Cache<Key, Val>>() {};
- disk(type, cacheName) //
- .memoryLimit(1024) // reasonable default for many sites
- .maxAge(MAX_AGE_MINUTES, MINUTES) // expire sessions if they are inactive
- .evictionPolicy(EvictionPolicy.LRU) // keep most recently used
+ persist(WebSessionManager.CACHE_NAME, String.class, Val.class)
+ .maximumWeight(1024) // reasonable default for many sites
+ .expireAfterWrite(MAX_AGE_MINUTES, MINUTES) // expire sessions if they are inactive
;
bind(WebSessionManager.class);
bind(WebSession.class)
View
18 gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -14,13 +14,13 @@
package com.google.gerrit.httpd;
+import com.google.common.cache.Cache;
import com.google.gerrit.common.data.Capable;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.git.AsyncReceiveCommits;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -99,11 +99,11 @@ protected void configure() {
install(new CacheModule() {
@Override
protected void configure() {
- TypeLiteral<Cache<AdvertisedObjectsCacheKey, Set<ObjectId>>> cache =
- new TypeLiteral<Cache<AdvertisedObjectsCacheKey, Set<ObjectId>>>() {};
- core(cache, ID_CACHE)
- .memoryLimit(4096)
- .maxAge(10, TimeUnit.MINUTES);
+ cache(ID_CACHE,
+ AdvertisedObjectsCacheKey.class,
+ new TypeLiteral<Set<ObjectId>>() {})
+ .maximumWeight(4096)
+ .expireAfterWrite(10, TimeUnit.MINUTES);
}
});
}
@@ -320,12 +320,12 @@ public void doFilter(ServletRequest request, ServletResponse response,
if (isGet) {
rc.advertiseHistory();
- cache.remove(cacheKey);
+ cache.invalidate(cacheKey);
} else {
- Set<ObjectId> ids = cache.get(cacheKey);
+ Set<ObjectId> ids = cache.getIfPresent(cacheKey);
if (ids != null) {
rp.getAdvertisedObjects().addAll(ids);
- cache.remove(cacheKey);
+ cache.invalidate(cacheKey);
}
}
View
30 gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -26,9 +26,9 @@
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
+import com.google.common.cache.Cache;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
@@ -55,11 +55,11 @@ static long now() {
private final long sessionMaxAgeMillis;
private final SecureRandom prng;
- private final Cache<Key, Val> self;
+ private final Cache<String, Val> self;
@Inject
WebSessionManager(@GerritServerConfig Config cfg,
- @Named(CACHE_NAME) final Cache<Key, Val> cache) {
+ @Named(CACHE_NAME) final Cache<String, Val> cache) {
prng = new SecureRandom();
self = cache;
@@ -76,7 +76,7 @@ Key createKey(final Account.Id who) {
prng.nextBytes(rnd);
buf = new ByteArrayOutputStream(3 + nonceLen);
- writeVarInt32(buf, (int) Key.serialVersionUID);
+ writeVarInt32(buf, (int) Val.serialVersionUID);
writeVarInt32(buf, who.get());
writeBytes(buf, rnd);
@@ -120,7 +120,7 @@ Val createVal(final Key key, final Account.Id who, final boolean remember,
Val val = new Val(who, refreshCookieAt, remember,
lastLogin, xsrfToken, expiresAt);
- self.put(key, val);
+ self.put(key.token, val);
return val;
}
@@ -141,21 +141,19 @@ int getCookieAge(final Val val) {
}
Val get(final Key key) {
- Val val = self.get(key);
+ Val val = self.getIfPresent(key.token);
if (val != null && val.expiresAt <= now()) {
- self.remove(key);
+ self.invalidate(key.token);
return null;
}
return val;
}
void destroy(final Key key) {
- self.remove(key);
+ self.invalidate(key.token);
}
- static final class Key implements Serializable {
- static final long serialVersionUID = 2L;
-
+ static final class Key {
private transient String token;
Key(final String t) {
@@ -175,18 +173,10 @@ public int hashCode() {
public boolean equals(Object obj) {
return obj instanceof Key && token.equals(((Key) obj).token);
}
-
- private void writeObject(final ObjectOutputStream out) throws IOException {
- writeString(out, token);
- }
-
- private void readObject(final ObjectInputStream in) throws IOException {
- token = readString(in);
- }
}
static final class Val implements Serializable {
- static final long serialVersionUID = Key.serialVersionUID;
+ static final long serialVersionUID = 2L;
private transient Account.Id accountId;
private transient long refreshCookieAt;
View
12 gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
@@ -14,13 +14,16 @@
package com.google.gerrit.httpd.plugins;
+import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.plugins.ModuleGenerator;
import com.google.gerrit.server.plugins.ReloadPluginListener;
import com.google.gerrit.server.plugins.StartPluginListener;
import com.google.inject.internal.UniqueAnnotations;
import com.google.inject.servlet.ServletModule;
public class HttpPluginModule extends ServletModule {
+ static final String PLUGIN_RESOURCES = "plugin_resources";
+
@Override
protected void configureServlets() {
bind(HttpPluginServlet.class);
@@ -36,5 +39,14 @@ protected void configureServlets() {
bind(ModuleGenerator.class)
.to(HttpAutoRegisterModuleGenerator.class);
+
+ install(new CacheModule() {
+ @Override
+ protected void configure() {
+ cache(PLUGIN_RESOURCES, ResourceKey.class, Resource.class)
+ .maximumWeight(2 << 20)
+ .weigher(ResourceWeigher.class);
+ }
+ });
}
}
View
125 gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -16,8 +16,6 @@
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.Weigher;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gerrit.extensions.registration.RegistrationHandle;
@@ -32,6 +30,7 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import com.google.inject.name.Named;
import com.google.inject.servlet.GuiceFilter;
import org.eclipse.jgit.lib.Config;
@@ -57,7 +56,6 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import javax.annotation.Nullable;
import javax.servlet.FilterChain;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
@@ -90,22 +88,12 @@
@Inject
HttpPluginServlet(MimeUtilFileTypeRegistry mimeUtil,
@CanonicalWebUrl Provider<String> webUrl,
+ @Named(HttpPluginModule.PLUGIN_RESOURCES) Cache<ResourceKey, Resource> cache,
@GerritServerConfig Config cfg,
SshInfo sshInfo) {
this.mimeUtil = mimeUtil;
this.webUrl = webUrl;
-
- this.resourceCache = CacheBuilder.newBuilder()
- .maximumWeight(cfg.getInt(
- "cache", "plugin_resources", "memoryLimit",
- 2 * 1024 * 1024))
- .weigher(new Weigher<ResourceKey, Resource>() {
- @Override
- public int weigh(ResourceKey key, Resource value) {
- return key.weight() + value.weight();
- }
- })
- .build();
+ this.resourceCache = cache;
String sshHost = "review.example.com";
int sshPort = 29418;
@@ -247,8 +235,8 @@ private void onDefault(PluginHolder holder,
if (exists(entry)) {
sendResource(jar, entry, key, res);
} else {
- resourceCache.put(key, NOT_FOUND);
- NOT_FOUND.send(req, res);
+ resourceCache.put(key, Resource.NOT_FOUND);
+ Resource.NOT_FOUND.send(req, res);
}
} else if (file.equals("Documentation")) {
res.sendRedirect(uri + "/index.html");
@@ -268,12 +256,12 @@ private void onDefault(PluginHolder holder,
} else if (exists(entry)) {
sendResource(jar, entry, key, res);
} else {
- resourceCache.put(key, NOT_FOUND);
- NOT_FOUND.send(req, res);
+ resourceCache.put(key, Resource.NOT_FOUND);
+ Resource.NOT_FOUND.send(req, res);
}
} else {
- resourceCache.put(key, NOT_FOUND);
- NOT_FOUND.send(req, res);
+ resourceCache.put(key, Resource.NOT_FOUND);
+ Resource.NOT_FOUND.send(req, res);
}
}
@@ -559,7 +547,7 @@ private static String extractName(HttpServletRequest req) {
return 0 <= s ? path.substring(1, s) : path.substring(1);
}
- private static void noCache(HttpServletResponse res) {
+ static void noCache(HttpServletResponse res) {
res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
res.setHeader("Pragma", "no-cache");
res.setHeader("Cache-Control", "no-cache, must-revalidate");
@@ -576,99 +564,6 @@ private static void noCache(HttpServletResponse res) {
}
}
- private static final class ResourceKey {
- private final Plugin.CacheKey plugin;
- private final String resource;
-
- ResourceKey(Plugin p, String r) {
- this.plugin = p.getCacheKey();
- this.resource = r;
- }
-
- int weight() {
- return 28 + resource.length();
- }
-
- @Override
- public int hashCode() {
- return plugin.hashCode() * 31 + resource.hashCode();
- }
-
- @Override
- public boolean equals(Object other) {
- if (other instanceof ResourceKey) {
- ResourceKey rk = (ResourceKey) other;
- return plugin == rk.plugin && resource.equals(rk.resource);
- }