Compare commits

...
Author SHA1 Message Date
Weimin YuandGitHub 65cf49f204 Fix sql script name conflict (#411)
* Fix sql script name conflict

There are two V11__ files due to concurrent merge. Renamed one
to V12__

Also removed a @NotNull annotation, which is the fist in the code base.
Most of the code base use @Nullable instead. If we do want to use
@NotNull, we may want to use the javax one instead.
2019-12-12 16:16:43 -05:00
Weimin YuandGitHub f48e3933f5 Run cross-release SQL integration tests (#403)
* Run cross-release SQL integration tests

Run SQL integration tests across arbitrary schema and server
releases.

Refer to integration/README.md in this change for more information.

TESTED=Cloud build changes tested with cloud-build-local
       Used the published jars to test sqlIntegration task locally.
2019-12-12 13:47:49 -05:00
Lai JiangandGitHub 9853f23d94 Fix null pointer excpetion bug (#407)
The factory method passes a null trustedCertificates instead of an empty
list.
2019-12-12 13:06:43 -05:00
Ben McIlwainandGitHub db7fcf6c38 Add Cloud SQL premium list caches and compare prices with Datastore (#376)
* Add Cloud SQL premium list caches and compare prices with Datastore

Nothing will fail if the prices can't be loaded from Cloud SQL, or if the prices
are different. All that happens is that the error is logged. Then, once this is
running in production for awhile, we'll look at the logs and see if there will
be any pricing implications from switching over to the Cloud SQL version of the
premium lists.

* Add setMaxResults(1) per code review

* Add tests and reorder public functions

* Don't statically import caches

* Improve test pass rate

* Merge branch 'master' into dual-read-premium

* Add PremiumEntry mapping

* Allow update

* Revert column order

* Alphabetize PremiumEntry columns

* Don't bother trying to enforce order

* Private constructor
2019-12-11 16:20:19 -05:00
26 changed files with 771 additions and 30 deletions
+12 -3
View File
@@ -221,9 +221,18 @@ subprojects {
}
afterEvaluate {
if (rootProject.enableDependencyLocking.toBoolean()) {
// Lock application dependencies except for the gradle-license-report
// plugin. See dependency_lic.gradle for the reason why.
if (rootProject.enableDependencyLocking.toBoolean()
&& project.name != 'integration') {
// The ':integration' project runs server/schema integration tests using
// dynamically specified jars with no transitive dependency. Therefore
// dependency-locking does not make sense. Furthermore, during
// evaluation it resolves the 'testRuntime' configuration, making it
// immutable. Locking activation would trigger an invalid operation
// exception.
//
// For all other projects, due to problem with the gradle-license-report
// plugin, the dependencyLicenseReport configuration must opt out of
// dependency-locking. See dependency_lic.gradle for the reason why.
//
// To selectively activate dependency locking without hardcoding them
// in the 'configurations' block, the following code must run after
+66
View File
@@ -16,6 +16,7 @@ import java.lang.reflect.Constructor
plugins {
id 'java-library'
id 'maven-publish'
}
// Path to code generated by ad hoc tasks in this project. A separate path is
@@ -112,6 +113,11 @@ configurations {
soy
closureCompiler
// Published jars that are used for server/schema compatibility tests.
// See <a href="../integration/README.md">the integration project</a>
// for details.
nomulus_test
// Exclude non-canonical servlet-api jars. Our AppEngine deployment uses
// javax.servlet:servlet-api:2.5
// For reasons we do not understand, marking the following dependencies as
@@ -814,6 +820,66 @@ test {
createUberJar('nomulus', 'nomulus', 'google.registry.tools.RegistryTool')
project.nomulus.dependsOn project(':third_party').jar
// A jar with classes and resources from main sourceSet, excluding internal
// data. See comments on configurations.nomulus_test above for details.
task nomulusFossJar (type: Jar) {
archiveBaseName = 'nomulus'
archiveClassifier = 'public'
from (project.sourceSets.main.output) {
exclude 'google/registry/config/files/**'
}
from (project.sourceSets.main.output) {
include 'google/registry/config/files/default-config.yaml'
include 'google/registry/config/files/nomulus-config-unittest.yaml'
}
}
// An UberJar of registry test classes, resources and all dependencies.
// See comments on configurations.nomulus_test above for details.
// TODO(weiminyu): extract shareable code with root.ext.createUberJar
task testUberJar (
type: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
mergeServiceFiles()
archiveBaseName = 'nomulus-tests'
archiveClassifier = 'alldeps'
zip64 = true
archiveVersion = ''
configurations = [project.configurations.testRuntimeClasspath]
from project.sourceSets.test.output
// Excludes signature files that accompany some dependency jars, like
// bonuncycastle. If they are present, only classes from those signed jars are
// made available to the class loader.
// see https://discuss.gradle.org/t/signing-a-custom-gradle-plugin-thats-downloaded-by-the-build-system-from-github/1365
exclude "META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA"
// Exclude SQL schema, which is a test dependency.
exclude 'sql/flyway/**'
// ShadowJar includes java source files when used on sourceSets.test.output.
// They are not needed.
exclude '**/*.java'
}
artifacts {
nomulus_test nomulusFossJar
nomulus_test testUberJar
}
publishing {
repositories {
maven {
url project.publish_repo
}
}
publications {
nomulusTestsPublication(MavenPublication) {
groupId 'google.registry'
artifactId 'nomulus_test'
version project.nomulus_version
artifact nomulusFossJar
artifact testUberJar
}
}
}
task buildToolImage(dependsOn: nomulus, type: Exec) {
commandLine 'docker', 'build', '-t', 'nomulus-tool', '.'
}
@@ -0,0 +1,3 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
@@ -791,6 +791,12 @@ public class Registry extends ImmutableObject implements Buildable {
return this;
}
@VisibleForTesting
public Builder setPremiumListKey(@Nullable Key<PremiumList> premiumList) {
getInstance().premiumList = premiumList;
return this;
}
public Builder setRestoreBillingCost(Money amount) {
checkArgument(amount.isPositiveOrZero(), "restoreBillingCost cannot be negative");
getInstance().restoreBillingCost = amount;
@@ -36,11 +36,13 @@ import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.googlecode.objectify.Key;
import google.registry.model.registry.Registry;
import google.registry.model.registry.label.DomainLabelMetrics.PremiumListCheckOutcome;
import google.registry.model.registry.label.PremiumList.PremiumListEntry;
import google.registry.model.registry.label.PremiumList.PremiumListRevision;
import google.registry.schema.tld.PremiumListDao;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -52,7 +54,9 @@ import org.joda.time.DateTime;
public final class PremiumListUtils {
/** The number of premium list entry entities that are created and deleted per batch. */
static final int TRANSACTION_BATCH_SIZE = 200;
private static final int TRANSACTION_BATCH_SIZE = 200;
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Value type class used by {@link #checkStatus} to return the results of a premiumness check. */
@AutoValue
@@ -97,6 +101,19 @@ public final class PremiumListUtils {
listName,
checkResults.checkOutcome(),
DateTime.now(UTC).getMillis() - startTime.getMillis());
// Also load the value from Cloud SQL, compare the two results, and log if different.
try {
Optional<Money> priceFromSql = PremiumListDao.getPremiumPrice(label, registry);
if (!priceFromSql.equals(checkResults.premiumPrice())) {
logger.atWarning().log(
"Unequal prices for domain %s.%s from Datastore (%s) and Cloud SQL (%s).",
label, registry.getTldStr(), checkResults.premiumPrice(), priceFromSql);
}
} catch (Throwable t) {
logger.atSevere().withCause(t).log(
"Error loading price of domain %s.%s from Cloud SQL.", label, registry.getTldStr());
}
return checkResults.premiumPrice();
}
@@ -0,0 +1,43 @@
// Copyright 2019 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.schema.tld;
import java.io.Serializable;
import java.math.BigDecimal;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
/**
* Entity class for the premium price of an individual domain label.
*
* <p>These are not persisted directly, but rather, using {@link PremiumList#getLabelsToPrices()}.
*/
@Entity
public class PremiumEntry implements Serializable {
@Id
@Column(nullable = false)
Long revisionId;
@Column(nullable = false)
BigDecimal price;
@Id
@Column(nullable = false)
String domainLabel;
private PremiumEntry() {}
}
@@ -23,6 +23,7 @@ import com.google.common.hash.BloomFilter;
import google.registry.model.CreateAutoTimestamp;
import java.math.BigDecimal;
import java.util.Map;
import javax.annotation.Nullable;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
@@ -34,6 +35,7 @@ import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.MapKeyColumn;
import javax.persistence.Table;
import org.hibernate.LazyInitializationException;
import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime;
@@ -97,10 +99,16 @@ public class PremiumList {
return name;
}
/** Returns the {@link CurrencyUnit} used for this list. */
public CurrencyUnit getCurrency() {
return currency;
}
/** Returns the ID of this revision, or throws if null. */
public Long getRevisionId() {
checkState(
revisionId != null, "revisionId is null because it is not persisted in the database");
revisionId != null,
"revisionId is null because this object has not yet been persisted to the DB");
return revisionId;
}
@@ -109,9 +117,17 @@ public class PremiumList {
return creationTimestamp.getTimestamp();
}
/** Returns a {@link Map} of domain labels to prices. */
/**
* Returns a {@link Map} of domain labels to prices.
*
* <p>Note that this is lazily loaded and thus will throw a {@link LazyInitializationException} if
* used outside the transaction in which the given entity was loaded. You generally should not be
* using this anyway as it's inefficient to load all of the PremiumEntry rows if you don't need
* them. To check prices, use {@link PremiumListDao#getPremiumPrice} instead.
*/
@Nullable
public ImmutableMap<String, BigDecimal> getLabelsToPrices() {
return ImmutableMap.copyOf(labelsToPrices);
return labelsToPrices == null ? null : ImmutableMap.copyOf(labelsToPrices);
}
/**
@@ -0,0 +1,106 @@
// Copyright 2019 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.schema.tld;
import static google.registry.config.RegistryConfig.getDomainLabelListCacheDuration;
import static google.registry.config.RegistryConfig.getSingletonCachePersistDuration;
import static google.registry.config.RegistryConfig.getStaticPremiumListMaxCachedEntries;
import static google.registry.schema.tld.PremiumListDao.getPriceForLabel;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import google.registry.util.NonFinalForTesting;
import java.math.BigDecimal;
import java.util.Optional;
import org.joda.time.Duration;
/** Caching utils for {@link PremiumList}s. */
class PremiumListCache {
/**
* In-memory cache for premium lists.
*
* <p>This is cached for a shorter duration because we need to periodically reload from the DB to
* check if a new revision has been published, and if so, then use that.
*/
@NonFinalForTesting
static LoadingCache<String, Optional<PremiumList>> cachePremiumLists =
createCachePremiumLists(getDomainLabelListCacheDuration());
@VisibleForTesting
static LoadingCache<String, Optional<PremiumList>> createCachePremiumLists(
Duration cachePersistDuration) {
return CacheBuilder.newBuilder()
.expireAfterWrite(cachePersistDuration.getMillis(), MILLISECONDS)
.build(
new CacheLoader<String, Optional<PremiumList>>() {
@Override
public Optional<PremiumList> load(String premiumListName) {
return PremiumListDao.getLatestRevision(premiumListName);
}
});
}
/**
* In-memory price cache for for a given premium list revision and domain label.
*
* <p>Note that premium list revision ids are globally unique, so this cache is specific to a
* given premium list. Premium list entries might not be present, as indicated by the Optional
* wrapper, and we want to cache that as well.
*
* <p>This is cached for a long duration (essentially indefinitely) because premium list revisions
* are immutable and cannot ever be changed once created, so the cache need not ever expire.
*
* <p>A maximum size is set here on the cache because it can potentially grow too big to fit in
* memory if there are a large number of distinct premium list entries being queried (both those
* that exist, as well as those that might exist according to the Bloom filter, must be cached).
* The entries judged least likely to be accessed again will be evicted first.
*/
@NonFinalForTesting
static LoadingCache<RevisionIdAndLabel, Optional<BigDecimal>> cachePremiumEntries =
createCachePremiumEntries(getSingletonCachePersistDuration());
@VisibleForTesting
static LoadingCache<RevisionIdAndLabel, Optional<BigDecimal>> createCachePremiumEntries(
Duration cachePersistDuration) {
return CacheBuilder.newBuilder()
.expireAfterWrite(cachePersistDuration.getMillis(), MILLISECONDS)
.maximumSize(getStaticPremiumListMaxCachedEntries())
.build(
new CacheLoader<RevisionIdAndLabel, Optional<BigDecimal>>() {
@Override
public Optional<BigDecimal> load(RevisionIdAndLabel revisionIdAndLabel) {
return getPriceForLabel(revisionIdAndLabel);
}
});
}
@AutoValue
abstract static class RevisionIdAndLabel {
abstract long revisionId();
abstract String label();
static RevisionIdAndLabel create(long revisionId, String label) {
return new AutoValue_PremiumListCache_RevisionIdAndLabel(revisionId, label);
}
}
private PremiumListCache() {}
}
@@ -17,9 +17,37 @@ package google.registry.schema.tld;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
import com.google.common.util.concurrent.UncheckedExecutionException;
import google.registry.model.registry.Registry;
import google.registry.schema.tld.PremiumListCache.RevisionIdAndLabel;
import java.math.BigDecimal;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import org.joda.money.Money;
/** Data access object class for {@link PremiumList}. */
public class PremiumListDao {
/**
* Returns the premium price for the specified label and registry, or absent if the label is not
* premium.
*/
public static Optional<Money> getPremiumPrice(String label, Registry registry) {
// If the registry has no configured premium list, then no labels are premium.
if (registry.getPremiumList() == null) {
return Optional.empty();
}
String premiumListName = registry.getPremiumList().getName();
PremiumList premiumList =
getLatestRevisionCached(premiumListName)
.orElseThrow(
() ->
new IllegalStateException(
String.format("Could not load premium list '%s'", premiumListName)));
return getPremiumPriceFromList(label, premiumList);
}
/** Persist a new premium list to Cloud SQL. */
public static void saveNew(PremiumList premiumList) {
jpaTm()
@@ -27,18 +55,80 @@ public class PremiumListDao {
() -> {
checkArgument(
!checkExists(premiumList.getName()),
"A premium list of this name already exists: %s.",
"Premium list '%s' already exists",
premiumList.getName());
jpaTm().getEntityManager().persist(premiumList);
});
}
/** Persist a new revision of an existing premium list to Cloud SQL. */
public static void update(PremiumList premiumList) {
jpaTm()
.transact(
() -> {
checkArgument(
checkExists(premiumList.getName()),
"Can't update non-existent premium list '%s'",
premiumList.getName());
jpaTm().getEntityManager().persist(premiumList);
});
}
/**
* Returns the most recent revision of the PremiumList with the specified name, if it exists.
*
* <p>Note that this does not load <code>PremiumList.labelsToPrices</code>! If you need to check
* prices, use {@link #getPremiumPrice}.
*/
static Optional<PremiumList> getLatestRevision(String premiumListName) {
return jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
"SELECT pl FROM PremiumList pl WHERE pl.name = :name ORDER BY"
+ " pl.revisionId DESC",
PremiumList.class)
.setParameter("name", premiumListName)
.setMaxResults(1)
.getResultStream()
.findFirst());
}
static Optional<BigDecimal> getPriceForLabel(RevisionIdAndLabel revisionIdAndLabel) {
return jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
"SELECT pe.price FROM PremiumEntry pe WHERE pe.revisionId = :revisionId"
+ " AND pe.domainLabel = :label",
BigDecimal.class)
.setParameter("revisionId", revisionIdAndLabel.revisionId())
.setParameter("label", revisionIdAndLabel.label())
.setMaxResults(1)
.getResultStream()
.findFirst());
}
/** Returns the most recent revision of the PremiumList with the specified name, from cache. */
static Optional<PremiumList> getLatestRevisionCached(String premiumListName) {
try {
return PremiumListCache.cachePremiumLists.get(premiumListName);
} catch (ExecutionException e) {
throw new UncheckedExecutionException(
"Could not retrieve premium list named " + premiumListName, e);
}
}
/**
* Returns whether the premium list of the given name exists.
*
* <p>This means that at least one premium list revision must exist for the given name.
*/
public static boolean checkExists(String premiumListName) {
static boolean checkExists(String premiumListName) {
return jpaTm()
.transact(
() ->
@@ -52,5 +142,24 @@ public class PremiumListDao {
> 0);
}
private static Optional<Money> getPremiumPriceFromList(String label, PremiumList premiumList) {
// Consult the bloom filter and immediately return if the label definitely isn't premium.
if (!premiumList.getBloomFilter().mightContain(label)) {
return Optional.empty();
}
RevisionIdAndLabel revisionIdAndLabel =
RevisionIdAndLabel.create(premiumList.getRevisionId(), label);
try {
Optional<BigDecimal> price = PremiumListCache.cachePremiumEntries.get(revisionIdAndLabel);
return price.map(p -> Money.of(premiumList.getCurrency(), p));
} catch (InvalidCacheLoadException | ExecutionException e) {
throw new RuntimeException(
String.format(
"Could not load premium entry %s for list %s",
revisionIdAndLabel, premiumList.getName()),
e);
}
}
private PremiumListDao() {}
}
@@ -25,6 +25,7 @@
<class>google.registry.schema.cursor.Cursor</class>
<class>google.registry.model.transfer.BaseTransferObject</class>
<class>google.registry.schema.tld.PremiumList</class>
<class>google.registry.schema.tld.PremiumEntry</class>
<class>google.registry.schema.tld.ReservedList</class>
<class>google.registry.model.domain.secdns.DelegationSignerData</class>
<class>google.registry.model.domain.DesignatedContact</class>
@@ -85,6 +85,7 @@ public class JpaTransactionManagerRule extends ExternalResource {
private static final String POSTGRES_DB_NAME = "postgres";
// Name of the optional property that specifies the root path of the golden schema.
// TODO(weiminyu): revert this. The :integration project offers a better solution.
@VisibleForTesting
static final String GOLDEN_SCHEMA_RESOURCE_ROOT_PROP = "sql_schema_resource_root";
@@ -15,13 +15,25 @@
package google.registry.schema.tld;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.testing.DatastoreHelper.createTld;
import static google.registry.testing.DatastoreHelper.newRegistry;
import static google.registry.testing.DatastoreHelper.persistResource;
import static google.registry.testing.JUnitBackports.assertThrows;
import static org.joda.money.CurrencyUnit.JPY;
import static org.joda.money.CurrencyUnit.USD;
import com.google.common.collect.ImmutableMap;
import com.googlecode.objectify.Key;
import google.registry.model.registry.Registry;
import google.registry.model.transaction.JpaTransactionManagerRule;
import google.registry.testing.AppEngineRule;
import java.math.BigDecimal;
import org.joda.money.CurrencyUnit;
import java.util.List;
import java.util.Optional;
import org.joda.money.Money;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -35,6 +47,8 @@ public class PremiumListDaoTest {
public final JpaTransactionManagerRule jpaTmRule =
new JpaTransactionManagerRule.Builder().build();
@Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build();
private static final ImmutableMap<String, BigDecimal> TEST_PRICES =
ImmutableMap.of(
"silver",
@@ -46,7 +60,7 @@ public class PremiumListDaoTest {
@Test
public void saveNew_worksSuccessfully() {
PremiumList premiumList = PremiumList.create("testname", CurrencyUnit.USD, TEST_PRICES);
PremiumList premiumList = PremiumList.create("testname", USD, TEST_PRICES);
PremiumListDao.saveNew(premiumList);
jpaTm()
.transact(
@@ -64,22 +78,114 @@ public class PremiumListDaoTest {
});
}
@Test
public void update_worksSuccessfully() {
PremiumListDao.saveNew(
PremiumList.create(
"testname", USD, ImmutableMap.of("firstversion", BigDecimal.valueOf(123.45))));
PremiumListDao.update(PremiumList.create("testname", USD, TEST_PRICES));
jpaTm()
.transact(
() -> {
List<PremiumList> persistedLists =
jpaTm()
.getEntityManager()
.createQuery(
"SELECT pl FROM PremiumList pl WHERE pl.name = :name ORDER BY"
+ " pl.revisionId",
PremiumList.class)
.setParameter("name", "testname")
.getResultList();
assertThat(persistedLists).hasSize(2);
assertThat(persistedLists.get(1).getLabelsToPrices())
.containsExactlyEntriesIn(TEST_PRICES);
assertThat(persistedLists.get(1).getCreationTimestamp())
.isEqualTo(jpaTmRule.getTxnClock().nowUtc());
});
}
@Test
public void saveNew_throwsWhenPremiumListAlreadyExists() {
PremiumListDao.saveNew(PremiumList.create("testlist", CurrencyUnit.USD, TEST_PRICES));
PremiumListDao.saveNew(PremiumList.create("testlist", USD, TEST_PRICES));
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
PremiumListDao.saveNew(
PremiumList.create("testlist", CurrencyUnit.USD, TEST_PRICES)));
assertThat(thrown).hasMessageThat().contains("A premium list of this name already exists");
() -> PremiumListDao.saveNew(PremiumList.create("testlist", USD, TEST_PRICES)));
assertThat(thrown).hasMessageThat().isEqualTo("Premium list 'testlist' already exists");
}
@Test
public void update_throwsWhenPremiumListDoesntExist() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> PremiumListDao.update(PremiumList.create("testlist", USD, TEST_PRICES)));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Can't update non-existent premium list 'testlist'");
}
@Test
public void checkExists_worksSuccessfully() {
assertThat(PremiumListDao.checkExists("testlist")).isFalse();
PremiumListDao.saveNew(PremiumList.create("testlist", CurrencyUnit.USD, TEST_PRICES));
PremiumListDao.saveNew(PremiumList.create("testlist", USD, TEST_PRICES));
assertThat(PremiumListDao.checkExists("testlist")).isTrue();
}
@Test
public void getLatestRevision_returnsEmptyForNonexistentList() {
assertThat(PremiumListDao.getLatestRevision("nonexistentlist")).isEmpty();
}
@Test
public void getLatestRevision_worksSuccessfully() {
PremiumListDao.saveNew(
PremiumList.create("list1", JPY, ImmutableMap.of("wrong", BigDecimal.valueOf(1000.50))));
PremiumListDao.update(PremiumList.create("list1", JPY, TEST_PRICES));
jpaTm()
.transact(
() -> {
Optional<PremiumList> persistedList = PremiumListDao.getLatestRevision("list1");
assertThat(persistedList).isPresent();
assertThat(persistedList.get().getName()).isEqualTo("list1");
assertThat(persistedList.get().getCurrency()).isEqualTo(JPY);
assertThat(persistedList.get().getLabelsToPrices())
.containsExactlyEntriesIn(TEST_PRICES);
});
}
@Test
public void getPremiumPrice_returnsNoneWhenNoPremiumListConfigured() {
persistResource(newRegistry("foobar", "FOOBAR").asBuilder().setPremiumList(null).build());
assertThat(PremiumListDao.getPremiumPrice("rich", Registry.get("foobar"))).isEmpty();
}
@Test
public void getPremiumPrice_worksSuccessfully() {
persistResource(
newRegistry("foobar", "FOOBAR")
.asBuilder()
.setPremiumListKey(
Key.create(
getCrossTldKey(),
google.registry.model.registry.label.PremiumList.class,
"premlist"))
.build());
PremiumListDao.saveNew(PremiumList.create("premlist", USD, TEST_PRICES));
assertThat(PremiumListDao.getPremiumPrice("silver", Registry.get("foobar")))
.hasValue(Money.of(USD, 10.23));
assertThat(PremiumListDao.getPremiumPrice("gold", Registry.get("foobar")))
.hasValue(Money.of(USD, 1305.47));
assertThat(PremiumListDao.getPremiumPrice("zirconium", Registry.get("foobar"))).isEmpty();
}
@Test
public void testGetPremiumPrice_throwsWhenPremiumListCantBeLoaded() {
createTld("tld");
IllegalStateException thrown =
assertThrows(
IllegalStateException.class,
() -> PremiumListDao.getPremiumPrice("foobar", Registry.get("tld")));
assertThat(thrown).hasMessageThat().isEqualTo("Could not load premium list 'tld'");
}
}
+3 -2
View File
@@ -106,17 +106,18 @@ task compileApiJar(type: Jar) {
configurations {
compileApi
schema
}
artifacts {
archives schemaJar
compileApi compileApiJar
schema schemaJar
}
publishing {
repositories {
maven {
url project.schema_publish_repo
url project.publish_repo
}
}
publications {
@@ -0,0 +1,3 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
@@ -0,0 +1,16 @@
-- Copyright 2019 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.
-- Remove default set in V9 that we no longer need.
alter table "PremiumList" alter column currency drop default;
@@ -15,5 +15,5 @@
-- Note: We're OK with dropping this data since it's not live in production yet.
alter table "PremiumList" drop column if exists currency;
-- TODO(mcilwain): Add a subsequent schema change to remove this default.
-- Note: This default was removed in V11.
alter table "PremiumList" add column currency text not null default 'USD';
@@ -132,8 +132,8 @@
create table "PremiumEntry" (
revision_id int8 not null,
price numeric(19, 2) not null,
domain_label text not null,
price numeric(19, 2) not null,
primary key (revision_id, domain_label)
);
@@ -93,7 +93,7 @@ CREATE TABLE public."PremiumList" (
creation_timestamp timestamp with time zone NOT NULL,
name text NOT NULL,
bloom_filter bytea NOT NULL,
currency text DEFAULT 'USD'::text NOT NULL
currency text NOT NULL
);
+6 -3
View File
@@ -22,7 +22,10 @@ dbName=postgres
dbUser=
dbPassword=
# Maven repository of the Cloud SQL schema jar, which contains the
# SQL DDL scripts.
schema_publish_repo=
# Maven repository that hosts the Cloud SQL schema jar and the registry
# server test jars. Such jars are needed for server/schema integration tests.
# Please refer to <a href="./integration/README.md">integration project</a>
# for more information.
publish_repo=
schema_version=
nomulus_version=
+120
View File
@@ -0,0 +1,120 @@
## Summary
This project runs cross-version server/schema integration tests with arbitrary
version pairs. It may be used by presubmit tests and continuous-integration
tests, or as a gating test during release and/or deployment.
## Maven Dependencies
This release process is expected to publish the following Maven dependencies to
a well-known repository:
* google.registry:schema, which contains the schema DDL scripts. This is done
by the ':db:publish' task.
* google.registry:nomulus_test, which contains the nomulus classes and
dependencies needed for the integration tests. This is done by the
':core:publish' task.
After each deployment in sandbox or production, the deployment process is
expected to save the version tag of the binary or schema along with the
environment. These tags will be made available to test runners.
## Usage
The ':integration:sqlIntegrationTest' task is the test runner. It uses the
following properties:
* nomulus_version: a Registry server release tag, or 'local' if the code in
the local Git tree should be used.
* schema_version: a schema release tag, or 'local' if the code in the local
Git tree should be used.
* publish_repo: the Maven repository where release jars may be found. This is
required if neither of the above is 'local'.
Given a program 'fetch_version_tag' that retrieves the currently deployed
version tag of SQL schema or server binary in a particular environment (which as
mentioned earlier are saved by the deployment process), the following code
snippet checks if the current PR or local clone has schema changes, and if yes,
tests the production server's version with the new schema.
```shell
current_prod_schema=$(fetch_version_tag schema production)
current_prod_server=$(fetch_version_tag server production)
schema_changes=$(git diff ${current_prod_schema} --name-only \
./db/src/main/resources/sql/flyway/ | wc -l)
[[ schema_changes -gt 0 ]] && ./gradlew :integration:sqlIntegrationTest \
-Ppublish_repo=${REPO} -Pschema_version=local \
-Pnomulus_version=current_prod_server
```
## Implementation Notes
### Run Tests from Jar
Gradle test runner does not look for runnable tests in jars. We must extract
tests to a directory. For now, only the SqlIntegrationTestSuite.class needs to
be extracted. Gradle has no trouble finding its member classes.
### Hibernate Behavior
If all :core classes (main and test) and dependencies are assembled in a single
jar, Hibernate behaves strangely: every time an EntityManagerFactory is created,
regardless of what Entity classes are specified, Hibernate would scan the entire
jar for all Entity classes and complain about duplicate mapping (due to the
TestEntity classes declared by tests).
We worked around this problem by creating two jars from :core:
* The nomulus-public.jar: contains the classes and resources in the main
sourceSet (and excludes internal files under the config package).
* The nomulus-tests-alldeps.jar: contains the test classes as well as all
dependencies.
## Alternatives Tried
### Use Git Branches
One alternative is to rely on Git branches to set up the classes. For example,
the shell snippet shown earlier can be implemented as:
```shell
current_prod_schema=$(fetch_version_tag schema production)
current_prod_server=$(fetch_version_tag server production)
schema_changes=$(git diff ${current_prod_schema} --name-only \
./db/src/main/resources/sql/flyway/ | wc -l)
if [[ schema_changes -gt 0 ]]; then
current_branch=$(git rev-parse --abbrev-ref HEAD)
schema_folder=$(mktemp -d)
./gradlew :db:schemaJar && cp ./db/build/libs/schema.jar ${schema_folder}
git checkout ${current_prod_server}
./gradlew sqlIntegrationTest \
-Psql_schema_resource_root=${schema_folder}/schema.jar
git checkout ${current_branch}
fi
```
The drawbacks of this approach include:
* Switching branches back and forth is error-prone and risky, especially when
we run this as a gating test during release.
* Switching branches makes implicit assumptions on how the test platform would
check out the repository (e.g., whether we may be on a headless branch when
we switch).
* The generated jar is not saved, making it harder to troubleshoot.
* To use this locally during development, the Git tree must not have
uncommitted changes.
### Smaller Jars
Another alternative follows the same idea as our current approach. However,
instead of including dependencies in a fat jar, it simply records their versions
in a file. At testing time these dependencies will be imported into the gradle
project file with forced resolution (e.g., testRuntime ('junit:junit:4.12)'
{forced = true} ). This way the published jars will be smaller.
This approach conflicts with our current dependency-locking processing. Due to
issues with the license-check plugin, dependency-locking is activated after all
projects are evaluated. This approach will resolve some configurations in :core
(and make them immutable) during evaluation, causing the lock-activation (which
counts as a mutation) call to fail.
+99
View File
@@ -0,0 +1,99 @@
// Copyright 2019 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.
// This source-less project is used to run cross-release server/SQL integration
// tests. See the README.md file in this folder for more information.
import static com.google.common.base.Preconditions.checkArgument
import static com.google.common.base.Strings.isNullOrEmpty
if (schema_version == '' || nomulus_version == '') {
return
}
def USE_LOCAL = 'local'
if (schema_version != USE_LOCAL || nomulus_version != USE_LOCAL) {
checkArgument(
!isNullOrEmpty(publish_repo),
'The publish_repo is required when remote jars are needed.')
repositories {
maven {
url project.publish_repo
}
}
}
def testUberJarName = ''
dependencies {
gradleLint.ignore('unused-dependency') {
if (schema_version == USE_LOCAL) {
testRuntime project(path: ':db', configuration: 'schema')
} else {
testRuntime "google.registry:schema:${schema_version}"
}
if (nomulus_version == USE_LOCAL) {
testRuntime project(path: ':core', configuration: 'nomulus_test')
testUberJarName = 'nomulus-tests-alldeps.jar'
} else {
testRuntime "google.registry:nomulus_test:${nomulus_version}:public"
testRuntime "google.registry:nomulus_test:${nomulus_version}:alldeps"
testUberJarName = "nomulus_test-${nomulus_version}-alldeps.jar"
}
}
}
configurations.testRuntime.transitive = false
def unpackedTestDir = "${projectDir}/build/unpackedTests/${nomulus_version}"
// Extracts SqlIntegrationTestSuite.class to a temp folder. Gradle's test
// runner only looks for runnable tests on a regular file system. However,
// it can load member classes of test suites from jars.
task extractSqlIntegrationTestSuite (type: Copy) {
doFirst {
file(unpackedTestDir).mkdirs()
}
outputs.dir unpackedTestDir
from zipTree(
configurations.testRuntime
.filter { it.name == testUberJarName}
.singleFile).matching {
include 'google/registry/**/SqlIntegrationTestSuite.class'
}
into unpackedTestDir
includeEmptyDirs = false
}
// TODO(weiminyu): inherit from FilteringTest (defined in :core).
task sqlIntegrationTest(type: Test) {
testClassesDirs = files(unpackedTestDir)
classpath = configurations.testRuntime
include 'google/registry/schema/integration/SqlIntegrationTestSuite.*'
dependsOn extractSqlIntegrationTestSuite
finalizedBy tasks.create('removeUnpackedTests') {
doLast {
delete file(unpackedTestDir)
}
}
// Disable incremental build/test since Gradle cannot detect changes
// in dependencies on its own. Will not fix since this test is typically
// run once (in presubmit or ci tests).
outputs.upToDateWhen { false }
}
@@ -0,0 +1,3 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
@@ -106,7 +106,7 @@ public class SslClientInitializer<C extends Channel> extends ChannelInitializer<
SslContextBuilder.forClient()
.sslProvider(sslProvider)
.trustManager(
trustedCertificates.isEmpty()
trustedCertificates == null || trustedCertificates.isEmpty()
? null
: trustedCertificates.toArray(new X509Certificate[0]));
+16 -4
View File
@@ -65,8 +65,9 @@ steps:
# Build and package the deployment files for production.
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
args: ['release/build_nomulus_for_env.sh', 'production', 'output']
# Tentatively build Cloud SQL schema jar here, before schema release process
# is finalized.
# Tentatively build and publish Cloud SQL schema jar here, before schema release
# process is finalized. Also publish nomulus:core jars that are needed for
# server/schema compatibility tests.
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
entrypoint: /bin/bash
args:
@@ -74,9 +75,20 @@ steps:
- |
set -e
./gradlew \
:db:schemaJar \
:db:publish \
-PmavenUrl=https://storage.googleapis.com/domain-registry-maven-repository/maven \
-PpluginsUrl=https://storage.googleapis.com/domain-registry-maven-repository/plugins
-PpluginsUrl=https://storage.googleapis.com/domain-registry-maven-repository/plugins \
-Ppublish_repo=gcs://${PROJECT_ID}-deployed-tags/maven \
-Pschema_version=${TAG_NAME}
./gradlew \
:core:publish \
-PmavenUrl=https://storage.googleapis.com/domain-registry-maven-repository/maven \
-PpluginsUrl=https://storage.googleapis.com/domain-registry-maven-repository/plugins \
-Ppublish_repo=gcs://${PROJECT_ID}-deployed-tags/maven \
-Pnomulus_version=${TAG_NAME}
# Upload schema jar for use by schema deployment.
# TODO(weiminyu): consider using the jar in maven repo during deployment and
# stop the upload here.
cp db/build/libs/schema.jar output/
# The tarballs and jars to upload to GCS.
artifacts:
+1
View File
@@ -30,6 +30,7 @@ rootProject.name = 'nomulus'
include 'common'
include 'core'
include 'db'
include 'integration'
include 'networking'
include 'prober'
include 'proxy'