Compare commits

..
Author SHA1 Message Date
sarahcaseybotandGitHub 2d1a67b01b Add a parameter to prevent spec11 from sending emails (#1407) 2021-11-05 13:02:59 -04:00
Rachel GuanandGitHub 01d3932122 Test vkey behaviors when in a task queue (#1406)
* Test vkey behavior in task queue
2021-11-04 21:04:18 -04:00
sarahcaseybotandGitHub 2eb8bb3996 Add Cloud SQL queries for transaction reports (#1397)
* Add the Cloud SQL queries for transaction reports

* Add the remaining queries

* Some query fixes

* Fix comments

* Fix indentation in total_nameservers

* Fix indentation on other Case condition
2021-11-03 11:25:31 -04:00
Rachel GuanandGitHub 2218663d55 Add VKey to String and String to VKey methods (#1396)
* Add stringify and parse methods to SerializeUTils

* Improve comments and test cases

* Fix comments and test strings

* Fix dependency warning
2021-11-02 13:25:35 -04:00
gbrodmanandGitHub e0dc2e43bb Pass the ICANN reporting BQ dataset to the DNS query coordinator (#1405) 2021-11-02 13:24:04 -04:00
Weimin YuandGitHub 7fedd40739 Fix InitSqlPipeline regarding synthesized history (#1404)
* Fix InitSqlPipeline regarding synthesized history

There are a few bad domains in Datastore that we hardcoded to ignore
during SQL population. They didn't have history so we didn't try to
filter when writing history.

Recently we created synthesized history for domains, including the bad
domains. Now we need to filter History entries.
2021-11-02 11:12:57 -04:00
Weimin YuandGitHub f793ca5b68 Support shared database snapshot (#1403)
* Support shared database snapshot

Allow multiple workers to share a CONSISTENT database snapshot. The
motivating use case is SQL database snapshot loading, where it is too
slow to depend on one worker to load everything.

This currently is postgresql-specific, but will be improved to be
vendor-independent.

Also made sure AppEngineEnvironment.java clears the cached environment
in call cases when tearing down.
2021-11-01 13:01:37 -04:00
gbrodmanandGitHub 395ed19601 Canonicalize domain/host names in async DS->SQL replay (#1350) 2021-11-01 12:08:20 -04:00
Michael MullerandGitHub cecc1a6cc7 Update terraform files and instructions (#1402)
* Update terraform files and instructions

Update proxy terraform files based on current best practices and allow
exclusion of forwarding rules for HTTP endpoints.  Specifically:
-   Add a "public_web_whois" input to allow disabling the public HTTP
    whois forwarding.
-   Add "description" fields to all variables.
-   Move outputs of the top-level module into "outputs.tf".
-   Auto-reformat using hclfmt.
2021-10-29 09:10:23 -04:00
Rachel GuanandGitHub 77bc072aac Add domain pa notification response to first delete domain poll message (#1400)
* Add domain pa notification response to first delete domain poll message

* Add test case for poll message

* Change time in response data to now
2021-10-28 15:45:50 -04:00
Weimin YuandGitHub 93a479837f Make entities serializable for DB validation (#1401)
* Make entities serializable for DB validation

Make entities that are asynchronously replicated between Datastore and
Cloud SQL serializable so that they may be used in BEAM pipeline based
comparison tool.

Introduced an UnsafeSerializable interface (extending Serializable) and
added to relevant classes. Implementing classes are allowed some
shortcuts as explained in the interface's Javadoc. Post migration we
will decide whether to revert this change or properly implement
serialization.

Verified with production data.
2021-10-28 12:19:09 -04:00
gbrodmanandGitHub 1e7aae26a3 Create a mechanism for storing / using locks explicitly only in SQL (#1392)
This is used for the replay locks so that Beam pipelines (which will be
used for database comparison) can acquire / release locks as necessary
to avoid database contention. If we're comparing contents of Datastore
and SQL databases, we shouldn't have replay actively running during the
comparison, so the pipeline will grab the locks.

Beam doesn't always play nicely with loading from / saving to Datastore,
so we need to make sure that we store the replay locks in SQL at all
times, even when Datastore is the primary DB.
2021-10-27 16:20:35 -04:00
Michael MullerandGitHub 201b6e8e0b Re-enable replay tests for most environments (#1399)
* Re-enable replay tests for most environments

This enables the replay tests except in environments where
the NOMULUS_DISABLE_REPLAY_TESTS environment variable is set to "true".

* Add a check for null
2021-10-25 12:11:02 -04:00
Rachel GuanandGitHub 43074ea32f Send expiring notification emails to admins if no tech emails are on file (#1387)
* Send emails to admin if tech emails are not present

* Improve test cases and comments
2021-10-21 12:59:31 -04:00
Weimin YuandGitHub 1a4a31569e Alt entity model for fast JPA bulk query (#1398)
* Alt entity model for fast JPA bulk query

Defined an alternative JPA entity model that allows fast bulk loading of
multi-level entities, DomainBase and DomainHistory. The idea is to bulk
the base table as well as the child tables separately, and assemble them
into the target entity in memory in a pipeline.

For DomainBase:

- Defined a DomainBaseLite class that models the "Domain" table only.

- Defined a DomainHost class that models the "DomainHost" table
  (nsHosts field).

- Exposed ID fields in GracePeriod so that they can be mapped to domains
  after being loaded into memory.

For DomainHistory:

- Defined a DomainHistoryLite class that models the "DomainHistory"
  table only.

- Defined a DomainHistoryHost class that models its namesake table.

- Exposed ID fields in GracePeriodHistory and DomainDsDataHistory
  classes so that they can be mapped to DomainHistory after being
  loaded into memory.

In PersistenceModule, provisioned a JpaTransactionManager that uses
the alternative entity model.

Also added a pipeline option that specifies which JpaTransactionManager
to use in a pipeline.
2021-10-20 16:48:56 -04:00
gbrodmanandGitHub c7f50dae92 Use READ_COMMITTED serialization level in CreateSyntheticHEA (#1395)
I observed an instance in which a couple queries from this action were,
for whatever reason, hanging around as idle for >30 minutes. Assuming
the behavior that we saw before where "an open idle serializable
transaction means all pg read-locks stick around forever" still holds,
that's the reason why the amount of read-locks in use spirals out of
control.

I'm not sure why those queries aren't timing out, but that's a separate
issue.
2021-10-19 11:36:15 -04:00
Michael MullerandGitHub 7344c424d1 Fix problems with the format tasks (#1390)
* Fix problems with the format tasks

The format check is using python2, and if "python" doesn't exist on the path
(or isn't python 2, or there is any other error in the python code or in the
shell script...) the format check just succeeds.

This change:
- Refactors out the gradle code that finds a python3 executable and use it
  to get the python executable to be used for the format check.
- Upgrades google-java-format-diff.py to python3 and removes #! line.
- Fixes shell script to ensure that failures are propagated.
- Suppresses error output when checking for python commands.

Tested:
- verified that python errors cause the build to fail
- verified that introducing a bad format diff causes check to fail
- verified that javaIncrementalFormatDryRun shows the diffs that would be
  introduced.
- verified that javaIncrementalFormatApply reformats a file.
- verified that well formatted code passes the format check.
- verified that an invalid or missing PYTHON env var causes
  google-java-format-git-diff.sh to fail with the appropriate error.

* Fix presubmit issues

Omit the format presubmit when not in a git repo and remove unused "string"
import.
2021-10-18 08:10:09 -04:00
136 changed files with 4412 additions and 509 deletions
+39 -26
View File
@@ -200,11 +200,28 @@ rootProject.ext {
pyver = { exe ->
try {
ext.execInBash(
exe + " -c 'import sys; print(sys.hexversion)'", "/") as Integer
exe + " -c 'import sys; print(sys.hexversion)' 2>/dev/null",
"/") as Integer
} catch (org.gradle.process.internal.ExecException e) {
return -1;
}
}
// Return the path to a usable python3 executable.
getPythonExecutable = {
// Find a python version greater than 3.7.3 (this is somewhat arbitrary, we
// know we'd like at least 3.6, but 3.7.3 is the latest that ships with
// Debian so it seems like that should be available anywhere).
def MIN_PY_VER = 0x3070300
if (pyver('python') >= MIN_PY_VER) {
return 'python'
} else if (pyver('/usr/bin/python3') >= MIN_PY_VER) {
return '/usr/bin/python3'
} else {
throw new GradleException("No usable Python version found (build " +
"requires at least python 3.7.3)");
}
}
}
task runPresubmits(type: Exec) {
@@ -212,18 +229,7 @@ task runPresubmits(type: Exec) {
args('config/presubmits.py')
doFirst {
// Find a python version greater than 3.7.3 (this is somewhat arbitrary, we
// know we'd like at least 3.6, but 3.7.3 is the latest that ships with
// Debian so it seems like that should be available anywhere).
def MIN_PY_VER = 0x3070300
if (pyver('python') >= MIN_PY_VER) {
executable 'python'
} else if (pyver('/usr/bin/python3') >= MIN_PY_VER) {
executable '/usr/bin/python3'
} else {
throw new GradleException("No usable Python version found (build " +
"requires at least python 3.7.3)");
}
executable getPythonExecutable()
}
}
@@ -438,9 +444,10 @@ rootProject.ext {
? "${rootDir}/.."
: rootDir
def formatDiffScript = "${scriptDir}/google-java-format-git-diff.sh"
def pythonExe = getPythonExecutable()
return ext.execInBash(
"${formatDiffScript} ${action}", "${workingDir}")
"PYTHON=${pythonExe} ${formatDiffScript} ${action}", "${workingDir}")
}
}
@@ -448,18 +455,23 @@ rootProject.ext {
// Note that this task checks modified Java files in the entire repository.
task javaIncrementalFormatCheck {
doLast {
def checkResult = invokeJavaDiffFormatScript("check")
if (checkResult == 'true') {
throw new IllegalStateException(
"Some Java files need to be reformatted. You may use the "
+ "'javaIncrementalFormatDryRun' task to review\n "
+ "the changes, or the 'javaIncrementalFormatApply' task "
+ "to reformat.")
} else if (checkResult != 'false') {
throw new RuntimeException(
"Failed to invoke format check script:\n" + checkResult)
// We can only do this in a git tree.
if (new File("${rootDir}/.git").exists()) {
def checkResult = invokeJavaDiffFormatScript("check")
if (checkResult == 'true') {
throw new IllegalStateException(
"Some Java files need to be reformatted. You may use the "
+ "'javaIncrementalFormatDryRun' task to review\n "
+ "the changes, or the 'javaIncrementalFormatApply' task "
+ "to reformat.")
} else if (checkResult != 'false') {
throw new RuntimeException(
"Failed to invoke format check script:\n" + checkResult)
}
println("Incremental Java format check ok.")
} else {
println("Omitting format check: not in a git directory.")
}
println("Incremental Java format check ok.")
}
}
@@ -489,7 +501,8 @@ task javadoc(type: Javadoc) {
options.addBooleanOption('Xdoclint:all,-missing', true)
options.addBooleanOption("-allow-script-in-comments",true)
options.tags = ["type:a:Generic Type",
"error:a:Expected Error"]
"error:a:Expected Error",
"invariant:a:Guaranteed Property"]
}
tasks.build.dependsOn(tasks.javadoc)
+2 -1
View File
@@ -119,9 +119,10 @@ PRESUBMITS = {
"AppEngineExtension.register(...) instead.",
# PostgreSQLContainer instantiation must specify docker tag
# TODO(b/204572437): Fix the pattern to pass DatabaseSnapshotTest.java
PresubmitCheck(
r"[\s\S]*new\s+PostgreSQLContainer(<[\s\S]*>)?\(\s*\)[\s\S]*",
"java", {}):
"java", {"DatabaseSnapshotTest.java"}):
"PostgreSQLContainer instantiation must specify docker tag.",
# Various Soy linting checks
@@ -112,7 +112,7 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
return;
}
Optional<Lock> lock =
Lock.acquire(
Lock.acquireSql(
this.getClass().getSimpleName(), null, LEASE_LENGTH, requestStatusChecker, false);
if (!lock.isPresent()) {
String message = "Can't acquire SQL commit log replay lock, aborting.";
@@ -113,9 +113,11 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
}
/**
* Returns a list of registrars that should receive expiring notification emails. There are two
* certificates that should be considered (the main certificate and failOver certificate). The
* registrars should receive notifications if one of the certificate checks returns true.
* Returns a list of registrars that should receive expiring notification emails.
*
* <p>There are two certificates that should be considered (the main certificate and failOver
* certificate). The registrars should receive notifications if one of the certificate checks
* returns true.
*/
@VisibleForTesting
ImmutableList<RegistrarInfo> getRegistrarsWithExpiringCertificates() {
@@ -157,15 +159,17 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
}
try {
ImmutableSet<InternetAddress> recipients = getEmailAddresses(registrar, Type.TECH);
ImmutableSet<InternetAddress> ccs = getEmailAddresses(registrar, Type.ADMIN);
Date expirationDate = certificateChecker.getCertificate(certificate.get()).getNotAfter();
logger.atInfo().log(
"Registrar %s should receive an email that its %s SSL certificate will expire on %s.",
registrar.getRegistrarName(),
" %s SSL certificate of registrar '%s' will expire on %s.",
certificateType.getDisplayName(),
registrar.getRegistrarName(),
expirationDate.toString());
if (recipients.isEmpty()) {
if (recipients.isEmpty() && ccs.isEmpty()) {
logger.atWarning().log(
"Registrar %s contains no email addresses to receive notification email.",
"Registrar %s contains no TECH nor ADMIN email addresses to receive notification"
+ " email.",
registrar.getRegistrarName());
return false;
}
@@ -180,7 +184,7 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
expirationDate,
registrar.getRegistrarId()))
.setRecipients(recipients)
.setCcs(getEmailAddresses(registrar, Type.ADMIN))
.setCcs(ccs)
.build());
/*
* A duration time offset is used here to ensure that date comparison between two
@@ -249,30 +253,32 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
/** Sends notification emails to registrars with expiring certificates. */
@VisibleForTesting
int sendNotificationEmails() {
int emailsSent = 0;
int numEmailsSent = 0;
for (RegistrarInfo registrarInfo : getRegistrarsWithExpiringCertificates()) {
Registrar registrar = registrarInfo.registrar();
if (registrarInfo.isCertExpiring()) {
sendNotificationEmail(
registrar,
registrar.getLastExpiringCertNotificationSentDate(),
CertificateType.PRIMARY,
registrar.getClientCertificate());
emailsSent++;
if (registrarInfo.isCertExpiring()
&& sendNotificationEmail(
registrar,
registrar.getLastExpiringCertNotificationSentDate(),
CertificateType.PRIMARY,
registrar.getClientCertificate())) {
numEmailsSent++;
}
if (registrarInfo.isFailOverCertExpiring()) {
sendNotificationEmail(
registrar,
registrar.getLastExpiringFailoverCertNotificationSentDate(),
CertificateType.FAILOVER,
registrar.getFailoverClientCertificate());
emailsSent++;
if (registrarInfo.isFailOverCertExpiring()
&& sendNotificationEmail(
registrar,
registrar.getLastExpiringFailoverCertNotificationSentDate(),
CertificateType.FAILOVER,
registrar.getFailoverClientCertificate())) {
numEmailsSent++;
}
}
return emailsSent;
return numEmailsSent;
}
/** Returns a list of email addresses of the registrar that should receive a notification email */
/**
* Returns a list of email addresses of the registrar that should receive a notification email.
*/
@VisibleForTesting
ImmutableSet<InternetAddress> getEmailAddresses(Registrar registrar, Type contactType) {
ImmutableSortedSet<RegistrarContact> contacts = registrar.getContactsOfType(contactType);
@@ -0,0 +1,88 @@
// Copyright 2021 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.beam.common;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import com.google.common.flogger.FluentLogger;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
/**
* A database snapshot shareable by concurrent queries from multiple database clients. A snapshot is
* uniquely identified by its {@link #getSnapshotId snapshotId}, and must stay open until all
* concurrent queries to this snapshot have attached to it by calling {@link
* google.registry.persistence.transaction.JpaTransactionManager#setDatabaseSnapshot}. However, it
* can be closed before those queries complete.
*
* <p>This feature is <em>Postgresql-only</em>.
*
* <p>To support large queries, transaction isolation level is fixed at the REPEATABLE_READ to avoid
* exhausting predicate locks at the SERIALIZABLE level.
*/
// TODO(b/193662898): vendor-independent support for richer transaction semantics.
public class DatabaseSnapshot implements AutoCloseable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private String snapshotId;
private EntityManager entityManager;
private EntityTransaction transaction;
private DatabaseSnapshot() {}
public String getSnapshotId() {
checkState(entityManager != null, "Snapshot not opened yet.");
checkState(entityManager.isOpen(), "Snapshot already closed.");
return snapshotId;
}
private DatabaseSnapshot open() {
entityManager = jpaTm().getStandaloneEntityManager();
transaction = entityManager.getTransaction();
transaction.setRollbackOnly();
transaction.begin();
entityManager
.createNativeQuery("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ")
.executeUpdate();
List<?> snapshotIds =
entityManager.createNativeQuery("SELECT pg_export_snapshot();").getResultList();
checkState(snapshotIds.size() == 1, "Unexpected number of snapshots: %s", snapshotIds.size());
snapshotId = (String) snapshotIds.get(0);
return this;
}
@Override
public void close() {
if (transaction != null && transaction.isActive()) {
try {
transaction.rollback();
} catch (Exception e) {
logger.atWarning().withCause(e).log("Failed to close a Database Snapshot");
}
}
if (entityManager != null && entityManager.isOpen()) {
entityManager.close();
}
}
public static DatabaseSnapshot createSnapshot() {
return new DatabaseSnapshot().open();
}
}
@@ -138,6 +138,9 @@ public final class RegistryJpaIO {
abstract Coder<T> coder();
@Nullable
abstract String snapshotId();
abstract Builder<R, T> toBuilder();
@Override
@@ -145,7 +148,9 @@ public final class RegistryJpaIO {
public PCollection<T> expand(PBegin input) {
return input
.apply("Starting " + name(), Create.of((Void) null))
.apply("Run query for " + name(), ParDo.of(new QueryRunner<>(query(), resultMapper())))
.apply(
"Run query for " + name(),
ParDo.of(new QueryRunner<>(query(), resultMapper(), snapshotId())))
.setCoder(coder())
.apply("Reshuffle", Reshuffle.viaRandomKey());
}
@@ -162,6 +167,18 @@ public final class RegistryJpaIO {
return toBuilder().coder(coder).build();
}
/**
* Specifies the database snapshot to use for this query.
*
* <p>This feature is <em>Postgresql-only</em>. User is responsible for keeping the snapshot
* available until all JVM workers have started using it by calling {@link
* JpaTransactionManager#setDatabaseSnapshot}.
*/
// TODO(b/193662898): vendor-independent support for richer transaction semantics.
public Read<R, T> withSnapshot(String snapshotId) {
return toBuilder().snapshotId(snapshotId).build();
}
static <R, T> Builder<R, T> builder() {
return new AutoValue_RegistryJpaIO_Read.Builder<R, T>()
.name(DEFAULT_NAME)
@@ -179,6 +196,8 @@ public final class RegistryJpaIO {
abstract Builder<R, T> coder(Coder coder);
abstract Builder<R, T> snapshotId(@Nullable String sharedSnapshotId);
abstract Read<R, T> build();
Builder<R, T> criteriaQuery(CriteriaQuerySupplier<R> criteriaQuery) {
@@ -201,17 +220,28 @@ public final class RegistryJpaIO {
static class QueryRunner<R, T> extends DoFn<Void, T> {
private final RegistryQuery<R> query;
private final SerializableFunction<R, T> resultMapper;
// java.util.Optional is not serializable. Use of Guava Optional is discouraged.
@Nullable private final String snapshotId;
QueryRunner(RegistryQuery<R> query, SerializableFunction<R, T> resultMapper) {
QueryRunner(
RegistryQuery<R> query,
SerializableFunction<R, T> resultMapper,
@Nullable String snapshotId) {
this.query = query;
this.resultMapper = resultMapper;
this.snapshotId = snapshotId;
}
@ProcessElement
public void processElement(OutputReceiver<T> outputReceiver) {
jpaTm()
.transactNoRetry(
() -> query.stream().map(resultMapper::apply).forEach(outputReceiver::output));
() -> {
if (snapshotId != null) {
jpaTm().setDatabaseSnapshot(snapshotId);
}
query.stream().map(resultMapper::apply).forEach(outputReceiver::output);
});
}
}
}
@@ -21,6 +21,7 @@ import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.persistence.PersistenceModule;
import google.registry.persistence.PersistenceModule.BeamBulkQueryJpaTm;
import google.registry.persistence.PersistenceModule.BeamJpaTm;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.transaction.JpaTransactionManager;
@@ -45,9 +46,19 @@ public interface RegistryPipelineComponent {
@Config("projectId")
String getProjectId();
/** Returns the regular {@link JpaTransactionManager} for general use. */
@BeamJpaTm
Lazy<JpaTransactionManager> getJpaTransactionManager();
/**
* Returns a {@link JpaTransactionManager} optimized for bulk loading multi-level JPA entities
* ({@link google.registry.model.domain.DomainBase} and {@link
* google.registry.model.domain.DomainHistory}). Please refer to {@link
* google.registry.model.bulkquery.BulkQueryEntities} for more information.
*/
@BeamBulkQueryJpaTm
Lazy<JpaTransactionManager> getBulkQueryJpaTransactionManager();
@Component.Builder
interface Builder {
@@ -16,6 +16,7 @@ package google.registry.beam.common;
import google.registry.beam.common.RegistryJpaIO.Write;
import google.registry.config.RegistryEnvironment;
import google.registry.persistence.PersistenceModule.JpaTransactionManagerType;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import java.util.Objects;
import javax.annotation.Nullable;
@@ -44,6 +45,12 @@ public interface RegistryPipelineOptions extends GcpOptions {
void setIsolationOverride(TransactionIsolationLevel isolationOverride);
@Description("The JPA Transaction Manager to use.")
@Default.Enum(value = "REGULAR")
JpaTransactionManagerType getJpaTransactionManagerType();
void setJpaTransactionManagerType(JpaTransactionManagerType jpaTransactionManagerType);
@Description("The number of entities to write to the SQL database in one operation.")
@Default.Integer(20)
int getSqlWriteBatchSize();
@@ -49,10 +49,19 @@ public class RegistryPipelineWorkerInitializer implements JvmInitializer {
}
logger.atInfo().log("Setting up RegistryEnvironment %s.", environment);
environment.setup();
Lazy<JpaTransactionManager> transactionManagerLazy =
toRegistryPipelineComponent(registryOptions).getJpaTransactionManager();
RegistryPipelineComponent registryPipelineComponent =
toRegistryPipelineComponent(registryOptions);
Lazy<JpaTransactionManager> transactionManagerLazy;
switch (registryOptions.getJpaTransactionManagerType()) {
case BULK_QUERY:
transactionManagerLazy = registryPipelineComponent.getBulkQueryJpaTransactionManager();
break;
case REGULAR:
default:
transactionManagerLazy = registryPipelineComponent.getJpaTransactionManager();
}
TransactionManagerFactory.setJpaTmOnBeamWorker(transactionManagerLazy::get);
// Masquarade all threads as App Engine threads so we can create Ofy keys in the pipeline. Also
// Masquerade all threads as App Engine threads so we can create Ofy keys in the pipeline. Also
// loads all ofy entities.
new AppEngineEnvironment("Beam").setEnvironmentForAllThreads();
// Set the system property so that we can call IdService.allocateId() without access to
@@ -262,8 +262,8 @@ public final class Transforms {
// Production data repair configs go below. See b/185954992.
// Prober domains in bad state, without associated contacts, hosts, billings, and history.
// They can be safely ignored.
// Prober domains in bad state, without associated contacts, hosts, billings, and non-synthesized
// history. They can be safely ignored.
private static final ImmutableSet<String> IGNORED_DOMAINS =
ImmutableSet.of("6AF6D2-IQCANT", "2-IQANYT");
@@ -299,7 +299,7 @@ public final class Transforms {
return !IGNORED_HOSTS.contains(roid);
}
if (entity.getKind().equals("HistoryEntry")) {
// Remove production bad data: History of the contacts to be ignored:
// Remove production bad data: Histories of ignored EPP resources:
com.google.appengine.api.datastore.Key parentKey = entity.getKey().getParent();
if (parentKey.getKind().equals("ContactResource")) {
String contactRoid = parentKey.getName();
@@ -309,6 +309,10 @@ public final class Transforms {
String hostRoid = parentKey.getName();
return !IGNORED_HOSTS.contains(hostRoid);
}
if (parentKey.getKind().equals("DomainBase")) {
String domainRoid = parentKey.getName();
return !IGNORED_DOMAINS.contains(domainRoid);
}
}
// End of production-specific checks.
@@ -371,6 +371,10 @@ public final class DomainDeleteFlow implements TransactionalFlow {
String.format(
"Domain %s was deleted by registry administrator with final deletion effective: %s",
existingDomain.getDomainName(), deletionTime))
.setResponseData(
ImmutableList.of(
DomainPendingActionNotificationResponse.create(
existingDomain.getDomainName(), true, trid, now)))
.build();
}
@@ -43,10 +43,22 @@ public class AppEngineEnvironment {
private Environment environment;
/**
* Constructor for use by tests.
*
* <p>All test suites must use the same appId for environments, since when tearing down we do not
* clear cached environments in spawned threads. See {@link #unsetEnvironmentForAllThreads} for
* more information.
*/
public AppEngineEnvironment() {
this("PlaceholderAppId");
/**
* Use AppEngineExtension's appId here so that ofy and sql entities can be compared with {@code
* Objects#equals()}. The choice of this value does not impact functional correctness.
*/
this("test");
}
/** Constructor for use by applications, e.g., BEAM pipelines. */
public AppEngineEnvironment(String appId) {
environment = createAppEngineEnvironment(appId);
}
@@ -65,7 +77,17 @@ public class AppEngineEnvironment {
ApiProxy.clearEnvironmentForCurrentThread();
}
/**
* Unsets the test environment in all threads with best effort.
*
* <p>This method unsets the environment factory and clears the cached environment in the current
* thread (the main test runner thread). We do not clear the cache in spawned threads, even though
* they may be reused. This is not a problem as long as the appId stays the same: those threads
* are used only in AppEngine or BEAM tests, and expect the presence of an environment.
*/
public void unsetEnvironmentForAllThreads() {
unsetEnvironmentForCurrentThread();
try {
Method method = ApiProxy.class.getDeclaredMethod("clearEnvironmentFactory");
method.setAccessible(true);
@@ -14,6 +14,7 @@
package google.registry.model;
import google.registry.util.PreconditionsUtils;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.AttributeOverride;
@@ -30,7 +31,7 @@ import javax.xml.bind.annotation.XmlTransient;
* that we can enforce strictly increasing timestamps.
*/
@MappedSuperclass
public abstract class BackupGroupRoot extends ImmutableObject {
public abstract class BackupGroupRoot extends ImmutableObject implements UnsafeSerializable {
/**
* An automatically managed timestamp of when this object was last written to Datastore.
@@ -49,4 +50,14 @@ public abstract class BackupGroupRoot extends ImmutableObject {
public UpdateAutoTimestamp getUpdateTimestamp() {
return updateTimestamp;
}
/**
* Copies {@link #updateTimestamp} from another entity.
*
* <p>This method is for the few cases when {@code updateTimestamp} is copied between different
* types of entities. Use {@link #clone} for same-type copying.
*/
protected void copyUpdateTimestamp(BackupGroupRoot other) {
this.updateTimestamp = PreconditionsUtils.checkArgumentNotNull(other, "other").updateTimestamp;
}
}
@@ -23,7 +23,7 @@ import org.joda.time.DateTime;
*
* @see CreateAutoTimestampTranslatorFactory
*/
public class CreateAutoTimestamp extends ImmutableObject {
public class CreateAutoTimestamp extends ImmutableObject implements UnsafeSerializable {
DateTime timestamp;
@@ -85,6 +85,9 @@ public abstract class ImmutableObject implements Cloneable {
@Target(FIELD)
public @interface Insignificant {}
// Note: if this class is made to implement Serializable, this field must become 'transient' since
// hashing is not stable across executions. Also note that @XmlTransient is forbidden on transient
// fields and need to be removed if transient is added.
@Ignore @XmlTransient protected Integer hashCode;
private boolean equalsImmutableObject(ImmutableObject other) {
@@ -0,0 +1,34 @@
// Copyright 2021 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.model;
import java.io.Serializable;
/**
* Marker interface for Nomulus entities whose serialization are implemented in a fragile way. These
* entities are made {@link Serializable} so that they can be passed between JVMs. The intended use
* case is BEAM pipeline-based cross-database data validation between Datastore and Cloud SQL during
* the migration. Note that only objects loaded from the SQL database need serialization support.
* Objects exported from Datastore can already be serialized as protocol buffers.
*
* <p>All entities implementing this interface take advantage of the fact that all Java collection
* classes we use, either directly or indirectly, including those in Java libraries, Guava,
* Objectify, and Hibernate are {@code Serializable}.
*
* <p>The {@code serialVersionUID} field has also been omitted in the implementing classes, since
* they are not used for persistence.
*/
// TODO(b/203609782): either remove this interface or fix implementors post migration.
public interface UnsafeSerializable extends Serializable {}
@@ -38,7 +38,7 @@ import org.joda.time.DateTime;
* @see UpdateAutoTimestampTranslatorFactory
*/
@Embeddable
public class UpdateAutoTimestamp extends ImmutableObject {
public class UpdateAutoTimestamp extends ImmutableObject implements UnsafeSerializable {
// When set to true, database converters/translators should do the auto update. When set to
// false, auto update should be suspended (this exists to allow us to preserve the original value
@@ -39,6 +39,7 @@ import com.googlecode.objectify.annotation.Parent;
import com.googlecode.objectify.condition.IfNull;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.common.TimeOfYear;
import google.registry.model.domain.DomainBase;
@@ -72,7 +73,7 @@ import org.joda.time.DateTime;
/** A billable event in a domain's lifecycle. */
@MappedSuperclass
public abstract class BillingEvent extends ImmutableObject
implements Buildable, TransferServerApproveEntity {
implements Buildable, TransferServerApproveEntity, UnsafeSerializable {
/** The reason for the bill, which maps 1:1 to skus in go/registry-billing-skus. */
public enum Reason {
@@ -0,0 +1,109 @@
// Copyright 2021 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.model.bulkquery;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainContent;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.GracePeriod.GracePeriodHistory;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.domain.secdns.DomainDsDataHistory;
import google.registry.model.host.HostResource;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.JpaTransactionManager;
/**
* Utilities for managing an alternative JPA entity model optimized for bulk loading multi-level
* entities such as {@link DomainBase} and {@link DomainHistory}.
*
* <p>In a bulk query for a multi-level JPA entity type, the JPA framework only generates a bulk
* query (SELECT * FROM table) for the base table. Then, for each row in the base table, additional
* queries are issued to load associated rows in child tables. This can be very slow when an entity
* type has multiple child tables.
*
* <p>We have defined an alternative entity model for {@code DomainBase} and {@code DomainHistory},
* where the base table as well as the child tables are mapped to single-level entity types. The
* idea is to load each of these types using a bulk query, and assemble them into the target type in
* memory in a pipeline. The main use case is Datastore-Cloud SQL validation during the Registry
* database migration, where we will need the full database snapshots frequently.
*/
public class BulkQueryEntities {
/**
* The JPA entity classes in persistence.xml to replace when creating the {@link
* JpaTransactionManager} for bulk query.
*/
public static final ImmutableMap<String, String> JPA_ENTITIES_REPLACEMENTS =
ImmutableMap.of(
DomainBase.class.getCanonicalName(),
DomainBaseLite.class.getCanonicalName(),
DomainHistory.class.getCanonicalName(),
DomainHistoryLite.class.getCanonicalName());
/* The JPA entity classes that are not included in persistence.xml and need to be added to
* the {@link JpaTransactionManager} for bulk query.*/
public static final ImmutableList<String> JPA_ENTITIES_NEW =
ImmutableList.of(
DomainHost.class.getCanonicalName(), DomainHistoryHost.class.getCanonicalName());
public static DomainBase assembleDomainBase(
DomainBaseLite domainBaseLite,
ImmutableSet<GracePeriod> gracePeriods,
ImmutableSet<DelegationSignerData> delegationSignerData,
ImmutableSet<VKey<HostResource>> nsHosts) {
DomainBase.Builder builder = new DomainBase.Builder();
builder.copyFrom(domainBaseLite);
builder.setGracePeriods(gracePeriods);
builder.setDsData(delegationSignerData);
builder.setNameservers(nsHosts);
return builder.build();
}
public static DomainHistory assembleDomainHistory(
DomainHistoryLite domainHistoryLite,
ImmutableSet<DomainDsDataHistory> dsDataHistories,
ImmutableSet<VKey<HostResource>> domainHistoryHosts,
ImmutableSet<GracePeriodHistory> gracePeriodHistories,
ImmutableSet<DomainTransactionRecord> transactionRecords) {
DomainHistory.Builder builder = new DomainHistory.Builder();
builder.copyFrom(domainHistoryLite);
DomainContent rawDomainContent = domainHistoryLite.domainContent;
if (rawDomainContent != null) {
DomainContent newDomainContent =
domainHistoryLite
.domainContent
.asBuilder()
.setNameservers(domainHistoryHosts)
.setGracePeriods(
gracePeriodHistories.stream()
.map(GracePeriod::createFromHistory)
.collect(toImmutableSet()))
.setDsData(
dsDataHistories.stream()
.map(DelegationSignerData::create)
.collect(toImmutableSet()))
.build();
builder.setDomain(newDomainContent);
}
return builder.buildAndAssemble(
dsDataHistories, domainHistoryHosts, gracePeriodHistories, transactionRecords);
}
}
@@ -0,0 +1,49 @@
// Copyright 2021 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.model.bulkquery;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainContent;
import google.registry.model.replay.SqlOnlyEntity;
import google.registry.persistence.VKey;
import google.registry.persistence.WithStringVKey;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Entity;
/**
* A 'light' version of {@link DomainBase} with only base table ("Domain") attributes, which allows
* fast bulk loading. They are used in in-memory assembly of {@code DomainBase} instances along with
* bulk-loaded child entities ({@code GracePeriod} etc). The in-memory assembly achieves much higher
* performance than loading {@code DomainBase} directly.
*
* <p>Please refer to {@link BulkQueryEntities} for more information.
*/
@Entity(name = "Domain")
@WithStringVKey
@Access(AccessType.FIELD)
public class DomainBaseLite extends DomainContent implements SqlOnlyEntity {
@Override
@javax.persistence.Id
@Access(AccessType.PROPERTY)
public String getRepoId() {
return super.getRepoId();
}
public static VKey<DomainBaseLite> createVKey(String repoId) {
return VKey.createSql(DomainBaseLite.class, repoId);
}
}
@@ -0,0 +1,50 @@
// Copyright 2021 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.model.bulkquery;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.host.HostResource;
import google.registry.model.replay.SqlOnlyEntity;
import google.registry.persistence.VKey;
import java.io.Serializable;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
/**
* A name server host referenced by a {@link google.registry.model.domain.DomainHistory} record.
* Please refer to {@link BulkQueryEntities} for usage.
*/
@Entity
@Access(AccessType.FIELD)
@IdClass(DomainHistoryHost.class)
public class DomainHistoryHost implements Serializable, SqlOnlyEntity {
@Id private Long domainHistoryHistoryRevisionId;
@Id private String domainHistoryDomainRepoId;
@Id private String hostRepoId;
private DomainHistoryHost() {}
public DomainHistoryId getDomainHistoryId() {
return new DomainHistoryId(domainHistoryDomainRepoId, domainHistoryHistoryRevisionId);
}
public VKey<HostResource> getHostVKey() {
return VKey.create(HostResource.class, hostRepoId);
}
}
@@ -0,0 +1,122 @@
// Copyright 2021 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.model.bulkquery;
import com.googlecode.objectify.Key;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainContent;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.Period;
import google.registry.model.replay.SqlOnlyEntity;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import javax.annotation.Nullable;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.PostLoad;
/**
* A 'light' version of {@link DomainHistory} with only base table ("DomainHistory") attributes,
* which allows fast bulk loading. They are used in in-memory assembly of {@code DomainHistory}
* instances along with bulk-loaded child entities ({@code GracePeriodHistory} etc). The in-memory
* assembly achieves much higher performance than loading {@code DomainHistory} directly.
*
* <p>Please refer to {@link BulkQueryEntities} for more information.
*
* <p>This class is adapted from {@link DomainHistory} by removing the {@code dsDataHistories},
* {@code gracePeriodHistories}, and {@code nsHosts} fields and associated methods.
*/
@Entity(name = "DomainHistory")
@Access(AccessType.FIELD)
@IdClass(DomainHistoryId.class)
public class DomainHistoryLite extends HistoryEntry implements SqlOnlyEntity {
// Store DomainContent instead of DomainBase so we don't pick up its @Id
// Nullable for the sake of pre-Registry-3.0 history objects
@Nullable DomainContent domainContent;
@Id
@Access(AccessType.PROPERTY)
public String getDomainRepoId() {
// We need to handle null case here because Hibernate sometimes accesses this method before
// parent gets initialized
return parent == null ? null : parent.getName();
}
/** This method is private because it is only used by Hibernate. */
@SuppressWarnings("unused")
private void setDomainRepoId(String domainRepoId) {
parent = Key.create(DomainBase.class, domainRepoId);
}
@Override
@Nullable
@Access(AccessType.PROPERTY)
@AttributeOverrides({
@AttributeOverride(name = "unit", column = @Column(name = "historyPeriodUnit")),
@AttributeOverride(name = "value", column = @Column(name = "historyPeriodValue"))
})
public Period getPeriod() {
return super.getPeriod();
}
/**
* For transfers, the id of the other registrar.
*
* <p>For requests and cancels, the other registrar is the losing party (because the registrar
* sending the EPP transfer command is the gaining party). For approves and rejects, the other
* registrar is the gaining party.
*/
@Nullable
@Access(AccessType.PROPERTY)
@Column(name = "historyOtherRegistrarId")
@Override
public String getOtherRegistrarId() {
return super.getOtherRegistrarId();
}
@Id
@Column(name = "historyRevisionId")
@Access(AccessType.PROPERTY)
@Override
public long getId() {
return super.getId();
}
/** The key to the {@link DomainBase} this is based off of. */
public VKey<DomainBase> getParentVKey() {
return VKey.create(DomainBase.class, getDomainRepoId());
}
@PostLoad
void postLoad() {
if (domainContent == null) {
return;
}
// See inline comments in DomainHistory.postLoad for reasons for the following lines.
if (domainContent.getDomainName() == null) {
domainContent = null;
} else if (domainContent.getRepoId() == null) {
domainContent.setRepoId(parent.getName());
}
}
}
@@ -0,0 +1,46 @@
// Copyright 2021 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.model.bulkquery;
import google.registry.model.host.HostResource;
import google.registry.model.replay.SqlOnlyEntity;
import google.registry.persistence.VKey;
import java.io.Serializable;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
/** A name server host of a domain. Please refer to {@link BulkQueryEntities} for usage. */
@Entity
@Access(AccessType.FIELD)
@IdClass(DomainHost.class)
public class DomainHost implements Serializable, SqlOnlyEntity {
@Id private String domainRepoId;
@Id private String hostRepoId;
DomainHost() {}
public String getDomainRepoId() {
return domainRepoId;
}
public VKey<HostResource> getHostVKey() {
return VKey.create(HostResource.class, hostRepoId);
}
}
@@ -27,13 +27,13 @@ import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.OnLoad;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.common.Cursor.CursorId;
import google.registry.model.replay.DatastoreAndSqlEntity;
import google.registry.model.tld.Registry;
import google.registry.persistence.VKey;
import java.io.Serializable;
import java.util.List;
import java.util.Optional;
import javax.persistence.Column;
@@ -53,7 +53,7 @@ import org.joda.time.DateTime;
@javax.persistence.Entity
@IdClass(CursorId.class)
@InCrossTld
public class Cursor extends ImmutableObject implements DatastoreAndSqlEntity {
public class Cursor extends ImmutableObject implements DatastoreAndSqlEntity, UnsafeSerializable {
/** The scope of a global cursor. A global cursor is a cursor that is not specific to one tld. */
public static final String GLOBAL = "GLOBAL";
@@ -283,7 +283,7 @@ public class Cursor extends ImmutableObject implements DatastoreAndSqlEntity {
return cursorTime;
}
static class CursorId extends ImmutableObject implements Serializable {
public static class CursorId extends ImmutableObject implements UnsafeSerializable {
public CursorType type;
public String scope;
@@ -28,6 +28,7 @@ import com.google.common.collect.Range;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Index;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import java.util.List;
import javax.persistence.Embeddable;
import org.joda.time.DateTime;
@@ -45,7 +46,7 @@ import org.joda.time.DateTime;
*/
@Embed
@Embeddable
public class TimeOfYear extends ImmutableObject {
public class TimeOfYear extends ImmutableObject implements UnsafeSerializable {
/**
* The time as "month day millis" with all fields left-padded with zeroes so that lexographic
@@ -27,7 +27,9 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.googlecode.objectify.mapper.Mapper;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.util.TypeUtils;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
@@ -53,11 +55,12 @@ import org.joda.time.DateTime;
* to use for storing the list of transitions. The user is given this choice of subclass so that the
* field of the value type stored in the transition can be given a customized name.
*/
public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedTransition<V>>
extends ForwardingMap<DateTime, T> {
public class TimedTransitionProperty<
V extends Serializable, T extends TimedTransitionProperty.TimedTransition<V>>
extends ForwardingMap<DateTime, T> implements UnsafeSerializable {
/**
* A transition to a value of type {@code V} at a certain time. This superclass only has a field
* A transition to a value of type {@code V} at a certain time. This superclass only has a field
* for the {@code DateTime}, which means that subclasses should supply the field of type {@code V}
* and implementations of the abstract getter and setter methods to access that field. This design
* is so that subclasses tagged with @Embed can define a custom field name for their value, for
@@ -65,11 +68,12 @@ public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedT
*
* <p>The public visibility of this class exists only so that it can be subclassed; clients should
* never call any methods on this class or attempt to access its members, but should instead treat
* it as a customizable implementation detail of {@code TimedTransitionProperty}. However, note
* it as a customizable implementation detail of {@code TimedTransitionProperty}. However, note
* that subclasses must also have public visibility so that they can be instantiated via
* reflection in a call to {@code fromValueMap}.
*/
public abstract static class TimedTransition<V> extends ImmutableObject {
public abstract static class TimedTransition<V extends Serializable> extends ImmutableObject
implements UnsafeSerializable {
/** The time at which this value becomes the active value. */
private DateTime transitionTime;
@@ -89,16 +93,16 @@ public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedT
}
/**
* Converts the provided value map into the equivalent transition map, using transition objects
* of the given TimedTransition subclass. The value map must be sorted according to the natural
* Converts the provided value map into the equivalent transition map, using transition objects of
* the given TimedTransition subclass. The value map must be sorted according to the natural
* ordering of its DateTime keys, and keys cannot be earlier than START_OF_TIME.
*/
// NB: The Class<T> parameter could be eliminated by getting the class via reflection, but then
// the callsite cannot infer T, so unless you explicitly call this as .<V, T>fromValueMap() it
// will default to using just TimedTransition<V>, which fails at runtime.
private static <V, T extends TimedTransition<V>> NavigableMap<DateTime, T> makeTransitionMap(
ImmutableSortedMap<DateTime, V> valueMap,
final Class<T> timedTransitionSubclass) {
private static <V extends Serializable, T extends TimedTransition<V>>
NavigableMap<DateTime, T> makeTransitionMap(
ImmutableSortedMap<DateTime, V> valueMap, final Class<T> timedTransitionSubclass) {
checkArgument(
Ordering.natural().equals(valueMap.comparator()),
"Timed transition value map must have transition time keys in chronological order");
@@ -121,9 +125,9 @@ public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedT
*
* <p>This method should be the normal method for constructing a {@link TimedTransitionProperty}.
*/
public static <V, T extends TimedTransition<V>> TimedTransitionProperty<V, T> fromValueMap(
ImmutableSortedMap<DateTime, V> valueMap,
final Class<T> timedTransitionSubclass) {
public static <V extends Serializable, T extends TimedTransition<V>>
TimedTransitionProperty<V, T> fromValueMap(
ImmutableSortedMap<DateTime, V> valueMap, final Class<T> timedTransitionSubclass) {
return new TimedTransitionProperty<>(ImmutableSortedMap.copyOf(
makeTransitionMap(valueMap, timedTransitionSubclass)));
}
@@ -175,10 +179,10 @@ public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedT
* @param allowedTransitions optional map of all possible state-to-state transitions
* @param allowedTransitionMapName optional transition map description string for error messages
* @param initialValue optional initial value; if present, the first transition must have this
* value
* value
* @param badInitialValueErrorMessage option error message string if the initial value is wrong
*/
public static <V, T extends TimedTransitionProperty.TimedTransition<V>>
public static <V extends Serializable, T extends TimedTransitionProperty.TimedTransition<V>>
TimedTransitionProperty<V, T> make(
ImmutableSortedMap<DateTime, V> newTransitions,
Class<T> transitionClass,
@@ -200,7 +204,7 @@ public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedT
* Validates that a transition map is not null or empty, starts at START_OF_TIME, and has
* transitions which move from one value to another in allowed ways.
*/
public static <V, T extends TimedTransitionProperty.TimedTransition<V>>
public static <V extends Serializable, T extends TimedTransitionProperty.TimedTransition<V>>
void validateTimedTransitionMap(
@Nullable NavigableMap<DateTime, V> transitionMap,
ImmutableMultimap<V, V> allowedTransitions,
@@ -240,8 +244,9 @@ public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedT
* annotation. The map for those fields must be mutable so that Objectify can load values from
* Datastore into the map, but clients should still never mutate the field's map directly.
*/
public static <V, T extends TimedTransition<V>> TimedTransitionProperty<V, T> forMapify(
ImmutableSortedMap<DateTime, V> valueMap, Class<T> timedTransitionSubclass) {
public static <V extends Serializable, T extends TimedTransition<V>>
TimedTransitionProperty<V, T> forMapify(
ImmutableSortedMap<DateTime, V> valueMap, Class<T> timedTransitionSubclass) {
return new TimedTransitionProperty<>(
new TreeMap<>(makeTransitionMap(valueMap, timedTransitionSubclass)));
}
@@ -254,8 +259,9 @@ public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedT
* annotation. The map for those fields must be mutable so that Objectify can load values from
* Datastore into the map, but clients should still never mutate the field's map directly.
*/
public static <V, T extends TimedTransition<V>> TimedTransitionProperty<V, T> forMapify(
V valueAtStartOfTime, Class<T> timedTransitionSubclass) {
public static <V extends Serializable, T extends TimedTransition<V>>
TimedTransitionProperty<V, T> forMapify(
V valueAtStartOfTime, Class<T> timedTransitionSubclass) {
return forMapify(
ImmutableSortedMap.of(START_OF_TIME, valueAtStartOfTime), timedTransitionSubclass);
}
@@ -20,6 +20,7 @@ import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.EntitySubclass;
import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.contact.ContactHistory.ContactHistoryId;
import google.registry.model.replay.DatastoreEntity;
import google.registry.model.replay.SqlEntity;
@@ -59,7 +60,7 @@ import javax.persistence.PostLoad;
@EntitySubclass
@Access(AccessType.FIELD)
@IdClass(ContactHistoryId.class)
public class ContactHistory extends HistoryEntry implements SqlEntity {
public class ContactHistory extends HistoryEntry implements SqlEntity, UnsafeSerializable {
// Store ContactBase instead of ContactResource so we don't pick up its @Id
// Nullable for the sake of pre-Registry-3.0 history objects
@@ -20,7 +20,9 @@ import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.eppcommon.PresenceMarker;
import java.io.Serializable;
import java.util.List;
import javax.persistence.Embeddable;
import javax.persistence.Embedded;
@@ -31,7 +33,7 @@ import javax.xml.bind.annotation.XmlType;
@Embed
@Embeddable
@XmlType(propOrder = {"name", "org", "addr", "voice", "fax", "email"})
public class Disclose extends ImmutableObject {
public class Disclose extends ImmutableObject implements UnsafeSerializable {
List<PostalInfoChoice> name;
@@ -78,7 +80,7 @@ public class Disclose extends ImmutableObject {
/** The "intLocType" from <a href="http://tools.ietf.org/html/rfc5733">RFC5733</a>. */
@Embed
public static class PostalInfoChoice extends ImmutableObject {
public static class PostalInfoChoice extends ImmutableObject implements Serializable {
@XmlAttribute
PostalInfo.Type type;
@@ -20,6 +20,7 @@ import com.googlecode.objectify.annotation.Embed;
import google.registry.model.Buildable;
import google.registry.model.Buildable.Overlayable;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import java.util.Optional;
import javax.persistence.Embeddable;
import javax.persistence.EnumType;
@@ -38,7 +39,8 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@Embed
@Embeddable
@XmlType(propOrder = {"name", "org", "address", "type"})
public class PostalInfo extends ImmutableObject implements Overlayable<PostalInfo> {
public class PostalInfo extends ImmutableObject
implements Overlayable<PostalInfo>, UnsafeSerializable {
/** The type of the address, either localized or international. */
public enum Type {
@@ -21,6 +21,7 @@ import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.Index;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.contact.ContactResource;
import google.registry.persistence.VKey;
import javax.persistence.Embeddable;
@@ -46,7 +47,7 @@ import javax.xml.bind.annotation.XmlEnumValue;
*/
@Embed
@Embeddable
public class DesignatedContact extends ImmutableObject {
public class DesignatedContact extends ImmutableObject implements UnsafeSerializable {
/**
* XML type for contact types. This can be either: {@code "admin"}, {@code "billing"}, or
@@ -24,6 +24,7 @@ import google.registry.model.host.HostResource;
import google.registry.model.replay.DatastoreAndSqlEntity;
import google.registry.persistence.VKey;
import google.registry.persistence.WithStringVKey;
import google.registry.util.DomainNameUtils;
import java.util.Set;
import javax.persistence.Access;
import javax.persistence.AccessType;
@@ -164,6 +165,11 @@ public class DomainBase extends DomainContent
return cloneDomainProjectedAtTime(this, now);
}
@Override
public void beforeSqlSaveOnReplay() {
fullyQualifiedDomainName = DomainNameUtils.canonicalizeDomainName(fullyQualifiedDomainName);
}
@Override
public void beforeDatastoreSaveOnReplay() {
saveIndexesToDatastore();
@@ -189,6 +195,7 @@ public class DomainBase extends DomainContent
}
public Builder copyFrom(DomainContent domainContent) {
this.getInstance().copyUpdateTimestamp(domainContent);
return this.setAuthInfo(domainContent.getAuthInfo())
.setAutorenewPollMessage(domainContent.getAutorenewPollMessage())
.setAutorenewBillingEvent(domainContent.getAutorenewBillingEvent())
@@ -33,6 +33,7 @@ import google.registry.model.replay.SqlEntity;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import google.registry.util.DomainNameUtils;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Optional;
@@ -216,6 +217,10 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
return super.getId();
}
public DomainHistoryId getDomainHistoryId() {
return new DomainHistoryId(getDomainRepoId(), getId());
}
/** Returns keys to the {@link HostResource} that are the nameservers for the domain. */
public Set<VKey<HostResource>> getNsHosts() {
return nsHosts;
@@ -299,6 +304,8 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
public void beforeSqlSaveOnReplay() {
if (domainContent == null) {
domainContent = jpaTm().getEntityManager().find(DomainBase.class, getDomainRepoId());
domainContent.fullyQualifiedDomainName =
DomainNameUtils.canonicalizeDomainName(domainContent.fullyQualifiedDomainName);
fillAuxiliaryFieldsFromDomain(this);
}
}
@@ -314,6 +321,8 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
nullToEmptyImmutableCopy(domainHistory.domainContent.getGracePeriods()).stream()
.map(gracePeriod -> GracePeriodHistory.createFrom(domainHistory.id, gracePeriod))
.collect(toImmutableSet());
} else {
domainHistory.nsHosts = ImmutableSet.of();
}
}
@@ -393,8 +402,16 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
if (domainContent == null) {
return this;
}
// TODO(b/203609982): if actual type of domainContent is DomainBase, convert to DomainContent
// Note: a DomainHistory fetched by JPA has DomainContent in this field. Allowing DomainBase
// in the setter makes equality checks messy.
getInstance().domainContent = domainContent;
return super.setParent(domainContent);
if (domainContent instanceof DomainBase) {
super.setParent(domainContent);
} else {
super.setParent(Key.create(DomainBase.class, domainContent.getRepoId()));
}
return this;
}
public Builder setDomainRepoId(String domainRepoId) {
@@ -412,5 +429,19 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
fillAuxiliaryFieldsFromDomain(instance);
return instance;
}
public DomainHistory buildAndAssemble(
ImmutableSet<DomainDsDataHistory> dsDataHistories,
ImmutableSet<VKey<HostResource>> domainHistoryHosts,
ImmutableSet<GracePeriodHistory> gracePeriodHistories,
ImmutableSet<DomainTransactionRecord> transactionRecords) {
DomainHistory instance = super.build();
instance.dsDataHistories = dsDataHistories;
instance.nsHosts = domainHistoryHosts;
instance.gracePeriodHistories = gracePeriodHistories;
instance.domainTransactionRecords = transactionRecords;
instance.hashCode = null;
return instance;
}
}
}
@@ -22,6 +22,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.replay.DatastoreAndSqlEntity;
import google.registry.model.replay.SqlOnlyEntity;
@@ -203,7 +204,7 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
/** Entity class to represent a historic {@link GracePeriod}. */
@Entity(name = "GracePeriodHistory")
@Table(indexes = @Index(columnList = "domainRepoId"))
static class GracePeriodHistory extends GracePeriodBase implements SqlOnlyEntity {
public static class GracePeriodHistory extends GracePeriodBase implements SqlOnlyEntity {
@Id Long gracePeriodHistoryRevisionId;
/** ID for the associated {@link DomainHistory} entity. */
@@ -215,6 +216,10 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
return super.getGracePeriodId();
}
public DomainHistoryId getDomainHistoryId() {
return new DomainHistoryId(getDomainRepoId(), domainHistoryRevisionId);
}
static GracePeriodHistory createFrom(long historyRevisionId, GracePeriod gracePeriod) {
GracePeriodHistory instance = new GracePeriodHistory();
instance.gracePeriodHistoryRevisionId = allocateId();
@@ -17,6 +17,7 @@ package google.registry.model.domain;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.billing.BillingEvent;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.persistence.BillingVKey.BillingEventVKey;
@@ -35,7 +36,7 @@ import org.joda.time.DateTime;
@Embed
@MappedSuperclass
@Access(AccessType.FIELD)
public class GracePeriodBase extends ImmutableObject {
public class GracePeriodBase extends ImmutableObject implements UnsafeSerializable {
/** Unique id required for hibernate representation. */
@Transient long gracePeriodId;
@@ -16,6 +16,7 @@ package google.registry.model.domain;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.xml.bind.annotation.XmlAttribute;
@@ -25,7 +26,7 @@ import javax.xml.bind.annotation.XmlValue;
/** The "periodType" from <a href="http://tools.ietf.org/html/rfc5731">RFC5731</a>. */
@Embed
@javax.persistence.Embeddable
public class Period extends ImmutableObject {
public class Period extends ImmutableObject implements UnsafeSerializable {
@Enumerated(EnumType.STRING)
@XmlAttribute
@@ -27,6 +27,7 @@ import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.IgnoreSave;
import com.googlecode.objectify.condition.IfNull;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import java.util.Optional;
import javax.persistence.Embedded;
import javax.xml.bind.annotation.XmlAttribute;
@@ -39,7 +40,7 @@ import org.joda.time.DateTime;
@Embed
@XmlType(propOrder = {"noticeId", "expirationTime", "acceptedTime"})
@javax.persistence.Embeddable
public class LaunchNotice extends ImmutableObject {
public class LaunchNotice extends ImmutableObject implements UnsafeSerializable {
/** An empty instance to use in place of null. */
private static final NoticeIdType EMPTY_NOTICE_ID = new NoticeIdType();
@@ -47,14 +48,13 @@ public class LaunchNotice extends ImmutableObject {
/** An id with a validator-id attribute. */
@Embed
@javax.persistence.Embeddable
public static class NoticeIdType extends ImmutableObject {
public static class NoticeIdType extends ImmutableObject implements UnsafeSerializable {
/**
* The Trademark Claims Notice ID from
* {@link "http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-6.3"}.
* The Trademark Claims Notice ID from <a
* href="http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-6.3">the RFC</a>.
*/
@XmlValue
String tcnId;
@XmlValue String tcnId;
/** The identifier of the TMDB provider to use, defaulting to the TMCH. */
@IgnoreSave(IfNull.class)
@@ -17,6 +17,7 @@ package google.registry.model.domain.secdns;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.MappedSuperclass;
@@ -31,7 +32,7 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@Embed
@MappedSuperclass
@Access(AccessType.FIELD)
public abstract class DomainDsDataBase extends ImmutableObject {
public abstract class DomainDsDataBase extends ImmutableObject implements UnsafeSerializable {
@Ignore @XmlTransient @Transient String domainRepoId;
@@ -16,7 +16,9 @@ package google.registry.model.domain.secdns;
import static google.registry.model.IdService.allocateId;
import google.registry.model.UnsafeSerializable;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.replay.SqlOnlyEntity;
import javax.persistence.Access;
import javax.persistence.AccessType;
@@ -26,7 +28,8 @@ import javax.persistence.Id;
/** Entity class to represent a historic {@link DelegationSignerData}. */
@Entity
public class DomainDsDataHistory extends DomainDsDataBase implements SqlOnlyEntity {
public class DomainDsDataHistory extends DomainDsDataBase
implements SqlOnlyEntity, UnsafeSerializable {
@Id Long dsDataHistoryRevisionId;
@@ -53,6 +56,10 @@ public class DomainDsDataHistory extends DomainDsDataBase implements SqlOnlyEnti
return instance;
}
public DomainHistory.DomainHistoryId getDomainHistoryId() {
return new DomainHistoryId(getDomainRepoId(), domainHistoryRevisionId);
}
@Override
@Access(AccessType.PROPERTY)
public String getDomainRepoId() {
@@ -27,6 +27,7 @@ import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.JsonMapBuilder;
import google.registry.model.Jsonifiable;
import google.registry.model.UnsafeSerializable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -56,7 +57,7 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlTransient
@Embeddable
@MappedSuperclass
public class Address extends ImmutableObject implements Jsonifiable {
public class Address extends ImmutableObject implements Jsonifiable, UnsafeSerializable {
/** The schema validation will enforce that this has 3 lines at most. */
// TODO(b/177569726): Remove this field after migration. We need to figure out how to generate
@@ -16,6 +16,7 @@ package google.registry.model.eppcommon;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import javax.persistence.Embeddable;
import javax.persistence.Embedded;
import javax.persistence.MappedSuperclass;
@@ -35,7 +36,7 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlTransient
@Embeddable
@MappedSuperclass
public abstract class AuthInfo extends ImmutableObject {
public abstract class AuthInfo extends ImmutableObject implements UnsafeSerializable {
@Embedded protected PasswordAuth pw;
@@ -47,7 +48,7 @@ public abstract class AuthInfo extends ImmutableObject {
@Embed
@XmlType(namespace = "urn:ietf:params:xml:ns:eppcom-1.0")
@Embeddable
public static class PasswordAuth extends ImmutableObject {
public static class PasswordAuth extends ImmutableObject implements UnsafeSerializable {
@XmlValue
@XmlJavaTypeAdapter(NormalizedStringAdapter.class)
String value;
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import javax.persistence.Embeddable;
import javax.persistence.MappedSuperclass;
import javax.xml.bind.annotation.XmlAttribute;
@@ -49,7 +50,7 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlTransient
@Embeddable
@MappedSuperclass
public class PhoneNumber extends ImmutableObject {
public class PhoneNumber extends ImmutableObject implements UnsafeSerializable {
@XmlValue
@XmlJavaTypeAdapter(CollapsedStringAdapter.class)
@@ -18,6 +18,7 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.xml.bind.annotation.XmlElement;
@@ -32,7 +33,7 @@ import javax.xml.bind.annotation.XmlType;
@Embed
@XmlType(propOrder = {"clientTransactionId", "serverTransactionId"})
@javax.persistence.Embeddable
public class Trid extends ImmutableObject {
public class Trid extends ImmutableObject implements UnsafeSerializable {
/** The server transaction id. */
@XmlElement(name = "svTRID", namespace = "urn:ietf:params:xml:ns:epp-1.0")
@@ -20,11 +20,13 @@ import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.EntitySubclass;
import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.host.HostHistory.HostHistoryId;
import google.registry.model.replay.DatastoreEntity;
import google.registry.model.replay.SqlEntity;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import google.registry.util.DomainNameUtils;
import java.io.Serializable;
import java.util.Optional;
import javax.annotation.Nullable;
@@ -60,7 +62,7 @@ import javax.persistence.PostLoad;
@EntitySubclass
@Access(AccessType.FIELD)
@IdClass(HostHistoryId.class)
public class HostHistory extends HistoryEntry implements SqlEntity {
public class HostHistory extends HistoryEntry implements SqlEntity, UnsafeSerializable {
// Store HostBase instead of HostResource so we don't pick up its @Id
// Nullable for the sake of pre-Registry-3.0 history objects
@@ -140,6 +142,8 @@ public class HostHistory extends HistoryEntry implements SqlEntity {
public void beforeSqlSaveOnReplay() {
if (hostBase == null) {
hostBase = jpaTm().getEntityManager().find(HostResource.class, getHostRepoId());
hostBase.fullyQualifiedHostName =
DomainNameUtils.canonicalizeDomainName(hostBase.fullyQualifiedHostName);
}
}
@@ -22,6 +22,7 @@ import google.registry.model.annotations.ReportedOn;
import google.registry.model.replay.DatastoreAndSqlEntity;
import google.registry.persistence.VKey;
import google.registry.persistence.WithStringVKey;
import google.registry.util.DomainNameUtils;
import javax.persistence.Access;
import javax.persistence.AccessType;
@@ -51,6 +52,11 @@ public class HostResource extends HostBase
return VKey.create(HostResource.class, getRepoId(), Key.create(this));
}
@Override
public void beforeSqlSaveOnReplay() {
fullyQualifiedHostName = DomainNameUtils.canonicalizeDomainName(fullyQualifiedHostName);
}
@Override
public void beforeDatastoreSaveOnReplay() {
saveIndexesToDatastore();
@@ -17,6 +17,7 @@ package google.registry.model.poll;
import com.google.common.annotations.VisibleForTesting;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.eppcommon.Trid;
import google.registry.model.eppoutput.EppResponse.ResponseData;
import javax.persistence.Embeddable;
@@ -31,12 +32,13 @@ import org.joda.time.DateTime;
/** The {@link ResponseData} returned when completing a pending action on a domain. */
@XmlTransient
@Embeddable
public class PendingActionNotificationResponse extends ImmutableObject implements ResponseData {
public class PendingActionNotificationResponse extends ImmutableObject
implements ResponseData, UnsafeSerializable {
/** The inner name type that contains a name and the result boolean. */
@Embed
@Embeddable
static class NameOrId extends ImmutableObject {
static class NameOrId extends ImmutableObject implements UnsafeSerializable {
@XmlValue
String value;
@@ -33,6 +33,7 @@ import com.googlecode.objectify.annotation.Parent;
import google.registry.model.Buildable;
import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.annotations.ExternalMessagingName;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.contact.ContactResource;
@@ -98,7 +99,7 @@ import org.joda.time.DateTime;
@javax.persistence.Index(columnList = "eventTime")
})
public abstract class PollMessage extends ImmutableObject
implements Buildable, DatastoreAndSqlEntity, TransferServerApproveEntity {
implements Buildable, DatastoreAndSqlEntity, TransferServerApproveEntity, UnsafeSerializable {
/** Entity id. */
@Id
@@ -72,6 +72,7 @@ import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
import google.registry.model.JsonMapBuilder;
import google.registry.model.Jsonifiable;
import google.registry.model.UnsafeSerializable;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.annotations.ReportedOn;
@@ -116,7 +117,7 @@ import org.joda.time.DateTime;
})
@InCrossTld
public class Registrar extends ImmutableObject
implements Buildable, DatastoreAndSqlEntity, Jsonifiable {
implements Buildable, DatastoreAndSqlEntity, Jsonifiable, UnsafeSerializable {
/** Represents the type of a registrar entity. */
public enum Type {
@@ -404,7 +405,7 @@ public class Registrar extends ImmutableObject
/** A billing account entry for this registrar, consisting of a currency and an account Id. */
@Embed
public static class BillingAccountEntry extends ImmutableObject {
public static class BillingAccountEntry extends ImmutableObject implements UnsafeSerializable {
CurrencyUnit currency;
String accountId;
@@ -46,6 +46,7 @@ import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.JsonMapBuilder;
import google.registry.model.Jsonifiable;
import google.registry.model.UnsafeSerializable;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.registrar.RegistrarContact.RegistrarPocId;
@@ -82,7 +83,7 @@ import javax.persistence.Transient;
@IdClass(RegistrarPocId.class)
@InCrossTld
public class RegistrarContact extends ImmutableObject
implements DatastoreAndSqlEntity, Jsonifiable {
implements DatastoreAndSqlEntity, Jsonifiable, UnsafeSerializable {
@Parent @Transient Key<Registrar> parent;
@@ -169,7 +169,7 @@ public class ReplicateToDatastoreAction implements Runnable {
return;
}
Optional<Lock> lock =
Lock.acquire(
Lock.acquireSql(
this.getClass().getSimpleName(), null, LEASE_LENGTH, requestStatusChecker, false);
if (!lock.isPresent()) {
String message = "Can't acquire ReplicateToDatastoreAction lock, aborting.";
@@ -22,6 +22,8 @@ import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.replay.DatastoreAndSqlEntity;
import javax.persistence.Column;
import javax.persistence.Entity;
@@ -45,11 +47,10 @@ import org.joda.time.DateTime;
@Embed
@Entity
public class DomainTransactionRecord extends ImmutableObject
implements Buildable, DatastoreAndSqlEntity {
implements Buildable, DatastoreAndSqlEntity, UnsafeSerializable {
@Id
@Ignore
@ImmutableObject.DoNotCompare
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ImmutableObject.Insignificant
Long id;
@@ -58,6 +59,14 @@ public class DomainTransactionRecord extends ImmutableObject
@Column(nullable = false)
String tld;
// The following two fields are exposed in this entity to support bulk-loading in Cloud SQL by the
// Datastore-SQL validation. They are excluded from equality check since they are not set in
// Datastore.
// TODO(b/203609782): post migration, decide whether to keep these two fields.
@Ignore @ImmutableObject.Insignificant String domainRepoId;
@Ignore @ImmutableObject.Insignificant Long historyRevisionId;
/**
* The time this Transaction takes effect (counting grace periods and other nuances).
*
@@ -174,6 +183,10 @@ public class DomainTransactionRecord extends ImmutableObject
}
}
public DomainHistoryId getDomainHistoryId() {
return new DomainHistoryId(domainRepoId, historyRevisionId);
}
public DateTime getReportingTime() {
return reportingTime;
}
@@ -31,6 +31,7 @@ import com.googlecode.objectify.condition.IfNull;
import google.registry.model.Buildable;
import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.contact.ContactBase;
import google.registry.model.contact.ContactHistory;
@@ -83,7 +84,8 @@ import org.joda.time.DateTime;
@Entity
@MappedSuperclass
@Access(AccessType.FIELD)
public class HistoryEntry extends ImmutableObject implements Buildable, DatastoreEntity {
public class HistoryEntry extends ImmutableObject
implements Buildable, DatastoreEntity, UnsafeSerializable {
/** Represents the type of history entry. */
public enum Type {
@@ -32,6 +32,8 @@ import google.registry.model.annotations.NotBackedUp;
import google.registry.model.annotations.NotBackedUp.Reason;
import google.registry.model.replay.DatastoreAndSqlEntity;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.TransactionManager;
import google.registry.util.RequestStatusChecker;
import google.registry.util.RequestStatusCheckerImpl;
import java.io.Serializable;
@@ -212,6 +214,47 @@ public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Seri
Duration leaseLength,
RequestStatusChecker requestStatusChecker,
boolean checkThreadRunning) {
return acquireWithTransactionManager(
resourceName, tld, leaseLength, requestStatusChecker, checkThreadRunning, tm());
}
/**
* Try to acquire a lock in SQL. Returns absent if it can't be acquired.
*
* <p>This method exists so that Beam pipelines can acquire / load / release locks.
*/
public static Optional<Lock> acquireSql(
String resourceName,
@Nullable String tld,
Duration leaseLength,
RequestStatusChecker requestStatusChecker,
boolean checkThreadRunning) {
return acquireWithTransactionManager(
resourceName, tld, leaseLength, requestStatusChecker, checkThreadRunning, jpaTm());
}
/** Release the lock. */
public void release() {
releaseWithTransactionManager(tm());
}
/**
* Release the lock from SQL.
*
* <p>This method exists so that Beam pipelines can acquire / load / release locks.
*/
public void releaseSql() {
releaseWithTransactionManager(jpaTm());
}
/** Try to acquire a lock. Returns absent if it can't be acquired. */
private static Optional<Lock> acquireWithTransactionManager(
String resourceName,
@Nullable String tld,
Duration leaseLength,
RequestStatusChecker requestStatusChecker,
boolean checkThreadRunning,
TransactionManager transactionManager) {
String scope = (tld != null) ? tld : GLOBAL;
String lockId = makeLockId(resourceName, scope);
// It's important to use transactNew rather than transact, because a Lock can be used to control
@@ -219,11 +262,12 @@ public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Seri
// must be definitively acquired before it is used, even when called inside another transaction.
Supplier<AcquireResult> lockAcquirer =
() -> {
DateTime now = tm().getTransactionTime();
DateTime now = transactionManager.getTransactionTime();
// Checking if an unexpired lock still exists - if so, the lock can't be acquired.
Lock lock =
tm().loadByKeyIfPresent(
transactionManager
.loadByKeyIfPresent(
VKey.create(
Lock.class,
new LockId(resourceName, scope),
@@ -249,13 +293,15 @@ public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Seri
create(resourceName, scope, requestStatusChecker.getLogId(), now, leaseLength);
// Locks are not parented under an EntityGroupRoot (so as to avoid write
// contention) and don't need to be backed up.
tm().putIgnoringReadOnly(newLock);
transactionManager.putIgnoringReadOnly(newLock);
return AcquireResult.create(now, lock, newLock, lockState);
};
// In ofy, backup is determined per-action, but in SQL it's determined per-transaction
AcquireResult acquireResult =
tm().isOfy() ? tm().transactNew(lockAcquirer) : jpaTm().transactWithoutBackup(lockAcquirer);
transactionManager.isOfy()
? transactionManager.transactNew(lockAcquirer)
: ((JpaTransactionManager) transactionManager).transactWithoutBackup(lockAcquirer);
logAcquireResult(acquireResult);
lockMetrics.recordAcquire(resourceName, scope, acquireResult.lockState());
@@ -263,7 +309,7 @@ public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Seri
}
/** Release the lock. */
public void release() {
private void releaseWithTransactionManager(TransactionManager transactionManager) {
// Just use the default clock because we aren't actually doing anything that will use the clock.
Supplier<Void> lockReleaser =
() -> {
@@ -274,15 +320,17 @@ public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Seri
VKey<Lock> key =
VKey.create(
Lock.class, new LockId(resourceName, tld), Key.create(Lock.class, lockId));
Lock loadedLock = tm().loadByKeyIfPresent(key).orElse(null);
Lock loadedLock = transactionManager.loadByKeyIfPresent(key).orElse(null);
if (Lock.this.equals(loadedLock)) {
// Use deleteIgnoringReadOnly() so that we don't create a commit log entry for deleting
// the lock.
logger.atInfo().log("Deleting lock: %s", lockId);
tm().deleteIgnoringReadOnly(key);
transactionManager.deleteIgnoringReadOnly(key);
lockMetrics.recordRelease(
resourceName, tld, new Duration(acquiredTime, tm().getTransactionTime()));
resourceName,
tld,
new Duration(acquiredTime, transactionManager.getTransactionTime()));
} else {
logger.atSevere().log(
"The lock we acquired was transferred to someone else before we"
@@ -294,11 +342,12 @@ public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Seri
}
return null;
};
// In ofy, backup is determined per-action, but in SQL it's determined per-transaction
if (tm().isOfy()) {
tm().transact(lockReleaser);
if (transactionManager.isOfy()) {
transactionManager.transact(lockReleaser);
} else {
jpaTm().transactWithoutBackup(lockReleaser);
((JpaTransactionManager) transactionManager).transactWithoutBackup(lockReleaser);
}
}
@@ -52,6 +52,7 @@ import com.googlecode.objectify.annotation.Parent;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.common.EntityGroupRoot;
@@ -88,7 +89,8 @@ import org.joda.time.Duration;
@Entity
@javax.persistence.Entity(name = "Tld")
@InCrossTld
public class Registry extends ImmutableObject implements Buildable, DatastoreAndSqlEntity {
public class Registry extends ImmutableObject
implements Buildable, DatastoreAndSqlEntity, UnsafeSerializable {
@Parent @Transient Key<EntityGroupRoot> parent = getCrossTldKey();
@@ -308,7 +310,7 @@ public class Registry extends ImmutableObject implements Buildable, DatastoreAnd
* <p>There must be at least one entry in this set.
*
* <p>All entries of this list must be valid keys for the map of {@code DnsWriter}s injected by
* <code>@Inject Map<String, DnsWriter></code>
* {@code @Inject Map<String, DnsWriter>}
*/
@Column(nullable = false)
Set<String> dnsWriters;
@@ -16,6 +16,7 @@ package google.registry.model.transfer;
import google.registry.model.Buildable.GenericBuilder;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import javax.persistence.Column;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
@@ -27,7 +28,7 @@ import org.joda.time.DateTime;
/** Fields common to {@link TransferData} and {@link TransferResponse}. */
@XmlTransient
@MappedSuperclass
public abstract class BaseTransferObject extends ImmutableObject {
public abstract class BaseTransferObject extends ImmutableObject implements UnsafeSerializable {
/**
* The status of the current or last transfer. Can be null if never transferred. Note that we
* leave IgnoreSave off this field so that we can ensure that TransferData loaded from Objectify
@@ -0,0 +1,65 @@
// Copyright 2021 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.persistence;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import google.registry.model.bulkquery.BulkQueryEntities;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.JpaTransactionManagerImpl;
import google.registry.util.Clock;
import java.util.List;
import javax.persistence.EntityManagerFactory;
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
import org.hibernate.jpa.boot.spi.Bootstrap;
/**
* Defines factory method for instantiating the bulk-query optimized {@link JpaTransactionManager}.
*/
public final class BulkQueryJpaFactory {
private BulkQueryJpaFactory() {}
static EntityManagerFactory createBulkQueryEntityManagerFactory(
ImmutableMap<String, String> cloudSqlConfigs) {
ParsedPersistenceXmlDescriptor descriptor =
PersistenceXmlUtility.getParsedPersistenceXmlDescriptor();
List<String> updatedManagedClasses =
Streams.concat(
descriptor.getManagedClassNames().stream(),
BulkQueryEntities.JPA_ENTITIES_NEW.stream())
.map(
name -> {
if (BulkQueryEntities.JPA_ENTITIES_REPLACEMENTS.containsKey(name)) {
return BulkQueryEntities.JPA_ENTITIES_REPLACEMENTS.get(name);
}
return name;
})
.collect(ImmutableList.toImmutableList());
descriptor.getManagedClassNames().clear();
descriptor.getManagedClassNames().addAll(updatedManagedClasses);
return Bootstrap.getEntityManagerFactoryBuilder(descriptor, cloudSqlConfigs).build();
}
public static JpaTransactionManager createBulkQueryJpaTransactionManager(
ImmutableMap<String, String> cloudSqlConfigs, Clock clock) {
return new JpaTransactionManagerImpl(
createBulkQueryEntityManagerFactory(cloudSqlConfigs), clock);
}
}
@@ -152,13 +152,36 @@ public abstract class PersistenceModule {
@Singleton
@BeamPipelineCloudSqlConfigs
static ImmutableMap<String, String> provideBeamPipelineCloudSqlConfigs(
@Config("beamCloudSqlJdbcUrl") String jdbcUrl,
@Config("beamCloudSqlInstanceConnectionName") String instanceConnectionName,
@DefaultHibernateConfigs ImmutableMap<String, String> defaultConfigs,
SqlCredentialStore credentialStore,
@Config("instanceConnectionNameOverride")
Optional<Provider<String>> instanceConnectionNameOverride,
@Config("beamIsolationOverride")
Optional<Provider<TransactionIsolationLevel>> isolationOverride) {
return createPartialSqlConfigs(
jdbcUrl, instanceConnectionName, defaultConfigs, isolationOverride);
Optional<Provider<TransactionIsolationLevel>> isolationOverride,
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs) {
HashMap<String, String> overrides = Maps.newHashMap(cloudSqlConfigs);
// TODO(b/175700623): make sql username configurable from config file.
SqlCredential credential = credentialStore.getCredential(new RobotUser(RobotId.NOMULUS));
overrides.put(Environment.USER, credential.login());
overrides.put(Environment.PASS, credential.password());
// Override the default minimum which is tuned for the Registry server. A worker VM should
// release all connections if it no longer interacts with the database.
overrides.put(HIKARI_MINIMUM_IDLE, "0");
/**
* Disable Hikari's maxPoolSize limit check by setting it to an absurdly large number. The
* effective (and desirable) limit is the number of pipeline threads on the pipeline worker,
* which can be configured using pipeline options. See {@link RegistryPipelineOptions} for more
* information.
*/
overrides.put(HIKARI_MAXIMUM_POOL_SIZE, String.valueOf(Integer.MAX_VALUE));
instanceConnectionNameOverride
.map(Provider::get)
.ifPresent(
instanceConnectionName ->
overrides.put(HIKARI_DS_CLOUD_SQL_INSTANCE, instanceConnectionName));
isolationOverride
.map(Provider::get)
.ifPresent(isolation -> overrides.put(Environment.ISOLATION, isolation.name()));
return ImmutableMap.copyOf(overrides);
}
@VisibleForTesting
@@ -230,37 +253,17 @@ public abstract class PersistenceModule {
@Singleton
@BeamJpaTm
static JpaTransactionManager provideBeamJpaTm(
SqlCredentialStore credentialStore,
@Config("instanceConnectionNameOverride")
Optional<Provider<String>> instanceConnectionNameOverride,
@Config("beamIsolationOverride")
Optional<Provider<TransactionIsolationLevel>> isolationOverride,
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs,
Clock clock) {
HashMap<String, String> overrides = Maps.newHashMap(cloudSqlConfigs);
// TODO(b/175700623): make sql username configurable from config file.
SqlCredential credential = credentialStore.getCredential(new RobotUser(RobotId.NOMULUS));
overrides.put(Environment.USER, credential.login());
overrides.put(Environment.PASS, credential.password());
// Override the default minimum which is tuned for the Registry server. A worker VM should
// release all connections if it no longer interacts with the database.
overrides.put(HIKARI_MINIMUM_IDLE, "0");
/**
* Disable Hikari's maxPoolSize limit check by setting it to an absurdly large number. The
* effective (and desirable) limit is the number of pipeline threads on the pipeline worker,
* which can be configured using pipeline options. See {@link RegistryPipelineOptions} for more
* information.
*/
overrides.put(HIKARI_MAXIMUM_POOL_SIZE, String.valueOf(Integer.MAX_VALUE));
instanceConnectionNameOverride
.map(Provider::get)
.ifPresent(
instanceConnectionName ->
overrides.put(HIKARI_DS_CLOUD_SQL_INSTANCE, instanceConnectionName));
isolationOverride
.map(Provider::get)
.ifPresent(isolation -> overrides.put(Environment.ISOLATION, isolation.name()));
return new JpaTransactionManagerImpl(create(overrides), clock);
@BeamPipelineCloudSqlConfigs ImmutableMap<String, String> beamCloudSqlConfigs, Clock clock) {
return new JpaTransactionManagerImpl(create(beamCloudSqlConfigs), clock);
}
@Provides
@Singleton
@BeamBulkQueryJpaTm
static JpaTransactionManager provideBeamBulkQueryJpaTm(
@BeamPipelineCloudSqlConfigs ImmutableMap<String, String> beamCloudSqlConfigs, Clock clock) {
return new JpaTransactionManagerImpl(
BulkQueryJpaFactory.createBulkQueryEntityManagerFactory(beamCloudSqlConfigs), clock);
}
@Provides
@@ -346,6 +349,17 @@ public abstract class PersistenceModule {
}
}
/** Types of {@link JpaTransactionManager JpaTransactionManagers}. */
public enum JpaTransactionManagerType {
/** The regular {@link JpaTransactionManager} for general use. */
REGULAR,
/**
* The {@link JpaTransactionManager} optimized for bulk loading multi-level JPA entities. Please
* see {@link google.registry.model.bulkquery.BulkQueryEntities} for more information.
*/
BULK_QUERY
}
/** Dagger qualifier for JDBC {@link Connection} with schema management privilege. */
@Qualifier
@Documented
@@ -357,11 +371,18 @@ public abstract class PersistenceModule {
@interface AppEngineJpaTm {}
/** Dagger qualifier for {@link JpaTransactionManager} used inside BEAM pipelines. */
// Note: @SocketFactoryJpaTm will be phased out in favor of this qualifier.
@Qualifier
@Documented
public @interface BeamJpaTm {}
/**
* Dagger qualifier for {@link JpaTransactionManager} that uses an alternative entity model for
* faster bulk queries.
*/
@Qualifier
@Documented
public @interface BeamBulkQueryJpaTm {}
/** Dagger qualifier for {@link JpaTransactionManager} used for Nomulus tool. */
@Qualifier
@Documented
@@ -18,10 +18,13 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.googlecode.objectify.Key;
import google.registry.model.BackupGroupRoot;
import google.registry.model.ImmutableObject;
import google.registry.model.translators.VKeyTranslatorFactory;
import google.registry.util.SerializeUtils;
import java.io.Serializable;
import java.util.Optional;
import javax.annotation.Nullable;
@@ -36,6 +39,15 @@ public class VKey<T> extends ImmutableObject implements Serializable {
private static final long serialVersionUID = -5291472863840231240L;
// Info that's stored in in vkey string generated via stringify().
private static final String SQL_LOOKUP_KEY = "sql";
private static final String OFY_LOOKUP_KEY = "ofy";
private static final String CLASS_TYPE = "kind";
// Web safe delimiters that won't be used in base 64.
private static final String KV_SEPARATOR = ":";
private static final String DELIMITER = "@";
// The SQL key for the referenced entity.
Serializable sqlKey;
@@ -114,6 +126,47 @@ public class VKey<T> extends ImmutableObject implements Serializable {
return new VKey<T>(kind, Key.create(kind, name), name);
}
/**
* Constructs a {@link VKey} from the string representation of a vkey.
*
* <p>There are two types of string representations: 1) existing ofy key string handled by
* fromWebsafeKey() and 2) string encoded via stringify() where @ separates the substrings and
* each of the substrings contains a look up key, ":", and its corresponding value. The key info
* is encoded via Base64. The string begins with "kind:" and it must contains at least ofy key or
* sql key.
*
* <p>Example of a Vkey string by fromWebsafeKey(): "agR0ZXN0chYLEgpEb21haW5CYXNlIgZST0lELTEM"
*
* <p>Example of a vkey string by stringify(): "google.registry.testing.TestObject@sql:rO0ABX" +
* "QAA2Zvbw@ofy:agR0ZXN0cjELEg9FbnRpdHlHcm91cFJvb3QiCWNyb3NzLXRsZAwLEgpUZXN0T2JqZWN0IgNmb28M",
* where sql key and ofy key are values are encoded in Base64.
*/
public static <T> VKey<T> create(String keyString) throws Exception {
if (!keyString.startsWith(CLASS_TYPE + KV_SEPARATOR)) {
// to handle the existing ofy key string
return fromWebsafeKey(keyString);
} else {
ImmutableMap<String, String> kvs =
ImmutableMap.copyOf(
Splitter.on(DELIMITER).withKeyValueSeparator(KV_SEPARATOR).split(keyString));
Class classType = Class.forName(kvs.get(CLASS_TYPE));
if (kvs.containsKey(SQL_LOOKUP_KEY) && kvs.containsKey(OFY_LOOKUP_KEY)) {
return VKey.create(
classType,
SerializeUtils.parse(Serializable.class, kvs.get(SQL_LOOKUP_KEY)),
Key.create(kvs.get(OFY_LOOKUP_KEY)));
} else if (kvs.containsKey(SQL_LOOKUP_KEY)) {
return VKey.createSql(
classType, SerializeUtils.parse(Serializable.class, kvs.get(SQL_LOOKUP_KEY)));
} else if (kvs.containsKey(OFY_LOOKUP_KEY)) {
return VKey.createOfy(classType, Key.create(kvs.get(OFY_LOOKUP_KEY)));
} else {
throw new IllegalArgumentException(String.format("Cannot parse key string: %s", keyString));
}
}
}
/**
* Returns a clone with an ofy key restored from {@code ancestors}.
*
@@ -233,4 +286,29 @@ public class VKey<T> extends ImmutableObject implements Serializable {
public static <T> VKey<T> fromWebsafeKey(String ofyKeyRepr) {
return from(Key.create(ofyKeyRepr));
}
/**
* Constructs the string representation of a {@link VKey}.
*
* <p>The string representation of a vkey contains its type, and sql key or ofy key, or both. Each
* of the keys is first serialized into a byte array then encoded via Base64 into a web safe
* string.
*
* <p>The string representation of a vkey contains key values pairs separated by delimiter "@".
* Another delimiter ":" is put in between each key and value. The following is the complete
* format of the string: "kind:class_name@sql:encoded_sqlKey@ofy:encoded_ofyKey", where kind is
* required. The string representation may contain an encoded ofy key, or an encoded sql key, or
* both.
*/
public String stringify() {
// class type is required to create a vkey
String key = CLASS_TYPE + KV_SEPARATOR + getKind().getName();
if (maybeGetSqlKey().isPresent()) {
key += DELIMITER + SQL_LOOKUP_KEY + KV_SEPARATOR + SerializeUtils.stringify(getSqlKey());
}
if (maybeGetOfyKey().isPresent()) {
key += DELIMITER + OFY_LOOKUP_KEY + KV_SEPARATOR + getOfyKey().getString();
}
return key;
}
}
@@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.model.common.TimedTransitionProperty.TimedTransition;
import google.registry.persistence.converter.StringMapDescriptor.StringMap;
import java.io.Serializable;
import java.util.Map;
import javax.annotation.Nullable;
import javax.persistence.AttributeConverter;
@@ -30,7 +31,8 @@ import org.joda.time.DateTime;
* Base JPA converter for {@link TimedTransitionProperty} objects that are stored in a column with
* data type of hstore in the database.
*/
public abstract class TimedTransitionPropertyConverterBase<K, V extends TimedTransition<K>>
public abstract class TimedTransitionPropertyConverterBase<
K extends Serializable, V extends TimedTransition<K>>
implements AttributeConverter<TimedTransitionProperty<K, V>, StringMap> {
abstract Map.Entry<String, String> convertToDatabaseMapEntry(Map.Entry<DateTime, V> entry);
@@ -24,7 +24,33 @@ import javax.persistence.criteria.CriteriaQuery;
/** Sub-interface of {@link TransactionManager} which defines JPA related methods. */
public interface JpaTransactionManager extends TransactionManager {
/** Returns the {@link EntityManager} for the current request. */
/**
* Returns a long-lived {@link EntityManager} not bound to a particular transaction.
*
* <p>Caller is responsible for closing the returned instance.
*/
EntityManager getStandaloneEntityManager();
/**
* Specifies a database snapshot exported by another transaction to use in the current
* transaction.
*
* <p>This is a Postgresql-specific feature. This method must be called before any other SQL
* commands in a transaction.
*
* <p>To support large queries, transaction isolation level is fixed at the REPEATABLE_READ to
* avoid exhausting predicate locks at the SERIALIZABLE level.
*
* @see google.registry.beam.common.DatabaseSnapshot
*/
// TODO(b/193662898): vendor-independent support for richer transaction semantics.
JpaTransactionManager setDatabaseSnapshot(String snapshotId);
/**
* Returns the {@link EntityManager} for the current request.
*
* <p>The returned instance is closed when the current transaction completes.
*/
EntityManager getEntityManager();
/**
@@ -120,6 +120,11 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
emf.close();
}
@Override
public EntityManager getStandaloneEntityManager() {
return emf.createEntityManager();
}
@Override
public EntityManager getEntityManager() {
EntityManager entityManager = transactionInfo.get().entityManager;
@@ -131,6 +136,22 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
return entityManager;
}
@Override
public JpaTransactionManager setDatabaseSnapshot(String snapshotId) {
// Postgresql-specific: 'set transaction' command must be called inside a transaction
assertInTransaction();
EntityManager entityManager = getEntityManager();
// Isolation is hardcoded to REPEATABLE READ, as specified by parent's Javadoc.
entityManager
.createNativeQuery("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ")
.executeUpdate();
entityManager
.createNativeQuery(String.format("SET TRANSACTION SNAPSHOT '%s'", snapshotId))
.executeUpdate();
return this;
}
@Override
public <T> TypedQuery<T> query(String sqlString, Class<T> resultClass) {
return new DetachingTypedQuery<>(getEntityManager().createQuery(sqlString, resultClass));
@@ -14,6 +14,7 @@
package google.registry.reporting;
import static google.registry.request.RequestParameters.extractOptionalBooleanParameter;
import static google.registry.request.RequestParameters.extractOptionalParameter;
import static google.registry.request.RequestParameters.extractRequiredParameter;
@@ -55,6 +56,12 @@ public class ReportingModule {
/** The request parameter specifying the jobId for a running Dataflow pipeline. */
public static final String PARAM_JOB_ID = "jobId";
/**
* The request parameter name used by actions to indicate if an email should be sent. This
* parameter defaults to true if not specified.
*/
public static final String SEND_EMAIL = "email";
/** Provides the Cloud Dataflow jobId for a pipeline. */
@Provides
@Parameter(PARAM_JOB_ID)
@@ -62,6 +69,13 @@ public class ReportingModule {
return extractRequiredParameter(req, PARAM_JOB_ID);
}
/** Provides the boolean value indicating if emails should be sent. */
@Provides
@Parameter(SEND_EMAIL)
static boolean provideSendEmail(HttpServletRequest req) {
return extractOptionalBooleanParameter(req, SEND_EMAIL).orElse(true);
}
/** Extracts an optional YearMonth in yyyy-MM format from the request. */
@Provides
@Parameter(PARAM_YEAR_MONTH)
@@ -35,14 +35,19 @@ public interface DnsCountQueryCoordinator {
* <p>If your report query requires any additional parameters, add them here.
*/
class Params {
public BigqueryConnection bigquery;
/** The Google Cloud project id. */
public String projectId;
public Params(BigqueryConnection bigquery, String projectId) {
/** The BigQuery dataset from which to query. */
public String icannReportingDataSet;
public Params(BigqueryConnection bigquery, String projectId, String icannReportingDataSet) {
this.bigquery = bigquery;
this.projectId = projectId;
this.icannReportingDataSet = icannReportingDataSet;
}
}
@@ -14,6 +14,7 @@
package google.registry.reporting.icann;
import static google.registry.reporting.icann.IcannReportingModule.ICANN_REPORTING_DATA_SET;
import static google.registry.util.TypeUtils.getClassFromString;
import static google.registry.util.TypeUtils.instantiate;
@@ -21,6 +22,7 @@ import dagger.Module;
import dagger.Provides;
import google.registry.bigquery.BigqueryConnection;
import google.registry.config.RegistryConfig.Config;
import javax.inject.Named;
/** Dagger module to provide the DnsCountQueryCoordinator. */
@Module
@@ -30,9 +32,10 @@ public class DnsCountQueryCoordinatorModule {
static DnsCountQueryCoordinator provideDnsCountQueryCoordinator(
@Config("dnsCountQueryCoordinatorClass") String customClass,
BigqueryConnection bigquery,
@Config("projectId") String projectId) {
@Config("projectId") String projectId,
@Named(ICANN_REPORTING_DATA_SET) String icannReportingDataSet) {
DnsCountQueryCoordinator.Params params =
new DnsCountQueryCoordinator.Params(bigquery, projectId);
new DnsCountQueryCoordinator.Params(bigquery, projectId, icannReportingDataSet);
return instantiate(getClassFromString(customClass, DnsCountQueryCoordinator.class), params);
}
}
@@ -14,6 +14,7 @@
package google.registry.reporting.icann;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.reporting.icann.IcannReportingModule.DATASTORE_EXPORT_DATA_SET;
import static google.registry.reporting.icann.IcannReportingModule.ICANN_REPORTING_DATA_SET;
import static google.registry.reporting.icann.QueryBuilderUtils.getQueryFromFile;
@@ -71,102 +72,180 @@ public final class TransactionsReportingQueryBuilder implements QueryBuilder {
DateTime latestReportTime = earliestReportTime.plusMonths(1).minusMillis(1);
ImmutableMap.Builder<String, String> queriesBuilder = ImmutableMap.builder();
String registrarIanaIdQuery =
SqlTemplate.create(getQueryFromFile("registrar_iana_id.sql"))
.put("PROJECT_ID", projectId)
.put("DATASTORE_EXPORT_DATA_SET", DATASTORE_EXPORT_DATA_SET)
.put("REGISTRAR_TABLE", "Registrar")
.build();
String registrarIanaIdQuery;
if (tm().isOfy()) {
registrarIanaIdQuery =
SqlTemplate.create(getQueryFromFile("registrar_iana_id.sql"))
.put("PROJECT_ID", projectId)
.put("DATASTORE_EXPORT_DATA_SET", DATASTORE_EXPORT_DATA_SET)
.put("REGISTRAR_TABLE", "Registrar")
.build();
} else {
registrarIanaIdQuery =
SqlTemplate.create(getQueryFromFile("cloud_sql_registrar_iana_id.sql"))
.put("PROJECT_ID", projectId)
.build();
}
queriesBuilder.put(getTableName(REGISTRAR_IANA_ID, yearMonth), registrarIanaIdQuery);
String totalDomainsQuery =
SqlTemplate.create(getQueryFromFile("total_domains.sql"))
.put("PROJECT_ID", projectId)
.put("DATASTORE_EXPORT_DATA_SET", DATASTORE_EXPORT_DATA_SET)
.put("DOMAINBASE_TABLE", "DomainBase")
.put("REGISTRAR_TABLE", "Registrar")
.build();
String totalDomainsQuery;
if (tm().isOfy()) {
totalDomainsQuery =
SqlTemplate.create(getQueryFromFile("total_domains.sql"))
.put("PROJECT_ID", projectId)
.put("DATASTORE_EXPORT_DATA_SET", DATASTORE_EXPORT_DATA_SET)
.put("DOMAINBASE_TABLE", "DomainBase")
.put("REGISTRAR_TABLE", "Registrar")
.build();
} else {
totalDomainsQuery =
SqlTemplate.create(getQueryFromFile("cloud_sql_total_domains.sql"))
.put("PROJECT_ID", projectId)
.build();
}
queriesBuilder.put(getTableName(TOTAL_DOMAINS, yearMonth), totalDomainsQuery);
DateTimeFormatter timestampFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS");
String totalNameserversQuery =
SqlTemplate.create(getQueryFromFile("total_nameservers.sql"))
.put("PROJECT_ID", projectId)
.put("DATASTORE_EXPORT_DATA_SET", DATASTORE_EXPORT_DATA_SET)
.put("HOSTRESOURCE_TABLE", "HostResource")
.put("DOMAINBASE_TABLE", "DomainBase")
.put("REGISTRAR_TABLE", "Registrar")
.put("LATEST_REPORT_TIME", timestampFormatter.print(latestReportTime))
.build();
String totalNameserversQuery;
if (tm().isOfy()) {
totalNameserversQuery =
SqlTemplate.create(getQueryFromFile("total_nameservers.sql"))
.put("PROJECT_ID", projectId)
.put("DATASTORE_EXPORT_DATA_SET", DATASTORE_EXPORT_DATA_SET)
.put("HOSTRESOURCE_TABLE", "HostResource")
.put("DOMAINBASE_TABLE", "DomainBase")
.put("REGISTRAR_TABLE", "Registrar")
.put("LATEST_REPORT_TIME", timestampFormatter.print(latestReportTime))
.build();
} else {
totalNameserversQuery =
SqlTemplate.create(getQueryFromFile("cloud_sql_total_nameservers.sql"))
.put("PROJECT_ID", projectId)
.put("LATEST_REPORT_TIME", timestampFormatter.print(latestReportTime))
.build();
}
queriesBuilder.put(getTableName(TOTAL_NAMESERVERS, yearMonth), totalNameserversQuery);
String transactionCountsQuery =
SqlTemplate.create(getQueryFromFile("transaction_counts.sql"))
.put("PROJECT_ID", projectId)
.put("DATASTORE_EXPORT_DATA_SET", DATASTORE_EXPORT_DATA_SET)
.put("REGISTRAR_TABLE", "Registrar")
.put("HISTORYENTRY_TABLE", "HistoryEntry")
.put("EARLIEST_REPORT_TIME", timestampFormatter.print(earliestReportTime))
.put("LATEST_REPORT_TIME", timestampFormatter.print(latestReportTime))
.put("CLIENT_ID", "clientId")
.put("OTHER_CLIENT_ID", "otherClientId")
.put("TRANSFER_SUCCESS_FIELD", "TRANSFER_GAINING_SUCCESSFUL")
.put("TRANSFER_NACKED_FIELD", "TRANSFER_GAINING_NACKED")
.put("DEFAULT_FIELD", "field")
.build();
String transactionCountsQuery;
if (tm().isOfy()) {
transactionCountsQuery =
SqlTemplate.create(getQueryFromFile("transaction_counts.sql"))
.put("PROJECT_ID", projectId)
.put("DATASTORE_EXPORT_DATA_SET", DATASTORE_EXPORT_DATA_SET)
.put("REGISTRAR_TABLE", "Registrar")
.put("HISTORYENTRY_TABLE", "HistoryEntry")
.put("EARLIEST_REPORT_TIME", timestampFormatter.print(earliestReportTime))
.put("LATEST_REPORT_TIME", timestampFormatter.print(latestReportTime))
.put("CLIENT_ID", "clientId")
.put("OTHER_CLIENT_ID", "otherClientId")
.put("TRANSFER_SUCCESS_FIELD", "TRANSFER_GAINING_SUCCESSFUL")
.put("TRANSFER_NACKED_FIELD", "TRANSFER_GAINING_NACKED")
.put("DEFAULT_FIELD", "field")
.build();
} else {
transactionCountsQuery =
SqlTemplate.create(getQueryFromFile("cloud_sql_transaction_counts.sql"))
.put("PROJECT_ID", projectId)
.put("EARLIEST_REPORT_TIME", timestampFormatter.print(earliestReportTime))
.put("LATEST_REPORT_TIME", timestampFormatter.print(latestReportTime))
.build();
}
queriesBuilder.put(getTableName(TRANSACTION_COUNTS, yearMonth), transactionCountsQuery);
String transactionTransferLosingQuery =
SqlTemplate.create(getQueryFromFile("transaction_counts.sql"))
.put("PROJECT_ID", projectId)
.put("DATASTORE_EXPORT_DATA_SET", DATASTORE_EXPORT_DATA_SET)
.put("REGISTRAR_TABLE", "Registrar")
.put("HISTORYENTRY_TABLE", "HistoryEntry")
.put("EARLIEST_REPORT_TIME", timestampFormatter.print(earliestReportTime))
.put("LATEST_REPORT_TIME", timestampFormatter.print(latestReportTime))
// Roles are reversed for losing queries
.put("CLIENT_ID", "otherClientId")
.put("OTHER_CLIENT_ID", "clientId")
.put("TRANSFER_SUCCESS_FIELD", "TRANSFER_LOSING_SUCCESSFUL")
.put("TRANSFER_NACKED_FIELD", "TRANSFER_LOSING_NACKED")
.put("DEFAULT_FIELD", "NULL")
.build();
String transactionTransferLosingQuery;
if (tm().isOfy()) {
transactionTransferLosingQuery =
SqlTemplate.create(getQueryFromFile("transaction_counts.sql"))
.put("PROJECT_ID", projectId)
.put("DATASTORE_EXPORT_DATA_SET", DATASTORE_EXPORT_DATA_SET)
.put("REGISTRAR_TABLE", "Registrar")
.put("HISTORYENTRY_TABLE", "HistoryEntry")
.put("EARLIEST_REPORT_TIME", timestampFormatter.print(earliestReportTime))
.put("LATEST_REPORT_TIME", timestampFormatter.print(latestReportTime))
// Roles are reversed for losing queries
.put("CLIENT_ID", "otherClientId")
.put("OTHER_CLIENT_ID", "clientId")
.put("TRANSFER_SUCCESS_FIELD", "TRANSFER_LOSING_SUCCESSFUL")
.put("TRANSFER_NACKED_FIELD", "TRANSFER_LOSING_NACKED")
.put("DEFAULT_FIELD", "NULL")
.build();
} else {
transactionTransferLosingQuery =
SqlTemplate.create(getQueryFromFile("cloud_sql_transaction_transfer_losing.sql"))
.put("PROJECT_ID", projectId)
.put("EARLIEST_REPORT_TIME", timestampFormatter.print(earliestReportTime))
.put("LATEST_REPORT_TIME", timestampFormatter.print(latestReportTime))
.build();
}
queriesBuilder.put(
getTableName(TRANSACTION_TRANSFER_LOSING, yearMonth), transactionTransferLosingQuery);
// App Engine log table suffixes use YYYYMMDD format
DateTimeFormatter logTableFormatter = DateTimeFormat.forPattern("yyyyMMdd");
String attemptedAddsQuery =
SqlTemplate.create(getQueryFromFile("attempted_adds.sql"))
.put("PROJECT_ID", projectId)
.put("DATASTORE_EXPORT_DATA_SET", DATASTORE_EXPORT_DATA_SET)
.put("REGISTRAR_TABLE", "Registrar")
.put("APPENGINE_LOGS_DATA_SET", "appengine_logs")
.put("REQUEST_TABLE", "appengine_googleapis_com_request_log_")
.put("FIRST_DAY_OF_MONTH", logTableFormatter.print(earliestReportTime))
.put("LAST_DAY_OF_MONTH", logTableFormatter.print(latestReportTime))
// All metadata logs for reporting come from google.registry.flows.FlowReporter.
.put(
"METADATA_LOG_PREFIX",
"google.registry.flows.FlowReporter recordToLogs: FLOW-LOG-SIGNATURE-METADATA")
.build();
String attemptedAddsQuery;
if (tm().isOfy()) {
attemptedAddsQuery =
SqlTemplate.create(getQueryFromFile("attempted_adds.sql"))
.put("PROJECT_ID", projectId)
.put("DATASTORE_EXPORT_DATA_SET", DATASTORE_EXPORT_DATA_SET)
.put("REGISTRAR_TABLE", "Registrar")
.put("APPENGINE_LOGS_DATA_SET", "appengine_logs")
.put("REQUEST_TABLE", "appengine_googleapis_com_request_log_")
.put("FIRST_DAY_OF_MONTH", logTableFormatter.print(earliestReportTime))
.put("LAST_DAY_OF_MONTH", logTableFormatter.print(latestReportTime))
// All metadata logs for reporting come from google.registry.flows.FlowReporter.
.put(
"METADATA_LOG_PREFIX",
"google.registry.flows.FlowReporter recordToLogs: FLOW-LOG-SIGNATURE-METADATA")
.build();
} else {
attemptedAddsQuery =
SqlTemplate.create(getQueryFromFile("cloud_sql_attempted_adds.sql"))
.put("PROJECT_ID", projectId)
.put("APPENGINE_LOGS_DATA_SET", "appengine_logs")
.put("REQUEST_TABLE", "appengine_googleapis_com_request_log_")
.put("FIRST_DAY_OF_MONTH", logTableFormatter.print(earliestReportTime))
.put("LAST_DAY_OF_MONTH", logTableFormatter.print(latestReportTime))
// All metadata logs for reporting come from google.registry.flows.FlowReporter.
.put(
"METADATA_LOG_PREFIX",
"google.registry.flows.FlowReporter recordToLogs: FLOW-LOG-SIGNATURE-METADATA")
.build();
}
queriesBuilder.put(getTableName(ATTEMPTED_ADDS, yearMonth), attemptedAddsQuery);
String aggregateQuery =
SqlTemplate.create(getQueryFromFile("transactions_report_aggregation.sql"))
.put("PROJECT_ID", projectId)
.put("DATASTORE_EXPORT_DATA_SET", DATASTORE_EXPORT_DATA_SET)
.put("REGISTRY_TABLE", "Registry")
.put("ICANN_REPORTING_DATA_SET", icannReportingDataSet)
.put("REGISTRAR_IANA_ID_TABLE", getTableName(REGISTRAR_IANA_ID, yearMonth))
.put("TOTAL_DOMAINS_TABLE", getTableName(TOTAL_DOMAINS, yearMonth))
.put("TOTAL_NAMESERVERS_TABLE", getTableName(TOTAL_NAMESERVERS, yearMonth))
.put("TRANSACTION_COUNTS_TABLE", getTableName(TRANSACTION_COUNTS, yearMonth))
.put(
"TRANSACTION_TRANSFER_LOSING_TABLE",
getTableName(TRANSACTION_TRANSFER_LOSING, yearMonth))
.put("ATTEMPTED_ADDS_TABLE", getTableName(ATTEMPTED_ADDS, yearMonth))
.build();
String aggregateQuery;
if (tm().isOfy()) {
aggregateQuery =
SqlTemplate.create(getQueryFromFile("transactions_report_aggregation.sql"))
.put("PROJECT_ID", projectId)
.put("DATASTORE_EXPORT_DATA_SET", DATASTORE_EXPORT_DATA_SET)
.put("REGISTRY_TABLE", "Registry")
.put("ICANN_REPORTING_DATA_SET", icannReportingDataSet)
.put("REGISTRAR_IANA_ID_TABLE", getTableName(REGISTRAR_IANA_ID, yearMonth))
.put("TOTAL_DOMAINS_TABLE", getTableName(TOTAL_DOMAINS, yearMonth))
.put("TOTAL_NAMESERVERS_TABLE", getTableName(TOTAL_NAMESERVERS, yearMonth))
.put("TRANSACTION_COUNTS_TABLE", getTableName(TRANSACTION_COUNTS, yearMonth))
.put(
"TRANSACTION_TRANSFER_LOSING_TABLE",
getTableName(TRANSACTION_TRANSFER_LOSING, yearMonth))
.put("ATTEMPTED_ADDS_TABLE", getTableName(ATTEMPTED_ADDS, yearMonth))
.build();
} else {
aggregateQuery =
SqlTemplate.create(getQueryFromFile("cloud_sql_transactions_report_aggregation.sql"))
.put("PROJECT_ID", projectId)
.put("ICANN_REPORTING_DATA_SET", icannReportingDataSet)
.put("REGISTRAR_IANA_ID_TABLE", getTableName(REGISTRAR_IANA_ID, yearMonth))
.put("TOTAL_DOMAINS_TABLE", getTableName(TOTAL_DOMAINS, yearMonth))
.put("TOTAL_NAMESERVERS_TABLE", getTableName(TOTAL_NAMESERVERS, yearMonth))
.put("TRANSACTION_COUNTS_TABLE", getTableName(TRANSACTION_COUNTS, yearMonth))
.put(
"TRANSACTION_TRANSFER_LOSING_TABLE",
getTableName(TRANSACTION_TRANSFER_LOSING, yearMonth))
.put("ATTEMPTED_ADDS_TABLE", getTableName(ATTEMPTED_ADDS, yearMonth))
.build();
}
queriesBuilder.put(getTableName(TRANSACTIONS_REPORT_AGGREGATION, yearMonth), aggregateQuery);
return queriesBuilder.build();
@@ -0,0 +1,73 @@
#standardSQL
-- Copyright 2021 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.
-- Determine the number of attempted adds each registrar made.
-- Since the specification requests all 'attempted' adds, we regex the
-- monthly App Engine logs, searching for all create commands and associating
-- them with their corresponding registrars.
-- Example log generated by FlowReporter in App Engine logs:
--google.registry.flows.FlowReporter
-- recordToLogs: FLOW-LOG-SIGNATURE-METADATA:
--{"serverTrid":"oNwL2J2eRya7bh7c9oHIzg==-2360a","clientId":"ipmirror"
-- ,"commandType":"hello", "resourceType":"","flowClassName":"HelloFlow"
-- ,"targetId":"","targetIds":[],"tld":"",
-- "tlds":[],"icannActivityReportField":""}
-- This outer select just converts the registrar's clientId to their name.
SELECT
tld,
registrar_table.registrar_name AS registrar_name,
'ATTEMPTED_ADDS' AS metricName,
count AS metricValue
FROM (
SELECT
JSON_EXTRACT_SCALAR(json, '$.tld') AS tld,
JSON_EXTRACT_SCALAR(json, '$.clientId') AS clientId,
COUNT(json) AS count
FROM (
-- Extract JSON metadata package from monthly logs
SELECT
REGEXP_EXTRACT(logMessages, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$')
AS json
FROM (
SELECT
protoPayload.resource AS requestPath,
ARRAY(
SELECT logMessage
FROM UNNEST(protoPayload.line)) AS logMessage
FROM
`%PROJECT_ID%.%APPENGINE_LOGS_DATA_SET%.%REQUEST_TABLE%*`
WHERE _TABLE_SUFFIX
BETWEEN '%FIRST_DAY_OF_MONTH%'
AND '%LAST_DAY_OF_MONTH%')
JOIN UNNEST(logMessage) AS logMessages
-- Look for metadata logs from epp and registrar console requests
WHERE requestPath IN ('/_dr/epp', '/_dr/epptool', '/registrar-xhr')
AND STARTS_WITH(logMessages, "%METADATA_LOG_PREFIX%")
-- Look for domain creates
AND REGEXP_CONTAINS(
logMessages, r'"commandType":"create","resourceType":"domain"')
-- Filter prober data
AND NOT REGEXP_CONTAINS(
logMessages, r'"prober-[a-z]{2}-((any)|(canary))"') )
GROUP BY tld, clientId ) AS logs_table
JOIN
EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql",
'''SELECT registrar_id, registrar_name FROM "Registrar";''') AS registrar_table
ON
logs_table.clientId = registrar_table.registrar_id
ORDER BY tld, registrar_name
@@ -0,0 +1,37 @@
#standardSQL
-- Copyright 2021 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.
-- Gather a list of all tld-registrar pairs, with their IANA IDs.
-- This establishes which registrars will appear in the reports.
SELECT
allowedTlds AS tld,
registrar_name,
iana_identifier AS iana_id
FROM
EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql",
'''SELECT
allowedTlds,
registrar_name,
iana_identifier
FROM
"Registrar" AS r,
UNNEST(allowed_tlds) AS allowedTlds
WHERE
r.type='REAL' OR r.type='INTERNAL';''')
-- Filter out prober data
WHERE NOT ENDS_WITH(allowedTlds, "test")
ORDER BY tld, registrar_name
@@ -0,0 +1,43 @@
#standardSQL
-- Copyright 2021 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.
-- Determine the number of domains each registrar sponsors per tld.
-- This is just the number of fullyQualifiedDomainNames under each
-- tld-registrar pair.
SELECT
tld,
registrar_name,
'TOTAL_DOMAINS' as metricName,
COUNT(domain_name) as metricValue
FROM
EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql",
'''SELECT
registrar_name,
registrar_id
FROM "Registrar" AS r
WHERE r.type='REAL' OR r.type='INTERNAL';''')
JOIN
EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql",
'''SELECT
tld,
current_sponsor_registrar_id,
domain_name
FROM "Domain" AS d;''')
ON
current_sponsor_registrar_id = registrar_id
GROUP BY tld, registrar_name
ORDER BY tld, registrar_name
@@ -0,0 +1,80 @@
#standardSQL
-- Copyright 2021 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.
-- Determine the number of referenced nameservers for a registrar's domains.
-- We count the number of unique hosts under each tld-registrar combo by
-- collecting all domains' listed hosts that were still valid at the
-- end of the reporting month.
SELECT
tld,
registrar_name,
'TOTAL_NAMESERVERS' AS metricName,
COUNT(host_name) AS metricValue
FROM
EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql",
'''SELECT
host_name,
current_sponsor_registrar_id,
creation_time AS host_creation_time,
CASE
WHEN deletion_time > to_timestamp('9999-12-31 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
THEN to_timestamp('9999-12-31 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
ELSE
deletion_time
END as host_deletion_time
FROM "Host";''')
JOIN (
SELECT
registrar_id,
registrar_name
FROM
EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql", '''SELECT registrar_id, registrar_name FROM "Registrar" AS r WHERE r.type='REAL' OR r.type='INTERNAL';'''))
ON current_sponsor_registrar_id = registrar_id
JOIN (
SELECT
tld,
referencedHostName
FROM
EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql",
'''SELECT
repo_id,
tld,
creation_time AS domain_creation_time,
CASE
WHEN deletion_time > to_timestamp('9999-12-31 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
THEN to_timestamp('9999-12-31 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
ELSE
deletion_time
END as domain_deletion_time
FROM "Domain";''')
JOIN
EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql", '''SELECT domain_repo_id, host_repo_id FROM "DomainHost";''')
ON
repo_id = domain_repo_id
JOIN
EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql", '''SELECT repo_id as host_table_repo_id, host_name AS referencedHostName FROM "Host";''')
ON
host_table_repo_id = host_repo_id
WHERE
domain_creation_time <= TIMESTAMP("%LATEST_REPORT_TIME%")
AND domain_deletion_time > TIMESTAMP("%LATEST_REPORT_TIME%"))
ON host_name = referencedHostName
WHERE
host_creation_time <= TIMESTAMP("%LATEST_REPORT_TIME%")
AND host_deletion_time > TIMESTAMP("%LATEST_REPORT_TIME%")
GROUP BY tld, registrar_name
ORDER BY tld, registrar_name
@@ -0,0 +1,72 @@
#standardSQL
-- Copyright 2021 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.
-- Counts the number of mutating transactions each registrar made.
-- We populate the fields through explicit logging of
-- DomainTransactionRecords, which contain all necessary information for
-- reporting (such as reporting time, report field, report amount, etc.
SELECT
tld,
registrar_table.registrar_name AS registrar_name,
metricName,
metricValue
FROM (
SELECT
tld,
clientId,
CASE WHEN field = 'TRANSFER_SUCCESSFUL' THEN 'TRANSFER_GAINING_SUCCESSFUL'
WHEN field = 'TRANSFER_NACKED' THEN 'TRANSFER_GAINING_NACKED'
ELSE field
END AS metricName,
SUM(amount) AS metricValue
FROM (
SELECT
CASE
-- Explicit transfer acks (approve) and nacks (reject) are done
-- by the opposing registrar. Thus, for these specific actions,
-- we swap the 'history_other_registrar_id' with the
-- 'history_registrar_id' to properly account for this reversal.
WHEN (history_type = 'DOMAIN_TRANSFER_APPROVE'
OR history_type = 'DOMAIN_TRANSFER_REJECT')
THEN history_other_registrar_id
ELSE history_registrar_id
END AS clientId,
tld,
report_field AS field,
report_amount AS amount,
reporting_time AS reportingTime
FROM EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql",
''' SELECT history_type, history_other_registrar_id, history_registrar_id, domain_repo_id, history_revision_id FROM "DomainHistory";''') AS dh
JOIN
EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql",
'''SELECT domain_repo_id, history_revision_id, reporting_time, tld, report_field, report_amount FROM "DomainTransactionRecord";''') AS dtr
ON
dh.domain_repo_id = dtr.domain_repo_id AND dh.history_revision_id = dtr.history_revision_id
)
-- Only look at this month's data
WHERE reportingTime
BETWEEN TIMESTAMP('%EARLIEST_REPORT_TIME%')
AND TIMESTAMP('%LATEST_REPORT_TIME%')
GROUP BY
tld,
clientId,
field ) AS counts_table
JOIN
EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql",
'''SELECT registrar_id, registrar_name FROM "Registrar";''') AS registrar_table
ON
counts_table.clientId = registrar_table.registrar_id
@@ -0,0 +1,72 @@
#standardSQL
-- Copyright 2021 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.
-- Counts the number of mutating transactions each registrar made.
-- We populate the fields through explicit logging of
-- DomainTransactionRecords, which contain all necessary information for
-- reporting (such as reporting time, report field, report amount, etc.
SELECT
tld,
registrar_table.registrar_name AS registrar_name,
metricName,
metricValue
FROM (
SELECT
tld,
clientId,
CASE WHEN field = 'TRANSFER_SUCCESSFUL' THEN 'TRANSFER_LOSING_SUCCESSFUL'
WHEN field = 'TRANSFER_NACKED' THEN 'TRANSFER_LOSING_NACKED'
ELSE NULL
END AS metricName,
SUM(amount) AS metricValue
FROM (
SELECT
CASE
-- Explicit transfer acks (approve) and nacks (reject) are done
-- by the opposing registrar. Thus, for these specific actions,
-- we swap the 'history_other_registrar_id' with the
-- 'history_registrar_id' to properly account for this reversal.
WHEN (history_type = 'DOMAIN_TRANSFER_APPROVE'
OR history_type = 'DOMAIN_TRANSFER_REJECT')
THEN history_registrar_id
ELSE history_other_registrar_id
END AS clientId,
tld,
report_field AS field,
report_amount AS amount,
reporting_time AS reportingTime
FROM EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql",
''' SELECT history_type, history_other_registrar_id, history_registrar_id, domain_repo_id, history_revision_id FROM "DomainHistory";''') AS dh
JOIN
EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql",
'''SELECT domain_repo_id, history_revision_id, reporting_time, tld, report_field, report_amount FROM "DomainTransactionRecord";''') AS dtr
ON
dh.domain_repo_id = dtr.domain_repo_id AND dh.history_revision_id = dtr.history_revision_id
)
-- Only look at this month's data
WHERE reportingTime
BETWEEN TIMESTAMP('%EARLIEST_REPORT_TIME%')
AND TIMESTAMP('%LATEST_REPORT_TIME%')
GROUP BY
tld,
clientId,
field ) AS counts_table
JOIN
EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql",
'''SELECT registrar_id, registrar_name FROM "Registrar";''') AS registrar_table
ON
counts_table.clientId = registrar_table.registrar_id
@@ -0,0 +1,97 @@
#standardSQL
-- Copyright 2021 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.
-- Construct the transaction reports' rows from the intermediary data views.
-- This query pulls from all intermediary tables to create the activity
-- report csv, via a table transpose and sum over all activity report fields.
SELECT
tlds.tld_name as tld,
-- Surround registrar names with quotes to handle names containing a comma.
FORMAT("\"%s\"", registrars.registrar_name) as registrar_name,
registrars.iana_id as iana_id,
SUM(IF(metrics.metricName = 'TOTAL_DOMAINS', metrics.metricValue, 0)) AS total_domains,
SUM(IF(metrics.metricName = 'TOTAL_NAMESERVERS', metrics.metricValue, 0)) AS total_nameservers,
SUM(IF(metrics.metricName = 'NET_ADDS_1_YR', metrics.metricValue, 0)) AS net_adds_1_yr,
SUM(IF(metrics.metricName = 'NET_ADDS_2_YR', metrics.metricValue, 0)) AS net_adds_2_yr,
SUM(IF(metrics.metricName = 'NET_ADDS_3_YR', metrics.metricValue, 0)) AS net_adds_3_yr,
SUM(IF(metrics.metricName = 'NET_ADDS_4_YR', metrics.metricValue, 0)) AS net_adds_4_yr,
SUM(IF(metrics.metricName = 'NET_ADDS_5_YR', metrics.metricValue, 0)) AS net_adds_5_yr,
SUM(IF(metrics.metricName = 'NET_ADDS_6_YR', metrics.metricValue, 0)) AS net_adds_6_yr,
SUM(IF(metrics.metricName = 'NET_ADDS_7_YR', metrics.metricValue, 0)) AS net_adds_7_yr,
SUM(IF(metrics.metricName = 'NET_ADDS_8_YR', metrics.metricValue, 0)) AS net_adds_8_yr,
SUM(IF(metrics.metricName = 'NET_ADDS_9_YR', metrics.metricValue, 0)) AS net_adds_9_yr,
SUM(IF(metrics.metricName = 'NET_ADDS_10_Yr', metrics.metricValue, 0)) AS net_adds_10_yr,
SUM(IF(metrics.metricName = 'NET_RENEWS_1_YR', metrics.metricValue, 0)) AS net_renews_1_yr,
SUM(IF(metrics.metricName = 'NET_RENEWS_2_YR', metrics.metricValue, 0)) AS net_renews_2_yr,
SUM(IF(metrics.metricName = 'NET_RENEWS_3_YR', metrics.metricValue, 0)) AS net_renews_3_yr,
SUM(IF(metrics.metricName = 'NET_RENEWS_4_YR', metrics.metricValue, 0)) AS net_renews_4_yr,
SUM(IF(metrics.metricName = 'NET_RENEWS_5_YR', metrics.metricValue, 0)) AS net_renews_5_yr,
SUM(IF(metrics.metricName = 'NET_RENEWS_6_YR', metrics.metricValue, 0)) AS net_renews_6_yr,
SUM(IF(metrics.metricName = 'NET_RENEWS_7_YR', metrics.metricValue, 0)) AS net_renews_7_yr,
SUM(IF(metrics.metricName = 'NET_RENEWS_8_YR', metrics.metricValue, 0)) AS net_renews_8_yr,
SUM(IF(metrics.metricName = 'NET_RENEWS_9_YR', metrics.metricValue, 0)) AS net_renews_9_yr,
SUM(IF(metrics.metricName = 'NET_RENEWS_10_YR', metrics.metricValue, 0)) AS net_renews_10_yr,
SUM(IF(metrics.metricName = 'TRANSFER_GAINING_SUCCESSFUL', metrics.metricValue, 0)) AS transfer_gaining_successful,
SUM(IF(metrics.metricName = 'TRANSFER_GAINING_NACKED', metrics.metricValue, 0)) AS transfer_gaining_nacked,
SUM(IF(metrics.metricName = 'TRANSFER_LOSING_SUCCESSFUL', metrics.metricValue, 0)) AS transfer_losing_successful,
SUM(IF(metrics.metricName = 'TRANSFER_LOSING_NACKED', metrics.metricValue, 0)) AS transfer_losing_nacked,
-- We don't interact with transfer disputes
0 AS transfer_disputed_won,
0 AS transfer_disputed_lost,
0 AS transfer_disputed_nodecision,
SUM(IF(metrics.metricName = 'DELETED_DOMAINS_GRACE', metrics.metricValue, 0)) AS deleted_domains_grace,
SUM(IF(metrics.metricName = 'DELETED_DOMAINS_NOGRACE', metrics.metricValue, 0)) AS deleted_domains_nograce,
SUM(IF(metrics.metricName = 'RESTORED_DOMAINS', metrics.metricValue, 0)) AS restored_domains,
-- We don't require restore reports
0 AS restored_noreport,
-- We don't enforce AGP limits right now
0 AS agp_exemption_requests,
0 AS agp_exemptions_granted,
0 AS agp_exempted_domains,
SUM(IF(metrics.metricName = 'ATTEMPTED_ADDS', metrics.metricValue, 0)) AS attempted_adds
FROM
-- Only produce reports for real TLDs
EXTERNAL_QUERY("projects/%PROJECT_ID%/locations/us/connections/%PROJECT_ID%-sql", '''SELECT "tld_name" FROM "Tld" AS t WHERE t.tld_type='REAL';''') AS tlds
JOIN
(SELECT *
FROM `%PROJECT_ID%.%ICANN_REPORTING_DATA_SET%.%REGISTRAR_IANA_ID_TABLE%`)
AS registrars
ON tlds.tld_name = registrars.tld
-- We LEFT JOIN to produce reports even if the registrar made no transactions
LEFT OUTER JOIN (
-- Gather all intermediary data views
SELECT *
FROM `%PROJECT_ID%.%ICANN_REPORTING_DATA_SET%.%TOTAL_DOMAINS_TABLE%`
UNION ALL
SELECT *
FROM `%PROJECT_ID%.%ICANN_REPORTING_DATA_SET%.%TOTAL_NAMESERVERS_TABLE%`
UNION ALL
SELECT *
FROM `%PROJECT_ID%.%ICANN_REPORTING_DATA_SET%.%TRANSACTION_COUNTS_TABLE%`
UNION ALL
SELECT *
FROM `%PROJECT_ID%.%ICANN_REPORTING_DATA_SET%.%TRANSACTION_TRANSFER_LOSING_TABLE%`
UNION ALL
SELECT *
FROM `%PROJECT_ID%.%ICANN_REPORTING_DATA_SET%.%ATTEMPTED_ADDS_TABLE%` ) AS metrics
-- Join on tld and registrar name
ON registrars.tld = metrics.tld
AND registrars.registrar_name = metrics.registrar_name
GROUP BY
tld, registrar_name, iana_id
ORDER BY
tld, registrar_name
@@ -72,6 +72,7 @@ public class GenerateSpec11ReportAction implements Runnable {
private final Response response;
private final Dataflow dataflow;
private final PrimaryDatabase database;
private final boolean sendEmail;
@Inject
GenerateSpec11ReportAction(
@@ -82,6 +83,7 @@ public class GenerateSpec11ReportAction implements Runnable {
@Key("safeBrowsingAPIKey") String apiKey,
@Parameter(ReportingModule.PARAM_DATE) LocalDate date,
@Parameter(RequestParameters.PARAM_DATABASE) PrimaryDatabase database,
@Parameter(ReportingModule.SEND_EMAIL) boolean sendEmail,
Clock clock,
Response response,
Dataflow dataflow) {
@@ -98,6 +100,7 @@ public class GenerateSpec11ReportAction implements Runnable {
this.clock = clock;
this.response = response;
this.dataflow = dataflow;
this.sendEmail = sendEmail;
}
@Override
@@ -136,7 +139,9 @@ public class GenerateSpec11ReportAction implements Runnable {
Map<String, String> beamTaskParameters =
ImmutableMap.of(
ReportingModule.PARAM_JOB_ID, jobId, ReportingModule.PARAM_DATE, date.toString());
enqueueBeamReportingTask(PublishSpec11ReportAction.PATH, beamTaskParameters);
if (sendEmail) {
enqueueBeamReportingTask(PublishSpec11ReportAction.PATH, beamTaskParameters);
}
response.setStatus(SC_OK);
response.setPayload(String.format("Launched Spec11 pipeline: %s", jobId));
} catch (IOException e) {
@@ -22,8 +22,8 @@ import org.joda.time.Duration;
/**
* Code execution locked on some shared resource.
*
* <p>Locks are either specific to a tld or global to the entire system, in which case a tld of
* null is used.
* <p>Locks are either specific to a tld or global to the entire system, in which case a tld of null
* is used.
*/
public interface LockHandler extends Serializable {
@@ -42,4 +42,22 @@ public interface LockHandler extends Serializable {
@Nullable String tld,
Duration leaseLength,
String... lockNames);
/**
* Acquire one or more locks using only Cloud SQL and execute a Void {@link Callable}.
*
* <p>Runs on a thread that will be killed if it doesn't complete before the lease expires.
*
* <p>Note that locks are specific either to a given tld or to the entire system (in which case
* tld should be passed as null).
*
* <p>This method exists so that Beam pipelines can acquire / load / release locks.
*
* @return true if all locks were acquired and the callable was run; false otherwise.
*/
boolean executeWithSqlLocks(
final Callable<Void> callable,
@Nullable String tld,
Duration leaseLength,
String... lockNames);
}
@@ -73,12 +73,42 @@ public class LockHandlerImpl implements LockHandler {
@Nullable String tld,
Duration leaseLength,
String... lockNames) {
return executeWithLockAcquirer(callable, tld, leaseLength, this::acquire, lockNames);
}
/**
* Acquire one or more locks using only Cloud SQL and execute a Void {@link Callable}.
*
* <p>Thread will be killed if it doesn't complete before the lease expires.
*
* <p>Note that locks are specific either to a given tld or to the entire system (in which case
* tld should be passed as null).
*
* <p>This method exists so that Beam pipelines can acquire / load / release locks.
*
* @return whether all locks were acquired and the callable was run.
*/
@Override
public boolean executeWithSqlLocks(
final Callable<Void> callable,
@Nullable String tld,
Duration leaseLength,
String... lockNames) {
return executeWithLockAcquirer(callable, tld, leaseLength, this::acquireSql, lockNames);
}
private boolean executeWithLockAcquirer(
final Callable<Void> callable,
@Nullable String tld,
Duration leaseLength,
LockAcquirer lockAcquirer,
String... lockNames) {
DateTime startTime = clock.nowUtc();
String sanitizedTld = Strings.emptyToNull(tld);
try {
return AppEngineTimeLimiter.create()
.callWithTimeout(
new LockingCallable(callable, sanitizedTld, leaseLength, lockNames),
new LockingCallable(callable, lockAcquirer, sanitizedTld, leaseLength, lockNames),
leaseLength.minus(LOCK_TIMEOUT_FUDGE).getMillis(),
TimeUnit.MILLISECONDS);
} catch (ExecutionException | UncheckedExecutionException e) {
@@ -108,17 +138,32 @@ public class LockHandlerImpl implements LockHandler {
return Lock.acquire(lockName, tld, leaseLength, requestStatusChecker, true);
}
@VisibleForTesting
Optional<Lock> acquireSql(String lockName, @Nullable String tld, Duration leaseLength) {
return Lock.acquireSql(lockName, tld, leaseLength, requestStatusChecker, true);
}
private interface LockAcquirer {
Optional<Lock> acquireLock(String lockName, @Nullable String tld, Duration leaseLength);
}
/** A {@link Callable} that acquires and releases a lock around a delegate {@link Callable}. */
private class LockingCallable implements Callable<Boolean> {
private static class LockingCallable implements Callable<Boolean> {
final Callable<Void> delegate;
final LockAcquirer lockAcquirer;
@Nullable final String tld;
final Duration leaseLength;
final Set<String> lockNames;
LockingCallable(
Callable<Void> delegate, String tld, Duration leaseLength, String... lockNames) {
Callable<Void> delegate,
LockAcquirer lockAcquirer,
String tld,
Duration leaseLength,
String... lockNames) {
checkArgument(leaseLength.isLongerThan(LOCK_TIMEOUT_FUDGE));
this.delegate = delegate;
this.lockAcquirer = lockAcquirer;
this.tld = tld;
this.leaseLength = leaseLength;
// Make sure we join locks in a fixed (lexicographical) order to avoid deadlock.
@@ -130,7 +175,7 @@ public class LockHandlerImpl implements LockHandler {
Set<Lock> acquiredLocks = new HashSet<>();
try {
for (String lockName : lockNames) {
Optional<Lock> lock = acquire(lockName, tld, leaseLength);
Optional<Lock> lock = lockAcquirer.acquireLock(lockName, tld, leaseLength);
if (!lock.isPresent()) {
logger.atInfo().log("Couldn't acquire lock named: %s for TLD %s.", lockName, tld);
return false;
@@ -108,6 +108,16 @@ public class CreateSyntheticHistoryEntriesAction implements Runnable {
return jpaTm()
.transact(
() -> {
// Use READ COMMITTED isolation level so that any long-living queries don't cause
// collection of predicate locks to spiral out of control (as would happen with a
// SERIALIZABLE isolation level)
//
// NB: setting the isolation level inside the transaction only works for Postgres and
// will be reverted to the default once the transaction is committed.
jpaTm()
.getEntityManager()
.createNativeQuery("SET TRANSACTION ISOLATION LEVEL READ COMMITTED")
.executeUpdate();
// The class we're searching from is based on which parent type (e.g. Domain) we have
Class<? extends HistoryEntry> historyClass =
getHistoryClassFromParent(resource.getClass());
@@ -34,6 +34,7 @@ import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -111,6 +112,7 @@ public class ReplayCommitLogsToSqlActionTest {
DelegationSignerData.class,
DomainBase.class,
GracePeriod.class,
Lock.class,
PremiumList.class,
PremiumEntry.class,
RegistrarContact.class,
@@ -135,6 +137,7 @@ public class ReplayCommitLogsToSqlActionTest {
@BeforeEach
void beforeEach() {
inject.setStaticField(Ofy.class, "clock", fakeClock);
lenient().when(requestStatusChecker.getLogId()).thenReturn("requestLogId");
action.gcsUtils = gcsUtils;
action.response = response;
action.requestStatusChecker = requestStatusChecker;
@@ -464,9 +467,10 @@ public class ReplayCommitLogsToSqlActionTest {
}
});
runAndAssertSuccess(now.minusMinutes(1), 1, 1);
// jpaTm()::putIgnoringReadOnly should only have been called with the checkpoint
// jpaTm()::putIgnoringReadOnly should only have been called with the checkpoint and the lock
verify(spy, times(2)).putIgnoringReadOnly(any(SqlReplayCheckpoint.class));
verify(spy, times(2)).putIgnoringReadOnly(any());
verify(spy).putIgnoringReadOnly(any(Lock.class));
verify(spy, times(3)).putIgnoringReadOnly(any());
}
@Test
@@ -506,7 +510,7 @@ public class ReplayCommitLogsToSqlActionTest {
@Test
void testFailure_cannotAcquireLock() {
Truth8.assertThat(
Lock.acquire(
Lock.acquireSql(
ReplayCommitLogsToSqlAction.class.getSimpleName(),
null,
Duration.standardHours(1),
@@ -142,7 +142,7 @@ class SendExpiringCertificateNotificationEmailActionTest {
}
@TestOfyAndSql
void sendNotificationEmail_returnsTrue() throws Exception {
void sendNotificationEmail_techEMailAsRecipient_returnsTrue() throws Exception {
X509Certificate expiringCertificate =
SelfSignedCaCertificate.create(
"www.example.tld",
@@ -157,25 +157,64 @@ class SendExpiringCertificateNotificationEmailActionTest {
.asBuilder()
.setFailoverClientCertificate(cert.get(), clock.nowUtc())
.build());
ImmutableList<RegistrarContact> contacts =
ImmutableList.of(
new RegistrarContact.Builder()
.setParent(registrar)
.setName("Will Doe")
.setEmailAddress("will@example-registrar.tld")
.setPhoneNumber("+1.3105551213")
.setFaxNumber("+1.3105551213")
.setTypes(ImmutableSet.of(RegistrarContact.Type.TECH))
.setVisibleInWhoisAsAdmin(true)
.setVisibleInWhoisAsTech(false)
.build());
persistSimpleResources(contacts);
persistResource(registrar);
persistSampleContacts(registrar, Type.TECH);
assertThat(
action.sendNotificationEmail(registrar, START_OF_TIME, CertificateType.FAILOVER, cert))
.isEqualTo(true);
}
@TestOfyAndSql
void sendNotificationEmail_adminEMailAsRecipient_returnsTrue() throws Exception {
X509Certificate expiringCertificate =
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert();
Optional<String> cert =
Optional.of(certificateChecker.serializeCertificate(expiringCertificate));
Registrar registrar =
persistResource(
makeRegistrar1()
.asBuilder()
.setFailoverClientCertificate(cert.get(), clock.nowUtc())
.build());
persistSampleContacts(registrar, Type.ADMIN);
assertThat(
action.sendNotificationEmail(registrar, START_OF_TIME, CertificateType.FAILOVER, cert))
.isEqualTo(true);
}
@TestOfyAndSql
void sendNotificationEmail_returnsFalse_unsupportedEmailType() throws Exception {
Registrar registrar =
persistResource(
createRegistrar(
"testId",
"testName",
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert(),
null)
.build());
persistSampleContacts(registrar, Type.LEGAL);
assertThat(
action.sendNotificationEmail(
registrar,
START_OF_TIME,
CertificateType.FAILOVER,
Optional.of(
certificateChecker.serializeCertificate(
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert()))))
.isEqualTo(false);
}
@TestOfyAndSql
void sendNotificationEmail_returnsFalse_noEmailRecipients() throws Exception {
X509Certificate expiringCertificate =
@@ -247,93 +286,82 @@ class SendExpiringCertificateNotificationEmailActionTest {
}
@TestOfyAndSql
void sendNotificationEmails_allEmailsBeingAttemptedToSend() throws Exception {
X509Certificate expiringCertificate =
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert();
X509Certificate certificate =
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-10-01T00:00:00Z"))
.cert();
int numOfRegistrars = 10;
int numOfRegistrarsWithExpiringCertificates = 2;
for (int i = 1; i <= numOfRegistrarsWithExpiringCertificates; i++) {
persistResource(
createRegistrar("oldcert" + i, "name" + i, expiringCertificate, null).build());
void sendNotificationEmails_allEmailsBeingSent_onlyMainCertificates() throws Exception {
for (int i = 1; i <= 10; i++) {
Registrar registrar =
persistResource(
createRegistrar(
"oldcert" + i,
"name" + i,
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert(),
null)
.build());
persistSampleContacts(registrar, Type.TECH);
}
for (int i = numOfRegistrarsWithExpiringCertificates; i <= numOfRegistrars; i++) {
persistResource(createRegistrar("goodcert" + i, "name" + i, certificate, null).build());
}
assertThat(action.sendNotificationEmails()).isEqualTo(numOfRegistrarsWithExpiringCertificates);
assertThat(action.sendNotificationEmails()).isEqualTo(10);
}
@TestOfyAndSql
void sendNotificationEmails_allEmailsBeingAttemptedToSend_onlyMainCertificates()
throws Exception {
X509Certificate expiringCertificate =
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert();
int numOfRegistrars = 10;
for (int i = 1; i <= numOfRegistrars; i++) {
persistResource(
createRegistrar("oldcert" + i, "name" + i, expiringCertificate, null).build());
void sendNotificationEmails_allEmailsBeingSent_onlyFailOverCertificates() throws Exception {
for (int i = 1; i <= 10; i++) {
Registrar registrar =
persistResource(
createRegistrar(
"oldcert" + i,
"name" + i,
null,
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert())
.build());
persistSampleContacts(registrar, Type.TECH);
}
assertThat(action.sendNotificationEmails()).isEqualTo(numOfRegistrars);
assertThat(action.sendNotificationEmails()).isEqualTo(10);
}
@TestOfyAndSql
void sendNotificationEmails_allEmailsBeingAttemptedToSend_onlyFailOverCertificates()
throws Exception {
void sendNotificationEmails_allEmailsBeingSent_mixedOfCertificates() throws Exception {
X509Certificate expiringCertificate =
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert();
int numOfRegistrars = 10;
for (int i = 1; i <= numOfRegistrars; i++) {
persistResource(
createRegistrar("oldcert" + i, "name" + i, null, expiringCertificate).build());
}
assertThat(action.sendNotificationEmails()).isEqualTo(numOfRegistrars);
}
@TestOfyAndSql
void sendNotificationEmails_allEmailsBeingAttemptedToSend_mixedOfCertificates() throws Exception {
X509Certificate expiringCertificate =
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert();
int numOfRegistrars = 10;
int numOfExpiringFailOverOnly = 2;
int numOfExpiringPrimaryOnly = 3;
for (int i = 1; i <= numOfExpiringFailOverOnly; i++) {
persistResource(
createRegistrar("cl" + i, "expiringFailOverOnly" + i, null, expiringCertificate).build());
for (int i = 1; i <= 3; i++) {
Registrar registrar =
persistResource(
createRegistrar(
"cl" + i, "regWIthexpiringFailOverOnly" + i, null, expiringCertificate)
.build());
persistSampleContacts(registrar, Type.TECH);
}
for (int i = 1; i <= numOfExpiringPrimaryOnly; i++) {
persistResource(
createRegistrar("cli" + i, "expiringPrimaryOnly" + i, expiringCertificate, null).build());
for (int i = 1; i <= 5; i++) {
Registrar registrar =
persistResource(
createRegistrar(
"cli" + i, "regWithexpiringPrimaryOnly" + i, expiringCertificate, null)
.build());
persistSampleContacts(registrar, Type.TECH);
}
for (int i = numOfExpiringFailOverOnly + numOfExpiringPrimaryOnly + 1;
i <= numOfRegistrars;
i++) {
persistResource(
createRegistrar("client" + i, "regularReg" + i, expiringCertificate, expiringCertificate)
.build());
for (int i = 1; i <= 4; i++) {
Registrar registrar =
persistResource(
createRegistrar(
"client" + i,
"regWithTwoExpiring" + i,
expiringCertificate,
expiringCertificate)
.build());
persistSampleContacts(registrar, Type.ADMIN);
}
assertThat(action.sendNotificationEmails())
.isEqualTo(numOfRegistrars + numOfExpiringFailOverOnly + numOfExpiringPrimaryOnly);
assertThat(action.sendNotificationEmails()).isEqualTo(16);
}
@TestOfyAndSql
@@ -649,17 +677,19 @@ class SendExpiringCertificateNotificationEmailActionTest {
@TestOfyAndSql
void run_sentEmails_responseStatusIs200() throws Exception {
for (int i = 1; i <= 5; i++) {
persistResource(
createRegistrar(
"id_" + i,
"name" + i,
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert(),
null)
.build());
Registrar registrar =
persistResource(
createRegistrar(
"id_" + i,
"name" + i,
SelfSignedCaCertificate.create(
"www.example.tld",
DateTime.parse("2020-09-02T00:00:00Z"),
DateTime.parse("2021-06-01T00:00:00Z"))
.cert(),
null)
.build());
persistSampleContacts(registrar, Type.TECH);
}
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
@@ -667,7 +697,9 @@ class SendExpiringCertificateNotificationEmailActionTest {
.isEqualTo("Done. Sent 5 expiring certificate notification emails in total.");
}
/** Returns a sample registrar with a customized registrar name, client id and certificate* */
/**
* Returns a sample registrar builder with a customized registrar name, client id and certificate.
*/
private Registrar.Builder createRegistrar(
String registrarId,
String registrarName,
@@ -706,4 +738,27 @@ class SendExpiringCertificateNotificationEmailActionTest {
}
return builder;
}
/** Returns persisted sample contacts with a customized contact email type. */
private ImmutableList<RegistrarContact> persistSampleContacts(
Registrar registrar, RegistrarContact.Type emailType) {
return persistSimpleResources(
ImmutableList.of(
new RegistrarContact.Builder()
.setParent(registrar)
.setName("Will Doe")
.setEmailAddress("will@example-registrar.tld")
.setPhoneNumber("+1.0105551213")
.setFaxNumber("+1.0105551213")
.setTypes(ImmutableSet.of(emailType))
.build(),
new RegistrarContact.Builder()
.setParent(registrar)
.setName("Will Smith")
.setEmailAddress("will@test-registrar.tld")
.setPhoneNumber("+1.3105551213")
.setFaxNumber("+1.3105551213")
.setTypes(ImmutableSet.of(emailType))
.build()));
}
}
@@ -0,0 +1,174 @@
// Copyright 2021 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.beam.common;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import com.google.common.collect.ImmutableMap;
import com.google.common.truth.Truth;
import google.registry.beam.TestPipelineExtension;
import google.registry.beam.common.RegistryJpaIO.Read;
import google.registry.model.tld.Registry;
import google.registry.persistence.NomulusPostgreSql;
import google.registry.persistence.PersistenceModule;
import google.registry.persistence.transaction.CriteriaQueryBuilder;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.JpaTransactionManagerImpl;
import google.registry.persistence.transaction.TransactionManagerFactory;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
import javax.persistence.Persistence;
import org.apache.beam.sdk.testing.PAssert;
import org.apache.beam.sdk.values.PCollection;
import org.hibernate.cfg.Environment;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
/** Unit tests for {@link DatabaseSnapshot}. */
@Testcontainers
public class DatabaseSnapshotTest {
/**
* For reasons unknown, an EntityManagerFactory created by {@code JpaIntegrationTestExtension} or
* {@code JpaUnitTestExtension} enters a bad state after exporting the first snapshot. Starting
* with the second attempt, exports alternate between error ("cannot export a snapshot from a
* subtransaction") and success. The {@link #createSnapshot_twiceNoRead} test below fails with
* either extension. EntityManagerFactory created for production does not have this problem.
*/
@Container
private static PostgreSQLContainer sqlContainer =
new PostgreSQLContainer<>(NomulusPostgreSql.getDockerTag())
.withInitScript("sql/schema/nomulus.golden.sql");
@RegisterExtension
final transient TestPipelineExtension testPipeline =
TestPipelineExtension.create().enableAbandonedNodeEnforcement(true);
static JpaTransactionManager origJpa;
static JpaTransactionManager jpa;
static Registry registry;
@BeforeAll
static void setup() {
ImmutableMap<String, String> jpaProperties =
new ImmutableMap.Builder<String, String>()
.put(Environment.URL, sqlContainer.getJdbcUrl())
.put(Environment.USER, sqlContainer.getUsername())
.put(Environment.PASS, sqlContainer.getPassword())
.putAll(PersistenceModule.provideDefaultDatabaseConfigs())
.build();
jpa =
new JpaTransactionManagerImpl(
Persistence.createEntityManagerFactory("nomulus", jpaProperties), new FakeClock());
origJpa = jpaTm();
TransactionManagerFactory.setJpaTm(() -> jpa);
Registry tld = DatabaseHelper.newRegistry("tld", "TLD");
jpaTm().transact(() -> jpaTm().put(tld));
registry = jpaTm().transact(() -> jpaTm().loadByEntity(tld));
}
@AfterAll
static void tearDown() {
TransactionManagerFactory.setJpaTm(() -> origJpa);
if (jpa != null) {
jpa.teardown();
}
}
@Test
void createSnapshot_onceNoRead() {
try (DatabaseSnapshot databaseSnapshot = DatabaseSnapshot.createSnapshot()) {}
}
@Test
void createSnapshot_twiceNoRead() {
try (DatabaseSnapshot databaseSnapshot = DatabaseSnapshot.createSnapshot()) {}
try (DatabaseSnapshot databaseSnapshot = DatabaseSnapshot.createSnapshot()) {}
}
@Test
void readSnapshot() {
try (DatabaseSnapshot databaseSnapshot = DatabaseSnapshot.createSnapshot()) {
Registry snapshotRegistry =
jpaTm()
.transact(
() ->
jpaTm()
.setDatabaseSnapshot(databaseSnapshot.getSnapshotId())
.loadByEntity(registry));
Truth.assertThat(snapshotRegistry).isEqualTo(registry);
}
}
@Test
void readSnapshot_withSubsequentChange() {
try (DatabaseSnapshot databaseSnapshot = DatabaseSnapshot.createSnapshot()) {
Registry updated =
registry
.asBuilder()
.setCreateBillingCost(registry.getStandardCreateCost().plus(1))
.build();
jpaTm().transact(() -> jpaTm().put(updated));
Registry persistedUpdate = jpaTm().transact(() -> jpaTm().loadByEntity(registry));
Truth.assertThat(persistedUpdate).isNotEqualTo(registry);
Registry snapshotRegistry =
jpaTm()
.transact(
() ->
jpaTm()
.setDatabaseSnapshot(databaseSnapshot.getSnapshotId())
.loadByEntity(registry));
Truth.assertThat(snapshotRegistry).isEqualTo(registry);
} finally {
// Revert change to registry in DB, which is shared by all test methods.
jpaTm().transact(() -> jpaTm().put(registry));
}
}
@Test
void readWithRegistryJpaIO() {
try (DatabaseSnapshot databaseSnapshot = DatabaseSnapshot.createSnapshot()) {
Registry updated =
registry
.asBuilder()
.setCreateBillingCost(registry.getStandardCreateCost().plus(1))
.build();
jpaTm().transact(() -> jpaTm().put(updated));
Read<Registry, Registry> read =
RegistryJpaIO.read(() -> CriteriaQueryBuilder.create(Registry.class).build(), x -> x)
.withSnapshot(databaseSnapshot.getSnapshotId());
PCollection<Registry> registries = testPipeline.apply(read);
// This assertion depends on Registry being Serializable, which may change if the
// UnsafeSerializable interface is removed after migration.
PAssert.that(registries).containsInAnyOrder(registry);
testPipeline.run();
} finally {
// Revert change to registry in DB, which is shared by all test methods.
jpaTm().transact(() -> jpaTm().put(registry));
}
}
}
@@ -19,6 +19,7 @@ import static google.registry.beam.common.RegistryPipelineOptions.validateRegist
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.config.RegistryEnvironment;
import google.registry.persistence.PersistenceModule.JpaTransactionManagerType;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.testing.SystemPropertyExtension;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
@@ -123,4 +124,37 @@ class RegistryPipelineOptionsTest {
validateRegistryPipelineOptions(options);
assertThat(options.getProject()).isEqualTo("some-project");
}
@Test
void jpaTransactionManagerType_default() {
RegistryPipelineOptions options =
PipelineOptionsFactory.fromArgs(
"--registryEnvironment=" + RegistryEnvironment.UNITTEST.name())
.withValidation()
.as(RegistryPipelineOptions.class);
assertThat(options.getJpaTransactionManagerType()).isEqualTo(JpaTransactionManagerType.REGULAR);
}
@Test
void jpaTransactionManagerType_regularJpa() {
RegistryPipelineOptions options =
PipelineOptionsFactory.fromArgs(
"--registryEnvironment=" + RegistryEnvironment.UNITTEST.name(),
"--jpaTransactionManagerType=REGULAR")
.withValidation()
.as(RegistryPipelineOptions.class);
assertThat(options.getJpaTransactionManagerType()).isEqualTo(JpaTransactionManagerType.REGULAR);
}
@Test
void jpaTransactionManagerType_bulkQueryJpa() {
RegistryPipelineOptions options =
PipelineOptionsFactory.fromArgs(
"--registryEnvironment=" + RegistryEnvironment.UNITTEST.name(),
"--jpaTransactionManagerType=BULK_QUERY")
.withValidation()
.as(RegistryPipelineOptions.class);
assertThat(options.getJpaTransactionManagerType())
.isEqualTo(JpaTransactionManagerType.BULK_QUERY);
}
}
@@ -106,7 +106,7 @@ class InitSqlPipelineTest {
@RegisterExtension
@Order(Order.DEFAULT - 1)
final transient DatastoreEntityExtension datastore =
new DatastoreEntityExtension("test").allThreads(true);
new DatastoreEntityExtension().allThreads(true);
@RegisterExtension final transient InjectExtension injectExtension = new InjectExtension();
@@ -838,14 +838,19 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
CommitMode.LIVE, UserPrivileges.SUPERUSER, loadFile("domain_delete_response_pending.xml"));
HistoryEntry deleteHistoryEntry = getOnlyHistoryEntryOfType(domain, DOMAIN_DELETE);
DateTime now = clock.nowUtc();
assertPollMessages(
new PollMessage.OneTime.Builder()
.setRegistrarId("TheRegistrar")
.setParent(deleteHistoryEntry)
.setEventTime(clock.nowUtc())
.setEventTime(now)
.setMsg(
"Domain example.tld was deleted by registry administrator with final deletion"
+ " effective: 2000-07-11T22:00:00.013Z")
.setResponseData(
ImmutableList.of(
DomainPendingActionNotificationResponse.create(
"example.tld", true, deleteHistoryEntry.getTrid(), now)))
.build(),
new PollMessage.OneTime.Builder()
.setRegistrarId("TheRegistrar")
@@ -156,6 +156,30 @@ class PollRequestFlowTest extends FlowTestCase<PollRequestFlow> {
runFlowAssertResponse(loadFile("poll_response_domain_pending_notification.xml"));
}
@TestOfyAndSql
void testSuccess_domainPendingActionImmediateDelete() throws Exception {
persistResource(
new PollMessage.OneTime.Builder()
.setRegistrarId(getRegistrarIdForFlow())
.setEventTime(clock.nowUtc())
.setMsg(
String.format(
"Domain %s was deleted by registry administrator with final deletion"
+ " effective: %s",
domain.getDomainName(), clock.nowUtc().minusMinutes(5)))
.setResponseData(
ImmutableList.of(
DomainPendingActionNotificationResponse.create(
domain.getDomainName(),
true,
Trid.create("ABC-12345", "other-trid"),
clock.nowUtc())))
.setParent(createHistoryEntryForEppResource(domain))
.build());
assertTransactionalFlow(false);
runFlowAssertResponse(loadFile("poll_message_domain_pending_action_immediate_delete.xml"));
}
@TestOfyAndSql
void testSuccess_domainAutorenewMessage() throws Exception {
persistResource(
@@ -412,6 +412,10 @@ public final class ImmutableObjectSubject extends Subject {
// don't use ImmutableMap or a stream->collect model since we can have nulls
Map<Field, Object> result = new LinkedHashMap<>();
for (Map.Entry<Field, Object> entry : originalFields.entrySet()) {
// TODO(b/203685960): filter by @DoNotCompare instead.
if (entry.getKey().isAnnotationPresent(ImmutableObject.Insignificant.class)) {
continue;
}
if (!ignoredFieldSet.contains(entry.getKey().getName())) {
result.put(entry.getKey(), entry.getValue());
}
@@ -426,7 +430,9 @@ public final class ImmutableObjectSubject extends Subject {
// don't use ImmutableMap or a stream->collect model since we can have nulls
Map<Field, Object> result = new LinkedHashMap<>();
for (Map.Entry<Field, Object> entry : originalFields.entrySet()) {
if (!entry.getKey().isAnnotationPresent(annotation)) {
// TODO(b/203685960): filter by @DoNotCompare instead.
if (!entry.getKey().isAnnotationPresent(annotation)
&& !entry.getKey().isAnnotationPresent(ImmutableObject.Insignificant.class)) {
// Perform any necessary substitutions.
if (entry.getKey().isAnnotationPresent(ImmutableObject.EmptySetToNull.class)
@@ -25,6 +25,7 @@ import static google.registry.testing.DatabaseHelper.loadByKey;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.SerializeUtils.serializeDeserialize;
import static org.joda.money.CurrencyUnit.USD;
import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -46,6 +47,7 @@ import google.registry.persistence.VKey;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import google.registry.testing.TestSqlOnly;
import google.registry.util.DateTimeUtils;
import org.joda.money.Money;
import org.joda.time.DateTime;
@@ -195,6 +197,20 @@ public class BillingEventTest extends EntityTestCase {
ofyTmOrDoNothing(() -> assertThat(tm().loadByEntity(modification)).isEqualTo(modification));
}
@TestSqlOnly
void testSerializable() {
BillingEvent persisted = loadByEntity(oneTime);
assertThat(serializeDeserialize(persisted)).isEqualTo(persisted);
persisted = loadByEntity(oneTimeSynthetic);
assertThat(serializeDeserialize(persisted)).isEqualTo(persisted);
persisted = loadByEntity(recurring);
assertThat(serializeDeserialize(persisted)).isEqualTo(persisted);
persisted = loadByEntity(cancellationOneTime);
assertThat(serializeDeserialize(persisted)).isEqualTo(persisted);
persisted = loadByEntity(cancellationRecurring);
assertThat(serializeDeserialize(persisted)).isEqualTo(persisted);
}
@TestOfyOnly
void testParenting() {
// Note that these are all tested separately because BillingEvent is an abstract base class that
@@ -0,0 +1,87 @@
// Copyright 2021 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.model.bulkquery;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.GracePeriod.GracePeriodHistory;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.domain.secdns.DomainDsDataHistory;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.persistence.VKey;
/**
* Helpers for bulk-loading {@link google.registry.model.domain.DomainBase} and {@link
* google.registry.model.domain.DomainHistory} entities in <em>tests</em>.
*/
public class BulkQueryHelper {
static DomainBase loadAndAssembleDomainBase(String domainRepoId) {
return jpaTm()
.transact(
() ->
BulkQueryEntities.assembleDomainBase(
jpaTm().loadByKey(DomainBaseLite.createVKey(domainRepoId)),
jpaTm()
.loadAllOfStream(GracePeriod.class)
.filter(gracePeriod -> gracePeriod.getDomainRepoId().equals(domainRepoId))
.collect(toImmutableSet()),
jpaTm()
.loadAllOfStream(DelegationSignerData.class)
.filter(dsData -> dsData.getDomainRepoId().equals(domainRepoId))
.collect(toImmutableSet()),
jpaTm()
.loadAllOfStream(DomainHost.class)
.filter(domainHost -> domainHost.getDomainRepoId().equals(domainRepoId))
.map(DomainHost::getHostVKey)
.collect(toImmutableSet())));
}
static DomainHistory loadAndAssembleDomainHistory(DomainHistoryId domainHistoryId) {
return jpaTm()
.transact(
() ->
BulkQueryEntities.assembleDomainHistory(
jpaTm().loadByKey(VKey.createSql(DomainHistoryLite.class, domainHistoryId)),
jpaTm()
.loadAllOfStream(DomainDsDataHistory.class)
.filter(
domainDsDataHistory ->
domainDsDataHistory.getDomainHistoryId().equals(domainHistoryId))
.collect(toImmutableSet()),
jpaTm()
.loadAllOfStream(DomainHistoryHost.class)
.filter(
domainHistoryHost ->
domainHistoryHost.getDomainHistoryId().equals(domainHistoryId))
.map(DomainHistoryHost::getHostVKey)
.collect(toImmutableSet()),
jpaTm()
.loadAllOfStream(GracePeriodHistory.class)
.filter(
gracePeriodHistory ->
gracePeriodHistory.getDomainHistoryId().equals(domainHistoryId))
.collect(toImmutableSet()),
jpaTm()
.loadAllOfStream(DomainTransactionRecord.class)
.filter(x -> true)
.collect(toImmutableSet())));
}
}
@@ -0,0 +1,117 @@
// Copyright 2021 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.model.bulkquery;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static org.joda.time.DateTimeZone.UTC;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.google.common.truth.Truth8;
import google.registry.model.domain.DomainBase;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeClock;
import java.util.Set;
import java.util.stream.Collectors;
import javax.persistence.metamodel.Attribute;
import org.joda.time.DateTime;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for reading {@link DomainBaseLite}. */
class DomainBaseLiteTest {
protected FakeClock fakeClock = new FakeClock(DateTime.now(UTC));
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().withClock(fakeClock).build();
private final TestSetupHelper setupHelper = new TestSetupHelper(fakeClock);
@BeforeEach
void setUp() {
setupHelper.initializeAllEntities();
}
@AfterEach
void afterEach() {
setupHelper.tearDownBulkQueryJpaTm();
}
@Test
void readDomainHost() {
setupHelper.applyChangeToDomainAndHistory();
setupHelper.setupBulkQueryJpaTm(appEngine);
Truth8.assertThat(
jpaTm().transact(() -> jpaTm().loadAllOf(DomainHost.class)).stream()
.map(DomainHost::getHostVKey))
.containsExactly(setupHelper.host.createVKey());
}
@Test
void domainBaseLiteAttributes_versusDomainBase() {
Set<String> domainBaseAttributes =
jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.getMetamodel()
.entity(DomainBase.class)
.getAttributes())
.stream()
.map(Attribute::getName)
.collect(Collectors.toSet());
setupHelper.setupBulkQueryJpaTm(appEngine);
Set<String> domainBaseLiteAttributes =
jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.getMetamodel()
.entity(DomainBaseLite.class)
.getAttributes())
.stream()
.map(Attribute::getName)
.collect(Collectors.toSet());
assertThat(domainBaseAttributes).containsAtLeastElementsIn(domainBaseLiteAttributes);
SetView<?> excludedFromDomainBase =
Sets.difference(domainBaseAttributes, domainBaseLiteAttributes);
assertThat(excludedFromDomainBase)
.containsExactly("internalDelegationSignerData", "internalGracePeriods", "nsHosts");
}
@Test
void readDomainBaseLite_simple() {
setupHelper.setupBulkQueryJpaTm(appEngine);
assertThat(BulkQueryHelper.loadAndAssembleDomainBase(TestSetupHelper.DOMAIN_REPO_ID))
.isEqualTo(setupHelper.domain);
}
@Test
void readDomainBaseLite_full() {
setupHelper.applyChangeToDomainAndHistory();
setupHelper.setupBulkQueryJpaTm(appEngine);
assertThat(BulkQueryHelper.loadAndAssembleDomainBase(TestSetupHelper.DOMAIN_REPO_ID))
.isEqualTo(setupHelper.domain);
}
}
@@ -0,0 +1,125 @@
// Copyright 2021 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.model.bulkquery;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static org.joda.time.DateTimeZone.UTC;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.google.common.truth.Truth8;
import google.registry.model.domain.DomainHistory;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeClock;
import java.util.Set;
import java.util.stream.Collectors;
import javax.persistence.metamodel.Attribute;
import org.joda.time.DateTime;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link DomainHistoryLite}. */
public class DomainHistoryLiteTest {
protected FakeClock fakeClock = new FakeClock(DateTime.now(UTC));
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().withClock(fakeClock).build();
private final TestSetupHelper setupHelper = new TestSetupHelper(fakeClock);
@BeforeEach
void setUp() {
setupHelper.initializeAllEntities();
}
@AfterEach
void afterEach() {
setupHelper.tearDownBulkQueryJpaTm();
}
@Test
void readDomainHistoryHost() {
setupHelper.applyChangeToDomainAndHistory();
setupHelper.setupBulkQueryJpaTm(appEngine);
Truth8.assertThat(
jpaTm().transact(() -> jpaTm().loadAllOf(DomainHistoryHost.class)).stream()
.map(DomainHistoryHost::getHostVKey))
.containsExactly(setupHelper.host.createVKey());
}
@Test
void domainHistoryLiteAttributes_versusDomainHistory() {
Set<String> domainHistoryAttributes =
jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.getMetamodel()
.entity(DomainHistory.class)
.getAttributes())
.stream()
.map(Attribute::getName)
.collect(Collectors.toSet());
setupHelper.setupBulkQueryJpaTm(appEngine);
Set<String> domainHistoryLiteAttributes =
jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.getMetamodel()
.entity(DomainHistoryLite.class)
.getAttributes())
.stream()
.map(Attribute::getName)
.collect(Collectors.toSet());
assertThat(domainHistoryAttributes).containsAtLeastElementsIn(domainHistoryLiteAttributes);
SetView<?> excludedFromDomainHistory =
Sets.difference(domainHistoryAttributes, domainHistoryLiteAttributes);
assertThat(excludedFromDomainHistory)
.containsExactly(
"dsDataHistories",
"gracePeriodHistories",
"internalDomainTransactionRecords",
"nsHosts");
}
@Test
void readDomainHistory_noContent() {
setupHelper.setupBulkQueryJpaTm(appEngine);
assertThat(
BulkQueryHelper.loadAndAssembleDomainHistory(
setupHelper.domainHistory.getDomainHistoryId()))
.isEqualTo(setupHelper.domainHistory);
}
@Test
void readDomainHistory_full() {
setupHelper.applyChangeToDomainAndHistory();
setupHelper.setupBulkQueryJpaTm(appEngine);
assertThat(
BulkQueryHelper.loadAndAssembleDomainHistory(
setupHelper.domainHistory.getDomainHistoryId()))
.isEqualTo(setupHelper.domainHistory);
}
}
@@ -0,0 +1,210 @@
// Copyright 2021 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.model.bulkquery;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.testing.SqlHelper.saveRegistrar;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableSet;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.Period;
import google.registry.model.domain.launch.LaunchNotice;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.HostResource;
import google.registry.model.registrar.Registrar;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tld.Registry;
import google.registry.model.transfer.ContactTransferData;
import google.registry.persistence.BulkQueryJpaFactory;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.TransactionManagerFactory;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
/** Entity creation utilities for domain-related tests. */
class TestSetupHelper {
public static final String TLD = "tld";
public static final String DOMAIN_REPO_ID = "4-TLD";
public static final String DOMAIN_NAME = "example.tld";
public static final String REGISTRAR_ID = "AnRegistrar";
private final FakeClock fakeClock;
Registry registry;
Registrar registrar;
ContactResource contact;
DomainBase domain;
DomainHistory domainHistory;
HostResource host;
private JpaTransactionManager originalJpaTm;
private JpaTransactionManager bulkQueryJpaTm;
TestSetupHelper(FakeClock fakeClock) {
this.fakeClock = fakeClock;
}
void initializeAllEntities() {
registry = putInDb(DatabaseHelper.newRegistry(TLD, Ascii.toUpperCase(TLD)));
registrar = saveRegistrar(REGISTRAR_ID);
contact = putInDb(createContact(DOMAIN_REPO_ID, REGISTRAR_ID));
domain = putInDb(createSimpleDomain(contact));
domainHistory = putInDb(createHistoryWithoutContent(domain, fakeClock));
host = putInDb(createHost());
}
void applyChangeToDomainAndHistory() {
domain = putInDb(createFullDomain(contact, host, fakeClock));
domainHistory = putInDb(createFullHistory(domain, fakeClock));
}
void setupBulkQueryJpaTm(AppEngineExtension appEngineExtension) {
bulkQueryJpaTm =
BulkQueryJpaFactory.createBulkQueryJpaTransactionManager(
appEngineExtension
.getJpaIntegrationTestExtension()
.map(JpaIntegrationTestExtension::getJpaProperties)
.orElseThrow(
() -> new IllegalStateException("Expecting JpaIntegrationTestExtension.")),
fakeClock);
originalJpaTm = TransactionManagerFactory.jpaTm();
TransactionManagerFactory.setJpaTm(() -> bulkQueryJpaTm);
}
void tearDownBulkQueryJpaTm() {
if (bulkQueryJpaTm != null) {
bulkQueryJpaTm.teardown();
TransactionManagerFactory.setJpaTm(() -> originalJpaTm);
}
}
static ContactResource createContact(String repoId, String registrarId) {
return new ContactResource.Builder()
.setRepoId(repoId)
.setCreationRegistrarId(registrarId)
.setTransferData(new ContactTransferData.Builder().build())
.setPersistedCurrentSponsorRegistrarId(registrarId)
.build();
}
static DomainBase createSimpleDomain(ContactResource contact) {
return DatabaseHelper.newDomainBase(DOMAIN_NAME, DOMAIN_REPO_ID, contact)
.asBuilder()
.setCreationRegistrarId(REGISTRAR_ID)
.setPersistedCurrentSponsorRegistrarId(REGISTRAR_ID)
.build();
}
static DomainBase createFullDomain(
ContactResource contact, HostResource host, FakeClock fakeClock) {
return createSimpleDomain(contact)
.asBuilder()
.setDomainName(DOMAIN_NAME)
.setRepoId(DOMAIN_REPO_ID)
.setCreationRegistrarId(REGISTRAR_ID)
.setLastEppUpdateTime(fakeClock.nowUtc())
.setLastEppUpdateRegistrarId(REGISTRAR_ID)
.setLastTransferTime(fakeClock.nowUtc())
.setNameservers(host.createVKey())
.setStatusValues(
ImmutableSet.of(
StatusValue.CLIENT_DELETE_PROHIBITED,
StatusValue.SERVER_DELETE_PROHIBITED,
StatusValue.SERVER_TRANSFER_PROHIBITED,
StatusValue.SERVER_UPDATE_PROHIBITED,
StatusValue.SERVER_RENEW_PROHIBITED,
StatusValue.SERVER_HOLD))
.setContacts(
ImmutableSet.of(
DesignatedContact.create(DesignatedContact.Type.ADMIN, contact.createVKey())))
.setSubordinateHosts(ImmutableSet.of("ns1.example.com"))
.setPersistedCurrentSponsorRegistrarId(REGISTRAR_ID)
.setRegistrationExpirationTime(fakeClock.nowUtc().plusYears(1))
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("password")))
.setDsData(ImmutableSet.of(DelegationSignerData.create(1, 2, 3, new byte[] {0, 1, 2})))
.setLaunchNotice(LaunchNotice.create("tcnid", "validatorId", START_OF_TIME, START_OF_TIME))
.setSmdId("smdid")
.addGracePeriod(
GracePeriod.create(
GracePeriodStatus.ADD, DOMAIN_REPO_ID, END_OF_TIME, REGISTRAR_ID, null, 100L))
.build();
}
static HostResource createHost() {
return new HostResource.Builder()
.setRepoId("host1")
.setHostName("ns1.example.com")
.setCreationRegistrarId(REGISTRAR_ID)
.setPersistedCurrentSponsorRegistrarId(REGISTRAR_ID)
.build();
}
static DomainTransactionRecord createDomainTransactionRecord(FakeClock fakeClock) {
return new DomainTransactionRecord.Builder()
.setTld(TLD)
.setReportingTime(fakeClock.nowUtc())
.setReportField(TransactionReportField.NET_ADDS_1_YR)
.setReportAmount(1)
.build();
}
static DomainHistory createHistoryWithoutContent(DomainBase domain, FakeClock fakeClock) {
return new DomainHistory.Builder()
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setXmlBytes("<xml></xml>".getBytes(UTF_8))
.setModificationTime(fakeClock.nowUtc())
.setRegistrarId(REGISTRAR_ID)
.setTrid(Trid.create("ABC-123", "server-trid"))
.setBySuperuser(false)
.setReason("reason")
.setRequestedByRegistrar(true)
.setDomainRepoId(domain.getRepoId())
.setOtherRegistrarId("otherClient")
.setPeriod(Period.create(1, Period.Unit.YEARS))
.build();
}
static DomainHistory createFullHistory(DomainBase domain, FakeClock fakeClock) {
return createHistoryWithoutContent(domain, fakeClock)
.asBuilder()
.setType(HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE)
.setDomain(domain)
.setDomainTransactionRecords(ImmutableSet.of(createDomainTransactionRecord(fakeClock)))
.build();
}
static <T> T putInDb(T entity) {
jpaTm().transact(() -> jpaTm().put(entity));
return jpaTm().transact(() -> jpaTm().loadByEntity(entity));
}
}
@@ -30,6 +30,8 @@ import google.registry.model.domain.DomainBase;
import google.registry.model.tld.Registry;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestSqlOnly;
import google.registry.util.SerializeUtils;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -46,6 +48,15 @@ public class CursorTest extends EntityTestCase {
fakeClock.setTo(DateTime.parse("2010-10-17TZ"));
}
@TestSqlOnly
void testSerializable() {
final DateTime time = DateTime.parse("2012-07-12T03:30:00.000Z");
tm().transact(() -> tm().put(Cursor.createGlobal(RECURRING_BILLING, time)));
Cursor persisted =
tm().transact(() -> tm().loadByKey(Cursor.createGlobalVKey(RECURRING_BILLING)));
assertThat(SerializeUtils.serializeDeserialize(persisted)).isEqualTo(persisted);
}
@TestOfyAndSql
void testSuccess_persistScopedCursor() {
createTld("tld");
@@ -48,6 +48,7 @@ import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import google.registry.testing.TestSqlOnly;
import google.registry.util.SerializeUtils;
import org.junit.jupiter.api.BeforeEach;
/** Unit tests for {@link ContactResource}. */
@@ -174,6 +175,14 @@ public class ContactResourceTest extends EntityTestCase {
.hasValue(contactResource);
}
@TestSqlOnly
void testSerializable() {
ContactResource persisted =
loadByForeignKey(ContactResource.class, contactResource.getForeignKey(), fakeClock.nowUtc())
.get();
assertThat(SerializeUtils.serializeDeserialize(persisted)).isEqualTo(persisted);
}
@TestOfyOnly
void testIndexing() throws Exception {
verifyDatastoreIndexing(
@@ -56,6 +56,7 @@ import google.registry.testing.AppEngineExtension;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.TestSqlOnly;
import google.registry.util.SerializeUtils;
import java.util.Arrays;
import org.joda.money.Money;
import org.joda.time.DateTime;
@@ -354,6 +355,14 @@ public class DomainBaseSqlTest {
});
}
@TestSqlOnly
void testSerializable() {
createTld("com");
insertInDb(contact, contact2, domain, host);
DomainBase persisted = jpaTm().transact(() -> jpaTm().loadByEntity(domain));
assertThat(SerializeUtils.serializeDeserialize(persisted)).isEqualTo(persisted);
}
@TestSqlOnly
void testUpdates() {
createTld("com");
@@ -921,6 +921,19 @@ public class DomainBaseTest extends EntityTestCase {
.containsExactly(EppResourceIndex.create(Key.create(domain)));
}
@Test
void testBeforeSqlSaveOnReplay_canonicalName() {
domain.fullyQualifiedDomainName = "EXAMPLE.COM";
assertThat(domain.getDomainName()).isEqualTo("EXAMPLE.COM");
domain.beforeSqlSaveOnReplay();
assertThat(domain.getDomainName()).isEqualTo("example.com");
domain.fullyQualifiedDomainName = "kittyçat.com";
assertThat(domain.getDomainName()).isEqualTo("kittyçat.com");
domain.beforeSqlSaveOnReplay();
assertThat(domain.getDomainName()).isEqualTo("xn--kittyat-yxa.com");
}
static class BillEventInfo extends ImmutableObject {
VKey<BillingEvent.Recurring> billingEventRecurring;
Long billingEventRecurringHistoryId;
@@ -41,6 +41,8 @@ import google.registry.model.reporting.HistoryEntry;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import google.registry.testing.TestSqlOnly;
import google.registry.util.SerializeUtils;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -93,6 +95,44 @@ public class AllocationTokenTest extends EntityTestCase {
assertThat(loadByEntity(singleUseToken)).isEqualTo(singleUseToken);
}
@TestSqlOnly
void testSerializable() {
AllocationToken unlimitedUseToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123Unlimited")
.setTokenType(UNLIMITED_USE)
.setCreationTimeForTest(DateTime.parse("2010-11-12T05:00:00Z"))
.setAllowedTlds(ImmutableSet.of("dev", "app"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar, NewRegistrar"))
.setDiscountFraction(0.5)
.setDiscountPremiums(true)
.setDiscountYears(3)
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
.put(START_OF_TIME, NOT_STARTED)
.put(DateTime.now(UTC), TokenStatus.VALID)
.put(DateTime.now(UTC).plusWeeks(8), TokenStatus.ENDED)
.build())
.build());
AllocationToken persisted = loadByEntity(unlimitedUseToken);
assertThat(SerializeUtils.serializeDeserialize(persisted)).isEqualTo(persisted);
DomainBase domain = persistActiveDomain("example.foo");
Key<HistoryEntry> historyEntryKey = Key.create(Key.create(domain), HistoryEntry.class, 1);
AllocationToken singleUseToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123Single")
.setRedemptionHistoryEntry(HistoryEntry.createVKey(historyEntryKey))
.setDomainName("example.foo")
.setCreationTimeForTest(DateTime.parse("2010-11-12T05:00:00Z"))
.setTokenType(SINGLE_USE)
.build());
persisted = loadByEntity(singleUseToken);
assertThat(SerializeUtils.serializeDeserialize(persisted)).isEqualTo(persisted);
}
@TestOfyOnly
void testIndexing() throws Exception {
DomainBase domain = persistActiveDomain("blahdomain.foo");
@@ -39,6 +39,7 @@ import google.registry.persistence.VKey;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyOnly;
import google.registry.testing.TestSqlOnly;
import google.registry.util.SerializeUtils;
/** Tests for {@link ContactHistory}. */
@DualDatabaseTest
@@ -64,6 +65,18 @@ public class ContactHistoryTest extends EntityTestCase {
});
}
@TestSqlOnly
void testSerializable() {
ContactResource contact = newContactResourceWithRoid("contactId", "contact1");
insertInDb(contact);
ContactResource contactFromDb = loadByEntity(contact);
ContactHistory contactHistory = createContactHistory(contactFromDb);
insertInDb(contactHistory);
ContactHistory fromDatabase =
jpaTm().transact(() -> jpaTm().loadByKey(contactHistory.createVKey()));
assertThat(SerializeUtils.serializeDeserialize(fromDatabase)).isEqualTo(fromDatabase);
}
@TestSqlOnly
void testLegacyPersistence_nullContactBase() {
ContactResource contact = newContactResourceWithRoid("contactId", "contact1");
@@ -55,6 +55,8 @@ import google.registry.testing.DatabaseHelper;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyOnly;
import google.registry.testing.TestSqlOnly;
import google.registry.util.SerializeUtils;
import java.lang.reflect.Field;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -87,6 +89,16 @@ public class DomainHistoryTest extends EntityTestCase {
});
}
@TestSqlOnly
void testSerializable() {
DomainBase domain = addGracePeriodForSql(createDomainWithContactsAndHosts());
DomainHistory domainHistory = createDomainHistory(domain);
insertInDb(domainHistory);
DomainHistory fromDatabase =
jpaTm().transact(() -> jpaTm().loadByKey(domainHistory.createVKey()));
assertThat(SerializeUtils.serializeDeserialize(fromDatabase)).isEqualTo(fromDatabase);
}
@TestSqlOnly
void testLegacyPersistence_nullResource() {
DomainBase domain = addGracePeriodForSql(createDomainWithContactsAndHosts());
@@ -125,8 +137,7 @@ public class DomainHistoryTest extends EntityTestCase {
DomainHistory domainHistory = createDomainHistory(domain);
tm().transact(() -> tm().insert(domainHistory));
// retrieving a HistoryEntry or a DomainHistory with the same key should return the same
// object
// retrieving a HistoryEntry or a DomainHistory with the same key should return the same object
// note: due to the @EntitySubclass annotation. all Keys for DomainHistory objects will have
// type HistoryEntry
VKey<DomainHistory> domainHistoryVKey = domainHistory.createVKey();
@@ -221,6 +232,66 @@ public class DomainHistoryTest extends EntityTestCase {
jpaTm().loadByEntity(historyWithoutResource).getDomainContent().get()));
}
@TestSqlOnly
void testBeforeSqlSave_canonicalNameUncapitalized() throws Exception {
Field domainNameField = DomainContent.class.getDeclaredField("fullyQualifiedDomainName");
// reflection hacks to get around visibility issues
domainNameField.setAccessible(true);
DomainBase domain = createDomainWithContactsAndHosts();
domainNameField.set(domain, "EXAMPLE.TLD");
DomainHistory historyWithoutResource =
new DomainHistory.Builder()
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setXmlBytes("<xml></xml>".getBytes(UTF_8))
.setModificationTime(fakeClock.nowUtc())
.setRegistrarId("TheRegistrar")
.setTrid(Trid.create("ABC-123", "server-trid"))
.setBySuperuser(false)
.setReason("reason")
.setRequestedByRegistrar(true)
.setDomainRepoId(domain.getRepoId())
.setOtherRegistrarId("otherClient")
.setPeriod(Period.create(1, Period.Unit.YEARS))
.build();
DatabaseHelper.putInDb(domain, historyWithoutResource);
jpaTm().transact(historyWithoutResource::beforeSqlSaveOnReplay);
assertThat(historyWithoutResource.getDomainContent().get().getDomainName())
.isEqualTo("example.tld");
}
@TestSqlOnly
void testBeforeSqlSave_canonicalNameUtf8() throws Exception {
Field domainNameField = DomainContent.class.getDeclaredField("fullyQualifiedDomainName");
// reflection hacks to get around visibility issues
domainNameField.setAccessible(true);
DomainBase domain = createDomainWithContactsAndHosts();
domainNameField.set(domain, "kittyçat.tld");
DomainHistory historyWithoutResource =
new DomainHistory.Builder()
.setType(HistoryEntry.Type.DOMAIN_CREATE)
.setXmlBytes("<xml></xml>".getBytes(UTF_8))
.setModificationTime(fakeClock.nowUtc())
.setRegistrarId("TheRegistrar")
.setTrid(Trid.create("ABC-123", "server-trid"))
.setBySuperuser(false)
.setReason("reason")
.setRequestedByRegistrar(true)
.setDomainRepoId(domain.getRepoId())
.setOtherRegistrarId("otherClient")
.setPeriod(Period.create(1, Period.Unit.YEARS))
.build();
DatabaseHelper.putInDb(domain, historyWithoutResource);
jpaTm().transact(historyWithoutResource::beforeSqlSaveOnReplay);
assertThat(historyWithoutResource.getDomainContent().get().getDomainName())
.isEqualTo("xn--kittyat-yxa.tld");
}
static DomainBase createDomainWithContactsAndHosts() {
createTld("tld");
HostResource host = newHostResourceWithRoid("ns1.example.com", "host1");
@@ -33,9 +33,12 @@ import google.registry.model.host.HostHistory;
import google.registry.model.host.HostResource;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyOnly;
import google.registry.testing.TestSqlOnly;
import google.registry.util.SerializeUtils;
import java.lang.reflect.Field;
/** Tests for {@link HostHistory}. */
@DualDatabaseTest
@@ -61,6 +64,17 @@ public class HostHistoryTest extends EntityTestCase {
});
}
@TestSqlOnly
void testSerializable() {
HostResource host = newHostResourceWithRoid("ns1.example.com", "host1");
insertInDb(host);
HostResource hostFromDb = loadByEntity(host);
HostHistory hostHistory = createHostHistory(hostFromDb);
insertInDb(hostHistory);
HostHistory fromDatabase = jpaTm().transact(() -> jpaTm().loadByKey(hostHistory.createVKey()));
assertThat(SerializeUtils.serializeDeserialize(fromDatabase)).isEqualTo(fromDatabase);
}
@TestSqlOnly
void testLegacyPersistence_nullHostBase() {
HostResource host = newHostResourceWithRoid("ns1.example.com", "host1");
@@ -134,6 +148,58 @@ public class HostHistoryTest extends EntityTestCase {
.hasFieldsEqualTo(jpaTm().loadByEntity(hostHistory).getHostBase().get()));
}
@TestSqlOnly
void testBeforeSqlSave_canonicalNameUncapitalized() throws Exception {
Field hostNameField = HostBase.class.getDeclaredField("fullyQualifiedHostName");
// reflection hacks to get around visibility issues
hostNameField.setAccessible(true);
HostResource hostResource = newHostResource("ns1.example.tld");
hostNameField.set(hostResource, "NS1.EXAMPLE.TLD");
HostHistory hostHistory =
new HostHistory.Builder()
.setType(HistoryEntry.Type.HOST_CREATE)
.setXmlBytes("<xml></xml>".getBytes(UTF_8))
.setModificationTime(fakeClock.nowUtc())
.setRegistrarId("TheRegistrar")
.setTrid(Trid.create("ABC-123", "server-trid"))
.setBySuperuser(false)
.setReason("reason")
.setRequestedByRegistrar(true)
.setHostRepoId(hostResource.getRepoId())
.build();
DatabaseHelper.putInDb(hostResource, hostHistory);
jpaTm().transact(hostHistory::beforeSqlSaveOnReplay);
assertThat(hostHistory.getHostBase().get().getHostName()).isEqualTo("ns1.example.tld");
}
@TestSqlOnly
void testBeforeSqlSave_canonicalNameUtf8() throws Exception {
Field hostNameField = HostBase.class.getDeclaredField("fullyQualifiedHostName");
// reflection hacks to get around visibility issues
hostNameField.setAccessible(true);
HostResource hostResource = newHostResource("ns1.example.tld");
hostNameField.set(hostResource, "ns1.kittyçat.tld");
HostHistory hostHistory =
new HostHistory.Builder()
.setType(HistoryEntry.Type.HOST_CREATE)
.setXmlBytes("<xml></xml>".getBytes(UTF_8))
.setModificationTime(fakeClock.nowUtc())
.setRegistrarId("TheRegistrar")
.setTrid(Trid.create("ABC-123", "server-trid"))
.setBySuperuser(false)
.setReason("reason")
.setRequestedByRegistrar(true)
.setHostRepoId(hostResource.getRepoId())
.build();
DatabaseHelper.putInDb(hostResource, hostHistory);
jpaTm().transact(hostHistory::beforeSqlSaveOnReplay);
assertThat(hostHistory.getHostBase().get().getHostName()).isEqualTo("ns1.xn--kittyat-yxa.tld");
}
private void assertHostHistoriesEqual(HostHistory one, HostHistory two) {
assertAboutImmutableObjects().that(one).isEqualExceptFields(two, "hostBase");
assertAboutImmutableObjects()
@@ -45,6 +45,8 @@ import google.registry.model.transfer.TransferStatus;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import google.registry.testing.TestSqlOnly;
import google.registry.util.SerializeUtils;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -111,6 +113,14 @@ class HostResourceTest extends EntityTestCase {
.containsExactly(newHost);
}
@TestSqlOnly
void testSerializable() {
HostResource newHost = host.asBuilder().setRepoId("NEWHOST").build();
tm().transact(() -> tm().insert(newHost));
HostResource persisted = tm().transact(() -> tm().loadByEntity(newHost));
assertThat(SerializeUtils.serializeDeserialize(persisted)).isEqualTo(persisted);
}
@TestOfyOnly
void testLoadingByForeignKey() {
assertThat(loadByForeignKey(HostResource.class, host.getForeignKey(), fakeClock.nowUtc()))
@@ -309,4 +319,17 @@ class HostResourceTest extends EntityTestCase {
assertThat(ofyTm().loadAllOf(EppResourceIndex.class))
.containsExactly(EppResourceIndex.create(Key.create(host)));
}
@TestOfyOnly
void testBeforeSqlSaveOnReplay_canonicalName() {
host.fullyQualifiedHostName = "NS1.EXAMPLE.COM";
assertThat(host.getHostName()).isEqualTo("NS1.EXAMPLE.COM");
host.beforeSqlSaveOnReplay();
assertThat(host.getHostName()).isEqualTo("ns1.example.com");
host.fullyQualifiedHostName = "ns1.kittyçat.com";
assertThat(host.getHostName()).isEqualTo("ns1.kittyçat.com");
host.beforeSqlSaveOnReplay();
assertThat(host.getHostName()).isEqualTo("ns1.xn--kittyat-yxa.com");
}
}
@@ -36,6 +36,7 @@ import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import google.registry.testing.TestSqlOnly;
import google.registry.util.SerializeUtils;
import org.junit.jupiter.api.BeforeEach;
/** Unit tests for {@link PollMessage}. */
@@ -118,6 +119,20 @@ public class PollMessageTest extends EntityTestCase {
assertThat(tm().transact(() -> tm().loadByEntity(pollMessage))).isEqualTo(pollMessage);
}
@TestSqlOnly
void testSerializableOneTime() {
PollMessage.OneTime pollMessage =
persistResource(
new PollMessage.OneTime.Builder()
.setRegistrarId("TheRegistrar")
.setEventTime(fakeClock.nowUtc())
.setMsg("Test poll message")
.setParent(historyEntry)
.build());
PollMessage persisted = tm().transact(() -> tm().loadByEntity(pollMessage));
assertThat(SerializeUtils.serializeDeserialize(persisted)).isEqualTo(persisted);
}
@TestOfyAndSql
void testPersistenceAutorenew() {
PollMessage.Autorenew pollMessage =
@@ -133,6 +148,22 @@ public class PollMessageTest extends EntityTestCase {
assertThat(tm().transact(() -> tm().loadByEntity(pollMessage))).isEqualTo(pollMessage);
}
@TestSqlOnly
void testSerializableAutorenew() {
PollMessage.Autorenew pollMessage =
persistResource(
new PollMessage.Autorenew.Builder()
.setRegistrarId("TheRegistrar")
.setEventTime(fakeClock.nowUtc())
.setMsg("Test poll message")
.setParent(historyEntry)
.setAutorenewEndTime(fakeClock.nowUtc().plusDays(365))
.setTargetId("foobar.foo")
.build());
PollMessage persisted = tm().transact(() -> tm().loadByEntity(pollMessage));
assertThat(SerializeUtils.serializeDeserialize(persisted)).isEqualTo(persisted);
}
@TestOfyOnly
void testIndexingAutorenew() throws Exception {
PollMessage.Autorenew pollMessage =
@@ -45,7 +45,9 @@ import google.registry.model.tld.Registries;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import google.registry.testing.TestSqlOnly;
import google.registry.util.CidrAddressBlock;
import google.registry.util.SerializeUtils;
import org.joda.money.CurrencyUnit;
import org.junit.jupiter.api.BeforeEach;
@@ -135,6 +137,12 @@ class RegistrarTest extends EntityTestCase {
assertThat(tm().transact(() -> tm().loadByKey(registrar.createVKey()))).isEqualTo(registrar);
}
@TestSqlOnly
void testSerializable() {
Registrar persisted = tm().transact(() -> tm().loadByKey(registrar.createVKey()));
assertThat(SerializeUtils.serializeDeserialize(persisted)).isEqualTo(persisted);
}
@TestOfyOnly
void testIndexing() throws Exception {
verifyDatastoreIndexing(registrar, "registrarName", "ianaIdentifier");
@@ -42,6 +42,7 @@ import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.testing.InjectExtension;
import google.registry.testing.ReplayExtension;
import google.registry.testing.TestObject;
import google.registry.util.RequestStatusChecker;
import java.util.List;
@@ -49,7 +50,8 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junitpioneer.jupiter.RetryingTest;
@@ -73,8 +75,7 @@ public class ReplicateToDatastoreActionTest {
private ReplicateToDatastoreAction action;
private FakeResponse response;
// TODO(b/197534789): fix these tests and re-add the @BeforeEach
// @BeforeEach
@BeforeEach
void setUp() {
resetAction();
injectExtension.setStaticField(Ofy.class, "clock", fakeClock);
@@ -88,16 +89,18 @@ public class ReplicateToDatastoreActionTest {
TestObject.beforeDatastoreSaveCallCount = 0;
}
// TODO(b/197534789): fix these tests and re-add the @AfterEach
// @AfterEach
@AfterEach
void tearDown() {
DatabaseHelper.removeDatabaseMigrationSchedule();
fakeClock.disableAutoIncrement();
}
@RetryingTest(4)
@Disabled("b/197534789")
void testReplication() {
if (!ReplayExtension.replayTestsEnabled()) {
return;
}
TestObject foo = TestObject.create("foo");
TestObject bar = TestObject.create("bar");
TestObject baz = TestObject.create("baz");
@@ -122,8 +125,11 @@ public class ReplicateToDatastoreActionTest {
}
@RetryingTest(4)
@Disabled("b/197534789")
void testReplayFromLastTxn() {
if (!ReplayExtension.replayTestsEnabled()) {
return;
}
TestObject foo = TestObject.create("foo");
TestObject bar = TestObject.create("bar");
@@ -145,8 +151,11 @@ public class ReplicateToDatastoreActionTest {
}
@RetryingTest(4)
@Disabled("b/197534789")
void testUnintentionalConcurrency() {
if (!ReplayExtension.replayTestsEnabled()) {
return;
}
TestObject foo = TestObject.create("foo");
TestObject bar = TestObject.create("bar");
@@ -181,8 +190,11 @@ public class ReplicateToDatastoreActionTest {
}
@RetryingTest(4)
@Disabled("b/197534789")
void testMissingTransactions() {
if (!ReplayExtension.replayTestsEnabled()) {
return;
}
// Write a transaction (should have a transaction id of 1).
TestObject foo = TestObject.create("foo");
insertInDb(foo);
@@ -199,8 +211,11 @@ public class ReplicateToDatastoreActionTest {
}
@Test
@Disabled("b/197534789")
void testMissingTransactions_fullTask() {
if (!ReplayExtension.replayTestsEnabled()) {
return;
}
// Write a transaction (should have a transaction id of 1).
TestObject foo = TestObject.create("foo");
insertInDb(foo);
@@ -218,8 +233,11 @@ public class ReplicateToDatastoreActionTest {
}
@Test
@Disabled("b/197534789")
void testBeforeDatastoreSaveCallback() {
if (!ReplayExtension.replayTestsEnabled()) {
return;
}
TestObject testObject = TestObject.create("foo");
insertInDb(testObject);
action.run();
@@ -228,8 +246,11 @@ public class ReplicateToDatastoreActionTest {
}
@Test
@Disabled("b/197534789")
void testNotInMigrationState_doesNothing() {
if (!ReplayExtension.replayTestsEnabled()) {
return;
}
// set a schedule that backtracks the current status to DATASTORE_PRIMARY
DateTime now = fakeClock.nowUtc();
jpaTm()
@@ -265,12 +286,15 @@ public class ReplicateToDatastoreActionTest {
}
@Test
@Disabled("b/197534789")
void testFailure_cannotAcquireLock() {
if (!ReplayExtension.replayTestsEnabled()) {
return;
}
RequestStatusChecker requestStatusChecker = mock(RequestStatusChecker.class);
when(requestStatusChecker.getLogId()).thenReturn("logId");
Truth8.assertThat(
Lock.acquire(
Lock.acquireSql(
ReplicateToDatastoreAction.class.getSimpleName(),
null,
Duration.standardHours(1),

Some files were not shown because too many files have changed in this diff Show More