Add tests for Lock implementation Change-Id: I28317763f14493301e4c539e5f7710183ad19311
diff --git a/src/main/java/com/googlesource/gerrit/plugins/spannerrefdb/Lock.java b/src/main/java/com/googlesource/gerrit/plugins/spannerrefdb/Lock.java index 20a921b..dd01966 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/spannerrefdb/Lock.java +++ b/src/main/java/com/googlesource/gerrit/plugins/spannerrefdb/Lock.java
@@ -138,7 +138,6 @@ Struct row = transaction.readRow( "locks", Key.of(projectName, refName), Arrays.asList("token", "heartbeat")); - if (row == null) { // If the old lock doesn't exist any more, we create a new one transaction.buffer(createLock()); @@ -155,7 +154,7 @@ .bind("ref") .to(refName) .build()); - if (resultSet.getLong("seconds") <= SECONDS_FOR_STALE_HEARTBEAT) { + if (resultSet.next() && resultSet.getLong("seconds") <= SECONDS_FOR_STALE_HEARTBEAT) { // Check if the old lock is stale; if not, do nothing return false; }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/spannerrefdb/EmulatedSpannerRefDb.java b/src/test/java/com/googlesource/gerrit/plugins/spannerrefdb/EmulatedSpannerRefDb.java index 70adc77..4ed2b90 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/spannerrefdb/EmulatedSpannerRefDb.java +++ b/src/test/java/com/googlesource/gerrit/plugins/spannerrefdb/EmulatedSpannerRefDb.java
@@ -114,6 +114,10 @@ return spannerRefDb; } + public DatabaseClient getDatabaseClient() { + return databaseClient; + } + private SpannerOptions getEmulatorOptions() { return SpannerOptions.newBuilder() .setEmulatorHost(container.getEmulatorGrpcEndpoint())
diff --git a/src/test/java/com/googlesource/gerrit/plugins/spannerrefdb/LockTest.java b/src/test/java/com/googlesource/gerrit/plugins/spannerrefdb/LockTest.java new file mode 100644 index 0000000..cd5d65c --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/spannerrefdb/LockTest.java
@@ -0,0 +1,147 @@ +// 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 static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; + +import com.gerritforge.gerrit.globalrefdb.GlobalRefDbLockException; +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.Struct; +import com.google.gerrit.entities.Project; +import java.util.Arrays; +import java.util.Collections; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class LockTest implements RefFixture { + private EmulatedSpannerRefDb emulator; + private SpannerRefDatabase refDb; + private DatabaseClient dbClient; + + private String TOKEN = "token"; + private String HEARTBEAT = "heartbeat"; + + @Before + public void setUp() throws Exception { + emulator = new EmulatedSpannerRefDb(); + refDb = emulator.getSpannerRefDatabase(); + dbClient = emulator.getDatabaseClient(); + } + + @After + public void tearDown() { + emulator.cleanup(); + } + + @Test + public void lockUnlockedRef_Success() throws Exception { + try (AutoCloseable refLock = refDb.lockRef(PROJECT_NAME_KEY, REF_NAME)) { + assertThat(getLockTimestamp(PROJECT_NAME_KEY, REF_NAME, TOKEN)).isNotNull(); + + Timestamp heartbeat = getLockTimestamp(PROJECT_NAME_KEY, REF_NAME, HEARTBEAT); + try { + Thread.sleep(4_000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + assertThat(getLockTimestamp(PROJECT_NAME_KEY, REF_NAME, HEARTBEAT).compareTo(heartbeat)) + .isGreaterThan(0); + } + + assertThat(getLockRow(PROJECT_NAME_KEY, REF_NAME)).isNull(); + } + + @Test + public void lockAlreadyLockedRef_Rejected() throws Exception { + try (AutoCloseable refLock = refDb.lockRef(PROJECT_NAME_KEY, REF_NAME)) { + GlobalRefDbLockException e = + assertThrows( + GlobalRefDbLockException.class, () -> refDb.lockRef(PROJECT_NAME_KEY, REF_NAME)); + assertThat(e) + .hasMessageThat() + .contains( + String.format("Unable to lock ref %s on project %s", REF_NAME, PROJECT_NAME_KEY)); + } + } + + @Test + public void lockReleasedRef_Success() throws Exception { + Timestamp lockToken; + try (AutoCloseable refLock = refDb.lockRef(PROJECT_NAME_KEY, REF_NAME)) { + lockToken = getLockTimestamp(PROJECT_NAME_KEY, REF_NAME, TOKEN); + } + + try (AutoCloseable refLock = refDb.lockRef(PROJECT_NAME_KEY, REF_NAME)) { + Timestamp newLockToken = getLockTimestamp(PROJECT_NAME_KEY, REF_NAME, TOKEN); + assertThat(lockToken).isNotEqualTo(newLockToken); + } + } + + @Test + public void reclaimStaleLockedRef_Success() throws Exception { + Timestamp staleTimestamp = Timestamp.ofTimeSecondsAndNanos(0, 0); + insertLockRow(staleTimestamp); + try (AutoCloseable refLock = refDb.lockRef(PROJECT_NAME_KEY, REF_NAME)) { + Timestamp newLockToken = getLockTimestamp(PROJECT_NAME_KEY, REF_NAME, TOKEN); + assertThat(newLockToken).isNotNull(); + assertThat(staleTimestamp.compareTo(newLockToken)).isLessThan(0); + } + } + + @Test + public void reclaimFreshLockedRef_Rejected() throws Exception { + Timestamp freshTimestamp = Timestamp.now(); + insertLockRow(freshTimestamp); + GlobalRefDbLockException e = + assertThrows( + GlobalRefDbLockException.class, () -> refDb.lockRef(PROJECT_NAME_KEY, REF_NAME)); + assertThat(e) + .hasMessageThat() + .contains(String.format("Unable to lock ref %s on project %s", REF_NAME, PROJECT_NAME_KEY)); + } + + private Timestamp getLockTimestamp(Project.NameKey project, String refName, String column) { + Struct row = getLockRow(project, refName); + return row != null ? row.getTimestamp(column) : null; + } + + private Struct getLockRow(Project.NameKey project, String refName) { + return dbClient + .singleUse() + .readRow("locks", Key.of(project.get(), refName), Arrays.asList(TOKEN, HEARTBEAT)); + } + + private void insertLockRow(Timestamp timestamp) { + Mutation staleLock = + Mutation.newInsertBuilder("locks") + .set("project") + .to(PROJECT_NAME_KEY.get()) + .set("ref") + .to(REF_NAME) + .set("heartbeat") + .to(timestamp) + .set("token") + .to(timestamp) + .set("owner") + .to("") + .build(); + dbClient.write(Collections.singletonList(staleLock)); + } +}