blob: 05931395d19cf28f4f73b2c08a1d7696ecff6ea4 [file] [log] [blame]
// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.googlesource.gerrit.plugins.spannerrefdb;
import com.gerritforge.gerrit.globalrefdb.GlobalRefDatabase;
import com.gerritforge.gerrit.globalrefdb.GlobalRefDbLockException;
import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.Key;
import com.google.cloud.spanner.KeySet;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.ReadOnlyTransaction;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.Struct;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Project;
import com.google.inject.Inject;
import java.util.Arrays;
import java.util.Collections;
import java.util.Optional;
import javax.inject.Singleton;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
@Singleton
public class SpannerRefDatabase implements GlobalRefDatabase {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final DatabaseClient dbClient;
private final Lock.Factory lockFactory;
@Inject
SpannerRefDatabase(DatabaseClient dbClient, Lock.Factory lockFactory) {
this.dbClient = dbClient;
this.lockFactory = lockFactory;
}
@Override
public boolean isUpToDate(Project.NameKey project, Ref ref) throws GlobalRefDbLockException {
String valueInSpanner = get(project, ref.getName());
if (valueInSpanner == null) {
// If it doesn't exist, assume up to date (will populate on next compareAndPut)
return true;
}
ObjectId objectIdInSharedRefDb = ObjectId.fromString(valueInSpanner);
boolean isUpToDate = objectIdInSharedRefDb.equals(ref.getObjectId());
if (!isUpToDate) {
logger.atFine().log(
"%s:%s is out of sync: local=%s spanner=%s",
project, ref.getName(), ref.getObjectId(), objectIdInSharedRefDb);
}
return isUpToDate;
}
@Override
public boolean compareAndPut(Project.NameKey project, Ref ref, ObjectId newValue)
throws GlobalRefDbSystemError {
newValue = Optional.ofNullable(newValue).orElse(ObjectId.zeroId());
ObjectId currValue = Optional.ofNullable(ref.getObjectId()).orElse(ObjectId.zeroId());
return doCompareAndPut(project, ref.getName(), currValue.getName(), newValue.name());
}
@Override
public <T> boolean compareAndPut(
Project.NameKey project, String refName, T expectedValue, T newValue)
throws GlobalRefDbSystemError {
String newRefValue =
Optional.ofNullable(newValue).map(Object::toString).orElse(ObjectId.zeroId().getName());
String expectedRefValue =
Optional.ofNullable(expectedValue)
.map(Object::toString)
.orElse(ObjectId.zeroId().getName());
return doCompareAndPut(project, refName, expectedRefValue, newRefValue);
}
/**
* Run the compare and put to set the ref value to the newValue if and only if the current ref
* value is expectedValue. Its primary key is the refPath.
*
* <p>If the ref doesn't exist at all, insert it and return true. If the ref exists and its
* current value is the same as expected current value, update and return true. Otherwise, return
* false.
*
* @param project - the project
* @param refName - the ref
* @param expectedValue - the expected value to be found in the global-refdb
* @param newValue - the new value to put in the global-refdb if the expectedValue is present
* @return success
*/
private boolean doCompareAndPut(
Project.NameKey project, String refName, String expectedValue, String newValue)
throws GlobalRefDbSystemError {
logger.atInfo().log(
"Do compare and put for %s / %s. Expected value: %s. New value: %s",
project.get(), refName, expectedValue, newValue);
return dbClient
.readWriteTransaction()
.run(
transaction -> {
Struct row =
transaction.readRow(
"refs", Key.of(project.get(), refName), Arrays.asList("value"));
// If the newValue is zeroId, delete the row if the expected value is correct
if (newValue.equals(ObjectId.zeroId().name())) {
if (row == null) {
return true;
}
if (row.getString(0).equals(expectedValue)) {
transaction.buffer(Mutation.delete("refs", Key.of(project.get(), refName)));
return true;
}
return false;
}
// If the row is null, the row doesn't exist and will be created.
// If the value is the expected value, the row should be updated.
if (row == null || row.getString(0).equals(expectedValue)) {
transaction.buffer(
Mutation.newInsertOrUpdateBuilder("refs")
.set("project")
.to(project.get())
.set("ref")
.to(refName)
.set("value")
.to(newValue)
.build());
return true;
}
return false;
});
}
@Override
public AutoCloseable lockRef(Project.NameKey project, String refName)
throws GlobalRefDbLockException {
Lock lock = lockFactory.create(project.get(), refName);
lock.tryLock();
return lock;
}
@Override
public boolean exists(Project.NameKey project, String refName) {
logger.atInfo().log("Checking if ref %s %s exists.", project.get(), refName);
return get(project, refName) != null;
}
@Override
public void remove(Project.NameKey project) throws GlobalRefDbSystemError {
// Delete all rows with matching project
logger.atInfo().log("Removing project %s from global-refdb", project.get());
dbClient.write(
Collections.singletonList(
Mutation.delete("refs", KeySet.prefixRange(Key.of(project.get())))));
}
@VisibleForTesting
String get(Project.NameKey project, String refName) throws GlobalRefDbSystemError {
try (ReadOnlyTransaction transaction = dbClient.readOnlyTransaction()) {
ResultSet resultSet =
transaction.executeQuery(
Statement.newBuilder("SELECT value FROM refs WHERE project = @project and ref = @ref")
.bind("project")
.to(project.get())
.bind("ref")
.to(refName)
.build());
if (resultSet.next()) {
return resultSet.getString("value");
}
return null;
} catch (Exception e) {
throw new GlobalRefDbSystemError(String.format("Cannot get value for %s", project.get()), e);
}
}
@Override
public <T> Optional<T> get(Project.NameKey project, String refName, Class<T> clazz)
throws GlobalRefDbSystemError {
// The only usage of this is as a String but the API requires Optional<T>?
// It looks like zookeeper has/uses a StringDeserializerFactory/StringDeserializer to deal
// with this, while the aws implementation opts for this.
return Optional.ofNullable((T) get(project, refName));
}
public Optional<ObjectId> getObjectId(Project.NameKey project, String refName) {
String idName = get(project, refName);
if (idName == null) {
return Optional.empty();
}
return Optional.of(ObjectId.fromString(idName));
}
}