Enable rolling upgrade to next versions

Gerrit version N and N+1 (e.g. Gerrit v3.1 is N, v3.2 is N+1)
have typically a semi-compatible schema which enables the ability
to perform a live-upgrade to the new release without the need of
a general outage.

Document how to enable rolling upgrade mode and make the schema
version check more flexible so that two versions can
share the same repositories over NFS.

Test plan:

1. Install Gerrit version N on two nodes sharing the repositories
   over NFS.
2. Set gerrit.experimentalRollingUpgrade to true
3. Upgrade one of the two nodes to version N+1
4. Both nodes should be able to stop/start without issues
5. Both nodes should be able to serve read/writes without issues
6. Upgrade the other node then confirm as above.

Change-Id: I8ceb352365da2eb553d82495c9635eb034e57352
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index b8b66ef..177862a 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2127,6 +2127,36 @@
 +
 Defaults to the full hostname of the Gerrit server.
 
+[[gerrit.experimentalRollingUpgrade]]gerrit.experimentalRollingUpgrade::
++
+Enable Gerrit rolling upgrade to the next version.
+For example if Gerrit v3.1 is version N (All-Projects:refs/meta/version=181)
+then its next version N+1 is v3.2 (All-Projects:refs/meta/version=183).
+Allow Gerrit to start even if the underlying schema version has been bumped to
+the next Gerrit version.
++
+Set to true if Gerrit is installed in
+[high-availability configuration](https://gerrit.googlesource.com/plugins/high-availability/+/refs/heads/master/README.md)
+during the rolling upgrade to the next version.
++
+By default false.
++
+The rolling upgrade process, at high level, assumes that Gerrit is installed
+on two or more nodes sharing the repositories over NFS. The upgrade is composed
+of the following steps:
++
+1. Set gerrit.experimentalRollingUpgrade to true on all Gerrit masters
+2. Set the first master unhealthy
+3. Shutdown the first master and [upgrade](install.html#init) to the next version
+4. Startup the first master, wait for the online reindex to complete
+5. Verify the the first master upgrade is successful and online reindex is complete
+6. Set the first master healthy
+7. Repeat steps 2. to 6. for all the other Gerrit nodes
++
+[WARNING]
+Rolling upgrade may or may not be possible depending on the changes introduced
+by the target version of the upgrade. Refer to the release notes and check whether
+the rolling upgrade is possible or not and the associated constraints.
 
 [[gerrit.serverId]]gerrit.serverId::
 +
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java b/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java
index 33534fc..0360ec0 100644
--- a/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java
@@ -14,15 +14,20 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.events.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.Inject;
 import com.google.inject.Module;
 import com.google.inject.ProvisionException;
+import org.eclipse.jgit.lib.Config;
 
 public class NoteDbSchemaVersionCheck implements LifecycleListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   public static Module module() {
     return new LifecycleModule() {
       @Override
@@ -34,11 +39,16 @@
 
   private final NoteDbSchemaVersionManager versionManager;
   private final SitePaths sitePaths;
+  private Config gerritConfig;
 
   @Inject
-  NoteDbSchemaVersionCheck(NoteDbSchemaVersionManager versionManager, SitePaths sitePaths) {
+  NoteDbSchemaVersionCheck(
+      NoteDbSchemaVersionManager versionManager,
+      SitePaths sitePaths,
+      @GerritServerConfig Config gerritConfig) {
     this.versionManager = versionManager;
     this.sitePaths = sitePaths;
+    this.gerritConfig = gerritConfig;
   }
 
   @Override
@@ -53,7 +63,18 @@
                 sitePaths.site_path.toAbsolutePath()));
       }
       int expected = NoteDbSchemaVersions.LATEST;
-      if (current != expected) {
+
+      if (current > expected
+          && gerritConfig.getBoolean("gerrit", "experimentalRollingUpgrade", false)) {
+        logger.atWarning().log(
+            "Gerrit has detected refs/meta/version %d different than the expected %d."
+                + "Bear in mind that this is supported ONLY for rolling upgrades to immediate next "
+                + "Gerrit version (e.g. v3.1 to v3.2). If this is not expected, remove gerrit.experimentalRollingUpgrade "
+                + "from $GERRIT_SITE/etc/gerrit.config and restart Gerrit."
+                + "Please note that gerrit.experimentalRollingUpgrade is intended to be used "
+                + "for the rolling upgrade phase only and should be disabled afterwards.",
+            current, expected);
+      } else if (current != expected) {
         String advice =
             current > expected
                 ? "Downgrade is not supported"
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java
new file mode 100644
index 0000000..a5fd4a2
--- /dev/null
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2020 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.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.testing.InMemoryRepositoryManager;
+import com.google.inject.ProvisionException;
+import java.io.IOException;
+import java.nio.file.Paths;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+public class NoteDbSchemaVersionCheckTest {
+  private NoteDbSchemaVersionManager versionManager;
+  private SitePaths sitePaths;
+
+  @Before
+  public void setup() throws Exception {
+    AllProjectsName allProjectsName = new AllProjectsName("All-Projects");
+    GitRepositoryManager repoManager = new InMemoryRepositoryManager();
+    repoManager.createRepository(allProjectsName);
+    versionManager = new NoteDbSchemaVersionManager(allProjectsName, repoManager);
+    versionManager.init();
+
+    sitePaths = new SitePaths(Paths.get("/tmp/foo"));
+  }
+
+  @Test
+  public void shouldNotFailIfCurrentVersionIsExpected() {
+    new NoteDbSchemaVersionCheck(versionManager, sitePaths, new Config()).start();
+    // No exceptions should be thrown
+  }
+
+  @Test
+  public void shouldFailIfCurrentVersionIsOneMoreThanExpected() throws IOException {
+    versionManager.increment(NoteDbSchemaVersions.LATEST);
+
+    ProvisionException e =
+        assertThrows(
+            ProvisionException.class,
+            () -> new NoteDbSchemaVersionCheck(versionManager, sitePaths, new Config()).start());
+
+    assertThat(e)
+        .hasMessageThat()
+        .contains("Unsupported schema version " + (NoteDbSchemaVersions.LATEST + 1));
+  }
+
+  @Test
+  public void
+      shouldNotFailWithExperimentalRollingUpgradeEnabledAndCurrentVersionIsOneMoreThanExpected()
+          throws IOException {
+    Config gerritConfig = new Config();
+    gerritConfig.setBoolean("gerrit", null, "experimentalRollingUpgrade", true);
+    versionManager.increment(NoteDbSchemaVersions.LATEST);
+
+    NoteDbSchemaVersionCheck versionCheck =
+        new NoteDbSchemaVersionCheck(versionManager, sitePaths, gerritConfig);
+    versionCheck.start();
+    // No exceptions should be thrown
+  }
+}