// Copyright 2016 The Nomulus Authors. All Rights Reserved.
//
// 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 google.registry.backup;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ofy.CommitLogBucket.getBucketKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.testing.DatastoreHelper.createTld;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.googlecode.objectify.VoidWork;
import google.registry.config.TestRegistryConfig;
import google.registry.model.common.Cursor;
import google.registry.model.common.Cursor.CursorType;
import google.registry.model.ofy.CommitLogBucket;
import google.registry.model.ofy.CommitLogCheckpoint;
import google.registry.model.ofy.Ofy;
import google.registry.model.registry.Registry;
import google.registry.testing.AppEngineRule;
import google.registry.testing.FakeClock;
import google.registry.testing.InjectRule;
import google.registry.testing.RegistryConfigRule;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
/** Unit tests for {@link CommitLogCheckpointStrategy}. */
@RunWith(MockitoJUnitRunner.class)
public class CommitLogCheckpointStrategyTest {
@Rule
public final AppEngineRule appEngine = AppEngineRule.builder()
.withDatastore()
.build();
@Rule
public final InjectRule inject = new InjectRule();
@Rule
public final RegistryConfigRule configRule = new RegistryConfigRule();
final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ"));
final Ofy ofy = new Ofy(clock);
final CommitLogCheckpointStrategy strategy = new CommitLogCheckpointStrategy();
/**
* Supplier to inject into CommitLogBucket for doling out predictable bucket IDs.
*
*
If not overridden, the supplier returns 1 so that other saves won't hit an NPE (since even
* if they use saveWithoutBackup() the transaction still selects a bucket key early).
*/
final FakeSupplier fakeBucketIdSupplier = new FakeSupplier<>(1);
/** Gross but necessary supplier that can be modified to return the desired value. */
private static class FakeSupplier implements Supplier {
/** Default value to return if 'value' is not set. */
final T defaultValue;
/** Set this value field to make the supplier return this value. */
T value = null;
public FakeSupplier(T defaultValue) {
this.defaultValue = defaultValue;
}
@Override
public T get() {
return value == null ? defaultValue : value;
}
}
@Before
public void before() throws Exception {
strategy.clock = clock;
strategy.ofy = ofy;
// Use three commit log buckets for easier but sufficiently complex testing.
configRule.override(new TestRegistryConfig() {
@Override
public int getCommitLogBucketCount() {
return 3;
}});
// Need to inject clock into Ofy so that createTld() below will get the right time.
inject.setStaticField(Ofy.class, "clock", clock);
// Inject a fake bucket ID supplier so we can dole out specific bucket IDs to commit logs.
inject.setStaticField(CommitLogBucket.class, "bucketIdSupplier", fakeBucketIdSupplier);
// Create some fake TLDs to parent RegistryCursor test objects under.
createTld("tld1");
createTld("tld2");
createTld("tld3");
clock.advanceOneMilli();
}
@Test
public void test_readBucketTimestamps_noCommitLogs() throws Exception {
assertThat(strategy.readBucketTimestamps())
.containsExactly(1, START_OF_TIME, 2, START_OF_TIME, 3, START_OF_TIME);
}
@Test
public void test_readBucketTimestamps_withSomeCommitLogs() throws Exception {
DateTime startTime = clock.nowUtc();
writeCommitLogToBucket(1);
clock.advanceOneMilli();
writeCommitLogToBucket(2);
assertThat(strategy.readBucketTimestamps())
.containsExactly(1, startTime, 2, startTime.plusMillis(1), 3, START_OF_TIME);
}
@Test
public void test_readBucketTimestamps_againAfterUpdate_reflectsUpdate() throws Exception {
DateTime firstTime = clock.nowUtc();
writeCommitLogToBucket(1);
writeCommitLogToBucket(2);
writeCommitLogToBucket(3);
assertThat(strategy.readBucketTimestamps().values())
.containsExactly(firstTime, firstTime, firstTime);
clock.advanceOneMilli();
writeCommitLogToBucket(1);
DateTime secondTime = clock.nowUtc();
assertThat(strategy.readBucketTimestamps())
.containsExactly(1, secondTime, 2, firstTime, 3, firstTime);
}
@Test
public void test_readNewCommitLogsAndFindThreshold_noCommitsAtAll_returnsEndOfTime() {
ImmutableMap bucketTimes =
ImmutableMap.of(1, START_OF_TIME, 2, START_OF_TIME, 3, START_OF_TIME);
assertThat(strategy.readNewCommitLogsAndFindThreshold(bucketTimes)).isEqualTo(END_OF_TIME);
}
@Test
public void test_readNewCommitLogsAndFindThreshold_noNewCommits_returnsEndOfTime() {
DateTime now = clock.nowUtc();
writeCommitLogToBucket(1);
clock.advanceOneMilli();
writeCommitLogToBucket(2);
clock.advanceOneMilli();
writeCommitLogToBucket(3);
ImmutableMap bucketTimes =
ImmutableMap.of(1, now, 2, now.plusMillis(1), 3, now.plusMillis(2));
assertThat(strategy.readNewCommitLogsAndFindThreshold(bucketTimes)).isEqualTo(END_OF_TIME);
}
@Test
public void test_readNewCommitLogsAndFindThreshold_tiedNewCommits_returnsCommitTimeMinusOne() {
DateTime now = clock.nowUtc();
writeCommitLogToBucket(1);
writeCommitLogToBucket(2);
writeCommitLogToBucket(3);
assertThat(strategy.readNewCommitLogsAndFindThreshold(
ImmutableMap.of(1, START_OF_TIME, 2, START_OF_TIME, 3, START_OF_TIME)))
.isEqualTo(now.minusMillis(1));
}
@Test
public void test_readNewCommitLogsAndFindThreshold_someNewCommits_returnsEarliestTimeMinusOne() {
DateTime now = clock.nowUtc();
writeCommitLogToBucket(1); // 1A
writeCommitLogToBucket(2); // 2A
writeCommitLogToBucket(3); // 3A
clock.advanceBy(Duration.millis(5));
writeCommitLogToBucket(1); // 1B
writeCommitLogToBucket(2); // 2B
writeCommitLogToBucket(3); // 3B
clock.advanceBy(Duration.millis(5));
writeCommitLogToBucket(1); // 1C
writeCommitLogToBucket(2); // 2C
writeCommitLogToBucket(3); // 3C
// First pass times: 1 at T0, 2 at T+5, 3 at T+10.
// Commits 1A, 2B, 3C are the commits seen in the first pass.
// Commits 2A, 3A, 3B are all old prior commits that should be ignored.
// Commit 1B is the first new commit for bucket 1, at T+5.
// Commit 1C is the second new commit for bucket 1, at T+10, and should be ignored.
// Commit 2C is the first new commit for bucket 2, at T+10.
// Since 1B as a new commit is older than 1C, T+5 is the oldest new commit time.
// Therefore, expect T+4 as the threshold time.
assertThat(strategy.readNewCommitLogsAndFindThreshold(
ImmutableMap.of(1, now, 2, now.plusMillis(5), 3, now.plusMillis(10))))
.isEqualTo(now.plusMillis(4));
}
@Test
public void test_readNewCommitLogsAndFindThreshold_commitsAtBucketTimes() {
DateTime now = clock.nowUtc();
ImmutableMap bucketTimes =
ImmutableMap.of(1, now.minusMillis(1), 2, now, 3, now.plusMillis(1));
assertThat(strategy.readNewCommitLogsAndFindThreshold(bucketTimes)).isEqualTo(END_OF_TIME);
}
@Test
public void test_computeBucketCheckpointTimes_earlyThreshold_setsEverythingToThreshold() {
DateTime now = clock.nowUtc();
ImmutableMap bucketTimes =
ImmutableMap.of(1, now.minusMillis(1), 2, now, 3, now.plusMillis(1));
assertThat(strategy.computeBucketCheckpointTimes(bucketTimes, now.minusMillis(2)).values())
.containsExactly(now.minusMillis(2), now.minusMillis(2), now.minusMillis(2));
}
@Test
public void test_computeBucketCheckpointTimes_middleThreshold_clampsToThreshold() {
DateTime now = clock.nowUtc();
ImmutableMap bucketTimes =
ImmutableMap.of(1, now.minusMillis(1), 2, now, 3, now.plusMillis(1));
assertThat(strategy.computeBucketCheckpointTimes(bucketTimes, now))
.containsExactly(1, now.minusMillis(1), 2, now, 3, now);
}
@Test
public void test_computeBucketCheckpointTimes_lateThreshold_leavesBucketTimesAsIs() {
DateTime now = clock.nowUtc();
ImmutableMap bucketTimes =
ImmutableMap.of(1, now.minusMillis(1), 2, now, 3, now.plusMillis(1));
assertThat(strategy.computeBucketCheckpointTimes(bucketTimes, now.plusMillis(2)))
.isEqualTo(bucketTimes);
}
@Test
public void test_computeCheckpoint_noCommitsAtAll_bucketCheckpointTimesAreStartOfTime() {
assertThat(strategy.computeCheckpoint())
.isEqualTo(CommitLogCheckpoint.create(
clock.nowUtc(),
ImmutableMap.of(1, START_OF_TIME, 2, START_OF_TIME, 3, START_OF_TIME)));
}
@Test
public void test_computeCheckpoint_noNewCommitLogs_bucketCheckpointTimesAreBucketTimes() {
DateTime now = clock.nowUtc();
writeCommitLogToBucket(1);
clock.advanceOneMilli();
writeCommitLogToBucket(2);
clock.advanceOneMilli();
writeCommitLogToBucket(3);
clock.advanceOneMilli();
DateTime checkpointTime = clock.nowUtc();
assertThat(strategy.computeCheckpoint())
.isEqualTo(CommitLogCheckpoint.create(
checkpointTime,
ImmutableMap.of(1, now, 2, now.plusMillis(1), 3, now.plusMillis(2))));
}
@Test
public void test_computeCheckpoint_someNewCommits_bucketCheckpointTimesAreClampedToThreshold() {
DateTime now = clock.nowUtc();
writeCommitLogToBucket(1); // 1A
writeCommitLogToBucket(2); // 2A
writeCommitLogToBucket(3); // 3A
clock.advanceBy(Duration.millis(5));
writeCommitLogToBucket(1); // 1B
writeCommitLogToBucket(2); // 2B
writeCommitLogToBucket(3); // 3B
clock.advanceBy(Duration.millis(5));
writeCommitLogToBucket(1); // 1C
writeCommitLogToBucket(2); // 2C
writeCommitLogToBucket(3); // 3C
// Set first pass times: 1 at T0, 2 at T+5, 3 at T+10.
saveBucketWithLastWrittenTime(1, now);
saveBucketWithLastWrittenTime(2, now.plusMillis(5));
saveBucketWithLastWrittenTime(3, now.plusMillis(10));
// Commits 1A, 2B, 3C are the commits seen in the first pass.
// Commits 2A, 3A, 3B are all old prior commits that should be ignored.
// Commit 1B is the first new commit for bucket 1, at T+5.
// Commit 1C is the second new commit for bucket 1, at T+10, and should be ignored.
// Commit 2C is the first new commit for bucket 2, at T+10.
// Since 1B as a new commit is older than 1C, T+5 is the oldest new commit time.
// Therefore, expect T+4 as the threshold time.
DateTime threshold = now.plusMillis(4);
// Advance clock before taking checkpoint.
clock.advanceBy(Duration.millis(10));
DateTime checkpointTime = clock.nowUtc();
// Bucket checkpoint times should be clamped as expected.
assertThat(strategy.computeCheckpoint())
.isEqualTo(CommitLogCheckpoint.create(
checkpointTime,
ImmutableMap.of(1, now, 2, threshold, 3, threshold)));
}
private void writeCommitLogToBucket(final int bucketId) {
fakeBucketIdSupplier.value = bucketId;
ofy.transact(
new VoidWork() {
@Override
public void vrun() {
String tld = "tld" + bucketId;
ofy().save().entity(
Cursor.create(CursorType.RDE_REPORT, ofy.getTransactionTime(), Registry.get(tld)));
}
});
fakeBucketIdSupplier.value = null;
}
private void saveBucketWithLastWrittenTime(final int bucketId, final DateTime lastWrittenTime) {
ofy.transact(new VoidWork() {
@Override
public void vrun() {
ofy.saveWithoutBackup().entity(
CommitLogBucket.loadBucket(getBucketKey(bucketId)).asBuilder()
.setLastWrittenTime(lastWrittenTime)
.build());
}});
}
}