Merge branch 'master' into exp-nosql

* master:
  Fix all of our pom.xml versions to be 2.1-SNAPSHOT
  Use internal templates to simplify minor formatting commands.
  Convert 3 email classes to file based templates.
  Convert the Abandoned and MergeFail email classes to templates
  Use a template to set the contents of the CommentEmails.
  Use a template to set the footer on ChangeEmails.
  Use a template to set the contents of the MergedEmails.
  Use a template to set the subject line.
  Add framework for using velocity templates in email classes
  Add ability to deactivate a user when they leave the project.

Conflicts:
	gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
	gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
	gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
	gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
	gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
	gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java

Change-Id: Iac92b56e10f9cc4063fc0ec3cea6c79a5a087994
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 591ffa5..63b5192 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -630,6 +630,12 @@
 * `JDBC`
 +
 Connect using a JDBC driver class name and URL.
++
+* `NOSQL_HEAP_FILE`
++
+Connect to a locally stored NoSQL database.  Similar to H2, except
+its a binary tree NoSQL implementation that is only suitable for
+development and testing.  Do not use for a production server.
 
 +
 If not specified, database.driver and database.url are used as-is,
@@ -648,8 +654,8 @@
 +
 For POSTGRESQL or MYSQL, the name of the database on the server.
 +
-For H2, this is the path to the database, and if not absolute is
-relative to `'$site_path'`.
+For H2 and NOSQL_HEAP_FILE, this is the path to the database,
+and if not absolute is relative to `'$site_path'`.
 
 [[database.username]]database.username::
 +
@@ -1489,6 +1495,8 @@
 If multiple values are supplied, the daemon will listen on all
 of them.
 +
+To disable the internal SSHD, set listenAddress to `off`.
++
 By default, *:29418.
 
 [[sshd.reuseAddress]]sshd.reuseAddress::
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 25194a9..6d543d5 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -17,6 +17,7 @@
 gwtjsonrpc                  <<apache2,Apache License 2.0>>
 gwtorm                      <<apache2,Apache License 2.0>>
 Google Gson                 <<apache2,Apache License 2.0>>
+Google Guava Libraries      <<apache2,Apache License 2.0>>
 Google Web Toolkit          <<apache2,Apache License 2.0>>
 Guice                       <<apache2,Apache License 2.0>>
 Apache Commons Codec        <<apache2,Apache License 2.0>>
@@ -38,6 +39,7 @@
 mime-util                   <<apache2,Apache License 2.0>>
 Jetty                       <<apache2,Apache License 2.0>>, or link:http://www.eclipse.org/legal/epl-v10.html[EPL]
 Google Code Prettify        <<apache2,Apache License 2.0>>
+Google Protobuf             <<apache2,Apache License 2.0>>
 JGit                        <<jgit,New-Style BSD>>
 JSch                        <<sshd,New-Style BSD>>
 PostgreSQL JDBC Driver      <<postgresql,New-Style BSD>>
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdProviderPattern.java b/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdProviderPattern.java
index 731d765..ec51941 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdProviderPattern.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdProviderPattern.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.common.auth.openid;
 
 import com.google.gerrit.reviewdb.AccountExternalId;
+import com.google.gwtorm.client.Column;
 
 public class OpenIdProviderPattern {
   public static OpenIdProviderPattern create(String pattern) {
@@ -24,7 +25,10 @@
     return r;
   }
 
+  @Column(id = 1)
   protected boolean regex;
+
+  @Column(id = 2)
   protected String pattern;
 
   protected OpenIdProviderPattern() {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
index 717a492..27b6ac9 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
@@ -21,5 +21,6 @@
 public class HostPageData {
   public Account account;
   public AccountDiffPreference accountDiffPref;
+  public String xsrfToken;
   public GerritConfig config;
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
index 048d440..0597ee9 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.prettify.client.ClientSideFormatter;
 import com.google.gerrit.prettify.common.EditList;
+import com.google.gerrit.prettify.common.LineEdit;
 import com.google.gerrit.prettify.common.PrettyFormatter;
 import com.google.gerrit.prettify.common.SparseFileContent;
 import com.google.gerrit.prettify.common.SparseHtmlFile;
@@ -25,8 +26,6 @@
 import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.Patch.ChangeType;
 
-import org.eclipse.jgit.diff.Edit;
-
 import java.util.List;
 
 public class PatchScript {
@@ -42,7 +41,7 @@
   protected AccountDiffPreference diffPrefs;
   protected SparseFileContent a;
   protected SparseFileContent b;
-  protected List<Edit> edits;
+  protected List<LineEdit> edits;
   protected DisplayMethod displayMethodA;
   protected DisplayMethod displayMethodB;
   protected CommentDetail comments;
@@ -53,7 +52,7 @@
   public PatchScript(final Change.Key ck, final ChangeType ct, final String on,
       final String nn, final List<String> h, final AccountDiffPreference dp,
       final SparseFileContent ca, final SparseFileContent cb,
-      final List<Edit> e, final DisplayMethod ma, final DisplayMethod mb,
+      final List<LineEdit> e, final DisplayMethod ma, final DisplayMethod mb,
       final CommentDetail cd, final List<Patch> hist, final boolean hf,
       final boolean id) {
     changeId = ck;
@@ -170,7 +169,7 @@
     return f;
   }
 
-  public List<Edit> getEdits() {
+  public List<LineEdit> getEdits() {
     return edits;
   }
 
diff --git a/gerrit-ehcache/.gitignore b/gerrit-ehcache/.gitignore
new file mode 100644
index 0000000..903c6c8
--- /dev/null
+++ b/gerrit-ehcache/.gitignore
@@ -0,0 +1,4 @@
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
diff --git a/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs b/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..82eb859
--- /dev/null
+++ b/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,3 @@
+#Tue Sep 02 16:59:24 PDT 2008
+eclipse.preferences.version=1
+encoding/<project>=UTF-8
diff --git a/gerrit-ehcache/.settings/org.eclipse.core.runtime.prefs b/gerrit-ehcache/.settings/org.eclipse.core.runtime.prefs
new file mode 100644
index 0000000..8667cfd
--- /dev/null
+++ b/gerrit-ehcache/.settings/org.eclipse.core.runtime.prefs
@@ -0,0 +1,3 @@
+#Tue Sep 02 16:59:24 PDT 2008
+eclipse.preferences.version=1
+line.separator=\n
diff --git a/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs b/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..04afc7f
--- /dev/null
+++ b/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,268 @@
+#Tue May 12 17:44:13 PDT 2009
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_assignment=16
+org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
+org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_after_package=1
+org.eclipse.jdt.core.formatter.blank_lines_before_field=0
+org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_before_imports=0
+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=0
+org.eclipse.jdt.core.formatter.blank_lines_before_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
+org.eclipse.jdt.core.formatter.blank_lines_before_package=0
+org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
+org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=2
+org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
+org.eclipse.jdt.core.formatter.comment.format_block_comments=true
+org.eclipse.jdt.core.formatter.comment.format_header=true
+org.eclipse.jdt.core.formatter.comment.format_html=true
+org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
+org.eclipse.jdt.core.formatter.comment.format_line_comments=true
+org.eclipse.jdt.core.formatter.comment.format_source_code=true
+org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false
+org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
+org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
+org.eclipse.jdt.core.formatter.comment.line_length=80
+org.eclipse.jdt.core.formatter.compact_else_if=true
+org.eclipse.jdt.core.formatter.continuation_indentation=2
+org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
+org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_empty_lines=false
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true
+org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
+org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true
+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.lineSplit=80
+org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=3
+org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=false
+org.eclipse.jdt.core.formatter.tabulation.char=space
+org.eclipse.jdt.core.formatter.tabulation.size=2
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
diff --git a/gerrit-ehcache/.settings/org.eclipse.jdt.ui.prefs b/gerrit-ehcache/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..d4218a5
--- /dev/null
+++ b/gerrit-ehcache/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,61 @@
+#Wed Jul 29 11:31:38 PDT 2009
+eclipse.preferences.version=1
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+formatter_profile=_Google Format
+formatter_settings_version=11
+org.eclipse.jdt.ui.ignorelowercasenames=true
+org.eclipse.jdt.ui.importorder=com.google;com;junit;net;org;java;javax;
+org.eclipse.jdt.ui.ondemandthreshold=99
+org.eclipse.jdt.ui.staticondemandthreshold=99
+org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/>
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=false
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=false
+sp_cleanup.always_use_this_for_non_static_field_access=false
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.convert_to_enhanced_for_loop=false
+sp_cleanup.correct_indentation=false
+sp_cleanup.format_source_code=false
+sp_cleanup.format_source_code_changes_only=false
+sp_cleanup.make_local_variable_final=true
+sp_cleanup.make_parameters_final=true
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=false
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=true
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.organize_imports=false
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_trailing_whitespaces=true
+sp_cleanup.remove_trailing_whitespaces_all=true
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
+sp_cleanup.remove_unnecessary_casts=false
+sp_cleanup.remove_unnecessary_nls_tags=false
+sp_cleanup.remove_unused_imports=false
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.use_blocks=false
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_parentheses_in_expressions=false
+sp_cleanup.use_this_for_non_static_field_access=false
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+sp_cleanup.use_this_for_non_static_method_access=false
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
diff --git a/gerrit-ehcache/pom.xml b/gerrit-ehcache/pom.xml
new file mode 100644
index 0000000..a7c6cf5
--- /dev/null
+++ b/gerrit-ehcache/pom.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2010 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>com.google.gerrit</groupId>
+    <artifactId>gerrit-parent</artifactId>
+    <version>2.1-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>gerrit-ehcache</artifactId>
+  <name>Gerrit Code Review - Ehcache Bindings</name>
+
+  <description>
+    Bindings to Ehcache
+  </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.protobuf</groupId>
+      <artifactId>protobuf-java</artifactId>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java
new file mode 100644
index 0000000..760b88f
--- /dev/null
+++ b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java
@@ -0,0 +1,287 @@
+// 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.lifecycle.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.cache.Cache;
+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.ProvisionException;
+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<?, ?>>();
+  }
+
+  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.disk()) {
+          c = new ProtobufEntryCreator(c, p.getKeyClass(), p.getValueClass());
+        }
+
+        Cache m;
+        if (c != null) {
+          m = new PopulatingCache(eh, c);
+        } else {
+          m = new SimpleCache(eh);
+        }
+        if (p.disk()) {
+          m = new ProtobufCache(m, p.getKeyClass(), p.getValueClass(), p
+                  .getValueProvider());
+        }
+
+        p.bind(m);
+      }
+    }
+  }
+
+  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 void configureDefaultCache() {
+      final 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);
+      }
+
+      mgr.setDefaultCacheConfiguration(c);
+    }
+
+    private CacheConfiguration newCache(final String name) {
+      try {
+        final CacheConfiguration c;
+        c = mgr.getDefaultCacheConfiguration().clone();
+        c.setName(name);
+        return c;
+      } catch (CloneNotSupportedException e) {
+        throw new ProvisionException("Cannot configure cache " + name, e);
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/PopulatingCache.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java
similarity index 79%
rename from gerrit-server/src/main/java/com/google/gerrit/server/cache/PopulatingCache.java
rename to gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java
index 0822cc0..3630733 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/PopulatingCache.java
+++ b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java
@@ -12,10 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.cache;
+package com.google.gerrit.ehcache;
 
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+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;
@@ -59,7 +64,8 @@
   }
 
   /**
-   * Get the element from the cache, or {@link EntryCreator#missing(Object)} if not found.
+   * 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>
@@ -75,9 +81,9 @@
    * @return either the cached entry, or {@code missing(key)} if not found.
    */
   @SuppressWarnings("unchecked")
-  public V get(final K key) {
+  public ListenableFuture<V> get(final K key) {
     if (key == null) {
-      return creator.missing(key);
+      return Futures.immediateFuture(creator.missing(key));
     }
 
     final Element m;
@@ -85,27 +91,32 @@
       m = self.get(key);
     } catch (IllegalStateException err) {
       log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
-      return creator.missing(key);
+      return Futures.immediateFuture(creator.missing(key));
     } catch (CacheException err) {
       log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
-      return creator.missing(key);
+      return Futures.immediateFuture(creator.missing(key));
     }
-    return m != null ? (V) m.getObjectValue() : creator.missing(key);
+    if (m != null) {
+      return Futures.immediateFuture((V) m.getObjectValue());
+    }
+    return Futures.immediateFuture(creator.missing(key));
   }
 
-  public void remove(final K key) {
+  public ListenableFuture<Void> putAsync(final K key, final V value) {
+    self.put(new Element(key, value));
+    return Futures.immediateFuture(null);
+  }
+
+  public ListenableFuture<Void> removeAsync(final K key) {
     if (key != null) {
       self.remove(key);
     }
+    return Futures.immediateFuture(null);
   }
 
-  /** Remove all cached items, forcing them to be created again on demand. */
-  public void removeAll() {
+  public ListenableFuture<Void> removeAllAsync() {
     self.removeAll();
-  }
-
-  public void put(K key, V value) {
-    self.put(new Element(key, value));
+    return Futures.immediateFuture(null);
   }
 
   @Override
diff --git a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/ProtobufCache.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/ProtobufCache.java
new file mode 100644
index 0000000..45cabb7
--- /dev/null
+++ b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/ProtobufCache.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2010 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.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gerrit.server.cache.Cache;
+import com.google.gwtorm.protobuf.CodecFactory;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+import com.google.inject.Provider;
+
+import java.util.concurrent.TimeUnit;
+
+class ProtobufCache<K, V> implements Cache<K, V> {
+  private final Cache<SerializableProtobuf<K>, SerializableProtobuf<V>> cache;
+  private final ProtobufCodec<K> keyCodec;
+  private final ProtobufCodec<V> valueCodec;
+  private final Provider<V> valueProvider;
+  private final Function<SerializableProtobuf<V>, V> unpack;
+
+  ProtobufCache(Cache<SerializableProtobuf<K>, SerializableProtobuf<V>> self,
+      Class<K> keyClass, Class<V> valueClass, Provider<V> valProvider) {
+    keyCodec = CodecFactory.encoder(keyClass);
+    valueCodec = CodecFactory.encoder(valueClass);
+    valueProvider = valProvider;
+    cache = self;
+
+    unpack = new Function<SerializableProtobuf<V>, V>() {
+      @Override
+      public V apply(SerializableProtobuf<V> val) {
+        return val != null ? val.toObject(valueCodec, valueProvider) : null;
+      }
+    };
+  }
+
+  @Override
+  public ListenableFuture<V> get(K key) {
+    return Futures.compose(cache.get(wrapKey(key)), unpack);
+  }
+
+  @Override
+  public ListenableFuture<Void> putAsync(final K key, final V value) {
+    return cache.putAsync(wrapKey(key), wrapValue(value));
+  }
+
+  @Override
+  public ListenableFuture<Void> removeAsync(final K key) {
+    if (key != null) {
+      return cache.removeAsync(wrapKey(key));
+    } else {
+      return Futures.immediateFuture(null);
+    }
+  }
+
+  @Override
+  public ListenableFuture<Void> removeAllAsync() {
+    return cache.removeAllAsync();
+  }
+
+  @Override
+  public long getTimeToLive(TimeUnit unit) {
+    return cache.getTimeToLive(unit);
+  }
+
+  @Override
+  public String toString() {
+    return cache.toString();
+  }
+
+  private SerializableProtobuf<K> wrapKey(K key) {
+    return new SerializableProtobuf<K>(key, keyCodec);
+  }
+
+  private SerializableProtobuf<V> wrapValue(final V value) {
+    return new SerializableProtobuf<V>(value, valueCodec);
+  }
+}
diff --git a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/ProtobufEntryCreator.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/ProtobufEntryCreator.java
new file mode 100644
index 0000000..9feff06
--- /dev/null
+++ b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/ProtobufEntryCreator.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2010 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.EntryCreator;
+import com.google.gwtorm.protobuf.CodecFactory;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+
+class ProtobufEntryCreator<K, V> extends
+    EntryCreator<SerializableProtobuf<K>, SerializableProtobuf<V>> {
+  private final ProtobufCodec<K> keyCodec;
+  private final ProtobufCodec<V> valueCodec;
+  private final EntryCreator<K, V> creator;
+
+  public ProtobufEntryCreator(EntryCreator<K, V> entryCreator,
+      Class<K> keyClass, Class<V> valueClass) {
+    this.keyCodec = CodecFactory.encoder(keyClass);
+    this.valueCodec = CodecFactory.encoder(valueClass);
+    this.creator = entryCreator;
+  }
+
+  @Override
+  public SerializableProtobuf<V> createEntry(SerializableProtobuf<K> key)
+      throws Exception {
+    return new SerializableProtobuf<V>(creator.createEntry(key.toObject(
+        keyCodec, null)), valueCodec);
+  }
+}
\ No newline at end of file
diff --git a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SerializableProtobuf.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SerializableProtobuf.java
new file mode 100644
index 0000000..31a3b5e
--- /dev/null
+++ b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SerializableProtobuf.java
@@ -0,0 +1,123 @@
+// Copyright (C) 2010 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.gwtorm.protobuf.ProtobufCodec;
+import com.google.inject.Provider;
+import com.google.protobuf.CodedOutputStream;
+
+import org.eclipse.jgit.util.IO;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.Arrays;
+
+class SerializableProtobuf<T> implements Serializable {
+  private static final long serialVersionUID = 100L;
+
+  private transient volatile Object data;
+  private transient ProtobufCodec<T> codec;
+  private transient int hash;
+
+  SerializableProtobuf(T object, ProtobufCodec<T> codec) {
+    this.data = object;
+    this.codec = codec;
+    this.hash = object.hashCode();
+  }
+
+  T toObject(ProtobufCodec<T> codec, Provider<T> provider) {
+    if (codec == null) {
+      return null;
+    }
+    Object d = data;
+    if (d instanceof byte[]) {
+      this.codec = codec;
+      if (provider == null) {
+        d = codec.decode((byte[]) d);
+      } else {
+        T tmp = provider.get();
+        codec.mergeFrom((byte[]) d, tmp);
+        d = tmp;
+      }
+      data = d;
+    }
+    return (T) d;
+  }
+
+  @Override
+  public int hashCode() {
+    return hash;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (!(obj instanceof SerializableProtobuf<?>)) {
+      return false;
+    }
+    SerializableProtobuf<T> other = ((SerializableProtobuf<T>) obj);
+
+    if (hash != other.hash) {
+      return false;
+    }
+
+    // Make sure we either both have codecs, or we both do not
+    if (this.codec == null && other.codec != null) {
+      this.codec = other.codec;
+    } else if (this.codec != null && other.codec == null) {
+      other.codec = this.codec;
+    }
+
+    // Equals is only ever called on keys, which cannot have providers
+    T thisObject = this.toObject(codec, null);
+    T otherObject = other.toObject(other.codec, null);
+
+    if (thisObject == null && otherObject == null) {
+      // Neither of us had codecs, so we must compare byte arrays
+      return Arrays.equals((byte[]) this.data, (byte[]) other.data);
+    } else if (thisObject != null && otherObject != null) {
+      return thisObject.equals(otherObject);
+    } else {
+      return false;
+    }
+  }
+
+  private void writeObject(ObjectOutputStream oos) throws IOException {
+    oos.writeInt(hash);
+
+    Object d = data;
+    if (d instanceof byte[]) {
+      byte[] buf = (byte[]) d;
+      oos.writeInt(buf.length);
+      oos.write(buf);
+    } else {
+      // We assume that if we have an object, we must have a codec
+      T obj = (T) d;
+      oos.writeInt(codec.sizeof(obj));
+      CodedOutputStream cos = CodedOutputStream.newInstance(oos);
+      codec.encode(obj, cos);
+      cos.flush();
+    }
+  }
+
+  private void readObject(ObjectInputStream in) throws IOException {
+    hash = in.readInt();
+    int len = in.readInt();
+    byte[] d = new byte[len];
+    IO.readFully(in, d, 0, len);
+    data = d;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/SimpleCache.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java
similarity index 74%
rename from gerrit-server/src/main/java/com/google/gerrit/server/cache/SimpleCache.java
rename to gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java
index 2283f96..f1d356d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/SimpleCache.java
+++ b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java
@@ -12,10 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.cache;
+package com.google.gerrit.ehcache;
 
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gerrit.server.cache.Cache;
+
 import net.sf.ehcache.CacheException;
 import net.sf.ehcache.Ehcache;
 import net.sf.ehcache.Element;
@@ -45,10 +49,11 @@
   }
 
   @SuppressWarnings("unchecked")
-  public V get(final K key) {
+  public ListenableFuture<V> get(final K key) {
     if (key == null) {
-      return null;
+      return Futures.immediateFuture(null);
     }
+
     final Element m;
     try {
       m = self.get(key);
@@ -59,21 +64,27 @@
       log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
       return null;
     }
-    return m != null ? (V) m.getObjectValue() : null;
+    if (m != null) {
+      return Futures.immediateFuture((V) m.getObjectValue());
+    }
+    return Futures.immediateFuture(null);
   }
 
-  public void put(final K key, final V value) {
+  public ListenableFuture<Void> putAsync(final K key, final V value) {
     self.put(new Element(key, value));
+    return Futures.immediateFuture(null);
   }
 
-  public void remove(final K key) {
+  public ListenableFuture<Void> removeAsync(final K key) {
     if (key != null) {
       self.remove(key);
     }
+    return Futures.immediateFuture(null);
   }
 
-  public void removeAll() {
+  public ListenableFuture<Void> removeAllAsync() {
     self.removeAll();
+    return Futures.immediateFuture(null);
   }
 
   @Override
diff --git a/gerrit-gwtdebug/pom.xml b/gerrit-gwtdebug/pom.xml
index 88246ac..d5a67e1 100644
--- a/gerrit-gwtdebug/pom.xml
+++ b/gerrit-gwtdebug/pom.xml
@@ -71,6 +71,12 @@
     </dependency>
 
     <dependency>
+      <groupId>com.google.protobuf</groupId>
+      <artifactId>protobuf-java</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
       <groupId>bouncycastle</groupId>
       <artifactId>bcprov-jdk15</artifactId>
       <version>140</version>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index ec6b9ed..e25419f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -66,12 +66,12 @@
   public static final GerritResources RESOURCES =
       GWT.create(GerritResources.class);
   public static final SystemInfoService SYSTEM_SVC;
-  private static final String SESSION_COOKIE = "GerritAccount";
 
   private static String myHost;
   private static GerritConfig myConfig;
   private static Account myAccount;
   private static AccountDiffPreference myAccountDiffPref;
+  private static String xsrfToken;
 
   private static TabPanel menuLeft;
   private static LinkMenuBar menuRight;
@@ -212,10 +212,15 @@
   }
 
   static void deleteSessionCookie() {
-    Cookies.removeCookie(SESSION_COOKIE);
     myAccount = null;
     myAccountDiffPref = null;
+    xsrfToken = null;
     refreshMenuBar();
+
+    // If the cookie was HttpOnly, this request to delete it will
+    // most likely not be successful.  We can try anyway though.
+    //
+    Cookies.removeCookie("GerritAccount");
   }
 
   public void onModuleLoad() {
@@ -251,6 +256,7 @@
         myConfig = result.config;
         if (result.account != null) {
           myAccount = result.account;
+          xsrfToken = result.xsrfToken;
         }
         if (result.accountDiffPref != null) {
           myAccountDiffPref = result.accountDiffPref;
@@ -377,7 +383,7 @@
     JsonUtil.setDefaultXsrfManager(new XsrfManager() {
       @Override
       public String getToken(JsonDefTarget proxy) {
-        return Cookies.getCookie(SESSION_COOKIE);
+        return xsrfToken;
       }
 
       @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
index 8c99d45..605d7f3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
@@ -99,18 +99,20 @@
       formBody.add(fp);
     }
 
-    final FlowPanel sshKeyGroup = new FlowPanel();
-    sshKeyGroup.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
-    sshKeyGroup.add(new SmallHeading(Util.C.welcomeSshKeyHeading()));
-    final HTML whySshKey = new HTML(Util.C.welcomeSshKeyText());
-    whySshKey.setStyleName(Gerrit.RESOURCES.css().registerScreenExplain());
-    sshKeyGroup.add(whySshKey);
-    sshKeyGroup.add(new SshPanel() {
-      {
-        setKeyTableVisible(false);
-      }
-    });
-    formBody.add(sshKeyGroup);
+    if (Gerrit.getConfig().getSshdAddress() != null) {
+      final FlowPanel sshKeyGroup = new FlowPanel();
+      sshKeyGroup.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
+      sshKeyGroup.add(new SmallHeading(Util.C.welcomeSshKeyHeading()));
+      final HTML whySshKey = new HTML(Util.C.welcomeSshKeyText());
+      whySshKey.setStyleName(Gerrit.RESOURCES.css().registerScreenExplain());
+      sshKeyGroup.add(whySshKey);
+      sshKeyGroup.add(new SshPanel() {
+        {
+          setKeyTableVisible(false);
+        }
+      });
+      formBody.add(sshKeyGroup);
+    }
 
     final FlowPanel choices = new FlowPanel();
     choices.setStyleName(Gerrit.RESOURCES.css().registerScreenNextLinks());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
index 9356018..97b2efb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
@@ -26,7 +26,9 @@
     link(Util.C.tabPreferences(), PageLinks.SETTINGS_PREFERENCES);
     link(Util.C.tabWatchedProjects(), PageLinks.SETTINGS_PROJECTS);
     link(Util.C.tabContactInformation(), PageLinks.SETTINGS_CONTACT);
-    link(Util.C.tabSshKeys(), PageLinks.SETTINGS_SSHKEYS);
+    if (Gerrit.getConfig().getSshdAddress() != null) {
+      link(Util.C.tabSshKeys(), PageLinks.SETTINGS_SSHKEYS);
+    }
     link(Util.C.tabHttpAccess(), PageLinks.SETTINGS_HTTP_PASSWORD);
     link(Util.C.tabWebIdentities(), PageLinks.SETTINGS_WEBIDENT);
     link(Util.C.tabMyGroups(), PageLinks.SETTINGS_MYGROUPS);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
index f67f12f..2118b9c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
@@ -20,9 +20,10 @@
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
+import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.gwtjsonrpc.server.XsrfException;
 import com.google.inject.Inject;
@@ -132,18 +133,24 @@
       return false;
     }
 
-    final AccountState who = accountCache.getByUsername(username);
-    if (who == null || ! who.getAccount().isActive()) {
+    final AccountExternalId who = FutureUtil.get( //
+        accountCache.get(AccountExternalId.forUsername(username)));
+    if (who == null) {
       rsp.sendError(SC_UNAUTHORIZED);
       return false;
     }
 
-    final String passwd = who.getPassword(username);
+    final String passwd = who.getPassword();
     if (passwd == null) {
       rsp.sendError(SC_UNAUTHORIZED);
       return false;
     }
 
+    if (!FutureUtil.get(accountCache.getAccount(who.getAccountId())).isActive()) {
+      rsp.sendError(SC_UNAUTHORIZED);
+      return false;
+    }
+
     final String A1 = username + ":" + realm + ":" + passwd;
     final String A2 = method + ":" + uri;
 
@@ -158,7 +165,7 @@
     if (expect.equals(response)) {
       try {
         if (tokens.checkToken(nonce, "") != null) {
-          session.get().setUserAccountId(who.getAccount().getId());
+          session.get().setUserAccountId(who.getAccountId());
           return true;
 
         } else {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/SessionCacheCleaner.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/SessionCacheCleaner.java
new file mode 100644
index 0000000..f0ff5d6
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/SessionCacheCleaner.java
@@ -0,0 +1,123 @@
+// Copyright (C) 2010 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.httpd;
+
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.reviewdb.ActiveSession;
+import com.google.gerrit.reviewdb.ActiveSessionAccess;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.ActiveSession.Key;
+import com.google.gerrit.server.cache.Cache;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Timestamp;
+import java.util.LinkedList;
+import java.util.List;
+
+/** Removes expired sessions from the session database cache */
+public class SessionCacheCleaner implements Runnable {
+  private static final Logger log =
+      LoggerFactory.getLogger(SessionCacheCleaner.class);
+
+  public static class Module extends LifecycleModule {
+    @Override
+    protected void configure() {
+      listener().to(Lifecycle.class);
+    }
+  }
+
+  static class Lifecycle implements LifecycleListener {
+    private final WorkQueue queue;
+    private final SessionCacheCleaner cleaner;
+
+    @Inject
+    Lifecycle(final WorkQueue queue, final SessionCacheCleaner cleaner) {
+      this.queue = queue;
+      this.cleaner = cleaner;
+    }
+
+    @Override
+    public void start() {
+      queue.getDefaultQueue().scheduleWithFixedDelay(cleaner, 1, 12, HOURS);
+    }
+
+    @Override
+    public void stop() {
+    }
+  }
+
+  private final SchemaFactory<ReviewDb> reviewDbFactory;
+  private final Cache<Key, ActiveSession> cache;
+
+  @Inject
+  public SessionCacheCleaner(
+      SchemaFactory<ReviewDb> reviewDbFactory,
+      @Named(WebSession.CACHE_NAME) final Cache<ActiveSession.Key, ActiveSession> cache) {
+    this.reviewDbFactory = reviewDbFactory;
+    this.cache = cache;
+  }
+
+  @Override
+  public void run() {
+    try {
+      ReviewDb db = reviewDbFactory.open();
+      try {
+        final ActiveSessionAccess access = db.activeSessions();
+        final List<ActiveSession> expiredSessions =
+            new LinkedList<ActiveSession>();
+        final List<ActiveSession> activeSessions = access.all().toList();
+        final Timestamp now = new Timestamp(System.currentTimeMillis());
+
+        for (ActiveSession as : activeSessions) {
+          if (expiredFromCache(as, now)) {
+            expiredSessions.add(as);
+          }
+        }
+
+        try {
+          access.delete(expiredSessions);
+        } catch (OrmException e) {
+          log.error("Unable to delete expired sessions from database", e);
+        }
+      } finally {
+        db.close();
+      }
+    } catch (OrmException e) {
+      log.error("Unable to fetch sessions from database", e);
+    }
+  }
+
+  private boolean expiredFromCache(ActiveSession as, Timestamp now) {
+    if (as.getLastSeen() == null) {
+      return true;
+    }
+    final Timestamp expireAt =
+        new Timestamp(as.getLastSeen().getTime()
+            + cache.getTimeToLive(MILLISECONDS));
+
+    return now.after(expireAt);
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
index 93b6d09..bf96326 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
@@ -37,12 +37,9 @@
 import com.google.gerrit.server.config.GerritRequestModule;
 import com.google.gerrit.server.contact.ContactStore;
 import com.google.gerrit.server.contact.ContactStoreProvider;
-import com.google.gerrit.server.ssh.SshInfo;
-import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
-import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.servlet.RequestScoped;
 import com.google.inject.servlet.ServletModule;
@@ -52,20 +49,14 @@
 import javax.annotation.Nullable;
 
 public class WebModule extends FactoryModule {
-  private final Provider<SshInfo> sshInfoProvider;
-  private final Provider<SshKeyCache> sshKeyCacheProvider;
   private final AuthType authType;
   private final boolean wantSSL;
   private final GitWebConfig gitWebConfig;
 
   @Inject
-  WebModule(final Provider<SshInfo> sshInfoProvider,
-      final Provider<SshKeyCache> sshKeyCacheProvider,
-      final AuthConfig authConfig,
+  WebModule(final AuthConfig authConfig,
       @CanonicalWebUrl @Nullable final String canonicalUrl,
       final Injector creatingInjector) {
-    this.sshInfoProvider = sshInfoProvider;
-    this.sshKeyCacheProvider = sshKeyCacheProvider;
     this.authType = authConfig.getAuthType();
     this.wantSSL = canonicalUrl != null && canonicalUrl.startsWith("https:");
 
@@ -124,9 +115,6 @@
     install(new GerritRequestModule());
     install(new ProjectServlet.Module());
 
-    bind(SshInfo.class).toProvider(sshInfoProvider);
-    bind(SshKeyCache.class).toProvider(sshKeyCacheProvider);
-
     bind(GitWebConfig.class).toInstance(gitWebConfig);
     if (gitWebConfig.getGitwebCGI() != null) {
       install(new GitWebModule());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
index 90ccdc5..151d7aa 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2010 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.
@@ -14,12 +14,17 @@
 
 package com.google.gerrit.httpd;
 
+import static com.google.gerrit.server.ioutil.BasicSerialization.writeBytes;
+import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
+import static com.google.inject.Scopes.SINGLETON;
 import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
 
-import com.google.gerrit.httpd.WebSessionManager.Key;
-import com.google.gerrit.httpd.WebSessionManager.Val;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountExternalId;
+import com.google.gerrit.reviewdb.ActiveSession;
+import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
@@ -28,76 +33,109 @@
 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.util.FutureUtil;
+import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Module;
+import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
 import com.google.inject.servlet.RequestScoped;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.sql.Timestamp;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 @RequestScoped
 public final class WebSession {
+  private static final Logger log = LoggerFactory.getLogger(WebSession.class);
   private static final String ACCOUNT_COOKIE = "GerritAccount";
+  static final String CACHE_NAME = "web_sessions";
+  private static final long UPDATE_WAIT_MILLISECONDS =
+      MILLISECONDS.convert(5, TimeUnit.MINUTES);
 
   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) //
+        final TypeLiteral<Cache<ActiveSession.Key, ActiveSession>> type =
+            new TypeLiteral<Cache<ActiveSession.Key, ActiveSession>>() {};
+        cache(type, CACHE_NAME) //
             .memoryLimit(1024) // reasonable default for many sites
             .maxAge(12, HOURS) // expire sessions if they are inactive
             .evictionPolicy(EvictionPolicy.LRU) // keep most recently used
         ;
-        bind(WebSessionManager.class);
         bind(WebSession.class).in(RequestScoped.class);
+        bind(WebSession.KeyGenerator.class).in(SINGLETON);
       }
     };
   }
 
+  // We want a singleton SecureRandom, but we don't want to make every
+  // SecureRandom a singleton, so instead we have a KeyGenerator class that can
+  // be used in its place.
+  @Singleton
+  private static class KeyGenerator extends SecureRandom {
+  }
+
+  private static long now() {
+    return System.currentTimeMillis();
+  }
+
+  private final KeyGenerator prng;
+  private final Cache<ActiveSession.Key, ActiveSession> cache;
   private final HttpServletRequest request;
   private final HttpServletResponse response;
-  private final WebSessionManager manager;
   private final AnonymousUser anonymous;
   private final IdentifiedUser.RequestFactory identified;
+  private final ReviewDb schema;
   private AccessPath accessPath = AccessPath.WEB_UI;
   private Cookie outCookie;
 
-  private Key key;
-  private Val val;
+  private ActiveSession.Key key;
+  private ActiveSession session;
 
   @Inject
   WebSession(final HttpServletRequest request,
-      final HttpServletResponse response, final WebSessionManager manager,
-      final AnonymousUser anonymous,
-      final IdentifiedUser.RequestFactory identified) {
+      final HttpServletResponse response, final AnonymousUser anonymous,
+      final IdentifiedUser.RequestFactory identified, ReviewDb schema,
+      @Named(CACHE_NAME) final Cache<ActiveSession.Key, ActiveSession> cache,
+      KeyGenerator prng) throws OrmException {
     this.request = request;
     this.response = response;
-    this.manager = manager;
     this.anonymous = anonymous;
     this.identified = identified;
+    this.schema = schema;
+    this.cache = cache;
+    this.prng = prng;
 
     final String cookie = readCookie();
     if (cookie != null) {
-      key = new Key(cookie);
-      val = manager.get(key);
+      key = new ActiveSession.Key(cookie);
+      session = get(key);
     } else {
       key = null;
-      val = null;
+      session = null;
     }
 
-    if (isSignedIn() && val.needsCookieRefresh()) {
+    if (isSignedIn() && session.needsCookieRefresh()) {
       // Cookie is more than half old. Send the cookie again to the
       // client with an updated expiration date. We don't dare to
       // change the key token here because there may be other RPCs
       // queued up in the browser whose xsrfKey would not get updated
       // with the new token, causing them to fail.
       //
-      val = manager.createVal(key, val);
+      session = createSession(key, session);
       saveCookie();
     }
   }
@@ -116,36 +154,43 @@
   }
 
   public boolean isSignedIn() {
-    return val != null;
+    return session != null;
   }
 
-  String getToken() {
-    return isSignedIn() ? key.getToken() : null;
+  public String getToken() {
+    return isSignedIn() ? session.getXsrfToken() : null;
   }
 
   public boolean isTokenValid(final String inputToken) {
-    return isSignedIn() && key.getToken().equals(inputToken);
+    return isSignedIn() //
+        && session.getXsrfToken() != null //
+        && session.getXsrfToken().equals(inputToken);
   }
 
   public AccountExternalId.Key getLastLoginExternalId() {
-    return val != null ? val.getExternalId() : null;
+    return session != null ? session.getExternalId() : null;
   }
 
   CurrentUser getCurrentUser() {
     if (isSignedIn()) {
-      return identified.create(accessPath, val.getAccountId());
+      return identified.create(accessPath, session.getAccountId());
     }
     return anonymous;
   }
 
-  public void login(final AuthResult res, final boolean rememberMe) {
+  public void login(final AuthResult res, final boolean rememberMe)
+      throws OrmException {
     final Account.Id id = res.getAccountId();
     final AccountExternalId.Key identity = res.getExternalId();
 
-    logout();
+    if (session != null) {
+      destroy(key);
+      key = null;
+      session = null;
+    }
 
-    key = manager.createKey(id);
-    val = manager.createVal(key, id, rememberMe, identity);
+    key = createKey(id);
+    session = createSession(key, id, rememberMe, identity, null);
     saveCookie();
   }
 
@@ -156,15 +201,19 @@
 
   /** Set the user account for this current request only. */
   void setUserAccountId(Account.Id id) {
-    key = new Key("id:" + id);
-    val = new Val(id, 0, false, null);
+    key = new ActiveSession.Key("id:" + id);
+    session = new ActiveSession(key, id, new Timestamp(0), false, null, "");
   }
 
   public void logout() {
-    if (val != null) {
-      manager.destroy(key);
+    if (session != null) {
+      try {
+        destroy(key);
+      } catch (OrmException e) {
+        log.error("Could not remove session key from cache", e);
+      }
       key = null;
-      val = null;
+      session = null;
       saveCookie();
     }
   }
@@ -177,22 +226,160 @@
       token = "";
       ageSeconds = 0 /* erase at client */;
     } else {
-      token = key.getToken();
-      ageSeconds = manager.getCookieAge(val);
+      token = key.get();
+      ageSeconds = getCookieAge(session);
     }
 
-    if (outCookie == null) {
-      String path = request.getContextPath();
-      if (path.equals("")) {
-        path = "/";
-      }
-      outCookie = new Cookie(ACCOUNT_COOKIE, token);
-      outCookie.setPath(path);
-      outCookie.setMaxAge(ageSeconds);
-      response.addCookie(outCookie);
-    } else {
-      outCookie.setValue(token);
-      outCookie.setMaxAge(ageSeconds);
+    String path = request.getContextPath();
+    if (path.equals("")) {
+      path = "/";
     }
+
+    if (outCookie != null) {
+      throw new IllegalStateException("Cookie " + ACCOUNT_COOKIE + " was set");
+    }
+
+    outCookie = new Cookie(ACCOUNT_COOKIE, token);
+    outCookie.setSecure(isSecure(request));
+    outCookie.setPath(path);
+    outCookie.setMaxAge(ageSeconds);
+    response.addCookie(outCookie);
+  }
+
+  private static boolean isSecure(final HttpServletRequest req) {
+    return req.isSecure() || "https".equals(req.getScheme());
+  }
+
+  private ActiveSession.Key createKey(final Account.Id who) {
+    try {
+      final int nonceLen = 20;
+      final ByteArrayOutputStream buf;
+      final byte[] rnd = new byte[nonceLen];
+      prng.nextBytes(rnd);
+
+      buf = new ByteArrayOutputStream(3 + nonceLen);
+      writeVarInt32(buf, who.get());
+      writeBytes(buf, rnd);
+
+      return new ActiveSession.Key(CookieBase64.encode(buf.toByteArray()));
+    } catch (IOException e) {
+      throw new RuntimeException("Cannot produce new account cookie", e);
+    }
+  }
+
+  private ActiveSession createSession(final ActiveSession.Key key,
+      final ActiveSession session) throws OrmException {
+    final Account.Id who = session.getAccountId();
+    final boolean remember = session.isPersistentCookie();
+    final AccountExternalId.Key lastLogin = session.getExternalId();
+    final String xsrfToken = session.getXsrfToken();
+
+    return createSession(key, who, remember, lastLogin, xsrfToken);
+  }
+
+  private ActiveSession createSession(final ActiveSession.Key key,
+      final Account.Id who, final boolean remember,
+      final AccountExternalId.Key lastLogin, String xsrfToken)
+      throws OrmException {
+    // Refresh the cookie every hour or when it is half-expired.
+    // This reduces the odds that the user session will be kicked
+    // early but also avoids us needing to refresh the cookie on
+    // every single request.
+    //
+    final long halfAgeRefresh = cache.getTimeToLive(MILLISECONDS) >>> 1;
+    final long minRefresh = MILLISECONDS.convert(1, HOURS);
+    final long refresh = Math.min(halfAgeRefresh, minRefresh);
+    final long refreshCookieAt = now() + refresh;
+
+    if (xsrfToken == null) {
+      // If we don't yet have a token for this session, establish one.
+      //
+      final int nonceLen = 20;
+      final ByteArrayOutputStream buf;
+      final byte[] rnd = new byte[nonceLen];
+      prng.nextBytes(rnd);
+      xsrfToken = CookieBase64.encode(rnd);
+    }
+
+    ActiveSession session =
+        new ActiveSession(key, who, new Timestamp(refreshCookieAt), remember,
+            lastLogin, xsrfToken);
+    put(session);
+    return session;
+  }
+
+  private int getCookieAge(final ActiveSession session) {
+    if (session.isPersistentCookie()) {
+      // Client may store the cookie until we would remove it from our
+      // own cache, after which it will certainly be invalid.
+      //
+      return (int) cache.getTimeToLive(SECONDS);
+    } else {
+      // Client should not store the cookie, as the user asked for us
+      // to not remember them long-term. Sending -1 as the age will
+      // cause the cookie to be only for this "browser session", which
+      // is usually until the user exits their browser.
+      //
+      return -1;
+    }
+  }
+
+  private ActiveSession get(final ActiveSession.Key key) throws OrmException {
+    ActiveSession as = FutureUtil.get(cache.get(key));
+    if (as == null) {
+      as = schema.activeSessions().get(key);
+
+      if (as == null) {
+        return null;
+      } else {
+        if (expiredFromCache(as)) {
+          destroy(key);
+          return null;
+        } else if (needsCacheRefresh(as)) {
+          as.updateLastSeen();
+          put(as);
+        }
+        return as;
+      }
+    } else {
+      if (needsCacheRefresh(as)) {
+        as.updateLastSeen();
+        put(as);
+      }
+      return as;
+    }
+  }
+
+  private boolean needsCacheRefresh(ActiveSession as) {
+    if (as.getLastSeen() == null) {
+      return true;
+    }
+    Timestamp refreshAt =
+        new Timestamp(as.getLastSeen().getTime() + UPDATE_WAIT_MILLISECONDS);
+    Timestamp now = new Timestamp(now());
+
+    return now.after(refreshAt);
+  }
+
+  private boolean expiredFromCache(ActiveSession as) {
+    if (as.getLastSeen() == null) {
+      return true;
+    }
+    Timestamp expireAt =
+        new Timestamp(as.getLastSeen().getTime()
+            + cache.getTimeToLive(MILLISECONDS));
+    Timestamp now = new Timestamp(now());
+
+    return now.after(expireAt);
+  }
+
+  private void destroy(final ActiveSession.Key key) throws OrmException {
+    schema.activeSessions().deleteKeys(Arrays.asList(key));
+    FutureUtil.waitFor(cache.removeAsync(key));
+  }
+
+  private void put(final ActiveSession as) throws OrmException {
+    schema.activeSessions().upsert(Arrays.asList(as));
+    FutureUtil.waitFor(cache.putAsync(as.getKey(), as));
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
deleted file mode 100644
index 43c99ad..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
+++ /dev/null
@@ -1,232 +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.httpd;
-
-import static com.google.gerrit.server.ioutil.BasicSerialization.readFixInt64;
-import static com.google.gerrit.server.ioutil.BasicSerialization.readString;
-import static com.google.gerrit.server.ioutil.BasicSerialization.readVarInt32;
-import static com.google.gerrit.server.ioutil.BasicSerialization.writeBytes;
-import static com.google.gerrit.server.ioutil.BasicSerialization.writeFixInt64;
-import static com.google.gerrit.server.ioutil.BasicSerialization.writeString;
-import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
-import static java.util.concurrent.TimeUnit.HOURS;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.server.cache.Cache;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import com.google.inject.name.Named;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.io.Serializable;
-import java.security.SecureRandom;
-
-@Singleton
-class WebSessionManager {
-  static final String CACHE_NAME = "web_sessions";
-
-  static long now() {
-    return System.currentTimeMillis();
-  }
-
-  private final SecureRandom prng;
-  private final Cache<Key, Val> self;
-
-  @Inject
-  WebSessionManager(@Named(CACHE_NAME) final Cache<Key, Val> cache) {
-    prng = new SecureRandom();
-    self = cache;
-  }
-
-  Key createKey(final Account.Id who) {
-    try {
-      final int nonceLen = 20;
-      final ByteArrayOutputStream buf;
-      final byte[] rnd = new byte[nonceLen];
-      prng.nextBytes(rnd);
-
-      buf = new ByteArrayOutputStream(3 + nonceLen);
-      writeVarInt32(buf, (int) Key.serialVersionUID);
-      writeVarInt32(buf, who.get());
-      writeBytes(buf, rnd);
-
-      return new Key(CookieBase64.encode(buf.toByteArray()));
-    } catch (IOException e) {
-      throw new RuntimeException("Cannot produce new account cookie", e);
-    }
-  }
-
-  Val createVal(final Key key, final Val val) {
-    final Account.Id who = val.getAccountId();
-    final boolean remember = val.isPersistentCookie();
-    final AccountExternalId.Key lastLogin = val.getExternalId();
-
-    return createVal(key, who, remember, lastLogin);
-  }
-
-  Val createVal(final Key key, final Account.Id who, final boolean remember,
-      final AccountExternalId.Key lastLogin) {
-    // Refresh the cookie every hour or when it is half-expired.
-    // This reduces the odds that the user session will be kicked
-    // early but also avoids us needing to refresh the cookie on
-    // every single request.
-    //
-    final long halfAgeRefresh = self.getTimeToLive(MILLISECONDS) >>> 1;
-    final long minRefresh = MILLISECONDS.convert(1, HOURS);
-    final long refresh = Math.min(halfAgeRefresh, minRefresh);
-    final long refreshCookieAt = now() + refresh;
-
-    final Val val = new Val(who, refreshCookieAt, remember, lastLogin);
-    self.put(key, val);
-    return val;
-  }
-
-  int getCookieAge(final Val val) {
-    if (val.isPersistentCookie()) {
-      // Client may store the cookie until we would remove it from our
-      // own cache, after which it will certainly be invalid.
-      //
-      return (int) self.getTimeToLive(SECONDS);
-    } else {
-      // Client should not store the cookie, as the user asked for us
-      // to not remember them long-term. Sending -1 as the age will
-      // cause the cookie to be only for this "browser session", which
-      // is usually until the user exits their browser.
-      //
-      return -1;
-    }
-  }
-
-  Val get(final Key key) {
-    return self.get(key);
-  }
-
-  void destroy(final Key key) {
-    self.remove(key);
-  }
-
-  static final class Key implements Serializable {
-    static final long serialVersionUID = 2L;
-
-    private transient String token;
-
-    Key(final String t) {
-      token = t;
-    }
-
-    String getToken() {
-      return token;
-    }
-
-    @Override
-    public int hashCode() {
-      return token.hashCode();
-    }
-
-    @Override
-    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;
-
-    private transient Account.Id accountId;
-    private transient long refreshCookieAt;
-    private transient boolean persistentCookie;
-    private transient AccountExternalId.Key externalId;
-
-    Val(final Account.Id accountId, final long refreshCookieAt,
-        final boolean persistentCookie, final AccountExternalId.Key externalId) {
-      this.accountId = accountId;
-      this.refreshCookieAt = refreshCookieAt;
-      this.persistentCookie = persistentCookie;
-      this.externalId = externalId;
-    }
-
-    Account.Id getAccountId() {
-      return accountId;
-    }
-
-    AccountExternalId.Key getExternalId() {
-      return externalId;
-    }
-
-    boolean needsCookieRefresh() {
-      return refreshCookieAt <= now();
-    }
-
-    boolean isPersistentCookie() {
-      return persistentCookie;
-    }
-
-    private void writeObject(final ObjectOutputStream out) throws IOException {
-      writeVarInt32(out, 1);
-      writeVarInt32(out, accountId.get());
-
-      writeVarInt32(out, 2);
-      writeFixInt64(out, refreshCookieAt);
-
-      writeVarInt32(out, 3);
-      writeVarInt32(out, persistentCookie ? 1 : 0);
-
-      if (externalId != null) {
-        writeVarInt32(out, 4);
-        writeString(out, externalId.get());
-      }
-
-      writeVarInt32(out, 0);
-    }
-
-    private void readObject(final ObjectInputStream in) throws IOException {
-      PARSE: for (;;) {
-        final int tag = readVarInt32(in);
-        switch (tag) {
-          case 0:
-            break PARSE;
-          case 1:
-            accountId = new Account.Id(readVarInt32(in));
-            continue;
-          case 2:
-            refreshCookieAt = readFixInt64(in);
-            continue;
-          case 3:
-            persistentCookie = readVarInt32(in) != 0;
-            continue;
-          case 4:
-            externalId = new AccountExternalId.Key(readString(in));
-            continue;
-          default:
-            throw new IOException("Unknown tag found in object: " + tag);
-        }
-      }
-    }
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSshGlueModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSshGlueModule.java
new file mode 100644
index 0000000..0de74b1
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSshGlueModule.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2010 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.httpd;
+
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+/**
+ * Pulls objects from the SSH injector over the HTTP injector.
+ * <p>
+ * This mess is only necessary because we build up two different injectors, in
+ * order to have different request scopes. But some HTTP RPCs can cause changes
+ * to the SSH side of the house, and thus needs access to it.
+ */
+public class WebSshGlueModule extends AbstractModule {
+  private final Provider<SshInfo> sshInfoProvider;
+  private final Provider<SshKeyCache> sshKeyCacheProvider;
+
+  @Inject
+  WebSshGlueModule(Provider<SshInfo> sshInfoProvider,
+      Provider<SshKeyCache> sshKeyCacheProvider) {
+    this.sshInfoProvider = sshInfoProvider;
+    this.sshKeyCacheProvider = sshKeyCacheProvider;
+  }
+
+  @Override
+  protected void configure() {
+    bind(SshInfo.class).toProvider(sshInfoProvider);
+    bind(SshKeyCache.class).toProvider(sshKeyCacheProvider);
+
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index c3f7de1..915e925 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -14,19 +14,19 @@
 
 package com.google.gerrit.httpd.auth.become;
 
-import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
-
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
@@ -58,18 +58,20 @@
   private final Provider<WebSession> webSession;
   private final Provider<String> urlProvider;
   private final AccountManager accountManager;
+  private final AccountCache accountCache;
   private final byte[] raw;
 
   @Inject
   BecomeAnyAccountLoginServlet(final Provider<WebSession> ws,
       final SchemaFactory<ReviewDb> sf,
       final @CanonicalWebUrl @Nullable Provider<String> up,
-      final AccountManager am, final ServletContext servletContext)
-      throws IOException {
+      final AccountManager am, final ServletContext servletContext,
+      final AccountCache ac) throws IOException {
     webSession = ws;
     schema = sf;
     urlProvider = up;
     accountManager = am;
+    accountCache = ac;
 
     final String pageName = "BecomeAnyAccount.html";
     final Document doc = HtmlDomUtil.parseFile(getClass(), pageName);
@@ -125,7 +127,21 @@
     }
 
     if (res != null) {
-      webSession.get().login(res, false);
+      try {
+        webSession.get().login(res, false);
+      } catch (OrmException e) {
+        rsp.setContentType("text/html");
+        rsp.setCharacterEncoding(HtmlDomUtil.ENC);
+        final Writer out = rsp.getWriter();
+        out.write("<html>");
+        out.write("<body>");
+        out.write("<h1>Could not log in</h1>");
+        out.write("</body>");
+        out.write("</html>");
+        out.close();
+        log("Could not log in", e);
+        return;
+      }
       final StringBuilder rdr = new StringBuilder();
       rdr.append(urlProvider.get());
       if (IS_DEV && req.getParameter("gwt.codesvr") != null) {
@@ -173,19 +189,8 @@
 
   private AuthResult byUserName(final HttpServletResponse rsp,
       final String userName) {
-    try {
-      final ReviewDb db = schema.open();
-      try {
-        AccountExternalId.Key key =
-            new AccountExternalId.Key(SCHEME_USERNAME, userName);
-        return auth(db.accountExternalIds().get(key));
-      } finally {
-        db.close();
-      }
-    } catch (OrmException e) {
-      getServletContext().log("cannot query database", e);
-      return null;
-    }
+    AccountExternalId.Key key = AccountExternalId.forUsername(userName);
+    return auth(FutureUtil.get(accountCache.get(key)));
   }
 
   private AuthResult byPreferredEmail(final HttpServletResponse rsp,
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index 58f589a..b3e1840 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -36,6 +37,7 @@
 import org.w3c.dom.NodeList;
 
 import java.io.IOException;
+import java.io.Writer;
 
 import javax.annotation.Nullable;
 import javax.servlet.ServletException;
@@ -136,7 +138,21 @@
     }
     rdr.append(token);
 
-    webSession.get().login(arsp, true /* persistent cookie */);
+    try {
+      webSession.get().login(arsp, true /* persistent cookie */);
+    } catch (OrmException e) {
+      log.error("Unable to log in user \"" + user + "\"", e);
+      rsp.setContentType("text/html");
+      rsp.setCharacterEncoding(HtmlDomUtil.ENC);
+      final Writer out = rsp.getWriter();
+      out.write("<html>");
+      out.write("<body>");
+      out.write("<h1>An error occurred while attempting to log in</h1>");
+      out.write("</body>");
+      out.write("</html>");
+      out.close();
+      return;
+    }
     rsp.sendRedirect(rdr.toString());
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java
index a59d013..c8c05ce 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java
@@ -22,12 +22,18 @@
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 class UserPassAuthServiceImpl implements UserPassAuthService {
   private final Provider<WebSession> webSession;
   private final AccountManager accountManager;
+  private final Logger log =
+      LoggerFactory.getLogger(UserPassAuthServiceImpl.class);
 
   @Inject
   UserPassAuthServiceImpl(final Provider<WebSession> webSession,
@@ -63,7 +69,14 @@
 
     result.success = true;
     result.isNew = res.isNew();
-    webSession.get().login(res, true /* persistent cookie */);
+    try {
+      webSession.get().login(res, true /* persistent cookie */);
+    } catch (OrmException e) {
+      log.error("Unable to log in", e);
+      result.success = false;
+      callback.onSuccess(result);
+      return;
+    }
     callback.onSuccess(result);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index 068855f..4b5e2fe 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -415,7 +416,13 @@
             lastId.setMaxAge(0);
           }
           rsp.addCookie(lastId);
-          webSession.get().login(arsp, remember);
+          try {
+            webSession.get().login(arsp, remember);
+          } catch (OrmException e) {
+            log.error("Unable to log in",e);
+            cancelWithError(req, rsp, "Cannot start new session");
+            return;
+          }
           if (arsp.isNew() && claimedIdentifier != null) {
             final com.google.gerrit.server.account.AuthRequest linkReq =
                 new com.google.gerrit.server.account.AuthRequest(
@@ -429,7 +436,11 @@
 
         case LINK_IDENTIY: {
           arsp = accountManager.link(identifiedUser.get().getAccountId(), areq);
-          webSession.get().login(arsp, remember);
+          try {
+            webSession.get().login(arsp, remember);
+          } catch (OrmException e) {
+            log.error("Unable to log in",e);
+          }
           callback(false, req, rsp);
           break;
         }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index 4521348..176a71f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.HostPageData;
 import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.SitePaths;
@@ -61,6 +62,7 @@
   private static final String HPD_ID = "gerrit_hostpagedata";
 
   private final Provider<CurrentUser> currentUser;
+  private final Provider<WebSession> session;
   private final GerritConfig config;
   private final SitePaths site;
   private final Document template;
@@ -69,10 +71,11 @@
   private volatile Page page;
 
   @Inject
-  HostPageServlet(final Provider<CurrentUser> cu, final SitePaths sp,
-      final GerritConfig gc, final ServletContext servletContext)
-      throws IOException, ServletException {
+  HostPageServlet(final Provider<CurrentUser> cu, final Provider<WebSession> w,
+      final SitePaths sp, final GerritConfig gc,
+      final ServletContext servletContext) throws IOException, ServletException {
     currentUser = cu;
+    session = w;
     config = gc;
     site = sp;
 
@@ -158,10 +161,17 @@
       w.write(HPD_ID + ".account=");
       json(((IdentifiedUser) user).getAccount(), w);
       w.write(";");
+
       w.write(HPD_ID + ".accountDiffPref=");
       json(((IdentifiedUser) user).getAccountDiffPreference(), w);
       w.write(";");
 
+      if (session.get().getToken() != null) {
+        w.write(HPD_ID + ".xsrfToken=");
+        json(session.get().getToken(), w);
+        w.write(";");
+      }
+
       final byte[] userData = w.toString().getBytes("UTF-8");
 
       raw = concat(page.part1, userData, page.part2);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
index 332e262..88769ad 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.reviewdb.PatchSetApproval;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.StarredChange;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
 import com.google.gerrit.server.project.ChangeControl;
@@ -354,7 +355,7 @@
 
   private abstract class QueryPrev extends QueryNext {
     QueryPrev(int pageSize, String pos) {
-      super(pageSize, pos);
+      super(pageSize, ChangeUtil.invertSortKey(pos));
     }
 
     @Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
index 8f7754a..d23ef5d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
@@ -52,8 +52,8 @@
   protected GsonBuilder createGsonBuilder() {
     final GsonBuilder g = super.createGsonBuilder();
 
-    g.registerTypeAdapter(org.eclipse.jgit.diff.Edit.class,
-        new org.eclipse.jgit.diff.EditDeserializer());
+    g.registerTypeAdapter(com.google.gerrit.prettify.common.LineEdit.class,
+        new com.google.gerrit.prettify.server.EditDeserializer());
 
     return g;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
index 864c550..4595520 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.rpc;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.common.data.SuggestService;
 import com.google.gerrit.reviewdb.Account;
@@ -25,6 +27,7 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
@@ -34,6 +37,7 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Future;
 
 class SuggestServiceImpl extends BaseServiceImplementation implements
     SuggestService {
@@ -62,15 +66,20 @@
         final int max = 10;
         final int n = limit <= 0 ? max : Math.min(limit, max);
 
-        final CurrentUser user = currentUser.get();
-        final List<Project.NameKey> r = new ArrayList<Project.NameKey>();
-        for (final Project p : db.projects().suggestByName(a, b, n)) {
-          final ProjectState e = projectCache.get(p.getNameKey());
+        List<Future<ProjectState>> want = Lists.newArrayList();
+        for (Project p : db.projects().suggestByName(a, b, n)) {
+          want.add(projectCache.get(p.getNameKey()));
+        }
+
+        CurrentUser user = currentUser.get();
+        List<Project.NameKey> res = Lists.newArrayList();
+        for (Future<ProjectState> f : want) {
+          ProjectState e = FutureUtil.getOrNull(f);
           if (e != null && e.controlFor(user).isVisible()) {
-            r.add(p.getNameKey());
+            res.add(e.getProject().getNameKey());
           }
         }
-        return r;
+        return res;
       }
     });
   }
@@ -84,40 +93,44 @@
         final int max = 10;
         final int n = limit <= 0 ? max : Math.min(limit, max);
 
-        final LinkedHashMap<Account.Id, AccountInfo> r =
-            new LinkedHashMap<Account.Id, AccountInfo>();
-        for (final Account p : db.accounts().suggestByFullName(a, b, n)) {
-          addSuggestion(r, p, new AccountInfo(p), active);
-        }
-        if (r.size() < n) {
-          for (final Account p : db.accounts().suggestByPreferredEmail(a, b,
-              n - r.size())) {
-            addSuggestion(r, p, new AccountInfo(p), active);
+        LinkedHashMap<Account.Id, AccountInfo> res = Maps.newLinkedHashMap();
+        for (Account p : db.accounts().suggestByFullName(a, b, n)) {
+          if (active == null || active == p.isActive()) {
+            res.put(p.getId(), new AccountInfo(p));
           }
         }
-        if (r.size() < n) {
-          for (final AccountExternalId e : db.accountExternalIds()
-              .suggestByEmailAddress(a, b, n - r.size())) {
-            if (!r.containsKey(e.getAccountId())) {
-              final Account p = accountCache.get(e.getAccountId()).getAccount();
-              final AccountInfo info = new AccountInfo(p);
-              info.setPreferredEmail(e.getEmailAddress());
-              addSuggestion(r, p, info, active);
+        if (res.size() < n) {
+          for (Account p : db.accounts().suggestByPreferredEmail(a, b,
+              n - res.size())) {
+            if (active == null || active == p.isActive()) {
+              res.put(p.getId(), new AccountInfo(p));
             }
           }
         }
-        return new ArrayList<AccountInfo>(r.values());
+        if (res.size() < n) {
+          Map<String, Future<Account>> want = Maps.newHashMap();
+          for (AccountExternalId e : db.accountExternalIds()
+              .suggestByEmailAddress(a, b, n - res.size())) {
+            if (!res.containsKey(e.getAccountId())) {
+              want.put(e.getEmailAddress(), //
+                  accountCache.getAccount(e.getAccountId()));
+            }
+          }
+
+          for (Map.Entry<String, Future<Account>> ent : want.entrySet()) {
+            Account p = FutureUtil.get(ent.getValue());
+            if (active == null || active == p.isActive()) {
+              AccountInfo info = new AccountInfo(p);
+              info.setPreferredEmail(ent.getKey());
+              res.put(p.getId(), info);
+            }
+          }
+        }
+        return new ArrayList<AccountInfo>(res.values());
       }
     });
   }
 
-  private void addSuggestion(Map map, Account account, AccountInfo info,
-      Boolean active) {
-    if (active == null || active == account.isActive()) {
-      map.put(account.getId(), info);
-    }
-  }
-
   public void suggestAccountGroup(final String query, final int limit,
       final AsyncCallback<List<AccountGroupName>> callback) {
     run(callback, new Action<List<AccountGroupName>>() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
index 77662a1..f1aa8b9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.common.data.AccountSecurity;
 import com.google.gerrit.common.errors.ContactInformationStoreException;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
@@ -31,7 +32,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountByEmailCache;
+import com.google.gerrit.server.account.AccountAgreementsCache;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
@@ -45,6 +46,7 @@
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.RegisterNewEmailSender;
 import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtjsonrpc.client.VoidResult;
 import com.google.gwtjsonrpc.server.ValidToken;
@@ -52,6 +54,7 @@
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.internal.Lists;
 
 import org.eclipse.jgit.util.Base64;
 import org.slf4j.Logger;
@@ -71,8 +74,8 @@
   private final Provider<IdentifiedUser> user;
   private final RegisterNewEmailSender.Factory registerNewEmailFactory;
   private final SshKeyCache sshKeyCache;
-  private final AccountByEmailCache byEmailCache;
   private final AccountCache accountCache;
+  private final AccountAgreementsCache accountAgreementsCache;
   private final AccountManager accountManager;
   private final boolean useContactInfo;
 
@@ -88,7 +91,7 @@
       final Provider<CurrentUser> currentUser, final ContactStore cs,
       final AuthConfig ac, final Realm r, final Provider<IdentifiedUser> u,
       final RegisterNewEmailSender.Factory esf, final SshKeyCache skc,
-      final AccountByEmailCache abec, final AccountCache uac,
+      final AccountCache uac, final AccountAgreementsCache aac,
       final AccountManager am,
       final ClearPassword.Factory clearPasswordFactory,
       final GeneratePassword.Factory generatePasswordFactory,
@@ -103,8 +106,8 @@
     user = u;
     registerNewEmailFactory = esf;
     sshKeyCache = skc;
-    byEmailCache = abec;
     accountCache = uac;
+    accountAgreementsCache = aac;
     accountManager = am;
 
     useContactInfo = contactStore != null && contactStore.isEnabled();
@@ -143,7 +146,7 @@
           throw new Failure(e);
         }
         db.accountSshKeys().insert(Collections.singleton(key));
-        uncacheSshKeys();
+        FutureUtil.waitFor(sshKeyCache.evictAsync(user.get().getUserName()));
         return key;
       }
     });
@@ -160,17 +163,12 @@
         }
 
         db.accountSshKeys().deleteKeys(ids);
-        uncacheSshKeys();
-
+        FutureUtil.waitFor(sshKeyCache.evictAsync(user.get().getUserName()));
         return VoidResult.INSTANCE;
       }
     });
   }
 
-  private void uncacheSshKeys() {
-    sshKeyCache.evict(user.get().getUserName());
-  }
-
   @Override
   public void changeUserName(final String newName,
       final AsyncCallback<VoidResult> callback) {
@@ -211,6 +209,8 @@
       final ContactInformation info, final AsyncCallback<Account> callback) {
     run(callback, new Action<Account>() {
       public Account run(ReviewDb db) throws OrmException, Failure {
+        List<ListenableFuture<Void>> evictions = Lists.newArrayList();
+
         final Account me = db.accounts().get(user.get().getAccountId());
         final String oldEmail = me.getPreferredEmail();
         if (realm.allowsEdit(Account.FieldName.FULL_NAME)) {
@@ -232,10 +232,11 @@
         }
         db.accounts().update(Collections.singleton(me));
         if (!eq(oldEmail, me.getPreferredEmail())) {
-          byEmailCache.evict(oldEmail);
-          byEmailCache.evict(me.getPreferredEmail());
+          evictions.add(accountCache.evictEmailAsync(oldEmail));
+          evictions.add(accountCache.evictEmailAsync(me.getPreferredEmail()));
         }
-        accountCache.evict(me.getId());
+        evictions.add(accountCache.evictAsync(me.getId()));
+        FutureUtil.waitFor(evictions);
         return me;
       }
     });
@@ -264,6 +265,7 @@
           a.review(AccountAgreement.Status.VERIFIED, null);
         }
         db.accountAgreements().insert(Collections.singleton(a));
+        FutureUtil.waitFor(accountAgreementsCache.evictAsync(a.getKey()));
         return VoidResult.INSTANCE;
       }
     });
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
index 4fd8e28..0d16f81 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
@@ -28,10 +28,13 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountDiffPreferencesCache;
+import com.google.gerrit.server.account.AccountProjectWatchCache;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtjsonrpc.client.VoidResult;
 import com.google.gwtorm.client.OrmDuplicateKeyException;
@@ -51,6 +54,8 @@
   private final AccountCache accountCache;
   private final ProjectControl.Factory projectControlFactory;
   private final AgreementInfoFactory.Factory agreementInfoFactory;
+  private final AccountProjectWatchCache accountProjectWatchCache;
+  private final AccountDiffPreferencesCache accountDiffPreferencesCache;
   private final ChangeQueryBuilder.Factory queryBuilder;
 
   @Inject
@@ -59,12 +64,16 @@
       final AccountCache accountCache,
       final ProjectControl.Factory projectControlFactory,
       final AgreementInfoFactory.Factory agreementInfoFactory,
+      final AccountProjectWatchCache accountProjectWatchCache,
+      final AccountDiffPreferencesCache accountDiffPreferencesCache,
       final ChangeQueryBuilder.Factory queryBuilder) {
     super(schema, identifiedUser);
     this.currentUser = identifiedUser;
     this.accountCache = accountCache;
     this.projectControlFactory = projectControlFactory;
     this.agreementInfoFactory = agreementInfoFactory;
+    this.accountProjectWatchCache = accountProjectWatchCache;
+    this.accountDiffPreferencesCache = accountDiffPreferencesCache;
     this.queryBuilder = queryBuilder;
   }
 
@@ -92,7 +101,7 @@
         }
         a.setGeneralPreferences(pref);
         db.accounts().update(Collections.singleton(a));
-        accountCache.evict(a.getId());
+        FutureUtil.waitFor(accountCache.evictAsync(a.getId()));
         return VoidResult.INSTANCE;
       }
     });
@@ -110,6 +119,7 @@
               + " the accountId of the signed in user " + getAccountId());
         }
         db.accountDiffPreferences().upsert(Collections.singleton(diffPref));
+        FutureUtil.waitFor(accountDiffPreferencesCache.evictAsync(accountId));
         return VoidResult.INSTANCE;
       }
     });
@@ -122,13 +132,14 @@
         final List<AccountProjectWatchInfo> r =
             new ArrayList<AccountProjectWatchInfo>();
 
-        for (final AccountProjectWatch w : db.accountProjectWatches()
-            .byAccount(getAccountId()).toList()) {
+        for (AccountProjectWatch w : FutureUtil.get(accountProjectWatchCache
+            .byAccount(getAccountId()))) {
           final ProjectControl ctl;
           try {
             ctl = projectControlFactory.validateFor(w.getProjectNameKey());
           } catch (NoSuchProjectException e) {
             db.accountProjectWatches().delete(Collections.singleton(w));
+            accountProjectWatchCache.evictAsync(w.getKey());
             continue;
           }
           r.add(new AccountProjectWatchInfo(w, ctl.getProject()));
@@ -171,6 +182,7 @@
         } catch (OrmDuplicateKeyException alreadyHave) {
           watch = db.accountProjectWatches().get(watch.getKey());
         }
+        FutureUtil.waitFor(accountProjectWatchCache.evictAsync(watch.getKey()));
         return new AccountProjectWatchInfo(watch, ctl.getProject());
       }
     });
@@ -186,6 +198,7 @@
     run(callback, new Action<VoidResult>() {
       public VoidResult run(ReviewDb db) throws OrmException {
         db.accountProjectWatches().update(Collections.singleton(watch));
+        FutureUtil.waitFor(accountProjectWatchCache.evictAsync(watch.getKey()));
         return VoidResult.INSTANCE;
       }
     });
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
index f638d48..bc45775 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
@@ -14,21 +14,29 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.common.data.AgreementInfo;
 import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.AbstractAgreement;
 import com.google.gerrit.reviewdb.AccountAgreement;
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.AccountGroupAgreement;
 import com.google.gerrit.reviewdb.ContributorAgreement;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountAgreementsCache;
+import com.google.gerrit.server.account.AccountGroupAgreementsCache;
+import com.google.gerrit.server.util.FutureUtil;
+import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 
-import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Future;
 
 class AgreementInfoFactory extends Handler<AgreementInfo> {
   interface Factory {
@@ -37,52 +45,61 @@
 
   private final ReviewDb db;
   private final IdentifiedUser user;
+  private final AccountAgreementsCache accountAgreementsCache;
+  private final AccountGroupAgreementsCache accountGroupAgreementsCache;
 
   private AgreementInfo info;
 
   @Inject
-  AgreementInfoFactory(final ReviewDb db, final IdentifiedUser user) {
+  AgreementInfoFactory(final ReviewDb db, final IdentifiedUser user,
+      final AccountAgreementsCache aac, final AccountGroupAgreementsCache agac) {
     this.db = db;
     this.user = user;
+    this.accountAgreementsCache = aac;
+    this.accountGroupAgreementsCache = agac;
   }
 
   @Override
   public AgreementInfo call() throws Exception {
-    final List<AccountAgreement> userAccepted =
-        db.accountAgreements().byAccount(user.getAccountId()).toList();
+    Future<List<AccountAgreement>> wantUser =
+        accountAgreementsCache.byAccount(user.getAccountId());
 
-    Collections.reverse(userAccepted);
-
-    final List<AccountGroupAgreement> groupAccepted =
-        new ArrayList<AccountGroupAgreement>();
-    for (final AccountGroup.Id groupId : user.getEffectiveGroups()) {
-      final List<AccountGroupAgreement> temp =
-          db.accountGroupAgreements().byGroup(groupId).toList();
-
-      Collections.reverse(temp);
-
-      groupAccepted.addAll(temp);
+    List<ListenableFuture<List<AccountGroupAgreement>>> wantGroup =
+        Lists.newArrayList();
+    for (AccountGroup.Id groupId : user.getEffectiveGroups()) {
+      wantGroup.add(accountGroupAgreementsCache.byGroup(groupId));
     }
 
-    final Map<ContributorAgreement.Id, ContributorAgreement> agreements =
-        new HashMap<ContributorAgreement.Id, ContributorAgreement>();
-    for (final AccountAgreement a : userAccepted) {
-      final ContributorAgreement.Id id = a.getAgreementId();
-      if (!agreements.containsKey(id)) {
-        agreements.put(id, db.contributorAgreements().get(id));
-      }
-    }
-    for (final AccountGroupAgreement a : groupAccepted) {
-      final ContributorAgreement.Id id = a.getAgreementId();
-      if (!agreements.containsKey(id)) {
-        agreements.put(id, db.contributorAgreements().get(id));
-      }
-    }
+    List<AccountAgreement> userAccepted = FutureUtil.getOrEmptyList(wantUser);
+    List<AccountGroupAgreement> groupAccepted =
+        FutureUtil.getOrEmptyList(FutureUtil.concat(wantGroup));
+
+    Collections.sort(userAccepted, AbstractAgreement.SORT);
+    Collections.sort(groupAccepted, AbstractAgreement.SORT);
 
     info = new AgreementInfo();
     info.setUserAccepted(userAccepted);
     info.setGroupAccepted(groupAccepted);
-    info.setAgreements(agreements);
+    info.setAgreements(agreements(userAccepted, groupAccepted));
     return info;
   }
+
+  private Map<ContributorAgreement.Id, ContributorAgreement> agreements(
+      List<AccountAgreement> userAccepted,
+      List<AccountGroupAgreement> groupAccepted) throws OrmException {
+    Map<ContributorAgreement.Id, ContributorAgreement> all = Maps.newHashMap();
+    for (AccountAgreement a : userAccepted) {
+      ContributorAgreement.Id id = a.getAgreementId();
+      if (!all.containsKey(id)) {
+        all.put(id, db.contributorAgreements().get(id));
+      }
+    }
+    for (AccountGroupAgreement a : groupAccepted) {
+      ContributorAgreement.Id id = a.getAgreementId();
+      if (!all.containsKey(id)) {
+        all.put(id, db.contributorAgreements().get(id));
+      }
+    }
+    return all;
+  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/CreateGroup.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/CreateGroup.java
index 74e2966..6c10f9c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/CreateGroup.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/CreateGroup.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmDuplicateKeyException;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
@@ -83,7 +84,7 @@
         Collections.singleton(new AccountGroupMemberAudit(member, me)));
     db.accountGroupMembers().insert(Collections.singleton(member));
 
-    accountCache.evict(me);
+    FutureUtil.waitFor(accountCache.evictAsync(me));
     return id;
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/DeleteExternalIds.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/DeleteExternalIds.java
index 4edd571..3c2cb15 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/DeleteExternalIds.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/DeleteExternalIds.java
@@ -14,15 +14,17 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountByEmailCache;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import com.google.inject.internal.Lists;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -39,7 +41,6 @@
   private final ReviewDb db;
   private final IdentifiedUser user;
   private final ExternalIdDetailFactory detailFactory;
-  private final AccountByEmailCache byEmailCache;
   private final AccountCache accountCache;
 
   private final Set<AccountExternalId.Key> keys;
@@ -47,13 +48,12 @@
   @Inject
   DeleteExternalIds(final ReviewDb db, final IdentifiedUser user,
       final ExternalIdDetailFactory detailFactory,
-      final AccountByEmailCache byEmailCache, final AccountCache accountCache,
+      final AccountCache accountCache,
 
       @Assisted final Set<AccountExternalId.Key> keys) {
     this.db = db;
     this.user = user;
     this.detailFactory = detailFactory;
-    this.byEmailCache = byEmailCache;
     this.accountCache = accountCache;
 
     this.keys = keys;
@@ -71,19 +71,20 @@
       }
     }
 
+    List<ListenableFuture<Void>> evictions = Lists.newArrayList();
     if (!toDelete.isEmpty()) {
       db.accountExternalIds().delete(toDelete);
-      accountCache.evict(user.getAccountId());
+      evictions.add(accountCache.evictAsync(user.getAccountId()));
       for (AccountExternalId e : toDelete) {
-        byEmailCache.evict(e.getEmailAddress());
+        evictions.add(accountCache.evictEmailAsync(e.getEmailAddress()));
+        evictions.add(accountCache.evictAsync(e.getKey()));
       }
     }
-
+    FutureUtil.waitFor(evictions);
     return toKeySet(toDelete);
   }
 
-  private Map<AccountExternalId.Key, AccountExternalId> have()
-      throws OrmException {
+  private Map<AccountExternalId.Key, AccountExternalId> have() {
     Map<AccountExternalId.Key, AccountExternalId> r;
 
     r = new HashMap<AccountExternalId.Key, AccountExternalId>();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/ExternalIdDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/ExternalIdDetailFactory.java
index de0dec9..42ee569 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/ExternalIdDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/ExternalIdDetailFactory.java
@@ -16,42 +16,43 @@
 
 import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
 
+import com.google.common.collect.Lists;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AuthConfig;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.protobuf.CodecFactory;
+import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.inject.Inject;
 
 import java.util.Collections;
 import java.util.List;
 
 class ExternalIdDetailFactory extends Handler<List<AccountExternalId>> {
+  private static final ProtobufCodec<AccountExternalId> codec =
+      CodecFactory.encoder(AccountExternalId.class);
+
   interface Factory {
     ExternalIdDetailFactory create();
   }
 
-  private final ReviewDb db;
   private final IdentifiedUser user;
   private final AuthConfig authConfig;
   private final WebSession session;
 
   @Inject
-  ExternalIdDetailFactory(final ReviewDb db, final IdentifiedUser user,
+  ExternalIdDetailFactory(final IdentifiedUser user,
       final AuthConfig authConfig, final WebSession session) {
-    this.db = db;
     this.user = user;
     this.authConfig = authConfig;
     this.session = session;
   }
 
   @Override
-  public List<AccountExternalId> call() throws OrmException {
-    final AccountExternalId.Key last = session.getLastLoginExternalId();
-    final List<AccountExternalId> ids =
-        db.accountExternalIds().byAccount(user.getAccountId()).toList();
+  public List<AccountExternalId> call() {
+    AccountExternalId.Key last = session.getLastLoginExternalId();
+    List<AccountExternalId> ids = load();
 
     for (final AccountExternalId e : ids) {
       e.setTrusted(authConfig.isIdentityTrustable(Collections.singleton(e)));
@@ -66,6 +67,19 @@
         e.setCanDelete(last != null && !last.equals(e.getKey()));
       }
     }
+
     return ids;
   }
+
+  private List<AccountExternalId> load() {
+    List<AccountExternalId> res = Lists.newArrayList();
+    for (AccountExternalId id : user.getAccountState().getExternalIds()) {
+      res.add(clone(id));
+    }
+    return res;
+  }
+
+  private static AccountExternalId clone(AccountExternalId id) {
+    return codec.decode(codec.encodeToByteArray(id));
+  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
index 870d77c..eb12537 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.common.data.GroupAdminService;
 import com.google.gerrit.common.data.GroupDetail;
 import com.google.gerrit.common.errors.InactiveAccountException;
@@ -33,11 +34,13 @@
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.NoSuchGroupException;
 import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtjsonrpc.client.VoidResult;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.internal.Lists;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -135,7 +138,7 @@
         assertAmGroupOwner(db, group);
         group.setDescription(description);
         db.accountGroups().update(Collections.singleton(group));
-        groupCache.evict(group);
+        FutureUtil.waitFor(groupCache.evictAsync(group));
         return VoidResult.INSTANCE;
       }
     });
@@ -149,14 +152,15 @@
         assertAmGroupOwner(db, group);
 
         final AccountGroup owner =
-            groupCache.get(new AccountGroup.NameKey(newOwnerName));
+            FutureUtil.get(groupCache
+                .get(new AccountGroup.NameKey(newOwnerName)));
         if (owner == null) {
           throw new Failure(new NoSuchEntityException());
         }
 
         group.setOwnerGroupId(owner.getId());
         db.accountGroups().update(Collections.singleton(group));
-        groupCache.evict(group);
+        FutureUtil.waitFor(groupCache.evictAsync(group));
         return VoidResult.INSTANCE;
       }
     });
@@ -175,7 +179,7 @@
         assertAmGroupOwner(db, group);
         group.setType(newType);
         db.accountGroups().update(Collections.singleton(group));
-        groupCache.evict(group);
+        FutureUtil.waitFor(groupCache.evictAsync(group));
         return VoidResult.INSTANCE;
       }
     });
@@ -190,7 +194,7 @@
         assertAmGroupOwner(db, group);
         group.setExternalNameKey(bindTo);
         db.accountGroups().update(Collections.singleton(group));
-        groupCache.evict(group);
+        FutureUtil.waitFor(groupCache.evictAsync(group));
         return VoidResult.INSTANCE;
       }
     });
@@ -238,7 +242,7 @@
               Collections.singleton(new AccountGroupMemberAudit(m,
                   getAccountId())));
           db.accountGroupMembers().insert(Collections.singleton(m));
-          accountCache.evict(m.getAccountId());
+          FutureUtil.waitFor(accountCache.evictAsync(m.getAccountId()));
         }
 
         return groupDetailFactory.create(groupId).call();
@@ -263,6 +267,7 @@
           }
         }
 
+        List<ListenableFuture<Void>> evictions = Lists.newArrayList();
         final Account.Id me = getAccountId();
         for (final AccountGroupMember.Key k : keys) {
           final AccountGroupMember m = db.accountGroupMembers().get(k);
@@ -292,9 +297,10 @@
             }
 
             db.accountGroupMembers().delete(Collections.singleton(m));
-            accountCache.evict(m.getAccountId());
+            evictions.add(accountCache.evictAsync(m.getAccountId()));
           }
         }
+        FutureUtil.waitFor(evictions);
         return VoidResult.INSTANCE;
       }
     });
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupDetailFactory.java
index 1b05660..8adae93 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupDetailFactory.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.NoSuchGroupException;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -65,12 +66,13 @@
     final AccountGroup group = control.getAccountGroup();
     final GroupDetail detail = new GroupDetail();
     detail.setGroup(group);
-    detail.setOwnerGroup(groupCache.get(group.getOwnerGroupId()));
     switch (group.getType()) {
       case INTERNAL:
         detail.setMembers(loadMembers());
         break;
     }
+    detail.setOwnerGroup(FutureUtil.get( //
+        groupCache.get(group.getOwnerGroupId())));
     detail.setAccounts(aic.create());
     return detail;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java
index b3e993e..af251f6 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java
@@ -14,17 +14,18 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.inject.Inject;
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
-import java.util.Set;
 
 class MyGroupsFactory extends Handler<List<AccountGroup>> {
   interface Factory {
@@ -42,18 +43,18 @@
 
   @Override
   public List<AccountGroup> call() throws Exception {
-    final Set<AccountGroup.Id> effective = user.getEffectiveGroups();
-    final int cnt = effective.size();
-    final List<AccountGroup> groupList = new ArrayList<AccountGroup>(cnt);
-    for (final AccountGroup.Id groupId : effective) {
-      groupList.add(groupCache.get(groupId));
+    List<ListenableFuture<AccountGroup>> want = Lists.newArrayList();
+    for (AccountGroup.Id id : user.getEffectiveGroups()) {
+      want.add(groupCache.get(id));
     }
-    Collections.sort(groupList, new Comparator<AccountGroup>() {
+
+    List<AccountGroup> all = FutureUtil.get(FutureUtil.concatSingletons(want));
+    Collections.sort(all, new Comparator<AccountGroup>() {
       @Override
       public int compare(AccountGroup a, AccountGroup b) {
         return a.getName().compareTo(b.getName());
       }
     });
-    return groupList;
+    return all;
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
index 63b5bce..b512f0f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.AccountGroup;
@@ -22,13 +23,16 @@
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.NoSuchGroupException;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtjsonrpc.client.VoidResult;
 import com.google.gwtorm.client.OrmDuplicateKeyException;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import com.google.inject.internal.Lists;
 
 import java.util.Collections;
+import java.util.List;
 
 class RenameGroup extends Handler<VoidResult> {
   interface Factory {
@@ -91,9 +95,10 @@
       db.accountGroupNames().delete(Collections.singleton(priorName));
     }
 
-    groupCache.evict(group);
-    groupCache.evictAfterRename(old);
-
+    List<ListenableFuture<Void>> evictions = Lists.newArrayList();
+    evictions.add(groupCache.evictAsync(group));
+    evictions.add(groupCache.evictAfterRenameAsync(old));
+    FutureUtil.waitFor(evictions);
     return VoidResult.INSTANCE;
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
index 0fcdf40..e8fa475 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -80,7 +81,7 @@
       }
     }
 
-    final PatchList list = patchListCache.get(control.getChange(), patchSet);
+    PatchList list = FutureUtil.get(patchListCache.get(control.getChange(), patchSet));
     if (list == null) {
       throw new NoSuchEntityException();
     }
@@ -91,7 +92,7 @@
       byKey.put(p.getKey(), p);
     }
 
-    for (final PatchLineComment c : db.patchComments().published(psId)) {
+    for (final PatchLineComment c : db.patchComments().publishedByPatchSet(psId)) {
       final Patch p = byKey.get(c.getKey().getParentKey());
       if (p != null) {
         p.setCommentCount(p.getCommentCount() + 1);
@@ -111,7 +112,7 @@
       // quickly locate where they have pending drafts, and review them.
       //
       final Account.Id me = ((IdentifiedUser) user).getAccountId();
-      for (final PatchLineComment c : db.patchComments().draft(psId, me)) {
+      for (final PatchLineComment c : db.patchComments().draftByPatchSet(psId, me)) {
         final Patch p = byKey.get(c.getKey().getParentKey());
         if (p != null) {
           p.setDraftCount(p.getDraftCount() + 1);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
index 779475e..321ed5f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
@@ -36,7 +36,6 @@
 import com.google.gerrit.server.project.CanSubmitResult;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefControl;
 import com.google.gwtorm.client.OrmException;
@@ -54,7 +53,6 @@
     PatchSetPublishDetailFactory create(PatchSet.Id patchSetId);
   }
 
-  private final ProjectCache projectCache;
   private final PatchSetInfoFactory infoFactory;
   private final ApprovalTypes approvalTypes;
   private final ReviewDb db;
@@ -70,15 +68,14 @@
   private List<PatchLineComment> drafts;
   private Map<ApprovalCategory.Id, Set<ApprovalCategoryValue.Id>> allowed;
   private Map<ApprovalCategory.Id, PatchSetApproval> given;
+  private ChangeControl control;
 
   @Inject
   PatchSetPublishDetailFactory(final PatchSetInfoFactory infoFactory,
-      final ProjectCache projectCache, final ApprovalTypes approvalTypes,
-      final ReviewDb db,
+      final ApprovalTypes approvalTypes, final ReviewDb db,
       final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
       final ChangeControl.Factory changeControlFactory,
       final IdentifiedUser user, @Assisted final PatchSet.Id patchSetId) {
-    this.projectCache = projectCache;
     this.infoFactory = infoFactory;
     this.approvalTypes = approvalTypes;
     this.db = db;
@@ -93,10 +90,10 @@
   public PatchSetPublishDetail call() throws OrmException,
       PatchSetInfoNotAvailableException, NoSuchChangeException {
     final Change.Id changeId = patchSetId.getParentKey();
-    final ChangeControl control = changeControlFactory.validateFor(changeId);
+    control = changeControlFactory.validateFor(changeId);
     change = control.getChange();
     patchSetInfo = infoFactory.get(patchSetId);
-    drafts = db.patchComments().draft(patchSetId, user.getAccountId()).toList();
+    drafts = db.patchComments().draftByPatchSet(patchSetId, user.getAccountId()).toList();
 
     allowed = new HashMap<ApprovalCategory.Id, Set<ApprovalCategoryValue.Id>>();
     given = new HashMap<ApprovalCategory.Id, PatchSetApproval>();
@@ -128,7 +125,7 @@
 
   private void computeAllowed() {
     final Set<AccountGroup.Id> am = user.getEffectiveGroups();
-    final ProjectState pe = projectCache.get(change.getProject());
+    final ProjectState pe = control.getProjectControl().getProjectState();
     for (ApprovalCategory.Id category : approvalTypes.getApprovalCategories()) {
       RefControl rc = pe.controlFor(user).controlForRef(change.getDest());
       List<RefRight> categoryRights = rc.getApplicableRights(category);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
index 9218edd..f631bd4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
@@ -17,7 +17,9 @@
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
+import com.google.gerrit.prettify.common.BaseEdit;
 import com.google.gerrit.prettify.common.EditList;
+import com.google.gerrit.prettify.common.LineEdit;
 import com.google.gerrit.prettify.common.SparseFileContent;
 import com.google.gerrit.reviewdb.AccountDiffPreference;
 import com.google.gerrit.reviewdb.Change;
@@ -33,7 +35,6 @@
 import eu.medsea.mimeutil.MimeType;
 import eu.medsea.mimeutil.MimeUtil2;
 
-import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -55,9 +56,9 @@
   static final int MAX_CONTEXT = 5000000;
   static final int BIG_FILE = 9000;
 
-  private static final Comparator<Edit> EDIT_SORT = new Comparator<Edit>() {
+  private static final Comparator<BaseEdit> EDIT_SORT = new Comparator<BaseEdit>() {
     @Override
-    public int compare(final Edit o1, final Edit o2) {
+    public int compare(final BaseEdit o1, final BaseEdit o2) {
       return o1.getBeginA() - o2.getBeginA();
     }
   };
@@ -71,7 +72,7 @@
   private final Side a;
   private final Side b;
 
-  private List<Edit> edits;
+  private List<LineEdit> edits;
   private final FileTypeRegistry registry;
   private int context;
 
@@ -115,7 +116,7 @@
       //
       return new PatchScript(change.getKey(), content.getChangeType(), content
           .getOldName(), content.getNewName(), content.getHeaderLines(),
-          diffPrefs, a.dst, b.dst, Collections.<Edit> emptyList(),
+          diffPrefs, a.dst, b.dst, Collections.<LineEdit> emptyList(),
           a.displayMethod, b.displayMethod, comments, history, false, false);
     }
 
@@ -125,7 +126,7 @@
     a.resolve(null, aId);
     b.resolve(a, bId);
 
-    edits = new ArrayList<Edit>(content.getEdits());
+    edits = new ArrayList<LineEdit>(content.getEdits());
     ensureCommentsVisible(comments);
 
     boolean hugeFile = false;
@@ -140,8 +141,8 @@
       for (int i = 0; i < a.size(); i++) {
         a.addLine(i);
       }
-      edits = new ArrayList<Edit>(1);
-      edits.add(new Edit(a.size(), a.size()));
+      edits = new ArrayList<LineEdit>(1);
+      edits.add(new LineEdit(a.size(), a.size()));
 
     } else {
       if (BIG_FILE < Math.max(a.size(), b.size())) {
@@ -210,7 +211,7 @@
     // correct hunks from this, but because the Edit is empty they will not
     // style it specially.
     //
-    final List<Edit> empty = new ArrayList<Edit>();
+    final List<LineEdit> empty = new ArrayList<LineEdit>();
     int lastLine;
 
     lastLine = -1;
@@ -219,7 +220,7 @@
       if (lastLine != a) {
         final int b = mapA2B(a - 1);
         if (0 <= b) {
-          safeAdd(empty, new Edit(a - 1, b));
+          safeAdd(empty, new LineEdit(a - 1, b));
         }
         lastLine = a;
       }
@@ -231,7 +232,7 @@
       if (lastLine != b) {
         final int a = mapB2A(b - 1);
         if (0 <= a) {
-          safeAdd(empty, new Edit(a, b - 1));
+          safeAdd(empty, new LineEdit(a, b - 1));
         }
         lastLine = b;
       }
@@ -244,10 +245,10 @@
     Collections.sort(edits, EDIT_SORT);
   }
 
-  private void safeAdd(final List<Edit> empty, final Edit toAdd) {
+  private void safeAdd(final List<LineEdit> empty, final LineEdit toAdd) {
     final int a = toAdd.getBeginA();
     final int b = toAdd.getBeginB();
-    for (final Edit e : edits) {
+    for (final BaseEdit e : edits) {
       if (e.getBeginA() <= a && a <= e.getEndA()) {
         return;
       }
@@ -266,7 +267,7 @@
     }
 
     for (int i = 0; i < edits.size(); i++) {
-      final Edit e = edits.get(i);
+      final BaseEdit e = edits.get(i);
       if (a < e.getBeginA()) {
         if (i == 0) {
           // Special case of context at start of file.
@@ -280,7 +281,7 @@
       }
     }
 
-    final Edit last = edits.get(edits.size() - 1);
+    final BaseEdit last = edits.get(edits.size() - 1);
     return last.getBeginB() + (a - last.getEndA());
   }
 
@@ -292,7 +293,7 @@
     }
 
     for (int i = 0; i < edits.size(); i++) {
-      final Edit e = edits.get(i);
+      final BaseEdit e = edits.get(i);
       if (b < e.getBeginB()) {
         if (i == 0) {
           // Special case of context at start of file.
@@ -306,12 +307,13 @@
       }
     }
 
-    final Edit last = edits.get(edits.size() - 1);
+    final BaseEdit last = edits.get(edits.size() - 1);
     return last.getBeginA() + (b - last.getEndB());
   }
 
   private void packContent(boolean ignoredWhitespace) {
-    EditList list = new EditList(edits, context, a.size(), b.size());
+    EditList list =
+        new EditList(edits, context, a.size(), b.size());
     for (final EditList.Hunk hunk : list.getHunks()) {
       while (hunk.next()) {
         if (hunk.isContextLine()) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
index ee82418..597bf01 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.patch.PatchListKey;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -168,7 +169,7 @@
   }
 
   private PatchList listFor(final PatchListKey key) {
-    return patchListCache.get(key);
+    return FutureUtil.get(patchListCache.get(key));
   }
 
   private PatchScriptBuilder newBuilder(final PatchList list, Repository git) {
@@ -288,7 +289,7 @@
 
   private void loadPublished(final Map<Patch.Key, Patch> byKey,
       final AccountInfoCacheFactory aic, final String file) throws OrmException {
-    for (PatchLineComment c : db.patchComments().published(changeId, file)) {
+    for (PatchLineComment c : db.patchComments().publishedByChangeFile(changeId, file)) {
       if (comments.include(c)) {
         aic.want(c.getAuthor());
       }
@@ -304,7 +305,7 @@
   private void loadDrafts(final Map<Patch.Key, Patch> byKey,
       final AccountInfoCacheFactory aic, final Account.Id me, final String file)
       throws OrmException {
-    for (PatchLineComment c : db.patchComments().draft(changeId, file, me)) {
+    for (PatchLineComment c : db.patchComments().draftByChangeFile(changeId, file, me)) {
       if (comments.include(c)) {
         aic.want(me);
       }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java
index 3c2c93f..f4e551c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -183,7 +184,7 @@
       throw new NoSuchRefException(refPattern);
     }
 
-    final AccountGroup group = groupCache.get(groupName);
+    final AccountGroup group = FutureUtil.get(groupCache.get(groupName));
     if (group == null) {
       throw new NoSuchGroupException(groupName);
     }
@@ -201,7 +202,7 @@
       rr.setMaxValue(max);
       db.refRights().update(Collections.singleton(rr));
     }
-    projectCache.evictAll();
+    FutureUtil.waitFor(projectCache.evictAllAsync());
     return projectDetailFactory.create(projectName).call();
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
index 8831e90..c46d28e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -70,7 +71,7 @@
 
     proj.copySettingsFrom(update);
     db.projects().update(Collections.singleton(proj));
-    projectCache.evict(proj);
+    FutureUtil.waitFor(projectCache.evictAsync(proj));
 
     if (!projectControl.getProjectState().isSpecialWildProject()) {
       repoManager.setProjectDescription(projectName.get(), update.getDescription());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java
index 92154c4..19a1f80 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java
@@ -23,7 +23,7 @@
 import com.google.gerrit.server.project.NoSuchRefException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -82,7 +82,7 @@
         db.refRights().delete(Collections.singleton(m));
       }
     }
-    projectCache.evictAll();
+    FutureUtil.waitFor(projectCache.evictAllAsync());
     return projectDetailFactory.create(projectName).call();
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
index 3ff3892..dfa5a87 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.InheritedRefRight;
@@ -26,16 +28,15 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
+import java.util.concurrent.Future;
 
 class ProjectDetailFactory extends Handler<ProjectDetail> {
   interface Factory {
@@ -47,7 +48,7 @@
   private final ProjectControl.Factory projectControlFactory;
 
   private final Project.NameKey projectName;
-  private Map<AccountGroup.Id, AccountGroup> groups;
+  private Map<AccountGroup.Id, Future<AccountGroup>> groups;
 
   @Inject
   ProjectDetailFactory(final ApprovalTypes approvalTypes,
@@ -71,26 +72,25 @@
     final ProjectDetail detail = new ProjectDetail();
     detail.setProject(projectState.getProject());
 
-    groups = new HashMap<AccountGroup.Id, AccountGroup>();
-    final List<InheritedRefRight> refRights = new ArrayList<InheritedRefRight>();
+    groups = Maps.newHashMap();
 
+    final List<InheritedRefRight> refRights = Lists.newArrayList();
     for (final RefRight r : projectState.getInheritedRights()) {
-      InheritedRefRight refRight = new InheritedRefRight(
-          r, true, pc.controlForRef(r.getRefPattern()).isOwner());
+      InheritedRefRight refRight =
+          new InheritedRefRight(r, true, pc.controlForRef(r.getRefPattern())
+              .isOwner());
       if (!refRights.contains(refRight)) {
         refRights.add(refRight);
-        wantGroup(r.getAccountGroupId());
+        want(r.getAccountGroupId());
       }
     }
 
     for (final RefRight r : projectState.getLocalRights()) {
-      refRights.add(new InheritedRefRight(
-          r, false, pc.controlForRef(r.getRefPattern()).isOwner()));
-      wantGroup(r.getAccountGroupId());
+      refRights.add(new InheritedRefRight(r, false, pc.controlForRef(
+          r.getRefPattern()).isOwner()));
+      want(r.getAccountGroupId());
     }
 
-    loadGroups();
-
     Collections.sort(refRights, new Comparator<InheritedRefRight>() {
       @Override
       public int compare(final InheritedRefRight a, final InheritedRefRight b) {
@@ -115,8 +115,8 @@
         return type.getCategory().getName();
       }
 
-      private String groupOf(final RefRight r) {
-        return groups.get(r.getAccountGroupId()).getName();
+      private String groupOf(RefRight r) {
+        return FutureUtil.get(want(r.getAccountGroupId())).getName();
       }
     });
 
@@ -124,7 +124,7 @@
     final boolean userIsOwnerAnyRef = pc.isOwnerAnyRef();
 
     detail.setRights(refRights);
-    detail.setGroups(groups);
+    detail.setGroups(FutureUtil.getMap(groups));
     detail.setCanModifyAccess(userIsOwnerAnyRef);
     detail.setCanModifyAgreements(userIsOwner);
     detail.setCanModifyDescription(userIsOwner);
@@ -132,15 +132,12 @@
     return detail;
   }
 
-  private void wantGroup(final AccountGroup.Id id) {
-    groups.put(id, null);
-  }
-
-  private void loadGroups() {
-    final Set<AccountGroup.Id> toGet = groups.keySet();
-    groups = new HashMap<AccountGroup.Id, AccountGroup>();
-    for (AccountGroup.Id groupId : toGet) {
-      groups.put(groupId, groupCache.get(groupId));
+  private Future<AccountGroup> want(AccountGroup.Id id) {
+    Future<AccountGroup> f = groups.get(id);
+    if (f == null) {
+      f = groupCache.get(id);
+      groups.put(id, f);
     }
+    return f;
   }
 }
diff --git a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/ReplaceEdit.java b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/ReplaceEdit.java
deleted file mode 100644
index 46681c6..0000000
--- a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/ReplaceEdit.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (C) 2010 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 org.eclipse.jgit.diff;
-
-import java.util.List;
-
-public class ReplaceEdit extends Edit {
-  private List<Edit> internalEdit;
-
-  public ReplaceEdit(int as, int ae, int bs, int be, List<Edit> internal) {
-    super(as, ae, bs, be);
-    internalEdit = internal;
-  }
-
-  public ReplaceEdit(Edit orig, List<Edit> internal) {
-    super(orig.getBeginA(), orig.getEndA(), orig.getBeginB(), orig.getEndB());
-    internalEdit = internal;
-  }
-
-  public List<Edit> getInternalEdits() {
-    return internalEdit;
-  }
-}
diff --git a/gerrit-pgm/pom.xml b/gerrit-pgm/pom.xml
index f658f52..d3eee42 100644
--- a/gerrit-pgm/pom.xml
+++ b/gerrit-pgm/pom.xml
@@ -49,6 +49,11 @@
     </dependency>
 
     <dependency>
+      <groupId>com.google.protobuf</groupId>
+      <artifactId>protobuf-java</artifactId>
+    </dependency>
+
+    <dependency>
       <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-main</artifactId>
       <version>${project.version}</version>
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Backup.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Backup.java
new file mode 100644
index 0000000..dc8a3ef
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Backup.java
@@ -0,0 +1,141 @@
+// Copyright (C) 2010 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.pgm;
+
+import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
+
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.schema.backup.BackupAccess;
+import com.google.gerrit.server.schema.backup.BackupDatabase;
+import com.google.gerrit.server.schema.backup.Counters;
+import com.google.gwtorm.client.Access;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.eclipse.jgit.storage.file.LockFile;
+import org.eclipse.jgit.util.FS;
+import org.kohsuke.args4j.Option;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.zip.GZIPOutputStream;
+
+/** Backup the database as a compressed series of protobuf objects. */
+public class Backup extends SiteProgram {
+  private final LifecycleManager manager = new LifecycleManager();
+  private Injector dbInjector;
+
+  @Option(name = "--quiet", aliases = {"-q"}, usage = "Quiet (suppress progress)")
+  private boolean quiet;
+
+  @Option(name = "--output", aliases = {"-o"}, required = true, metaVar = "FILE", usage = "File to write the backup into")
+  private File output;
+
+  @Inject
+  private SchemaFactory<ReviewDb> srcdb;
+
+  @Override
+  public int run() throws Exception {
+    mustHaveValidSite();
+
+    dbInjector = createDbInjector(SINGLE_USER);
+    manager.add(dbInjector);
+    manager.start();
+    dbInjector.injectMembers(this);
+
+    BackupDatabase<ReviewDb> bck = new BackupDatabase<ReviewDb>(ReviewDb.class);
+    ReviewDb dst = bck.open();
+
+    ReviewDb src = srcdb.open();
+    try {
+      final LockFile lf = new LockFile(output.getAbsoluteFile(), FS.DETECTED);
+      if (!lf.lock()) {
+        throw die("Cannot lock " + output);
+      }
+      try {
+        BufferedOutputStream out =
+            new BufferedOutputStream(new GZIPOutputStream(lf.getOutputStream(),
+                8192));
+        try {
+          backup(src, dst, out);
+        } finally {
+          out.close();
+        }
+        if (!lf.commit()) {
+          throw die("Cannot commit " + output);
+        }
+        if (!quiet) {
+          System.err.println("Backup completed to " + output);
+        }
+      } finally {
+        lf.unlock();
+      }
+    } finally {
+      src.close();
+    }
+    return 0;
+  }
+
+  @SuppressWarnings("unchecked")
+  private void backup(ReviewDb src, ReviewDb dst, BufferedOutputStream out)
+      throws OrmException, IOException {
+    Map<Integer, BackupAccess<?, ?>> relations = index(dst);
+
+    ProgressMonitor pm =
+        quiet ? NullProgressMonitor.INSTANCE : new TextProgressMonitor();
+
+    Counters cnts = new Counters();
+    cnts.accountGroupId = src.nextAccountGroupId();
+    cnts.accountId = src.nextAccountId();
+    cnts.changeId = src.nextChangeId();
+    cnts.changeMessageId = src.nextChangeMessageId();
+    cnts.contributorAgreementId = src.nextContributorAgreementId();
+    Counters.CODEC.encodeWithSize(cnts, out);
+
+    for (Access<?, ?> s : src.allRelations()) {
+      BackupAccess<?, ?> d = relations.get(s.getRelationID());
+      ProtobufCodec pc = d.getObjectCodec();
+
+      pm.beginTask("Backup " + s.getRelationName(), ProgressMonitor.UNKNOWN);
+      for (Object obj : s.iterateAllEntities()) {
+        pc.encodeWithSize(obj, out);
+        pm.update(1);
+      }
+      pm.endTask();
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private Map<Integer, BackupAccess<?, ?>> index(ReviewDb dst) {
+    Map<Integer, BackupAccess<?, ?>> relations =
+        new HashMap<Integer, BackupAccess<?, ?>>();
+
+    for (Access<?, ?> a : dst.allRelations()) {
+      relations.put(a.getRelationID(), (BackupAccess) a);
+    }
+    return relations;
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 3e10336..d9885f2 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -16,8 +16,11 @@
 
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 
+import com.google.gerrit.ehcache.EhcachePoolImpl;
 import com.google.gerrit.httpd.HttpCanonicalWebUrlProvider;
+import com.google.gerrit.httpd.SessionCacheCleaner;
 import com.google.gerrit.httpd.WebModule;
+import com.google.gerrit.httpd.WebSshGlueModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.pgm.http.jetty.JettyEnv;
 import com.google.gerrit.pgm.http.jetty.JettyModule;
@@ -31,6 +34,9 @@
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
 import com.google.gerrit.server.config.GerritGlobalModule;
 import com.google.gerrit.server.config.MasterNodeStartup;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.mail.SmtpEmailSender;
+import com.google.gerrit.server.ssh.NoSshModule;
 import com.google.gerrit.sshd.SshModule;
 import com.google.gerrit.sshd.commands.MasterCommandModule;
 import com.google.gerrit.sshd.commands.SlaveCommandModule;
@@ -113,10 +119,6 @@
     if (slave && httpd) {
       throw die("Cannot combine --slave and --enable-httpd");
     }
-    if (httpd && !sshd) {
-      // TODO Support HTTP without SSH.
-      throw die("--enable-httpd currently requires --enable-sshd");
-    }
 
     if (consoleLog) {
     } else {
@@ -187,6 +189,9 @@
     final List<Module> modules = new ArrayList<Module>();
     modules.add(new LogFileCompressor.Module());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
+    modules.add(new SmtpEmailSender.Module());
+    modules.add(new LocalDiskRepositoryManager.Module());
+    modules.add(new EhcachePoolImpl.Module());
     if (httpd) {
       modules.add(new CanonicalWebUrlModule() {
         @Override
@@ -215,11 +220,15 @@
 
   private Injector createSshInjector() {
     final List<Module> modules = new ArrayList<Module>();
-    modules.add(new SshModule());
-    if (slave) {
-      modules.add(new SlaveCommandModule());
+    if (sshd) {
+      modules.add(new SshModule());
+      if (slave) {
+        modules.add(new SlaveCommandModule());
+      } else {
+        modules.add(new MasterCommandModule());
+      }
     } else {
-      modules.add(new MasterCommandModule());
+      modules.add(new NoSshModule());
     }
     return sysInjector.createChildInjector(modules);
   }
@@ -237,8 +246,14 @@
 
   private Injector createWebInjector() {
     final List<Module> modules = new ArrayList<Module>();
-    modules.add(sshInjector.getInstance(ProjectQoSFilter.Module.class));
+    if (sshd) {
+      modules.add(sshInjector.getInstance(ProjectQoSFilter.Module.class));
+    }
     modules.add(sshInjector.getInstance(WebModule.class));
+    modules.add(sshInjector.getInstance(WebSshGlueModule.class));
+    if (!slave) {
+      modules.add(new SessionCacheCleaner.Module());
+    }
     return sysInjector.createChildInjector(modules);
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtoGen.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtoGen.java
new file mode 100644
index 0000000..0629cff
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtoGen.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2010 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.pgm;
+
+import com.google.gerrit.pgm.util.AbstractProgram;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.schema.java.JavaSchemaModel;
+
+import org.eclipse.jgit.storage.file.LockFile;
+import org.eclipse.jgit.util.FS;
+import org.kohsuke.args4j.Option;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+
+public class ProtoGen extends AbstractProgram {
+  @Option(name = "--output", aliases = {"-o"}, required = true, metaVar = "FILE", usage = "File to write .proto into")
+  private File file;
+
+  @Override
+  public int run() throws Exception {
+    LockFile lf = new LockFile(file.getAbsoluteFile(), FS.DETECTED);
+    if (!lf.lock()) {
+      throw die("Cannot lock " + file);
+    }
+    try {
+      JavaSchemaModel jsm = new JavaSchemaModel(ReviewDb.class);
+      PrintWriter out =
+          new PrintWriter(new BufferedWriter(new OutputStreamWriter(lf
+              .getOutputStream(), "UTF-8")));
+      try {
+        out.println("// Gerrit Code Review (version "
+            + com.google.gerrit.common.Version.getVersion() + ")");
+        out.println();
+
+        out.println("package gerritcodereview;\n");
+        out.println();
+
+        jsm.generateProto(out);
+        out.flush();
+      } finally {
+        out.close();
+      }
+      if (!lf.commit()) {
+        throw die("Could not write to " + file);
+      }
+    } finally {
+      lf.unlock();
+    }
+    return 0;
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Restore.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Restore.java
new file mode 100644
index 0000000..01611fc
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Restore.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2010 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.pgm;
+
+import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
+
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.schema.backup.RestoreBackup;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+import org.kohsuke.args4j.Option;
+
+import java.io.File;
+import java.io.FileInputStream;
+
+/** Restore the database from a compressed series of protobuf objects. */
+public class Restore extends SiteProgram {
+  private static final String OK_FLAG =
+      "--yes-really-import-and-destroy-current-data";
+
+  private final LifecycleManager manager = new LifecycleManager();
+  private Injector dbInjector;
+
+  @Option(name = "--input", aliases = {"-i"}, required = true, metaVar = "FILE", usage = "File to read backup from")
+  private File input;
+
+  @Option(name = OK_FLAG)
+  private boolean run;
+
+  @Inject
+  private SchemaFactory<ReviewDb> dstdb;
+
+  @Override
+  public int run() throws Exception {
+    mustHaveValidSite();
+
+    if (!run) {
+      throw die("Must pass " + OK_FLAG);
+    }
+
+    dbInjector = createDbInjector(SINGLE_USER);
+    manager.add(dbInjector);
+    manager.start();
+    dbInjector.injectMembers(this);
+
+    ReviewDb dst = dstdb.open();
+    try {
+      FileInputStream in = new FileInputStream(input);
+      try {
+        RestoreBackup.restore(in, dst);
+      } finally {
+        in.close();
+      }
+      System.err.println("Restore completed");
+    } finally {
+      dst.close();
+    }
+    return 0;
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index e9d0f2e..27b44cf 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -27,6 +27,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.servlet.GuiceFilter;
+import com.google.inject.servlet.GuiceHelper;
 import com.google.inject.servlet.GuiceServletContextListener;
 
 import org.eclipse.jetty.io.EndPoint;
@@ -65,6 +66,10 @@
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
 @Singleton
 public class JettyServer {
   static class Lifecycle implements LifecycleListener {
@@ -115,7 +120,23 @@
 
     Handler app = makeContext(env, cfg);
     if (cfg.getBoolean("httpd", "requestlog", !reverseProxy)) {
-      RequestLogHandler handler = new RequestLogHandler();
+      RequestLogHandler handler = new RequestLogHandler() {
+        @Override
+        public void handle(String target, Request baseRequest,
+            final HttpServletRequest req, final HttpServletResponse rsp)
+            throws IOException, ServletException {
+          // Force the user to construct, so its available to our HttpLog
+          // later on when the request gets logged out to the access file.
+          //
+          GuiceHelper.runInContext(req, rsp, new Runnable() {
+            @Override
+            public void run() {
+              userProvider.get();
+            }
+          });
+          super.handle(target, baseRequest, req, rsp);
+        }
+      };
       handler.setRequestLog(new HttpLog(site, userProvider));
       handler.setHandler(app);
       app = handler;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
index d501ea5..0f426cf 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
@@ -57,6 +57,7 @@
 
     final boolean userPassAuth;
     switch (db_type) {
+      case NOSQL_HEAP_FILE:
       case H2: {
         userPassAuth = false;
         String path = database.get("database");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
index 482405e..bfc0eaf 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
@@ -54,13 +54,20 @@
     String hostname = "*";
     int port = 29418;
     String listenAddress = sshd.get("listenAddress");
-    if (listenAddress != null && !listenAddress.isEmpty()) {
+    if (isOff(listenAddress)) {
+      hostname = "off";
+    } else if (listenAddress != null && !listenAddress.isEmpty()) {
       final InetSocketAddress addr = SocketUtil.parse(listenAddress, port);
       hostname = SocketUtil.hostname(addr);
       port = addr.getPort();
     }
 
     hostname = ui.readString(hostname, "Listen on address");
+    if (isOff(hostname)) {
+      sshd.set("listenAddress", "off");
+      return;
+    }
+
     port = ui.readInt(port, "Listen on port");
     sshd.set("listenAddress", SocketUtil.format(hostname, port));
 
@@ -73,6 +80,12 @@
     generateSshHostKeys();
   }
 
+  private static boolean isOff(String listenHostname) {
+    return "off".equalsIgnoreCase(listenHostname)
+        || "none".equalsIgnoreCase(listenHostname)
+        || "no".equalsIgnoreCase(listenHostname);
+  }
+
   private void generateSshHostKeys() throws InterruptedException, IOException {
     if (!site.ssh_key.exists() //
         && !site.ssh_rsa.exists() //
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
index e5ca2c6..f9985a8 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -18,17 +18,20 @@
 import static com.google.inject.Stage.PRODUCTION;
 
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.schema.DataSourceProvider;
-import com.google.gerrit.server.schema.DatabaseModule;
+import com.google.gerrit.server.schema.SchemaVersion;
 import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.AbstractModule;
 import com.google.inject.CreationException;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Module;
+import com.google.inject.TypeLiteral;
 import com.google.inject.name.Names;
 import com.google.inject.spi.Message;
 
@@ -156,13 +159,13 @@
       @Override
       protected void configure() {
         bind(DataSourceProvider.Context.class).toInstance(context);
-        bind(Key.get(DataSource.class, Names.named("ReviewDb"))).toProvider(
+        bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}).toProvider(
             DataSourceProvider.class).in(SINGLETON);
         listener().to(DataSourceProvider.class);
       }
     });
     modules.add(new GerritServerConfigModule());
-    modules.add(new DatabaseModule());
+    modules.add(new SchemaVersion.Module());
 
     try {
       return Guice.createInjector(PRODUCTION, modules);
diff --git a/gerrit-prettify/pom.xml b/gerrit-prettify/pom.xml
index 060ffdd..18ba674 100644
--- a/gerrit-prettify/pom.xml
+++ b/gerrit-prettify/pom.xml
@@ -55,5 +55,10 @@
       <artifactId>gwt-user</artifactId>
       <scope>provided</scope>
     </dependency>
+
+    <dependency>
+      <groupId>gwtorm</groupId>
+      <artifactId>gwtorm</artifactId>
+    </dependency>
   </dependencies>
 </project>
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/BaseEdit.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/BaseEdit.java
new file mode 100644
index 0000000..df3d216
--- /dev/null
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/BaseEdit.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2010 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.prettify.common;
+
+import com.google.gwtorm.client.Column;
+
+import org.eclipse.jgit.diff.Edit;
+import org.eclipse.jgit.diff.Edit.Type;
+
+public class BaseEdit {
+  @Column(id = 1)
+  protected int beginA;
+
+  @Column(id = 2)
+  protected int endA;
+
+  @Column(id = 3)
+  protected int beginB;
+
+  @Column(id = 4)
+  protected int endB;
+
+  protected BaseEdit() {
+  }
+
+  public BaseEdit(int beginA, int endA, int beginB, int endB) {
+    this.beginA = beginA;
+    this.endA = endA;
+    this.beginB = beginB;
+    this.endB = endB;
+  }
+
+  public BaseEdit(Edit edit) {
+    this(edit.getBeginA(), edit.getEndA(), edit.getBeginB(), edit.getEndB());
+  }
+
+  public int getBeginA() {
+    return beginA;
+  }
+
+  public int getEndA() {
+    return endA;
+  }
+
+  public int getBeginB() {
+    return beginB;
+  }
+
+  public int getEndB() {
+    return endB;
+  }
+
+  public final Type getType() {
+    if (beginA == endA) {
+      if (beginB < endB) {
+        return Type.INSERT;
+      }
+      if (beginB == endB) {
+        return Type.EMPTY;
+      }
+    }
+    if (beginA < endA && beginB == endB) {
+      return Type.DELETE;
+    }
+    return Type.REPLACE;
+  }
+}
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java
index d41865a..1fd647c 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java
@@ -14,18 +14,16 @@
 
 package com.google.gerrit.prettify.common;
 
-import org.eclipse.jgit.diff.Edit;
-
 import java.util.Iterator;
 import java.util.List;
 
 public class EditList {
-  private final List<Edit> edits;
+  private final List<LineEdit> edits;
   private final int context;
   private final int aSize;
   private final int bSize;
 
-  public EditList(final List<Edit> edits, final int contextLines,
+  public EditList(final List<LineEdit> edits, final int contextLines,
       final int aSize, final int bSize) {
     this.edits = edits;
     this.context = contextLines;
@@ -33,7 +31,7 @@
     this.bSize = bSize;
   }
 
-  public List<Edit> getEdits() {
+  public List<LineEdit> getEdits() {
     return edits;
   }
 
@@ -70,8 +68,8 @@
   }
 
   private boolean combineA(final int i) {
-    final Edit s = edits.get(i);
-    final Edit e = edits.get(i - 1);
+    final BaseEdit s = edits.get(i);
+    final BaseEdit e = edits.get(i - 1);
     return s.getBeginA() - e.getEndA() <= 2 * context;
   }
 
@@ -83,9 +81,9 @@
 
   public class Hunk {
     private int curIdx;
-    private Edit curEdit;
+    private BaseEdit curEdit;
     private final int endIdx;
-    private final Edit endEdit;
+    private final BaseEdit endEdit;
 
     private int aCur;
     private int bCur;
@@ -112,7 +110,7 @@
       return bCur;
     }
 
-    public Edit getCurEdit() {
+    public BaseEdit getCurEdit() {
       return curEdit;
     }
 
@@ -166,7 +164,7 @@
       return aCur < aEnd || bCur < bEnd;
     }
 
-    private boolean in(final Edit edit) {
+    private boolean in(final BaseEdit edit) {
       return aCur < edit.getEndA() || bCur < edit.getEndB();
     }
   }
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/LineEdit.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/LineEdit.java
new file mode 100644
index 0000000..ea77218
--- /dev/null
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/LineEdit.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2010 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.prettify.common;
+
+import com.google.gwtorm.client.Column;
+
+import org.eclipse.jgit.diff.Edit;
+
+import java.util.List;
+
+public class LineEdit extends BaseEdit {
+  @Column(id = 5)
+  protected List<BaseEdit> edits;
+
+  public LineEdit(int beginA, int endA, int beginB, int endB,
+      List<BaseEdit> edits) {
+    this.beginA = beginA;
+    this.endA = endA;
+    this.beginB = beginB;
+    this.endB = endB;
+    this.edits = edits;
+  }
+
+  protected LineEdit() {
+  }
+
+  public LineEdit(Edit edit) {
+    this(edit.getBeginA(), edit.getEndA(), edit.getBeginB(), edit.getEndB());
+  }
+
+  public LineEdit(BaseEdit edit, List<BaseEdit> edits) {
+    this(edit.beginA, edit.endA, edit.beginB, edit.endB, edits);
+  }
+
+  public LineEdit(int beginA, int endA, int beginB, int endB) {
+    this(beginA, endA, beginB, endB, null);
+  }
+
+  public LineEdit(int beginA, int beginB) {
+    this(beginA, beginA, beginB, beginB);
+  }
+
+  public List<BaseEdit> getEdits() {
+    return edits;
+  }
+}
diff --git a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/Edit_JsonSerializer.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/LineEdit_JsonSerializer.java
similarity index 69%
rename from gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/Edit_JsonSerializer.java
rename to gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/LineEdit_JsonSerializer.java
index 7870002..ae6e3d8 100644
--- a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/Edit_JsonSerializer.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/LineEdit_JsonSerializer.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package org.eclipse.jgit.diff;
+package com.google.gerrit.prettify.common;
 
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwtjsonrpc.client.impl.JsonSerializer;
@@ -20,11 +20,11 @@
 import java.util.ArrayList;
 import java.util.List;
 
-public class Edit_JsonSerializer extends JsonSerializer<Edit> {
-  public static final Edit_JsonSerializer INSTANCE = new Edit_JsonSerializer();
+public class LineEdit_JsonSerializer extends JsonSerializer<LineEdit> {
+  public static final LineEdit_JsonSerializer INSTANCE = new LineEdit_JsonSerializer();
 
   @Override
-  public Edit fromJson(Object jso) {
+  public LineEdit fromJson(Object jso) {
     if (jso == null) {
       return null;
     }
@@ -32,26 +32,26 @@
     final JavaScriptObject o = (JavaScriptObject) jso;
     final int cnt = length(o);
     if (4 == cnt) {
-      return new Edit(get(o, 0), get(o, 1), get(o, 2), get(o, 3));
+      return new LineEdit(get(o, 0), get(o, 1), get(o, 2), get(o, 3));
     }
 
-    List<Edit> l = new ArrayList<Edit>((cnt / 4) - 1);
+    List<BaseEdit> l = new ArrayList<BaseEdit>((cnt / 4) - 1);
     for (int i = 4; i < cnt;) {
       int as = get(o, i++);
       int ae = get(o, i++);
       int bs = get(o, i++);
       int be = get(o, i++);
-      l.add(new Edit(as, ae, bs, be));
+      l.add(new BaseEdit(as, ae, bs, be));
     }
-    return new ReplaceEdit(get(o, 0), get(o, 1), get(o, 2), get(o, 3), l);
+    return new LineEdit(get(o, 0), get(o, 1), get(o, 2), get(o, 3), l);
   }
 
   @Override
-  public void printJson(final StringBuilder sb, final Edit o) {
+  public void printJson(final StringBuilder sb, final LineEdit o) {
     sb.append('[');
     append(sb, o);
-    if (o instanceof ReplaceEdit) {
-      for (Edit e : ((ReplaceEdit) o).getInternalEdits()) {
+    if (o.getEdits() != null) {
+      for (BaseEdit e : o.getEdits()) {
         sb.append(',');
         append(sb, e);
       }
@@ -59,7 +59,7 @@
     sb.append(']');
   }
 
-  private void append(final StringBuilder sb, final Edit o) {
+  private void append(final StringBuilder sb, final BaseEdit o) {
     sb.append(o.getBeginA());
     sb.append(',');
     sb.append(o.getEndA());
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
index 5d1592d..74d903d 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
@@ -18,9 +18,6 @@
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
-import org.eclipse.jgit.diff.Edit;
-import org.eclipse.jgit.diff.ReplaceEdit;
-
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -30,9 +27,9 @@
   public static abstract class EditFilter {
     abstract String getStyleName();
 
-    abstract int getBegin(Edit edit);
+    abstract int getBegin(BaseEdit edit);
 
-    abstract int getEnd(Edit edit);
+    abstract int getEnd(BaseEdit edit);
   }
 
   public static final EditFilter A = new EditFilter() {
@@ -42,12 +39,12 @@
     }
 
     @Override
-    int getBegin(Edit edit) {
+    int getBegin(BaseEdit edit) {
       return edit.getBeginA();
     }
 
     @Override
-    int getEnd(Edit edit) {
+    int getEnd(BaseEdit edit) {
       return edit.getEndA();
     }
   };
@@ -59,19 +56,19 @@
     }
 
     @Override
-    int getBegin(Edit edit) {
+    int getBegin(BaseEdit edit) {
       return edit.getBeginB();
     }
 
     @Override
-    int getEnd(Edit edit) {
+    int getEnd(BaseEdit edit) {
       return edit.getEndB();
     }
   };
 
   protected SparseFileContent content;
   protected EditFilter side;
-  protected List<Edit> edits;
+  protected List<LineEdit> edits;
   protected AccountDiffPreference diffPrefs;
   protected String fileName;
   protected Set<Integer> trailingEdits;
@@ -103,7 +100,7 @@
     side = f;
   }
 
-  public void setEditList(List<Edit> all) {
+  public void setEditList(List<LineEdit> all) {
     edits = all;
   }
 
@@ -348,17 +345,17 @@
     // in the source. That simplifies our loop below because we'll never
     // run off the end of the edit list.
     //
-    List<Edit> edits = new ArrayList<Edit>(this.edits.size() + 1);
+    List<LineEdit> edits = new ArrayList<LineEdit>(this.edits.size() + 1);
     edits.addAll(this.edits);
-    edits.add(new Edit(src.size(), src.size()));
+    edits.add(new LineEdit(src.size(), src.size()));
 
     SafeHtmlBuilder buf = new SafeHtmlBuilder();
 
     int curIdx = 0;
-    Edit curEdit = edits.get(curIdx);
+    LineEdit curEdit = edits.get(curIdx);
 
-    ReplaceEdit lastReplace = null;
-    List<Edit> charEdits = null;
+    LineEdit lastReplace = null;
+    List<BaseEdit> charEdits = null;
     int lastPos = 0;
     int lastIdx = 0;
 
@@ -381,10 +378,10 @@
 
       // index occurs within the edit. The line is a modification.
       //
-      if (curEdit instanceof ReplaceEdit) {
+      if (curEdit.getEdits() != null) {
         if (lastReplace != curEdit) {
-          lastReplace = (ReplaceEdit) curEdit;
-          charEdits = lastReplace.getInternalEdits();
+          lastReplace = curEdit;
+          charEdits = lastReplace.getEdits();
           lastPos = 0;
           lastIdx = 0;
         }
@@ -396,7 +393,7 @@
             break;
           }
 
-          final Edit edit = charEdits.get(lastIdx);
+          final BaseEdit edit = charEdits.get(lastIdx);
           final int b = side.getBegin(edit) - lastPos;
           final int e = side.getEnd(edit) - lastPos;
 
@@ -468,7 +465,7 @@
     }
   }
 
-  private int compare(int index, Edit edit) {
+  private int compare(int index, BaseEdit edit) {
     if (index < side.getBegin(edit)) {
       return -1; // index occurs before the edit.
 
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
index a5373b8..eb32b17 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.prettify.common;
 
-import org.eclipse.jgit.diff.Edit;
-
 import java.util.ArrayList;
 import java.util.List;
 
@@ -219,7 +217,7 @@
     return b.toString();
   }
 
-  public SparseFileContent apply(SparseFileContent a, List<Edit> edits) {
+  public SparseFileContent apply(SparseFileContent a, List<LineEdit> edits) {
     EditList list = new EditList(edits, size, a.size(), size);
     ArrayList<String> lines = new ArrayList<String>(size);
     for (final EditList.Hunk hunk : list.getHunks()) {
diff --git a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/server/EditDeserializer.java
similarity index 75%
rename from gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java
rename to gerrit-prettify/src/main/java/com/google/gerrit/prettify/server/EditDeserializer.java
index 1df89b7..fc5af21 100644
--- a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/server/EditDeserializer.java
@@ -12,8 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package org.eclipse.jgit.diff;
+package com.google.gerrit.prettify.server;
 
+import com.google.gerrit.prettify.common.BaseEdit;
+import com.google.gerrit.prettify.common.LineEdit;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonDeserializationContext;
 import com.google.gson.JsonDeserializer;
@@ -28,9 +30,9 @@
 import java.util.ArrayList;
 import java.util.List;
 
-public class EditDeserializer implements JsonDeserializer<Edit>,
-    JsonSerializer<Edit> {
-  public Edit deserialize(final JsonElement json, final Type typeOfT,
+public class EditDeserializer implements JsonDeserializer<LineEdit>,
+    JsonSerializer<LineEdit> {
+  public LineEdit deserialize(final JsonElement json, final Type typeOfT,
       final JsonDeserializationContext context) throws JsonParseException {
     if (json.isJsonNull()) {
       return null;
@@ -46,18 +48,18 @@
     }
 
     if (4 == cnt) {
-      return new Edit(get(o, 0), get(o, 1), get(o, 2), get(o, 3));
+      return new LineEdit(get(o, 0), get(o, 1), get(o, 2), get(o, 3));
     }
 
-    List<Edit> l = new ArrayList<Edit>((cnt / 4) - 1);
+    List<BaseEdit> l = new ArrayList<BaseEdit>((cnt / 4) - 1);
     for (int i = 4; i < cnt;) {
       int as = get(o, i++);
       int ae = get(o, i++);
       int bs = get(o, i++);
       int be = get(o, i++);
-      l.add(new Edit(as, ae, bs, be));
+      l.add(new BaseEdit(as, ae, bs, be));
     }
-    return new ReplaceEdit(get(o, 0), get(o, 1), get(o, 2), get(o, 3), l);
+    return new LineEdit(get(o, 0), get(o, 1), get(o, 2), get(o, 3), l);
   }
 
   private static int get(final JsonArray a, final int idx)
@@ -73,22 +75,22 @@
     return p.getAsInt();
   }
 
-  public JsonElement serialize(final Edit src, final Type typeOfSrc,
+  public JsonElement serialize(final LineEdit src, final Type typeOfSrc,
       final JsonSerializationContext context) {
     if (src == null) {
       return new JsonNull();
     }
     final JsonArray a = new JsonArray();
     add(a, src);
-    if (src instanceof ReplaceEdit) {
-      for (Edit e : ((ReplaceEdit) src).getInternalEdits()) {
+    if (src.getEdits() != null) {
+      for (BaseEdit e : src.getEdits()) {
         add(a, e);
       }
     }
     return a;
   }
 
-  private void add(final JsonArray a, final Edit src) {
+  private void add(final JsonArray a, final BaseEdit src) {
     a.add(new JsonPrimitive(src.getBeginA()));
     a.add(new JsonPrimitive(src.getEndA()));
     a.add(new JsonPrimitive(src.getBeginB()));
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AbstractAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AbstractAgreement.java
index 987fc4b..417cbb9 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AbstractAgreement.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AbstractAgreement.java
@@ -15,9 +15,18 @@
 package com.google.gerrit.reviewdb;
 
 import java.sql.Timestamp;
+import java.util.Comparator;
 
 /** Base for {@link AccountAgreement} or {@link AccountGroupAgreement}. */
 public interface AbstractAgreement {
+  public static final Comparator<AbstractAgreement> SORT =
+      new Comparator<AbstractAgreement>() {
+        @Override
+        public int compare(AbstractAgreement a, AbstractAgreement b) {
+          return b.getAcceptedOn().compareTo(a.getAcceptedOn());
+        }
+      };
+
   public static enum Status {
     NEW('n'),
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Account.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Account.java
index 43b7b17..ee65c55 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Account.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Account.java
@@ -16,6 +16,7 @@
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
+import com.google.gwtorm.client.StringKey;
 
 import java.sql.Timestamp;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalId.java
index 853ebd5..bc93892 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalId.java
@@ -39,6 +39,10 @@
   /** Very old scheme from Gerrit Code Review 1.x imports. */
   public static final String LEGACY_GAE = "Google Account ";
 
+  public static AccountExternalId.Key forUsername(String username) {
+    return new Key(SCHEME_USERNAME, username);
+  }
+
   public static class Key extends StringKey<com.google.gwtorm.client.Key<?>> {
     private static final long serialVersionUID = 1L;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalIdAccess.java
index 0719035..761a2de 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalIdAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalIdAccess.java
@@ -32,10 +32,6 @@
   @Query("WHERE accountId = ?")
   ResultSet<AccountExternalId> byAccount(Account.Id id) throws OrmException;
 
-  @Query("WHERE accountId = ? AND emailAddress = ?")
-  ResultSet<AccountExternalId> byAccountEmail(Account.Id id, String email)
-      throws OrmException;
-
   @Query("WHERE emailAddress = ?")
   ResultSet<AccountExternalId> byEmailAddress(String email) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
index 52bef2b..2913ca3 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
@@ -51,6 +51,10 @@
       return accountId;
     }
 
+    public Project.NameKey getProjectName() {
+      return projectName;
+    }
+
     @Override
     public com.google.gwtorm.client.Key<?>[] members() {
       return new com.google.gwtorm.client.Key<?>[] {projectName, filter};
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ActiveSession.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ActiveSession.java
new file mode 100644
index 0000000..f90add5
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ActiveSession.java
@@ -0,0 +1,127 @@
+// Copyright (C) 2010 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.reviewdb;
+
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.client.StringKey;
+
+import java.sql.Timestamp;
+
+public final class ActiveSession {
+  public static final class Key extends
+      StringKey<com.google.gwtorm.client.Key<?>> {
+    private static final long serialVersionUID = 1L;
+
+    @Column(id = 1, length = 60)
+    protected String sessionCookie;
+
+    protected Key() {
+    }
+
+    public Key(final String token) {
+      this.sessionCookie = token;
+    }
+
+    @Override
+    public String get() {
+      return sessionCookie;
+    }
+
+    @Override
+    protected void set(String newValue) {
+      sessionCookie = newValue;
+    }
+  }
+
+  @Column(id = 1, name = Column.NONE)
+  protected ActiveSession.Key key;
+
+  @Column(id = 2)
+  protected Account.Id accountId;
+
+  @Column(id = 3)
+  protected Timestamp refreshCookieAt;
+
+  @Column(id = 4)
+  protected boolean persistentCookie;
+
+  @Column(id = 5, notNull = false)
+  protected AccountExternalId.Key externalId;
+
+  @Column(id = 6)
+  protected String xsrfToken;
+
+  @Column(id = 7)
+  protected Timestamp lastSeen;
+
+  protected ActiveSession() {
+  }
+
+  public ActiveSession(final ActiveSession.Key k, final Account.Id accountId,
+      final Timestamp refreshCookieAt, final boolean persistentCookie,
+      final AccountExternalId.Key externalId, final String xsrfToken) {
+    this.key = k;
+    this.accountId = accountId;
+    this.refreshCookieAt = refreshCookieAt;
+    this.persistentCookie = persistentCookie;
+    this.externalId = externalId;
+    this.xsrfToken = xsrfToken;
+    this.lastSeen = now();
+  }
+
+  public Key getKey() {
+    return key;
+  }
+
+  public Timestamp getLastSeen() {
+    return lastSeen;
+  }
+
+  public void updateLastSeen() {
+    lastSeen = now();
+  }
+
+  public Account.Id getAccountId() {
+    return accountId;
+  }
+
+  public Timestamp getRefreshCookieAt() {
+    return refreshCookieAt;
+  }
+
+  public void setRefreshCookieAt(Timestamp refreshCookieAt) {
+    this.refreshCookieAt = refreshCookieAt;
+  }
+
+  public boolean isPersistentCookie() {
+    return persistentCookie;
+  }
+
+  public AccountExternalId.Key getExternalId() {
+    return externalId;
+  }
+
+  public String getXsrfToken() {
+    return xsrfToken;
+  }
+
+  public boolean needsCookieRefresh() {
+    return refreshCookieAt.before(now());
+  }
+
+  private Timestamp now() {
+    return new Timestamp(System.currentTimeMillis());
+  }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ActiveSessionAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ActiveSessionAccess.java
new file mode 100644
index 0000000..6080566
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ActiveSessionAccess.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2010 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.reviewdb;
+
+import com.google.gwtorm.client.Access;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.PrimaryKey;
+import com.google.gwtorm.client.Query;
+import com.google.gwtorm.client.ResultSet;
+
+public interface ActiveSessionAccess extends
+    Access<ActiveSession, ActiveSession.Key> {
+  @PrimaryKey("key")
+  ActiveSession get(ActiveSession.Key key) throws OrmException;
+
+  @Query
+  ResultSet<ActiveSession> all() throws OrmException;
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java
index e4ae63d..0878fc2 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java
@@ -355,6 +355,10 @@
   @Column(id = 14, notNull = false)
   protected String topic;
 
+  /** Max 64 bit unsigned value (0xFFFFFFFFFFFFFFFF) minus {@link #sortKey} */
+  @Column(id = 15, length = 16)
+  protected String sortKeyDesc;
+
   protected Change() {
   }
 
@@ -406,8 +410,17 @@
     return sortKey;
   }
 
+  public String getSortKeyDesc() {
+    return sortKeyDesc;
+  }
+
   public void setSortKey(final String newSortKey) {
     sortKey = newSortKey;
+
+    // Since long is signed in Java, we need to use a little two's compliment
+    // trickery to get the same hex value that we would have gotten if we could
+    // do 0xFFFFFFFFFFFFFFFF - sortKey unsigned.
+    sortKeyDesc = Long.toHexString(-1l - Long.parseLong(sortKey, 16));
   }
 
   public Account.Id getOwner() {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeAccess.java
index dba2a58..6a29d6c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeAccess.java
@@ -41,10 +41,10 @@
   @Query("WHERE owner = ? AND open = true ORDER BY createdOn, changeId")
   ResultSet<Change> byOwnerOpen(Account.Id id) throws OrmException;
 
-  @Query("WHERE owner = ? AND open = false ORDER BY lastUpdatedOn DESC LIMIT 5")
+  @Query("WHERE owner = ? AND open = false ORDER BY sortKeyDesc LIMIT 5")
   ResultSet<Change> byOwnerClosed(Account.Id id) throws OrmException;
 
-  @Query("WHERE owner = ? AND open = false ORDER BY lastUpdatedOn")
+  @Query("WHERE owner = ? AND open = false ORDER BY sortKey")
   ResultSet<Change> byOwnerClosedAll(Account.Id id) throws OrmException;
 
   @Query("WHERE dest = ? AND status = '" + Change.STATUS_SUBMITTED
@@ -57,7 +57,7 @@
   @Query("WHERE open = true AND sortKey > ? ORDER BY sortKey LIMIT ?")
   ResultSet<Change> allOpenPrev(String sortKey, int limit) throws OrmException;
 
-  @Query("WHERE open = true AND sortKey < ? ORDER BY sortKey DESC LIMIT ?")
+  @Query("WHERE open = true AND sortKeyDesc > ? ORDER BY sortKeyDesc LIMIT ?")
   ResultSet<Change> allOpenNext(String sortKey, int limit) throws OrmException;
 
   @Query("WHERE open = true AND dest.projectName = ?")
@@ -68,8 +68,8 @@
   ResultSet<Change> byProjectOpenPrev(Project.NameKey p, String sortKey,
       int limit) throws OrmException;
 
-  @Query("WHERE open = true AND dest.projectName = ? AND sortKey < ?"
-      + " ORDER BY sortKey DESC LIMIT ?")
+  @Query("WHERE open = true AND dest.projectName = ? AND sortKeyDesc > ?"
+      + " ORDER BY sortKeyDesc LIMIT ?")
   ResultSet<Change> byProjectOpenNext(Project.NameKey p, String sortKey,
       int limit) throws OrmException;
 
@@ -78,8 +78,8 @@
   ResultSet<Change> byProjectClosedPrev(char status, Project.NameKey p,
       String sortKey, int limit) throws OrmException;
 
-  @Query("WHERE open = false AND status = ? AND dest.projectName = ? AND sortKey < ?"
-      + " ORDER BY sortKey DESC LIMIT ?")
+  @Query("WHERE open = false AND status = ? AND dest.projectName = ? AND sortKeyDesc > ?"
+      + " ORDER BY sortKeyDesc LIMIT ?")
   ResultSet<Change> byProjectClosedNext(char status, Project.NameKey p,
       String sortKey, int limit) throws OrmException;
 
@@ -87,7 +87,7 @@
   ResultSet<Change> allClosedPrev(char status, String sortKey, int limit)
       throws OrmException;
 
-  @Query("WHERE open = false AND status = ? AND sortKey < ? ORDER BY sortKey DESC LIMIT ?")
+  @Query("WHERE open = false AND status = ? AND sortKeyDesc > ? ORDER BY sortKeyDesc LIMIT ?")
   ResultSet<Change> allClosedNext(char status, String sortKey, int limit)
       throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java
index 26785a8..936519b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java
@@ -28,38 +28,28 @@
   @Query("WHERE key.patchKey.patchSetId.changeId = ?")
   ResultSet<PatchLineComment> byChange(Change.Id id) throws OrmException;
 
-  @Query("WHERE key.patchKey = ? AND status = '"
-      + PatchLineComment.STATUS_PUBLISHED + "' ORDER BY lineNbr,writtenOn")
-  ResultSet<PatchLineComment> published(Patch.Key patch) throws OrmException;
-
   @Query("WHERE key.patchKey.patchSetId.changeId = ?"
       + " AND key.patchKey.fileName = ? AND status = '"
       + PatchLineComment.STATUS_PUBLISHED + "' ORDER BY lineNbr,writtenOn")
-  ResultSet<PatchLineComment> published(Change.Id id, String file)
+  ResultSet<PatchLineComment> publishedByChangeFile(Change.Id id, String file)
       throws OrmException;
 
   @Query("WHERE key.patchKey.patchSetId = ? AND status = '"
       + PatchLineComment.STATUS_PUBLISHED + "'")
-  ResultSet<PatchLineComment> published(PatchSet.Id patchset)
+  ResultSet<PatchLineComment> publishedByPatchSet(PatchSet.Id patchset)
       throws OrmException;
 
   @Query("WHERE key.patchKey.patchSetId = ? AND status = '"
       + PatchLineComment.STATUS_DRAFT
       + "' AND author = ? ORDER BY key.patchKey,lineNbr,writtenOn")
-  ResultSet<PatchLineComment> draft(PatchSet.Id patchset, Account.Id author)
-      throws OrmException;
-
-  @Query("WHERE key.patchKey = ? AND status = '"
-      + PatchLineComment.STATUS_DRAFT
-      + "' AND author = ? ORDER BY lineNbr,writtenOn")
-  ResultSet<PatchLineComment> draft(Patch.Key patch, Account.Id author)
-      throws OrmException;
+  ResultSet<PatchLineComment> draftByPatchSet(PatchSet.Id patchset,
+      Account.Id author) throws OrmException;
 
   @Query("WHERE key.patchKey.patchSetId.changeId = ?"
       + " AND key.patchKey.fileName = ? AND author = ? AND status = '"
       + PatchLineComment.STATUS_DRAFT + "' ORDER BY lineNbr,writtenOn")
-  ResultSet<PatchLineComment> draft(Change.Id id, String file, Account.Id author)
-      throws OrmException;
+  ResultSet<PatchLineComment> draftByChangeFile(Change.Id id, String file,
+      Account.Id author) throws OrmException;
 
   @Query("WHERE status = '" + PatchLineComment.STATUS_DRAFT
       + "' AND author = ?")
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApproval.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApproval.java
index 341d085..e53df6f 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApproval.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApproval.java
@@ -88,6 +88,10 @@
   @Column(id = 5, length = 16, notNull = false)
   protected String changeSortKey;
 
+  /** <i>Cached copy of Change.sortKeyDesc</i>; only if {@link #changeOpen} = false */
+  @Column(id = 6, length = 16, notNull = false)
+  protected String changeSortKeyDesc;
+
   protected PatchSetApproval() {
   }
 
@@ -141,5 +145,6 @@
   public void cache(final Change c) {
     changeOpen = c.open;
     changeSortKey = c.sortKey;
+    changeSortKeyDesc = c.sortKeyDesc;
   }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApprovalAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApprovalAccess.java
index 417d264..1ab03a8 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApprovalAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApprovalAccess.java
@@ -40,7 +40,7 @@
       throws OrmException;
 
   @Query("WHERE changeOpen = false AND key.accountId = ?"
-      + " ORDER BY changeSortKey DESC LIMIT 10")
+      + " ORDER BY changeSortKeyDesc LIMIT 10")
   ResultSet<PatchSetApproval> closedByUser(Account.Id account)
       throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java
index f9b3cfa..9b7e9e4 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java
@@ -33,87 +33,90 @@
 public interface ReviewDb extends Schema {
   /* If you change anything, update SchemaVersion.C to use a new version. */
 
-  @Relation
+  @Relation(id = 1)
   SchemaVersionAccess schemaVersion();
 
-  @Relation
+  @Relation(id = 2)
   SystemConfigAccess systemConfig();
 
-  @Relation
+  @Relation(id = 3)
   ApprovalCategoryAccess approvalCategories();
 
-  @Relation
+  @Relation(id = 4)
   ApprovalCategoryValueAccess approvalCategoryValues();
 
-  @Relation
+  @Relation(id = 5)
   ContributorAgreementAccess contributorAgreements();
 
-  @Relation
+  @Relation(id = 6)
   AccountAccess accounts();
 
-  @Relation
+  @Relation(id = 7)
   AccountExternalIdAccess accountExternalIds();
 
-  @Relation
+  @Relation(id = 8)
   AccountSshKeyAccess accountSshKeys();
 
-  @Relation
+  @Relation(id = 9)
   AccountAgreementAccess accountAgreements();
 
-  @Relation
+  @Relation(id = 10)
   AccountGroupAccess accountGroups();
 
-  @Relation
+  @Relation(id = 11)
   AccountGroupNameAccess accountGroupNames();
 
-  @Relation
+  @Relation(id = 12)
   AccountGroupMemberAccess accountGroupMembers();
 
-  @Relation
+  @Relation(id = 13)
   AccountGroupMemberAuditAccess accountGroupMembersAudit();
 
-  @Relation
+  @Relation(id = 14)
   AccountGroupAgreementAccess accountGroupAgreements();
 
-  @Relation
+  @Relation(id = 15)
   AccountDiffPreferenceAccess accountDiffPreferences();
 
-  @Relation
+  @Relation(id = 16)
   StarredChangeAccess starredChanges();
 
-  @Relation
+  @Relation(id = 17)
   AccountProjectWatchAccess accountProjectWatches();
 
-  @Relation
+  @Relation(id = 18)
   AccountPatchReviewAccess accountPatchReviews();
 
-  @Relation
+  @Relation(id = 19)
   ProjectAccess projects();
 
-  @Relation
+  @Relation(id = 20)
   ChangeAccess changes();
 
-  @Relation
+  @Relation(id = 21)
   PatchSetApprovalAccess patchSetApprovals();
 
-  @Relation
+  @Relation(id = 22)
   ChangeMessageAccess changeMessages();
 
-  @Relation
+  @Relation(id = 23)
   PatchSetAccess patchSets();
 
-  @Relation
+  @Relation(id = 24)
   PatchSetAncestorAccess patchSetAncestors();
 
-  @Relation
+  @Relation(id = 25)
   PatchLineCommentAccess patchComments();
 
-  @Relation
+  @Relation(id = 26)
   RefRightAccess refRights();
 
-  @Relation
+  @Relation(id = 27)
   TrackingIdAccess trackingIds();
 
+  @Relation(id = 28)
+  ActiveSessionAccess activeSessions();
+
   /** Create the next unique id for an {@link Account}. */
   @Sequence(startWith = 1000000)
   int nextAccountId() throws OrmException;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/StarredChange.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/StarredChange.java
index 7e4359b..8f2dde0 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/StarredChange.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/StarredChange.java
@@ -43,6 +43,10 @@
       return accountId;
     }
 
+    public Change.Id getChangeId() {
+      return changeId;
+    }
+
     @Override
     public com.google.gwtorm.client.Key<?>[] members() {
       return new com.google.gwtorm.client.Key<?>[] {changeId};
@@ -66,4 +70,8 @@
   public Change.Id getChangeId() {
     return key.changeId;
   }
+
+  public StarredChange.Key getKey() {
+    return key;
+  }
 }
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql
index 0d41729..750b8ab 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql
@@ -89,10 +89,14 @@
 --    covers:             allOpenPrev, allOpenNext
 CREATE INDEX changes_allOpen
 ON changes (open, sort_key);
+CREATE INDEX changes_allOpenD
+ON changes (open, sort_key_desc);
 
 --    covers:             byProjectOpenPrev, byProjectOpenNext
 CREATE INDEX changes_byProjectOpen
 ON changes (open, dest_project_name, sort_key);
+CREATE INDEX changes_byProjectOpenD
+ON changes (open, dest_project_name, sort_key_desc);
 
 --    covers:             byProject
 CREATE INDEX changes_byProject
@@ -101,6 +105,8 @@
 --    covers:             allClosedPrev, allClosedNext
 CREATE INDEX changes_allClosed
 ON changes (open, status, sort_key);
+CREATE INDEX changes_allClosedD
+ON changes (open, status, sort_key_desc);
 
 CREATE INDEX changes_key
 ON changes (change_key);
@@ -116,6 +122,8 @@
 --    covers:             closedByUser
 CREATE INDEX patch_set_approvals_closedByUser
 ON patch_set_approvals (change_open, account_id, change_sort_key);
+CREATE INDEX patch_set_approvals_closedByUserD
+ON patch_set_approvals (change_open, account_id, change_sort_key_desc);
 
 
 -- *********************************************************************
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql
index b44351c..d3bda0b 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql
@@ -132,16 +132,25 @@
 CREATE INDEX changes_allOpen
 ON changes (sort_key)
 WHERE open = 'Y';
+CREATE INDEX changes_allOpenD
+ON changes (sort_key_desc)
+WHERE open = 'Y';
 
 --    covers:             byProjectOpenPrev, byProjectOpenNext
 CREATE INDEX changes_byProjectOpen
 ON changes (dest_project_name, sort_key)
 WHERE open = 'Y';
+CREATE INDEX changes_byProjectOpenD
+ON changes (dest_project_name, sort_key_desc)
+WHERE open = 'Y';
 
 --    covers:             allClosedPrev, allClosedNext
 CREATE INDEX changes_allClosed
 ON changes (status, sort_key)
 WHERE open = 'N';
+CREATE INDEX changes_allClosedD
+ON changes (status, sort_key_desc)
+WHERE open = 'N';
 
 --    covers:             byProject
 CREATE INDEX changes_byProject
@@ -163,6 +172,9 @@
 CREATE INDEX patch_set_approvals_closedByUser
 ON patch_set_approvals (account_id, change_sort_key)
 WHERE change_open = 'N';
+CREATE INDEX patch_set_approvals_closedByUserD
+ON patch_set_approvals (account_id, change_sort_key_desc)
+WHERE change_open = 'N';
 
 
 -- *********************************************************************
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/ChangeTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/ChangeTest.java
new file mode 100644
index 0000000..42f7401
--- /dev/null
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/ChangeTest.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2010 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.reviewdb;
+
+import junit.framework.TestCase;
+
+public class ChangeTest extends TestCase {
+
+  public void testSortKeyDesc() {
+    Change c = new Change();
+
+    c.setSortKey("000d4ad500003a36");
+    assertEquals("fff2b52affffc5c9", c.sortKeyDesc.toLowerCase());
+
+    c.setSortKey("0000000000000000");
+    assertEquals("ffffffffffffffff", c.sortKeyDesc.toLowerCase());
+  }
+
+}
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index 0a9ece3..d056ab3 100644
--- a/gerrit-server/pom.xml
+++ b/gerrit-server/pom.xml
@@ -54,11 +54,6 @@
     </dependency>
 
     <dependency>
-      <groupId>net.sf.ehcache</groupId>
-      <artifactId>ehcache-core</artifactId>
-    </dependency>
-
-    <dependency>
       <groupId>commons-dbcp</groupId>
       <artifactId>commons-dbcp</artifactId>
     </dependency>
@@ -116,6 +111,11 @@
     </dependency>
 
     <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+
+    <dependency>
       <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-common</artifactId>
       <version>${project.version}</version>
@@ -140,6 +140,11 @@
     </dependency>
 
     <dependency>
+      <groupId>com.google.protobuf</groupId>
+      <artifactId>protobuf-java</artifactId>
+    </dependency>
+
+    <dependency>
       <groupId>com.google.code.findbugs</groupId>
       <artifactId>jsr305</artifactId>
     </dependency>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
index 0ba85d1..76130a1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.events.ApprovalAttribute;
@@ -39,8 +38,10 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
@@ -184,11 +185,12 @@
      */
     public void doPatchsetCreatedHook(final Change change, final PatchSet patchSet) {
         final PatchSetCreatedEvent event = new PatchSetCreatedEvent();
-        final AccountState uploader = accountCache.get(patchSet.getUploader());
+        final Account uploader =
+          FutureUtil.get(accountCache.getAccount(patchSet.getUploader()));
 
         event.change = eventFactory.asChangeAttribute(change);
         event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
-        event.uploader = eventFactory.asAccountAttribute(uploader.getAccount());
+        event.uploader = eventFactory.asAccountAttribute(uploader);
         fireEvent(change, event);
 
         final List<String> args = new ArrayList<String>();
@@ -203,7 +205,7 @@
         args.add("--branch");
         args.add(event.change.branch);
         args.add("--uploader");
-        args.add(getDisplayName(uploader.getAccount()));
+        args.add(getDisplayName(uploader));
         args.add("--commit");
         args.add(event.patchSet.revision);
         args.add("--patchset");
@@ -375,7 +377,7 @@
     }
 
     private boolean isVisibleTo(Change change, IdentifiedUser user) {
-        final ProjectState pe = projectCache.get(change.getProject());
+        ProjectState pe = FutureUtil.getOrNull(projectCache.get(change.getProject()));
         if (pe == null) {
           return false;
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 36fc2ff..369532f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -210,4 +210,36 @@
       dst.setCharAt(o--, '0');
     }
   }
+
+  public static String invertSortKey(String sk) {
+    if (sk.equalsIgnoreCase("z")) {
+      return "/"; // The character before '0'
+    } else if (sk.equalsIgnoreCase("/")) {
+      return "z";
+    }
+
+    StringBuilder inv = new StringBuilder(16);
+    inv.setLength(16);
+    formatHexLong(inv, -1l - parseUnsignedHex(sk));
+
+    return inv.toString();
+  }
+
+  private static void formatHexLong(final StringBuilder dst, long l) {
+    int o = 15;
+    while (o >= 0 && l != 0) {
+      dst.setCharAt(o--, hexchar[(int) (l & 0xf)]);
+      l >>>= 4;
+    }
+    while (o >= 0) {
+      dst.setCharAt(o--, '0');
+    }
+  }
+
+  private static long parseUnsignedHex(String s) {
+    final long h = Long.parseLong(s.substring(0, 1), 16);
+    final long l = Long.parseLong(s.substring(1), 16);
+
+    return (h << 60) | l;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index 78cbed3..928ecf1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -22,11 +22,13 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.StarredChange;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountDiffPreferencesCache;
+import com.google.gerrit.server.account.AccountProjectWatchCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gwtorm.client.OrmException;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.inject.Inject;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
@@ -34,8 +36,6 @@
 
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.util.SystemReader;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -46,7 +46,6 @@
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Set;
 import java.util.TimeZone;
 
@@ -61,15 +60,24 @@
     private final Provider<String> canonicalUrl;
     private final Realm realm;
     private final AccountCache accountCache;
+    private final StarredChangesCache starredChangesCache;
+    private final AccountProjectWatchCache accountProjectWatchCache;
+    private final AccountDiffPreferencesCache accountDiffPreferencesCache;
 
     @Inject
     GenericFactory(final AuthConfig authConfig,
         final @CanonicalWebUrl Provider<String> canonicalUrl,
-        final Realm realm, final AccountCache accountCache) {
+        final Realm realm, final AccountCache accountCache,
+        final StarredChangesCache starredChangesCache,
+        final AccountProjectWatchCache accountProjectWatchCache,
+        final AccountDiffPreferencesCache accountDiffPreferencesCache) {
       this.authConfig = authConfig;
       this.canonicalUrl = canonicalUrl;
       this.realm = realm;
       this.accountCache = accountCache;
+      this.starredChangesCache = starredChangesCache;
+      this.accountProjectWatchCache = accountProjectWatchCache;
+      this.accountDiffPreferencesCache = accountDiffPreferencesCache;
     }
 
     public IdentifiedUser create(final Account.Id id) {
@@ -78,13 +86,15 @@
 
     public IdentifiedUser create(Provider<ReviewDb> db, Account.Id id) {
       return new IdentifiedUser(AccessPath.UNKNOWN, authConfig, canonicalUrl,
-          realm, accountCache, null, db, id);
+          realm, accountCache, starredChangesCache, accountProjectWatchCache,
+          accountDiffPreferencesCache, null, db, id);
     }
 
     public IdentifiedUser create(AccessPath accessPath,
         Provider<SocketAddress> remotePeerProvider, Account.Id id) {
       return new IdentifiedUser(accessPath, authConfig, canonicalUrl, realm,
-          accountCache, remotePeerProvider, null, id);
+          accountCache, starredChangesCache, accountProjectWatchCache,
+          accountDiffPreferencesCache, remotePeerProvider, null, id);
     }
   }
 
@@ -100,6 +110,9 @@
     private final Provider<String> canonicalUrl;
     private final Realm realm;
     private final AccountCache accountCache;
+    private final StarredChangesCache starredChangesCache;
+    private final AccountProjectWatchCache accountProjectWatchCache;
+    private final AccountDiffPreferencesCache accountDiffPreferencesCache;
 
     private final Provider<SocketAddress> remotePeerProvider;
     private final Provider<ReviewDb> dbProvider;
@@ -108,13 +121,18 @@
     RequestFactory(final AuthConfig authConfig,
         final @CanonicalWebUrl Provider<String> canonicalUrl,
         final Realm realm, final AccountCache accountCache,
-
+        final StarredChangesCache starredChangesCache,
+        final AccountProjectWatchCache accountProjectWatchCache,
+        final AccountDiffPreferencesCache accountDiffPreferencesCache,
         final @RemotePeer Provider<SocketAddress> remotePeerProvider,
         final Provider<ReviewDb> dbProvider) {
       this.authConfig = authConfig;
       this.canonicalUrl = canonicalUrl;
       this.realm = realm;
       this.accountCache = accountCache;
+      this.starredChangesCache = starredChangesCache;
+      this.accountProjectWatchCache = accountProjectWatchCache;
+      this.accountDiffPreferencesCache = accountDiffPreferencesCache;
 
       this.remotePeerProvider = remotePeerProvider;
       this.dbProvider = dbProvider;
@@ -123,16 +141,17 @@
     public IdentifiedUser create(final AccessPath accessPath,
         final Account.Id id) {
       return new IdentifiedUser(accessPath, authConfig, canonicalUrl, realm,
-          accountCache, remotePeerProvider, dbProvider, id);
+          accountCache, starredChangesCache, accountProjectWatchCache,
+          accountDiffPreferencesCache, remotePeerProvider, dbProvider, id);
     }
   }
 
-  private static final Logger log =
-      LoggerFactory.getLogger(IdentifiedUser.class);
-
   private final Provider<String> canonicalUrl;
   private final Realm realm;
   private final AccountCache accountCache;
+  private final StarredChangesCache starredChangesCache;
+  private final AccountProjectWatchCache accountProjectWatchCache;
+  private final AccountDiffPreferencesCache accountDiffPreferencesCache;
 
   @Nullable
   private final Provider<SocketAddress> remotePeerProvider;
@@ -151,20 +170,27 @@
   private IdentifiedUser(final AccessPath accessPath,
       final AuthConfig authConfig, final Provider<String> canonicalUrl,
       final Realm realm, final AccountCache accountCache,
+      final StarredChangesCache starredChangesCache,
+      final AccountProjectWatchCache accountProjectWatchCache,
+      final AccountDiffPreferencesCache accountDiffPreferencesCache,
       @Nullable final Provider<SocketAddress> remotePeerProvider,
       @Nullable final Provider<ReviewDb> dbProvider, final Account.Id id) {
     super(accessPath, authConfig);
     this.canonicalUrl = canonicalUrl;
     this.realm = realm;
     this.accountCache = accountCache;
+    this.starredChangesCache = starredChangesCache;
+    this.accountProjectWatchCache = accountProjectWatchCache;
+    this.accountDiffPreferencesCache = accountDiffPreferencesCache;
     this.remotePeerProvider = remotePeerProvider;
     this.dbProvider = dbProvider;
     this.accountId = id;
   }
 
-  private AccountState state() {
+  /** The account state of this user, caching most of their information. */
+  public AccountState getAccountState() {
     if (state == null) {
-      state = accountCache.get(getAccountId());
+      state = FutureUtil.get(accountCache.get(getAccountId()));
     }
     return state;
   }
@@ -176,30 +202,20 @@
 
   /** @return the user's user name; null if one has not been selected/assigned. */
   public String getUserName() {
-    return state().getUserName();
+    return getAccountState().getUserName();
   }
 
   public Account getAccount() {
-    return state().getAccount();
+    return getAccountState().getAccount();
   }
 
   public AccountDiffPreference getAccountDiffPreference() {
-    AccountDiffPreference diffPref;
-    try {
-      diffPref = dbProvider.get().accountDiffPreferences().get(getAccountId());
-      if (diffPref == null) {
-        diffPref = AccountDiffPreference.createDefault(getAccountId());
-      }
-    } catch (OrmException e) {
-      log.warn("Cannot query account diff preferences", e);
-      diffPref = AccountDiffPreference.createDefault(getAccountId());
-    }
-    return diffPref;
+    return FutureUtil.get(accountDiffPreferencesCache.get(getAccountId()));
   }
 
   public Set<String> getEmailAddresses() {
     if (emailAddresses == null) {
-      emailAddresses = state().getEmailAddresses();
+      emailAddresses = getAccountState().getEmailAddresses();
     }
     return emailAddresses;
   }
@@ -207,8 +223,8 @@
   @Override
   public Set<AccountGroup.Id> getEffectiveGroups() {
     if (effectiveGroups == null) {
-      if (authConfig.isIdentityTrustable(state().getExternalIds())) {
-        effectiveGroups = realm.groups(state());
+      if (authConfig.isIdentityTrustable(getAccountState().getExternalIds())) {
+        effectiveGroups = realm.groups(getAccountState());
 
       } else {
         effectiveGroups = authConfig.getRegisteredGroups();
@@ -224,13 +240,9 @@
         throw new OutOfScopeException("Not in request scoped user");
       }
       final Set<Change.Id> h = new HashSet<Change.Id>();
-      try {
-        for (final StarredChange sc : dbProvider.get().starredChanges()
-            .byAccount(getAccountId())) {
-          h.add(sc.getChangeId());
-        }
-      } catch (OrmException e) {
-        log.warn("Cannot query starred by user changes", e);
+      for (StarredChange sc : FutureUtil.get(starredChangesCache
+          .byAccount(getAccountId()))) {
+        h.add(sc.getChangeId());
       }
       starredChanges = Collections.unmodifiableSet(h);
     }
@@ -240,18 +252,8 @@
   @Override
   public Collection<AccountProjectWatch> getNotificationFilters() {
     if (notificationFilters == null) {
-      if (dbProvider == null) {
-        throw new OutOfScopeException("Not in request scoped user");
-      }
-      List<AccountProjectWatch> r;
-      try {
-        r = dbProvider.get().accountProjectWatches() //
-            .byAccount(getAccountId()).toList();
-      } catch (OrmException e) {
-        log.warn("Cannot query notification filters of a user", e);
-        r = Collections.emptyList();
-      }
-      notificationFilters = Collections.unmodifiableList(r);
+      notificationFilters =
+          FutureUtil.get(accountProjectWatchCache.byAccount(getAccountId()));
     }
     return notificationFilters;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java b/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java
index 23c0a6f..40bb3ea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java
@@ -33,16 +33,21 @@
       LoggerFactory.getLogger(RequestCleanup.class);
 
   private final List<Runnable> cleanup = new LinkedList<Runnable>();
+  private boolean run;
 
   /** Register a task to be completed after the request ends. */
   public void add(final Runnable task) {
     synchronized (cleanup) {
+      if (run) {
+        throw new IllegalStateException("Request has already been cleaned up");
+      }
       cleanup.add(task);
     }
   }
 
   public void run() {
     synchronized (cleanup) {
+      run = true;
       for (final Iterator<Runnable> i = cleanup.iterator(); i.hasNext();) {
         try {
           i.next().run();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesCache.java
new file mode 100644
index 0000000..cfa9554
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesCache.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2010 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;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.StarredChange;
+
+import java.util.List;
+
+public interface StarredChangesCache {
+  public ListenableFuture<List<StarredChange>> byAccount(Account.Id id);
+
+  public ListenableFuture<List<StarredChange>> byChange(Change.Id id);
+
+  public ListenableFuture<Void> evictAsync(StarredChange.Key key);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesCacheImpl.java
new file mode 100644
index 0000000..bfc8e64
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesCacheImpl.java
@@ -0,0 +1,148 @@
+// Copyright (C) 2010 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;
+
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.StarredChange;
+import com.google.gerrit.server.cache.Cache;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.EntryCreator;
+import com.google.gerrit.server.util.CompoundFuture;
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+
+import java.util.List;
+
+@Singleton
+public class StarredChangesCacheImpl implements StarredChangesCache {
+  private static final String BY_ACCOUNT_ID = "starred_user";
+  private static final String BY_CHANGE_ID = "starred_change";
+
+  protected static class StarredChangeList {
+    @Column(id = 1)
+    protected List<StarredChange> list;
+
+    protected StarredChangeList() {
+    }
+
+    public StarredChangeList(List<StarredChange> list) {
+      this.list = list;
+    }
+  }
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        final TypeLiteral<Cache<Account.Id, StarredChangeList>> byAccountIdType =
+            new TypeLiteral<Cache<Account.Id, StarredChangeList>>() {};
+        cache(byAccountIdType, BY_ACCOUNT_ID).populateWith(
+            ByAccountIdLoader.class);
+
+        final TypeLiteral<Cache<Change.Id, StarredChangeList>> byChangeIdType =
+            new TypeLiteral<Cache<Change.Id, StarredChangeList>>() {};
+        cache(byChangeIdType, BY_CHANGE_ID).populateWith(ByChangeIdLoader.class);
+
+        bind(StarredChangesCacheImpl.class);
+        bind(StarredChangesCache.class).to(StarredChangesCacheImpl.class);
+      }
+    };
+  }
+
+  private static final Function<StarredChangeList, List<StarredChange>> unpack =
+      new Function<StarredChangeList, List<StarredChange>>() {
+        @Override
+        public List<StarredChange> apply(StarredChangeList in) {
+          return in.list;
+        }
+      };
+
+  private final Cache<Account.Id, StarredChangeList> byAccountId;
+  private final Cache<Change.Id, StarredChangeList> byChangeId;
+
+  @Inject
+  StarredChangesCacheImpl(
+      @Named(BY_ACCOUNT_ID) Cache<Account.Id, StarredChangeList> byAccountId,
+      @Named(BY_CHANGE_ID) Cache<Change.Id, StarredChangeList> byChangeId) {
+    this.byAccountId = byAccountId;
+    this.byChangeId = byChangeId;
+  }
+
+  @Override
+  public ListenableFuture<List<StarredChange>> byAccount(Account.Id id) {
+    return Futures.compose(byAccountId.get(id), unpack);
+  }
+
+  @Override
+  public ListenableFuture<List<StarredChange>> byChange(Change.Id id) {
+    return Futures.compose(byChangeId.get(id), unpack);
+  }
+
+  @Override
+  public ListenableFuture<Void> evictAsync(StarredChange.Key key) {
+    return CompoundFuture.wrap(byAccountId.removeAsync(key.getParentKey()),
+        byChangeId.removeAsync(key.getChangeId()));
+  }
+
+  static class ByAccountIdLoader extends
+      EntryCreator<Account.Id, StarredChangeList> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    ByAccountIdLoader(SchemaFactory<ReviewDb> schema) {
+      this.schema = schema;
+    }
+
+    @Override
+    public StarredChangeList createEntry(Account.Id id) throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        return new StarredChangeList(db.starredChanges().byAccount(id).toList());
+      } finally {
+        db.close();
+      }
+    }
+  }
+
+  static class ByChangeIdLoader extends
+      EntryCreator<Change.Id, StarredChangeList> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    ByChangeIdLoader(SchemaFactory<ReviewDb> schema) {
+      this.schema = schema;
+    }
+
+    @Override
+    public StarredChangeList createEntry(Change.Id id) throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        return new StarredChangeList(db.starredChanges().byChange(id).toList());
+      } finally {
+        db.close();
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountAgreementsCache.java
similarity index 63%
copy from gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/account/AccountAgreementsCache.java
index 1eb1a4f..1640248 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountAgreementsCache.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2010 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.
@@ -14,13 +14,14 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountAgreement;
 
-import java.util.Set;
+import java.util.List;
 
-/** Translates an email address to a set of matching accounts. */
-public interface AccountByEmailCache {
-  public Set<Account.Id> get(String email);
+public interface AccountAgreementsCache {
+  public ListenableFuture<List<AccountAgreement>> byAccount(Account.Id id);
 
-  public void evict(String email);
+  public ListenableFuture<Void> evictAsync(AccountAgreement.Key key);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountAgreementsCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountAgreementsCacheImpl.java
new file mode 100644
index 0000000..3463ed6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountAgreementsCacheImpl.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2010 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.account;
+
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountAgreement;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.cache.Cache;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.EntryCreator;
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+
+import java.util.List;
+
+@Singleton
+public class AccountAgreementsCacheImpl implements AccountAgreementsCache {
+  private static final String BY_ACCOUNT_ID = "account_agreements";
+
+  protected static class AccountAgreementsList {
+    @Column(id = 1)
+    protected List<AccountAgreement> list;
+
+    protected AccountAgreementsList() {
+    }
+
+    public AccountAgreementsList(List<AccountAgreement> list) {
+      this.list = list;
+    }
+  }
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        final TypeLiteral<Cache<Account.Id, AccountAgreementsList>> byAccountIdType =
+            new TypeLiteral<Cache<Account.Id, AccountAgreementsList>>() {};
+        cache(byAccountIdType, BY_ACCOUNT_ID).populateWith(
+            ByAccountIdLoader.class);
+
+        bind(AccountAgreementsCacheImpl.class);
+        bind(AccountAgreementsCache.class).to(AccountAgreementsCacheImpl.class);
+      }
+    };
+  }
+
+  private static final Function<AccountAgreementsList, List<AccountAgreement>> unpack =
+      new Function<AccountAgreementsList, List<AccountAgreement>>() {
+        public List<AccountAgreement> apply(AccountAgreementsList in) {
+          return in.list;
+        }
+      };
+
+  private final Cache<Account.Id, AccountAgreementsList> byAccountId;
+
+  @Inject
+  AccountAgreementsCacheImpl(
+      @Named(BY_ACCOUNT_ID) Cache<Account.Id, AccountAgreementsList> byAccountId) {
+    this.byAccountId = byAccountId;
+  }
+
+  @Override
+  public ListenableFuture<List<AccountAgreement>> byAccount(Account.Id id) {
+    return Futures.compose(byAccountId.get(id), unpack);
+  }
+
+  @Override
+  public ListenableFuture<Void> evictAsync(AccountAgreement.Key key) {
+    return byAccountId.removeAsync(key.getParentKey());
+  }
+
+  static class ByAccountIdLoader extends
+      EntryCreator<Account.Id, AccountAgreementsList> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    ByAccountIdLoader(SchemaFactory<ReviewDb> schema) {
+      this.schema = schema;
+    }
+
+    @Override
+    public AccountAgreementsList createEntry(Account.Id id) throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        return new AccountAgreementsList(db.accountAgreements().byAccount(id)
+            .toList());
+      } finally {
+        db.close();
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
deleted file mode 100644
index 64046fa..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
+++ /dev/null
@@ -1,114 +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.server.account;
-
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
-import com.google.gwtorm.client.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Module;
-import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
-import com.google.inject.name.Named;
-
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
-/** Translates an email address to a set of matching accounts. */
-@Singleton
-public class AccountByEmailCacheImpl implements AccountByEmailCache {
-  private static final String CACHE_NAME = "accounts_byemail";
-
-  public static Module module() {
-    return new CacheModule() {
-      @Override
-      protected void configure() {
-        final TypeLiteral<Cache<String, Set<Account.Id>>> type =
-            new TypeLiteral<Cache<String, Set<Account.Id>>>() {};
-        core(type, CACHE_NAME).populateWith(Loader.class);
-        bind(AccountByEmailCacheImpl.class);
-        bind(AccountByEmailCache.class).to(AccountByEmailCacheImpl.class);
-      }
-    };
-  }
-
-  private final Cache<String, Set<Account.Id>> cache;
-
-  @Inject
-  AccountByEmailCacheImpl(
-      @Named(CACHE_NAME) final Cache<String, Set<Account.Id>> cache) {
-    this.cache = cache;
-  }
-
-  public Set<Account.Id> get(final String email) {
-    return cache.get(email);
-  }
-
-  public void evict(final String email) {
-    cache.remove(email);
-  }
-
-  static class Loader extends EntryCreator<String, Set<Account.Id>> {
-    private final SchemaFactory<ReviewDb> schema;
-
-    @Inject
-    Loader(final SchemaFactory<ReviewDb> schema) {
-      this.schema = schema;
-    }
-
-    @Override
-    public Set<Account.Id> createEntry(final String email) throws Exception {
-      final ReviewDb db = schema.open();
-      try {
-        final HashSet<Account.Id> r = new HashSet<Account.Id>();
-        for (Account a : db.accounts().byPreferredEmail(email)) {
-          r.add(a.getId());
-        }
-        for (AccountExternalId a : db.accountExternalIds()
-            .byEmailAddress(email)) {
-          r.add(a.getAccountId());
-        }
-        return pack(r);
-      } finally {
-        db.close();
-      }
-    }
-
-    @Override
-    public Set<Account.Id> missing(final String key) {
-      return Collections.emptySet();
-    }
-
-    private static Set<Account.Id> pack(final Set<Account.Id> c) {
-      switch (c.size()) {
-        case 0:
-          return Collections.emptySet();
-        case 1:
-          return one(c);
-        default:
-          return Collections.unmodifiableSet(new HashSet<Account.Id>(c));
-      }
-    }
-
-    private static <T> Set<T> one(final Set<T> c) {
-      return Collections.singleton(c.iterator().next());
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java
index 1b3626b..e35e186 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java
@@ -14,15 +14,33 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountExternalId;
+
+import java.util.Set;
 
 /** Caches important (but small) account state to avoid database hits. */
 public interface AccountCache {
-  public AccountState get(Account.Id accountId);
+  // Accounts indexed by unique internal identity.
 
-  public AccountState getByUsername(String username);
+  public ListenableFuture<AccountState> get(Account.Id accountId);
 
-  public void evict(Account.Id accountId);
+  public ListenableFuture<Account> getAccount(Account.Id accountId);
 
-  public void evictByUsername(String username);
+  public ListenableFuture<Void> evictAsync(Account.Id accountId);
+
+
+  // Accounts indexed by external identity (OpenID, HTTP/LDAP/SSH username).
+
+  public ListenableFuture<AccountExternalId> get(AccountExternalId.Key key);
+
+  public ListenableFuture<Void> evictAsync(AccountExternalId.Key id);
+
+
+  // Accounts indexed by email address.
+
+  public ListenableFuture<Set<Account.Id>> byEmail(String email);
+
+  public ListenableFuture<Void> evictEmailAsync(String email);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 52ccc66..5bd7886 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -14,6 +14,11 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.reviewdb.AccountGroup;
@@ -23,6 +28,8 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.util.FutureUtil;
+import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
@@ -34,13 +41,15 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 /** Caches important (but small) account state to avoid database hits. */
 @Singleton
 public class AccountCacheImpl implements AccountCache {
   private static final String BYID_NAME = "accounts";
-  private static final String BYUSER_NAME = "accounts_byname";
+  private static final String BYEXT_NAME = "accounts_byext";
+  private static final String BYEMAIL_NAME = "accounts_byemail";
 
   public static Module module() {
     return new CacheModule() {
@@ -48,11 +57,15 @@
       protected void configure() {
         final TypeLiteral<Cache<Account.Id, AccountState>> byIdType =
             new TypeLiteral<Cache<Account.Id, AccountState>>() {};
-        core(byIdType, BYID_NAME).populateWith(ByIdLoader.class);
+        cache(byIdType, BYID_NAME).populateWith(StateLoader.class);
 
-        final TypeLiteral<Cache<String, Account.Id>> byUsernameType =
-            new TypeLiteral<Cache<String, Account.Id>>() {};
-        core(byUsernameType, BYUSER_NAME).populateWith(ByNameLoader.class);
+        final TypeLiteral<Cache<AccountExternalId.Key, AccountExternalId>> byKeyType =
+            new TypeLiteral<Cache<AccountExternalId.Key, AccountExternalId>>() {};
+        cache(byKeyType, BYEXT_NAME).populateWith(ExtLoader.class);
+
+        final TypeLiteral<Cache<Email, AccountIdSet>> type =
+            new TypeLiteral<Cache<Email, AccountIdSet>>() {};
+        cache(type, BYEMAIL_NAME).populateWith(EmailLoader.class);
 
         bind(AccountCacheImpl.class);
         bind(AccountCache.class).to(AccountCacheImpl.class);
@@ -61,60 +74,76 @@
   }
 
   private final Cache<Account.Id, AccountState> byId;
-  private final Cache<String, Account.Id> byName;
+  private final Cache<AccountExternalId.Key, AccountExternalId> byExt;
+  private final Cache<Email, AccountIdSet> byEmail;
 
   @Inject
   AccountCacheImpl(@Named(BYID_NAME) Cache<Account.Id, AccountState> byId,
-      @Named(BYUSER_NAME) Cache<String, Account.Id> byUsername) {
+      @Named(BYEXT_NAME) Cache<AccountExternalId.Key, AccountExternalId> byExt,
+      @Named(BYEMAIL_NAME) final Cache<Email, AccountIdSet> byEmail) {
     this.byId = byId;
-    this.byName = byUsername;
+    this.byExt = byExt;
+    this.byEmail = byEmail;
   }
 
-  public AccountState get(final Account.Id accountId) {
+  public ListenableFuture<AccountState> get(Account.Id accountId) {
     return byId.get(accountId);
   }
 
+  public ListenableFuture<Account> getAccount(Account.Id accountId) {
+    return Futures.compose(get(accountId), AccountState.GET_ACCOUNT);
+  }
+
+  public ListenableFuture<Void> evictAsync(Account.Id accountId) {
+    return byId.removeAsync(accountId);
+  }
+
   @Override
-  public AccountState getByUsername(String username) {
-    Account.Id id = byName.get(username);
-    return id != null ? byId.get(id) : null;
+  public ListenableFuture<AccountExternalId> get(AccountExternalId.Key key) {
+    return byExt.get(key);
   }
 
-  public void evict(final Account.Id accountId) {
-    byId.remove(accountId);
+  @Override
+  public ListenableFuture<Void> evictAsync(AccountExternalId.Key key) {
+    return byExt.removeAsync(key);
   }
 
-  public void evictByUsername(String username) {
-    byName.remove(username);
+  @Override
+  public ListenableFuture<Set<Account.Id>> byEmail(String email) {
+    if (email == null) {
+      return Futures.immediateFuture(Collections.<Account.Id> emptySet());
+    }
+    return Futures.compose(byEmail.get(new Email(email)), AccountIdSet.unpack);
   }
 
-  static class ByIdLoader extends EntryCreator<Account.Id, AccountState> {
+  @Override
+  public ListenableFuture<Void> evictEmailAsync(String email) {
+    if (email == null) {
+      return Futures.immediateFuture(null);
+    }
+    return byEmail.removeAsync(new Email(email));
+  }
+
+  static class StateLoader extends EntryCreator<Account.Id, AccountState> {
     private final SchemaFactory<ReviewDb> schema;
     private final Set<AccountGroup.Id> registered;
     private final Set<AccountGroup.Id> anonymous;
     private final GroupCache groupCache;
-    private final Cache<String, Account.Id> byName;
 
     @Inject
-    ByIdLoader(SchemaFactory<ReviewDb> sf, AuthConfig auth,
-        GroupCache groupCache,
-        @Named(BYUSER_NAME) Cache<String, Account.Id> byUsername) {
+    StateLoader(SchemaFactory<ReviewDb> sf, AuthConfig auth,
+        GroupCache groupCache) {
       this.schema = sf;
       this.registered = auth.getRegisteredGroups();
       this.anonymous = auth.getAnonymousGroups();
       this.groupCache = groupCache;
-      this.byName = byUsername;
     }
 
     @Override
     public AccountState createEntry(final Account.Id key) throws Exception {
       final ReviewDb db = schema.open();
       try {
-        final AccountState state = load(db, key);
-        if (state.getUserName() != null) {
-          byName.put(state.getUserName(), state.getAccount().getId());
-        }
-        return state;
+        return load(db, key);
       } finally {
         db.close();
       }
@@ -129,16 +158,16 @@
         return missing(who);
       }
 
-      final Collection<AccountExternalId> externalIds =
-          Collections.unmodifiableCollection(db.accountExternalIds().byAccount(
-              who).toList());
+      List<ListenableFuture<AccountGroup>> myGroups = Lists.newArrayList();
+      for (AccountGroupMember g : db.accountGroupMembers().byAccount(who)) {
+        myGroups.add(groupCache.get(g.getAccountGroupId()));
+      }
 
       Set<AccountGroup.Id> internalGroups = new HashSet<AccountGroup.Id>();
-      for (AccountGroupMember g : db.accountGroupMembers().byAccount(who)) {
-        final AccountGroup.Id groupId = g.getAccountGroupId();
-        final AccountGroup group = groupCache.get(groupId);
-        if (group != null && group.getType() == AccountGroup.Type.INTERNAL) {
-          internalGroups.add(groupId);
+      for (ListenableFuture<AccountGroup> f : myGroups) {
+        AccountGroup group = FutureUtil.get(f);
+        if (group.getType() == AccountGroup.Type.INTERNAL) {
+          internalGroups.add(group.getId());
         }
       }
 
@@ -149,6 +178,9 @@
         internalGroups = Collections.unmodifiableSet(internalGroups);
       }
 
+      final Collection<AccountExternalId> externalIds =
+          Collections.unmodifiableCollection(db.accountExternalIds() //
+              .byAccount(who).toList());
       return new AccountState(account, internalGroups, externalIds);
     }
 
@@ -160,26 +192,86 @@
     }
   }
 
-  static class ByNameLoader extends EntryCreator<String, Account.Id> {
+  static class ExtLoader extends
+      EntryCreator<AccountExternalId.Key, AccountExternalId> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
-    ByNameLoader(final SchemaFactory<ReviewDb> sf) {
-      this.schema = sf;
+    ExtLoader(SchemaFactory<ReviewDb> schema) {
+      this.schema = schema;
     }
 
     @Override
-    public Account.Id createEntry(final String username) throws Exception {
+    public AccountExternalId createEntry(AccountExternalId.Key key)
+        throws Exception {
       final ReviewDb db = schema.open();
       try {
-        final AccountExternalId.Key key = new AccountExternalId.Key( //
-            AccountExternalId.SCHEME_USERNAME, //
-            username);
-        final AccountExternalId id = db.accountExternalIds().get(key);
-        return id != null ? id.getAccountId() : null;
+        return db.accountExternalIds().get(key);
       } finally {
         db.close();
       }
     }
   }
+
+  static class Email {
+    @Column(id = 1)
+    String email;
+
+    Email() {
+    }
+
+    Email(String email) {
+      this.email = email;
+    }
+  }
+
+  static class AccountIdSet {
+    static final Function<AccountIdSet, Set<Account.Id>> unpack =
+        new Function<AccountIdSet, Set<Account.Id>>() {
+          @Override
+          public Set<Account.Id> apply(AccountIdSet from) {
+            return from.ids;
+          }
+        };
+
+    @Column(id = 1)
+    Set<Account.Id> ids;
+
+    AccountIdSet() {
+    }
+
+    AccountIdSet(Set<Account.Id> ids) {
+      this.ids = ids;
+    }
+  }
+
+  static class EmailLoader extends EntryCreator<Email, AccountIdSet> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    EmailLoader(final SchemaFactory<ReviewDb> schema) {
+      this.schema = schema;
+    }
+
+    @Override
+    public AccountIdSet createEntry(Email key) throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        Set<Account.Id> res = Sets.newHashSet();
+        for (AccountExternalId extId : db.accountExternalIds().byEmailAddress(
+            key.email)) {
+          res.add(extId.getAccountId());
+        }
+        return new AccountIdSet(Collections.unmodifiableSet(res));
+      } finally {
+        db.close();
+      }
+    }
+
+    @Override
+    public AccountIdSet missing(Email key) {
+      Set<Account.Id> res = Collections.emptySet();
+      return new AccountIdSet(res);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDiffPreferencesCache.java
similarity index 65%
rename from gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDiffPreferencesCache.java
index 1eb1a4f..4fa2902 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDiffPreferencesCache.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2010 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.
@@ -14,13 +14,12 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountDiffPreference;
 
-import java.util.Set;
+public interface AccountDiffPreferencesCache {
+  public ListenableFuture<AccountDiffPreference> get(Account.Id key);
 
-/** Translates an email address to a set of matching accounts. */
-public interface AccountByEmailCache {
-  public Set<Account.Id> get(String email);
-
-  public void evict(String email);
+  public ListenableFuture<Void> evictAsync(Account.Id key);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDiffPreferencesCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDiffPreferencesCacheImpl.java
new file mode 100644
index 0000000..b54f50c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDiffPreferencesCacheImpl.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2010 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.account;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountDiffPreference;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.cache.Cache;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.EntryCreator;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+
+@Singleton
+public class AccountDiffPreferencesCacheImpl implements
+    AccountDiffPreferencesCache {
+  private static final String BY_ACCOUNT_ID = "diff_pref";
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        final TypeLiteral<Cache<Account.Id, AccountDiffPreference>> byAccountIdType =
+            new TypeLiteral<Cache<Account.Id, AccountDiffPreference>>() {};
+        cache(byAccountIdType, BY_ACCOUNT_ID).populateWith(
+            ByAccountIdLoader.class);
+
+        bind(AccountDiffPreferencesCacheImpl.class);
+        bind(AccountDiffPreferencesCache.class).to(
+            AccountDiffPreferencesCacheImpl.class);
+      }
+    };
+  }
+
+  private final Cache<Account.Id, AccountDiffPreference> byAccountId;
+
+  @Inject
+  AccountDiffPreferencesCacheImpl(
+      @Named(BY_ACCOUNT_ID) Cache<Account.Id, AccountDiffPreference> byAccountId) {
+    this.byAccountId = byAccountId;
+  }
+
+  @Override
+  public ListenableFuture<AccountDiffPreference> get(Account.Id key) {
+    return byAccountId.get(key);
+  }
+
+  @Override
+  public ListenableFuture<Void> evictAsync(Account.Id key) {
+    return byAccountId.removeAsync(key);
+  }
+
+  static class ByAccountIdLoader extends
+      EntryCreator<Account.Id, AccountDiffPreference> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    ByAccountIdLoader(SchemaFactory<ReviewDb> schema) {
+      this.schema = schema;
+    }
+
+    @Override
+    public AccountDiffPreference createEntry(Account.Id id) throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        return db.accountDiffPreferences().get(id);
+      } finally {
+        db.close();
+      }
+    }
+
+    @Override
+    public AccountDiffPreference missing(Account.Id key) {
+      return AccountDiffPreference.createDefault(key);
+    }
+
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountGroupAgreementsCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountGroupAgreementsCache.java
new file mode 100644
index 0000000..f1a502e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountGroupAgreementsCache.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2010 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.account;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountGroupAgreement;
+
+import java.util.List;
+
+public interface AccountGroupAgreementsCache {
+  public ListenableFuture<List<AccountGroupAgreement>> byGroup(
+      AccountGroup.Id id);
+
+  public ListenableFuture<Void> evictAsync(AccountGroupAgreement.Key key);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountGroupAgreementsCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountGroupAgreementsCacheImpl.java
new file mode 100644
index 0000000..0f0636a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountGroupAgreementsCacheImpl.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2010 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.account;
+
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountGroupAgreement;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.AccountGroupAgreement.Key;
+import com.google.gerrit.server.cache.Cache;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.EntryCreator;
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+
+import java.util.List;
+
+@Singleton
+public class AccountGroupAgreementsCacheImpl implements
+    AccountGroupAgreementsCache {
+  private static final String BY_GROUP_ID = "group_agreements";
+
+  protected static class AccountGroupAgreementList {
+    @Column(id = 1)
+    protected List<AccountGroupAgreement> list;
+
+    protected AccountGroupAgreementList() {
+    }
+
+    public AccountGroupAgreementList(List<AccountGroupAgreement> list) {
+      this.list = list;
+    }
+  }
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        final TypeLiteral<Cache<AccountGroup.Id, AccountGroupAgreementList>> byGroupIdType =
+            new TypeLiteral<Cache<AccountGroup.Id, AccountGroupAgreementList>>() {};
+        cache(byGroupIdType, BY_GROUP_ID).populateWith(ByGroupIdLoader.class);
+
+        bind(AccountGroupAgreementsCacheImpl.class);
+        bind(AccountGroupAgreementsCache.class).to(
+            AccountGroupAgreementsCacheImpl.class);
+      }
+    };
+  }
+
+  private static final Function<AccountGroupAgreementList, List<AccountGroupAgreement>> unpack =
+      new Function<AccountGroupAgreementList, List<AccountGroupAgreement>>() {
+        public List<AccountGroupAgreement> apply(AccountGroupAgreementList in) {
+          return in.list;
+        }
+      };
+
+  private final Cache<AccountGroup.Id, AccountGroupAgreementList> byGroupId;
+
+  @Inject
+  AccountGroupAgreementsCacheImpl(
+      @Named(BY_GROUP_ID) Cache<AccountGroup.Id, AccountGroupAgreementList> byGroupId) {
+    this.byGroupId = byGroupId;
+  }
+
+  @Override
+  public ListenableFuture<List<AccountGroupAgreement>> byGroup(
+      AccountGroup.Id id) {
+    return Futures.compose(byGroupId.get(id), unpack);
+  }
+
+  @Override
+  public ListenableFuture<Void> evictAsync(Key key) {
+    return byGroupId.removeAsync(key.getParentKey());
+  }
+
+  static class ByGroupIdLoader extends
+      EntryCreator<AccountGroup.Id, AccountGroupAgreementList> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    ByGroupIdLoader(SchemaFactory<ReviewDb> schema) {
+      this.schema = schema;
+    }
+
+    @Override
+    public AccountGroupAgreementList createEntry(AccountGroup.Id id)
+        throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        return new AccountGroupAgreementList(db.accountGroupAgreements()
+            .byGroup(id).toList());
+      } finally {
+        db.close();
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountGroupCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountGroupCollection.java
new file mode 100644
index 0000000..8d9f638
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountGroupCollection.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2010 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.account;
+
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gwtorm.client.Column;
+
+import java.util.Collection;
+import java.util.Collections;
+
+/** Wrapper around a Collection<Account.Id> */
+public class AccountGroupCollection {
+  @Column(id = 1)
+  protected Collection<AccountGroup> groups;
+
+  protected AccountGroupCollection(){
+  }
+
+  public AccountGroupCollection(Collection<AccountGroup> groups) {
+    this.groups = Collections.unmodifiableCollection(groups);
+  }
+
+  public Collection<AccountGroup> getGroups() {
+    return groups;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfoCacheFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfoCacheFactory.java
index bb6e278..5c2e872 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfoCacheFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfoCacheFactory.java
@@ -14,15 +14,18 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.inject.Inject;
 
-import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Future;
 
 /** Efficiently builds an {@link AccountInfoCache}. */
 public class AccountInfoCacheFactory {
@@ -31,12 +34,12 @@
   }
 
   private final AccountCache accountCache;
-  private final Map<Account.Id, Account> out;
+  private final Map<Account.Id, Future<Account>> want;
 
   @Inject
   AccountInfoCacheFactory(final AccountCache accountCache) {
     this.accountCache = accountCache;
-    this.out = new HashMap<Account.Id, Account>();
+    this.want = Maps.newHashMap();
   }
 
   /**
@@ -44,32 +47,34 @@
    *
    * @param id identity that will be needed in the future; may be null.
    */
-  public void want(final Account.Id id) {
-    if (id != null && !out.containsKey(id)) {
-      out.put(id, accountCache.get(id).getAccount());
+  public void want(Account.Id id) {
+    if (id != null && !want.containsKey(id)) {
+      want.put(id, accountCache.getAccount(id));
     }
   }
 
   /** Indicate one or more accounts will be needed later on. */
-  public void want(final Iterable<Account.Id> ids) {
-    for (final Account.Id id : ids) {
+  public void want(final Collection<Account.Id> ids) {
+    for (Account.Id id : ids) {
       want(id);
     }
   }
 
   public Account get(final Account.Id id) {
-    want(id);
-    return out.get(id);
+    if (id != null) {
+      want(id);
+      return FutureUtil.get(want.get(id));
+    } else {
+      return null;
+    }
   }
 
-  /**
-   * Create an AccountInfoCache with the currently loaded Account entities.
-   * */
+  /** Create an AccountInfoCache with the currently loaded Account entities. */
   public AccountInfoCache create() {
-    final List<AccountInfo> r = new ArrayList<AccountInfo>(out.size());
-    for (final Account a : out.values()) {
-      r.add(new AccountInfo(a));
+    List<AccountInfo> res = Lists.newArrayListWithCapacity(want.size());
+    for (Future<Account> f : want.values()) {
+      res.add(new AccountInfo(FutureUtil.get(f)));
     }
-    return new AccountInfoCache(r);
+    return new AccountInfoCache(res);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 83a5eed..3a241d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
 import com.google.gerrit.common.errors.InvalidUserNameException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
@@ -25,6 +26,8 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.util.FutureException;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
@@ -36,6 +39,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /** Tracks authentication related details for user accounts. */
@@ -45,8 +49,7 @@
       LoggerFactory.getLogger(AccountManager.class);
 
   private final SchemaFactory<ReviewDb> schema;
-  private final AccountCache byIdCache;
-  private final AccountByEmailCache byEmailCache;
+  private final AccountCache accountCache;
   private final AuthConfig authConfig;
   private final Realm realm;
   private final IdentifiedUser.GenericFactory userFactory;
@@ -55,13 +58,12 @@
 
   @Inject
   AccountManager(final SchemaFactory<ReviewDb> schema,
-      final AccountCache byIdCache, final AccountByEmailCache byEmailCache,
-      final AuthConfig authConfig, final Realm accountMapper,
+      final AccountCache accountCache, final AuthConfig authConfig,
+      final Realm accountMapper,
       final IdentifiedUser.GenericFactory userFactory,
       final ChangeUserName.Factory changeUserNameFactory) throws OrmException {
     this.schema = schema;
-    this.byIdCache = byIdCache;
-    this.byEmailCache = byEmailCache;
+    this.accountCache = accountCache;
     this.authConfig = authConfig;
     this.realm = accountMapper;
     this.userFactory = userFactory;
@@ -79,18 +81,13 @@
   /**
    * @return user identified by this external identity string, or null.
    */
-  public Account.Id lookup(final String externalId) throws AccountException {
+  public Account.Id lookup(String externalId) throws AccountException {
     try {
-      final ReviewDb db = schema.open();
-      try {
-        final AccountExternalId ext =
-            db.accountExternalIds().get(new AccountExternalId.Key(externalId));
-        return ext != null ? ext.getAccountId() : null;
-      } finally {
-        db.close();
-      }
-    } catch (OrmException e) {
-      throw new AccountException("Cannot lookup account " + externalId, e);
+      AccountExternalId.Key key = new AccountExternalId.Key(externalId);
+      AccountExternalId ext = FutureUtil.get(accountCache.get(key));
+      return ext != null ? ext.getAccountId() : null;
+    } catch (FutureException err) {
+      throw new AccountException("Cannot lookup account " + externalId, err);
     }
   }
 
@@ -107,8 +104,8 @@
     try {
       final ReviewDb db = schema.open();
       try {
-        final AccountExternalId.Key key = id(who);
-        final AccountExternalId id = db.accountExternalIds().get(key);
+        AccountExternalId.Key key = id(who);
+        AccountExternalId id = FutureUtil.get(accountCache.get(key));
         if (id == null) {
           // New account, automatically create and return.
           //
@@ -129,13 +126,16 @@
       } finally {
         db.close();
       }
+    } catch (FutureException e) {
+      throw new AccountException("Authentication error", e);
     } catch (OrmException e) {
       throw new AccountException("Authentication error", e);
     }
   }
 
   private void update(final ReviewDb db, final AuthRequest who,
-      final AccountExternalId extId) throws OrmException {
+      AccountExternalId extId) throws OrmException {
+    final List<Future<Void>> evictions = Lists.newArrayList();
     final IdentifiedUser user = userFactory.create(extId.getAccountId());
     Account toUpdate = null;
 
@@ -151,8 +151,14 @@
         toUpdate.setPreferredEmail(newEmail);
       }
 
+      AccountExternalId.Key extKey = extId.getKey();
+      extId = db.accountExternalIds().get(extKey);
+      if (extId == null) {
+        extId = new AccountExternalId(user.getAccountId(), extKey);
+      }
       extId.setEmailAddress(newEmail);
       db.accountExternalIds().update(Collections.singleton(extId));
+      evictions.add(accountCache.evictAsync(extKey));
     }
 
     if (!realm.allowsEdit(Account.FieldName.FULL_NAME)
@@ -171,12 +177,13 @@
     }
 
     if (newEmail != null && !newEmail.equals(oldEmail)) {
-      byEmailCache.evict(oldEmail);
-      byEmailCache.evict(newEmail);
+      evictions.add(accountCache.evictEmailAsync(oldEmail));
+      evictions.add(accountCache.evictEmailAsync(newEmail));
     }
     if (toUpdate != null) {
-      byIdCache.evict(toUpdate.getId());
+      evictions.add(accountCache.evictAsync(toUpdate.getId()));
     }
+    FutureUtil.waitFor(evictions);
   }
 
   private Account load(Account toUpdate, Account.Id accountId, ReviewDb db)
@@ -196,14 +203,16 @@
 
   private AuthResult create(final ReviewDb db, final AuthRequest who)
       throws OrmException, AccountException {
+    final List<Future<Void>> evictions = Lists.newArrayList();
+
     if (authConfig.isAllowGoogleAccountUpgrade()
         && who.isScheme(OpenIdUrls.URL_GOOGLE + "?")
         && who.getEmailAddress() != null) {
       final List<AccountExternalId> openId = new ArrayList<AccountExternalId>();
       final List<AccountExternalId> v1 = new ArrayList<AccountExternalId>();
 
-      for (final AccountExternalId extId : db.accountExternalIds()
-          .byEmailAddress(who.getEmailAddress())) {
+      for (AccountExternalId extId : db.accountExternalIds().byEmailAddress(
+          who.getEmailAddress())) {
         if (extId.isScheme(OpenIdUrls.URL_GOOGLE + "?")) {
           openId.add(extId);
         } else if (extId.isScheme(AccountExternalId.LEGACY_GAE)) {
@@ -236,9 +245,13 @@
           final AccountExternalId oldId = openId.get(0);
           db.accountExternalIds().upsert(Collections.singleton(newId));
           db.accountExternalIds().delete(Collections.singleton(oldId));
+          evictions.add(accountCache.evictAsync(newId.getKey()));
+          evictions.add(accountCache.evictAsync(oldId.getKey()));
         } else {
           db.accountExternalIds().insert(Collections.singleton(newId));
+          evictions.add(accountCache.evictAsync(newId.getKey()));
         }
+        FutureUtil.waitFor(evictions);
         return new AuthResult(accountId, newId.getKey(), false);
 
       } else if (v1.size() == 1) {
@@ -252,6 +265,9 @@
 
         db.accountExternalIds().upsert(Collections.singleton(newId));
         db.accountExternalIds().delete(Collections.singleton(oldId));
+        evictions.add(accountCache.evictAsync(newId.getKey()));
+        evictions.add(accountCache.evictAsync(oldId.getKey()));
+        FutureUtil.waitFor(evictions);
         return new AuthResult(newId.getAccountId(), newId.getKey(), false);
 
       } else if (v1.size() > 1) {
@@ -269,6 +285,7 @@
 
     db.accounts().insert(Collections.singleton(account));
     db.accountExternalIds().insert(Collections.singleton(extId));
+    evictions.add(accountCache.evictAsync(extId.getKey()));
 
     if (firstAccount.get() && firstAccount.compareAndSet(true, false)) {
       // This is the first user account on our site. Assume this user
@@ -300,7 +317,9 @@
       }
     }
 
-    byEmailCache.evict(account.getPreferredEmail());
+    evictions.add(accountCache.evictEmailAsync(account.getPreferredEmail()));
+    FutureUtil.waitFor(evictions);
+
     realm.onCreateAccount(who, account);
     return new AuthResult(newId, extId.getKey(), true);
   }
@@ -325,8 +344,9 @@
     try {
       final ReviewDb db = schema.open();
       try {
-        final AccountExternalId.Key key = id(who);
-        AccountExternalId extId = db.accountExternalIds().get(key);
+        List<Future<Void>> evictions = Lists.newArrayList();
+        AccountExternalId.Key key = id(who);
+        AccountExternalId extId = FutureUtil.get(accountCache.get(key));
         if (extId != null) {
           if (!extId.getAccountId().equals(to)) {
             throw new AccountException("Identity in use by another account");
@@ -337,6 +357,7 @@
           extId = createId(to, who);
           extId.setEmailAddress(who.getEmailAddress());
           db.accountExternalIds().insert(Collections.singleton(extId));
+          evictions.add(accountCache.evictAsync(extId.getKey()));
 
           if (who.getEmailAddress() != null) {
             final Account a = db.accounts().get(to);
@@ -347,16 +368,19 @@
           }
 
           if (who.getEmailAddress() != null) {
-            byEmailCache.evict(who.getEmailAddress());
-            byIdCache.evict(to);
+            evictions.add(accountCache.evictEmailAsync(who.getEmailAddress()));
+            evictions.add(accountCache.evictAsync(to));
           }
         }
 
+        FutureUtil.waitFor(evictions);
         return new AuthResult(to, key, false);
 
       } finally {
         db.close();
       }
+    } catch (FutureException e) {
+      throw new AccountException("Cannot link identity", e);
     } catch (OrmException e) {
       throw new AccountException("Cannot link identity", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountProjectWatchCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountProjectWatchCache.java
new file mode 100644
index 0000000..3e8a755
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountProjectWatchCache.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2010 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.account;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountProjectWatch;
+import com.google.gerrit.reviewdb.Project;
+
+import java.util.List;
+
+public interface AccountProjectWatchCache {
+  public ListenableFuture<List<AccountProjectWatch>> byAccount(Account.Id id);
+
+  public ListenableFuture<List<AccountProjectWatch>> byProject(Project.NameKey name);
+
+  public ListenableFuture<Void> evictAsync(AccountProjectWatch.Key key);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountProjectWatchCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountProjectWatchCacheImpl.java
new file mode 100644
index 0000000..c683031
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountProjectWatchCacheImpl.java
@@ -0,0 +1,154 @@
+// Copyright (C) 2010 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.account;
+
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountProjectWatch;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.cache.Cache;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.EntryCreator;
+import com.google.gerrit.server.util.CompoundFuture;
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+
+import java.util.List;
+
+@Singleton
+public class AccountProjectWatchCacheImpl implements AccountProjectWatchCache {
+  private static final String BY_ACCOUNT_ID = "apw_account";
+  private static final String BY_PROJECT_NAME = "apw_project";
+
+  protected static class AccountProjectWatchList {
+    @Column(id = 1)
+    protected List<AccountProjectWatch> list;
+
+    protected AccountProjectWatchList() {
+    }
+
+    public AccountProjectWatchList(List<AccountProjectWatch> list) {
+      this.list = list;
+    }
+  }
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        final TypeLiteral<Cache<Account.Id, AccountProjectWatchList>> byAccountIdType =
+            new TypeLiteral<Cache<Account.Id, AccountProjectWatchList>>() {};
+        cache(byAccountIdType, BY_ACCOUNT_ID).populateWith(
+            ByAccountIdLoader.class);
+
+        final TypeLiteral<Cache<Project.NameKey, AccountProjectWatchList>> byProjectNameType =
+            new TypeLiteral<Cache<Project.NameKey, AccountProjectWatchList>>() {};
+        cache(byProjectNameType, BY_PROJECT_NAME).populateWith(
+            ByProjectNameLoader.class);
+
+        bind(AccountProjectWatchCacheImpl.class);
+        bind(AccountProjectWatchCache.class).to(
+            AccountProjectWatchCacheImpl.class);
+      }
+    };
+  }
+
+  private static final Function<AccountProjectWatchList, List<AccountProjectWatch>> unpack =
+      new Function<AccountProjectWatchList, List<AccountProjectWatch>>() {
+        @Override
+        public List<AccountProjectWatch> apply(AccountProjectWatchList in) {
+          return in.list;
+        }
+      };
+
+  private final Cache<Account.Id, AccountProjectWatchList> byAccountId;
+  private final Cache<Project.NameKey, AccountProjectWatchList> byProjectName;
+
+  @Inject
+  AccountProjectWatchCacheImpl(
+      @Named(BY_ACCOUNT_ID) Cache<Account.Id, AccountProjectWatchList> byAccountId,
+      @Named(BY_PROJECT_NAME) Cache<Project.NameKey, AccountProjectWatchList> byProjectName) {
+    this.byAccountId = byAccountId;
+    this.byProjectName = byProjectName;
+  }
+
+  @Override
+  public ListenableFuture<List<AccountProjectWatch>> byAccount(Account.Id id) {
+    return Futures.compose(byAccountId.get(id), unpack);
+  }
+
+  @Override
+  public ListenableFuture<List<AccountProjectWatch>> byProject(
+      Project.NameKey name) {
+    return Futures.compose(byProjectName.get(name), unpack);
+  }
+
+  @Override
+  public ListenableFuture<Void> evictAsync(AccountProjectWatch.Key key) {
+    return CompoundFuture.wrap(byAccountId.removeAsync(key.getParentKey()),
+        byProjectName.removeAsync(key.getProjectName()));
+  }
+
+  static class ByAccountIdLoader extends
+      EntryCreator<Account.Id, AccountProjectWatchList> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    ByAccountIdLoader(SchemaFactory<ReviewDb> schema) {
+      this.schema = schema;
+    }
+
+    @Override
+    public AccountProjectWatchList createEntry(Account.Id id) throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        return new AccountProjectWatchList(db.accountProjectWatches()
+            .byAccount(id).toList());
+      } finally {
+        db.close();
+      }
+    }
+  }
+
+  static class ByProjectNameLoader extends
+      EntryCreator<Project.NameKey, AccountProjectWatchList> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    ByProjectNameLoader(SchemaFactory<ReviewDb> schema) {
+      this.schema = schema;
+    }
+
+    @Override
+    public AccountProjectWatchList createEntry(Project.NameKey name)
+        throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        return new AccountProjectWatchList(db.accountProjectWatches()
+            .byProject(name).toList());
+      } finally {
+        db.close();
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
index 3dce94e..d5dea63 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -30,16 +31,14 @@
 
 public class AccountResolver {
   private final Realm realm;
-  private final AccountByEmailCache byEmail;
-  private final AccountCache byId;
+  private final AccountCache accountCache;
   private final Provider<ReviewDb> schema;
 
   @Inject
-  AccountResolver(final Realm realm, final AccountByEmailCache byEmail,
-      final AccountCache byId, final Provider<ReviewDb> schema) {
+  AccountResolver(final Realm realm, final AccountCache accountCache,
+      final Provider<ReviewDb> schema) {
     this.realm = realm;
-    this.byEmail = byEmail;
-    this.byId = byId;
+    this.accountCache = accountCache;
     this.schema = schema;
   }
 
@@ -55,7 +54,10 @@
    */
   public Account find(final String nameOrEmail) throws OrmException {
     Set<Account.Id> r = findAll(nameOrEmail);
-    return r.size() == 1 ? byId.get(r.iterator().next()).getAccount() : null;
+    if (r.size() == 1) {
+      return FutureUtil.get(accountCache.getAccount(r.iterator().next()));
+    }
+    return null;
   }
 
   /**
@@ -65,10 +67,11 @@
    *        "Full Name &lt;email@example&gt;", just the email address
    *        ("email@example"), a full name ("Full Name"), an account id
    *        ("18419") or an user name ("username").
-   * @return the accounts that match, empty collection if none.  Never null.
+   * @return the accounts that match, empty collection if none. Never null.
    */
   public Set<Account.Id> findAll(String nameOrEmail) throws OrmException {
-    Matcher m = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$").matcher(nameOrEmail);
+    Matcher m =
+        Pattern.compile("^.* \\(([1-9][0-9]*)\\)$").matcher(nameOrEmail);
     if (m.matches()) {
       return Collections.singleton(Account.Id.parse(m.group(1)));
     }
@@ -78,9 +81,10 @@
     }
 
     if (nameOrEmail.matches(Account.USER_NAME_PATTERN)) {
-      AccountState who = byId.getByUsername(nameOrEmail);
+      AccountExternalId.Key key = AccountExternalId.forUsername(nameOrEmail);
+      AccountExternalId who = FutureUtil.get(accountCache.get(key));
       if (who != null) {
-        return Collections.singleton(who.getAccount().getId());
+        return Collections.singleton(who.getAccountId());
       }
     }
 
@@ -99,7 +103,10 @@
   public Account findByNameOrEmail(final String nameOrEmail)
       throws OrmException {
     Set<Account.Id> r = findAllByNameOrEmail(nameOrEmail);
-    return r.size() == 1 ? byId.get(r.iterator().next()).getAccount() : null;
+    if (r.size() == 1) {
+      return FutureUtil.get(accountCache.getAccount(r.iterator().next()));
+    }
+    return null;
   }
 
   /**
@@ -115,11 +122,12 @@
     final int lt = nameOrEmail.indexOf('<');
     final int gt = nameOrEmail.indexOf('>');
     if (lt >= 0 && gt > lt && nameOrEmail.contains("@")) {
-      return byEmail.get(nameOrEmail.substring(lt + 1, gt));
+      String email = nameOrEmail.substring(lt + 1, gt);
+      return FutureUtil.get(accountCache.byEmail(email));
     }
 
     if (nameOrEmail.contains("@")) {
-      return byEmail.get(nameOrEmail);
+      return FutureUtil.get(accountCache.byEmail(nameOrEmail));
     }
 
     final Account.Id id = realm.lookup(nameOrEmail);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
index 9393227..667d220 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
@@ -16,18 +16,39 @@
 
 import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
 
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gwtorm.client.Column;
 
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 public class AccountState {
-  private final Account account;
-  private final Set<AccountGroup.Id> internalGroups;
-  private final Collection<AccountExternalId> externalIds;
+  /** Convert from an AccountState to an Account. */
+  public static final Function<AccountState, Account> GET_ACCOUNT =
+      new Function<AccountState, Account>() {
+        @Override
+        public Account apply(AccountState in) {
+          return in.getAccount();
+        }
+      };
+
+  @Column(id = 1)
+  protected Account account;
+
+  @Column(id = 2)
+  protected Set<AccountGroup.Id> internalGroups;
+
+  @Column(id = 3)
+  protected Collection<AccountExternalId> externalIds;
+
+  protected AccountState() {
+  }
 
   public AccountState(final Account account,
       final Set<AccountGroup.Id> actualGroups,
@@ -53,17 +74,6 @@
     return account.getUserName();
   }
 
-  /** @return the password matching the requested username; or null. */
-  public String getPassword(String username) {
-    for (AccountExternalId id : getExternalIds()) {
-      if (id.isScheme(AccountExternalId.SCHEME_USERNAME)
-          && username.equals(id.getSchemeRest())) {
-        return id.getPassword();
-      }
-    }
-    return null;
-  }
-
   /**
    * All email addresses registered to this account.
    * <p>
@@ -88,6 +98,18 @@
     return externalIds;
   }
 
+  /** The external identities that match a particular email address. */
+  public Collection<AccountExternalId> getExternalIds(String emailAddress) {
+    List<AccountExternalId> r = Lists.newArrayListWithCapacity(externalIds.size());
+    for (AccountExternalId extId : externalIds) {
+      String accEmail = extId.getEmailAddress();
+      if (accEmail != null && accEmail.equals(emailAddress)) {
+        r.add(extId);
+      }
+    }
+    return r;
+  }
+
   /** The set of groups maintained directly within the Gerrit database. */
   public Set<AccountGroup.Id> getInternalGroups() {
     return internalGroups;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
index e875a19..ecdb4b9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
 
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.errors.InvalidUserNameException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.reviewdb.Account;
@@ -23,6 +24,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtjsonrpc.client.VoidResult;
 import com.google.gwtorm.client.OrmDuplicateKeyException;
 import com.google.gwtorm.client.OrmException;
@@ -30,10 +32,10 @@
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
-import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
 import java.util.regex.Pattern;
 
 import javax.annotation.Nullable;
@@ -72,7 +74,7 @@
 
   private final ReviewDb db;
   private final IdentifiedUser user;
-  private final String newUsername;
+  private final String newName;
 
   @Inject
   ChangeUserName(final AccountCache accountCache,
@@ -85,70 +87,41 @@
 
     this.db = db;
     this.user = user;
-    this.newUsername = newUsername;
+    this.newName = newUsername;
   }
 
   public VoidResult call() throws OrmException, NameAlreadyUsedException,
       InvalidUserNameException {
-    final Collection<AccountExternalId> old = old();
-    if (!old.isEmpty()) {
+    if (hasUsername()) {
       throw new IllegalStateException("Username cannot be changed.");
     }
-
-    if (newUsername != null && !newUsername.isEmpty()) {
-      if (!USER_NAME_PATTERN.matcher(newUsername).matches()) {
-        throw new InvalidUserNameException();
-      }
-
-      final AccountExternalId.Key key =
-          new AccountExternalId.Key(SCHEME_USERNAME, newUsername);
-      try {
-        final AccountExternalId id =
-            new AccountExternalId(user.getAccountId(), key);
-
-        for (AccountExternalId i : old) {
-          if (i.getPassword() != null) {
-            id.setPassword(i.getPassword());
-          }
-        }
-
-        db.accountExternalIds().insert(Collections.singleton(id));
-      } catch (OrmDuplicateKeyException dupeErr) {
-        // If we are using this identity, don't report the exception.
-        //
-        AccountExternalId other = db.accountExternalIds().get(key);
-        if (other != null && other.getAccountId().equals(user.getAccountId())) {
-          return VoidResult.INSTANCE;
-        }
-
-        // Otherwise, someone else has this identity.
-        //
-        throw new NameAlreadyUsedException();
-      }
+    if (newName == null || !USER_NAME_PATTERN.matcher(newName).matches()) {
+      throw new InvalidUserNameException();
     }
 
-    // If we have any older user names, remove them.
-    //
-    db.accountExternalIds().delete(old);
-    for (AccountExternalId i : old) {
-      sshKeyCache.evict(i.getSchemeRest());
-      accountCache.evictByUsername(i.getSchemeRest());
+    AccountExternalId.Key key = AccountExternalId.forUsername(newName);
+    List<Future<Void>> evictions = Lists.newArrayList();
+    try {
+      AccountExternalId id = new AccountExternalId(user.getAccountId(), key);
+      db.accountExternalIds().insert(Collections.singleton(id));
+      evictions.add(accountCache.evictAsync(key));
+    } catch (OrmDuplicateKeyException dupeErr) {
+      throw new NameAlreadyUsedException();
     }
 
-    accountCache.evict(user.getAccountId());
-    accountCache.evictByUsername(newUsername);
-    sshKeyCache.evict(newUsername);
+    evictions.add(accountCache.evictAsync(user.getAccountId()));
+    evictions.add(accountCache.evictAsync(key));
+    evictions.add(sshKeyCache.evictAsync(newName));
+    FutureUtil.waitFor(evictions);
     return VoidResult.INSTANCE;
   }
 
-  private Collection<AccountExternalId> old() throws OrmException {
-    final Collection<AccountExternalId> r = new ArrayList<AccountExternalId>(1);
-    for (AccountExternalId i : db.accountExternalIds().byAccount(
-        user.getAccountId())) {
+  private boolean hasUsername() {
+    for (AccountExternalId i : user.getAccountState().getExternalIds()) {
       if (i.isScheme(SCHEME_USERNAME)) {
-        r.add(i);
+        return true;
       }
     }
-    return r;
+    return false;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java
index 1fc87fe..ae12851 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java
@@ -14,16 +14,20 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
 
 /** Operation to clear a password for an account. */
 public class ClearPassword implements Callable<AccountExternalId> {
@@ -57,7 +61,11 @@
 
     id.setPassword(null);
     db.accountExternalIds().update(Collections.singleton(id));
-    accountCache.evict(user.getAccountId());
+
+    List<Future<Void>> evictions = Lists.newArrayList();
+    evictions.add(accountCache.evictAsync(id.getKey()));
+    evictions.add(accountCache.evictAsync(user.getAccountId()));
+    FutureUtil.waitFor(evictions);
     return id;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index a836f54..5bd0fdd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.inject.Inject;
 
 import java.util.Collections;
@@ -23,13 +24,13 @@
 
 public final class DefaultRealm implements Realm {
   private final EmailExpander emailExpander;
-  private final AccountByEmailCache byEmail;
+  private final AccountCache accountCache;
 
   @Inject
   DefaultRealm(final EmailExpander emailExpander,
-      final AccountByEmailCache byEmail) {
+      final AccountCache accountCache) {
     this.emailExpander = emailExpander;
-    this.byEmail = byEmail;
+    this.accountCache = accountCache;
   }
 
   @Override
@@ -58,7 +59,8 @@
   @Override
   public Account.Id lookup(final String accountName) {
     if (emailExpander.canExpand(accountName)) {
-      final Set<Account.Id> c = byEmail.get(emailExpander.expand(accountName));
+      Set<Account.Id> c = FutureUtil.getOrEmptySet( //
+          accountCache.byEmail(emailExpander.expand(accountName)));
       if (1 == c.size()) {
         return c.iterator().next();
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java
index 1b30503..6964929 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java
@@ -14,10 +14,13 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -27,6 +30,7 @@
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Callable;
 
 /** Operation to generate a password for an account. */
@@ -50,29 +54,32 @@
   private final ReviewDb db;
   private final IdentifiedUser user;
 
-  private final AccountExternalId.Key forUser;
+  private final AccountExternalId.Key idKey;
 
   @Inject
   GeneratePassword(final AccountCache accountCache, final ReviewDb db,
       final IdentifiedUser user,
 
-      @Assisted AccountExternalId.Key forUser) {
+      @Assisted AccountExternalId.Key idKey) {
     this.accountCache = accountCache;
     this.db = db;
     this.user = user;
 
-    this.forUser = forUser;
+    this.idKey = idKey;
   }
 
   public AccountExternalId call() throws OrmException, NoSuchEntityException {
-    AccountExternalId id = db.accountExternalIds().get(forUser);
+    AccountExternalId id = db.accountExternalIds().get(idKey);
     if (id == null || !user.getAccountId().equals(id.getAccountId())) {
       throw new NoSuchEntityException();
     }
 
+    List<ListenableFuture<Void>> evictions = Lists.newArrayList();
     id.setPassword(generate());
     db.accountExternalIds().update(Collections.singleton(id));
-    accountCache.evict(user.getAccountId());
+    evictions.add(accountCache.evictAsync(user.getAccountId()));
+    evictions.add(accountCache.evictAsync(idKey));
+    FutureUtil.waitFor(evictions);
     return id;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
index 978d9c2..daf0312 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
@@ -14,19 +14,20 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.reviewdb.AccountGroup;
 
-import java.util.Collection;
-
 /** Tracks group objects in memory for efficient access. */
 public interface GroupCache {
-  public AccountGroup get(AccountGroup.Id groupId);
+  public ListenableFuture<AccountGroup> get(AccountGroup.Id groupId);
 
-  public AccountGroup get(AccountGroup.NameKey name);
+  public ListenableFuture<AccountGroup> get(AccountGroup.NameKey name);
 
-  public Collection<AccountGroup> get(AccountGroup.ExternalNameKey externalName);
+  public ListenableFuture<AccountGroupCollection> get(
+      AccountGroup.ExternalNameKey externalName);
 
-  public void evict(AccountGroup group);
+  public ListenableFuture<Void> evictAsync(AccountGroup group);
 
-  public void evictAfterRename(AccountGroup.NameKey oldName);
+  public ListenableFuture<Void> evictAfterRenameAsync(
+      AccountGroup.NameKey oldName);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
index d948aef..b3a858a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -14,13 +14,16 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.AccountGroupName;
 import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.AccountGroup.ExternalNameKey;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.util.CompoundFuture;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -28,7 +31,7 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
-import java.util.Collection;
+import java.util.Collections;
 
 /** Tracks group objects in memory for efficient access. */
 @Singleton
@@ -43,15 +46,15 @@
       protected void configure() {
         final TypeLiteral<Cache<AccountGroup.Id, AccountGroup>> byId =
             new TypeLiteral<Cache<AccountGroup.Id, AccountGroup>>() {};
-        core(byId, BYID_NAME).populateWith(ByIdLoader.class);
+        cache(byId, BYID_NAME).populateWith(ByIdLoader.class);
 
         final TypeLiteral<Cache<AccountGroup.NameKey, AccountGroup>> byName =
             new TypeLiteral<Cache<AccountGroup.NameKey, AccountGroup>>() {};
-        core(byName, BYNAME_NAME).populateWith(ByNameLoader.class);
+        cache(byName, BYNAME_NAME).populateWith(ByNameLoader.class);
 
-        final TypeLiteral<Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>> byExternalName =
-            new TypeLiteral<Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>>() {};
-        core(byExternalName, BYEXT_NAME) //
+        final TypeLiteral<Cache<AccountGroup.ExternalNameKey, AccountGroupCollection>> byExternalName =
+            new TypeLiteral<Cache<AccountGroup.ExternalNameKey, AccountGroupCollection>>() {};
+        cache(byExternalName, BYEXT_NAME) //
             .populateWith(ByExternalNameLoader.class);
 
         bind(GroupCacheImpl.class);
@@ -62,38 +65,38 @@
 
   private final Cache<AccountGroup.Id, AccountGroup> byId;
   private final Cache<AccountGroup.NameKey, AccountGroup> byName;
-  private final Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName;
+  private final Cache<AccountGroup.ExternalNameKey, AccountGroupCollection> byExternalName;
 
   @Inject
   GroupCacheImpl(
       @Named(BYID_NAME) Cache<AccountGroup.Id, AccountGroup> byId,
       @Named(BYNAME_NAME) Cache<AccountGroup.NameKey, AccountGroup> byName,
-      @Named(BYEXT_NAME) Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName) {
+      @Named(BYEXT_NAME) Cache<AccountGroup.ExternalNameKey, AccountGroupCollection> byExternalName) {
     this.byId = byId;
     this.byName = byName;
     this.byExternalName = byExternalName;
   }
 
-  public AccountGroup get(final AccountGroup.Id groupId) {
+  public ListenableFuture<AccountGroup> get(final AccountGroup.Id groupId) {
     return byId.get(groupId);
   }
 
-  public void evict(final AccountGroup group) {
-    byId.remove(group.getId());
-    byName.remove(group.getNameKey());
-    byExternalName.remove(group.getExternalNameKey());
+  public ListenableFuture<Void> evictAsync(AccountGroup group) {
+    return CompoundFuture.wrap(byId.removeAsync(group.getId()), //
+        byName.removeAsync(group.getNameKey()),//
+        byExternalName.removeAsync(group.getExternalNameKey()));
   }
 
-  public void evictAfterRename(final AccountGroup.NameKey oldName) {
-    byName.remove(oldName);
+  public ListenableFuture<Void> evictAfterRenameAsync(AccountGroup.NameKey name) {
+    return byName.removeAsync(name);
   }
 
-  public AccountGroup get(final AccountGroup.NameKey name) {
+  public ListenableFuture<AccountGroup> get(AccountGroup.NameKey name) {
     return byName.get(name);
   }
 
-  public Collection<AccountGroup> get(
-      final AccountGroup.ExternalNameKey externalName) {
+  public ListenableFuture<AccountGroupCollection> get(
+      AccountGroup.ExternalNameKey externalName) {
     return byExternalName.get(externalName);
   }
 
@@ -161,7 +164,7 @@
   }
 
   static class ByExternalNameLoader extends
-      EntryCreator<AccountGroup.ExternalNameKey, Collection<AccountGroup>> {
+      EntryCreator<AccountGroup.ExternalNameKey, AccountGroupCollection> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -170,14 +173,20 @@
     }
 
     @Override
-    public Collection<AccountGroup> createEntry(
+    public AccountGroupCollection createEntry(
         final AccountGroup.ExternalNameKey key) throws Exception {
       final ReviewDb db = schema.open();
       try {
-        return db.accountGroups().byExternalName(key).toList();
+        return new AccountGroupCollection(db.accountGroups()
+            .byExternalName(key).toList());
       } finally {
         db.close();
       }
     }
+
+    @Override
+    public AccountGroupCollection missing(ExternalNameKey key) {
+      return new AccountGroupCollection(Collections.<AccountGroup> emptyList());
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index 40360b9..4b1ad2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -34,7 +35,7 @@
 
     public GroupControl controlFor(final AccountGroup.Id groupId)
         throws NoSuchGroupException {
-      final AccountGroup group = groupCache.get(groupId);
+      AccountGroup group = FutureUtil.getOrNull(groupCache.get(groupId));
       if (group == null) {
         throw new NoSuchGroupException(groupId);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
index 6f6a4d4..b95e39d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gerrit.util.ssl.BlindSSLSocketFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -41,7 +42,8 @@
 import javax.naming.directory.InitialDirContext;
 import javax.net.ssl.SSLSocketFactory;
 
-@Singleton class Helper {
+@Singleton
+class Helper {
   private final GroupCache groupCache;
   private final Config config;
   private final String server;
@@ -132,8 +134,8 @@
   }
 
   Set<AccountGroup.Id> queryForGroups(final DirContext ctx,
-      final String username, LdapQuery.Result account)
-      throws NamingException, AccountException {
+      final String username, LdapQuery.Result account) throws NamingException,
+      AccountException {
     final LdapSchema schema = getSchema(ctx);
     final Set<String> groupDNs = new HashSet<String>();
 
@@ -175,8 +177,8 @@
 
     final Set<AccountGroup.Id> actual = new HashSet<AccountGroup.Id>();
     for (String dn : groupDNs) {
-      for (AccountGroup group : groupCache
-          .get(new AccountGroup.ExternalNameKey(dn))) {
+      for (AccountGroup group : FutureUtil.get(
+          groupCache.get(new AccountGroup.ExternalNameKey(dn))).getGroups()) {
         if (group.getType() == AccountGroup.Type.LDAP) {
           actual.add(group.getId());
         }
@@ -238,9 +240,11 @@
 
       groupBases = LdapRealm.optionalList(config, "groupBase");
       groupScope = LdapRealm.scope(config, "groupScope");
-      groupPattern = LdapRealm.paramString(config, "groupPattern", type.groupPattern());
+      groupPattern =
+          LdapRealm.paramString(config, "groupPattern", type.groupPattern());
       final String groupMemberPattern =
-          LdapRealm.optdef(config, "groupMemberPattern", type.groupMemberPattern());
+          LdapRealm.optdef(config, "groupMemberPattern", type
+              .groupMemberPattern());
 
       for (String groupBase : groupBases) {
         if (groupMemberPattern != null) {
@@ -266,7 +270,8 @@
       // Account query
       //
       accountFullName =
-          LdapRealm.paramString(config, "accountFullName", type.accountFullName());
+          LdapRealm.paramString(config, "accountFullName", type
+              .accountFullName());
       if (accountFullName != null) {
         accountAtts.addAll(accountFullName.getParameterNames());
       }
@@ -277,12 +282,14 @@
         accountAtts.addAll(accountEmailAddress.getParameterNames());
       }
       accountSshUserName =
-          LdapRealm.paramString(config, "accountSshUserName", type.accountSshUserName());
+          LdapRealm.paramString(config, "accountSshUserName", type
+              .accountSshUserName());
       if (accountSshUserName != null) {
         accountAtts.addAll(accountSshUserName.getParameterNames());
       }
       accountMemberField =
-          LdapRealm.optdef(config, "accountMemberField", type.accountMemberField());
+          LdapRealm.optdef(config, "accountMemberField", type
+              .accountMemberField());
       if (accountMemberField != null) {
         accountAtts.add(accountMemberField);
       }
@@ -313,4 +320,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
index 810df28..d16b635 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
@@ -34,12 +34,12 @@
   protected void configure() {
     final TypeLiteral<Cache<String, Set<AccountGroup.Id>>> groups =
         new TypeLiteral<Cache<String, Set<AccountGroup.Id>>>() {};
-    core(groups, GROUP_CACHE).maxAge(1, HOURS) //
+    cache(groups, GROUP_CACHE).maxAge(1, HOURS) //
         .populateWith(LdapRealm.MemberLoader.class);
 
     final TypeLiteral<Cache<String, Account.Id>> usernames =
         new TypeLiteral<Cache<String, Account.Id>>() {};
-    core(usernames, USERNAME_CACHE) //
+    cache(usernames, USERNAME_CACHE) //
         .populateWith(LdapRealm.UserLoader.class);
 
     bind(Realm.class).to(LdapRealm.class).in(Scopes.SINGLETON);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index de33b44..80bfb60 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.AuthType;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AuthRequest;
@@ -33,8 +33,7 @@
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
@@ -110,14 +109,12 @@
     return v;
   }
 
-  static List<String> optionalList(final Config config,
-      final String name) {
+  static List<String> optionalList(final Config config, final String name) {
     String s[] = config.getStringList("ldap", null, name);
     return Arrays.asList(s);
   }
 
-  static List<String> requiredList(final Config config,
-      final String name) {
+  static List<String> requiredList(final Config config, final String name) {
     List<String> vlist = optionalList(config, name);
 
     if (vlist.isEmpty()) {
@@ -220,7 +217,8 @@
         // in the middle of authenticating the user, its likely we will
         // need to know what access rights they have soon.
         //
-        membershipCache.put(username, helper.queryForGroups(ctx, username, m));
+        FutureUtil.waitFor(membershipCache.putAsync(username, //
+            helper.queryForGroups(ctx, username, m)));
         return who;
       } finally {
         try {
@@ -237,13 +235,14 @@
 
   @Override
   public void onCreateAccount(final AuthRequest who, final Account account) {
-    usernameCache.put(who.getLocalUser(), account.getId());
+    String name = who.getLocalUser();
+    FutureUtil.waitFor(usernameCache.putAsync(name, account.getId()));
   }
 
   @Override
   public Set<AccountGroup.Id> groups(final AccountState who) {
     final HashSet<AccountGroup.Id> r = new HashSet<AccountGroup.Id>();
-    r.addAll(membershipCache.get(findId(who.getExternalIds())));
+    r.addAll(FutureUtil.get(membershipCache.get(findId(who.getExternalIds()))));
     r.addAll(who.getInternalGroups());
     return r;
   }
@@ -260,7 +259,7 @@
 
   @Override
   public Account.Id lookup(final String accountName) {
-    return usernameCache.get(accountName);
+    return FutureUtil.get(usernameCache.get(accountName));
   }
 
   @Override
@@ -298,29 +297,18 @@
   }
 
   static class UserLoader extends EntryCreator<String, Account.Id> {
-    private final SchemaFactory<ReviewDb> schema;
+    private final AccountCache accountCache;
 
     @Inject
-    UserLoader(SchemaFactory<ReviewDb> schema) {
-      this.schema = schema;
+    UserLoader(AccountCache accountCache) {
+      this.accountCache = accountCache;
     }
 
     @Override
     public Account.Id createEntry(final String username) throws Exception {
-      try {
-        final ReviewDb db = schema.open();
-        try {
-          final AccountExternalId extId =
-              db.accountExternalIds().get(
-                  new AccountExternalId.Key(SCHEME_GERRIT, username));
-          return extId != null ? extId.getAccountId() : null;
-        } finally {
-          db.close();
-        }
-      } catch (OrmException e) {
-        log.warn("Cannot query for username in database", e);
-        return null;
-      }
+      AccountExternalId extId = FutureUtil.get( //
+          accountCache.get(new AccountExternalId.Key(SCHEME_GERRIT, username)));
+      return extId != null ? extId.getAccountId() : null;
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java
index 7159501..f91d3c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java
@@ -14,30 +14,36 @@
 
 package com.google.gerrit.server.cache;
 
+import com.google.common.util.concurrent.ListenableFuture;
+
 import java.util.concurrent.TimeUnit;
 
-
 /**
- * A fast in-memory and/or on-disk based cache.
+ * An in-memory and/or on-disk based cache.
+ *
+ * The cache may need to perform operations over the network, in which case the
+ * returned Future can be used to wait for operation completion. Opening
+ * multiple concurrent Futures on such a cache may permit the cache to batch the
+ * operations together (but that is implementation specific).
  *
  * @type <K> type of key used to lookup entries in the cache.
  * @type <V> type of value stored within each cache entry.
  */
 public interface Cache<K, V> {
   /** Get the element from the cache, or null if not stored in the cache. */
-  public V get(K key);
+  public ListenableFuture<V> get(K key);
 
   /** Put one element into the cache, replacing any existing value. */
-  public void put(K key, V value);
+  public ListenableFuture<Void> putAsync(K key, V value);
 
   /** Remove any existing value from the cache, no-op if not present. */
-  public void remove(K key);
+  public ListenableFuture<Void> removeAsync(K key);
 
   /** Remove all cached items. */
-  public void removeAll();
+  public ListenableFuture<Void> removeAllAsync();
 
   /**
-   * Get the time an element will survive in the cache.
+   * Get the configured time an element will survive in the cache.
    *
    * @param unit desired units of the return value.
    * @return time an item can live before being purged.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
index 7fb3b3b..c185f7c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
@@ -22,8 +22,6 @@
 import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.name.Names;
 
-import java.io.Serializable;
-
 /**
  * Miniature DSL to support binding {@link Cache} instances in Guice.
  */
@@ -38,9 +36,9 @@
    * @return binding to describe the cache. Caller must set at least the name on
    *         the returned binding.
    */
-  protected <K, V> UnnamedCacheBinding<K, V> core(
+  protected <K, V> UnnamedCacheBinding<K, V> cache(
       final TypeLiteral<Cache<K, V>> type) {
-    return core(Key.get(type));
+    return cache(Key.get(type), type);
   }
 
   /**
@@ -53,52 +51,15 @@
    *        and with {@code @Named} annotations.
    * @return binding to describe the cache.
    */
-  protected <K, V> NamedCacheBinding<K, V> core(
+  protected <K, V> NamedCacheBinding<K, V> cache(
       final TypeLiteral<Cache<K, V>> type, final String name) {
-    return core(Key.get(type, Names.named(name))).name(name);
+    return cache(Key.get(type, Names.named(name)), type).name(name);
   }
 
-  private <K, V> UnnamedCacheBinding<K, V> core(final Key<Cache<K, V>> key) {
-    final boolean disk = false;
-    final CacheProvider<K, V> b = new CacheProvider<K, V>(disk, this);
-    bind(key).toProvider(b).in(Scopes.SINGLETON);
-    return b;
-  }
-
-  /**
-   * Declare an unnamed in-memory/on-disk cache.
-   *
-   * @param <K> type of key used to find entries, must be {@link Serializable}.
-   * @param <V> type of value stored by the cache, must be {@link Serializable}.
-   * @param type type literal for the cache, this literal will be used to match
-   *        injection sites. Injection sites are matched by this type literal
-   *        and with {@code @Named} annotations.
-   * @return binding to describe the cache. Caller must set at least the name on
-   *         the returned binding.
-   */
-  protected <K extends Serializable, V extends Serializable> UnnamedCacheBinding<K, V> disk(
+  private <K, V> UnnamedCacheBinding<K, V> cache(final Key<Cache<K, V>> key,
       final TypeLiteral<Cache<K, V>> type) {
-    return disk(Key.get(type));
-  }
-
-  /**
-   * Declare a named in-memory/on-disk cache.
-   *
-   * @param <K> type of key used to find entries, must be {@link Serializable}.
-   * @param <V> type of value stored by the cache, must be {@link Serializable}.
-   * @param type type literal for the cache, this literal will be used to match
-   *        injection sites. Injection sites are matched by this type literal
-   *        and with {@code @Named} annotations.
-   * @return binding to describe the cache.
-   */
-  protected <K extends Serializable, V extends Serializable> NamedCacheBinding<K, V> disk(
-      final TypeLiteral<Cache<K, V>> type, final String name) {
-    return disk(Key.get(type, Names.named(name))).name(name);
-  }
-
-  private <K, V> UnnamedCacheBinding<K, V> disk(final Key<Cache<K, V>> key) {
-    final boolean disk = true;
-    final CacheProvider<K, V> b = new CacheProvider<K, V>(disk, this);
+    final boolean disk = false;
+    final CacheProvider<K, V> b = new CacheProvider<K, V>(this, type);
     bind(key).toProvider(b).in(Scopes.SINGLETON);
     return b;
   }
@@ -110,6 +71,12 @@
     return getProvider(key);
   }
 
+  <V> Provider<V> getValueProvider(Class<V> type) {
+    Key<V> key = Key.get(type, UniqueAnnotations.create());
+    bind(key).to(type);
+    return getProvider(key);
+  }
+
   @SuppressWarnings("unchecked")
   private static <K, V> Key<EntryCreator<K, V>> newKey() {
     return (Key<EntryCreator<K, V>>) newKeyImpl();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
index 5230ff6..3370b08 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2010 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.
@@ -14,237 +14,6 @@
 
 package com.google.gerrit.server.cache;
 
-import static java.util.concurrent.TimeUnit.MINUTES;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import com.google.gerrit.lifecycle.LifecycleListener;
-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.ProvisionException;
-import com.google.inject.Singleton;
-
-import net.sf.ehcache.CacheManager;
-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 CachePool {
-  private static final Logger log = LoggerFactory.getLogger(CachePool.class);
-
-  public static class Lifecycle implements LifecycleListener {
-    private final CachePool cachePool;
-
-    @Inject
-    Lifecycle(final CachePool 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
-  CachePool(@GerritServerConfig final Config cfg, final SitePaths site) {
-    this.config = cfg;
-    this.site = site;
-    this.caches = new HashMap<String, CacheProvider<?, ?>>();
-  }
-
-  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()) {
-        p.bind(manager.getEhcache(p.getName()));
-      }
-    }
-  }
-
-  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;
-    }
-  }
-
-  <K, V> ProxyEhcache 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 ProxyEhcache(n);
-    }
-  }
-
-  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 void configureDefaultCache() {
-      final 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);
-      }
-
-      mgr.setDefaultCacheConfiguration(c);
-    }
-
-    private CacheConfiguration newCache(final String name) {
-      try {
-        final CacheConfiguration c;
-        c = mgr.getDefaultCacheConfiguration().clone();
-        c.setName(name);
-        return c;
-      } catch (CloneNotSupportedException e) {
-        throw new ProvisionException("Cannot configure cache " + name, e);
-      }
-    }
-  }
+public interface CachePool {
+  public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
index 0ba424f..72973e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
@@ -18,36 +18,67 @@
 import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.gwtorm.protobuf.CodecFactory;
+import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
+import com.google.inject.TypeLiteral;
 
-import net.sf.ehcache.Ehcache;
-
+import java.lang.reflect.Constructor;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
 import java.util.concurrent.TimeUnit;
 
-final class CacheProvider<K, V> implements Provider<Cache<K, V>>,
+public final class CacheProvider<K, V> implements Provider<Cache<K, V>>,
     NamedCacheBinding<K, V>, UnnamedCacheBinding<K, V> {
   private final CacheModule module;
-  private final boolean disk;
   private int memoryLimit;
   private int diskLimit;
   private long maxAge;
   private EvictionPolicy evictionPolicy;
   private String cacheName;
-  private ProxyEhcache cache;
+  private ProxyCache<K, V> cache;
   private Provider<EntryCreator<K, V>> entryCreator;
+  private Class<K> keyClass;
+  private Class<V> valueClass;
+  private Provider<V> valueProvider;
 
-  CacheProvider(final boolean disk, CacheModule module) {
-    this.disk = disk;
+  @SuppressWarnings("unchecked")
+  CacheProvider(CacheModule module, TypeLiteral<Cache<K, V>> typeLiteral) {
     this.module = module;
 
     memoryLimit(1024);
     maxAge(90, DAYS);
     evictionPolicy(LFU);
 
-    if (disk) {
-      diskLimit(16384);
+    Type[] tmp =
+        ((ParameterizedType) typeLiteral.getType()).getActualTypeArguments();
+
+    keyClass = (Class<K>) tmp[0];
+    valueClass = (Class<V>) tmp[1];
+
+    for (Constructor c : valueClass.getDeclaredConstructors()) {
+      if (c.getAnnotation(Inject.class) != null) {
+        valueProvider = module.getValueProvider(valueClass);
+      }
+    }
+
+    try {
+      ProtobufCodec<K> keyCodec = CodecFactory.encoder(keyClass);
+      keyCodec.decode(new byte[0]);
+    } catch (RuntimeException err) {
+      throw new IllegalStateException("Cannot support " + keyClass
+          + " in protobuf format", err);
+    }
+    if (valueProvider == null) {
+      try {
+        ProtobufCodec<V> valueCodec = CodecFactory.encoder(valueClass);
+        valueCodec.decode(new byte[0]);
+      } catch (RuntimeException err) {
+        throw new IllegalStateException("Cannot support " + valueClass
+            + " in protobuf format", err);
+      }
     }
   }
 
@@ -56,34 +87,53 @@
     this.cache = pool.register(this);
   }
 
-  void bind(final Ehcache ehcache) {
-    cache.bind(ehcache);
+  public void bind(Cache<K, V> impl) {
+    if (cache == null) {
+      throw new ProvisionException("Cache was never registered");
+    }
+    cache.bind(impl);
   }
 
-  String getName() {
+  public EntryCreator<K, V> getEntryCreator() {
+    return entryCreator != null ? entryCreator.get() : null;
+  }
+
+  public Provider<V> getValueProvider() {
+    return valueProvider;
+  }
+
+  public String getName() {
     if (cacheName == null) {
       throw new ProvisionException("Cache has no name");
     }
     return cacheName;
   }
 
-  boolean disk() {
-    return disk;
+  public boolean disk() {
+    return diskLimit() > 0;
   }
 
-  int memoryLimit() {
+  public int memoryLimit() {
     return memoryLimit;
   }
 
-  int diskLimit() {
+  public int diskLimit() {
     return diskLimit;
   }
 
-  long maxAge() {
+  public long maxAge() {
     return maxAge;
   }
 
-  EvictionPolicy evictionPolicy() {
+  public Class<K> getKeyClass() {
+    return keyClass;
+  }
+
+  public Class<V> getValueClass() {
+    return valueClass;
+  }
+
+  public EvictionPolicy evictionPolicy() {
     return evictionPolicy;
   }
 
@@ -101,13 +151,6 @@
   }
 
   public NamedCacheBinding<K, V> diskLimit(final int objects) {
-    if (!disk) {
-      // TODO This should really be a compile time type error, but I'm
-      // too lazy to create the mess of permutations required to setup
-      // type safe returns for bindings in our little DSL.
-      //
-      throw new IllegalStateException("Cache is not disk based");
-    }
     diskLimit = objects;
     return this;
   }
@@ -133,9 +176,6 @@
     if (cache == null) {
       throw new ProvisionException("Cache \"" + cacheName + "\" not available");
     }
-    if (entryCreator != null) {
-      return new PopulatingCache<K, V>(cache, entryCreator.get());
-    }
-    return new SimpleCache<K, V>(cache);
+    return cache;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyCache.java
new file mode 100644
index 0000000..1f18f3a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyCache.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2010 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;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.TimeUnit;
+
+/** Proxy around a cache which has not yet been created. */
+public final class ProxyCache<K, V> implements Cache<K, V> {
+  private volatile Cache<K, V> self;
+
+  public void bind(Cache<K, V> self) {
+    this.self = self;
+  }
+
+  @Override
+  public ListenableFuture<V> get(K key) {
+    return self.get(key);
+  }
+
+  @Override
+  public ListenableFuture<Void> putAsync(K key, V value) {
+    return self.putAsync(key, value);
+  }
+
+  @Override
+  public ListenableFuture<Void> removeAsync(K key) {
+    return self.removeAsync(key);
+  }
+
+  @Override
+  public ListenableFuture<Void> removeAllAsync() {
+    return self.removeAllAsync();
+  }
+
+  @Override
+  public long getTimeToLive(TimeUnit unit) {
+    return self.getTimeToLive(unit);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyEhcache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyEhcache.java
deleted file mode 100644
index 8b3179a..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyEhcache.java
+++ /dev/null
@@ -1,393 +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.server.cache;
-
-import net.sf.ehcache.CacheException;
-import net.sf.ehcache.CacheManager;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.Element;
-import net.sf.ehcache.Statistics;
-import net.sf.ehcache.Status;
-import net.sf.ehcache.bootstrap.BootstrapCacheLoader;
-import net.sf.ehcache.config.CacheConfiguration;
-import net.sf.ehcache.event.RegisteredEventListeners;
-import net.sf.ehcache.exceptionhandler.CacheExceptionHandler;
-import net.sf.ehcache.extension.CacheExtension;
-import net.sf.ehcache.loader.CacheLoader;
-import net.sf.ehcache.statistics.CacheUsageListener;
-import net.sf.ehcache.statistics.LiveCacheStatistics;
-import net.sf.ehcache.statistics.sampled.SampledCacheStatistics;
-
-import java.io.Serializable;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-
-/** Proxy around a cache which has not yet been created. */
-final class ProxyEhcache implements Ehcache {
-  private final String cacheName;
-  private volatile Ehcache self;
-
-  ProxyEhcache(final String cacheName) {
-    this.cacheName = cacheName;
-  }
-
-  void bind(final Ehcache self) {
-    this.self = self;
-  }
-
-  private Ehcache self() {
-    return self;
-  }
-
-  @Override
-  public Object clone() throws CloneNotSupportedException {
-    throw new CloneNotSupportedException();
-  }
-
-  @Override
-  public String getName() {
-    return cacheName;
-  }
-
-  @Override
-  public void setName(String name) {
-    throw new UnsupportedOperationException();
-  }
-
-  //
-  // Everything else delegates through self.
-  //
-
-  public void bootstrap() {
-    self().bootstrap();
-  }
-
-  public long calculateInMemorySize() throws IllegalStateException,
-      CacheException {
-    return self().calculateInMemorySize();
-  }
-
-  public void clearStatistics() {
-    self().clearStatistics();
-  }
-
-  public void dispose() throws IllegalStateException {
-    self().dispose();
-  }
-
-  public void evictExpiredElements() {
-    self().evictExpiredElements();
-  }
-
-  public void flush() throws IllegalStateException, CacheException {
-    self().flush();
-  }
-
-  public Element get(Object key) throws IllegalStateException, CacheException {
-    return self().get(key);
-  }
-
-  public Element get(Serializable key) throws IllegalStateException,
-      CacheException {
-    return self().get(key);
-  }
-
-  @SuppressWarnings("unchecked")
-  public Map getAllWithLoader(Collection keys, Object loaderArgument)
-      throws CacheException {
-    return self().getAllWithLoader(keys, loaderArgument);
-  }
-
-  public float getAverageGetTime() {
-    return self().getAverageGetTime();
-  }
-
-  public BootstrapCacheLoader getBootstrapCacheLoader() {
-    return self().getBootstrapCacheLoader();
-  }
-
-  public CacheConfiguration getCacheConfiguration() {
-    if (self == null) {
-      // In Ehcache 1.7, BlockingCache wants to ask us if we are
-      // clustered using Terracotta. Unfortunately it is too early
-      // to know for certain as the caches have not actually been
-      // created or configured.
-      //
-      return new CacheConfiguration();
-    }
-    return self().getCacheConfiguration();
-  }
-
-  public RegisteredEventListeners getCacheEventNotificationService() {
-    return self().getCacheEventNotificationService();
-  }
-
-  public CacheExceptionHandler getCacheExceptionHandler() {
-    return self().getCacheExceptionHandler();
-  }
-
-  public CacheManager getCacheManager() {
-    return self().getCacheManager();
-  }
-
-  public int getDiskStoreSize() throws IllegalStateException {
-    return self().getDiskStoreSize();
-  }
-
-  public String getGuid() {
-    return self().getGuid();
-  }
-
-  @SuppressWarnings("unchecked")
-  public List getKeys() throws IllegalStateException, CacheException {
-    return self().getKeys();
-  }
-
-  @SuppressWarnings("unchecked")
-  public List getKeysNoDuplicateCheck() throws IllegalStateException {
-    return self().getKeysNoDuplicateCheck();
-  }
-
-  @SuppressWarnings("unchecked")
-  public List getKeysWithExpiryCheck() throws IllegalStateException,
-      CacheException {
-    return self().getKeysWithExpiryCheck();
-  }
-
-  public long getMemoryStoreSize() throws IllegalStateException {
-    return self().getMemoryStoreSize();
-  }
-
-  public Element getQuiet(Object key) throws IllegalStateException,
-      CacheException {
-    return self().getQuiet(key);
-  }
-
-  public Element getQuiet(Serializable key) throws IllegalStateException,
-      CacheException {
-    return self().getQuiet(key);
-  }
-
-  public List<CacheExtension> getRegisteredCacheExtensions() {
-    return self().getRegisteredCacheExtensions();
-  }
-
-  public List<CacheLoader> getRegisteredCacheLoaders() {
-    return self().getRegisteredCacheLoaders();
-  }
-
-  public int getSize() throws IllegalStateException, CacheException {
-    return self().getSize();
-  }
-
-  public Statistics getStatistics() throws IllegalStateException {
-    return self().getStatistics();
-  }
-
-  public int getStatisticsAccuracy() {
-    return self().getStatisticsAccuracy();
-  }
-
-  public Status getStatus() {
-    return self().getStatus();
-  }
-
-  public Element getWithLoader(Object key, CacheLoader loader,
-      Object loaderArgument) throws CacheException {
-    return self().getWithLoader(key, loader, loaderArgument);
-  }
-
-  public void initialise() {
-    self().initialise();
-  }
-
-  public boolean isDisabled() {
-    return self().isDisabled();
-  }
-
-  public boolean isElementInMemory(Object key) {
-    return self().isElementInMemory(key);
-  }
-
-  public boolean isElementInMemory(Serializable key) {
-    return self().isElementInMemory(key);
-  }
-
-  public boolean isElementOnDisk(Object key) {
-    return self().isElementOnDisk(key);
-  }
-
-  public boolean isElementOnDisk(Serializable key) {
-    return self().isElementOnDisk(key);
-  }
-
-  public boolean isExpired(Element element) throws IllegalStateException,
-      NullPointerException {
-    return self().isExpired(element);
-  }
-
-  public boolean isKeyInCache(Object key) {
-    return self().isKeyInCache(key);
-  }
-
-  public boolean isValueInCache(Object value) {
-    return self().isValueInCache(value);
-  }
-
-  public void load(Object key) throws CacheException {
-    self().load(key);
-  }
-
-  @SuppressWarnings("unchecked")
-  public void loadAll(Collection keys, Object argument) throws CacheException {
-    self().loadAll(keys, argument);
-  }
-
-  public void put(Element element, boolean doNotNotifyCacheReplicators)
-      throws IllegalArgumentException, IllegalStateException, CacheException {
-    self().put(element, doNotNotifyCacheReplicators);
-  }
-
-  public void put(Element element) throws IllegalArgumentException,
-      IllegalStateException, CacheException {
-    self().put(element);
-  }
-
-  public void putQuiet(Element element) throws IllegalArgumentException,
-      IllegalStateException, CacheException {
-    self().putQuiet(element);
-  }
-
-  public void registerCacheExtension(CacheExtension cacheExtension) {
-    self().registerCacheExtension(cacheExtension);
-  }
-
-  public void registerCacheLoader(CacheLoader cacheLoader) {
-    self().registerCacheLoader(cacheLoader);
-  }
-
-  public boolean remove(Object key, boolean doNotNotifyCacheReplicators)
-      throws IllegalStateException {
-    return self().remove(key, doNotNotifyCacheReplicators);
-  }
-
-  public boolean remove(Object key) throws IllegalStateException {
-    return self().remove(key);
-  }
-
-  public boolean remove(Serializable key, boolean doNotNotifyCacheReplicators)
-      throws IllegalStateException {
-    return self().remove(key, doNotNotifyCacheReplicators);
-  }
-
-  public boolean remove(Serializable key) throws IllegalStateException {
-    return self().remove(key);
-  }
-
-  public void removeAll() throws IllegalStateException, CacheException {
-    self().removeAll();
-  }
-
-  public void removeAll(boolean doNotNotifyCacheReplicators)
-      throws IllegalStateException, CacheException {
-    self().removeAll(doNotNotifyCacheReplicators);
-  }
-
-  public boolean removeQuiet(Object key) throws IllegalStateException {
-    return self().removeQuiet(key);
-  }
-
-  public boolean removeQuiet(Serializable key) throws IllegalStateException {
-    return self().removeQuiet(key);
-  }
-
-  public void setBootstrapCacheLoader(BootstrapCacheLoader bootstrapCacheLoader)
-      throws CacheException {
-    self().setBootstrapCacheLoader(bootstrapCacheLoader);
-  }
-
-  public void setCacheExceptionHandler(
-      CacheExceptionHandler cacheExceptionHandler) {
-    self().setCacheExceptionHandler(cacheExceptionHandler);
-  }
-
-  public void setCacheManager(CacheManager cacheManager) {
-    self().setCacheManager(cacheManager);
-  }
-
-  public void setDisabled(boolean disabled) {
-    self().setDisabled(disabled);
-  }
-
-  public void setDiskStorePath(String diskStorePath) throws CacheException {
-    self().setDiskStorePath(diskStorePath);
-  }
-
-  public void setStatisticsAccuracy(int statisticsAccuracy) {
-    self().setStatisticsAccuracy(statisticsAccuracy);
-  }
-
-  public void unregisterCacheExtension(CacheExtension cacheExtension) {
-    self().unregisterCacheExtension(cacheExtension);
-  }
-
-  public void unregisterCacheLoader(CacheLoader cacheLoader) {
-    self().unregisterCacheLoader(cacheLoader);
-  }
-
-  public Object getInternalContext() {
-    return self.getInternalContext();
-  }
-
-  public LiveCacheStatistics getLiveCacheStatistics() throws IllegalStateException {
-    return self.getLiveCacheStatistics();
-  }
-
-  public SampledCacheStatistics getSampledCacheStatistics() {
-    return self.getSampledCacheStatistics();
-  }
-
-  public int getSizeBasedOnAccuracy(int statisticsAccuracy) throws IllegalArgumentException,
-      IllegalStateException, CacheException {
-    return self.getSizeBasedOnAccuracy(statisticsAccuracy);
-  }
-
-  public boolean isSampledStatisticsEnabled() {
-    return self.isSampledStatisticsEnabled();
-  }
-
-  public boolean isStatisticsEnabled() {
-    return self.isStatisticsEnabled();
-  }
-
-  public void registerCacheUsageListener(CacheUsageListener cacheUsageListener)
-      throws IllegalStateException {
-    self.registerCacheUsageListener(cacheUsageListener);
-  }
-
-  public void removeCacheUsageListener(CacheUsageListener cacheUsageListener)
-      throws IllegalStateException {
-    self.removeCacheUsageListener(cacheUsageListener);
-  }
-
-  public void setSampledStatisticsEnabled(boolean enableStatistics) {
-    self.setSampledStatisticsEnabled(enableStatistics);
-  }
-
-  public void setStatisticsEnabled(boolean enableStatistics) {
-    self.setStatisticsEnabled(enableStatistics);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 232c09d..5ebaada 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -29,20 +29,20 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.MimeUtilFileTypeRegistry;
 import com.google.gerrit.server.ReplicationUser;
-import com.google.gerrit.server.account.AccountByEmailCacheImpl;
+import com.google.gerrit.server.StarredChangesCacheImpl;
+import com.google.gerrit.server.account.AccountAgreementsCacheImpl;
 import com.google.gerrit.server.account.AccountCacheImpl;
+import com.google.gerrit.server.account.AccountDiffPreferencesCacheImpl;
+import com.google.gerrit.server.account.AccountGroupAgreementsCacheImpl;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
+import com.google.gerrit.server.account.AccountProjectWatchCacheImpl;
 import com.google.gerrit.server.account.DefaultRealm;
 import com.google.gerrit.server.account.EmailExpander;
 import com.google.gerrit.server.account.GroupCacheImpl;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.auth.ldap.LdapModule;
-import com.google.gerrit.server.cache.CachePool;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.git.ChangeMergeQueue;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.git.MergeQueue;
 import com.google.gerrit.server.git.PushAllProjectsOp;
 import com.google.gerrit.server.git.PushReplication;
@@ -50,10 +50,8 @@
 import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.mail.EmailSender;
 import com.google.gerrit.server.mail.FromAddressGenerator;
 import com.google.gerrit.server.mail.FromAddressGeneratorProvider;
-import com.google.gerrit.server.mail.SmtpEmailSender;
 import com.google.gerrit.server.patch.PatchListCacheImpl;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
@@ -68,7 +66,6 @@
 
 import org.apache.velocity.app.Velocity;
 import org.apache.velocity.runtime.RuntimeConstants;
-
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 
@@ -150,17 +147,19 @@
         GerritPersonIdentProvider.class);
 
     bind(IdGenerator.class);
-    bind(CachePool.class);
-    install(AccountByEmailCacheImpl.module());
     install(AccountCacheImpl.module());
     install(GroupCacheImpl.module());
     install(PatchListCacheImpl.module());
     install(ProjectCacheImpl.module());
+    install(StarredChangesCacheImpl.module());
+    install(AccountProjectWatchCacheImpl.module());
+    install(AccountAgreementsCacheImpl.module());
+    install(AccountGroupAgreementsCacheImpl.module());
+    install(AccountDiffPreferencesCacheImpl.module());
 
     factory(AccountInfoCacheFactory.Factory.class);
-    factory(ProjectState.Factory.class);
+    bind(ProjectState.class);
 
-    bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class);
     bind(FileTypeRegistry.class).to(MimeUtilFileTypeRegistry.class);
     bind(WorkQueue.class);
     bind(ToolsCatalog.class);
@@ -175,7 +174,6 @@
 
     bind(FromAddressGenerator.class).toProvider(
         FromAddressGeneratorProvider.class).in(SINGLETON);
-    bind(EmailSender.class).to(SmtpEmailSender.class).in(SINGLETON);
 
     bind(PatchSetInfoFactory.class);
     bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
@@ -187,8 +185,6 @@
     install(new LifecycleModule() {
       @Override
       protected void configure() {
-        listener().to(LocalDiskRepositoryManager.Lifecycle.class);
-        listener().to(CachePool.Lifecycle.class);
         listener().to(WorkQueue.Lifecycle.class);
         listener().to(VelocityLifecycle.class);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index 5a75995..66b67a8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -34,8 +34,7 @@
 import com.google.gerrit.server.patch.PublishComments;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.ChangeQueryRewriter;
+import com.google.gerrit.server.query.change.ChangeQueryModule;
 import com.google.inject.servlet.RequestScoped;
 
 /** Bindings for {@link RequestScoped} entities. */
@@ -47,13 +46,12 @@
         RequestScoped.class);
     bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
     bind(AccountResolver.class);
-    bind(ChangeQueryRewriter.class);
+    install(new ChangeQueryModule());
 
     bind(ChangeControl.Factory.class).in(SINGLETON);
     bind(GroupControl.Factory.class).in(SINGLETON);
     bind(ProjectControl.Factory.class).in(SINGLETON);
 
-    factory(ChangeQueryBuilder.Factory.class);
     factory(ReceiveCommits.Factory.class);
     factory(MergeOp.Factory.class);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java
index 5aa78cb..9c3da74 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.RequestCleanup;
 import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.Database;
+import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
@@ -26,11 +26,11 @@
 /** Provides {@link ReviewDb} database handle live only for this request. */
 @Singleton
 final class RequestScopedReviewDbProvider implements Provider<ReviewDb> {
-  private final Database<ReviewDb> schema;
+  private final SchemaFactory<ReviewDb> schema;
   private final Provider<RequestCleanup> cleanup;
 
   @Inject
-  RequestScopedReviewDbProvider(final Database<ReviewDb> schema,
+  RequestScopedReviewDbProvider(final SchemaFactory<ReviewDb> schema,
       final Provider<RequestCleanup> cleanup) {
     this.schema = schema;
     this.cleanup = cleanup;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreProvider.java
index da17c08..4b6dcaf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreProvider.java
@@ -14,10 +14,9 @@
 
 package com.google.gerrit.server.contact;
 
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
@@ -33,14 +32,14 @@
 public class ContactStoreProvider implements Provider<ContactStore> {
   private final Config config;
   private final SitePaths site;
-  private final SchemaFactory<ReviewDb> schema;
+  private final AccountCache accounts;
 
   @Inject
   ContactStoreProvider(@GerritServerConfig final Config config,
-      final SitePaths site, final SchemaFactory<ReviewDb> schema) {
+      final SitePaths site, final AccountCache accountCache) {
     this.config = config;
     this.site = site;
-    this.schema = schema;
+    this.accounts = accountCache;
   }
 
   @Override
@@ -68,7 +67,7 @@
       throw new ProvisionException("PGP public key file \""
           + pubkey.getAbsolutePath() + "\" not found");
     }
-    return new EncryptedContactStore(storeUrl, storeAPPSEC, pubkey, schema);
+    return new EncryptedContactStore(storeUrl, storeAPPSEC, pubkey, accounts);
   }
 
   private static boolean havePGP() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
index 05d4e7a..34185dc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
@@ -18,10 +18,10 @@
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.reviewdb.ContactInformation;
-import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.UrlEncoded;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.util.FutureException;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 
@@ -65,18 +65,18 @@
       LoggerFactory.getLogger(EncryptedContactStore.class);
   private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
 
-  private final SchemaFactory<ReviewDb> schema;
   private final PGPPublicKey dest;
   private final SecureRandom prng;
   private final URL storeUrl;
   private final String storeAPPSEC;
+  private final AccountCache accountCache;
 
   EncryptedContactStore(final URL storeUrl, final String storeAPPSEC,
-      final File pubKey, final SchemaFactory<ReviewDb> schema) {
+      final File pubKey, final AccountCache accountCache) {
     this.storeUrl = storeUrl;
     this.storeAPPSEC = storeAPPSEC;
-    this.schema = schema;
     this.dest = selectKey(readPubRing(pubKey));
+    this.accountCache = accountCache;
 
     final String prngName = "SHA1PRNG";
     try {
@@ -250,31 +250,26 @@
     field(b, "Preferred-Email", account.getPreferredEmail());
 
     try {
-      final ReviewDb db = schema.open();
-      try {
-        for (final AccountExternalId e : db.accountExternalIds().byAccount(
-            account.getId())) {
-          final StringBuilder oistr = new StringBuilder();
-          if (e.getEmailAddress() != null && e.getEmailAddress().length() > 0) {
-            if (oistr.length() > 0) {
-              oistr.append(' ');
-            }
-            oistr.append(e.getEmailAddress());
+      for (AccountExternalId e : FutureUtil.get(
+          accountCache.get(account.getId())).getExternalIds()) {
+        final StringBuilder oistr = new StringBuilder();
+        if (e.getEmailAddress() != null && e.getEmailAddress().length() > 0) {
+          if (oistr.length() > 0) {
+            oistr.append(' ');
           }
-          if (e.isScheme(AccountExternalId.SCHEME_MAILTO)) {
-            if (oistr.length() > 0) {
-              oistr.append(' ');
-            }
-            oistr.append('<');
-            oistr.append(e.getExternalId());
-            oistr.append('>');
-          }
-          field(b, "Identity", oistr.toString());
+          oistr.append(e.getEmailAddress());
         }
-      } finally {
-        db.close();
+        if (e.isScheme(AccountExternalId.SCHEME_MAILTO)) {
+          if (oistr.length() > 0) {
+            oistr.append(' ');
+          }
+          oistr.append('<');
+          oistr.append(e.getExternalId());
+          oistr.append('>');
+        }
+        field(b, "Identity", oistr.toString());
       }
-    } catch (OrmException e) {
+    } catch (FutureException e) {
       throw new ContactInformationStoreException(e);
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index 3ad397e..052ea34 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.reviewdb.TrackingId;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -143,7 +144,7 @@
    * @return object suitable for serialization to JSON
    */
   public AccountAttribute asAccountAttribute(Account.Id id) {
-    return asAccountAttribute(accountCache.get(id).getAccount());
+    return asAccountAttribute(FutureUtil.get(accountCache.getAccount(id)));
   }
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index b8cfb71..9b29b84 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -15,8 +15,10 @@
 package com.google.gerrit.server.git;
 
 import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -48,6 +50,20 @@
   private static final String UNNAMED =
       "Unnamed repository; edit this file to name it for gitweb.";
 
+  public static class Module extends AbstractModule {
+    @Override
+    protected void configure() {
+      bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class);
+
+      install(new LifecycleModule() {
+        @Override
+        protected void configure() {
+          listener().to(LocalDiskRepositoryManager.Lifecycle.class);
+        }
+      });
+    }
+  }
+
   public static class Lifecycle implements LifecycleListener {
     private final Config cfg;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 2898369..f93d2d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -42,6 +42,7 @@
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gerrit.server.workflow.CategoryFunction;
 import com.google.gerrit.server.workflow.FunctionState;
 import com.google.gwtorm.client.AtomicUpdate;
@@ -190,7 +191,8 @@
   }
 
   public void merge() throws MergeException {
-    final ProjectState pe = projectCache.get(destBranch.getParentKey());
+    ProjectState pe =
+        FutureUtil.get(projectCache.get(destBranch.getParentKey()));
     if (pe == null) {
       throw new MergeException("No such project: " + destBranch.getParentKey());
     }
@@ -1202,7 +1204,7 @@
 
     try {
       hooks.doChangeMergedHook(c, //
-          accountCache.get(submitter.getAccountId()).getAccount(), //
+          FutureUtil.get(accountCache.getAccount(submitter.getAccountId())), //
           schema.patchSets().get(c.currentPatchSetId()));
     } catch (OrmException ex) {
       log.error("Cannot run hook for submitted patch set " + c.getId(), ex);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index fbcdc09..a166b1e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.common.ChangeHookRunner;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.ApprovalType;
@@ -21,9 +23,7 @@
 import com.google.gerrit.common.errors.NoSuchAccountException;
 import com.google.gerrit.reviewdb.AbstractAgreement;
 import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountAgreement;
 import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupAgreement;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Change;
@@ -39,6 +39,8 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountAgreementsCache;
+import com.google.gerrit.server.account.AccountGroupAgreementsCache;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.TrackingFooters;
@@ -50,6 +52,7 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.AtomicUpdate;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
@@ -140,6 +143,8 @@
   private final String canonicalWebUrl;
   private final PersonIdent gerritIdent;
   private final TrackingFooters trackingFooters;
+  private final AccountAgreementsCache accountAgreementsCache;
+  private final AccountGroupAgreementsCache accountGroupAgreementsCache;
 
   private final ProjectControl projectControl;
   private final Project project;
@@ -172,6 +177,8 @@
       @CanonicalWebUrl @Nullable final String canonicalWebUrl,
       @GerritPersonIdent final PersonIdent gerritIdent,
       final TrackingFooters trackingFooters,
+      final AccountAgreementsCache accountAgreementsCache,
+      final AccountGroupAgreementsCache accountGroupAgreementsCache,
 
       @Assisted final ProjectControl projectControl,
       @Assisted final Repository repo) {
@@ -188,6 +195,8 @@
     this.canonicalWebUrl = canonicalWebUrl;
     this.gerritIdent = gerritIdent;
     this.trackingFooters = trackingFooters;
+    this.accountAgreementsCache = accountAgreementsCache;
+    this.accountGroupAgreementsCache = accountGroupAgreementsCache;
 
     this.projectControl = projectControl;
     this.project = projectControl.getProject();
@@ -296,40 +305,12 @@
     AbstractAgreement bestAgreement = null;
     ContributorAgreement bestCla = null;
 
-    OUTER: for (AccountGroup.Id groupId : currentUser.getEffectiveGroups()) {
-      final List<AccountGroupAgreement> temp =
-          db.accountGroupAgreements().byGroup(groupId).toList();
-
-      Collections.reverse(temp);
-
-      for (final AccountGroupAgreement a : temp) {
-        final ContributorAgreement cla =
-            db.contributorAgreements().get(a.getAgreementId());
-        if (cla == null) {
-          continue;
-        }
-
+    List<AbstractAgreement> accepted = getAgreements();
+    Collections.sort(accepted, AbstractAgreement.SORT);
+    for (AbstractAgreement a : accepted) {
+      bestCla = db.contributorAgreements().get(a.getAgreementId());
+      if (bestCla != null) {
         bestAgreement = a;
-        bestCla = cla;
-        break OUTER;
-      }
-    }
-
-    if (bestAgreement == null) {
-      final List<AccountAgreement> temp =
-          db.accountAgreements().byAccount(currentUser.getAccountId()).toList();
-
-      Collections.reverse(temp);
-
-      for (final AccountAgreement a : temp) {
-        final ContributorAgreement cla =
-            db.contributorAgreements().get(a.getAgreementId());
-        if (cla == null) {
-          continue;
-        }
-
-        bestAgreement = a;
-        bestCla = cla;
         break;
       }
     }
@@ -404,6 +385,23 @@
     return new Capable(msg.toString());
   }
 
+  @SuppressWarnings("unchecked")
+  private List<AbstractAgreement> getAgreements() {
+    ListenableFuture f;
+
+    List<ListenableFuture<List<AbstractAgreement>>> agreements =
+        Lists.newArrayList();
+
+    f = accountAgreementsCache.byAccount(currentUser.getAccountId());
+    agreements.add(f);
+
+    for (AccountGroup.Id groupId : currentUser.getEffectiveGroups()) {
+      f = accountGroupAgreementsCache.byGroup(groupId);
+      agreements.add(f);
+    }
+    return FutureUtil.getOrEmptyList(FutureUtil.concat(agreements));
+  }
+
   private static boolean missing(final String value) {
     return value == null || value.trim().equals("");
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
index 1640e05..624e626 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
@@ -16,8 +16,8 @@
 
 import java.io.UnsupportedEncodingException;
 
-class Address {
-  static Address parse(final String in) {
+public class Address {
+  public static Address parse(final String in) {
     final int lt = in.indexOf('<');
     final int gt = in.indexOf('>');
     final int at = in.indexOf("@");
@@ -37,15 +37,23 @@
   final String name;
   final String email;
 
-  Address(String email) {
+  public Address(String email) {
     this(null, email);
   }
 
-  Address(String name, String email) {
+  public Address(String name, String email) {
     this.name = name;
     this.email = email;
   }
 
+  public String getName() {
+    return name;
+  }
+
+  public String getEmail() {
+    return email;
+  }
+
   @Override
   public String toString() {
     try {
@@ -55,7 +63,7 @@
     }
   }
 
-  String toHeaderString() throws UnsupportedEncodingException {
+  public String toHeaderString() throws UnsupportedEncodingException {
     if (name != null) {
       return quotedPhrase(name) + " <" + email + ">";
     } else if (isSimple()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index 6e641a2..931fc37 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ChangeMessage;
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 
 import java.util.ArrayList;
@@ -99,15 +100,12 @@
   protected abstract void formatChange() throws EmailException;
 
   /** Setup the message headers and envelope (TO, CC, BCC). */
+  @Override
   protected void init() throws EmailException {
-    if (args.projectCache != null) {
-      projectState = args.projectCache.get(change.getProject());
-      projectName =
-          projectState != null ? projectState.getProject().getName() : null;
-    } else {
-      projectState = null;
-      projectName = null;
-    }
+    projectState = FutureUtil.get(args.projectCache.get(change.getProject()));
+    projectName = projectState != null //
+        ? projectState.getProject().getName() //
+        : null;
 
     if (patchSet == null) {
       try {
@@ -234,7 +232,7 @@
   /** Get the patch list corresponding to this patch set. */
   protected PatchList getPatchList() {
     if (patchSet != null) {
-      return args.patchListCache.get(change, patchSet);
+      return FutureUtil.getOrNull(args.patchListCache.get(change, patchSet));
     }
     return null;
   }
@@ -244,14 +242,6 @@
     return projectState;
   }
 
-  /** Get the groups which own the project. */
-  protected Set<AccountGroup.Id> getProjectOwners() {
-    final ProjectState r;
-
-    r = args.projectCache.get(change.getProject());
-    return r != null ? r.getOwners() : Collections.<AccountGroup.Id> emptySet();
-  }
-
   /** TO or CC all vested parties (change owner, patch set uploader, author). */
   protected void rcptToAuthors(final RecipientType rt) {
     add(rt, change.getOwner());
@@ -306,14 +296,18 @@
     List<AccountProjectWatch> matching = new ArrayList<AccountProjectWatch>();
     Set<Account.Id> projectWatchers = new HashSet<Account.Id>();
 
-    for (AccountProjectWatch w : args.db.get().accountProjectWatches()
-        .byProject(change.getProject())) {
+    ListenableFuture<List<AccountProjectWatch>> thisProject =
+        args.accountProjectWatchCache.byProject(change.getProject());
+
+    ListenableFuture<List<AccountProjectWatch>> allProjects =
+        args.accountProjectWatchCache.byProject(args.wildProject);
+
+    for (AccountProjectWatch w : FutureUtil.get(thisProject)) {
       projectWatchers.add(w.getAccountId());
       add(matching, w);
     }
 
-    for (AccountProjectWatch w : args.db.get().accountProjectWatches()
-        .byProject(args.wildProject)) {
+    for (AccountProjectWatch w : FutureUtil.get(allProjects)) {
       if (!projectWatchers.contains(w.getAccountId())) {
         add(matching, w);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
index 18bfe97..be830b9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
@@ -19,11 +19,14 @@
 import com.google.gerrit.reviewdb.AccountGroupMember;
 import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -75,4 +78,12 @@
       // who have a lower interest in the change.
     }
   }
+
+  /** Get the groups which own the project. */
+  private Set<AccountGroup.Id> getProjectOwners() {
+    final ProjectState r;
+
+    r = FutureUtil.getOrNull(args.projectCache.get(change.getProject()));
+    return r != null ? r.getOwners() : Collections.<AccountGroup.Id> emptySet();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
index f2ab9fa..196db4b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -17,8 +17,10 @@
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.StarredChangesCache;
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountProjectWatchCache;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.WildProjectName;
@@ -38,6 +40,8 @@
   final ProjectCache projectCache;
   final AccountCache accountCache;
   final PatchListCache patchListCache;
+  final StarredChangesCache starredChangesCache;
+  final AccountProjectWatchCache accountProjectWatchCache;
   final FromAddressGenerator fromAddressGenerator;
   final EmailSender emailSender;
   final PatchSetInfoFactory patchSetInfoFactory;
@@ -53,6 +57,8 @@
   @Inject
   EmailArguments(GitRepositoryManager server, ProjectCache projectCache,
       AccountCache accountCache, PatchListCache patchListCache,
+      StarredChangesCache starredChangesCache,
+      AccountProjectWatchCache accountProjectWatchCache,
       FromAddressGenerator fromAddressGenerator, EmailSender emailSender,
       PatchSetInfoFactory patchSetInfoFactory,
       GenericFactory identifiedUserFactory,
@@ -65,6 +71,8 @@
     this.projectCache = projectCache;
     this.accountCache = accountCache;
     this.patchListCache = patchListCache;
+	this.starredChangesCache = starredChangesCache;
+	this.accountProjectWatchCache = accountProjectWatchCache;
     this.fromAddressGenerator = fromAddressGenerator;
     this.emailSender = emailSender;
     this.patchSetInfoFactory = patchSetInfoFactory;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
index 10f6510..ce8c8f9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
@@ -19,29 +19,34 @@
 import java.io.Writer;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 
-abstract class EmailHeader {
-  abstract boolean isEmpty();
+public abstract class EmailHeader {
+  public abstract boolean isEmpty();
 
-  abstract void write(Writer w) throws IOException;
+  public abstract void write(Writer w) throws IOException;
 
-  static class String extends EmailHeader {
+  public static class String extends EmailHeader {
     private java.lang.String value;
 
-    String(java.lang.String v) {
+    public String(java.lang.String v) {
       value = v;
     }
 
+    public java.lang.String getString() {
+      return value;
+    }
+
     @Override
-    boolean isEmpty() {
+    public boolean isEmpty() {
       return value == null || value.length() == 0;
     }
 
     @Override
-    void write(Writer w) throws IOException {
+    public void write(Writer w) throws IOException {
       if (needsQuotedPrintable(value)) {
         w.write(quotedPrintable(value));
       } else {
@@ -84,20 +89,24 @@
     return r.toString();
   }
 
-  static class Date extends EmailHeader {
+  public static class Date extends EmailHeader {
     private java.util.Date value;
 
-    Date(java.util.Date v) {
+    public Date(java.util.Date v) {
       value = v;
     }
 
+    public java.util.Date getDate() {
+      return value;
+    }
+
     @Override
-    boolean isEmpty() {
+    public boolean isEmpty() {
       return value == null;
     }
 
     @Override
-    void write(Writer w) throws IOException {
+    public void write(Writer w) throws IOException {
       final SimpleDateFormat fmt;
       // Mon, 1 Jun 2009 10:49:44 -0700
       fmt = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
@@ -105,17 +114,21 @@
     }
   }
 
-  static class AddressList extends EmailHeader {
+  public static class AddressList extends EmailHeader {
     private final List<Address> list = new ArrayList<Address>();
 
-    AddressList() {
+    public AddressList() {
     }
 
-    AddressList(Address addr) {
+    public AddressList(Address addr) {
       add(addr);
     }
 
-    void add(Address addr) {
+    public List<Address> getAddressList() {
+      return Collections.unmodifiableList(list);
+    }
+
+    public void add(Address addr) {
       list.add(addr);
     }
 
@@ -128,12 +141,12 @@
     }
 
     @Override
-    boolean isEmpty() {
+    public boolean isEmpty() {
       return list.isEmpty();
     }
 
     @Override
-    void write(Writer w) throws IOException {
+    public void write(Writer w) throws IOException {
       int len = 8;
       boolean firstAddress = true;
       boolean needComma = false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
index 2d739ea..b1309a4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -87,7 +88,7 @@
     @Override
     public Address from(final Account.Id fromId) {
       if (fromId != null) {
-        final Account a = accountCache.get(fromId).getAccount();
+        Account a = FutureUtil.get(accountCache.getAccount(fromId));
         if (a.getPreferredEmail() != null) {
           return new Address(a.getFullName(), a.getPreferredEmail());
         }
@@ -138,7 +139,7 @@
       final String senderName;
 
       if (fromId != null) {
-        final Account account = accountCache.get(fromId).getAccount();
+        Account account = FutureUtil.get(accountCache.getAccount(fromId));
         String fullName = account.getFullName();
         if (fullName == null || "".equals(fullName)) {
           fullName = "Anonymous Coward";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index 913e352..587aeca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -16,6 +16,8 @@
 
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.UserIdentity;
+import com.google.gerrit.server.mail.EmailHeader.AddressList;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.mail.EmailHeader.AddressList;
 
@@ -40,7 +42,6 @@
 import java.util.List;
 import java.util.Map;
 
-
 /** Sends an email to one or more interested parties. */
 public abstract class OutgoingEmail {
   private static final Logger log = LoggerFactory.getLogger(OutgoingEmail.class);
@@ -85,7 +86,8 @@
     format();
     if (shouldSendMessage()) {
       if (fromId != null) {
-        final Account fromUser = args.accountCache.get(fromId).getAccount();
+        final Account fromUser =
+            FutureUtil.get(args.accountCache.getAccount(fromId));
 
         if (fromUser.getGeneralPreferences().isCopySelfOnEmails()) {
           // If we are impersonating a user, make sure they receive a CC of
@@ -155,7 +157,8 @@
   }
 
   protected String getFromLine() {
-    final Account account = args.accountCache.get(fromId).getAccount();
+    final Account account =
+	    FutureUtil.get(args.accountCache.getAccount(fromId));
     final String name = account.getFullName();
     final String email = account.getPreferredEmail();
     StringBuilder f = new StringBuilder();
@@ -232,7 +235,8 @@
       return "Anonymous Coward";
     }
 
-    final Account userAccount = args.accountCache.get(accountId).getAccount();
+    Account userAccount =
+        FutureUtil.get(args.accountCache.getAccount(accountId));
     String name = userAccount.getFullName();
     if (name == null) {
       name = userAccount.getPreferredEmail();
@@ -244,9 +248,9 @@
   }
 
   public String getNameEmailFor(Account.Id accountId) {
-    AccountState who = args.accountCache.get(accountId);
-    String name = who.getAccount().getFullName();
-    String email = who.getAccount().getPreferredEmail();
+    Account who = FutureUtil.get(args.accountCache.getAccount(accountId));
+    String name = who.getFullName();
+    String email = who.getPreferredEmail();
 
     if (name != null && email != null) {
       return name + " <" + email + ">";
@@ -330,7 +334,7 @@
   }
 
   private Address toAddress(final Account.Id id) {
-    final Account a = args.accountCache.get(id).getAccount();
+    final Account a = FutureUtil.get(args.accountCache.getAccount(id));
     final String e = a.getPreferredEmail();
     if (!a.isActive() || e == null) {
       return null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
index d67d3b3..1a92321 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.common.Version;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -38,6 +39,13 @@
 /** Sends email via a nearby SMTP server. */
 @Singleton
 public class SmtpEmailSender implements EmailSender {
+  public static class Module extends AbstractModule {
+    @Override
+    protected void configure() {
+      bind(EmailSender.class).to(SmtpEmailSender.class);
+    }
+  }
+
   public static enum Encryption {
     NONE, SSL, TLS;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
index a5121e9..cd39e1f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
@@ -15,39 +15,22 @@
 package com.google.gerrit.server.patch;
 
 
-import static com.google.gerrit.server.ioutil.BasicSerialization.readBytes;
-import static com.google.gerrit.server.ioutil.BasicSerialization.readVarInt32;
-import static com.google.gerrit.server.ioutil.BasicSerialization.writeBytes;
-import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.readCanBeNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.writeCanBeNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
-
 import com.google.gerrit.reviewdb.Patch;
 import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gwtorm.client.Column;
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
-import java.util.zip.DeflaterOutputStream;
-import java.util.zip.InflaterInputStream;
 
 import javax.annotation.Nullable;
 
-public class PatchList implements Serializable {
-  private static final long serialVersionUID = PatchListKey.serialVersionUID;
+public class PatchList {
   private static final Comparator<PatchListEntry> PATCH_CMP =
       new Comparator<PatchListEntry>() {
         @Override
@@ -57,35 +40,56 @@
       };
 
   @Nullable
-  private transient ObjectId oldId;
-  private transient ObjectId newId;
-  private transient boolean intralineDifference;
-  private transient PatchListEntry[] patches;
+  @Column(id = 1)
+  protected String oldIdName;
+
+  @Column(id = 2)
+  protected String newIdName;
+
+  @Column(id = 3)
+  protected boolean intralineDifference;
+
+  @Column(id = 4)
+  protected List<PatchListEntry> patches;
+
+  private volatile ObjectId oldId;
+  private volatile ObjectId newId;
+
+  protected PatchList() {
+  }
 
   PatchList(@Nullable final AnyObjectId oldId, final AnyObjectId newId,
       final boolean intralineDifference, final PatchListEntry[] patches) {
     this.oldId = oldId != null ? oldId.copy() : null;
+    oldIdName = oldId != null ? oldId.name() : null;
     this.newId = newId.copy();
+    newIdName = newId.name();
     this.intralineDifference = intralineDifference;
 
     Arrays.sort(patches, PATCH_CMP);
-    this.patches = patches;
+    this.patches = Arrays.asList(patches);
   }
 
   /** Old side tree or commit; null only if this is a combined diff. */
   @Nullable
   public ObjectId getOldId() {
+    if (oldId == null && oldIdName != null) {
+      oldId = ObjectId.fromString(oldIdName);
+    }
     return oldId;
   }
 
   /** New side commit. */
   public ObjectId getNewId() {
+    if (newId == null) {
+      newId = ObjectId.fromString(newIdName);
+    }
     return newId;
   }
 
   /** Get a sorted, unmodifiable list of all files in this list. */
   public List<PatchListEntry> getPatches() {
-    return Collections.unmodifiableList(Arrays.asList(patches));
+    return Collections.unmodifiableList(patches);
   }
 
   /** @return true if this list was computed with intraline difference enabled. */
@@ -108,7 +112,7 @@
    *        how the cache is keyed versus how the database is keyed.
    */
   public List<Patch> toPatchList(final PatchSet.Id setId) {
-    final ArrayList<Patch> r = new ArrayList<Patch>(patches.length);
+    final ArrayList<Patch> r = new ArrayList<Patch>(patches.size());
     for (final PatchListEntry e : patches) {
       r.add(e.toPatch(setId));
     }
@@ -118,15 +122,15 @@
   /** Find an entry by name, returning an empty entry if not present. */
   public PatchListEntry get(final String fileName) {
     final int index = search(fileName);
-    return 0 <= index ? patches[index] : PatchListEntry.empty(fileName);
+    return 0 <= index ? patches.get(index) : PatchListEntry.empty(fileName);
   }
 
   private int search(final String fileName) {
-    int high = patches.length;
+    int high = patches.size();
     int low = 0;
     while (low < high) {
       final int mid = (low + high) >>> 1;
-      final int cmp = patches[mid].getNewName().compareTo(fileName);
+      final int cmp = patches.get(mid).getNewName().compareTo(fileName);
       if (cmp < 0)
         low = mid + 1;
       else if (cmp == 0)
@@ -136,39 +140,4 @@
     }
     return -(low + 1);
   }
-
-  private void writeObject(final ObjectOutputStream output) throws IOException {
-    final ByteArrayOutputStream buf = new ByteArrayOutputStream();
-    final DeflaterOutputStream out = new DeflaterOutputStream(buf);
-    try {
-      writeCanBeNull(out, oldId);
-      writeNotNull(out, newId);
-      writeVarInt32(out, intralineDifference ? 1 : 0);
-      writeVarInt32(out, patches.length);
-      for (PatchListEntry p : patches) {
-        p.writeTo(out);
-      }
-    } finally {
-      out.close();
-    }
-    writeBytes(output, buf.toByteArray());
-  }
-
-  private void readObject(final ObjectInputStream input) throws IOException {
-    final ByteArrayInputStream buf = new ByteArrayInputStream(readBytes(input));
-    final InflaterInputStream in = new InflaterInputStream(buf);
-    try {
-      oldId = readCanBeNull(in);
-      newId = readNotNull(in);
-      intralineDifference = readVarInt32(in) != 0;
-      final int cnt = readVarInt32(in);
-      final PatchListEntry[] all = new PatchListEntry[cnt];
-      for (int i = 0; i < all.length; i++) {
-        all[i] = PatchListEntry.readFrom(in);
-      }
-      patches = all;
-    } finally {
-      in.close();
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
index 1a6d2ae..664bc02 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
@@ -14,15 +14,17 @@
 
 package com.google.gerrit.server.patch;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
 
 /** Provides a cached list of {@link PatchListEntry}. */
 public interface PatchListCache {
-  public PatchList get(PatchListKey key);
+  public ListenableFuture<PatchList> get(PatchListKey key);
 
-  public PatchList get(Change change, PatchSet patchSet);
+  public ListenableFuture<PatchList> get(Change change, PatchSet patchSet);
 
-  public PatchList get(Change change, PatchSet patchSet, Whitespace whitespace);
+  public ListenableFuture<PatchList> get(Change change, PatchSet patchSet,
+      Whitespace whitespace);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index eb62fe1..4f52e2a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -61,6 +61,9 @@
 package com.google.gerrit.server.patch;
 
 
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gerrit.prettify.common.BaseEdit;
+import com.google.gerrit.prettify.common.LineEdit;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.Project;
@@ -86,15 +89,12 @@
 import org.eclipse.jgit.diff.RawTextIgnoreTrailingWhitespace;
 import org.eclipse.jgit.diff.RawTextIgnoreWhitespaceChange;
 import org.eclipse.jgit.diff.RenameDetector;
-import org.eclipse.jgit.diff.ReplaceEdit;
-import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.patch.FileHeader;
 import org.eclipse.jgit.patch.FileHeader.PatchType;
@@ -127,8 +127,9 @@
       protected void configure() {
         final TypeLiteral<Cache<PatchListKey, PatchList>> type =
             new TypeLiteral<Cache<PatchListKey, PatchList>>() {};
-        disk(type, CACHE_NAME) //
+        cache(type, CACHE_NAME) //
             .memoryLimit(128) // very large items, cache only a few
+            .diskLimit(16384) // cache a lot on disk, they are hard to make
             .evictionPolicy(EvictionPolicy.LRU) // prefer most recent
             .populateWith(Loader.class) //
         ;
@@ -146,16 +147,17 @@
     cache = thecache;
   }
 
-  public PatchList get(final PatchListKey key) {
+  public ListenableFuture<PatchList> get(final PatchListKey key) {
     return cache.get(key);
   }
 
-  public PatchList get(final Change change, final PatchSet patchSet) {
+  public ListenableFuture<PatchList> get(final Change change,
+      final PatchSet patchSet) {
     return get(change, patchSet, Whitespace.IGNORE_NONE);
   }
 
-  public PatchList get(final Change change, final PatchSet patchSet,
-      final Whitespace whitespace) {
+  public ListenableFuture<PatchList> get(final Change change,
+      final PatchSet patchSet, final Whitespace whitespace) {
     final Project.NameKey projectKey = change.getProject();
     final ObjectId a = null;
     final ObjectId b = ObjectId.fromString(patchSet.getRevision().get());
@@ -234,24 +236,43 @@
       return new PatchList(a, b, computeIntraline, entries);
     }
 
+    private static List<LineEdit> editsToLineEdits(List<Edit> edits) {
+      List<LineEdit> l = new ArrayList<LineEdit>(edits.size());
+      for (Edit e : edits) {
+        l.add(new LineEdit(e));
+      }
+      return l;
+    }
+
+    private static List<BaseEdit> editsToBaseEdits(List<Edit> edits) {
+      List<BaseEdit> l = new ArrayList<BaseEdit>(edits.size());
+      for (Edit e : edits) {
+        l.add(new BaseEdit(e));
+      }
+      return l;
+    }
+
     private PatchListEntry newEntry(Repository repo, RevTree aTree,
         RevTree bTree, FileHeader fileHeader) throws IOException {
       final FileMode oldMode = fileHeader.getOldMode();
       final FileMode newMode = fileHeader.getNewMode();
 
       if (oldMode == FileMode.GITLINK || newMode == FileMode.GITLINK) {
-        return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
+        return new PatchListEntry(fileHeader, Collections
+            .<LineEdit> emptyList());
       }
 
       if (aTree == null // want combined diff
           || fileHeader.getPatchType() != PatchType.UNIFIED
           || fileHeader.getHunks().isEmpty()) {
-        return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
+        return new PatchListEntry(fileHeader, Collections
+            .<LineEdit> emptyList());
       }
 
-      List<Edit> edits = fileHeader.toEditList();
+      List<LineEdit> edits = editsToLineEdits(fileHeader.toEditList());
       if (edits.isEmpty()) {
-        return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
+        return new PatchListEntry(fileHeader, Collections
+            .<LineEdit> emptyList());
       }
       if (!computeIntraline) {
         return new PatchListEntry(fileHeader, edits);
@@ -267,11 +288,11 @@
       Text bContent = null;
 
       for (int i = 0; i < edits.size(); i++) {
-        Edit e = edits.get(i);
+        LineEdit e = edits.get(i);
 
         if (e.getType() == Edit.Type.REPLACE) {
           if (aContent == null) {
-            edits = new ArrayList<Edit>(edits);
+            edits = new ArrayList<LineEdit>(edits);
             aContent = read(repo, fileHeader.getOldPath(), aTree);
             bContent = read(repo, fileHeader.getNewPath(), bTree);
             combineLineEdits(edits, aContent, bContent);
@@ -282,15 +303,16 @@
           CharText a = new CharText(aContent, e.getBeginA(), e.getEndA());
           CharText b = new CharText(bContent, e.getBeginB(), e.getEndB());
 
-          List<Edit> wordEdits = new MyersDiff(a, b).getEdits();
+          List<BaseEdit> wordEdits =
+              editsToBaseEdits(new MyersDiff(a, b).getEdits());
 
           // Combine edits that are really close together. If they are
           // just a few characters apart we tend to get better results
           // by joining them together and taking the whole span.
           //
           for (int j = 0; j < wordEdits.size() - 1;) {
-            Edit c = wordEdits.get(j);
-            Edit n = wordEdits.get(j + 1);
+            BaseEdit c = wordEdits.get(j);
+            BaseEdit n = wordEdits.get(j + 1);
 
             if (n.getBeginA() - c.getEndA() <= 5
                 || n.getBeginB() - c.getEndB() <= 5) {
@@ -302,7 +324,7 @@
 
               if (canCoalesce(a, c.getEndA(), n.getBeginA())
                   && canCoalesce(b, c.getEndB(), n.getBeginB())) {
-                wordEdits.set(j, new Edit(ab, ae, bb, be));
+                wordEdits.set(j, new LineEdit(ab, ae, bb, be));
                 wordEdits.remove(j + 1);
                 continue;
               }
@@ -316,7 +338,7 @@
           // to produce some crazy stuff.
           //
           for (int j = 0; j < wordEdits.size(); j++) {
-            Edit c = wordEdits.get(j);
+            BaseEdit c = wordEdits.get(j);
             int ab = c.getBeginA();
             int ae = c.getEndA();
 
@@ -331,7 +353,7 @@
             // with silly stuff like "es" -> "es = Addresses".
             //
             if (1 < j) {
-              Edit p = wordEdits.get(j - 1);
+              BaseEdit p = wordEdits.get(j - 1);
               if (p.getEndA() == ab || p.getEndB() == bb) {
                 if (p.getEndA() == ab && p.getBeginA() < p.getEndA()) {
                   ab = p.getBeginA();
@@ -448,20 +470,20 @@
               be++;
             }
 
-            wordEdits.set(j, new Edit(ab, ae, bb, be));
+            wordEdits.set(j, new BaseEdit(ab, ae, bb, be));
           }
 
-          edits.set(i, new ReplaceEdit(e, wordEdits));
+          edits.set(i, new LineEdit(e, wordEdits));
         }
       }
 
       return new PatchListEntry(fileHeader, edits);
     }
 
-    private static void combineLineEdits(List<Edit> edits, Text a, Text b) {
+    private static void combineLineEdits(List<LineEdit> edits, Text a, Text b) {
       for (int j = 0; j < edits.size() - 1;) {
-        Edit c = edits.get(j);
-        Edit n = edits.get(j + 1);
+        BaseEdit c = edits.get(j);
+        BaseEdit n = edits.get(j + 1);
 
         // Combine edits that are really close together. Right now our rule
         // is, coalesce two line edits which are only one line apart if that
@@ -480,7 +502,7 @@
           int bb = c.getBeginB();
           int be = n.getEndB();
 
-          edits.set(j, new Edit(ab, ae, bb, be));
+          edits.set(j, new LineEdit(ab, ae, bb, be));
           edits.remove(j + 1);
           continue;
         }
@@ -512,7 +534,7 @@
       return true;
     }
 
-    private static int findLF(List<Edit> edits, int j, CharText t, int b) {
+    private static int findLF(List<BaseEdit> edits, int j, CharText t, int b) {
       int lf = b;
       int limit = 0 < j ? edits.get(j - 1).getEndB() : 0;
       while (limit < lf && t.charAt(lf) != '\n') {
@@ -536,13 +558,7 @@
       if (tw == null || tw.getFileMode(0).getObjectType() != Constants.OBJ_BLOB) {
         return Text.EMPTY;
       }
-      ObjectLoader ldr;
-      try {
-        ldr = repo.open(tw.getObjectId(0), Constants.OBJ_BLOB);
-      } catch (MissingObjectException notFound) {
-        return Text.EMPTY;
-      }
-      return new Text(ldr.getCachedBytes());
+      return new Text(repo.open(tw.getObjectId(0), Constants.OBJ_BLOB));
     }
 
     private static AnyObjectId aFor(final PatchListKey key,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
index c4484fc..88d5b68 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
@@ -23,13 +23,14 @@
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeString;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
 
+import com.google.gerrit.prettify.common.BaseEdit;
+import com.google.gerrit.prettify.common.LineEdit;
 import com.google.gerrit.reviewdb.Patch;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.Patch.ChangeType;
 import com.google.gerrit.reviewdb.Patch.PatchType;
+import com.google.gwtorm.client.Column;
 
-import org.eclipse.jgit.diff.Edit;
-import org.eclipse.jgit.diff.ReplaceEdit;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.patch.CombinedFileHeader;
@@ -50,20 +51,40 @@
 
   static PatchListEntry empty(final String fileName) {
     return new PatchListEntry(ChangeType.MODIFIED, PatchType.UNIFIED, null,
-        fileName, EMPTY_HEADER, Collections.<Edit> emptyList());
+        fileName, EMPTY_HEADER, Collections.<LineEdit> emptyList());
   }
 
-  private final ChangeType changeType;
-  private final PatchType patchType;
-  private final String oldName;
-  private final String newName;
-  private final byte[] header;
-  private final List<Edit> edits;
+  @Column(id = 1)
+  protected char changeTypeCode;
 
-  PatchListEntry(final FileHeader hdr, List<Edit> editList) {
+  @Column(id = 2)
+  protected char patchTypeCode;
+
+  @Column(id = 3)
+  protected String oldName;
+
+  @Column(id = 4)
+  protected String newName;
+
+  @Column(id = 5)
+  protected byte[] header;
+
+  @Column(id = 6)
+  protected List<LineEdit> edits;
+
+  protected ChangeType changeType;
+  protected PatchType patchType;
+
+  protected PatchListEntry(){
+  }
+
+  PatchListEntry(final FileHeader hdr, List<LineEdit> editList) {
     changeType = toChangeType(hdr);
     patchType = toPatchType(hdr);
 
+    changeTypeCode = changeType.getCode();
+    patchTypeCode = patchType.getCode();
+
     switch (changeType) {
       case DELETED:
         oldName = null;
@@ -94,15 +115,17 @@
         || hdr.getNewMode() == FileMode.GITLINK) {
       edits = Collections.emptyList();
     } else {
-      edits = Collections.unmodifiableList(editList);
+      edits = editList;
     }
   }
 
   private PatchListEntry(final ChangeType changeType,
       final PatchType patchType, final String oldName, final String newName,
-      final byte[] header, final List<Edit> edits) {
+      final byte[] header, final List<LineEdit> edits) {
     this.changeType = changeType;
     this.patchType = patchType;
+    changeTypeCode = changeType.getCode();
+    patchTypeCode = patchType.getCode();
     this.oldName = oldName;
     this.newName = newName;
     this.header = header;
@@ -110,10 +133,16 @@
   }
 
   public ChangeType getChangeType() {
+    if (changeType == null) {
+      changeType = ChangeType.forCode(changeTypeCode);
+    }
     return changeType;
   }
 
   public PatchType getPatchType() {
+    if (patchType == null) {
+      patchType = PatchType.forCode(patchTypeCode);
+    }
     return patchType;
   }
 
@@ -125,7 +154,7 @@
     return newName;
   }
 
-  public List<Edit> getEdits() {
+  public List<LineEdit> getEdits() {
     return edits;
   }
 
@@ -149,20 +178,20 @@
   }
 
   void writeTo(final OutputStream out) throws IOException {
-    writeEnum(out, changeType);
-    writeEnum(out, patchType);
+    writeEnum(out, getChangeType());
+    writeEnum(out, getPatchType());
     writeString(out, oldName);
     writeString(out, newName);
     writeBytes(out, header);
 
     writeVarInt32(out, edits.size());
-    for (final Edit e : edits) {
+    for (final LineEdit e : edits) {
       write(out, e);
 
-      if (e instanceof ReplaceEdit) {
-        ReplaceEdit r = (ReplaceEdit) e;
-        writeVarInt32(out, r.getInternalEdits().size());
-        for (Edit i : r.getInternalEdits()) {
+      if (e.getEdits() != null) {
+        List<BaseEdit> intlEdits = e.getEdits();
+        writeVarInt32(out, intlEdits.size());
+        for (BaseEdit i : intlEdits) {
           write(out, i);
         }
       } else {
@@ -171,7 +200,7 @@
     }
   }
 
-  private void write(final OutputStream out, final Edit e) throws IOException {
+  private void write(final OutputStream out, final BaseEdit e) throws IOException {
     writeVarInt32(out, e.getBeginA());
     writeVarInt32(out, e.getEndA());
     writeVarInt32(out, e.getBeginB());
@@ -186,34 +215,34 @@
     final byte[] hdr = readBytes(in);
 
     final int editCount = readVarInt32(in);
-    final Edit[] editArray = new Edit[editCount];
+    final LineEdit[] editArray = new LineEdit[editCount];
     for (int i = 0; i < editCount; i++) {
-      editArray[i] = readEdit(in);
+      BaseEdit self = readEdit(in);
 
       int innerCount = readVarInt32(in);
-      if (0 < innerCount) {
-        Edit[] inner = new Edit[innerCount];
+      if (innerCount == 0) {
+        editArray[i] = new LineEdit(self, null);
+      } else {
+        BaseEdit[] inner = new BaseEdit[innerCount];
         for (int innerIdx = 0; innerIdx < innerCount; innerIdx++) {
           inner[innerIdx] = readEdit(in);
         }
-        editArray[i] = new ReplaceEdit(editArray[i], toList(inner));
+        editArray[i] =
+            new LineEdit(self, Collections.unmodifiableList(Arrays
+                .asList((inner))));
       }
     }
 
     return new PatchListEntry(changeType, patchType, oldName, newName, hdr,
-        toList(editArray));
+        Collections.unmodifiableList(Arrays.asList(editArray)));
   }
 
-  private static List<Edit> toList(Edit[] l) {
-    return Collections.unmodifiableList(Arrays.asList(l));
-  }
-
-  private static Edit readEdit(final InputStream in) throws IOException {
+  private static BaseEdit readEdit(final InputStream in) throws IOException {
     final int beginA = readVarInt32(in);
     final int endA = readVarInt32(in);
     final int beginB = readVarInt32(in);
     final int endB = readVarInt32(in);
-    return  new Edit(beginA, endA, beginB, endB);
+    return new BaseEdit(beginA, endA, beginB, endB);
   }
 
   private static byte[] compact(final FileHeader h) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
index 26ee17b..e9d35bb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -14,55 +14,66 @@
 
 package com.google.gerrit.server.patch;
 
-import static com.google.gerrit.server.ioutil.BasicSerialization.readEnum;
-import static com.google.gerrit.server.ioutil.BasicSerialization.writeEnum;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.readCanBeNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.writeCanBeNull;
-import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
-
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
+import com.google.gwtorm.client.Column;
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.io.Serializable;
-
 import javax.annotation.Nullable;
 
-public class PatchListKey implements Serializable {
-  static final long serialVersionUID = 13L;
+public class PatchListKey {
+  @Column(id = 1)
+  protected String oldIdName;
 
-  private transient ObjectId oldId;
-  private transient ObjectId newId;
-  private transient Whitespace whitespace;
+  @Column(id = 2)
+  protected String newIdName;
+
+  @Column(id = 3)
+  protected char whitespaceCode;
+
+  private volatile ObjectId oldId;
+  private volatile ObjectId newId;
+  private volatile Whitespace whitespace;
 
   transient Project.NameKey projectKey; // not required to form the key
 
+  protected PatchListKey() {
+  }
+
   public PatchListKey(final Project.NameKey pk, final AnyObjectId a,
       final AnyObjectId b, final Whitespace ws) {
     projectKey = pk;
     oldId = a != null ? a.copy() : null;
     newId = b.copy();
+    oldIdName = oldId != null ? oldId.name() : null;
+    newIdName = newId.name();
     whitespace = ws;
+    whitespaceCode = ws.getCode();
   }
 
   /** Old side commit, or null to assume ancestor or combined merge. */
   @Nullable
   public ObjectId getOldId() {
+    if (oldId == null && oldIdName != null) {
+      oldId = ObjectId.fromString(oldIdName);
+    }
     return oldId;
   }
 
   /** New side commit name. */
   public ObjectId getNewId() {
+    if (newId == null) {
+      newId = ObjectId.fromString(newIdName);
+    }
     return newId;
   }
 
   public Whitespace getWhitespace() {
+    if (whitespace == null) {
+      whitespace = Whitespace.forCode(whitespaceCode);
+    }
     return whitespace;
   }
 
@@ -70,12 +81,12 @@
   public int hashCode() {
     int h = 0;
 
-    if (oldId != null) {
-      h = h * 31 + oldId.hashCode();
+    if (oldIdName != null) {
+      h = h * 31 + getOldId().hashCode();
     }
 
-    h = h * 31 + newId.hashCode();
-    h = h * 31 + whitespace.name().hashCode();
+    h = h * 31 + getNewId().hashCode();
+    h = h * 31 + whitespaceCode;
 
     return h;
   }
@@ -84,9 +95,10 @@
   public boolean equals(final Object o) {
     if (o instanceof PatchListKey) {
       final PatchListKey k = (PatchListKey) o;
-      return eq(oldId, k.oldId) //
-          && eq(newId, k.newId) //
-          && whitespace == k.whitespace;
+      return oldIdName != null ? oldIdName.equals(k.oldIdName)
+          : k.oldIdName == null //
+              && newIdName.equals(k.newIdName) //
+              && getWhitespace() == k.getWhitespace();
     }
     return false;
   }
@@ -99,31 +111,12 @@
       n.append(projectKey.get());
       n.append(" ");
     }
-    n.append(oldId != null ? oldId.name() : "BASE");
+    n.append(oldIdName != null ? oldIdName : "BASE");
     n.append("..");
-    n.append(newId.name());
+    n.append(newIdName);
     n.append(" ");
-    n.append(whitespace.name());
+    n.append(getWhitespace().name());
     n.append("]");
     return n.toString();
   }
-
-  private static boolean eq(final ObjectId a, final ObjectId b) {
-    if (a == null && b == null) {
-      return true;
-    }
-    return a != null && b != null && AnyObjectId.equals(a, b);
-  }
-
-  private void writeObject(final ObjectOutputStream out) throws IOException {
-    writeCanBeNull(out, oldId);
-    writeNotNull(out, newId);
-    writeEnum(out, whitespace);
-  }
-
-  private void readObject(final ObjectInputStream in) throws IOException {
-    oldId = readCanBeNull(in);
-    newId = readNotNull(in);
-    whitespace = readEnum(in, Whitespace.values());
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
index 9617172..c111ba7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.patch;
 
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
@@ -21,8 +24,9 @@
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.UserIdentity;
-import com.google.gerrit.server.account.AccountByEmailCache;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
@@ -37,6 +41,7 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Set;
+import java.util.concurrent.Future;
 
 
 /**
@@ -46,72 +51,80 @@
 public class PatchSetInfoFactory {
   private final GitRepositoryManager repoManager;
   private final SchemaFactory<ReviewDb> schemaFactory;
-  private final AccountByEmailCache byEmailCache;
+  private final AccountCache accountCache;
 
   @Inject
   public PatchSetInfoFactory(final GitRepositoryManager grm,
       final SchemaFactory<ReviewDb> schemaFactory,
-      final AccountByEmailCache byEmailCache) {
+      final AccountCache accountCache) {
     this.repoManager = grm;
     this.schemaFactory = schemaFactory;
-    this.byEmailCache = byEmailCache;
+    this.accountCache = accountCache;
   }
 
   public PatchSetInfo get(RevCommit src, PatchSet.Id psi) {
+    Future<UserIdentity> author = toUserIdentity(src.getAuthorIdent());
+    Future<UserIdentity> committer = toUserIdentity(src.getCommitterIdent());
+
     PatchSetInfo info = new PatchSetInfo(psi);
     info.setSubject(src.getShortMessage());
     info.setMessage(src.getFullMessage());
-    info.setAuthor(toUserIdentity(src.getAuthorIdent()));
-    info.setCommitter(toUserIdentity(src.getCommitterIdent()));
-
+    info.setAuthor(FutureUtil.get(author));
+    info.setCommitter(FutureUtil.get(committer));
     return info;
   }
 
   public PatchSetInfo get(PatchSet.Id patchSetId)
       throws PatchSetInfoNotAvailableException {
-    ReviewDb db = null;
-    Repository repo = null;
+    final RevCommit src;
+
     try {
-      db = schemaFactory.open();
-      final PatchSet patchSet = db.patchSets().get(patchSetId);
-      final Change change = db.changes().get(patchSet.getId().getParentKey());
-      final Project.NameKey projectKey = change.getProject();
-      final String projectName = projectKey.get();
-      repo = repoManager.openRepository(projectName);
-      final RevWalk rw = new RevWalk(repo);
-      final RevCommit src =
-          rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
-      return get(src, patchSetId);
-    } catch (OrmException e) {
-      throw new PatchSetInfoNotAvailableException(e);
-    } catch (IOException e) {
-      throw new PatchSetInfoNotAvailableException(e);
-    } finally {
-      if (db != null) {
+      final ReviewDb db = schemaFactory.open();
+      try {
+        final PatchSet patchSet = db.patchSets().get(patchSetId);
+        final String revId = patchSet.getRevision().get();
+        final Change change = db.changes().get(patchSet.getId().getParentKey());
+        final Project.NameKey projectKey = change.getProject();
+        try {
+          final Repository repo = repoManager.openRepository(projectKey.get());
+          try {
+            final RevWalk rw = new RevWalk(repo);
+            src = rw.parseCommit(ObjectId.fromString(revId));
+          } finally {
+            repo.close();
+          }
+        } catch (IOException e) {
+          throw new PatchSetInfoNotAvailableException(e);
+        }
+      } finally {
         db.close();
       }
-      if (repo != null) {
-        repo.close();
-      }
-    }
-  }
-
-  private UserIdentity toUserIdentity(final PersonIdent who) {
-    final UserIdentity u = new UserIdentity();
-    u.setName(who.getName());
-    u.setEmail(who.getEmailAddress());
-    u.setDate(new Timestamp(who.getWhen().getTime()));
-    u.setTimeZone(who.getTimeZoneOffset());
-
-    // If only one account has access to this email address, select it
-    // as the identity of the user.
-    //
-    final Set<Account.Id> a = byEmailCache.get(u.getEmail());
-    if (a.size() == 1) {
-      u.setAccount(a.iterator().next());
+    } catch (OrmException e) {
+      throw new PatchSetInfoNotAvailableException(e);
     }
 
-    return u;
+    return get(src, patchSetId);
   }
 
+  private ListenableFuture<UserIdentity> toUserIdentity(final PersonIdent who) {
+    return Futures.compose(accountCache.byEmail(who.getEmailAddress()),
+        new Function<Set<Account.Id>, UserIdentity>() {
+          @Override
+          public UserIdentity apply(Set<Account.Id> from) {
+            final UserIdentity u = new UserIdentity();
+            u.setName(who.getName());
+            u.setEmail(who.getEmailAddress());
+            u.setDate(new Timestamp(who.getWhen().getTime()));
+            u.setTimeZone(who.getTimeZoneOffset());
+
+            if (from != null && from.size() == 1) {
+              // If only one account has access to this email address, select it
+              // as the identity of the user.
+              //
+              u.setAccount(from.iterator().next());
+            }
+            return u;
+          }
+        });
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
index f3e2890..d1f883c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
@@ -274,7 +274,7 @@
   }
 
   private List<PatchLineComment> drafts() throws OrmException {
-    return db.patchComments().draft(patchSetId, user.getAccountId()).toList();
+    return db.patchComments().draftByPatchSet(patchSetId, user.getAccountId()).toList();
   }
 
   private void email() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
index 35b5ee5..748d87b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.reviewdb.Project;
 
 /** Cache of project information, including access rights. */
@@ -24,11 +25,11 @@
    * @param projectName name of the project.
    * @return the cached data; null if no such project exists.
    */
-  public ProjectState get(Project.NameKey projectName);
+  public ListenableFuture<ProjectState> get(Project.NameKey projectName);
 
   /** Invalidate the cached information about the given project. */
-  public void evict(Project p);
+  public ListenableFuture<Void> evictAsync(Project p);
 
   /** Invalidate the cached information about all projects. */
-  public void evictAll();
+  public ListenableFuture<Void> evictAllAsync();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 48eef87..2099432 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.RefRight;
 import com.google.gerrit.reviewdb.ReviewDb;
@@ -23,6 +25,7 @@
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
@@ -41,7 +44,7 @@
       protected void configure() {
         final TypeLiteral<Cache<Project.NameKey, ProjectState>> type =
             new TypeLiteral<Cache<Project.NameKey, ProjectState>>() {};
-        core(type, CACHE_NAME).populateWith(Loader.class);
+        cache(type, CACHE_NAME).populateWith(Loader.class);
         bind(ProjectCacheImpl.class);
         bind(ProjectCache.class).to(ProjectCacheImpl.class);
       }
@@ -62,29 +65,30 @@
    * @param projectName name of the project.
    * @return the cached data; null if no such project exists.
    */
-  public ProjectState get(final Project.NameKey projectName) {
+  public ListenableFuture<ProjectState> get(Project.NameKey projectName) {
     return byName.get(projectName);
   }
 
   /** Invalidate the cached information about the given project. */
-  public void evict(final Project p) {
+  public ListenableFuture<Void> evictAsync(final Project p) {
     if (p != null) {
-      byName.remove(p.getNameKey());
+      return byName.removeAsync(p.getNameKey());
     }
+    return Futures.immediateFuture(null);
   }
 
   /** Invalidate the cached information about all projects. */
-  public void evictAll() {
-    byName.removeAll();
+  public ListenableFuture<Void> evictAllAsync() {
+    return byName.removeAllAsync();
   }
 
   static class Loader extends EntryCreator<Project.NameKey, ProjectState> {
-    private final ProjectState.Factory projectStateFactory;
+    private final Provider<ProjectState> projectStateProvider;
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
-    Loader(ProjectState.Factory psf, SchemaFactory<ReviewDb> sf) {
-      projectStateFactory = psf;
+    Loader(Provider<ProjectState> psp, SchemaFactory<ReviewDb> sf) {
+      projectStateProvider = psp;
       schema = sf;
     }
 
@@ -101,7 +105,7 @@
             Collections.unmodifiableCollection(db.refRights().byProject(
                 p.getNameKey()).toList());
 
-        return projectStateFactory.create(p, rights);
+        return projectStateProvider.get().init(p, rights);
       } finally {
         db.close();
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 67f543b..502baf7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.reviewdb.RefRight;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.ReplicationUser;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -43,7 +44,7 @@
 
     public ProjectControl controlFor(Project.NameKey nameKey, CurrentUser user)
         throws NoSuchProjectException {
-      final ProjectState p = projectCache.get(nameKey);
+      final ProjectState p = FutureUtil.getOrNull(projectCache.get(nameKey));
       if (p == null) {
         throw new NoSuchProjectException(nameKey);
       }
@@ -63,7 +64,7 @@
 
     public ProjectControl controlFor(final Project.NameKey nameKey)
         throws NoSuchProjectException {
-      final ProjectState p = projectCache.get(nameKey);
+      final ProjectState p = FutureUtil.getOrNull(projectCache.get(nameKey));
       if (p == null) {
         throw new NoSuchProjectException(nameKey);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 00e92b6..09a345f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -21,8 +21,9 @@
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.WildProjectName;
+import com.google.gerrit.server.util.FutureUtil;
+import com.google.gwtorm.client.Column;
 import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -33,30 +34,31 @@
 
 /** Cached information on a project. */
 public class ProjectState {
-  public interface Factory {
-    ProjectState create(Project project, Collection<RefRight> localRights);
-  }
-
   private final AnonymousUser anonymousUser;
   private final Project.NameKey wildProject;
   private final ProjectCache projectCache;
 
-  private final Project project;
-  private final Collection<RefRight> localRights;
-  private final Set<AccountGroup.Id> owners;
+  @Column(id = 1)
+  protected Project project;
+
+  @Column(id = 2)
+  protected Collection<RefRight> localRights;
+
+  @Column(id = 3)
+  protected Set<AccountGroup.Id> owners;
 
   private volatile Collection<RefRight> inheritedRights;
 
   @Inject
   protected ProjectState(final AnonymousUser anonymousUser,
       final ProjectCache projectCache,
-      @WildProjectName final Project.NameKey wildProject,
-      @Assisted final Project project,
-      @Assisted final Collection<RefRight> rights) {
+      @WildProjectName final Project.NameKey wildProject) {
     this.anonymousUser = anonymousUser;
     this.projectCache = projectCache;
     this.wildProject = wildProject;
+  }
 
+  protected ProjectState init(final Project project, final Collection<RefRight> rights) {
     this.project = project;
     this.localRights = rights;
 
@@ -68,6 +70,7 @@
       }
     }
     owners = Collections.unmodifiableSet(groups);
+    return this;
   }
 
   public Project getProject() {
@@ -107,7 +110,7 @@
     Project.NameKey parent = project.getParent();
 
     while (parent != null && seen.add(parent)) {
-      ProjectState s = projectCache.get(parent);
+      ProjectState s = FutureUtil.get(projectCache.get(parent));
       if (s != null) {
         inherited.addAll(s.getLocalRights());
         parent = s.getProject().getParent();
@@ -125,7 +128,7 @@
   }
 
   private Collection<RefRight> getWildProjectRights() {
-    final ProjectState s = projectCache.get(wildProject);
+    ProjectState s = FutureUtil.get(projectCache.get(wildProject));
     return s != null ? s.getLocalRights() : Collections.<RefRight> emptyList();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
index 18a0c82..69344a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
@@ -21,10 +21,14 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Provider;
 
-class AgePredicate extends OperatorPredicate<ChangeData> {
+import java.util.EnumSet;
+
+class AgePredicate extends OperatorPredicate<ChangeData> implements
+    Prefetchable {
   private final Provider<ReviewDb> dbProvider;
   private final long cut;
 
@@ -51,4 +55,9 @@
   public int getCost() {
     return 1;
   }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    return EnumSet.of(NeededData.CHANGE);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
index e74b390..3af9061 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
@@ -14,19 +14,32 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.server.query.AndPredicate;
+import com.google.common.collect.Maps;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.ResultSet;
 import com.google.gwtorm.client.impl.ListResultSet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.EnumSet;
 import java.util.List;
+import java.util.Map;
 
-class AndSource extends AndPredicate<ChangeData> implements ChangeDataSource {
+class AndSource extends PrefetchableAndPredicate implements ChangeDataSource {
+  interface Factory {
+    AndSource create(Collection<? extends Predicate<ChangeData>> that);
+  }
+
   private static final Comparator<Predicate<ChangeData>> CMP =
       new Comparator<Predicate<ChangeData>>() {
         @Override
@@ -68,9 +81,15 @@
   }
 
   private int cardinality = -1;
+  private final Provider<ReviewDb> dbProvider;
+  private final ProjectCache projectCache;
 
-  AndSource(final Collection<? extends Predicate<ChangeData>> that) {
+  @Inject
+  AndSource(Provider<ReviewDb> dbProvider, ProjectCache projectCache,
+      @Assisted final Collection<? extends Predicate<ChangeData>> that) {
     super(sort(that));
+    this.dbProvider = dbProvider;
+    this.projectCache = projectCache;
   }
 
   @Override
@@ -91,17 +110,17 @@
     ArrayList<ChangeData> r = new ArrayList<ChangeData>();
     ChangeData last = null;
     boolean skipped = false;
-    for (ChangeData data : source.read()) {
-      if (match(data)) {
-        r.add(data);
+    for (ChangeData cd : prefetchData(source.read())) {
+      if (match(cd)) {
+        r.add(cd);
       } else {
         skipped = true;
       }
-      last = data;
+      last = cd;
     }
 
     if (skipped && last != null && source instanceof Paginated) {
-      // If we our source is a paginated source and we skipped at
+      // If our source is a paginated source and we skipped at
       // least one of its results, we may not have filled the full
       // limit the caller wants.  Restart the source and continue.
       //
@@ -110,13 +129,13 @@
         ChangeData lastBeforeRestart = last;
         skipped = false;
         last = null;
-        for (ChangeData data : p.restart(lastBeforeRestart)) {
-          if (match(data)) {
-            r.add(data);
+        for (ChangeData cd : prefetchData(p.restart(lastBeforeRestart))) {
+          if (match(cd)) {
+            r.add(cd);
           } else {
             skipped = true;
           }
-          last = data;
+          last = cd;
         }
       }
     }
@@ -133,6 +152,37 @@
     return null;
   }
 
+  private Collection<ChangeData> prefetchData(ResultSet<ChangeData> resultSet) throws OrmException {
+    final List<ChangeData> data = resultSet.toList();
+    final EnumSet<NeededData> needed = getNeededData();
+
+    if (needed.contains(NeededData.PROJECT_STATE)) {
+      needed.add(NeededData.CHANGE);
+    }
+
+    if (needed.contains(NeededData.CHANGE)) {
+      Map<Change.Id, ChangeData> need = Maps.newHashMap();
+      for (ChangeData cd : data) {
+        if (!cd.hasChange()) {
+          need.put(cd.getId(), cd);
+        }
+      }
+      if (!need.isEmpty()) {
+        for (Change c : dbProvider.get().changes().get(need.keySet())) {
+          need.get(c.getId()).setChange(c);
+        }
+      }
+    }
+
+    if (needed.contains(NeededData.PROJECT_STATE)) {
+      for (ChangeData cd : data) {
+        cd.initProjectState(dbProvider, projectCache);
+      }
+    }
+
+    return data;
+  }
+
   @Override
   public int getCardinality() {
     if (cardinality < 0) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java
index ae48fdc..7c3a9d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java
@@ -18,10 +18,14 @@
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Provider;
 
-class BranchPredicate extends OperatorPredicate<ChangeData> {
+import java.util.EnumSet;
+
+class BranchPredicate extends OperatorPredicate<ChangeData> implements
+    Prefetchable {
   private final Provider<ReviewDb> dbProvider;
   private final String shortName;
 
@@ -44,4 +48,9 @@
   public int getCost() {
     return 1;
   }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    return EnumSet.of(NeededData.CHANGE);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 479a5ef..7ef137d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -24,6 +24,9 @@
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Provider;
 
@@ -31,8 +34,13 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.Future;
 
 public class ChangeData {
+  public enum NeededData {
+    CHANGE, PATCHES, APPROVALS, COMMENTS, TRACKING_IDS, PROJECT_STATE
+  }
+
   private final Change.Id legacyId;
   private Change change;
   private Collection<PatchSet> patches;
@@ -42,6 +50,7 @@
   private Collection<PatchLineComment> comments;
   private Collection<TrackingId> trackingIds;
   private CurrentUser visibleTo;
+  private Future<ProjectState> projectState;
 
   public ChangeData(final Change.Id id) {
     legacyId = id;
@@ -68,7 +77,7 @@
         return null;
       }
 
-      PatchList p = cache.get(c, ps);
+      PatchList p = FutureUtil.get(cache.get(c, ps));
       List<String> r = new ArrayList<String>(p.getPatches().size());
       for (PatchListEntry e : p.getPatches()) {
         switch (e.getChangeType()) {
@@ -186,4 +195,37 @@
     }
     return trackingIds;
   }
+
+  public ProjectState projectState(Provider<ReviewDb> db,
+      ProjectCache projectCache) throws OrmException {
+    initProjectState(db, projectCache);
+    return FutureUtil.get(projectState);
+  }
+
+  void initProjectState(Provider<ReviewDb> db, ProjectCache projectCache)
+      throws OrmException {
+    if (projectState == null) {
+      projectState = projectCache.get(change(db).getProject());
+    }
+  }
+
+  void setChange(Change change) {
+    this.change = change;
+  }
+
+  void setPatches(Collection<PatchSet> patches) {
+    this.patches = patches;
+  }
+
+  void setApprovals(Collection<PatchSetApproval> approvals) {
+    this.approvals = approvals;
+  }
+
+  void setComments(Collection<PatchLineComment> comments) {
+    this.comments = comments;
+  }
+
+  void setTrackingIds(Collection<TrackingId> trackingIds) {
+    this.trackingIds = trackingIds;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
index 83107bb..2cd3e95 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
@@ -17,12 +17,15 @@
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.ResultSet;
 import com.google.inject.Provider;
 
+import java.util.EnumSet;
+
 class ChangeIdPredicate extends OperatorPredicate<ChangeData> implements
-    ChangeDataSource {
+    ChangeDataSource, Prefetchable {
   private final Provider<ReviewDb> dbProvider;
 
   ChangeIdPredicate(Provider<ReviewDb> dbProvider, String id) {
@@ -66,4 +69,9 @@
   public int getCardinality() {
     return ChangeCosts.CARD_KEY;
   }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    return EnumSet.of(NeededData.CHANGE);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index eccb2e8..62d2a27 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -29,10 +29,12 @@
 import com.google.gerrit.server.config.WildProjectName;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.IntPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryBuilder;
 import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -102,6 +104,7 @@
     final ApprovalTypes approvalTypes;
     final Project.NameKey wildProjectName;
     final PatchListCache patchListCache;
+    final ProjectCache projectCache;
 
     @Inject
     Arguments(Provider<ReviewDb> dbProvider,
@@ -112,7 +115,7 @@
         AccountResolver accountResolver, GroupCache groupCache,
         AuthConfig authConfig, ApprovalTypes approvalTypes,
         @WildProjectName Project.NameKey wildProjectName,
-        PatchListCache patchListCache) {
+        PatchListCache patchListCache, ProjectCache projectCache) {
       this.dbProvider = dbProvider;
       this.rewriter = rewriter;
       this.userFactory = userFactory;
@@ -124,6 +127,7 @@
       this.approvalTypes = approvalTypes;
       this.wildProjectName = wildProjectName;
       this.patchListCache = patchListCache;
+      this.projectCache = projectCache;
     }
   }
 
@@ -272,7 +276,8 @@
   @Operator
   public Predicate<ChangeData> label(String name) {
     return new LabelPredicate(args.changeControlGenericFactory,
-        args.userFactory, args.dbProvider, args.approvalTypes, name);
+        args.userFactory, args.dbProvider, args.approvalTypes,
+        args.projectCache, name);
   }
 
   @Operator
@@ -318,13 +323,14 @@
 
     // If its not an account, maybe its a group?
     //
-    AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(who));
+    AccountGroup g =
+        FutureUtil.get(args.groupCache.get(new AccountGroup.NameKey(who)));
     if (g != null) {
       return visibleto(new SingleGroupUser(args.authConfig, g.getId()));
     }
 
-    Collection<AccountGroup> matches =
-        args.groupCache.get(new AccountGroup.ExternalNameKey(who));
+    Collection<AccountGroup> matches = FutureUtil.get( //
+        args.groupCache.get(new AccountGroup.ExternalNameKey(who))).getGroups();
     if (matches != null && !matches.isEmpty()) {
       HashSet<AccountGroup.Id> ids = new HashSet<AccountGroup.Id>();
       for (AccountGroup group : matches) {
@@ -337,9 +343,7 @@
   }
 
   public Predicate<ChangeData> visibleto(CurrentUser user) {
-    return new IsVisibleToPredicate(args.dbProvider, //
-        args.changeControlGenericFactory, //
-        user);
+    return new IsVisibleToPredicate(args.dbProvider, args.projectCache, user);
   }
 
   public Predicate<ChangeData> is_visible() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryModule.java
new file mode 100644
index 0000000..3bc5d4a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryModule.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2010 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.query.change;
+
+import com.google.gerrit.server.config.FactoryModule;
+
+/** Module for package-private predicates. */
+public class ChangeQueryModule extends FactoryModule {
+  @Override
+  protected void configure() {
+    factory(AndSource.Factory.class);
+    bind(ChangeQueryRewriter.class);
+    factory(ChangeQueryBuilder.Factory.class);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
index 904bc3c..f793d99 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
@@ -38,24 +38,31 @@
               new ChangeQueryBuilder.Arguments( //
                   new InvalidProvider<ReviewDb>(), //
                   new InvalidProvider<ChangeQueryRewriter>(), //
-                  null, null, null, null, null, null, null, null, null), null));
+                  null, null, null, null, null, null, null, null, null, null), null));
 
   private final Provider<ReviewDb> dbProvider;
+  private final AndSource.Factory andSourceFactory;
 
   @Inject
-  ChangeQueryRewriter(Provider<ReviewDb> dbProvider) {
+  ChangeQueryRewriter(Provider<ReviewDb> dbProvider, AndSource.Factory andSourceFactory) {
     super(mydef);
     this.dbProvider = dbProvider;
+    this.andSourceFactory = andSourceFactory;
   }
 
   @Override
   public Predicate<ChangeData> and(Collection<? extends Predicate<ChangeData>> l) {
-    return hasSource(l) ? new AndSource(l) : super.and(l);
+    return hasSource(l) ? andSourceFactory.create(l) : new PrefetchableAndPredicate(l);
   }
 
   @Override
   public Predicate<ChangeData> or(Collection<? extends Predicate<ChangeData>> l) {
-    return hasSource(l) ? new OrSource(l) : super.or(l);
+    return hasSource(l) ? new OrSource(l) : new PrefetchableOrPredicate(l);
+  }
+
+  @Override
+  public Predicate<ChangeData> not(Predicate<ChangeData> that) {
+    return new PrefetchableNotPredicate(that);
   }
 
   @Rewrite("-status:open")
@@ -125,7 +132,7 @@
       @Named("P") final ProjectPredicate p,
       @Named("S") final SortKeyPredicate.After s,
       @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(500, s.getValue(), l.intValue()) {
+    return new PaginatedSource(500, s, l.intValue()) {
       @Override
       ResultSet<Change> scan(ChangeAccess a, String key, int limit)
           throws OrmException {
@@ -146,7 +153,7 @@
       @Named("P") final ProjectPredicate p,
       @Named("S") final SortKeyPredicate.Before s,
       @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(500, s.getValue(), l.intValue()) {
+    return new PaginatedSource(500, s, l.intValue()) {
       @Override
       ResultSet<Change> scan(ChangeAccess a, String key, int limit)
           throws OrmException {
@@ -167,7 +174,7 @@
       @Named("P") final ProjectPredicate p,
       @Named("S") final SortKeyPredicate.After s,
       @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+    return new PaginatedSource(40000, s, l.intValue()) {
       @Override
       ResultSet<Change> scan(ChangeAccess a, String key, int limit)
           throws OrmException {
@@ -189,7 +196,7 @@
       @Named("P") final ProjectPredicate p,
       @Named("S") final SortKeyPredicate.Before s,
       @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+    return new PaginatedSource(40000, s, l.intValue()) {
       @Override
       ResultSet<Change> scan(ChangeAccess a, String key, int limit)
           throws OrmException {
@@ -211,7 +218,7 @@
       @Named("P") final ProjectPredicate p,
       @Named("S") final SortKeyPredicate.After s,
       @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+    return new PaginatedSource(40000, s, l.intValue()) {
       @Override
       ResultSet<Change> scan(ChangeAccess a, String key, int limit)
           throws OrmException {
@@ -233,7 +240,7 @@
       @Named("P") final ProjectPredicate p,
       @Named("S") final SortKeyPredicate.Before s,
       @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+    return new PaginatedSource(40000, s, l.intValue()) {
       @Override
       ResultSet<Change> scan(ChangeAccess a, String key, int limit)
           throws OrmException {
@@ -254,7 +261,7 @@
   public Predicate<ChangeData> r20_byOpenPrev(
       @Named("S") final SortKeyPredicate.After s,
       @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(2000, s.getValue(), l.intValue()) {
+    return new PaginatedSource(2000, s, l.intValue()) {
       @Override
       ResultSet<Change> scan(ChangeAccess a, String key, int limit)
           throws OrmException {
@@ -272,7 +279,7 @@
   public Predicate<ChangeData> r20_byOpenNext(
       @Named("S") final SortKeyPredicate.Before s,
       @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(2000, s.getValue(), l.intValue()) {
+    return new PaginatedSource(2000, s, l.intValue()) {
       @Override
       ResultSet<Change> scan(ChangeAccess a, String key, int limit)
           throws OrmException {
@@ -291,7 +298,7 @@
   public Predicate<ChangeData> r20_byMergedPrev(
       @Named("S") final SortKeyPredicate.After s,
       @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+    return new PaginatedSource(50000, s, l.intValue()) {
       {
         init("r20_byMergedPrev", s, l);
       }
@@ -315,7 +322,7 @@
   public Predicate<ChangeData> r20_byMergedNext(
       @Named("S") final SortKeyPredicate.Before s,
       @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+    return new PaginatedSource(50000, s, l.intValue()) {
       {
         init("r20_byMergedNext", s, l);
       }
@@ -339,7 +346,7 @@
   public Predicate<ChangeData> r20_byAbandonedPrev(
       @Named("S") final SortKeyPredicate.After s,
       @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+    return new PaginatedSource(50000, s, l.intValue()) {
       {
         init("r20_byAbandonedPrev", s, l);
       }
@@ -363,7 +370,7 @@
   public Predicate<ChangeData> r20_byAbandonedNext(
       @Named("S") final SortKeyPredicate.Before s,
       @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+    return new PaginatedSource(50000, s, l.intValue()) {
       {
         init("r20_byAbandonedNext", s, l);
       }
@@ -601,12 +608,12 @@
 
   private abstract class PaginatedSource extends ChangeSource implements
       Paginated {
-    private final String startKey;
+    private final SortKeyPredicate skp;
     private final int limit;
 
-    PaginatedSource(int card, String start, int lim) {
+    PaginatedSource(int card, SortKeyPredicate skp, int lim) {
       super(card);
-      this.startKey = start;
+      this.skp = skp;
       this.limit = lim;
     }
 
@@ -617,13 +624,13 @@
 
     @Override
     ResultSet<Change> scan(ChangeAccess a) throws OrmException {
-      return scan(a, startKey, limit);
+      return scan(a, skp.getValue(), limit);
     }
 
     @Override
     public ResultSet<ChangeData> restart(ChangeData last) throws OrmException {
       return ChangeDataResultSet.change(scan(dbProvider.get().changes(), //
-          last.change(dbProvider).getSortKey(), //
+          skp.nextKey(last.change(dbProvider)), //
           limit));
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index 4ae2278..3dfdb96 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -18,11 +18,13 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
 import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Provider;
 
 import java.util.ArrayList;
 import java.util.EnumMap;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -35,7 +37,8 @@
  * status:} but may also be {@code is:} to help do-what-i-meanery for end-users
  * searching for changes. Either operator name has the same meaning.
  */
-final class ChangeStatusPredicate extends OperatorPredicate<ChangeData> {
+final class ChangeStatusPredicate extends OperatorPredicate<ChangeData>
+    implements Prefetchable {
   private static final Map<String, Change.Status> byName;
   private static final EnumMap<Change.Status, String> byEnum;
 
@@ -106,6 +109,11 @@
   }
 
   @Override
+  public EnumSet<NeededData> getNeededData() {
+    return EnumSet.of(NeededData.CHANGE);
+  }
+
+  @Override
   public int hashCode() {
     return status.hashCode();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
index c03cddc..16b702a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.reviewdb.RevId;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.query.ObjectIdPredicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.ResultSet;
 import com.google.inject.Provider;
@@ -25,8 +26,10 @@
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 
+import java.util.EnumSet;
+
 class CommitPredicate extends ObjectIdPredicate<ChangeData> implements
-    ChangeDataSource {
+    ChangeDataSource, Prefetchable {
   private final Provider<ReviewDb> dbProvider;
 
   CommitPredicate(Provider<ReviewDb> dbProvider, AbbreviatedObjectId id) {
@@ -74,4 +77,9 @@
   public int getCost() {
     return ChangeCosts.cost(ChangeCosts.PATCH_SETS_SCAN, getCardinality());
   }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    return EnumSet.of(NeededData.PATCHES);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
index 07d4dd2..820345f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
@@ -19,16 +19,18 @@
 import com.google.gerrit.reviewdb.PatchLineComment;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.ResultSet;
 import com.google.gwtorm.client.impl.ListResultSet;
 import com.google.inject.Provider;
 
 import java.util.ArrayList;
+import java.util.EnumSet;
 import java.util.HashSet;
 
 class HasDraftByPredicate extends OperatorPredicate<ChangeData> implements
-    ChangeDataSource {
+    ChangeDataSource, Prefetchable {
   private final Provider<ReviewDb> db;
   private final Account.Id accountId;
 
@@ -78,4 +80,9 @@
   public int getCost() {
     return ChangeCosts.cost(ChangeCosts.PATCH_SETS_SCAN, getCardinality());
   }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    return EnumSet.of(NeededData.COMMENTS);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
index 46c7741..7003b69 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
@@ -19,10 +19,14 @@
 import com.google.gerrit.reviewdb.PatchSetApproval;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Provider;
 
-class IsReviewedPredicate extends OperatorPredicate<ChangeData> {
+import java.util.EnumSet;
+
+class IsReviewedPredicate extends OperatorPredicate<ChangeData> implements
+    Prefetchable {
   private final Provider<ReviewDb> dbProvider;
 
   IsReviewedPredicate(Provider<ReviewDb> dbProvider) {
@@ -51,4 +55,9 @@
   public int getCost() {
     return 2;
   }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    return EnumSet.of(NeededData.APPROVALS, NeededData.CHANGE);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
index 1e9d405..8bfe885 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
@@ -18,13 +18,16 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Provider;
 
-class IsVisibleToPredicate extends OperatorPredicate<ChangeData> {
+import java.util.EnumSet;
+
+class IsVisibleToPredicate extends OperatorPredicate<ChangeData> implements
+    Prefetchable {
   private static String describe(CurrentUser user) {
     if (user instanceof IdentifiedUser) {
       return ((IdentifiedUser) user).getAccountId().toString();
@@ -37,14 +40,14 @@
   }
 
   private final Provider<ReviewDb> db;
-  private final ChangeControl.GenericFactory changeControl;
   private final CurrentUser user;
+  private final ProjectCache projectCache;
 
-  IsVisibleToPredicate(Provider<ReviewDb> db,
-      ChangeControl.GenericFactory changeControlFactory, CurrentUser user) {
+  IsVisibleToPredicate(Provider<ReviewDb> db, ProjectCache projectCache,
+      CurrentUser user) {
     super(ChangeQueryBuilder.FIELD_VISIBLETO, describe(user));
     this.db = db;
-    this.changeControl = changeControlFactory;
+    this.projectCache = projectCache;
     this.user = user;
   }
 
@@ -53,15 +56,12 @@
     if (cd.fastIsVisibleTo(user)) {
       return true;
     }
-    try {
-      Change c = cd.change(db);
-      if (c != null && changeControl.controlFor(c, user).isVisible()) {
-        cd.cacheVisibleTo(user);
-        return true;
-      } else {
-        return false;
-      }
-    } catch (NoSuchChangeException e) {
+    Change c = cd.change(db);
+    if (c != null
+        && cd.projectState(db, projectCache).controlFor(user).controlFor(c).isVisible()) {
+      cd.cacheVisibleTo(user);
+      return true;
+    } else {
       return false;
     }
   }
@@ -70,4 +70,9 @@
   public int getCost() {
     return 1;
   }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    return EnumSet.of(NeededData.PROJECT_STATE, NeededData.CHANGE);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index 870be73..f9e2200 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -17,19 +17,23 @@
 import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.Project.NameKey;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.query.OperatorPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 
 import java.util.ArrayList;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-class IsWatchedByPredicate extends OperatorPredicate<ChangeData> {
+class IsWatchedByPredicate extends OperatorPredicate<ChangeData> implements
+    Prefetchable {
   private static String describe(CurrentUser user) {
     if (user instanceof IdentifiedUser) {
       return ((IdentifiedUser) user).getAccountId().toString();
@@ -50,22 +54,7 @@
 
   @Override
   public boolean match(final ChangeData cd) throws OrmException {
-    if (rules == null) {
-      ChangeQueryBuilder builder = new ChangeQueryBuilder(args, user);
-      rules = new HashMap<Project.NameKey, List<Predicate<ChangeData>>>();
-      for (AccountProjectWatch w : user.getNotificationFilters()) {
-        List<Predicate<ChangeData>> list = rules.get(w.getProjectNameKey());
-        if (list == null) {
-          list = new ArrayList<Predicate<ChangeData>>(4);
-          rules.put(w.getProjectNameKey(), list);
-        }
-
-        Predicate<ChangeData> p = compile(builder, w);
-        if (p != null) {
-          list.add(p);
-        }
-      }
-    }
+    Map<Project.NameKey, List<Predicate<ChangeData>>> rules = getRules();
 
     if (rules.isEmpty()) {
       return false;
@@ -91,6 +80,26 @@
     return false;
   }
 
+  private Map<Project.NameKey, List<Predicate<ChangeData>>> getRules() {
+    if (rules == null) {
+      rules = new HashMap<Project.NameKey, List<Predicate<ChangeData>>>();
+      ChangeQueryBuilder builder = new ChangeQueryBuilder(args, user);
+      for (AccountProjectWatch w : user.getNotificationFilters()) {
+        List<Predicate<ChangeData>> list = rules.get(w.getProjectNameKey());
+        if (list == null) {
+          list = new ArrayList<Predicate<ChangeData>>(4);
+          rules.put(w.getProjectNameKey(), list);
+        }
+
+        Predicate<ChangeData> p = compile(builder, w);
+        if (p != null) {
+          list.add(p);
+        }
+      }
+    }
+    return rules;
+  }
+
   @SuppressWarnings("unchecked")
   private Predicate<ChangeData> compile(ChangeQueryBuilder builder,
       AccountProjectWatch w) {
@@ -118,4 +127,22 @@
   public int getCost() {
     return 1;
   }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    Map<NameKey, List<Predicate<ChangeData>>> rules = getRules();
+    if (rules.isEmpty()) {
+      return EnumSet.noneOf(NeededData.class);
+    }
+
+    EnumSet<NeededData> needed = EnumSet.of(NeededData.CHANGE);
+    for (List<Predicate<ChangeData>> list : rules.values()) {
+      for (Predicate<ChangeData> p : list) {
+        if (p instanceof Prefetchable) {
+          needed.addAll(((Prefetchable) p).getNeededData());
+        }
+      }
+    }
+    return needed;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
index e76c278..b3d3621 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -17,19 +17,23 @@
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.reviewdb.ApprovalCategory;
+import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSetApproval;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Provider;
 
+import java.util.EnumSet;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-class LabelPredicate extends OperatorPredicate<ChangeData> {
+class LabelPredicate extends OperatorPredicate<ChangeData> implements
+    Prefetchable {
   private static enum Test {
     EQ {
       @Override
@@ -102,17 +106,19 @@
   private final ChangeControl.GenericFactory ccFactory;
   private final IdentifiedUser.GenericFactory userFactory;
   private final Provider<ReviewDb> dbProvider;
+  private final ProjectCache projectCache;
   private final Test test;
   private final ApprovalCategory.Id category;
   private final short expVal;
 
   LabelPredicate(ChangeControl.GenericFactory ccFactory,
       IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
-      ApprovalTypes types, String value) {
+      ApprovalTypes types, ProjectCache projectCache, String value) {
     super(ChangeQueryBuilder.FIELD_LABEL, value);
     this.ccFactory = ccFactory;
     this.userFactory = userFactory;
     this.dbProvider = dbProvider;
+    this.projectCache = projectCache;
 
     Matcher m1 = Pattern.compile("(=|>=|<=)([+-]?\\d+)$").matcher(value);
     Matcher m2 = Pattern.compile("([+-]\\d+)$").matcher(value);
@@ -141,20 +147,18 @@
         if (test.match(psVal, expVal)) {
           // Double check the value is still permitted for the user.
           //
-          try {
-            ChangeControl cc = ccFactory.controlFor(object.change(dbProvider), //
-                userFactory.create(dbProvider, p.getAccountId()));
-            if (!cc.isVisible()) {
-              // The user can't see the change anymore.
-              //
-              continue;
-            }
-            psVal = cc.normalize(category, psVal);
-          } catch (NoSuchChangeException e) {
-            // The project has disappeared.
+          Change c = object.change(dbProvider);
+          IdentifiedUser user =
+              userFactory.create(dbProvider, p.getAccountId());
+          ChangeControl cc =
+              object.projectState(dbProvider, projectCache).controlFor(user)
+                  .controlFor(c);
+          if (!cc.isVisible()) {
+            // The user can't see the change anymore.
             //
             continue;
           }
+          psVal = cc.normalize(category, psVal);
 
           if (test.match(psVal, expVal)) {
             return true;
@@ -169,4 +173,10 @@
   public int getCost() {
     return 2;
   }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    return EnumSet.of(NeededData.APPROVALS, NeededData.CHANGE,
+        NeededData.PROJECT_STATE);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
index 617a14a..756fe90 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.server.query.OrPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.ResultSet;
@@ -25,7 +24,7 @@
 import java.util.Collection;
 import java.util.HashSet;
 
-class OrSource extends OrPredicate<ChangeData> implements ChangeDataSource {
+class OrSource extends PrefetchableOrPredicate implements ChangeDataSource {
   private int cardinality = -1;
 
   OrSource(final Collection<? extends Predicate<ChangeData>> that) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
index 224dce9..55fb685 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
@@ -18,10 +18,14 @@
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Provider;
 
-class OwnerPredicate extends OperatorPredicate<ChangeData> {
+import java.util.EnumSet;
+
+class OwnerPredicate extends OperatorPredicate<ChangeData> implements
+    Prefetchable {
   private final Provider<ReviewDb> dbProvider;
   private final Account.Id id;
 
@@ -45,4 +49,9 @@
   public int getCost() {
     return 1;
   }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    return EnumSet.of(NeededData.CHANGE);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateModule.java
similarity index 61%
copy from gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateModule.java
index 1eb1a4f..9f283d0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateModule.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2010 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.
@@ -12,15 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.account;
+package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.server.config.FactoryModule;
 
-import java.util.Set;
-
-/** Translates an email address to a set of matching accounts. */
-public interface AccountByEmailCache {
-  public Set<Account.Id> get(String email);
-
-  public void evict(String email);
+/** Module for package-private predicates. */
+public class PredicateModule extends FactoryModule {
+  @Override
+  protected void configure() {
+    factory(AndSource.Factory.class);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Prefetchable.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Prefetchable.java
new file mode 100644
index 0000000..c0c904b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Prefetchable.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2010 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.query.change;
+
+import java.util.EnumSet;
+
+/**
+ * Predicates that rely on ChangeData need to give some information about what
+ * data from ChangeData will be needed. This enables better caching and
+ * fetching.
+ */
+interface Prefetchable {
+
+  /**
+   * @return the set of data that will be needed by this predicate or its
+   *         children.
+   */
+  public EnumSet<ChangeData.NeededData> getNeededData();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PrefetchableAndPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PrefetchableAndPredicate.java
new file mode 100644
index 0000000..940c816
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PrefetchableAndPredicate.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2010 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.query.change;
+
+import com.google.gerrit.server.query.AndPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
+
+import java.util.Collection;
+import java.util.EnumSet;
+
+class PrefetchableAndPredicate extends AndPredicate<ChangeData>
+    implements Prefetchable {
+
+  PrefetchableAndPredicate(
+      Collection<? extends Predicate<ChangeData>> that) {
+    super(that);
+  }
+
+  PrefetchableAndPredicate(Predicate<ChangeData>... that) {
+    super(that);
+  }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    EnumSet<NeededData> needed = EnumSet.noneOf(NeededData.class);
+    for (Predicate<ChangeData> p : getChildren()) {
+      if (p instanceof Prefetchable) {
+        needed.addAll(((Prefetchable) p).getNeededData());
+      }
+    }
+    return needed;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PrefetchableNotPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PrefetchableNotPredicate.java
new file mode 100644
index 0000000..a8c65c9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PrefetchableNotPredicate.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2010 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.query.change;
+
+import com.google.gerrit.server.query.NotPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
+
+import java.util.EnumSet;
+
+class PrefetchableNotPredicate extends NotPredicate<ChangeData>
+    implements Prefetchable {
+  PrefetchableNotPredicate(Predicate<ChangeData> that) {
+    super(that);
+  }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    EnumSet<NeededData> needed = EnumSet.noneOf(NeededData.class);
+    for (Predicate<ChangeData> p : getChildren()) {
+      if (p instanceof Prefetchable) {
+        needed.addAll(((Prefetchable) p).getNeededData());
+      }
+    }
+    return needed;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PrefetchableOrPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PrefetchableOrPredicate.java
new file mode 100644
index 0000000..c8cdf23
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PrefetchableOrPredicate.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2010 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.query.change;
+
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
+
+import java.util.Collection;
+import java.util.EnumSet;
+
+class PrefetchableOrPredicate extends OrPredicate<ChangeData> implements
+    Prefetchable {
+  PrefetchableOrPredicate(Predicate<ChangeData>... that) {
+    super(that);
+  }
+
+  PrefetchableOrPredicate(
+      Collection<? extends Predicate<ChangeData>> that) {
+    super(that);
+  }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    EnumSet<NeededData> needed = EnumSet.noneOf(NeededData.class);
+    for (Predicate<ChangeData> p : getChildren()) {
+      if (p instanceof Prefetchable) {
+        needed.addAll(((Prefetchable) p).getNeededData());
+      }
+    }
+    return needed;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
index f5f83e2..75cfa95 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
@@ -17,10 +17,14 @@
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Provider;
 
-class RefPredicate extends OperatorPredicate<ChangeData> {
+import java.util.EnumSet;
+
+class RefPredicate extends OperatorPredicate<ChangeData> implements
+    Prefetchable {
   private final Provider<ReviewDb> dbProvider;
 
   RefPredicate(Provider<ReviewDb> dbProvider, String ref) {
@@ -41,4 +45,9 @@
   public int getCost() {
     return 1;
   }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    return EnumSet.of(NeededData.CHANGE);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
index bcece94..89a8fd1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -18,10 +18,14 @@
 import com.google.gerrit.reviewdb.PatchSetApproval;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Provider;
 
-class ReviewerPredicate extends OperatorPredicate<ChangeData> {
+import java.util.EnumSet;
+
+class ReviewerPredicate extends OperatorPredicate<ChangeData> implements
+    Prefetchable {
   private final Provider<ReviewDb> dbProvider;
   private final Account.Id id;
 
@@ -49,4 +53,9 @@
   public int getCost() {
     return 2;
   }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    return EnumSet.of(NeededData.APPROVALS);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
index 3ecc596..c5e4e7f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
@@ -4,11 +4,16 @@
 
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Provider;
 
-abstract class SortKeyPredicate extends OperatorPredicate<ChangeData> {
+import java.util.EnumSet;
+
+abstract class SortKeyPredicate extends OperatorPredicate<ChangeData> implements
+    Prefetchable {
   protected final Provider<ReviewDb> dbProvider;
 
   SortKeyPredicate(Provider<ReviewDb> dbProvider, String name, String value) {
@@ -21,15 +26,26 @@
     return 1;
   }
 
+  abstract String nextKey(Change last);
+
+  public EnumSet<NeededData> getNeededData() {
+    return EnumSet.of(NeededData.CHANGE);
+  }
+
   static class Before extends SortKeyPredicate {
     Before(Provider<ReviewDb> dbProvider, String value) {
-      super(dbProvider, "sortkey_before", value);
+      super(dbProvider, "sortkey_before", ChangeUtil.invertSortKey(value));
     }
 
     @Override
     public boolean match(ChangeData cd) throws OrmException {
-      Change change = cd.change(dbProvider);
-      return change != null && change.getSortKey().compareTo(getValue()) < 0;
+      Change c = cd.change(dbProvider);
+      return c != null && c.getSortKeyDesc().compareTo(getValue()) > 0;
+    }
+
+    @Override
+    String nextKey(Change last) {
+      return last.getSortKeyDesc();
     }
   }
 
@@ -43,5 +59,10 @@
       Change change = cd.change(dbProvider);
       return change != null && change.getSortKey().compareTo(getValue()) > 0;
     }
+
+    @Override
+    String nextKey(Change last) {
+      return last.getSortKey();
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
index 7bc972d..cc736d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
@@ -17,10 +17,14 @@
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Provider;
 
-class TopicPredicate extends OperatorPredicate<ChangeData> {
+import java.util.EnumSet;
+
+class TopicPredicate extends OperatorPredicate<ChangeData> implements
+    Prefetchable {
   private final Provider<ReviewDb> dbProvider;
 
   TopicPredicate(Provider<ReviewDb> dbProvider, String topic) {
@@ -41,4 +45,9 @@
   public int getCost() {
     return 1;
   }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    return EnumSet.of(NeededData.CHANGE);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
index eef568d..27194a5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
@@ -18,16 +18,18 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.TrackingId;
 import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.change.ChangeData.NeededData;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.ResultSet;
 import com.google.gwtorm.client.impl.ListResultSet;
 import com.google.inject.Provider;
 
 import java.util.ArrayList;
+import java.util.EnumSet;
 import java.util.HashSet;
 
 class TrackingIdPredicate extends OperatorPredicate<ChangeData> implements
-    ChangeDataSource {
+    ChangeDataSource, Prefetchable {
   private final Provider<ReviewDb> db;
 
   TrackingIdPredicate(Provider<ReviewDb> db, String trackingId) {
@@ -74,4 +76,9 @@
   public int getCost() {
     return ChangeCosts.cost(ChangeCosts.TR_SCAN, getCardinality());
   }
+
+  @Override
+  public EnumSet<NeededData> getNeededData() {
+    return EnumSet.of(NeededData.TRACKING_IDS);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
index 701e97a..e7d28b9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
@@ -15,13 +15,19 @@
 package com.google.gerrit.server.schema;
 
 import static com.google.gerrit.server.config.ConfigUtil.getEnum;
-import static java.util.concurrent.TimeUnit.*;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.jdbc.Database;
 import com.google.gwtorm.jdbc.SimpleDataSource;
+import com.google.gwtorm.nosql.heap.FileDatabase;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
@@ -29,44 +35,53 @@
 
 import org.apache.commons.dbcp.BasicDataSource;
 import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.IOException;
 import java.sql.SQLException;
 import java.util.Properties;
 
-import javax.sql.DataSource;
-
 /** Provides access to the DataSource. */
 @Singleton
-public final class DataSourceProvider implements Provider<DataSource>,
-    LifecycleListener {
-  private final DataSource ds;
+public final class DataSourceProvider implements
+    Provider<SchemaFactory<ReviewDb>>, LifecycleListener {
+  private static final Logger log =
+      LoggerFactory.getLogger(DataSourceProvider.class);
+
+  private final MyProvider ds;
 
   @Inject
   DataSourceProvider(final SitePaths site,
       @GerritServerConfig final Config cfg, Context ctx) {
-    ds = open(site, cfg, ctx);
+    try {
+      Type type = getEnum(cfg, "database", null, "type", Type.values(), null);
+      if (type == DataSourceProvider.Type.NOSQL_HEAP_FILE) {
+        ds = new NoSqlHeapFileProvider(site, cfg, ctx);
+
+      } else {
+        ds = new JdbcProvider(site, cfg, ctx);
+      }
+    } catch (OrmException e) {
+      throw new ProvisionException("Cannot create ReviewDb", e);
+    }
   }
 
   @Override
-  public synchronized DataSource get() {
-    return ds;
+  public SchemaFactory<ReviewDb> get() {
+    return ds.get();
   }
 
   @Override
   public void start() {
+    ds.start();
   }
 
+  @SuppressWarnings("unchecked")
   @Override
   public synchronized void stop() {
-    if (ds instanceof BasicDataSource) {
-      try {
-        ((BasicDataSource) ds).close();
-      } catch (SQLException e) {
-        // Ignore the close failure.
-      }
-    }
+    ds.stop();
   }
 
   public static enum Context {
@@ -74,141 +89,11 @@
   }
 
   public static enum Type {
-    H2, POSTGRESQL, MYSQL, JDBC;
-  }
+    // Traditional SQL databases
+    H2, POSTGRESQL, MYSQL, JDBC,
 
-  private DataSource open(final SitePaths site, final Config cfg,
-      final Context context) {
-    Type type = getEnum(cfg, "database", null, "type", Type.values(), null);
-    String driver = optional(cfg, "driver");
-    String url = optional(cfg, "url");
-    String username = optional(cfg, "username");
-    String password = optional(cfg, "password");
-
-    if (url == null || url.isEmpty()) {
-      if (type == null) {
-        if (url != null && !url.isEmpty()) {
-          type = Type.JDBC;
-        } else {
-          type = Type.H2;
-        }
-      }
-
-      switch (type) {
-        case H2: {
-          String database = optional(cfg, "database");
-          if (database == null || database.isEmpty()) {
-            database = "db/ReviewDB";
-          }
-          File db = site.resolve(database);
-          try {
-            db = db.getCanonicalFile();
-          } catch (IOException e) {
-            db = db.getAbsoluteFile();
-          }
-          url = "jdbc:h2:" + db.toURI().toString();
-          break;
-        }
-
-        case POSTGRESQL: {
-          final StringBuilder b = new StringBuilder();
-          b.append("jdbc:postgresql://");
-          b.append(hostname(optional(cfg, "hostname")));
-          b.append(port(optional(cfg, "port")));
-          b.append("/");
-          b.append(required(cfg, "database"));
-          url = b.toString();
-          break;
-        }
-
-        case MYSQL: {
-          final StringBuilder b = new StringBuilder();
-          b.append("jdbc:mysql://");
-          b.append(hostname(optional(cfg, "hostname")));
-          b.append(port(optional(cfg, "port")));
-          b.append("/");
-          b.append(required(cfg, "database"));
-          url = b.toString();
-          break;
-        }
-
-        case JDBC:
-          driver = required(cfg, "driver");
-          url = required(cfg, "url");
-          break;
-
-        default:
-          throw new IllegalArgumentException(type + " not supported");
-      }
-    }
-
-    if (driver == null || driver.isEmpty()) {
-      if (url.startsWith("jdbc:h2:")) {
-        driver = "org.h2.Driver";
-
-      } else if (url.startsWith("jdbc:postgresql:")) {
-        driver = "org.postgresql.Driver";
-
-      } else if (url.startsWith("jdbc:mysql:")) {
-        driver = "com.mysql.jdbc.Driver";
-
-      } else {
-        throw new IllegalArgumentException("database.driver must be set");
-      }
-    }
-
-    boolean usePool;
-    if (url.startsWith("jdbc:mysql:")) {
-      // MySQL has given us trouble with the connection pool,
-      // sometimes the backend disconnects and the pool winds
-      // up with a stale connection. Fortunately opening up
-      // a new MySQL connection is usually very fast.
-      //
-      usePool = false;
-    } else {
-      usePool = true;
-    }
-    usePool = cfg.getBoolean("database", "connectionpool", usePool);
-    if (context == Context.SINGLE_USER) {
-      usePool = false;
-    }
-
-    if (usePool) {
-      final BasicDataSource ds = new BasicDataSource();
-      ds.setDriverClassName(driver);
-      ds.setUrl(url);
-      if (username != null && !username.isEmpty()) {
-        ds.setUsername(username);
-      }
-      if (password != null && !password.isEmpty()) {
-        ds.setPassword(password);
-      }
-      ds.setMaxActive(cfg.getInt("database", "poollimit", 8));
-      ds.setMinIdle(cfg.getInt("database", "poolminidle", 4));
-      ds.setMaxIdle(cfg.getInt("database", "poolmaxidle", 4));
-      ds.setMaxWait(ConfigUtil.getTimeUnit(cfg, "database", null,
-          "poolmaxwait", MILLISECONDS.convert(30, SECONDS), MILLISECONDS));
-      ds.setInitialSize(ds.getMinIdle());
-      return ds;
-
-    } else {
-      // Don't use the connection pool.
-      //
-      try {
-        final Properties p = new Properties();
-        p.setProperty("driver", driver);
-        p.setProperty("url", url);
-        if (username != null) {
-          p.setProperty("user", username);
-        }
-        if (password != null) {
-          p.setProperty("password", password);
-        }
-        return new SimpleDataSource(p);
-      } catch (SQLException se) {
-        throw new ProvisionException("Database unavailable", se);
-      }
-    }
+    // NoSQL types we also support
+    NOSQL_HEAP_FILE;
   }
 
   private static String hostname(String hostname) {
@@ -239,4 +124,208 @@
     }
     return v;
   }
+
+  private abstract static class MyProvider implements
+      Provider<SchemaFactory<ReviewDb>>, LifecycleListener {
+  }
+
+  private static class NoSqlHeapFileProvider extends MyProvider {
+    private FileDatabase<ReviewDb> db;
+
+    NoSqlHeapFileProvider(final SitePaths site, final Config cfg,
+        final Context context) throws OrmException {
+      String database = optional(cfg, "database");
+      if (database == null || database.isEmpty()) {
+        database = "db/ReviewDB";
+      }
+      File path = site.resolve(database);
+      try {
+        path = path.getCanonicalFile();
+      } catch (IOException e) {
+        path = path.getAbsoluteFile();
+      }
+      db = new FileDatabase<ReviewDb>(path, ReviewDb.class);
+    }
+
+    @Override
+    public SchemaFactory<ReviewDb> get() {
+      return db;
+    }
+
+    @Override
+    public void start() {
+    }
+
+    @Override
+    public void stop() {
+      if (db != null) {
+        try {
+          db.close();
+        } catch (OrmException err) {
+          log.warn("Cannot safely close database", err);
+        } finally {
+          db = null;
+        }
+      }
+    }
+  }
+
+  private static class JdbcProvider extends MyProvider {
+    private Database<ReviewDb> db;
+    private BasicDataSource dataSource;
+
+    JdbcProvider(final SitePaths site, final Config cfg, final Context context)
+        throws OrmException {
+      Type type = getEnum(cfg, "database", null, "type", Type.values(), null);
+      String driver = optional(cfg, "driver");
+      String url = optional(cfg, "url");
+      String username = optional(cfg, "username");
+      String password = optional(cfg, "password");
+
+      if (url == null || url.isEmpty()) {
+        if (type == null) {
+          type = Type.H2;
+        }
+
+        switch (type) {
+          case H2: {
+            String database = optional(cfg, "database");
+            if (database == null || database.isEmpty()) {
+              database = "db/ReviewDB";
+            }
+            File db = site.resolve(database);
+            try {
+              db = db.getCanonicalFile();
+            } catch (IOException e) {
+              db = db.getAbsoluteFile();
+            }
+            url = "jdbc:h2:" + db.toURI().toString();
+            break;
+          }
+
+          case POSTGRESQL: {
+            final StringBuilder b = new StringBuilder();
+            b.append("jdbc:postgresql://");
+            b.append(hostname(optional(cfg, "hostname")));
+            b.append(port(optional(cfg, "port")));
+            b.append("/");
+            b.append(required(cfg, "database"));
+            url = b.toString();
+            break;
+          }
+
+          case MYSQL: {
+            final StringBuilder b = new StringBuilder();
+            b.append("jdbc:mysql://");
+            b.append(hostname(optional(cfg, "hostname")));
+            b.append(port(optional(cfg, "port")));
+            b.append("/");
+            b.append(required(cfg, "database"));
+            url = b.toString();
+            break;
+          }
+
+          case JDBC:
+            driver = required(cfg, "driver");
+            url = required(cfg, "url");
+            break;
+
+          default:
+            throw new IllegalArgumentException(type + " not supported");
+        }
+      }
+
+      if (driver == null || driver.isEmpty()) {
+        if (url.startsWith("jdbc:h2:")) {
+          driver = "org.h2.Driver";
+
+        } else if (url.startsWith("jdbc:postgresql:")) {
+          driver = "org.postgresql.Driver";
+
+        } else if (url.startsWith("jdbc:mysql:")) {
+          driver = "com.mysql.jdbc.Driver";
+
+        } else {
+          throw new IllegalArgumentException("database.driver must be set");
+        }
+      }
+
+      boolean usePool;
+      if (url.startsWith("jdbc:mysql:")) {
+        // MySQL has given us trouble with the connection pool,
+        // sometimes the backend disconnects and the pool winds
+        // up with a stale connection. Fortunately opening up
+        // a new MySQL connection is usually very fast.
+        //
+        usePool = false;
+      } else {
+        usePool = true;
+      }
+      usePool = cfg.getBoolean("database", "connectionpool", usePool);
+      if (context == Context.SINGLE_USER) {
+        usePool = false;
+      }
+
+      if (usePool) {
+        final BasicDataSource ds = new BasicDataSource();
+        ds.setDriverClassName(driver);
+        ds.setUrl(url);
+        if (username != null && !username.isEmpty()) {
+          ds.setUsername(username);
+        }
+        if (password != null && !password.isEmpty()) {
+          ds.setPassword(password);
+        }
+        ds.setMaxActive(cfg.getInt("database", "poollimit", 8));
+        ds.setMinIdle(cfg.getInt("database", "poolminidle", 4));
+        ds.setMaxIdle(cfg.getInt("database", "poolmaxidle", 4));
+        ds.setMaxWait(ConfigUtil.getTimeUnit(cfg, "database", null,
+            "poolmaxwait", MILLISECONDS.convert(30, SECONDS), MILLISECONDS));
+        ds.setInitialSize(ds.getMinIdle());
+        dataSource = ds;
+        db = new Database<ReviewDb>(ds, ReviewDb.class);
+
+      } else {
+        // Don't use the connection pool.
+        //
+        try {
+          final Properties p = new Properties();
+          p.setProperty("driver", driver);
+          p.setProperty("url", url);
+          if (username != null) {
+            p.setProperty("user", username);
+          }
+          if (password != null) {
+            p.setProperty("password", password);
+          }
+          db = new Database<ReviewDb>(new SimpleDataSource(p), ReviewDb.class);
+        } catch (SQLException se) {
+          throw new ProvisionException("Database unavailable", se);
+        }
+      }
+    }
+
+    @Override
+    public SchemaFactory<ReviewDb> get() {
+      return db;
+    }
+
+    @Override
+    public void start() {
+    }
+
+    @Override
+    public void stop() {
+      if (dataSource != null) {
+        try {
+          dataSource.close();
+        } catch (SQLException e) {
+          // Ignore the close failure.
+        } finally {
+          dataSource = null;
+          db = null;
+        }
+      }
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java
deleted file mode 100644
index 55a36d6..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java
+++ /dev/null
@@ -1,36 +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.server.schema;
-
-import static com.google.inject.Scopes.SINGLETON;
-
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.config.FactoryModule;
-import com.google.gwtorm.client.SchemaFactory;
-import com.google.gwtorm.jdbc.Database;
-import com.google.inject.TypeLiteral;
-
-/** Loads the database with standard dependencies. */
-public class DatabaseModule extends FactoryModule {
-  @Override
-  protected void configure() {
-    install(new SchemaVersion.Module());
-
-    bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}).to(
-        new TypeLiteral<Database<ReviewDb>>() {}).in(SINGLETON);
-    bind(new TypeLiteral<Database<ReviewDb>>() {}).toProvider(
-        ReviewDbDatabaseProvider.class).in(SINGLETON);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ReviewDbDatabaseProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ReviewDbDatabaseProvider.java
deleted file mode 100644
index 56b0379..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ReviewDbDatabaseProvider.java
+++ /dev/null
@@ -1,44 +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.server.schema;
-
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.Database;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-import com.google.inject.name.Named;
-
-import javax.sql.DataSource;
-
-/** Provides the {@code Database<ReviewDb>} database handle. */
-final class ReviewDbDatabaseProvider implements Provider<Database<ReviewDb>> {
-  private final DataSource datasource;
-
-  @Inject
-  ReviewDbDatabaseProvider(@Named("ReviewDb") final DataSource ds) {
-    datasource = ds;
-  }
-
-  @Override
-  public Database<ReviewDb> get() {
-    try {
-      return new Database<ReviewDb>(datasource, ReviewDb.class);
-    } catch (OrmException e) {
-      throw new ProvisionException("Cannot create ReviewDb", e);
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
index 302e22b..5460bed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.workflow.SubmitFunction;
 import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.StatementExecutor;
 import com.google.gwtorm.jdbc.JdbcExecutor;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.schema.sql.DialectH2;
@@ -71,12 +72,21 @@
   }
 
   public void create(final ReviewDb db) throws OrmException {
-    final JdbcSchema jdbc = (JdbcSchema) db;
-    final JdbcExecutor e = new JdbcExecutor(jdbc);
-    try {
-      jdbc.updateSchema(e);
-    } finally {
-      e.close();
+    if (db instanceof JdbcSchema) {
+      final JdbcSchema jdbc = (JdbcSchema) db;
+      final JdbcExecutor e = new JdbcExecutor(jdbc);
+      try {
+        jdbc.updateSchema(e);
+      } finally {
+        e.close();
+      }
+    } else {
+      db.updateSchema(new StatementExecutor() {
+        @Override
+        public void execute(String sql) throws OrmException {
+          throw new OrmException("Raw SQL not supported");
+        }
+      });
     }
 
     final CurrentSchemaVersion sVer = CurrentSchemaVersion.create();
@@ -94,19 +104,21 @@
     initForgeIdentityCategory(db, sConfig);
     initWildCardProject(db);
 
-    final SqlDialect d = jdbc.getDialect();
-    if (d instanceof DialectH2) {
-      index_generic.run(db);
+    if (db instanceof JdbcSchema) {
+      final SqlDialect d = ((JdbcSchema) db).getDialect();
+      if (d instanceof DialectH2) {
+        index_generic.run(db);
 
-    } else if (d instanceof DialectMySQL) {
-      index_generic.run(db);
-      mysql_nextval.run(db);
+      } else if (d instanceof DialectMySQL) {
+        index_generic.run(db);
+        mysql_nextval.run(db);
 
-    } else if (d instanceof DialectPostgreSQL) {
-      index_postgres.run(db);
+      } else if (d instanceof DialectPostgreSQL) {
+        index_postgres.run(db);
 
-    } else {
-      throw new OrmException("Unsupported database " + d.getClass().getName());
+      } else {
+        throw new OrmException("Unsupported database " + d.getClass().getName());
+      }
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 78edbc3..7df24b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  private static final Class<? extends SchemaVersion> C = Schema_41.class;
+  private static final Class<? extends SchemaVersion> C = Schema_103.class;
 
   public static class Module extends AbstractModule {
     @Override
@@ -81,22 +81,20 @@
   /** Runs check on the prior schema version, and then upgrades. */
   protected void upgradeFrom(UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db, boolean toTargetVersion)
       throws OrmException, SQLException {
-    final JdbcSchema s = (JdbcSchema) db;
-
     prior.get().check(ui, curr, db, false);
 
     ui.message("Upgrading database schema from version " + curr.versionNbr
         + " to " + versionNbr + " ...");
 
     preUpdateSchema(db);
-    final JdbcExecutor e = new JdbcExecutor(s);
+    final StatementExecutor e = newExecutor(db);
     try {
-      s.updateSchema(e);
+      db.updateSchema(e);
       migrateData(db, ui);
 
       if (toTargetVersion) {
         final List<String> pruneList = new ArrayList<String>();
-        s.pruneSchema(new StatementExecutor() {
+        db.pruneSchema(new StatementExecutor() {
           public void execute(String sql) {
             pruneList.add(sql);
           }
@@ -107,11 +105,25 @@
         }
       }
     } finally {
-      e.close();
+      if (e instanceof JdbcExecutor) {
+        ((JdbcExecutor)e).close();
+      }
     }
     finish(curr, db);
   }
 
+  private StatementExecutor newExecutor(ReviewDb db) throws OrmException {
+    if (db instanceof JdbcSchema) {
+      return new JdbcExecutor((JdbcSchema) db);
+    }
+    return new StatementExecutor() {
+      @Override
+      public void execute(String sql) throws OrmException {
+        throw new OrmException("SQL statement execution not supported");
+      }
+    };
+  }
+
   /** Invoke before updateSchema adds new columns/tables. */
   protected void preUpdateSchema(ReviewDb db) throws OrmException, SQLException {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_103.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_103.java
new file mode 100644
index 0000000..5474638
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_103.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2010 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.schema;
+
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.sql.DialectMySQL;
+import com.google.gwtorm.schema.sql.DialectPostgreSQL;
+import com.google.gwtorm.schema.sql.SqlDialect;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Schema_103 extends SchemaVersion {
+  private static final int MAX_SCAN_SIZE = 1000;
+
+  @Inject
+  Schema_103(Provider<Schema_41> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    final SqlDialect dialect = ((JdbcSchema) db).getDialect();
+    stmt.execute("CREATE INDEX changes_upgrade103 ON changes (sort_key_desc)");
+
+    List<ToUpdate> changes = new ArrayList<ToUpdate>(MAX_SCAN_SIZE);
+
+    PreparedStatement selectStmt =
+        ((JdbcSchema) db).getConnection().prepareStatement(
+            "SELECT change_id, sort_key FROM changes"
+                + " WHERE sort_key_desc IS NULL OR sort_key_desc=''");
+
+    selectStmt.setMaxRows(MAX_SCAN_SIZE);
+
+    PreparedStatement updateChangeStmt =
+        ((JdbcSchema) db).getConnection().prepareStatement(
+            "UPDATE changes SET sort_key_desc = ? WHERE change_id = ?");
+    PreparedStatement updateApprovalStmt =
+        ((JdbcSchema) db).getConnection().prepareStatement(
+            "UPDATE patch_set_approvals SET change_sort_key_desc = ?"
+                + " WHERE change_id = ?");
+
+    try {
+      while (true) {
+        ResultSet rs = selectStmt.executeQuery();
+        try {
+          while (rs.next() && changes.size() < MAX_SCAN_SIZE) {
+            changes.add(new ToUpdate(rs.getInt(1), rs.getString(2)));
+          }
+        } finally {
+          rs.close();
+        }
+
+        if (changes.isEmpty()) {
+          break;
+        }
+
+        int batchSize = 0;
+        for (ToUpdate u : changes) {
+          String desc = Long.toHexString(-1l - Long.parseLong(u.sortKey, 16));
+
+          updateChangeStmt.setString(1, desc);
+          updateChangeStmt.setInt(2, u.id);
+          updateChangeStmt.addBatch();
+
+          updateApprovalStmt.setString(1, desc);
+          updateApprovalStmt.setInt(2, u.id);
+          updateApprovalStmt.addBatch();
+
+          batchSize++;
+
+          if (batchSize >= 200) {
+            updateChangeStmt.executeBatch();
+            updateApprovalStmt.executeBatch();
+            batchSize = 0;
+          }
+        }
+        if (batchSize > 0) {
+          updateChangeStmt.executeBatch();
+          updateApprovalStmt.executeBatch();
+        }
+
+        changes.clear();
+      }
+
+      if (((JdbcSchema) db).getDialect() instanceof DialectMySQL) {
+        stmt.execute("DROP INDEX changes_upgrade103 ON changes");
+      } else {
+        stmt.execute("DROP INDEX changes_upgrade103");
+      }
+
+      if (dialect instanceof DialectPostgreSQL) {
+        stmt.execute("CREATE INDEX changes_allOpenD ON changes (sort_key_desc) WHERE open = 'Y'");
+        stmt.execute("CREATE INDEX changes_byProjectOpenD ON changes (dest_project_name, sort_key_desc) WHERE open = 'Y'");
+        stmt.execute("CREATE INDEX changes_allClosedD ON changes (status, sort_key_desc) WHERE open = 'N'");
+        stmt.execute("CREATE INDEX patch_set_approvals_closedByUserD ON patch_set_approvals (account_id, change_sort_key_desc) WHERE change_open = 'N'");
+
+      } else {
+        stmt.execute("CREATE INDEX changes_allOpenD ON changes (open, sort_key_desc)");
+        stmt.execute("CREATE INDEX changes_byProjectOpenD ON changes (open, dest_project_name, sort_key_desc)");
+        stmt.execute("CREATE INDEX changes_allClosedD ON changes (open, status, sort_key_desc);");
+        stmt.execute("CREATE INDEX patch_set_approvals_closedByUserD ON patch_set_approvals (change_open, account_id, change_sort_key_desc)");
+      }
+    } finally {
+      stmt.close();
+      updateChangeStmt.close();
+      selectStmt.close();
+    }
+  }
+
+  private static class ToUpdate {
+    int id;
+    String sortKey;
+
+    ToUpdate(int changeId, String sortKey) {
+      this.id = changeId;
+      this.sortKey = sortKey;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/backup/BackupAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/backup/BackupAccess.java
new file mode 100644
index 0000000..cdba707
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/backup/BackupAccess.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2010 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.schema.backup;
+
+import com.google.gwtorm.client.Key;
+import com.google.gwtorm.client.ResultSet;
+import com.google.gwtorm.nosql.IndexFunction;
+import com.google.gwtorm.nosql.NoSqlAccess;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+
+public abstract class BackupAccess<T, K extends Key<?>> extends
+    NoSqlAccess<T, K> {
+  @SuppressWarnings("unchecked")
+  protected BackupAccess(BackupSchema s) {
+    super(s);
+  }
+
+  @Override
+  public abstract ProtobufCodec<T> getObjectCodec();
+
+  @SuppressWarnings("unchecked")
+  @Override
+  protected ResultSet scanIndex(IndexFunction index, byte[] fromKey,
+      byte[] toKey, int limit, boolean order) {
+    throw new UnsupportedOperationException();
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  protected ResultSet scanPrimaryKey(byte[] fromKey, byte[] toKey, int limit,
+      boolean order) {
+    throw new UnsupportedOperationException();
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public void delete(Iterable instances) {
+    throw new UnsupportedOperationException();
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public T get(K key) {
+    throw new UnsupportedOperationException();
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public void insert(Iterable instances) {
+    throw new UnsupportedOperationException();
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public ResultSet iterateAllEntities() {
+    throw new UnsupportedOperationException();
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public void update(Iterable instances) {
+    throw new UnsupportedOperationException();
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public void upsert(Iterable instances) {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/backup/BackupDatabase.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/backup/BackupDatabase.java
new file mode 100644
index 0000000..d80de7a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/backup/BackupDatabase.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2010 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.schema.backup;
+
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.Schema;
+import com.google.gwtorm.nosql.NoSqlDatabase;
+
+@SuppressWarnings("unchecked")
+public class BackupDatabase<T extends Schema> extends
+    NoSqlDatabase<T, BackupSchema, BackupAccess> {
+  public BackupDatabase(Class<T> schema) throws OrmException {
+    super(BackupSchema.class, BackupAccess.class, schema);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/backup/BackupSchema.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/backup/BackupSchema.java
new file mode 100644
index 0000000..da19212
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/backup/BackupSchema.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2010 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.schema.backup;
+
+import com.google.gwtorm.client.Schema;
+import com.google.gwtorm.nosql.NoSqlSchema;
+
+public abstract class BackupSchema<T extends Schema> extends NoSqlSchema {
+  protected BackupSchema(BackupDatabase<T> d) {
+    super(d);
+  }
+
+  @Override
+  protected long nextLong(String poolName) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void close() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/backup/Counters.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/backup/Counters.java
new file mode 100644
index 0000000..6e9b05b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/backup/Counters.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2010 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.schema.backup;
+
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.protobuf.CodecFactory;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+
+public class Counters {
+  public static final ProtobufCodec<Counters> CODEC =
+      CodecFactory.encoder(Counters.class);
+
+  @Column(id = 1)
+  public int accountGroupId;
+
+  @Column(id = 2)
+  public int accountId;
+
+  @Column(id = 3)
+  public int changeId;
+
+  @Column(id = 4)
+  public int changeMessageId;
+
+  @Column(id = 5)
+  public int contributorAgreementId;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/backup/RestoreBackup.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/backup/RestoreBackup.java
new file mode 100644
index 0000000..dd03abc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/backup/RestoreBackup.java
@@ -0,0 +1,151 @@
+// Copyright (C) 2010 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.schema.backup;
+
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.client.Access;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import org.eclipse.jgit.util.IO;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.zip.GZIPInputStream;
+
+/** Restore the database from a compressed series of protobuf objects. */
+public class RestoreBackup {
+  public static void restore(InputStream in, ReviewDb dst) throws IOException,
+      OrmException {
+    in = new BufferedInputStream(new GZIPInputStream(in, 8192));
+
+    BackupDatabase<ReviewDb> bck = new BackupDatabase<ReviewDb>(ReviewDb.class);
+    ReviewDb src = bck.open();
+
+    dst.setAutoFlush(false);
+    restoreImpl(in, src, dst);
+    dst.flush();
+  }
+
+  @SuppressWarnings("unchecked")
+  private static void restoreImpl(InputStream in, ReviewDb src, ReviewDb dst)
+      throws IOException, OrmException {
+    // Remove every row, we're about to overwrite them all.
+    //
+    for (Access<?, ?> s : dst.allRelations()) {
+      List objects = s.iterateAllEntities().toList();
+      s.delete(objects);
+    }
+
+    Map<Integer, BackupAccess<?, ?>> read = index(src);
+    Map<Integer, Access<?, ?>> store = index(dst);
+
+    // The first object should be a Counters.
+    //
+    Counters cnts = Counters.CODEC.decodeWithSize(in);
+
+    // Remaining objects are length delimited until EOF.
+    //
+    Set<Integer> notKnown = new HashSet<Integer>();
+    for (;;) {
+      in.mark(1);
+      if (in.read() == -1) {
+        break;
+      }
+
+      in.reset();
+      in.mark(32);
+      int len = readRawVarint32(in);
+      int id = readRawVarint32(in) >>> 3;
+
+      BackupAccess<?, ?> r = read.get(id);
+      Access<?, ?> w = store.get(id);
+
+      if (r != null && w != null) {
+        in.reset();
+        ProtobufCodec pc = r.getObjectCodec();
+        Set s = Collections.singleton(pc.decodeWithSize(in));
+        w.upsert(s);
+
+      } else {
+        if (notKnown.add(id)) {
+          System.err.println("warning: Skipping relation " + id);
+        }
+        in.reset();
+        if (len != readRawVarint32(in)) {
+          throw new IOException("Stream didn't reset before skipping");
+        }
+        IO.skipFully(in, len);
+      }
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private static Map index(ReviewDb src) {
+    Map<Integer, Access<?, ?>> relations = new HashMap<Integer, Access<?, ?>>();
+    for (Access<?, ?> a : src.allRelations()) {
+      relations.put(a.getRelationID(), a);
+    }
+    return relations;
+  }
+
+  private static int readRawVarint32(InputStream in) throws IOException {
+    int b = in.read();
+    if (b == -1) {
+      throw new InvalidProtocolBufferException("Truncated input");
+    }
+
+    if ((b & 0x80) == 0) {
+      return b;
+    }
+
+    int result = b & 0x7f;
+    int offset = 7;
+    for (; offset < 32; offset += 7) {
+      b = in.read();
+      if (b == -1) {
+        throw new InvalidProtocolBufferException("Truncated input");
+      }
+      result |= (b & 0x7f) << offset;
+      if ((b & 0x80) == 0) {
+        return result;
+      }
+    }
+
+    // Keep reading up to 64 bits.
+    for (; offset < 64; offset += 7) {
+      b = in.read();
+      if (b == -1) {
+        throw new InvalidProtocolBufferException("Truncated input");
+      }
+      if ((b & 0x80) == 0) {
+        return result;
+      }
+    }
+
+    throw new InvalidProtocolBufferException("Malformed varint");
+  }
+
+  private RestoreBackup() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java
similarity index 61%
copy from gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java
index 1eb1a4f..882181d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2010 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.
@@ -12,15 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.account;
+package com.google.gerrit.server.ssh;
 
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.server.ssh.SshInfo;
 
-import java.util.Set;
+import com.jcraft.jsch.HostKey;
 
-/** Translates an email address to a set of matching accounts. */
-public interface AccountByEmailCache {
-  public Set<Account.Id> get(String email);
+import java.util.Collections;
+import java.util.List;
 
-  public void evict(String email);
+class NoSshInfo implements SshInfo {
+  @Override
+  public List<HostKey> getHostKeys() {
+    return Collections.emptyList();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshKeyCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
new file mode 100644
index 0000000..5917dfc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2010 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.ssh;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.reviewdb.AccountSshKey;
+
+class NoSshKeyCache implements SshKeyCache {
+  @Override
+  public ListenableFuture<Void> evictAsync(String username) {
+    return Futures.immediateFuture(null);
+  }
+
+  @Override
+  public AccountSshKey create(AccountSshKey.Id id, String encoded)
+      throws InvalidSshKeyException {
+    throw new InvalidSshKeyException();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshModule.java
new file mode 100644
index 0000000..21b1a54
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshModule.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2010 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.ssh;
+
+import com.google.inject.AbstractModule;
+
+/**
+ * Disables the SSH support by stubbing out relevant objects.
+ */
+public class NoSshModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(SshInfo.class).to(NoSshInfo.class);
+    bind(SshKeyCache.class).to(NoSshKeyCache.class);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java
index b56405a..9275dbf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.server.ssh;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.reviewdb.AccountSshKey;
 
 /** Permits controlling the contents of the SSH key cache area. */
 public interface SshKeyCache {
-  public void evict(String username);
+  public ListenableFuture<Void> evictAsync(String username);
 
   public AccountSshKey create(AccountSshKey.Id id, String encoded)
       throws InvalidSshKeyException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/CompoundFuture.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/CompoundFuture.java
new file mode 100644
index 0000000..1d0b91a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/CompoundFuture.java
@@ -0,0 +1,192 @@
+// Copyright (C) 2010 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.util;
+
+import com.google.common.util.concurrent.ForwardingListenableFuture;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/** A future that runs all of the futures given to it. */
+public class CompoundFuture<V> extends ForwardingListenableFuture<V> {
+  /**
+   * Construct a new compound future around several futures.
+   *
+   * @param <V> the type of the result this future produces.
+   * @param result the future that will provide the final result. This future
+   *        will be waited on last.
+   * @param other1 any other future that should also complete before this future
+   *        completes. Their results will be discarded, and thus should be
+   *        declared to return Void.
+   * @return a future to wait on several futures.
+   */
+  @SuppressWarnings("unchecked")
+  public static <V> CompoundFuture<V> wrap(ListenableFuture<V> result,
+      Future<Void> other1) {
+    return new CompoundFuture<V>(result, other1);
+  }
+
+  /**
+   * Construct a new compound future around several futures.
+   *
+   * @param <V> the type of the result this future produces.
+   * @param result the future that will provide the final result. This future
+   *        will be waited on last.
+   * @param other1 any other future that should also complete before this future
+   *        completes. Their results will be discarded, and thus should be
+   *        declared to return Void.
+   * @param other2 any other future that should also complete before this future
+   *        completes. Their results will be discarded, and thus should be
+   *        declared to return Void.
+   * @return a future to wait on several futures.
+   */
+  @SuppressWarnings("unchecked")
+  public static <V> CompoundFuture<V> wrap(ListenableFuture<V> result,
+      Future<Void> other1, Future<Void> other2) {
+    return new CompoundFuture<V>(result, other1, other2);
+  }
+
+  /**
+   * Construct a new compound future around several futures.
+   *
+   * @param <V> the type of the result this future produces.
+   * @param result the future that will provide the final result. This future
+   *        will be waited on last.
+   * @param other1 any other future that should also complete before this future
+   *        completes. Their results will be discarded, and thus should be
+   *        declared to return Void.
+   * @param other2 any other future that should also complete before this future
+   *        completes. Their results will be discarded, and thus should be
+   *        declared to return Void.
+   * @param other3 any other future that should also complete before this future
+   *        completes. Their results will be discarded, and thus should be
+   *        declared to return Void.
+   * @return a future to wait on several futures.
+   */
+  @SuppressWarnings("unchecked")
+  public static <V> CompoundFuture<V> wrap(ListenableFuture<V> result,
+      Future<Void> other1, Future<Void> other2, Future<Void> other3) {
+    return new CompoundFuture<V>(result, other1, other2, other3);
+  }
+
+  /**
+   * Construct a new compound future around several futures.
+   *
+   * @param <V> the type of the result this future produces.
+   * @param result the future that will provide the final result. This future
+   *        will be waited on last.
+   * @param others any other futures that should also complete before this
+   *        future completes. Their results will be discarded, and thus should
+   *        be declared to return Void.
+   * @return a future to wait on several futures.
+   */
+  public static <V> CompoundFuture<V> wrap(ListenableFuture<V> result,
+      Future<Void>... others) {
+    Future<Void>[] r = CompoundFuture.<Void> newArray(others.length);
+    System.arraycopy(others, 0, r, 0, others.length);
+    return new CompoundFuture<V>(result, r);
+  }
+
+  /**
+   * Construct a new compound future around several futures.
+   *
+   * @param <V> the type of the result this future produces.
+   * @param result the future that will provide the final result. This future
+   *        will be waited on last.
+   * @param others any other futures that should also complete before this
+   *        future completes. Their results will be discarded, and thus should
+   *        be declared to return Void.
+   * @return a future to wait on several futures.
+   */
+  public static <V> CompoundFuture<V> wrap(ListenableFuture<V> result,
+      Collection<Future<Void>> others) {
+    Future<Void>[] r = CompoundFuture.<Void> newArray(others.size());
+    return new CompoundFuture<V>(result, others.toArray(r));
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <T> Future<T>[] newArray(int sz) {
+    return new Future[sz];
+  }
+
+  private final ListenableFuture<V> result;
+  private final Future<Void>[] others;
+
+  private CompoundFuture(ListenableFuture<V> result, Future<Void>... others) {
+    this.result = result;
+    this.others = others;
+  }
+
+  @Override
+  protected ListenableFuture<V> delegate() {
+    return result;
+  }
+
+  @Override
+  public boolean cancel(boolean mayInterruptIfRunning) {
+    boolean res = super.cancel(mayInterruptIfRunning);
+    for (Future<Void> f : others) {
+      f.cancel(mayInterruptIfRunning);
+    }
+    return res;
+  }
+
+  @Override
+  public V get() throws InterruptedException, ExecutionException {
+    for (Future<Void> f : others) {
+      if (!f.isDone()) {
+        f.get();
+      }
+    }
+    return super.get();
+  }
+
+  @Override
+  public V get(long timeout, TimeUnit unit) throws InterruptedException,
+      ExecutionException, TimeoutException {
+    if (unit != TimeUnit.MILLISECONDS) {
+      timeout = TimeUnit.MILLISECONDS.convert(timeout, unit);
+      unit = TimeUnit.MILLISECONDS;
+    }
+
+    for (Future<Void> f : others) {
+      if (!f.isDone()) {
+        long start = System.currentTimeMillis();
+        f.get(timeout, unit);
+
+        timeout -= Math.max(0, System.currentTimeMillis() - start);
+        if (timeout <= 0) {
+          throw new TimeoutException();
+        }
+      }
+    }
+
+    return super.get(timeout, unit);
+  }
+
+  @Override
+  public boolean isDone() {
+    for (Future<Void> f : others) {
+      if (!f.isDone()) {
+        return false;
+      }
+    }
+    return super.isDone();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ConcatFuture.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ConcatFuture.java
new file mode 100644
index 0000000..9bacec0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ConcatFuture.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2010 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.util;
+
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.AbstractListenableFuture;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+class ConcatFuture<V> extends AbstractListenableFuture<List<V>> {
+  private final AtomicInteger pending;
+  private final List<ListenableFuture<List<V>>> sources;
+
+  ConcatFuture(List<ListenableFuture<List<V>>> all) {
+    sources = all;
+    pending = new AtomicInteger(sources.size());
+
+    Runnable done = new Runnable() {
+      @Override
+      public void run() {
+        finish();
+      }
+    };
+    for (ListenableFuture<List<V>> future : sources) {
+      future.addListener(done, MoreExecutors.sameThreadExecutor());
+    }
+  }
+
+  private void finish() {
+    if (pending.decrementAndGet() == 0) {
+      List<V> all = Lists.newArrayList();
+      for (ListenableFuture<List<V>> f : sources) {
+        all.addAll(FutureUtil.get(f));
+      }
+      set(all);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/FutureException.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/FutureException.java
new file mode 100644
index 0000000..9534bec
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/FutureException.java
@@ -0,0 +1,10 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+
+package com.google.gerrit.server.util;
+
+/** Exception thrown by {@link FutureUtil#get(java.util.concurrent.Future)}. */
+public class FutureException extends RuntimeException {
+  FutureException(Throwable why) {
+    super("Future computation failed", why);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/FutureUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/FutureUtil.java
new file mode 100644
index 0000000..6a01b4d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/FutureUtil.java
@@ -0,0 +1,221 @@
+// Copyright (C) 2010 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.util;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+/** Utilities to work with futures. */
+public class FutureUtil {
+  private static final Logger log = LoggerFactory.getLogger(FutureUtil.class);
+
+  /**
+   * Construct a future that concatenates the resulting lists.
+   *
+   * @param <V> type of the list element.
+   * @param all the futures whose results will be concatenated.
+   * @return a future to get all results.
+   */
+  public static <V> ListenableFuture<List<V>> concat(
+      List<ListenableFuture<List<V>>> all) {
+    return new ConcatFuture<V>(all);
+  }
+
+  /**
+   * Construct a future that concatenates the resulting lists.
+   *
+   * @param <V> type of the list element.
+   * @param all the futures whose results will be concatenated.
+   * @return a future to get all results.
+   */
+  public static <V> ListenableFuture<List<V>> concatSingletons(
+      List<ListenableFuture<V>> all) {
+    Function<V, List<V>> asList = new Function<V, List<V>>() {
+      public List<V> apply(V item) {
+        if (item != null) {
+          return Collections.singletonList(item);
+        } else {
+          return Collections.<V> emptyList();
+        }
+      }
+    };
+
+    List<ListenableFuture<List<V>>> r = Lists.newArrayList();
+    for (ListenableFuture<V> f : all) {
+      r.add(Futures.compose(f, asList));
+    }
+    return new ConcatFuture<V>(r);
+  }
+
+  /**
+   * Get the future's value and return it to the caller.
+   *
+   * If the method is interrupted during computation, or the future throws an
+   * exception this is wrapped into an {@link FutureException} and rethrown.
+   *
+   * @param <V> the return type of the future.
+   * @param future the future itself
+   * @return the value of the future.
+   * @throws FutureException if the future's get method threw an exception.
+   */
+  public static <V> V get(Future<V> future) {
+    try {
+      return future.get();
+    } catch (InterruptedException e) {
+      throw new FutureException(e);
+    } catch (ExecutionException e) {
+      throw new FutureException(e);
+    }
+  }
+
+  /**
+   * Get the future's value and return it to the caller.
+   *
+   * If the method is interrupted during computation, or the future throws an
+   * exception this is wrapped into an {@link FutureException} and rethrown.
+   *
+   * @param <V> the return type of the future.
+   * @param future the future itself
+   * @return the value of the future; null if the future returned null or it
+   *         threw an exception during completion.
+   */
+  public static <V> V getOrNull(Future<V> future) {
+    try {
+      return future.get();
+
+    } catch (InterruptedException e) {
+      log.warn("Interrupted while waiting on future, using empty result", e);
+      return null;
+
+    } catch (ExecutionException e) {
+      log.warn("Interrupted while waiting on future, using empty result", e);
+      return null;
+    }
+  }
+
+  /**
+   * Get the future's value and return it to the caller.
+   *
+   * If the method is interrupted during computation, or the future throws an
+   * exception, the event is logged and an empty collection is returned.
+   *
+   * @param <V> the return type of the future.
+   * @param future the future itself
+   * @return the value of the future.
+   */
+  @SuppressWarnings("unchecked")
+  public static <V> List<V> getOrEmptyList(Future<List<V>> future) {
+    try {
+      return future.get();
+
+    } catch (InterruptedException e) {
+      log.warn("Interrupted while waiting on future, using empty result", e);
+      return Lists.newArrayListWithCapacity(0);
+
+    } catch (ExecutionException e) {
+      log.warn("Error in future collection, using empty result", e);
+      return Lists.newArrayListWithCapacity(0);
+    }
+  }
+
+  /**
+   * Get the future's value and return it to the caller.
+   *
+   * If the method is interrupted during computation, or the future throws an
+   * exception, the event is logged and an empty collection is returned.
+   *
+   * @param <V> the return type of the future.
+   * @param future the future itself
+   * @return the value of the future.
+   */
+  @SuppressWarnings("unchecked")
+  public static <V> Set<V> getOrEmptySet(Future<Set<V>> future) {
+    try {
+      return future.get();
+
+    } catch (InterruptedException e) {
+      log.warn("Interrupted while waiting on future, using empty result", e);
+      return Sets.newHashSetWithExpectedSize(0);
+
+    } catch (ExecutionException e) {
+      log.warn("Error in future collection, using empty result", e);
+      return Sets.newHashSetWithExpectedSize(0);
+    }
+  }
+
+  /**
+   * Flatten a map of futures down to actual values.
+   *
+   * @param <K> type of the map entry key.
+   * @param <V> type of the map entry value.
+   * @param want the map of futures to resolve.
+   * @return the resulting map.
+   */
+  public static <K, V> Map<K, V> getMap(Map<K, Future<V>> want) {
+    Map<K, V> res = Maps.newHashMapWithExpectedSize(want.size());
+    for (Map.Entry<K, Future<V>> ent : want.entrySet()) {
+      res.put(ent.getKey(), get(ent.getValue()));
+    }
+    return res;
+  }
+
+  /**
+   * Wait for a future to complete, and discard its results.
+   *
+   * @param future the future to wait for completion of.
+   */
+  public static void waitFor(Future<Void> future) {
+    get(future);
+  }
+
+  /**
+   * Wait for multiple futures to complete, and discard all results.
+   *
+   * @param futures the futures to wait for completion of.
+   */
+  public static void waitFor(Future<Void>... futures) {
+    for (Future<Void> f : futures) {
+      waitFor(f);
+    }
+  }
+
+  /**
+   * Wait for multiple futures to complete, and discard all results.
+   *
+   * @param futures the futures to wait for completion of.
+   */
+  public static void waitFor(Iterable<? extends Future<Void>> futures) {
+    for (Future<Void> f : futures) {
+      waitFor(f);
+    }
+  }
+
+  private FutureUtil() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
index 36a52e2..61664f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -68,7 +69,7 @@
     this.userFactory = userFactory;
 
     change = c;
-    project = projectCache.get(change.getProject());
+    project = FutureUtil.get(projectCache.get(change.getProject()));
 
     for (final PatchSetApproval ca : all) {
       if (psId.equals(ca.getPatchSetId())) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/ChangeUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/ChangeUtilTest.java
new file mode 100644
index 0000000..dcc6d2f
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/ChangeUtilTest.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2010 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;
+
+import junit.framework.TestCase;
+
+public class ChangeUtilTest extends TestCase {
+
+  public void testInvertSortKey() {
+    assertEquals("ffffffffffffffff", ChangeUtil
+        .invertSortKey("0000000000000000"));
+
+    assertEquals("0000000000000001", ChangeUtil
+        .invertSortKey("fffffffffffffffe"));
+
+    assertEquals("0001600000000000", ChangeUtil
+        .invertSortKey("fffe9fffffffffff"));
+
+    assertEquals("/", ChangeUtil.invertSortKey("z"));
+
+    assertEquals("z", ChangeUtil.invertSortKey("/"));
+  }
+
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
index f819dac..11f6de6 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
@@ -20,6 +20,7 @@
 import static org.easymock.EasyMock.replay;
 import static org.easymock.EasyMock.verify;
 
+import com.google.common.util.concurrent.Futures;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.reviewdb.AccountGroup;
@@ -262,24 +263,22 @@
   }
 
   private Account.Id user(final String name, final String email) {
-    final AccountState s = makeUser(name, email);
-    expect(accountCache.get(eq(s.getAccount().getId()))).andReturn(s);
-    return s.getAccount().getId();
+    final Account s = makeUser(name, email);
+    expect(accountCache.getAccount(eq(s.getId()))) //
+        .andReturn(Futures.immediateFuture(s));
+    return s.getId();
   }
 
   private Account.Id userNoLookup(final String name, final String email) {
-    final AccountState s = makeUser(name, email);
-    return s.getAccount().getId();
+    final Account s = makeUser(name, email);
+    return s.getId();
   }
 
-  private AccountState makeUser(final String name, final String email) {
+  private Account makeUser(final String name, final String email) {
     final Account.Id userId = new Account.Id(42);
     final Account account = new Account(userId);
     account.setFullName(name);
     account.setPreferredEmail(email);
-    final AccountState s =
-        new AccountState(account, Collections.<AccountGroup.Id> emptySet(),
-            Collections.<AccountExternalId> emptySet());
-    return s;
+    return account;
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/NoSqlSchemaCreatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/NoSqlSchemaCreatorTest.java
new file mode 100644
index 0000000..6cb3d16
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/NoSqlSchemaCreatorTest.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2010 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.schema;
+
+import com.google.gerrit.reviewdb.CurrentSchemaVersion;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.SystemConfig;
+import com.google.gerrit.server.config.SystemConfigProvider;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.nosql.heap.MemoryDatabase;
+import com.google.inject.Guice;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+
+public class NoSqlSchemaCreatorTest extends TestCase {
+  private MemoryDatabase<ReviewDb> db;
+  private SchemaVersion schemaVersion;
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    db = new MemoryDatabase<ReviewDb>(ReviewDb.class);
+    schemaVersion =
+        Guice.createInjector(new SchemaVersion.Module()).getBinding(
+            Key.get(SchemaVersion.class, Current.class)).getProvider().get();
+  }
+
+  private CurrentSchemaVersion getSchemaVersion() throws OrmException {
+    final ReviewDb c = db.open();
+    try {
+      return c.schemaVersion().get(new CurrentSchemaVersion.Key());
+    } finally {
+      c.close();
+    }
+  }
+
+  private void assertSchemaVersion() throws OrmException {
+    final CurrentSchemaVersion act = getSchemaVersion();
+    TestCase.assertEquals(schemaVersion.getVersionNbr(), act.versionNbr);
+  }
+
+  private SystemConfig getSystemConfig() {
+    return new SystemConfigProvider(db, new Provider<SchemaVersion>() {
+      public SchemaVersion get() {
+        return schemaVersion;
+      }
+    }).get();
+  }
+
+  public void testCreateSchema() throws OrmException {
+    final ReviewDb c = db.open();
+    try {
+      new SchemaCreator(new File("."), schemaVersion).create(c);
+    } finally {
+      c.close();
+    }
+
+    assertSchemaVersion();
+    final SystemConfig config = getSystemConfig();
+    assertNotNull(config);
+    assertNotNull(config.adminGroupId);
+    assertNotNull(config.anonymousGroupId);
+    assertNotNull(config.registeredGroupId);
+
+    // By default sitePath is set to the current working directory.
+    //
+    File sitePath = new File(".").getAbsoluteFile();
+    if (sitePath.getName().equals(".")) {
+      sitePath = sitePath.getParentFile();
+    }
+    assertEquals(sitePath.getAbsolutePath(), config.sitePath);
+
+    // This is randomly generated and should be at least 20 bytes long.
+    //
+    assertNotNull(config.registerEmailPrivateKey);
+    assertTrue(20 < config.registerEmailPrivateKey.length());
+  }
+}
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
index 32bcf57..d48972f 100644
--- a/gerrit-sshd/pom.xml
+++ b/gerrit-sshd/pom.xml
@@ -64,5 +64,11 @@
       <artifactId>gerrit-server</artifactId>
       <version>${project.version}</version>
     </dependency>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-ehcache</artifactId>
+      <version>${project.version}</version>
+    </dependency>
   </dependencies>
 </project>
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index 977d209..e2a37f5 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -107,13 +107,13 @@
       }
     }
 
-    final Iterable<SshKeyCacheEntry> keyList = sshKeyCache.get(username);
-    final SshKeyCacheEntry key = find(keyList, suppliedKey);
+    final SshKeyCacheImpl.EntryList keyList = sshKeyCache.get(username);
+    final SshKeyCacheEntry key = find(keyList.getKeys(), suppliedKey);
     if (key == null) {
       final String err;
-      if (keyList == SshKeyCacheImpl.NO_SUCH_USER) {
+      if (keyList.getType() == SshKeyCacheImpl.EntryList.Type.NO_SUCH_USER) {
         err = "user-not-found";
-      } else if (keyList == SshKeyCacheImpl.NO_KEYS) {
+      } else if (keyList.getType() == SshKeyCacheImpl.EntryList.Type.NO_KEYS) {
         err = "key-list-empty";
       } else {
         err = "no-matching-key";
@@ -128,7 +128,7 @@
     // security check to ensure there aren't two users sharing the same
     // user name on the server.
     //
-    for (final SshKeyCacheEntry otherKey : keyList) {
+    for (SshKeyCacheEntry otherKey : keyList.getKeys()) {
       if (!key.getAccount().equals(otherKey.getAccount())) {
         sd.authenticationError(username, "keys-cross-accounts");
         return false;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index 2636ff2..b7c0761 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
@@ -185,7 +185,7 @@
 
   @Override
   public synchronized void start() {
-    if (acceptor == null) {
+    if (acceptor == null && !listen.isEmpty()) {
       checkConfig();
 
       acceptor = createAcceptor();
@@ -226,6 +226,10 @@
   }
 
   private List<HostKey> computeHostKeys() {
+    if (listen.isEmpty()) {
+      return Collections.emptyList();
+    }
+
     final List<PublicKey> keys = myHostKeys();
     final ArrayList<HostKey> r = new ArrayList<HostKey>();
     for (final PublicKey pub : keys) {
@@ -296,6 +300,10 @@
       return bind;
     }
 
+    if (want.length == 1 && isOff(want[0])) {
+      return bind;
+    }
+
     for (final String desc : want) {
       try {
         bind.add(SocketUtil.resolve(desc, DEFAULT_PORT));
@@ -306,6 +314,12 @@
     return bind;
   }
 
+  private static boolean isOff(String listenHostname) {
+    return "off".equalsIgnoreCase(listenHostname)
+        || "none".equalsIgnoreCase(listenHostname)
+        || "no".equalsIgnoreCase(listenHostname);
+  }
+
   @SuppressWarnings("unchecked")
   private void initProviderBouncyCastle() {
     setKeyExchangeFactories(Arrays.<NamedFactory<KeyExchange>> asList(
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheEntry.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheEntry.java
index 81e019e..cded3dc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheEntry.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheEntry.java
@@ -16,16 +16,45 @@
 
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountSshKey;
+import com.google.gwtorm.client.Column;
 
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
 import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
 
 class SshKeyCacheEntry {
-  private final AccountSshKey.Id id;
-  private final PublicKey publicKey;
+  private final static PublicKey INVALID_KEY = new PublicKey() {
+    @Override
+    public String getAlgorithm() {
+      throw new UnsupportedOperationException();
+    }
 
-  SshKeyCacheEntry(final AccountSshKey.Id i, final PublicKey k) {
+    @Override
+    public byte[] getEncoded() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getFormat() {
+      throw new UnsupportedOperationException();
+    }
+  };
+
+  @Column(id = 1)
+  protected AccountSshKey.Id id;
+
+  @Column(id = 2)
+  protected AccountSshKey key;
+
+  private transient volatile PublicKey publicKey;
+
+  SshKeyCacheEntry(final AccountSshKey.Id i, final AccountSshKey k)
+      throws NoSuchAlgorithmException, InvalidKeySpecException,
+      NoSuchProviderException {
     id = i;
-    publicKey = k;
+    key = k;
+    publicKey = SshUtil.parse(k);
   }
 
   Account.Id getAccount() {
@@ -33,6 +62,18 @@
   }
 
   boolean match(final PublicKey inkey) {
+    if (publicKey == null) {
+      try {
+        publicKey = SshUtil.parse(key);
+      } catch (OutOfMemoryError e) {
+        // This is the only case where we assume the problem has nothing
+        // to do with the key object, and instead we must abort this load.
+        //
+        throw e;
+      } catch (Throwable e) {
+        publicKey = INVALID_KEY;
+      }
+    }
     return publicKey.equals(inkey);
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index c5f64f7..fb4bcef 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -14,16 +14,21 @@
 
 package com.google.gerrit.sshd;
 
-import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
-
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.reviewdb.AccountSshKey;
 import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.server.util.FutureUtil;
+import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
@@ -38,8 +43,7 @@
 import java.security.NoSuchAlgorithmException;
 import java.security.NoSuchProviderException;
 import java.security.spec.InvalidKeySpecException;
-import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 
@@ -50,49 +54,44 @@
       LoggerFactory.getLogger(SshKeyCacheImpl.class);
   private static final String CACHE_NAME = "sshkeys";
 
-  static final Iterable<SshKeyCacheEntry> NO_SUCH_USER = none();
-  static final Iterable<SshKeyCacheEntry> NO_KEYS = none();
-
   public static Module module() {
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<String, Iterable<SshKeyCacheEntry>>> type =
-            new TypeLiteral<Cache<String, Iterable<SshKeyCacheEntry>>>() {};
-        core(type, CACHE_NAME).populateWith(Loader.class);
+        final TypeLiteral<Cache<Username, EntryList>> type =
+            new TypeLiteral<Cache<Username, EntryList>>() {};
+        cache(type, CACHE_NAME).populateWith(Loader.class);
         bind(SshKeyCacheImpl.class);
         bind(SshKeyCache.class).to(SshKeyCacheImpl.class);
       }
     };
   }
 
-  private static Iterable<SshKeyCacheEntry> none() {
-    return Collections.unmodifiableCollection(Arrays
-        .asList(new SshKeyCacheEntry[0]));
-  }
-
-  private final Cache<String, Iterable<SshKeyCacheEntry>> cache;
+  private final Cache<Username, EntryList> cache;
 
   @Inject
-  SshKeyCacheImpl(
-      @Named(CACHE_NAME) final Cache<String, Iterable<SshKeyCacheEntry>> cache) {
+  SshKeyCacheImpl(@Named(CACHE_NAME) final Cache<Username, EntryList> cache) {
     this.cache = cache;
   }
 
-  public Iterable<SshKeyCacheEntry> get(String username) {
-    return cache.get(username);
+  EntryList get(String username) {
+    return FutureUtil.get(cache.get(new Username(username)));
   }
 
-  public void evict(String username) {
-    cache.remove(username);
+  public ListenableFuture<Void> evictAsync(String username) {
+    if (username != null) {
+      return cache.removeAsync(new Username(username));
+    } else {
+      return Futures.immediateFuture(null);
+    }
   }
 
   @Override
   public AccountSshKey create(AccountSshKey.Id id, String encoded)
       throws InvalidSshKeyException {
     try {
-      final AccountSshKey key =
-          new AccountSshKey(id, SshUtil.toOpenSshPublicKey(encoded));
+      encoded = SshUtil.toOpenSshPublicKey(encoded);
+      AccountSshKey key = new AccountSshKey(id, encoded);
       SshUtil.parse(key);
       return key;
     } catch (NoSuchAlgorithmException e) {
@@ -107,50 +106,97 @@
     }
   }
 
-  static class Loader extends EntryCreator<String, Iterable<SshKeyCacheEntry>> {
+  static class Username {
+    @Column(id = 1)
+    String name;
+
+    Username() {
+    }
+
+    Username(String name) {
+      this.name = name;
+    }
+  }
+
+  static class EntryList {
+    static enum Type {
+      VALID_HAS_KEYS, INVALID_USER, NO_SUCH_USER, NO_KEYS
+    }
+
+    @Column(id = 1)
+    Type type;
+
+    @Column(id = 2)
+    Collection<SshKeyCacheEntry> keys;
+
+    EntryList() {
+      type = Type.NO_KEYS;
+      keys = Collections.emptyList();
+    }
+
+    EntryList(Type t, Collection<SshKeyCacheEntry> k) {
+      this.type = t;
+      this.keys = k;
+    }
+
+    Collection<SshKeyCacheEntry> getKeys() {
+      return keys;
+    }
+
+    Type getType() {
+      return type;
+    }
+  }
+
+  static class Loader extends EntryCreator<Username, EntryList> {
     private final SchemaFactory<ReviewDb> schema;
+    private final AccountCache accountCache;
 
     @Inject
-    Loader(SchemaFactory<ReviewDb> schema) {
+    Loader(SchemaFactory<ReviewDb> schema, AccountCache accountCache) {
       this.schema = schema;
+      this.accountCache = accountCache;
     }
 
     @Override
-    public Iterable<SshKeyCacheEntry> createEntry(String username)
-        throws Exception {
+    public EntryList createEntry(Username username) throws Exception {
+      AccountExternalId user = FutureUtil.get(accountCache.get( //
+          AccountExternalId.forUsername(username.name)));
+      if (user == null) {
+        Collection<SshKeyCacheEntry> none = Collections.emptyList();
+        return new EntryList(EntryList.Type.NO_SUCH_USER, none);
+      }
+
+      final Account.Id accountId = user.getAccountId();
       final ReviewDb db = schema.open();
       try {
-        final AccountExternalId.Key key =
-            new AccountExternalId.Key(SCHEME_USERNAME, username);
-        final AccountExternalId user = db.accountExternalIds().get(key);
-        if (user == null) {
-          return NO_SUCH_USER;
-        }
-
-        final List<SshKeyCacheEntry> kl = new ArrayList<SshKeyCacheEntry>(4);
-        for (AccountSshKey k : db.accountSshKeys().byAccount(
-            user.getAccountId())) {
+        List<SshKeyCacheEntry> kl = Lists.newArrayListWithExpectedSize(4);
+        for (AccountSshKey k : db.accountSshKeys().byAccount(accountId)) {
           if (k.isValid()) {
             add(db, kl, k);
           }
         }
         if (kl.isEmpty()) {
-          return NO_KEYS;
+          Collection<SshKeyCacheEntry> none = Collections.emptyList();
+          return new EntryList(EntryList.Type.NO_KEYS, none);
+        } else {
+          kl = Collections.unmodifiableList(kl);
+          return new EntryList(EntryList.Type.VALID_HAS_KEYS, kl);
         }
-        return Collections.unmodifiableList(kl);
       } finally {
         db.close();
       }
     }
 
     @Override
-    public Iterable<SshKeyCacheEntry> missing(String username) {
-      return Collections.emptyList();
+    public EntryList missing(Username username) {
+      Collection<SshKeyCacheEntry> none = Collections.emptyList();
+      return new EntryList(EntryList.Type.INVALID_USER, none);
     }
 
     private void add(ReviewDb db, List<SshKeyCacheEntry> kl, AccountSshKey k) {
       try {
-        kl.add(new SshKeyCacheEntry(k.getKey(), SshUtil.parse(k)));
+        kl.add(new SshKeyCacheEntry(k.getKey(), k));
       } catch (OutOfMemoryError e) {
         // This is the only case where we assume the problem has nothing
         // to do with the key object, and instead we must abort this load.
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupIdHandler.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupIdHandler.java
index 13ff1b1..9ac0465 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupIdHandler.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupIdHandler.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -42,7 +43,7 @@
   public final int parseArguments(final Parameters params)
       throws CmdLineException {
     final String n = params.getParameter(0);
-    final AccountGroup group = groupCache.get(new AccountGroup.NameKey(n));
+    final AccountGroup group = get(n);
     if (group == null) {
       throw new CmdLineException(owner, "Group \"" + n + "\" does not exist");
     }
@@ -50,6 +51,10 @@
     return 1;
   }
 
+  private AccountGroup get(String name) {
+    return FutureUtil.getOrNull(groupCache.get(new AccountGroup.NameKey(name)));
+  }
+
   @Override
   public final String getDefaultMetaVariable() {
     return "GROUP";
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminCreateAccount.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminCreateAccount.java
index 06e92ca..8744cfa 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminCreateAccount.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminCreateAccount.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountExternalId;
@@ -23,14 +24,15 @@
 import com.google.gerrit.reviewdb.AccountSshKey;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountByEmailCache;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gerrit.sshd.AdminCommand;
 import com.google.gerrit.sshd.BaseCommand;
 import com.google.gwtorm.client.OrmDuplicateKeyException;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.internal.Lists;
 
 import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Argument;
@@ -75,9 +77,6 @@
   @Inject
   private AccountCache accountCache;
 
-  @Inject
-  private AccountByEmailCache byEmailCache;
-
   @Override
   public void start(final Environment env) {
     startThread(new CommandRunnable() {
@@ -100,32 +99,39 @@
     final AccountSshKey key = readSshKey(id);
 
     AccountExternalId extUser =
-        new AccountExternalId(id, new AccountExternalId.Key(
-            AccountExternalId.SCHEME_USERNAME, username));
+        new AccountExternalId(id, AccountExternalId.forUsername(username));
 
-    if (db.accountExternalIds().get(extUser.getKey()) != null) {
+    if (FutureUtil.get(accountCache.get(extUser.getKey())) != null) {
       throw die("username '" + username + "' already exists");
     }
-    if (email != null && db.accountExternalIds().get(getEmailKey()) != null) {
+    if (email != null && FutureUtil.get(accountCache.byEmail(email)) != null) {
       throw die("email '" + email + "' already exists");
     }
 
+    List<ListenableFuture<Void>> evictions = Lists.newArrayList();
     try {
       db.accountExternalIds().insert(Collections.singleton(extUser));
+      evictions.add(accountCache.evictAsync(extUser.getKey()));
     } catch (OrmDuplicateKeyException duplicateKey) {
+      FutureUtil.waitFor(evictions);
       throw die("username '" + username + "' already exists");
     }
 
     if (email != null) {
-      AccountExternalId extMailto = new AccountExternalId(id, getEmailKey());
+      AccountExternalId extMailto =
+          new AccountExternalId(id, new AccountExternalId.Key(
+              AccountExternalId.SCHEME_MAILTO, email));
       extMailto.setEmailAddress(email);
       try {
         db.accountExternalIds().insert(Collections.singleton(extMailto));
+        evictions.add(accountCache.evictAsync(extMailto.getKey()));
       } catch (OrmDuplicateKeyException duplicateKey) {
         try {
           db.accountExternalIds().delete(Collections.singleton(extUser));
+          evictions.add(accountCache.evictAsync(extUser.getKey()));
         } catch (OrmException cleanupError) {
         }
+        FutureUtil.waitFor(evictions);
         throw die("email '" + email + "' already exists");
       }
     }
@@ -147,13 +153,10 @@
       db.accountGroupMembers().insert(Collections.singleton(m));
     }
 
-    sshKeyCache.evict(username);
-    accountCache.evictByUsername(username);
-    byEmailCache.evict(email);
-  }
-
-  private AccountExternalId.Key getEmailKey() {
-    return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
+    evictions.add(sshKeyCache.evictAsync(username));
+    evictions.add(accountCache.evictAsync(extUser.getKey()));
+    evictions.add(accountCache.evictEmailAsync(email));
+    FutureUtil.waitFor(evictions);
   }
 
   private AccountSshKey readSshKey(final Account.Id id)
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminReplicate.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminReplicate.java
index 8e2c74f..6db74c4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminReplicate.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminReplicate.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.server.git.PushAllProjectsOp;
 import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gerrit.sshd.AdminCommand;
 import com.google.gerrit.sshd.BaseCommand;
 import com.google.inject.Inject;
@@ -75,9 +76,9 @@
       pushAllOpFactory.create(urlMatch).start(0, TimeUnit.SECONDS);
 
     } else {
-      for (final String name : projectNames) {
-        final Project.NameKey key = new Project.NameKey(name);
-        if (projectCache.get(key) != null) {
+      for (String name : projectNames) {
+        Project.NameKey key = new Project.NameKey(name);
+        if (FutureUtil.getOrNull(projectCache.get(key)) != null) {
           replication.scheduleFullSync(key, urlMatch);
         } else {
           throw new Failure(1, "error: '" + name + "': not a Gerrit project");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index bf82a42..e9adbc5 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gerrit.sshd.AdminCommand;
 import com.google.gerrit.sshd.BaseCommand;
 import com.google.gwtorm.client.OrmException;
@@ -79,7 +80,7 @@
       //
       Project.NameKey gp = newParent.getProject().getParent();
       while (gp != null && grandParents.add(gp)) {
-        final ProjectState s = projectCache.get(gp);
+        ProjectState s = FutureUtil.get(projectCache.get(gp));
         if (s != null) {
           gp = s.getProject().getParent();
         } else {
@@ -126,7 +127,7 @@
 
     // Invalidate all projects in cache since inherited rights were changed.
     //
-    projectCache.evictAll();
+    FutureUtil.waitFor(projectCache.evictAllAsync());
 
     if (err.length() > 0) {
       while (err.charAt(err.length() - 1) == '\n') {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
index 313b9dc..083759c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.server.cache.CachePool;
+import com.google.gerrit.ehcache.EhcachePoolImpl;
 import com.google.gerrit.sshd.BaseCommand;
 import com.google.inject.Inject;
 
@@ -27,7 +27,7 @@
 
 abstract class CacheCommand extends BaseCommand {
   @Inject
-  protected CachePool cachePool;
+  protected EhcachePoolImpl cachePool;
 
   protected SortedSet<String> cacheNames() {
     final SortedSet<String> names = new TreeSet<String>();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjects.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjects.java
index 7618432..f0833a1 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjects.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjects.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.common.collect.Lists;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
@@ -22,6 +23,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gerrit.sshd.BaseCommand;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
@@ -36,6 +38,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.TreeMap;
+import java.util.concurrent.Future;
 
 final class ListProjects extends BaseCommand {
   private static final String NODE_PREFIX = "|-- ";
@@ -93,24 +96,27 @@
     }
 
     try {
-      for (final Project p : db.projects().all()) {
+      List<Future<ProjectState>> want = Lists.newArrayList();
+      for (Project p : db.projects().all()) {
         if (p.getNameKey().equals(wildProject)) {
           // This project "doesn't exist". At least not as a repository.
           //
-          continue;
+        } else {
+          want.add(projectCache.get(p.getNameKey()));
         }
+      }
 
-        final ProjectState e = projectCache.get(p.getNameKey());
+      for (Future<ProjectState> fEnt : want) {
+        final ProjectState e = FutureUtil.getOrNull(fEnt);
         if (e == null) {
           // If we can't get it from the cache, pretend its not present.
           //
           continue;
         }
 
+        final Project p = e.getProject();
         final ProjectControl pctl = e.controlFor(currentUser);
-
         if (!showTree) {
-
           if (!pctl.isVisible()) {
             // Require the project itself to be visible to the user.
             //
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
index e68c747..148d48e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
@@ -79,16 +79,20 @@
     try {
       db = dbFactory.open();
       try {
-        connection = ((JdbcSchema) db).getConnection();
-        connection.setAutoCommit(true);
+        if (db instanceof JdbcSchema) {
+          connection = ((JdbcSchema) db).getConnection();
+          connection.setAutoCommit(true);
 
-        statement = connection.createStatement();
-        try {
-          showBanner();
-          readEvalPrintLoop();
-        } finally {
-          statement.close();
-          statement = null;
+          statement = connection.createStatement();
+          try {
+            showBanner();
+            readEvalPrintLoop();
+          } finally {
+            statement.close();
+            statement = null;
+          }
+        } else {
+          out.println("fatal: Backend database is not SQL; aborting.");
         }
       } finally {
         db.close();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
index a196a3e..e3a0170 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.server.git.WorkQueue.Task;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.FutureUtil;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
 import com.google.gerrit.sshd.BaseCommand;
@@ -146,7 +147,7 @@
 
         ProjectState e = null;
         if (projectName != null) {
-          e = projectCache.get(projectName);
+          e = FutureUtil.getOrNull(projectCache.get(projectName));
         }
 
         regularUserCanSee = e != null && e.controlFor(userProvider).isVisible();
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java
index 233d53d..50f1507 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java
@@ -15,6 +15,10 @@
 package com.google.gerrit.httpd;
 
 import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.jdbc.Database;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
@@ -25,14 +29,22 @@
 
 /** Provides access to the {@code ReviewDb} DataSource. */
 @Singleton
-final class ReviewDbDataSourceProvider implements Provider<DataSource>,
-    LifecycleListener {
-  private DataSource ds;
+final class ReviewDbDataSourceProvider implements
+    Provider<SchemaFactory<ReviewDb>>, LifecycleListener {
+  private SchemaFactory<ReviewDb> ds;
+  private DataSource dataSource;
 
   @Override
-  public synchronized DataSource get() {
+  public synchronized SchemaFactory<ReviewDb> get() {
+    if (dataSource == null) {
+      dataSource = open();
+    }
     if (ds == null) {
-      ds = open();
+      try {
+        ds = new Database<ReviewDb>(dataSource, ReviewDb.class);
+      } catch (OrmException err) {
+        throw new ProvisionException("Cannot initialize database", err);
+      }
     }
     return ds;
   }
@@ -43,8 +55,8 @@
 
   @Override
   public synchronized void stop() {
-    if (ds != null) {
-      closeDataSource(ds);
+    if (dataSource != null) {
+      closeDataSource(dataSource);
     }
   }
 
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 19c16ca..06ba82d 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -17,8 +17,10 @@
 import static com.google.inject.Scopes.SINGLETON;
 import static com.google.inject.Stage.PRODUCTION;
 
+import com.google.gerrit.ehcache.EhcachePoolImpl;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.config.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
 import com.google.gerrit.server.config.GerritGlobalModule;
@@ -26,18 +28,20 @@
 import com.google.gerrit.server.config.MasterNodeStartup;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePathFromSystemConfigProvider;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.mail.SmtpEmailSender;
 import com.google.gerrit.server.schema.DataSourceProvider;
-import com.google.gerrit.server.schema.DatabaseModule;
+import com.google.gerrit.server.schema.SchemaVersion;
 import com.google.gerrit.sshd.SshModule;
 import com.google.gerrit.sshd.commands.MasterCommandModule;
+import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.AbstractModule;
 import com.google.inject.CreationException;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
-import com.google.inject.Key;
 import com.google.inject.Module;
 import com.google.inject.Provider;
-import com.google.inject.name.Names;
+import com.google.inject.TypeLiteral;
 import com.google.inject.servlet.GuiceServletContextListener;
 import com.google.inject.spi.Message;
 
@@ -51,7 +55,6 @@
 
 import javax.servlet.ServletContextEvent;
 import javax.servlet.http.HttpServletRequest;
-import javax.sql.DataSource;
 
 /** Configures the web application environment for Gerrit Code Review. */
 public class WebAppInitializer extends GuiceServletContextListener {
@@ -129,7 +132,7 @@
           bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
           bind(DataSourceProvider.Context.class).toInstance(
               DataSourceProvider.Context.MULTI_USER);
-          bind(Key.get(DataSource.class, Names.named("ReviewDb"))).toProvider(
+          bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}).toProvider(
               DataSourceProvider.class).in(SINGLETON);
           listener().to(DataSourceProvider.class);
         }
@@ -140,13 +143,13 @@
       modules.add(new LifecycleModule() {
         @Override
         protected void configure() {
-          bind(Key.get(DataSource.class, Names.named("ReviewDb"))).toProvider(
+          bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}).toProvider(
               ReviewDbDataSourceProvider.class).in(SINGLETON);
           listener().to(ReviewDbDataSourceProvider.class);
         }
       });
     }
-    modules.add(new DatabaseModule());
+    modules.add(new SchemaVersion.Module());
     return Guice.createInjector(PRODUCTION, modules);
   }
 
@@ -173,6 +176,9 @@
   private Injector createSysInjector() {
     final List<Module> modules = new ArrayList<Module>();
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
+    modules.add(new SmtpEmailSender.Module());
+    modules.add(new EhcachePoolImpl.Module());
+    modules.add(new LocalDiskRepositoryManager.Module());
     modules.add(new CanonicalWebUrlModule() {
       @Override
       protected Class<? extends Provider<String>> provider() {
@@ -193,6 +199,8 @@
   private Injector createWebInjector() {
     final List<Module> modules = new ArrayList<Module>();
     modules.add(sshInjector.getInstance(WebModule.class));
+    modules.add(sshInjector.getInstance(WebSshGlueModule.class));
+    modules.add(new SessionCacheCleaner.Module());
     return sysInjector.createChildInjector(modules);
   }
 
diff --git a/pom.xml b/pom.xml
index 26a065a..93ee2be 100644
--- a/pom.xml
+++ b/pom.xml
@@ -47,7 +47,7 @@
 
   <properties>
     <jgitVersion>0.8.4.242-g09130b8</jgitVersion>
-    <gwtormVersion>1.1.4</gwtormVersion>
+    <gwtormVersion>1.2-SNAPSHOT</gwtormVersion>
     <gwtjsonrpcVersion>1.2.2</gwtjsonrpcVersion>
     <gwtexpuiVersion>1.2.1</gwtexpuiVersion>
     <gwtVersion>2.0.4</gwtVersion>
@@ -74,6 +74,7 @@
     <module>gerrit-util-ssl</module>
 
     <module>gerrit-common</module>
+    <module>gerrit-ehcache</module>
     <module>gerrit-httpd</module>
     <module>gerrit-launcher</module>
     <module>gerrit-main</module>
@@ -531,6 +532,12 @@
       </dependency>
 
       <dependency>
+        <groupId>com.google.guava</groupId>
+        <artifactId>guava</artifactId>
+        <version>r06</version>
+      </dependency>
+
+      <dependency>
         <groupId>commons-net</groupId>
         <artifactId>commons-net</artifactId>
         <version>2.0</version>
@@ -561,6 +568,12 @@
       </dependency>
 
       <dependency>
+        <groupId>com.google.protobuf</groupId>
+        <artifactId>protobuf-java</artifactId>
+        <version>2.3.0</version>
+      </dependency>
+
+      <dependency>
         <groupId>eu.medsea.mimeutil</groupId>
         <artifactId>mime-util</artifactId>
         <version>2.1.3</version>