Update project version on global ref-db only when more recent
The project version (refs/multi-site/version) on the local project is more
up-to-date than the one on global ref-db, update unconditionally
without requiring the local SHA1 to be the same of the global ref-db.
When refs are updated in a short period of time (e.g. refs/multi-site/version
and refs/changes/N/NNNN/meta) it is not guaranteed that the replication
will be able to push all the intermediate SHA1s. Skipping SHA1s to push
for non-fast-forward branches like refs/multi-site/version is a valid
use-case and thus can be performed as long as the local version is
the most recent one.
Failing to update the global ref-db project version causes split-brain
with all the other nodes because the replication would filter out
the refs out of sync.
Bug: Issue 12413
Change-Id: Ib5e49edd3ba37ed60f34736f7af5e5a324248e51
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdate.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdate.java
index a8f344e..d13ed79 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdate.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdate.java
@@ -18,10 +18,8 @@
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError;
-import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
-import com.google.common.primitives.Ints;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.events.Event;
@@ -42,8 +40,6 @@
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectIdRef;
import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
@@ -105,23 +101,20 @@
}
try {
Project.NameKey projectNameKey = refUpdatedEvent.getProjectNameKey();
- Ref currentProjectVersionRef = getLocalProjectVersionRef(refUpdatedEvent.getProjectNameKey());
- Optional<Long> currentProjectVersionValue =
- getLongFromObjectId(projectNameKey.get(), currentProjectVersionRef.getObjectId());
Optional<Long> lastRefUpdatedTimestamp = getLastRefUpdatedTimestamp(projectNameKey, refName);
- Optional<ObjectId> newProjectVersionObjectId =
+ Optional<RefUpdate> newProjectVersionRefUpdate =
updateLocalProjectVersion(projectNameKey, lastRefUpdatedTimestamp);
- if (newProjectVersionObjectId.isPresent()) {
+ if (newProjectVersionRefUpdate.isPresent() && lastRefUpdatedTimestamp.isPresent()) {
verLogger.log(projectNameKey, lastRefUpdatedTimestamp.get(), 0L);
- updateSharedProjectVersion(
+ if (updateSharedProjectVersion(
projectNameKey,
- currentProjectVersionRef,
- newProjectVersionObjectId.get(),
- currentProjectVersionValue,
- lastRefUpdatedTimestamp);
+ newProjectVersionRefUpdate.get().getNewObjectId(),
+ lastRefUpdatedTimestamp.get())) {
+ gitReferenceUpdated.fire(projectNameKey, newProjectVersionRefUpdate.get(), null);
+ }
} else {
logger.atWarning().log(
"Ref %s not found on projet %s: skipping project version update",
@@ -149,19 +142,6 @@
return newId;
}
- private Ref getLocalProjectVersionRef(Project.NameKey projectNameKey)
- throws LocalProjectVersionUpdateException {
- try (Repository repository = gitRepositoryManager.openRepository(projectNameKey)) {
- Ref ref = repository.findRef(MULTI_SITE_VERSIONING_REF);
- return ref != null ? ref : NULL_PROJECT_VERSION_REF;
- } catch (IOException e) {
- String message =
- String.format("Error while getting current version ref for %s", projectNameKey.get());
- logger.atSevere().withCause(e).log(message);
- throw new LocalProjectVersionUpdateException(message);
- }
- }
-
private Optional<Long> getLastRefUpdatedTimestamp(Project.NameKey projectNameKey, String refName)
throws LocalProjectVersionUpdateException {
logger.atFine().log(
@@ -187,33 +167,49 @@
}
}
- private void updateSharedProjectVersion(
- Project.NameKey projectNameKey,
- Ref currentRef,
- ObjectId newObjectId,
- Optional<Long> currentVersion,
- Optional<Long> newVersion)
+ private boolean updateSharedProjectVersion(
+ Project.NameKey projectNameKey, ObjectId newObjectId, Long newVersion)
throws SharedProjectVersionUpdateException {
- logger.atFine().log(
- String.format(
- "Updating shared project version for %s. Current value %s, new value: %s",
- projectNameKey.get(), currentRef.getObjectId(), newObjectId));
+ Ref sharedRef =
+ sharedRefDb
+ .get(projectNameKey, MULTI_SITE_VERSIONING_REF, String.class)
+ .map(
+ (String objectId) ->
+ new ObjectIdRef.Unpeeled(
+ Ref.Storage.NEW, MULTI_SITE_VERSIONING_REF, ObjectId.fromString(objectId)))
+ .orElse(
+ new ObjectIdRef.Unpeeled(
+ Ref.Storage.NEW, MULTI_SITE_VERSIONING_REF, ObjectId.zeroId()));
+ Optional<Long> sharedVersion =
+ sharedRefDb
+ .get(projectNameKey, MULTI_SITE_VERSIONING_VALUE_REF, String.class)
+ .map(Long::parseLong);
+
try {
- if (!sharedRefDb.exists(projectNameKey, MULTI_SITE_VERSIONING_REF)) {
- currentRef =
- new ObjectIdRef.Unpeeled(Ref.Storage.NEW, MULTI_SITE_VERSIONING_REF, ObjectId.zeroId());
- }
- if (!sharedRefDb.exists(projectNameKey, MULTI_SITE_VERSIONING_VALUE_REF)) {
- currentVersion = Optional.empty();
+ if (sharedVersion.isPresent() && sharedVersion.get() >= newVersion) {
+ logger.atWarning().log(
+ String.format(
+ "NOT Updating project %s version %s (value=%d) in shared ref-db because is more recent than the local one %s (value=%d) ",
+ projectNameKey.get(),
+ newObjectId,
+ newVersion,
+ sharedRef.getObjectId().getName(),
+ sharedVersion.get()));
+ return false;
}
- boolean success = sharedRefDb.compareAndPut(projectNameKey, currentRef, newObjectId);
+ logger.atFine().log(
+ String.format(
+ "Updating shared project %s version to %s (value=%d)",
+ projectNameKey.get(), newObjectId, newVersion));
+
+ boolean success = sharedRefDb.compareAndPut(projectNameKey, sharedRef, newObjectId);
if (!success) {
String message =
String.format(
"Project version blob update failed for %s. Current value %s, new value: %s",
- projectNameKey.get(), safeGetObjectId(currentRef), newObjectId);
+ projectNameKey.get(), safeGetObjectId(sharedRef), newObjectId);
logger.atSevere().log(message);
throw new SharedProjectVersionUpdateException(message);
}
@@ -222,22 +218,24 @@
sharedRefDb.compareAndPut(
projectNameKey,
MULTI_SITE_VERSIONING_VALUE_REF,
- currentVersion.map(Object::toString).orElse(null),
- newVersion.map(Object::toString).orElse(null));
+ sharedVersion.map(Object::toString).orElse(null),
+ newVersion.toString());
if (!success) {
String message =
String.format(
"Project version update failed for %s. Current value %s, new value: %s",
- projectNameKey.get(), safeGetObjectId(currentRef), newObjectId);
+ projectNameKey.get(), safeGetObjectId(sharedRef), newObjectId);
logger.atSevere().log(message);
throw new SharedProjectVersionUpdateException(message);
}
+
+ return true;
} catch (GlobalRefDbSystemError refDbSystemError) {
String message =
String.format(
"Error while updating shared project version for %s. Current value %s, new value: %s. Error: %s",
projectNameKey.get(),
- currentRef.getObjectId(),
+ sharedRef.getObjectId(),
newObjectId,
refDbSystemError.getMessage());
logger.atSevere().withCause(refDbSystemError).log(message);
@@ -283,33 +281,7 @@
}
}
- private Optional<Long> getLongFromObjectId(String projectName, ObjectId objectId) {
- if (objectId.equals(ObjectId.zeroId())) {
- return Optional.empty();
- }
- try (Repository repository =
- gitRepositoryManager.openRepository(Project.NameKey.parse(projectName))) {
- ObjectReader or = repository.newObjectReader();
- ObjectLoader ol = or.open(objectId, OBJ_BLOB);
- if (ol.getType() != OBJ_BLOB) {
- // In theory this should be thrown by open but not all implementations may do it properly
- // (certainly InMemoryRepository doesn't).
- logger.atSevere().log("Incorrect object type loaded for objectId %s", objectId.toString());
- return Optional.empty();
- }
- String str = CharMatcher.whitespace().trimFrom(new String(ol.getCachedBytes(), UTF_8));
- Integer value = Ints.tryParse(str);
- logger.atInfo().log(
- "Found remote version for project %s, value: %s - %d",
- projectName, objectId.toString(), value);
- return Optional.of(Integer.toUnsignedLong(value));
- } catch (IOException e) {
- logger.atSevere().withCause(e).log("Cannot parse objectId %s", objectId.toString());
- return Optional.empty();
- }
- }
-
- private Optional<ObjectId> updateLocalProjectVersion(
+ private Optional<RefUpdate> updateLocalProjectVersion(
Project.NameKey projectNameKey, Optional<Long> lastRefUpdatedTimestamp)
throws LocalProjectVersionUpdateException {
if (!lastRefUpdatedTimestamp.isPresent()) {
@@ -329,8 +301,7 @@
throw new LocalProjectVersionUpdateException(message);
}
- gitReferenceUpdated.fire(projectNameKey, refUpdate, null);
- return Optional.of(refUpdate.getNewObjectId());
+ return Optional.of(refUpdate);
} catch (IOException e) {
String message = "Cannot create versioning command for " + projectNameKey.get();
logger.atSevere().withCause(e).log(message);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateTest.java
index 90e3274..1839fe1 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateTest.java
@@ -88,12 +88,16 @@
@Test
public void producerShouldUpdateProjectVersionUponRefUpdatedEvent() throws IOException {
Context.setForwardedEvent(false);
- when(sharedRefDb.exists(
- A_TEST_PROJECT_NAME_KEY, ProjectVersionRefUpdate.MULTI_SITE_VERSIONING_REF))
- .thenReturn(true);
- when(sharedRefDb.exists(
- A_TEST_PROJECT_NAME_KEY, ProjectVersionRefUpdate.MULTI_SITE_VERSIONING_VALUE_REF))
- .thenReturn(true);
+ when(sharedRefDb.get(
+ A_TEST_PROJECT_NAME_KEY,
+ ProjectVersionRefUpdate.MULTI_SITE_VERSIONING_REF,
+ String.class))
+ .thenReturn(Optional.of("26f7ee61bf0e470e8393c884526eec8a9b943a63"));
+ when(sharedRefDb.get(
+ A_TEST_PROJECT_NAME_KEY,
+ ProjectVersionRefUpdate.MULTI_SITE_VERSIONING_VALUE_REF,
+ String.class))
+ .thenReturn(Optional.of("" + (masterCommit.getCommitTime() - 1)));
when(sharedRefDb.compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class)))
.thenReturn(true);
when(sharedRefDb.compareAndPut(any(Project.NameKey.class), any(String.class), any(), any()))
@@ -122,12 +126,16 @@
public void producerShouldCreateNewProjectVersionWhenMissingUponRefUpdatedEvent()
throws IOException {
Context.setForwardedEvent(false);
- when(sharedRefDb.exists(
- A_TEST_PROJECT_NAME_KEY, ProjectVersionRefUpdate.MULTI_SITE_VERSIONING_REF))
- .thenReturn(false);
- when(sharedRefDb.exists(
- A_TEST_PROJECT_NAME_KEY, ProjectVersionRefUpdate.MULTI_SITE_VERSIONING_VALUE_REF))
- .thenReturn(false);
+ when(sharedRefDb.get(
+ A_TEST_PROJECT_NAME_KEY,
+ ProjectVersionRefUpdate.MULTI_SITE_VERSIONING_REF,
+ String.class))
+ .thenReturn(Optional.empty());
+ when(sharedRefDb.get(
+ A_TEST_PROJECT_NAME_KEY,
+ ProjectVersionRefUpdate.MULTI_SITE_VERSIONING_VALUE_REF,
+ String.class))
+ .thenReturn(Optional.empty());
when(sharedRefDb.compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class)))
.thenReturn(true);