Compare commits

..
Author SHA1 Message Date
gbrodmanandGitHub 1e650bd0a1 Convert to tm() some low-hanging ofy fruit (#1029)
* Convert to tm() some low-hanging ofy fruit
2021-03-25 20:01:53 -04:00
Ben McIlwainandGitHub 2649a9362a Make TransactionManager.loadAllOf() smart w.r.t the cross-TLD entity group (#1040)
* Make TransactionManager.loadAllOf() smart w.r.t the cross-TLD entity group

The loadAllOf() method will now automatically append the cross-TLD entity group
ancestor query as necessary, iff the entity class being loaded is tagged with
the new @IsCrossTld annotation.

* Add tests
2021-03-25 18:55:18 -04:00
Weimin YuandGitHub 3c65ad0f8a Add SQL wipeout action in QA (#1035)
* Add SQL wipeout action in QA

Added the WipeOutSqlAction that deletes all data in Cloud SQL.

Wipe out is restricted to the QA environment, which will get production
data during migration testing.

Also added a cron job that invokes wipeout on every saturday morning.
This is part of the privacy requirments for using production data in QA.

Tested in QA.
2021-03-25 16:59:09 -04:00
Ben McIlwainandGitHub 2bfd02f977 Add some load convenience methods to DatabaseHelper (#1038)
* Add some load convenience methods to DatabaseHelper

These can only be called by test code, and they automatically wrap the load
in a transaction if one isn't already specified (for convenience).

In production code we don't want to be able to use these, as we have to be
more thoughtful about transactions in production code (e.g. make sure that
we aren't loading and then saving a resource in separate transactions in a
way that makes it prone to contention errors).
2021-03-25 16:14:46 -04:00
Ben McIlwainandGitHub 3af0f8c148 Include ReservedList in BigQuery exports (#1037)
* Include ReservedList in BigQuery exports
2021-03-25 13:14:29 -04:00
Weimin YuandGitHub 553b24e005 Add Gradle tasks to stage BEAM pipelines (#1031)
* Add Gradle tasks to stage BEAM pipelines

Add a Gracle task to stage flex-template based pipelines for alpha and
crash environments.

This is a follow up to go/r3pr/1028, which is also under review.
2021-03-24 18:47:14 -04:00
Ben McIlwainandGitHub 3bf697c43c Make some minor improvements to TransactionManager API (#1032)
* Make some minor improvements to TransactionManager API
2021-03-24 10:40:13 -04:00
Weimin YuandGitHub fe30f619e4 * Add release info to Nomulus config files
Add the actual release tag and beam staging project id to the config
file. This allows the Nomulus server to find the right version of the
BEAM pipelines to launch.
2021-03-23 10:08:15 -04:00
Michael MullerandGitHub dc88b48772 Disallow admin triggering of internal endpoints (#1030)
* Disallow admin triggering of internal endpoints

Stop simply relying on the presence of the X-AppEngine-QueueName as an
indicator that an endpoint has been triggered internally, as this allows
admins to trigger a remote execution vulnerability.

We now supplement this check by ensuring that there is no authenticated user.
Since only an admin user can set these headers, this means that the header
must have been set by an internal request.

Tested:
  In addition to the new unit test, verified on Crash that:
  - Internal requests are still getting authenticated via the internal auth
    mechanism.
  - Admin requests with the X-AppEngine-QueueName header are rejected as
    "unauthorized."

* Reformatted.
2021-03-23 08:50:56 -04:00
sarahcaseybotandGitHub b6e4ff4e80 Fix TmchSmdrlAction log messages (#1027) 2021-03-22 15:22:55 -04:00
Michael MullerandGitHub f5fb07eb77 Pass --java-binary to _all_ formatter invocations (#1024)
* Pass --java-binary to _all_ formatter invocations

When implementing a flag to pass in the java binary to
google-java-format-diff.py, I missed the location in showNoncompliantFiles
which gets run before a check.

This change also refactors the core logic of the script so that
google-java-format-diff.py is only called from one place and (in all but one
case) only one time.

Tested:
Ran check format and show, with and without diffs present in the tree.
2021-03-22 13:35:51 -04:00
gbrodmanandGitHub 28fd425ccb Add SQL queries to RdapNameserverSearchAction (#987)
This has the same issue as the domain-search action where the database
ordering is not consistent between Objectify and SQL -- as a result,
there is one test that we have to duplicate in order to account for the
two sort orders.

In addition, there isn't a way to query @Convert-ed fields in Postgres
via the standard Hibernate / JPA query language, meaning we have to use
a raw Postgres query for that.
2021-03-22 12:33:11 -04:00
sarahcaseybotandGitHub 955f1b1ff8 Modify DeleteReservedListCommand to delete from both databases (#1025)
* Modify DeleteReservedListCommand to use both databases

* switch to confirming command

* fix typo
2021-03-19 18:49:15 -04:00
Ben McIlwainandGitHub 3159e663dc Add a jpaTm().query(...) convenience method (#1023)
* Add a jpaTm().query(...) convenience method

This replaces the more ungainly jpaTm().getEntityManager().createQuery(...).

Note that this is in JpaTransactionManager, not the parent TransactionManager,
because this is not an operation that Datastore can support. Once we finish
migrating away from Datastore this won't matter anyway because
JpaTransactionManager will be merged into TransactionManager and then deleted.

In the process of writing this PR I discovered several other methods available
on the EntityManager that may merit their own convenience methods if we start
using them enough. The more commonly used ones will be addressed in subsequent
PRs. They are:

jpaTm().getEntityManager().getMetamodel().entity(...).getName()
jpaTm().getEntityManager().getCriteriaBuilder().createQuery(...)
jpaTm().getEntityManager().createNativeQuery(...)
jpaTm().getEntityManager().find(...)

This PR also addresses some existing callsites that were calling
getEntityManager() rather than using extant convenience methods, such as
jpa().insert(...).
2021-03-19 16:34:37 -04:00
Michael MullerandGitHub de09994b36 Add replay to remaining (non-trivial) flow tests (#1020)
* Add replay to remaining (non-trivial) flow tests

Convert all remaining flow tests to do replay/compare testing.  In the course
of this:
- Move the class specific SetClock extension into its own place.
- Fix another "cyclic" foreign key (there may be another solution in this case
  because HostHistory is actually different from HistoryEntry, but that would
  require changing the way we establish priority since HostHistory is not
  distinguished from HistoryEntry in the current methodology)
2021-03-19 13:20:53 -04:00
Weimin YuandGitHub 89fe53e339 Attempt to fix flakey deleteExpiredDomain test (#1022)
* Attempt to fix flakey deleteExpiredDomain test

Though hard to reproduce locally, the test_deletesThreeDomainsInOneRun
test has failed multiple times on Kokoro. The root cause may be the
non-transactional query executed by the Action object, which was by
design. Observing that the other test never fails, this PR follows its behavior
and adds a transactional query before invoking the action.
2021-03-19 12:38:54 -04:00
Weimin YuandGitHub ccfa145ab7 Allow nom_build to run in Cloudbuild (#1021)
* Allow nom_build to run in Cloudbuild

Our builder comes with python3.6 and cannot support nom_build out of
box. Nom_build requires dataclasses which is introduced in v3.7.

I haven't found an easy way to get python3.7+ without changing the base
linux image. This PR explicitly installs dataclasses.
2021-03-19 11:28:18 -04:00
gbrodmanandGitHub 87f096ae40 Create a ClaimsListDualDatabaseDao (#1011)
The dual DAO takes care of switching between databases, comparing the
results of one to the results of the other, and caching the result. All
calls to ClaimsList retrieval or storing should use the
dual-database-DAO.

Previously, calls to comparing the lists were somewhat scattered
throughout the codebase. Now, there is one class for retrieval and
comparison (the dual DAO), one class for retrieval from SQL (the SQL
DAO), and one class for retrieval from Datastore (ClaimsListShard
itself, though the retrieval could be moved in to a separate DAO if we
wished).

In addition, we rename the ClaimsListDao to ClaimsListSqlDao
2021-03-18 23:37:08 -04:00
Weimin YuandGitHub 6bee440194 Update creation script for schema_deployer (#1019)
* Update creation script for schema_deployer

Move the create user command for schema_deployer before the
initialization of roles. As the owner of all schema objects, it needs to
be present before grant statements are executed.

Also fixed a bug in credential printing, which fails when the password
contains '%'.
2021-03-18 22:24:03 -04:00
gbrodmanandGitHub 8b2ddf8249 Refactor Cursor to exist in one class (#988)
This allows us to get rid of the DAO as well as the sanity-checking
methods since we can be reasonably sure that the fields will be the
same. Future PRs will add conversions from ofy() to tm() calls that will
make sure that we get the same proper data in both Datastore and SQL
2021-03-18 21:58:07 -04:00
Michael MullerandGitHub 6bc943bb7d Convert more flow tests to replay/compare (#1009)
* Convert more flow tests to replay/compare

Add the replay extension to another batch of flow tests.  In the course of
this:

- Refactor out domain deletion code into DatabaseHelper so that it can be used
  from multiple tests.
- Make null handling uniform for contact phone numbers.

* Convert postLoad method to onLoad.

* Remove "Test" import missed during rebase

* Deal with persistence of billing cancellations

Deal with the persistence of billing cancellations, which were added in the
master branch since before this PR was initially sent for review.

* Adding forgotten flyway file

* Removed debug variable
2021-03-18 14:31:58 -04:00
Weimin YuandGitHub deb84cf74d Add schema_deployer SQL user to SecretManager (#1018)
* Add schema_deployer SQL user to SecretManager

Add the 'schema_deployer' user to the SecretManager so that its
credential can be set up. The schema deployment process will use this
user instead of the 'postgres' user.

Changed the output of the get_sql_credential command for the schema
deployment process.

Added a sql script that documents the privileges granted to
'schema_deployer'.
2021-03-17 19:31:44 -04:00
Ben McIlwainandGitHub 127ae08790 Clear autorenew end time when a domain is restored (#1015)
* Clear autorenew end time when a domain is restored

This allows us to still see in the database which now-deleted domains had
reached expiration, while correctly not re-deleting the domain immediately if
the registrar pays to explicitly restore the domain.

This also resolves some TODOs around data migration for this field on domain so
that it's not null, as said migration has already been completed.
2021-03-17 15:39:13 -04:00
Michael MullerandGitHub df74a347cb Allow java-format to use java from the PATH (#1014)
* Allow java-format to use java from the PATH

When invoking java from the google-java-format-git-diff.sh script, if there is
no JAVA_HOME environment variable, attempt to instead run the java binary that
is on the PATH.

This also adds a few checks to verify that a java binary is available in one
of those locations and that the version discovered is Java 11 (which we know
to be compatible with the google-java-format jar).

Tested:
- unset JAVA_HOME, verified that we get the version on the PATH
- Set JAVA_HOME to an invalid directory, verified that we get an error.
- Changed the "which" command to lookup an nonexistent binary, unset JAVA_HOME
  and verified that we get a "java not found" error.
- Changed the path to point to an old version of java, verified that we get a
  "bad java version" error.
- Verified that the script still runs normally.
2021-03-17 10:29:32 -04:00
Ben McIlwainandGitHub 1154271ea5 Remove grace period ID @OnLoads now that migration is complete (#1016)
* Remove grace period ID @OnLoads now that migration is complete

I verified in BigQuery that all grace period IDs are now allocated (as expected
given that the re-save all EPP resource mapreduce has been run several times
since this migration started last year). The query I used for verification is:

SELECT fullyQualifiedDomainName, gp, ot
FROM `domain-registry.latest_datastore_export.DomainBase`
JOIN UNNEST(gracePeriods.billingEventRecurring) AS gp
JOIN UNNEST(gracePeriods.billingEventOneTime) AS ot
WHERE gp.id IS NULL or ot.id IS NULL

BUG=169873747
2021-03-17 10:18:53 -04:00
sarahcaseybotandGitHub e9330f5419 Refactor ReservedListDualDatabaseDao for easy database cutover (#1003)
* Refactor ReservedListDualDatabaseDao

* Fix merge conflict

* Fix test name

* Fix tests

* more small fixes

* Format fix
2021-03-16 16:37:14 -04:00
Ben McIlwainandGitHub 27b6117a8b Add daily cron entries to for DeleteExpiredDomainsAction (#1013)
* Add daily cron entries to for DeleteExpiredDomainsAction

This also requires setting this action to GET instead of POST, as GAE cron makes
GET requests.
2021-03-16 14:57:32 -04:00
Weimin YuandGitHub eb2e1c60ca Use shared jar to stage BEAM pipeline if possible (#1008)
* Use shared jar to stage BEAM pipeline if possible

Allow multiple BEAM pipelines with the same classes and dependencies to
share one Uber jar.

Added metadata for BulkDeleteDatastorePipeline.

Updated shell and Cloud Build scripts to stage all pipelines in one
step.
2021-03-16 13:19:30 -04:00
Weimin YuandGitHub bae5dacbae Closing the bug regarding Cloud SQL connection configs (#1012)
* Add comments to Cloud SQL configs

I believe the similarity in trace to https://github.com/brettwooldridge/HikariCP/issues/1212
is misleading.

The real cause of the exceptions may be that we ran out of connections. At the
time, the production Cloud SQL server could handle 500 connections at the
maximum. That number was within reach of a busy Nomulus server.

The maximum connection in production has been increased to 1000. We
haven't encountered this issue for a long time. All connection problems
are due to Cloud SQL maintenance or other GCP related issues.

This issue is tracked by b/154720215, which is being closed with this
PR.
2021-03-16 10:29:30 -04:00
Ben McIlwainandGitHub 58e561704c Improve logging messages and error level for DeleteExpiredDomainsActions (#1010)
* Improve logging messages and error level for DeleteExpiredDomainsActions
2021-03-15 23:24:32 -04:00
gbrodmanandGitHub cdbecac103 Convert DomainTransferRequestFlow to tm() calls (#1002)
* Convert DomainTransferRequestFlow to tm() calls

Besides the standard ofy-to-tm conversions this includes storing the
billing event cancellation VKey in the DomainTransferData object so that
we know to handle it on process / cancellation.
2021-03-15 20:01:59 -04:00
Michael MullerandGitHub c8385617bd Remove now-unused "logger" from ReplaySpecializer (#1007)
* Remove now-unused "logger" from ReplaySpecializer

This causes a build warning.

* Take out the import, too
2021-03-12 13:26:45 -05:00
Michael MullerandGitHub 684517e35a Don't use --fork-point to determine merge base (#1001)
* Don't use --fork-point to determine merge base

It turns out that the --fork-point option is subtle and error-prone.  Its
intent is not to show the nearest common base commit, but rather the commit
on a branch that the HEAD (in this case) was originally forked off of,
_whether it is currently part of the history of the specified branch or not_
(this can happen if the branch is rewritten).  The option also relies on the
presence of the fork point in the reflog for the branch, which can be
discarded in the course of a "git gc".

It is fairly easy to construct a case where the use of --fork-point causes an
error and outputs nothing.  In fact, I discovered the problem as a result of
this occuring spontaneously on one of my own branches (likely related to a
rebase).  Since the fork-point is empty, we end up diffing against the index
instead of the common commit.

This may have been a factor in some of the unrelated reformatting that we've
seen in past PRs.

Change this to a simple "merge-base origin/master HEAD", which outputs the
commit id of the most recent common base revision.

This change also quotes the forkPoint variable, which likely would have
resulted in an error in this case instead of silently producing the wrong
output.
2021-03-12 11:08:28 -05:00
Weimin YuandGitHub 1bbc38c65e Stage the init_sql_pipeline in CloudBuild (#1004)
* Stage the init_sql_pipeline in CloudBuild

Defined metadata file and added Gradle uberJar task for the pipeline,
which are needed for staging.

Updated cloud build script to stage this pipeline during the build
processs.
2021-03-12 10:36:57 -05:00
210 changed files with 5393 additions and 4116 deletions
+3 -1
View File
@@ -196,7 +196,7 @@ PRESUBMITS = {
# - concatenation of literals: (\s*\+\s*"([^"]|\\")*")*
# Line 3: , or the closing parenthesis, marking the end of the first
# parameter
r'.*\.create(Native)?Query\('
r'.*\.(query|createQuery|createNativeQuery)\('
r'(?!(\s*([A-Z_]+|"([^"]|\\")*"(\s*\+\s*"([^"]|\\")*")*)'
r'(,|\s*\))))',
"java",
@@ -206,10 +206,12 @@ PRESUBMITS = {
# using Criteria
"ForeignKeyIndex.java",
"HistoryEntryDao.java",
"JpaTransactionManager.java",
"JpaTransactionManagerImpl.java",
# CriteriaQueryBuilder is a false positive
"CriteriaQueryBuilder.java",
"RdapDomainSearchAction.java",
"RdapNameserverSearchAction.java",
"RdapSearchActionBase.java",
},
):
+54 -2
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import java.lang.reflect.Constructor
import com.google.common.base.CaseFormat
import java.util.Optional
plugins {
@@ -751,7 +751,8 @@ project.tasks.create('initSqlPipeline', JavaExec) {
// nom_build :core:bulkDeleteDatastore --args="--project=domain-registry-crash \
// --region=us-central1 --runner=DataflowRunner --kindsToDelete=*"
createToolTask(
'bulkDeleteDatastore', 'google.registry.beam.datastore.BulkDeletePipeline')
'bulkDeleteDatastore',
'google.registry.beam.datastore.BulkDeleteDatastorePipeline')
project.tasks.create('generateSqlSchema', JavaExec) {
classpath = sourceSets.nonprod.runtimeClasspath
@@ -782,6 +783,57 @@ generateGoldenImages.finalizedBy(findGoldenImages)
createUberJar('nomulus', 'nomulus', 'google.registry.tools.RegistryTool')
// Build the Uber jar shared by all flex-template based BEAM pipelines.
// This packages more code and dependency than necessary. However, without
// restructuring the source tree it is difficult to generate leaner jars.
createUberJar(
'beam_pipeline_common',
'beam_pipeline_common',
'')
// Create beam staging task if environment is alpha or crash.
// All other environments use formally released pipelines through CloudBuild.
//
// User should install gcloud and login to GCP before invoking this tasks.
if (environment in ['alpha', 'crash']) {
def pipelines = [
[
mainClass: 'google.registry.beam.initsql.InitSqlPipeline',
metaData: 'google/registry/beam/init_sql_pipeline_metadata.json'
],
[
mainClass: 'google.registry.beam.datastore.BulkDeleteDatastorePipeline',
metaData: 'google/registry/beam/bulk_delete_datastore_pipeline_metadata.json'
],
]
project.tasks.create("stage_beam_pipelines") {
doLast {
pipelines.each {
def mainClass = it['mainClass']
def metaData = it['metaData']
def pipelineName = CaseFormat.UPPER_CAMEL.to(
CaseFormat.LOWER_UNDERSCORE,
mainClass.substring(mainClass.lastIndexOf('.') + 1))
def imageName = "gcr.io/${gcpProject}/beam/${pipelineName}"
def metaDataBaseName = metaData.substring(metaData.lastIndexOf('/') + 1)
def uberJarName = tasks.beam_pipeline_common.outputs.files.asPath
def command = "\
gcloud dataflow flex-template build \
gs://${gcpProject}-deploy/live/beam/${metaDataBaseName} \
--image-gcr-path ${imageName}:live \
--sdk-language JAVA \
--flex-template-base-image JAVA11 \
--metadata-file ${projectDir}/src/main/resources/${metaData} \
--jar ${uberJarName} \
--env FLEX_TEMPLATE_JAVA_MAIN_CLASS=${mainClass} \
--project ${gcpProject}".toString()
rootProject.ext.execInBash(command, '/tmp')
}
}
}.dependsOn(tasks.beam_pipeline_common)
}
// A jar with classes and resources from main sourceSet, excluding internal
// data. See comments on configurations.nomulus_test above for details.
// TODO(weiminyu): release process should build this using the public repo to eliminate the need
@@ -37,13 +37,13 @@ import google.registry.model.domain.DomainBase;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.model.eppoutput.EppOutput;
import google.registry.request.Action;
import google.registry.request.Action.Method;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.request.lock.LockHandler;
import google.registry.util.Clock;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import javax.inject.Inject;
import org.joda.time.DateTime;
import org.joda.time.Duration;
@@ -59,12 +59,17 @@ import org.joda.time.Duration;
* <p>Note that this action works by running a superuser EPP domain delete command, and as a side
* effect of when domains are deleted (just past their expiration date), they are invariably in the
* autorenew grace period when this happens.
*
* <p>Note also that the delete flow may fail in the uncommon case that a non-autorenewing domain
* has a subordinate host. It is not trivial to handle this case automatically (as said host may be
* in use by other domains), nor is it possible to take the correct action without exercising some
* human judgment. Accordingly, such deletes will fail with SEVERE-level log messages every day when
* this action runs, thus alerting us that human action is needed to correctly process the delete.
*/
@Action(
service = Action.Service.BACKEND,
path = DeleteExpiredDomainsAction.PATH,
auth = Auth.AUTH_INTERNAL_OR_ADMIN,
method = Method.POST)
auth = Auth.AUTH_INTERNAL_OR_ADMIN)
public class DeleteExpiredDomainsAction implements Runnable {
public static final String PATH = "/_dr/task/deleteExpiredDomains";
@@ -141,12 +146,23 @@ public class DeleteExpiredDomainsAction implements Runnable {
String.join(
", ",
domainsToDelete.stream().map(DomainBase::getDomainName).collect(toImmutableList())));
domainsToDelete.forEach(this::runDomainDeleteFlow);
logger.atInfo().log("Finished deleting domains.");
response.setPayload("Finished deleting domains.");
int successes = 0;
for (DomainBase domain : domainsToDelete) {
if (runDomainDeleteFlow(domain)) {
successes++;
}
}
int failures = domainsToDelete.size() - successes;
String msg =
String.format(
"Finished; %d domains were successfully deleted and %d errored out.",
successes, failures);
logger.at(failures == 0 ? Level.INFO : Level.SEVERE).log(msg);
response.setPayload(msg);
}
private void runDomainDeleteFlow(DomainBase domain) {
/** Runs the actual domain delete flow and returns whether the deletion was successful. */
private boolean runDomainDeleteFlow(DomainBase domain) {
logger.atInfo().log("Attempting to delete domain %s", domain.getDomainName());
// Create a new transaction that the flow's execution will be enlisted in that loads the domain
// transactionally. This way we can ensure that nothing else has modified the domain in question
@@ -185,10 +201,11 @@ public class DeleteExpiredDomainsAction implements Runnable {
if (eppOutput.get().isSuccess()) {
logger.atInfo().log("Successfully deleted domain %s", domain.getDomainName());
} else {
logger.atWarning().log(
logger.atSevere().log(
"Failed to delete domain %s; EPP response:\n\n%s",
domain.getDomainName(), new String(marshalWithLenientRetry(eppOutput.get()), UTF_8));
}
}
return eppOutput.map(EppOutput::isSuccess).orElse(false);
}
}
@@ -25,8 +25,6 @@ import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_AUTORENEW;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost;
import static google.registry.schema.cursor.Cursor.GLOBAL;
import static google.registry.schema.cursor.CursorDao.loadAndCompare;
import static google.registry.util.CollectionUtils.union;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.DateTimeUtils.earliestOf;
@@ -60,7 +58,6 @@ import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.schema.cursor.CursorDao;
import google.registry.util.Clock;
import java.util.Optional;
import java.util.Set;
@@ -95,7 +92,6 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
@Override
public void run() {
Cursor cursor = ofy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now();
loadAndCompare(cursor, GLOBAL);
DateTime executeTime = clock.nowUtc();
DateTime persistedCursorTime = (cursor == null ? START_OF_TIME : cursor.getCursorTime());
DateTime cursorTime = cursorTimeParam.orElse(persistedCursorTime);
@@ -332,7 +328,6 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
tm().transact(
() -> {
Cursor cursor = ofy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now();
loadAndCompare(cursor, GLOBAL);
DateTime currentCursorTime =
(cursor == null ? START_OF_TIME : cursor.getCursorTime());
if (!currentCursorTime.equals(expectedPersistedCursorTime)) {
@@ -342,8 +337,7 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
return;
}
if (!isDryRun) {
CursorDao.saveCursor(
Cursor.createGlobal(RECURRING_BILLING, executionTime), GLOBAL);
tm().put(Cursor.createGlobal(RECURRING_BILLING, executionTime));
}
});
}
@@ -0,0 +1,98 @@
// 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.batch;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.persistence.PersistenceModule.SchemaManagerConnection;
import google.registry.request.Action;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Retrier;
import java.sql.Connection;
import java.sql.Statement;
import java.util.function.Supplier;
import javax.inject.Inject;
import org.flywaydb.core.api.FlywayException;
/**
* Wipes out all Cloud SQL data in a Nomulus GCP environment.
*
* <p>This class is created for the QA environment, where migration testing with production data
* will happen. A regularly scheduled wipeout is a prerequisite to using production data there.
*/
@Action(
service = Action.Service.BACKEND,
path = "/_dr/task/wipeOutCloudSql",
auth = Auth.AUTH_INTERNAL_OR_ADMIN)
public class WipeOutCloudSqlAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
// As a short-lived class, hardcode allowed projects here instead of using config files.
private static final ImmutableSet<String> ALLOWED_PROJECTS =
ImmutableSet.of("domain-registry-qa");
private final String projectId;
private final Supplier<Connection> connectionSupplier;
private final Response response;
private final Retrier retrier;
@Inject
WipeOutCloudSqlAction(
@Config("projectId") String projectId,
@SchemaManagerConnection Supplier<Connection> connectionSupplier,
Response response,
Retrier retrier) {
this.projectId = projectId;
this.connectionSupplier = connectionSupplier;
this.response = response;
this.retrier = retrier;
}
@Override
public void run() {
response.setContentType(PLAIN_TEXT_UTF_8);
if (!ALLOWED_PROJECTS.contains(projectId)) {
response.setStatus(SC_FORBIDDEN);
response.setPayload("Wipeout is not allowed in " + projectId);
return;
}
try {
retrier.callWithRetry(
() -> {
try (Connection conn = connectionSupplier.get();
Statement statement = conn.createStatement()) {
statement.execute("drop owned by schema_deployer;");
}
return null;
},
e -> !(e instanceof FlywayException));
response.setStatus(SC_OK);
response.setPayload("Wiped out Cloud SQL in " + projectId);
} catch (RuntimeException e) {
logger.atSevere().withCause(e).log("Failed to wipe out Cloud SQL data.");
response.setStatus(SC_INTERNAL_SERVER_ERROR);
response.setPayload("Failed to wipe out Cloud SQL in " + projectId);
}
}
}
@@ -78,7 +78,7 @@ import org.apache.beam.sdk.values.TupleTagList;
* types in the Datastore using the {@code --numOfKindsHint} argument. If the default value for this
* parameter is too low, performance will suffer.
*/
public class BulkDeletePipeline {
public class BulkDeleteDatastorePipeline {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
// This tool is not for use in our critical projects.
@@ -89,7 +89,7 @@ public class BulkDeletePipeline {
private final Pipeline pipeline;
BulkDeletePipeline(BulkDeletePipelineOptions options) {
BulkDeleteDatastorePipeline(BulkDeletePipelineOptions options) {
this.options = options;
pipeline = Pipeline.create(options);
}
@@ -303,7 +303,7 @@ public class BulkDeletePipeline {
public static void main(String[] args) {
BulkDeletePipelineOptions options =
PipelineOptionsFactory.fromArgs(args).withValidation().as(BulkDeletePipelineOptions.class);
BulkDeletePipeline pipeline = new BulkDeletePipeline(options);
BulkDeleteDatastorePipeline pipeline = new BulkDeleteDatastorePipeline(options);
pipeline.run();
System.exit(0);
}
@@ -300,10 +300,9 @@ public class BigqueryConnection implements AutoCloseable {
* Initializes the BigqueryConnection object by setting up the API client and creating the default
* dataset if it doesn't exist.
*/
private BigqueryConnection initialize() throws Exception {
private void initialize() throws Exception {
createDatasetIfNeeded(datasetId);
createDatasetIfNeeded(TEMP_DATASET_NAME);
return this;
}
/**
@@ -378,13 +377,11 @@ public class BigqueryConnection implements AutoCloseable {
/**
* Starts an asynchronous load job to populate the specified destination table with the given
* source URIs and source format. Returns a ListenableFuture that holds the same destination
* table object on success.
* source URIs and source format. Returns a ListenableFuture that holds the same destination table
* object on success.
*/
public ListenableFuture<DestinationTable> load(
DestinationTable dest,
SourceFormat sourceFormat,
Iterable<String> sourceUris) {
public ListenableFuture<DestinationTable> startLoad(
DestinationTable dest, SourceFormat sourceFormat, Iterable<String> sourceUris) {
Job job = new Job()
.setConfiguration(new JobConfiguration()
.setLoad(new JobConfigurationLoad()
@@ -400,9 +397,7 @@ public class BigqueryConnection implements AutoCloseable {
* of the specified query, or if the table is a view, to update the view to reflect that query.
* Returns a ListenableFuture that holds the same destination table object on success.
*/
public ListenableFuture<DestinationTable> query(
String querySql,
DestinationTable dest) {
public ListenableFuture<DestinationTable> startQuery(String querySql, DestinationTable dest) {
if (dest.type == TableType.VIEW) {
// Use Futures.transform() rather than calling apply() directly so that any exceptions thrown
// by calling updateTable will be propagated on the get() call, not from here.
@@ -562,20 +557,18 @@ public class BigqueryConnection implements AutoCloseable {
// Tracking bug for query-to-GCS support is b/13777340.
DestinationTable tempTable = buildTemporaryTable().build();
return transformAsync(
query(querySql, tempTable),
startQuery(querySql, tempTable),
tempTable1 -> extractTable(tempTable1, destinationUri, destinationFormat, printHeader),
directExecutor());
}
/** @see #runJob(Job, AbstractInputStreamContent) */
public Job runJob(Job job) {
private Job runJob(Job job) {
return runJob(job, null);
}
/**
* Launch a job, wait for it to complete, but <i>do not</i> check for errors.
*/
public Job runJob(Job job, @Nullable AbstractInputStreamContent data) {
/** Launch a job, wait for it to complete, but <i>do not</i> check for errors. */
private Job runJob(Job job, @Nullable AbstractInputStreamContent data) {
return checkJob(waitForJob(launchJob(job, data)));
}
@@ -652,6 +652,13 @@ public final class RegistryConfig {
return config.beam.defaultJobZone;
}
/** Returns the GCS bucket URL with all staged BEAM flex templates. */
@Provides
@Config("beamStagingBucketUrl")
public static String provideBeamStagingBucketUrl(RegistryConfigSettings config) {
return config.beam.stagingBucketUrl;
}
/**
* Returns the URL of the GCS location we store jar dependencies for beam pipelines.
*
@@ -133,6 +133,7 @@ public class RegistryConfigSettings {
public static class Beam {
public String defaultJobRegion;
public String defaultJobZone;
public String stagingBucketUrl;
}
/** Configuration for Cloud DNS. */
@@ -207,18 +207,13 @@ hibernate:
# Connection pool configurations.
hikariConnectionTimeout: 20000
# We occasionally received "Connection is not available, request timed out"
# exception when setting minimumIdle to 0 and it turned out it is a bug (See
# https://github.com/brettwooldridge/HikariCP/issues/1212) in HikariCP.
#
# We tried to use a fixed size pool but ran into an issue(See b/155383029),
# so we need further investigation to figure out the proper size of the pool.
#
# HikariCP also recommends not setting minimumIdle for maximum performance
# and responsiveness to spike demands (See
# https://github.com/brettwooldridge/HikariCP).
#
# TODO(b/154720215): Investigate the long term fix.
# Cloud SQL connections are a relatively scarce resource (maximum is 1000 as
# of March 2021). The minimumIdle should be a small value so that machines may
# release connections after a demand spike. The maximumPoolSize is set to 10
# because that is the maximum number of concurrent requests a Nomulus server
# instance can handle (as limited by AppEngine for basic/manual scaling). Note
# that BEAM pipelines are not subject to the maximumPoolSize value defined
# here. See PersistenceModule.java for more information.
hikariMinimumIdle: 1
hikariMaximumPoolSize: 10
hikariIdleTimeout: 300000
@@ -430,6 +425,7 @@ beam:
# The default zone to run Apache Beam (Cloud Dataflow) jobs in.
# TODO(weiminyu): consider dropping zone config. No obvious needs for this.
defaultJobZone: us-east1-c
stagingBucketUrl: gcs-bucket-with-staged-templates
keyring:
# The name of the active keyring, either "KMS" or "Dummy".
@@ -142,6 +142,16 @@
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/task/deleteExpiredDomains]]></url>
<description>
This job runs an action that deletes domains that are past their
autorenew end date.
</description>
<schedule>every day 03:07</schedule>
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/cron/fanout?queue=export-snapshot&endpoint=/_dr/task/backupDatastore&runInEmpty]]></url>
<description>
@@ -379,6 +379,12 @@
<url-pattern>/_dr/task/relockDomain</url-pattern>
</servlet-mapping>
<!-- Action to wipeout Cloud SQL data -->
<servlet-mapping>
<servlet-name>backend-servlet</servlet-name>
<url-pattern>/_dr/task/wipeOutCloudSql</url-pattern>
</servlet-mapping>
<!-- Security config -->
<security-constraint>
<web-resource-collection>
@@ -190,4 +190,14 @@
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/task/deleteExpiredDomains]]></url>
<description>
This job runs an action that deletes domains that are past their
autorenew end date.
</description>
<schedule>every day 03:07</schedule>
<target>backend</target>
</cron>
</cronentries>
@@ -183,6 +183,16 @@
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/task/deleteExpiredDomains]]></url>
<description>
This job runs an action that deletes domains that are past their
autorenew end date.
</description>
<schedule>every day 03:07</schedule>
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/cron/fanout?queue=export-snapshot&endpoint=/_dr/task/backupDatastore&runInEmpty]]></url>
<description>
@@ -72,4 +72,23 @@
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/task/deleteExpiredDomains]]></url>
<description>
This job runs an action that deletes domains that are past their
autorenew end date.
</description>
<schedule>every day 03:07</schedule>
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/task/wipeOutCloudSql]]></url>
<description>
This job runs an action that deletes all data in Cloud SQL.
</description>
<schedule>every saturday 03:07</schedule>
<target>backend</target>
</cron>
</cronentries>
@@ -158,6 +158,16 @@
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/task/deleteExpiredDomains]]></url>
<description>
This job runs an action that deletes domains that are past their
autorenew end date.
</description>
<schedule>every day 03:07</schedule>
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/cron/fanout?queue=export-snapshot&endpoint=/_dr/task/backupDatastore&runInEmpty]]></url>
<description>
@@ -21,12 +21,13 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Ordering;
import com.googlecode.objectify.Key;
import google.registry.model.EntityClasses;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.annotations.NotBackedUp;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.annotations.VirtualEntity;
/** Constants related to export code. */
public final class ExportConstants {
public final class AnnotatedEntities {
/** Returns the names of kinds to include in Datastore backups. */
public static ImmutableSet<String> getBackupKinds() {
@@ -49,4 +50,13 @@ public final class ExportConstants {
.map(Key::getKind)
.collect(toImmutableSortedSet(Ordering.natural()));
}
/** Returns the names of kinds that are in the cross-TLD entity group. */
public static ImmutableSet<String> getCrossTldKinds() {
return EntityClasses.ALL_CLASSES.stream()
.filter(hasAnnotation(InCrossTld.class))
.filter(hasAnnotation(VirtualEntity.class).negate())
.map(Key::getKind)
.collect(toImmutableSortedSet(Ordering.natural()));
}
}
@@ -66,12 +66,13 @@ public class BackupDatastoreAction implements Runnable {
try {
Operation backup =
datastoreAdmin
.export(RegistryConfig.getDatastoreBackupsBucket(), ExportConstants.getBackupKinds())
.export(
RegistryConfig.getDatastoreBackupsBucket(), AnnotatedEntities.getBackupKinds())
.execute();
String backupName = backup.getName();
// Enqueue a poll task to monitor the backup and load REPORTING-related kinds into bigquery.
enqueuePollTask(backupName, ExportConstants.getReportingKinds());
enqueuePollTask(backupName, AnnotatedEntities.getReportingKinds());
String message =
String.format(
"Datastore backup started with name: %s\nSaving to %s",
@@ -25,8 +25,7 @@ import static google.registry.model.registrar.RegistrarContact.Type.LEGAL;
import static google.registry.model.registrar.RegistrarContact.Type.MARKETING;
import static google.registry.model.registrar.RegistrarContact.Type.TECH;
import static google.registry.model.registrar.RegistrarContact.Type.WHOIS;
import static google.registry.schema.cursor.Cursor.GLOBAL;
import static google.registry.schema.cursor.CursorDao.loadAndCompare;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.base.Joiner;
@@ -38,7 +37,6 @@ import google.registry.model.common.Cursor;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarContact;
import google.registry.schema.cursor.CursorDao;
import google.registry.util.Clock;
import google.registry.util.DateTimeUtils;
import java.io.IOException;
@@ -64,7 +62,6 @@ class SyncRegistrarsSheet {
*/
boolean wereRegistrarsModified() {
Cursor cursor = ofy().load().key(Cursor.createGlobalKey(SYNC_REGISTRAR_SHEET)).now();
loadAndCompare(cursor, GLOBAL);
DateTime lastUpdateTime = (cursor == null) ? START_OF_TIME : cursor.getCursorTime();
for (Registrar registrar : Registrar.loadAllCached()) {
if (DateTimeUtils.isAtOrAfter(registrar.getLastUpdateTime(), lastUpdateTime)) {
@@ -155,9 +152,7 @@ class SyncRegistrarsSheet {
return builder.build();
})
.collect(toImmutableList()));
CursorDao.saveCursor(
Cursor.createGlobal(SYNC_REGISTRAR_SHEET, executionTime),
google.registry.schema.cursor.Cursor.GLOBAL);
tm().transact(() -> tm().put(Cursor.createGlobal(SYNC_REGISTRAR_SHEET, executionTime)));
}
private static String convertContacts(
@@ -45,7 +45,7 @@ import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.registry.Registry;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.model.tmch.ClaimsListShard;
import google.registry.model.tmch.ClaimsListDualDatabaseDao;
import google.registry.util.Clock;
import java.util.HashSet;
import java.util.Optional;
@@ -104,7 +104,8 @@ public final class DomainClaimsCheckFlow implements Flow {
verifyClaimsPeriodNotEnded(registry, now);
}
}
Optional<String> claimKey = ClaimsListShard.get().getClaimKey(parsedDomain.parts().get(0));
Optional<String> claimKey =
ClaimsListDualDatabaseDao.get().getClaimKey(parsedDomain.parts().get(0));
launchChecksBuilder.add(
LaunchCheck.create(
LaunchCheckName.create(claimKey.isPresent(), domainName), claimKey.orElse(null)));
@@ -129,7 +129,7 @@ import google.registry.model.registry.label.ReservedList;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tmch.ClaimsListShard;
import google.registry.model.tmch.ClaimsListDualDatabaseDao;
import google.registry.persistence.VKey;
import google.registry.tldconfig.idn.IdnLabelValidator;
import google.registry.util.Idn;
@@ -994,7 +994,7 @@ public class DomainFlowUtils {
InternetDomainName domainName, boolean hasSignedMarks, boolean hasClaimsNotice)
throws EppException {
boolean isInClaimsList =
ClaimsListShard.get().getClaimKey(domainName.parts().get(0)).isPresent();
ClaimsListDualDatabaseDao.get().getClaimKey(domainName.parts().get(0)).isPresent();
if (hasClaimsNotice && !isInClaimsList) {
throw new UnexpectedClaimsNoticeException(domainName.toString());
}
@@ -1093,8 +1093,7 @@ public class DomainFlowUtils {
.list();
} else {
return jpaTm()
.getEntityManager()
.createQuery(
.query(
"FROM DomainHistory WHERE modificationTime >= :beginning "
+ "ORDER BY modificationTime ASC",
DomainHistory.class)
@@ -233,6 +233,9 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
.setDeletePollMessage(null)
.setAutorenewBillingEvent(autorenewEvent.createVKey())
.setAutorenewPollMessage(autorenewPollMessage.createVKey())
// Clear the autorenew end time so if it had expired but is now explicitly being restored,
// it won't immediately be deleted again.
.setAutorenewEndTime(Optional.empty())
.setLastEppUpdateTime(now)
.setLastEppUpdateClientId(clientId)
.build();
@@ -31,7 +31,6 @@ import static google.registry.flows.domain.DomainTransferUtils.createPendingTran
import static google.registry.flows.domain.DomainTransferUtils.createTransferResponse;
import static google.registry.flows.domain.DomainTransferUtils.createTransferServerApproveEntities;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableList;
@@ -227,12 +226,11 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
.setLastEppUpdateClientId(gainingClientId)
.build();
asyncTaskEnqueuer.enqueueAsyncResave(newDomain, now, automaticTransferTime);
ofy().save()
.entities(new ImmutableSet.Builder<>()
.add(newDomain, historyEntry, requestPollMessage)
.addAll(serverApproveEntities)
.build())
.now();
tm().putAll(
new ImmutableSet.Builder<>()
.add(newDomain, historyEntry, requestPollMessage)
.addAll(serverApproveEntities)
.build());
return responseBuilder
.setResultFromCode(SUCCESS_WITH_ACTION_PENDING)
.setResData(createResponse(period, existingDomain, newDomain, now))
@@ -58,16 +58,16 @@ class CommitLogManifestReader
@Override
public QueryResultIterator<Key<CommitLogManifest>> getQueryIterator(@Nullable Cursor cursor) {
return startQueryAt(query(), cursor).keys().iterator();
return startQueryAt(createBucketQuery(), cursor).keys().iterator();
}
@Override
public int getTotal() {
return query().count();
return createBucketQuery().count();
}
/** Query for children of this bucket. */
Query<CommitLogManifest> query() {
Query<CommitLogManifest> createBucketQuery() {
Query<CommitLogManifest> query = ofy().load().type(CommitLogManifest.class).ancestor(bucketKey);
if (olderThan != null) {
query = query.filterKey(
@@ -14,6 +14,8 @@
package google.registry.model;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryEnvironment;
import google.registry.model.common.DatabaseTransitionSchedule;
@@ -44,5 +46,10 @@ public class DatabaseMigrationUtils {
.orElse(PrimaryDatabase.DATASTORE);
}
public static boolean isDatastore(TransitionId transitionId) {
return tm().transactNew(() -> DatabaseMigrationUtils.getPrimaryDatabase(transitionId))
.equals(PrimaryDatabase.DATASTORE);
}
private DatabaseMigrationUtils() {}
}
@@ -418,8 +418,7 @@ public final class EppResourceUtils {
if (isContactKey) {
query =
jpaTm()
.getEntityManager()
.createQuery(CONTACT_LINKED_DOMAIN_QUERY, String.class)
.query(CONTACT_LINKED_DOMAIN_QUERY, String.class)
.setParameter("fkRepoId", key)
.setParameter("now", now);
} else {
@@ -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.annotations;
import com.googlecode.objectify.annotation.Entity;
import google.registry.model.common.EntityGroupRoot;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation for an Objectify {@link Entity} to indicate that it is in the cross-TLD entity group.
*
* <p>This means that the entity's <code>@Parent</code> field has to have the value of {@link
* EntityGroupRoot#getCrossTldKey}.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Inherited
public @interface InCrossTld {}
@@ -20,11 +20,13 @@ import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.InCrossTld;
import javax.persistence.MappedSuperclass;
import javax.persistence.Transient;
/** A singleton entity in Datastore. */
@MappedSuperclass
@InCrossTld
public abstract class CrossTldSingleton extends ImmutableObject {
public static final long SINGLETON_ID = 1; // There is always exactly one of these.
@@ -24,12 +24,24 @@ import com.google.common.base.Splitter;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
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.UpdateAutoTimestamp;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.common.Cursor.CursorId;
import google.registry.model.registry.Registry;
import google.registry.schema.replay.DatastoreOnlyEntity;
import google.registry.persistence.VKey;
import google.registry.schema.replay.DatastoreAndSqlEntity;
import java.io.Serializable;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.IdClass;
import javax.persistence.PostLoad;
import javax.persistence.Transient;
import org.joda.time.DateTime;
/**
@@ -38,7 +50,13 @@ import org.joda.time.DateTime;
* scoped on {@link EntityGroupRoot}.
*/
@Entity
public class Cursor extends ImmutableObject implements DatastoreOnlyEntity {
@javax.persistence.Entity
@IdClass(CursorId.class)
@InCrossTld
public class Cursor extends ImmutableObject implements DatastoreAndSqlEntity {
/** 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";
/** The types of cursors, used as the string id field for each cursor in Datastore. */
public enum CursorType {
@@ -104,9 +122,9 @@ public class Cursor extends ImmutableObject implements DatastoreOnlyEntity {
/**
* If there are multiple cursors for a given cursor type, a cursor must also have a scope
* defined (distinct from a parent, which is always the EntityGroupRoot key). For instance,
* for a cursor that is defined at the registry level, the scope type will be Registry.class.
* For a cursor (theoretically) defined for each EPP resource, the scope type will be
* defined (distinct from a parent, which is always the EntityGroupRoot key). For instance, for
* a cursor that is defined at the registry level, the scope type will be Registry.class. For a
* cursor (theoretically) defined for each EPP resource, the scope type will be
* EppResource.class. For a global cursor, i.e. one that applies per environment, this will be
* {@link EntityGroupRoot}.
*/
@@ -115,24 +133,73 @@ public class Cursor extends ImmutableObject implements DatastoreOnlyEntity {
}
}
@Parent
Key<EntityGroupRoot> parent = getCrossTldKey();
@Transient @Parent Key<EntityGroupRoot> parent = getCrossTldKey();
@Id
String id;
@Transient @Id String id;
@Ignore
@Enumerated(EnumType.STRING)
@Column(nullable = false)
@javax.persistence.Id
CursorType type;
@Ignore
@Column(nullable = false)
@javax.persistence.Id
String scope;
@Column(nullable = false)
DateTime cursorTime = START_OF_TIME;
/** An automatically managed timestamp of when this object was last written to Datastore. */
@Column(nullable = false)
UpdateAutoTimestamp lastUpdateTime = UpdateAutoTimestamp.create(null);
@OnLoad
void onLoad() {
scope = getScopeFromId(id);
type = getTypeFromId(id);
}
@PostLoad
void postLoad() {
// "Generate" the ID based on the scope and type
Key<? extends ImmutableObject> scopeKey =
scope.equals(GLOBAL)
? getCrossTldKey()
: Key.create(getCrossTldKey(), Registry.class, scope);
id = generateId(type, scopeKey);
}
public static VKey<Cursor> createVKey(Key<Cursor> key) {
String id = key.getName();
return VKey.create(Cursor.class, new CursorId(getTypeFromId(id), getScopeFromId(id)), key);
}
public VKey<Cursor> createVKey() {
return createVKey(type, scope);
}
public static VKey<Cursor> createGlobalVKey(CursorType type) {
return createVKey(type, GLOBAL);
}
public static VKey<Cursor> createVKey(CursorType type, String scope) {
Key<Cursor> key =
scope.equals(GLOBAL) ? createGlobalKey(type) : createKey(type, Registry.get(scope));
return VKey.create(Cursor.class, new CursorId(type, scope), key);
}
public DateTime getLastUpdateTime() {
return lastUpdateTime.getTimestamp();
}
public String getScope() {
return scope;
}
public CursorType getType() {
List<String> id = Splitter.on('_').splitToList(this.id);
return CursorType.valueOf(String.join("_", id.subList(1, id.size())));
return type;
}
/**
@@ -142,10 +209,12 @@ public class Cursor extends ImmutableObject implements DatastoreOnlyEntity {
private static void checkValidCursorTypeForScope(
CursorType cursorType, Key<? extends ImmutableObject> scope) {
checkArgument(
cursorType.getScopeClass().equals(
scope.equals(EntityGroupRoot.getCrossTldKey())
? EntityGroupRoot.class
: ofy().factory().getMetadata(scope).getEntityClass()),
cursorType
.getScopeClass()
.equals(
scope.equals(getCrossTldKey())
? EntityGroupRoot.class
: ofy().factory().getMetadata(scope).getEntityClass()),
"Class required for cursor does not match scope class");
}
@@ -154,6 +223,20 @@ public class Cursor extends ImmutableObject implements DatastoreOnlyEntity {
return String.format("%s_%s", scope.getString(), cursorType.name());
}
private static String getScopeFromId(String id) {
List<String> idSplit = Splitter.on('_').splitToList(id);
// The "parent" is always the crossTldKey; in order to find the scope (either Registry or
// cross-tld-key) we have to parse the part of the ID
Key<?> scopeKey = Key.valueOf(idSplit.get(0));
return scopeKey.equals(getCrossTldKey()) ? GLOBAL : scopeKey.getName();
}
private static CursorType getTypeFromId(String id) {
List<String> idSplit = Splitter.on('_').splitToList(id);
// The cursor type is the second part of the ID string
return CursorType.valueOf(String.join("_", idSplit.subList(1, idSplit.size())));
}
/** Creates a unique key for a given scope and cursor type. */
public static Key<Cursor> createKey(CursorType cursorType, ImmutableObject scope) {
Key<? extends ImmutableObject> scopeKey = Key.create(scope);
@@ -166,13 +249,12 @@ public class Cursor extends ImmutableObject implements DatastoreOnlyEntity {
checkArgument(
cursorType.getScopeClass().equals(EntityGroupRoot.class),
"Cursor type is not a global cursor.");
return Key.create(
getCrossTldKey(), Cursor.class, generateId(cursorType, EntityGroupRoot.getCrossTldKey()));
return Key.create(getCrossTldKey(), Cursor.class, generateId(cursorType, getCrossTldKey()));
}
/** Creates a new global cursor instance. */
public static Cursor createGlobal(CursorType cursorType, DateTime cursorTime) {
return create(cursorType, cursorTime, EntityGroupRoot.getCrossTldKey());
return create(cursorType, cursorTime, getCrossTldKey());
}
/** Creates a new cursor instance with a given {@link Key} scope. */
@@ -184,8 +266,10 @@ public class Cursor extends ImmutableObject implements DatastoreOnlyEntity {
checkNotNull(cursorType, "Cursor type cannot be null");
checkValidCursorTypeForScope(cursorType, scope);
instance.id = generateId(cursorType, scope);
instance.type = cursorType;
instance.scope = scope.equals(getCrossTldKey()) ? GLOBAL : scope.getName();
return instance;
}
}
/** Creates a new cursor instance with a given {@link ImmutableObject} scope. */
public static Cursor create(CursorType cursorType, DateTime cursorTime, ImmutableObject scope) {
@@ -203,4 +287,17 @@ public class Cursor extends ImmutableObject implements DatastoreOnlyEntity {
public DateTime getCursorTime() {
return cursorTime;
}
static class CursorId extends ImmutableObject implements Serializable {
public CursorType type;
public String scope;
private CursorId() {}
public CursorId(CursorType type, String scope) {
this.type = type;
this.scope = scope;
}
}
}
@@ -32,11 +32,13 @@ import com.googlecode.objectify.annotation.Mapify;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.ImmutableObject;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.common.TimedTransitionProperty.TimeMapper;
import google.registry.model.common.TimedTransitionProperty.TimedTransition;
import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.ReservedList;
import google.registry.model.smd.SignedMarkRevocationList;
import google.registry.model.tmch.ClaimsListShard;
import google.registry.persistence.VKey;
import google.registry.schema.replay.DatastoreOnlyEntity;
import java.util.Optional;
@@ -45,6 +47,7 @@ import org.joda.time.DateTime;
@Entity
@Immutable
@InCrossTld
public class DatabaseTransitionSchedule extends ImmutableObject implements DatastoreOnlyEntity {
/**
@@ -58,6 +61,8 @@ public class DatabaseTransitionSchedule extends ImmutableObject implements Datas
/** The id of the transition schedule. */
public enum TransitionId {
/** The schedule for migration of {@link ClaimsListShard} entities. */
CLAIMS_LIST,
/** The schedule for the migration of {@link PremiumList} and {@link ReservedList}. */
DOMAIN_LABEL_LISTS,
/** The schedule for the migration of the {@link SignedMarkRevocationList} entity. */
@@ -21,6 +21,7 @@ import static google.registry.model.EppResourceUtils.projectResourceOntoBuilderA
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.annotation.IgnoreSave;
import com.googlecode.objectify.annotation.Index;
import com.googlecode.objectify.annotation.OnLoad;
import com.googlecode.objectify.condition.IfNull;
import google.registry.model.EppResource;
import google.registry.model.EppResource.ResourceWithTransferData;
@@ -189,6 +190,17 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
+ " use ContactResource instead");
}
@OnLoad
void onLoad() {
if (voice != null && voice.hasNullFields()) {
voice = null;
}
if (fax != null && fax.hasNullFields()) {
fax = null;
}
}
public String getContactId() {
return contactId;
}
@@ -325,11 +337,17 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
}
public B setVoiceNumber(ContactPhoneNumber voiceNumber) {
if (voiceNumber != null && voiceNumber.hasNullFields()) {
voiceNumber = null;
}
getInstance().voice = voiceNumber;
return thisCastToDerived();
}
public B setFaxNumber(ContactPhoneNumber faxNumber) {
if (faxNumber != null && faxNumber.hasNullFields()) {
faxNumber = null;
}
getInstance().fax = faxNumber;
return thisCastToDerived();
}
@@ -302,12 +302,6 @@ public class DomainContent extends EppResource
@OnLoad
void load() {
// Back fill with correct END_OF_TIME sentinel value.
// TODO(mcilwain): Remove this once back-filling is complete.
if (autorenewEndTime == null) {
autorenewEndTime = END_OF_TIME;
}
// Reconstitute all of the contacts so that they have VKeys.
allContacts =
allContacts.stream().map(DesignatedContact::reconstitute).collect(toImmutableSet());
@@ -321,9 +315,6 @@ public class DomainContent extends EppResource
nullToEmptyImmutableCopy(gracePeriods).stream()
.map(gracePeriod -> gracePeriod.cloneAfterOfyLoad(getRepoId()))
.collect(toImmutableSet());
// TODO(b/169873747): Remove this method after explicitly re-saving all domain entities.
// See also: GradePeriod.onLoad.
gracePeriods.forEach(GracePeriod::onLoad);
// Restore history record ids.
autorenewPollMessageHistoryId = getHistoryId(autorenewPollMessage);
@@ -387,13 +378,11 @@ public class DomainContent extends EppResource
public static void beforeSqlDelete(VKey<DomainBase> key) {
// Delete all grace periods associated with the domain.
jpaTm()
.getEntityManager()
.createQuery("DELETE FROM GracePeriod WHERE domain_repo_id = :repo_id")
.query("DELETE FROM GracePeriod WHERE domain_repo_id = :repo_id")
.setParameter("repo_id", key.getSqlKey())
.executeUpdate();
jpaTm()
.getEntityManager()
.createQuery("DELETE FROM DelegationSignerData WHERE domain_repo_id = :repo_id")
.query("DELETE FROM DelegationSignerData WHERE domain_repo_id = :repo_id")
.setParameter("repo_id", key.getSqlKey())
.executeUpdate();
}
@@ -443,12 +432,7 @@ public class DomainContent extends EppResource
* purposes of more legible business logic.
*/
public Optional<DateTime> getAutorenewEndTime() {
// TODO(mcilwain): Remove null handling for autorenewEndTime once data migration away from null
// is complete.
return Optional.ofNullable(
(autorenewEndTime == null || autorenewEndTime.equals(END_OF_TIME))
? null
: autorenewEndTime);
return Optional.ofNullable(autorenewEndTime.equals(END_OF_TIME) ? null : autorenewEndTime);
}
@Override
@@ -54,17 +54,6 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
return super.getGracePeriodId();
}
// TODO(b/169873747): Remove this method after explicitly re-saving all domain entities.
// This method is invoked from DomainContent.load(): Objectify's @OnLoad annotation
// apparently does not work on embedded objects inside an entity.
// Changing signature to void onLoad(@AlsoLoad("gracePeriodId") Long gracePeriodId)
// would not work. Method is not called if gracePeriodId is null.
void onLoad() {
if (gracePeriodId == null) {
gracePeriodId = ObjectifyService.allocateId();
}
}
private static GracePeriod createInternal(
GracePeriodStatus type,
String domainRepoId,
@@ -71,6 +71,11 @@ public class PhoneNumber extends ImmutableObject {
return phoneNumber + (extension != null ? " x" + extension : "");
}
/** Returns true if both fields of the phone number are null. */
public boolean hasNullFields() {
return phoneNumber == null && extension == null;
}
/** A builder for constructing {@link PhoneNumber}. */
public static class Builder<T extends PhoneNumber> extends Buildable.Builder<T> {
@Override
@@ -204,8 +204,7 @@ public abstract class ForeignKeyIndex<E extends EppResource> extends BackupGroup
String entityName =
jpaTm().getEntityManager().getMetamodel().entity(clazz).getName();
return jpaTm()
.getEntityManager()
.createQuery(
.query(
String.format(
"FROM %s WHERE %s IN :propertyValue and deletionTime > :now ",
entityName, property),
@@ -17,6 +17,7 @@ package google.registry.model.ofy;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
@@ -29,6 +30,8 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Result;
import com.googlecode.objectify.cmd.Query;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.DomainHistory;
import google.registry.model.host.HostHistory;
@@ -251,7 +254,13 @@ public class DatastoreTransactionManager implements TransactionManager {
@Override
public <T> ImmutableList<T> loadAllOf(Class<T> clazz) {
return ImmutableList.copyOf(getOfy().load().type(clazz));
Query<T> query = getOfy().load().type(clazz);
// If the entity is in the cross-TLD entity group, then we can take advantage of an ancestor
// query to give us strong transactional consistency.
if (clazz.isAnnotationPresent(InCrossTld.class)) {
query = query.ancestor(getCrossTldKey());
}
return ImmutableList.copyOf(query);
}
@Override
@@ -298,6 +307,11 @@ public class DatastoreTransactionManager implements TransactionManager {
getOfy().clearSessionCache();
}
@Override
public boolean isOfy() {
return true;
}
/**
* Executes the given {@link Result} instance synchronously if not in a transaction.
*
@@ -72,6 +72,7 @@ import google.registry.model.ImmutableObject;
import google.registry.model.JsonMapBuilder;
import google.registry.model.Jsonifiable;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.common.EntityGroupRoot;
import google.registry.model.registrar.Registrar.BillingAccountEntry.CurrencyMapper;
@@ -112,6 +113,7 @@ import org.joda.time.DateTime;
columnList = "ianaIdentifier",
name = "registrar_iana_identifier_idx"),
})
@InCrossTld
public class Registrar extends ImmutableObject
implements Buildable, DatastoreAndSqlEntity, Jsonifiable {
@@ -651,8 +653,7 @@ public class Registrar extends ImmutableObject
return tm().transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
.query(
"FROM RegistrarPoc WHERE registrarId = :registrarId",
RegistrarContact.class)
.setParameter("registrarId", clientIdentifier)
@@ -986,9 +987,7 @@ public class Registrar extends ImmutableObject
/** Loads all registrar entities directly from Datastore. */
public static Iterable<Registrar> loadAll() {
return tm().isOfy()
? ImmutableList.copyOf(ofy().load().type(Registrar.class).ancestor(getCrossTldKey()))
: tm().transact(() -> tm().loadAllOf(Registrar.class));
return transactIfJpaTm(() -> tm().loadAllOf(Registrar.class));
}
/** Loads all registrar entities using an in-memory cache. */
@@ -44,6 +44,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.annotations.InCrossTld;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.registrar.RegistrarContact.RegistrarPocId;
import google.registry.persistence.VKey;
@@ -77,6 +78,7 @@ import javax.persistence.Transient;
@javax.persistence.Index(columnList = "gaeUserId", name = "registrarpoc_gae_user_id_idx")
})
@IdClass(RegistrarPocId.class)
@InCrossTld
public class RegistrarContact extends ImmutableObject
implements DatastoreAndSqlEntity, Jsonifiable {
@@ -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.annotations.InCrossTld;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.common.EntityGroupRoot;
import google.registry.model.common.TimedTransitionProperty;
@@ -86,6 +87,7 @@ import org.joda.time.Duration;
@ReportedOn
@Entity
@javax.persistence.Entity(name = "Tld")
@InCrossTld
public class Registry extends ImmutableObject implements Buildable, DatastoreAndSqlEntity {
@Parent @Transient Key<EntityGroupRoot> parent = getCrossTldKey();
@@ -20,7 +20,6 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import com.google.common.collect.ImmutableList;
import google.registry.schema.domain.RegistryLock;
import java.util.Optional;
import javax.persistence.EntityManager;
/** Data access object for {@link google.registry.schema.domain.RegistryLock}. */
public final class RegistryLockDao {
@@ -34,15 +33,16 @@ public final class RegistryLockDao {
/** Returns the most recent version of the {@link RegistryLock} referred to by the code. */
public static Optional<RegistryLock> getByVerificationCode(String verificationCode) {
jpaTm().assertInTransaction();
EntityManager em = jpaTm().getEntityManager();
Long revisionId =
em.createQuery(
jpaTm()
.query(
"SELECT MAX(revisionId) FROM RegistryLock WHERE verificationCode ="
+ " :verificationCode",
Long.class)
.setParameter("verificationCode", verificationCode)
.getSingleResult();
return Optional.ofNullable(revisionId).map(revision -> em.find(RegistryLock.class, revision));
return Optional.ofNullable(revisionId)
.map(revision -> jpaTm().getEntityManager().find(RegistryLock.class, revision));
}
/** Returns all lock objects that this registrar has created, including pending locks. */
@@ -50,8 +50,7 @@ public final class RegistryLockDao {
jpaTm().assertInTransaction();
return ImmutableList.copyOf(
jpaTm()
.getEntityManager()
.createQuery(
.query(
"SELECT lock FROM RegistryLock lock"
+ " WHERE lock.registrarId = :registrarId"
+ " AND lock.unlockCompletionTimestamp IS NULL"
@@ -69,8 +68,7 @@ public final class RegistryLockDao {
public static Optional<RegistryLock> getMostRecentByRepoId(String repoId) {
jpaTm().assertInTransaction();
return jpaTm()
.getEntityManager()
.createQuery(
.query(
"SELECT lock FROM RegistryLock lock WHERE lock.repoId = :repoId"
+ " ORDER BY lock.revisionId DESC",
RegistryLock.class)
@@ -89,8 +87,7 @@ public final class RegistryLockDao {
public static Optional<RegistryLock> getMostRecentVerifiedLockByRepoId(String repoId) {
jpaTm().assertInTransaction();
return jpaTm()
.getEntityManager()
.createQuery(
.query(
"SELECT lock FROM RegistryLock lock WHERE lock.repoId = :repoId AND"
+ " lock.lockCompletionTimestamp IS NOT NULL AND"
+ " lock.unlockCompletionTimestamp IS NULL ORDER BY lock.revisionId"
@@ -111,8 +108,7 @@ public final class RegistryLockDao {
public static Optional<RegistryLock> getMostRecentVerifiedUnlockByRepoId(String repoId) {
jpaTm().assertInTransaction();
return jpaTm()
.getEntityManager()
.createQuery(
.query(
"SELECT lock FROM RegistryLock lock WHERE lock.repoId = :repoId AND"
+ " lock.unlockCompletionTimestamp IS NOT NULL ORDER BY lock.revisionId"
+ " DESC",
@@ -35,6 +35,7 @@ import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.common.EntityGroupRoot;
import google.registry.model.registry.Registry;
import google.registry.model.registry.label.ReservedList.ReservedListEntry;
@@ -59,6 +60,7 @@ import org.joda.time.DateTime;
* must subclass {@link DomainLabelEntry}.
*/
@MappedSuperclass
@InCrossTld
public abstract class BaseDomainLabelList<T extends Comparable<?>, R extends DomainLabelEntry<T, ?>>
extends ImmutableObject implements Buildable {
@@ -31,6 +31,7 @@ import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.registry.Registry;
import google.registry.schema.replay.DatastoreOnlyEntity;
@@ -96,6 +97,7 @@ public final class PremiumList extends BaseDomainLabelList<Money, PremiumList.Pr
/** Virtual parent entity for premium list entry entities associated with a single revision. */
@ReportedOn
@Entity
@InCrossTld
public static class PremiumListRevision extends ImmutableObject implements DatastoreOnlyEntity {
@Parent Key<PremiumList> parent;
@@ -195,6 +197,7 @@ public final class PremiumList extends BaseDomainLabelList<Money, PremiumList.Pr
*/
@ReportedOn
@Entity
@InCrossTld
public static class PremiumListEntry extends DomainLabelEntry<Money, PremiumListEntry>
implements Buildable, DatastoreOnlyEntity {
@@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.config.RegistryConfig.getDomainLabelListCacheDuration;
import static google.registry.model.registry.label.ReservationType.FULLY_BLOCKED;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.nullToEmpty;
import static org.joda.time.DateTimeZone.UTC;
@@ -36,6 +35,7 @@ import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Mapify;
import com.googlecode.objectify.mapper.Mapper;
import google.registry.model.Buildable;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.registry.Registry;
import google.registry.model.registry.label.DomainLabelMetrics.MetricsReservedListMatch;
import google.registry.schema.replay.NonReplicatedEntity;
@@ -63,6 +63,7 @@ import org.joda.time.DateTime;
* revisionId. This is fine though, because we only use the list with the highest revisionId.
*/
@Entity
@ReportedOn
@javax.persistence.Entity
@Table(indexes = {@Index(columnList = "name", name = "reservedlist_name_idx")})
public final class ReservedList
@@ -247,9 +248,7 @@ public final class ReservedList
new CacheLoader<String, ReservedList>() {
@Override
public ReservedList load(String listName) {
return tm().isOfy()
? ReservedListDualDatabaseDao.getLatestRevision(listName).orElse(null)
: ReservedListSqlDao.getLatestRevision(listName).orElse(null);
return ReservedListDualDatabaseDao.getLatestRevision(listName).orElse(null);
}
});
@@ -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.registry.label;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import com.googlecode.objectify.Key;
import google.registry.persistence.VKey;
import java.util.Optional;
/** A {@link ReservedList} DAO for Datastore. */
public class ReservedListDatastoreDao {
private ReservedListDatastoreDao() {}
/** Persist a new reserved list to Datastore. */
public static void save(ReservedList reservedList) {
ofyTm().transact(() -> ofyTm().put(reservedList));
}
/** Delete a reserved list from Datastore. */
public static void delete(ReservedList reservedList) {
ofyTm().transact(() -> ofyTm().delete(reservedList));
}
/**
* Returns the most recent revision of the {@link ReservedList} with the specified name, if it
* exists.
*/
public static Optional<ReservedList> getLatestRevision(String reservedListName) {
return ofyTm()
.loadByKeyIfPresent(
VKey.createOfy(
ReservedList.class,
Key.create(getCrossTldKey(), ReservedList.class, reservedListName)));
}
}
@@ -15,43 +15,55 @@
package google.registry.model.registry.label;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.model.DatabaseMigrationUtils.isDatastore;
import com.google.common.collect.MapDifference;
import com.google.common.collect.MapDifference.ValueDifference;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import com.googlecode.objectify.Key;
import google.registry.model.DatabaseMigrationUtils;
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
import google.registry.model.registry.label.ReservedList.ReservedListEntry;
import google.registry.persistence.VKey;
import java.util.Map;
import java.util.Optional;
/**
* A {@link ReservedList} DAO that does dual-write and dual-read against Datastore and Cloud SQL. It
* still uses Datastore as the primary storage and suppresses any exception thrown by Cloud SQL.
* A {@link ReservedList} DAO that does dual-write and dual-read against Datastore and Cloud SQL.
*
* <p>TODO(b/160993806): Delete this DAO and switch to use the SQL only DAO after migrating to Cloud
* SQL.
*/
public class ReservedListDualDatabaseDao {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private ReservedListDualDatabaseDao() {}
/** Persist a new reserved list to Cloud SQL. */
/** Persist a new reserved list to the database. */
public static void save(ReservedList reservedList) {
ofyTm().transact(() -> ofyTm().put(reservedList));
logger.atInfo().log("Saving reserved list %s to Cloud SQL", reservedList.getName());
DatabaseMigrationUtils.suppressExceptionUnlessInTest(
() -> ReservedListSqlDao.save(reservedList),
"Error saving the reserved list to Cloud SQL.");
logger.atInfo().log(
"Saved reserved list %s with %d entries to Cloud SQL",
reservedList.getName(), reservedList.getReservedListEntries().size());
if (isDatastore(TransitionId.DOMAIN_LABEL_LISTS)) {
ReservedListDatastoreDao.save(reservedList);
DatabaseMigrationUtils.suppressExceptionUnlessInTest(
() -> ReservedListSqlDao.save(reservedList),
"Error saving the reserved list to Cloud SQL.");
} else {
ReservedListSqlDao.save(reservedList);
DatabaseMigrationUtils.suppressExceptionUnlessInTest(
() -> ReservedListDatastoreDao.save(reservedList),
"Error saving the reserved list to Datastore.");
}
}
/** Delete a reserved list from both databases. */
public static void delete(ReservedList reservedList) {
if (isDatastore(TransitionId.DOMAIN_LABEL_LISTS)) {
ReservedListDatastoreDao.delete(reservedList);
DatabaseMigrationUtils.suppressExceptionUnlessInTest(
() -> ReservedListSqlDao.delete(reservedList),
"Error deleting the reserved list from Cloud SQL.");
} else {
ReservedListSqlDao.delete(reservedList);
DatabaseMigrationUtils.suppressExceptionUnlessInTest(
() -> ReservedListDatastoreDao.delete(reservedList),
"Error deleting the reserved list from Datastore.");
}
}
/**
@@ -59,63 +71,88 @@ public class ReservedListDualDatabaseDao {
* exists.
*/
public static Optional<ReservedList> getLatestRevision(String reservedListName) {
Optional<ReservedList> maybeDatastoreList =
ofyTm()
.loadByKeyIfPresent(
VKey.createOfy(
ReservedList.class,
Key.create(getCrossTldKey(), ReservedList.class, reservedListName)));
// Also load the list from Cloud SQL, compare the two lists, and log if different.
Optional<ReservedList> maybePrimaryList =
isDatastore(TransitionId.DOMAIN_LABEL_LISTS)
? ReservedListDatastoreDao.getLatestRevision(reservedListName)
: ReservedListSqlDao.getLatestRevision(reservedListName);
DatabaseMigrationUtils.suppressExceptionUnlessInTest(
() -> maybeDatastoreList.ifPresent(ReservedListDualDatabaseDao::loadAndCompareCloudSqlList),
() -> maybePrimaryList.ifPresent(primaryList -> loadAndCompare(primaryList)),
"Error comparing reserved lists.");
return maybeDatastoreList;
return maybePrimaryList;
}
private static void loadAndCompareCloudSqlList(ReservedList datastoreList) {
Optional<ReservedList> maybeCloudSqlList =
ReservedListSqlDao.getLatestRevision(datastoreList.getName());
if (maybeCloudSqlList.isPresent()) {
Map<String, ReservedListEntry> datastoreLabelsToReservations =
datastoreList.reservedListMap.entrySet().parallelStream()
.collect(
toImmutableMap(
Map.Entry::getKey,
entry ->
ReservedListEntry.create(
entry.getKey(),
entry.getValue().reservationType,
entry.getValue().comment)));
private static void loadAndCompare(ReservedList primaryList) {
Optional<ReservedList> maybeSecondaryList =
isDatastore(TransitionId.DOMAIN_LABEL_LISTS)
? ReservedListSqlDao.getLatestRevision(primaryList.getName())
: ReservedListDatastoreDao.getLatestRevision(primaryList.getName());
if (!maybeSecondaryList.isPresent()) {
throw new IllegalStateException(
String.format(
"Reserved list in the secondary database (%s) is empty.",
isDatastore(TransitionId.DOMAIN_LABEL_LISTS) ? "Cloud SQL" : "Datastore"));
}
Map<String, ReservedListEntry> labelsToReservations =
primaryList.reservedListMap.entrySet().parallelStream()
.collect(
toImmutableMap(
Map.Entry::getKey,
entry ->
ReservedListEntry.create(
entry.getKey(),
entry.getValue().reservationType,
entry.getValue().comment)));
ReservedList cloudSqlList = maybeCloudSqlList.get();
MapDifference<String, ReservedListEntry> diff =
Maps.difference(datastoreLabelsToReservations, cloudSqlList.reservedListMap);
ReservedList secondaryList = maybeSecondaryList.get();
MapDifference<String, ReservedListEntry> diff =
Maps.difference(labelsToReservations, secondaryList.reservedListMap);
if (!diff.areEqual()) {
if (diff.entriesDiffering().size() > 10) {
throw new IllegalStateException(
String.format(
"Unequal reserved lists detected, Cloud SQL list with revision"
+ " id %d has %d different records than the current"
+ " Datastore list.",
cloudSqlList.getRevisionId(), diff.entriesDiffering().size()));
} else {
throw new IllegalStateException(
String.format(
"Unequal reserved lists detected, %s list with revision"
+ " id %d has %d different records than the current"
+ " primary database list.",
isDatastore(TransitionId.DOMAIN_LABEL_LISTS) ? "Cloud SQL" : "Datastore",
secondaryList.getRevisionId(),
diff.entriesDiffering().size()));
}
StringBuilder diffMessage = new StringBuilder("Unequal reserved lists detected:\n");
diff.entriesDiffering().entrySet().stream()
.forEach(
entry -> {
String label = entry.getKey();
ValueDifference<ReservedListEntry> valueDiff = entry.getValue();
diffMessage.append(
String.format(
"Domain label %s has entry %s in Datastore and entry"
+ " %s in Cloud SQL.\n",
label, valueDiff.leftValue(), valueDiff.rightValue()));
});
diff.entriesDiffering().entrySet().stream()
.forEach(
entry -> {
String label = entry.getKey();
ValueDifference<ReservedListEntry> valueDiff = entry.getValue();
diffMessage.append(
String.format(
"Domain label %s has entry %s in %s and entry"
+ " %s in the secondary database.\n",
label,
valueDiff.leftValue(),
isDatastore(TransitionId.DOMAIN_LABEL_LISTS) ? "Datastore" : "Cloud SQL",
valueDiff.rightValue()));
});
diff.entriesOnlyOnLeft().entrySet().stream()
.forEach(
entry -> {
String label = entry.getKey();
diffMessage.append(
String.format(
"Domain label %s has entry in %s, but not in the secondary database.\n",
label,
isDatastore(TransitionId.DOMAIN_LABEL_LISTS) ? "Datastore" : "Cloud SQL"));
});
diff.entriesOnlyOnRight().entrySet().stream()
.forEach(
entry -> {
String label = entry.getKey();
diffMessage.append(
String.format(
"Domain label %s has entry in %s, but not in the primary database.\n",
label,
isDatastore(TransitionId.DOMAIN_LABEL_LISTS) ? "Cloud SQL" : "Datastore"));
});
throw new IllegalStateException(diffMessage.toString());
}
}
} else {
throw new IllegalStateException("Reserved list in Cloud SQL is empty.");
}
}
}
@@ -17,6 +17,7 @@ package google.registry.model.registry.label;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.flogger.FluentLogger;
import java.util.Optional;
/**
@@ -26,12 +27,23 @@ import java.util.Optional;
*/
public class ReservedListSqlDao {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private ReservedListSqlDao() {}
/** Persist a new reserved list to Cloud SQL. */
public static void save(ReservedList reservedList) {
checkArgumentNotNull(reservedList, "Must specify reservedList");
logger.atInfo().log("Saving reserved list %s to Cloud SQL", reservedList.getName());
jpaTm().transact(() -> jpaTm().insert(reservedList));
logger.atInfo().log(
"Saved reserved list %s with %d entries to Cloud SQL",
reservedList.getName(), reservedList.getReservedListEntries().size());
}
/** Deletes a reserved list from Cloud SQL. */
public static void delete(ReservedList reservedList) {
jpaTm().transact(() -> jpaTm().delete(reservedList));
}
/**
@@ -43,8 +55,7 @@ public class ReservedListSqlDao {
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
.query(
"FROM ReservedList rl LEFT JOIN FETCH rl.reservedListMap WHERE"
+ " rl.revisionId IN (SELECT MAX(revisionId) FROM ReservedList subrl"
+ " WHERE subrl.name = :name)",
@@ -64,8 +75,7 @@ public class ReservedListSqlDao {
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery("SELECT 1 FROM ReservedList WHERE name = :name", Integer.class)
.query("SELECT 1 FROM ReservedList WHERE name = :name", Integer.class)
.setParameter("name", reservedListName)
.setMaxResults(1)
.getResultList()
@@ -31,7 +31,6 @@ import google.registry.model.host.HostHistory;
import google.registry.model.host.HostResource;
import google.registry.persistence.VKey;
import java.util.Comparator;
import javax.persistence.EntityManager;
import org.joda.time.DateTime;
/**
@@ -90,15 +89,14 @@ public class HistoryEntryDao {
VKey<? extends EppResource> parentKey, DateTime afterTime, DateTime beforeTime) {
Class<? extends HistoryEntry> historyClass = getHistoryClassFromParent(parentKey.getKind());
String repoIdFieldName = getRepoIdFieldNameFromHistoryClass(historyClass);
EntityManager entityManager = jpaTm().getEntityManager();
String tableName = entityManager.getMetamodel().entity(historyClass).getName();
String tableName = jpaTm().getEntityManager().getMetamodel().entity(historyClass).getName();
String queryString =
String.format(
"SELECT entry FROM %s entry WHERE entry.modificationTime >= :afterTime AND "
+ "entry.modificationTime <= :beforeTime AND entry.%s = :parentKey",
tableName, repoIdFieldName);
return entityManager
.createQuery(queryString, historyClass)
return jpaTm()
.query(queryString, historyClass)
.setParameter("afterTime", afterTime)
.setParameter("beforeTime", beforeTime)
.setParameter("parentKey", parentKey.getSqlKey().toString())
@@ -129,13 +127,12 @@ public class HistoryEntryDao {
private static Iterable<? extends HistoryEntry> loadAllHistoryObjectsFromSql(
Class<? extends HistoryEntry> historyClass, DateTime afterTime, DateTime beforeTime) {
EntityManager entityManager = jpaTm().getEntityManager();
return entityManager
.createQuery(
return jpaTm()
.query(
String.format(
"SELECT entry FROM %s entry WHERE entry.modificationTime >= :afterTime AND "
+ "entry.modificationTime <= :beforeTime",
entityManager.getMetamodel().entity(historyClass).getName()),
jpaTm().getEntityManager().getMetamodel().entity(historyClass).getName()),
historyClass)
.setParameter("afterTime", afterTime)
.setParameter("beforeTime", beforeTime)
@@ -32,8 +32,7 @@ public class Spec11ThreatMatchDao {
public static void deleteEntriesByDate(JpaTransactionManager jpaTm, LocalDate date) {
jpaTm.assertInTransaction();
jpaTm
.getEntityManager()
.createQuery("DELETE FROM Spec11ThreatMatch WHERE check_date = :date")
.query("DELETE FROM Spec11ThreatMatch WHERE check_date = :date")
.setParameter("date", DateTimeUtils.toSqlDate(date), TemporalType.DATE)
.executeUpdate();
}
@@ -44,8 +43,7 @@ public class Spec11ThreatMatchDao {
jpaTm.assertInTransaction();
return ImmutableList.copyOf(
jpaTm
.getEntityManager()
.createQuery(
.query(
"SELECT match FROM Spec11ThreatMatch match WHERE match.checkDate = :date",
Spec11ThreatMatch.class)
.setParameter("date", date)
@@ -21,6 +21,7 @@ import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.common.EntityGroupRoot;
import google.registry.schema.replay.DatastoreOnlyEntity;
@@ -28,6 +29,7 @@ import google.registry.schema.replay.DatastoreOnlyEntity;
/** Pointer to the latest {@link KmsSecretRevision}. */
@Entity
@ReportedOn
@InCrossTld
public class KmsSecret extends ImmutableObject implements DatastoreOnlyEntity {
/** The unique name of this {@link KmsSecret}. */
@@ -26,6 +26,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.annotations.InCrossTld;
import google.registry.model.annotations.ReportedOn;
import google.registry.schema.replay.NonReplicatedEntity;
import javax.persistence.Column;
@@ -58,6 +59,7 @@ import javax.persistence.Transient;
@ReportedOn
@javax.persistence.Entity(name = "KmsSecret")
@Table(indexes = {@Index(columnList = "secretName")})
@InCrossTld
public class KmsSecretRevision extends ImmutableObject implements NonReplicatedEntity {
/**
@@ -42,8 +42,7 @@ public class KmsSecretRevisionSqlDao {
checkArgument(!isNullOrEmpty(secretName), "secretName cannot be null or empty");
jpaTm().assertInTransaction();
return jpaTm()
.getEntityManager()
.createQuery(
.query(
"FROM KmsSecret ks WHERE ks.revisionKey IN (SELECT MAX(revisionKey) FROM "
+ "KmsSecret subKs WHERE subKs.secretName = :secretName)",
KmsSecretRevision.class)
@@ -30,6 +30,7 @@ import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.OnSave;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.annotations.NotBackedUp;
import google.registry.model.annotations.NotBackedUp.Reason;
import google.registry.model.common.EntityGroupRoot;
@@ -67,6 +68,7 @@ import org.joda.time.DateTime;
@Entity
@javax.persistence.Entity
@NotBackedUp(reason = Reason.EXTERNALLY_SOURCED)
@InCrossTld
public class SignedMarkRevocationList extends ImmutableObject implements NonReplicatedEntity {
@VisibleForTesting static final int SHARD_SIZE = 10000;
@@ -40,7 +40,6 @@ import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
import google.registry.util.CollectionUtils;
import java.util.Map;
import java.util.Optional;
import javax.persistence.EntityManager;
import org.joda.time.DateTime;
public class SignedMarkRevocationListDao {
@@ -153,11 +152,12 @@ public class SignedMarkRevocationListDao {
return jpaTm()
.transact(
() -> {
EntityManager em = jpaTm().getEntityManager();
Long revisionId =
em.createQuery("SELECT MAX(revisionId) FROM SignedMarkRevocationList", Long.class)
jpaTm()
.query("SELECT MAX(revisionId) FROM SignedMarkRevocationList", Long.class)
.getSingleResult();
return em.createQuery(
return jpaTm()
.query(
"FROM SignedMarkRevocationList smrl LEFT JOIN FETCH smrl.revokes "
+ "WHERE smrl.revisionId = :revisionId",
SignedMarkRevocationList.class)
@@ -196,7 +196,7 @@ public class SignedMarkRevocationListDao {
}
private static void saveToCloudSql(SignedMarkRevocationList signedMarkRevocationList) {
jpaTm().transact(() -> jpaTm().getEntityManager().persist(signedMarkRevocationList));
jpaTm().transact(() -> jpaTm().insert(signedMarkRevocationList));
logger.atInfo().log(
"Inserted %,d signed mark revocations into Cloud SQL.",
signedMarkRevocationList.revokes.size());
@@ -1,89 +0,0 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.tmch;
import static google.registry.config.RegistryConfig.getDomainLabelListCacheDuration;
import static google.registry.model.CacheUtils.tryMemoizeWithExpiration;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import com.google.common.base.Supplier;
import com.google.common.flogger.FluentLogger;
import google.registry.util.NonFinalForTesting;
import java.util.Optional;
import javax.persistence.EntityManager;
/** Data access object for {@link ClaimsListShard}. */
public class ClaimsListDao {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** In-memory cache for claims list. */
@NonFinalForTesting
private static Supplier<Optional<ClaimsListShard>> cacheClaimsList =
tryMemoizeWithExpiration(getDomainLabelListCacheDuration(), ClaimsListDao::getLatestRevision);
private static void save(ClaimsListShard claimsList) {
jpaTm().transact(() -> jpaTm().getEntityManager().persist(claimsList));
}
/**
* Try to save the given {@link ClaimsListShard} into Cloud SQL. If the save fails, the error will
* be logged but no exception will be thrown.
*
* <p>This method is used during the dual-write phase of database migration as Datastore is still
* the authoritative database.
*/
static void trySave(ClaimsListShard claimsList) {
try {
ClaimsListDao.save(claimsList);
logger.atInfo().log(
"Inserted %,d claims into Cloud SQL, created at %s",
claimsList.getLabelsToKeys().size(), claimsList.getTmdbGenerationTime());
} catch (Throwable e) {
logger.atSevere().withCause(e).log("Error inserting claims into Cloud SQL");
}
}
/**
* Returns the most recent revision of the {@link ClaimsListShard} in Cloud SQL, if it exists.
* TODO(b/177569979): Change this method to package level access after dual-read phase.
* ClaimsListShard uses this method to retrieve claims list in Cloud SQL for the comparison, and
* ClaimsListShard is not in this package.
*/
public static Optional<ClaimsListShard> getLatestRevision() {
return jpaTm()
.transact(
() -> {
EntityManager em = jpaTm().getEntityManager();
Long revisionId =
em.createQuery("SELECT MAX(revisionId) FROM ClaimsList", Long.class)
.getSingleResult();
return em.createQuery(
"FROM ClaimsList cl LEFT JOIN FETCH cl.labelsToKeys WHERE cl.revisionId ="
+ " :revisionId",
ClaimsListShard.class)
.setParameter("revisionId", revisionId)
.getResultStream()
.findFirst();
});
}
/** Returns the most recent revision of the {@link ClaimsListShard}, from cache. */
public static Optional<ClaimsListShard> getLatestRevisionCached() {
return cacheClaimsList.get();
}
private ClaimsListDao() {}
}
@@ -0,0 +1,148 @@
// 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.tmch;
import static google.registry.config.RegistryConfig.getDomainLabelListCacheDuration;
import static google.registry.model.CacheUtils.tryMemoizeWithExpiration;
import static google.registry.model.DatabaseMigrationUtils.isDatastore;
import static google.registry.model.DatabaseMigrationUtils.suppressExceptionUnlessInTest;
import static google.registry.model.common.DatabaseTransitionSchedule.TransitionId.CLAIMS_LIST;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import google.registry.util.NonFinalForTesting;
import java.util.Optional;
/**
* DAO for {@link ClaimsListShard} objects that handles the branching paths for SQL and Datastore.
*
* <p>For write actions, this class will perform the action against the primary database then, after
* * that success or failure, against the secondary database. If the secondary database fails, an
* error is logged (but not thrown).
*
* <p>For read actions, we will log if the primary and secondary databases * have different values
* (or if the retrieval from the second database fails).
*/
public class ClaimsListDualDatabaseDao {
/** In-memory cache for claims list. */
@NonFinalForTesting
private static Supplier<ClaimsListShard> claimsListCache =
tryMemoizeWithExpiration(
getDomainLabelListCacheDuration(), ClaimsListDualDatabaseDao::getUncached);
/**
* Saves the given {@link ClaimsListShard} to both the primary and secondary databases, logging
* and skipping errors in the secondary DB.
*/
public static void save(ClaimsListShard claimsList) {
if (isDatastore(CLAIMS_LIST)) {
claimsList.saveToDatastore();
suppressExceptionUnlessInTest(
() -> ClaimsListSqlDao.save(claimsList), "Error saving ClaimsList to SQL.");
} else {
ClaimsListSqlDao.save(claimsList);
suppressExceptionUnlessInTest(
claimsList::saveToDatastore, "Error saving ClaimsListShard to Datastore.");
}
}
/** Returns the most recent revision of the {@link ClaimsListShard}, from cache. */
public static ClaimsListShard get() {
return claimsListCache.get();
}
/** Retrieves and compares the latest revision from the databases. */
private static ClaimsListShard getUncached() {
Optional<ClaimsListShard> primaryResult;
if (isDatastore(CLAIMS_LIST)) {
primaryResult = ClaimsListShard.getFromDatastore();
suppressExceptionUnlessInTest(
() -> {
Optional<ClaimsListShard> secondaryResult = ClaimsListSqlDao.get();
compareClaimsLists(primaryResult, secondaryResult);
},
"Error loading ClaimsList from SQL.");
} else {
primaryResult = ClaimsListSqlDao.get();
suppressExceptionUnlessInTest(
() -> {
Optional<ClaimsListShard> secondaryResult = ClaimsListShard.getFromDatastore();
compareClaimsLists(primaryResult, secondaryResult);
},
"Error loading ClaimsListShard from Datastore.");
}
return primaryResult.orElse(ClaimsListShard.create(START_OF_TIME, ImmutableMap.of()));
}
private static void compareClaimsLists(
Optional<ClaimsListShard> maybePrimary, Optional<ClaimsListShard> maybeSecondary) {
if (maybePrimary.isPresent() && !maybeSecondary.isPresent()) {
throw new IllegalStateException("Claims list found in primary DB but not in secondary DB.");
}
if (!maybePrimary.isPresent() && maybeSecondary.isPresent()) {
throw new IllegalStateException("Claims list found in secondary DB but not in primary DB.");
}
if (!maybePrimary.isPresent()) {
return;
}
ClaimsListShard primary = maybePrimary.get();
ClaimsListShard secondary = maybeSecondary.get();
MapDifference<String, String> diff =
Maps.difference(primary.labelsToKeys, secondary.getLabelsToKeys());
if (!diff.areEqual()) {
if (diff.entriesDiffering().size()
+ diff.entriesOnlyOnRight().size()
+ diff.entriesOnlyOnLeft().size()
> 10) {
throw new IllegalStateException(
String.format(
"Unequal claims lists detected, secondary list with revision id %d has %d"
+ " different records than the current primary list.",
secondary.getRevisionId(), diff.entriesDiffering().size()));
} else {
StringBuilder diffMessage = new StringBuilder("Unequal claims lists detected:\n");
diff.entriesDiffering()
.forEach(
(label, valueDiff) ->
diffMessage.append(
String.format(
"Domain label %s has key %s in the primary DB and key %s "
+ "in the secondary DB.\n",
label, valueDiff.leftValue(), valueDiff.rightValue())));
diff.entriesOnlyOnLeft()
.forEach(
(label, valueDiff) ->
diffMessage.append(
String.format(
"Domain label %s with key %s only appears in the primary DB.\n",
label, valueDiff)));
diff.entriesOnlyOnRight()
.forEach(
(label, valueDiff) ->
diffMessage.append(
String.format(
"Domain label %s with key %s only appears in the secondary DB.\n",
label, valueDiff)));
throw new IllegalStateException(diffMessage.toString());
}
}
}
private ClaimsListDualDatabaseDao() {}
}
@@ -18,19 +18,13 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Throwables.throwIfUnchecked;
import static com.google.common.base.Verify.verify;
import static google.registry.model.CacheUtils.memoizeWithShortExpiration;
import static google.registry.model.ofy.ObjectifyService.allocateId;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.MapDifference;
import com.google.common.collect.MapDifference.ValueDifference;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.EmbedMap;
@@ -41,6 +35,7 @@ import com.googlecode.objectify.annotation.OnSave;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.annotations.NotBackedUp;
import google.registry.model.annotations.NotBackedUp.Reason;
import google.registry.model.annotations.VirtualEntity;
@@ -71,8 +66,8 @@ import org.joda.time.DateTime;
* A list of TMCH claims labels and their associated claims keys.
*
* <p>The claims list is actually sharded into multiple {@link ClaimsListShard} entities to work
* around the Datastore limitation of 1M max size per entity. However, when calling {@link #get} all
* of the shards are recombined into one {@link ClaimsListShard} object.
* around the Datastore limitation of 1M max size per entity. However, when calling {@link
* #getFromDatastore} all of the shards are recombined into one {@link ClaimsListShard} object.
*
* <p>ClaimsList shards are tied to a specific revision and are persisted individually, then the
* entire claims list is atomically shifted over to using the new shards by persisting the new
@@ -95,10 +90,9 @@ import org.joda.time.DateTime;
@NotBackedUp(reason = Reason.EXTERNALLY_SOURCED)
@javax.persistence.Entity(name = "ClaimsList")
@Table
@InCrossTld
public class ClaimsListShard extends ImmutableObject implements NonReplicatedEntity {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** The number of claims list entries to store per shard. */
private static final int SHARD_SIZE = 10000;
@@ -143,107 +137,57 @@ public class ClaimsListShard extends ImmutableObject implements NonReplicatedEnt
private static final Retrier LOADER_RETRIER = new Retrier(new SystemSleeper(), 2);
private static ClaimsListShard loadClaimsListShard() {
private static Optional<ClaimsListShard> loadClaimsListShard() {
// Find the most recent revision.
Key<ClaimsListRevision> revisionKey = getCurrentRevision();
if (revisionKey == null) {
return Optional.empty();
}
Map<String, String> combinedLabelsToKeys = new HashMap<>();
DateTime creationTime = START_OF_TIME;
if (revisionKey != null) {
// Grab all of the keys for the shards that belong to the current revision.
final List<Key<ClaimsListShard>> shardKeys =
ofy().load().type(ClaimsListShard.class).ancestor(revisionKey).keys().list();
// Grab all of the keys for the shards that belong to the current revision.
final List<Key<ClaimsListShard>> shardKeys =
ofy().load().type(ClaimsListShard.class).ancestor(revisionKey).keys().list();
List<ClaimsListShard> shards;
try {
// Load all of the shards concurrently, each in a separate transaction.
shards =
Concurrent.transform(
shardKeys,
key ->
tm().transactNewReadOnly(
() -> {
ClaimsListShard claimsListShard = ofy().load().key(key).now();
checkState(
claimsListShard != null,
"Key not found when loading claims list shards.");
return claimsListShard;
}));
} catch (UncheckedExecutionException e) {
// We retry on IllegalStateException. However, there's a checkState inside the
// Concurrent.transform, so if it's thrown it'll be wrapped in an
// UncheckedExecutionException. We want to unwrap it so it's caught by the retrier.
if (e.getCause() != null) {
throwIfUnchecked(e.getCause());
}
throw e;
}
// Combine the shards together and return the concatenated ClaimsList.
if (!shards.isEmpty()) {
creationTime = shards.get(0).creationTime;
for (ClaimsListShard shard : shards) {
combinedLabelsToKeys.putAll(shard.labelsToKeys);
checkState(
creationTime.equals(shard.creationTime),
"Inconsistent claims list shard creation times.");
}
}
}
ClaimsListShard datastoreList = create(creationTime, ImmutableMap.copyOf(combinedLabelsToKeys));
// Also load the list from Cloud SQL, compare the two lists, and log if different.
List<ClaimsListShard> shards;
try {
loadAndCompareCloudSqlList(datastoreList);
} catch (Throwable t) {
logger.atSevere().withCause(t).log("Error comparing claims lists.");
}
return datastoreList;
};
private static void loadAndCompareCloudSqlList(ClaimsListShard datastoreList) {
Optional<ClaimsListShard> maybeCloudSqlList = ClaimsListDao.getLatestRevision();
if (maybeCloudSqlList.isPresent()) {
ClaimsListShard cloudSqlList = maybeCloudSqlList.get();
MapDifference<String, String> diff =
Maps.difference(datastoreList.labelsToKeys, cloudSqlList.getLabelsToKeys());
if (!diff.areEqual()) {
if (diff.entriesDiffering().size() > 10) {
logger.atWarning().log(
String.format(
"Unequal claims lists detected, Cloud SQL list with revision id %d has %d"
+ " different records than the current Datastore list.",
cloudSqlList.getRevisionId(), diff.entriesDiffering().size()));
} else {
StringBuilder diffMessage = new StringBuilder("Unequal claims lists detected:\n");
diff.entriesDiffering().entrySet().stream()
.forEach(
entry -> {
String label = entry.getKey();
ValueDifference<String> valueDiff = entry.getValue();
diffMessage.append(
String.format(
"Domain label %s has key %s in Datastore and key %s in Cloud"
+ " SQL.\n",
label, valueDiff.leftValue(), valueDiff.rightValue()));
});
logger.atWarning().log(diffMessage.toString());
}
// Load all of the shards concurrently, each in a separate transaction.
shards =
Concurrent.transform(
shardKeys,
key ->
tm().transactNewReadOnly(
() -> {
ClaimsListShard claimsListShard = ofy().load().key(key).now();
checkState(
claimsListShard != null,
"Key not found when loading claims list shards.");
return claimsListShard;
}));
} catch (UncheckedExecutionException e) {
// We retry on IllegalStateException. However, there's a checkState inside the
// Concurrent.transform, so if it's thrown it'll be wrapped in an
// UncheckedExecutionException. We want to unwrap it so it's caught by the retrier.
if (e.getCause() != null) {
throwIfUnchecked(e.getCause());
}
} else {
logger.atWarning().log("Claims list in Cloud SQL is empty.");
throw e;
}
}
/**
* A cached supplier that fetches the claims list shards from Datastore and recombines them into a
* single {@link ClaimsListShard} object.
*/
private static final Supplier<ClaimsListShard> CACHE =
memoizeWithShortExpiration(
() ->
LOADER_RETRIER.callWithRetry(
ClaimsListShard::loadClaimsListShard, IllegalStateException.class));
// Combine the shards together and return the concatenated ClaimsList.
if (!shards.isEmpty()) {
creationTime = shards.get(0).creationTime;
for (ClaimsListShard shard : shards) {
combinedLabelsToKeys.putAll(shard.labelsToKeys);
checkState(
creationTime.equals(shard.creationTime),
"Inconsistent claims list shard creation times.");
}
}
return Optional.of(create(creationTime, ImmutableMap.copyOf(combinedLabelsToKeys)));
}
/** Returns the revision id of this claims list, or throws exception if it is null. */
public Long getRevisionId() {
@@ -286,9 +230,8 @@ public class ClaimsListShard extends ImmutableObject implements NonReplicatedEnt
* Save the Claims list to Datastore by writing the new shards in a series of transactions,
* switching over to using them atomically, then deleting the old ones.
*/
public void save() {
void saveToDatastore() {
saveToDatastore(SHARD_SIZE);
ClaimsListDao.trySave(this);
}
@VisibleForTesting
@@ -301,7 +244,7 @@ public class ClaimsListShard extends ImmutableObject implements NonReplicatedEnt
Concurrent.transform(
CollectionUtils.partitionMap(labelsToKeys, shardSize),
(final ImmutableMap<String, String> labelsToKeysShard) ->
tm().transactNew(
tm().transact(
() -> {
ClaimsListShard shard = create(creationTime, labelsToKeysShard);
shard.isShard = true;
@@ -311,7 +254,7 @@ public class ClaimsListShard extends ImmutableObject implements NonReplicatedEnt
}));
// Persist the new revision, thus causing the newly created shards to go live.
tm().transactNew(
tm().transact(
() -> {
verify(
(getCurrentRevision() == null && oldRevision == null)
@@ -337,9 +280,9 @@ public class ClaimsListShard extends ImmutableObject implements NonReplicatedEnt
}
/** Return a single logical instance that combines all Datastore shards. */
@Nullable
public static ClaimsListShard get() {
return CACHE.get();
static Optional<ClaimsListShard> getFromDatastore() {
return LOADER_RETRIER.callWithRetry(
ClaimsListShard::loadClaimsListShard, IllegalStateException.class);
}
/** As a safety mechanism, fail if someone tries to save this class directly. */
@@ -0,0 +1,53 @@
// 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.tmch;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import java.util.Optional;
/** Data access object for {@link ClaimsListShard}. */
public class ClaimsListSqlDao {
/** Saves the given {@link ClaimsListShard} to Cloud SQL. */
static void save(ClaimsListShard claimsList) {
jpaTm().transact(() -> jpaTm().insert(claimsList));
}
/**
* Returns the most recent revision of the {@link ClaimsListShard} in SQL or an empty list if it
* doesn't exist.
*/
static Optional<ClaimsListShard> get() {
return jpaTm()
.transact(
() -> {
Long revisionId =
jpaTm()
.query("SELECT MAX(revisionId) FROM ClaimsList", Long.class)
.getSingleResult();
return jpaTm()
.query(
"FROM ClaimsList cl LEFT JOIN FETCH cl.labelsToKeys WHERE cl.revisionId ="
+ " :revisionId",
ClaimsListShard.class)
.setParameter("revisionId", revisionId)
.getResultStream()
.findFirst();
});
}
private ClaimsListSqlDao() {}
}
@@ -79,10 +79,7 @@ public final class TmchCrl extends CrossTldSingleton implements NonReplicatedEnt
.transactNew(
() -> {
// Delete the old one and insert the new one
jpaTm()
.getEntityManager()
.createQuery("DELETE FROM TmchCrl")
.executeUpdate();
jpaTm().query("DELETE FROM TmchCrl").executeUpdate();
jpaTm().putWithoutBackup(tmchCrl);
});
});
@@ -15,6 +15,7 @@
package google.registry.model.transfer;
import static google.registry.util.CollectionUtils.forceEmptyToNull;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
@@ -23,6 +24,7 @@ import com.googlecode.objectify.annotation.AlsoLoad;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.IgnoreSave;
import com.googlecode.objectify.annotation.OnLoad;
import com.googlecode.objectify.annotation.Unindex;
import com.googlecode.objectify.condition.IfNull;
import google.registry.model.billing.BillingEvent;
@@ -31,6 +33,7 @@ import google.registry.model.domain.Period;
import google.registry.model.domain.Period.Unit;
import google.registry.model.poll.PollMessage;
import google.registry.persistence.VKey;
import java.util.Set;
import javax.annotation.Nullable;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
@@ -80,7 +83,11 @@ public class DomainTransferData extends TransferData<DomainTransferData.Builder>
@Ignore
@Column(name = "transfer_billing_cancellation_id")
Long billingCancellationId;
public VKey<BillingEvent.Cancellation> billingCancellationId;
@Ignore
@Column(name = "transfer_billing_cancellation_history_id")
Long billingCancellationHistoryId;
/**
* The regular one-time billing event that will be charged for a server-approved transfer.
@@ -146,6 +153,17 @@ public class DomainTransferData extends TransferData<DomainTransferData.Builder>
serverApproveAutorenewPollMessage =
DomainBase.restoreOfyFrom(
rootKey, serverApproveAutorenewPollMessage, serverApproveAutorenewPollMessageHistoryId);
billingCancellationId =
DomainBase.restoreOfyFrom(rootKey, billingCancellationId, billingCancellationHistoryId);
// Reconstruct server approve entities. We currently have to call postLoad() a _second_ time
// if the billing cancellation id has been reconstituted, as it is part of that set.
// TODO(b/183010623): Normalize the approaches to VKey reconstitution for the TransferData
// hierarchy (the logic currently lives either in PostLoad or here, depending on the key).
if (billingCancellationId != null) {
serverApproveEntities = null;
postLoad();
}
}
/**
@@ -176,6 +194,12 @@ public class DomainTransferData extends TransferData<DomainTransferData.Builder>
serverApproveAutorenewPollMessageHistoryId = DomainBase.getHistoryId(val);
}
@SuppressWarnings("unused") // For Hibernate.
private void billingCancellationHistoryId(
@AlsoLoad("billingCancellationHistoryId") VKey<BillingEvent.Cancellation> val) {
billingCancellationHistoryId = DomainBase.getHistoryId(val);
}
public Period getTransferPeriod() {
return transferPeriod;
}
@@ -218,11 +242,20 @@ public class DomainTransferData extends TransferData<DomainTransferData.Builder>
return serverApproveAutorenewPollMessageHistoryId;
}
@OnLoad
@Override
void onLoad(
@AlsoLoad("serverApproveEntities")
Set<VKey<? extends TransferServerApproveEntity>> serverApproveEntities) {
super.onLoad(serverApproveEntities);
mapBillingCancellationEntityToField(serverApproveEntities, this);
}
@PostLoad
@Override
void postLoad() {
// The superclass's serverApproveEntities should include the billing events if present
super.postLoad();
// The superclass's serverApproveEntities should include the billing events if present
ImmutableSet.Builder<VKey<? extends TransferServerApproveEntity>> serverApproveEntitiesBuilder =
new ImmutableSet.Builder<>();
if (serverApproveEntities != null) {
@@ -234,8 +267,8 @@ public class DomainTransferData extends TransferData<DomainTransferData.Builder>
if (serverApproveAutorenewEvent != null) {
serverApproveEntitiesBuilder.add(serverApproveAutorenewEvent);
}
if (serverApproveAutorenewPollMessage != null) {
serverApproveEntitiesBuilder.add(serverApproveAutorenewPollMessage);
if (billingCancellationId != null) {
serverApproveEntitiesBuilder.add(billingCancellationId);
}
serverApproveEntities = forceEmptyToNull(serverApproveEntitiesBuilder.build());
}
@@ -250,6 +283,28 @@ public class DomainTransferData extends TransferData<DomainTransferData.Builder>
return new Builder(clone(this));
}
/** Maps serverApproveEntities set to the individual fields. */
@SuppressWarnings("unchecked")
static void mapBillingCancellationEntityToField(
Set<VKey<? extends TransferServerApproveEntity>> serverApproveEntities,
DomainTransferData domainTransferData) {
if (isNullOrEmpty(serverApproveEntities)) {
domainTransferData.billingCancellationId = null;
domainTransferData.billingCancellationHistoryId = null;
} else {
domainTransferData.billingCancellationId =
(VKey<BillingEvent.Cancellation>)
serverApproveEntities.stream()
.filter(k -> k.getKind().equals(BillingEvent.Cancellation.class))
.findFirst()
.orElse(null);
domainTransferData.billingCancellationHistoryId =
domainTransferData.billingCancellationId != null
? DomainBase.getHistoryId(domainTransferData.billingCancellationId)
: null;
}
}
public static class Builder extends TransferData.Builder<DomainTransferData, Builder> {
/** Create a {@link DomainTransferData.Builder} wrapping a new instance. */
public Builder() {}
@@ -259,6 +314,12 @@ public class DomainTransferData extends TransferData<DomainTransferData.Builder>
super(instance);
}
@Override
public DomainTransferData build() {
mapBillingCancellationEntityToField(getInstance().serverApproveEntities, getInstance());
return super.build();
}
public Builder setTransferPeriod(Period transferPeriod) {
getInstance().transferPeriod = transferPeriod;
return this;
@@ -36,6 +36,8 @@ import google.registry.keyring.api.KeyModule;
import google.registry.keyring.kms.KmsModule;
import google.registry.module.backend.BackendRequestComponent.BackendRequestComponentModule;
import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.persistence.PersistenceModule;
import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.rde.JSchModule;
import google.registry.request.Modules.DatastoreServiceModule;
import google.registry.request.Modules.Jackson2Module;
@@ -71,6 +73,8 @@ import javax.inject.Singleton;
KeyringModule.class,
KmsModule.class,
NetHttpTransportModule.class,
PersistenceModule.class,
SecretManagerModule.class,
ServerTridProviderModule.class,
SheetsServiceModule.class,
StackdriverModule.class,
@@ -30,6 +30,7 @@ import google.registry.batch.RefreshDnsOnHostRenameAction;
import google.registry.batch.RelockDomainAction;
import google.registry.batch.ResaveAllEppResourcesAction;
import google.registry.batch.ResaveEntityAction;
import google.registry.batch.WipeOutCloudSqlAction;
import google.registry.cron.CommitLogFanoutAction;
import google.registry.cron.CronModule;
import google.registry.cron.TldFanoutAction;
@@ -205,6 +206,8 @@ interface BackendRequestComponent {
PublishInvoicesAction uploadInvoicesAction();
WipeOutCloudSqlAction wipeOutCloudSqlAction();
@Subcomponent.Builder
abstract class Builder implements RequestComponentBuilder<BackendRequestComponent> {
@@ -44,10 +44,14 @@ import google.registry.tools.AuthModule.CloudSqlClientCredential;
import google.registry.util.Clock;
import java.lang.annotation.Documented;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import javax.inject.Provider;
import javax.inject.Qualifier;
@@ -163,6 +167,37 @@ public abstract class PersistenceModule {
return ImmutableMap.copyOf(overrides);
}
/**
* Provides a {@link Supplier} of single-use JDBC {@link Connection connections} that can manage
* the database DDL schema.
*/
@Provides
@Singleton
@SchemaManagerConnection
static Supplier<Connection> provideSchemaManagerConnectionSupplier(
SqlCredentialStore credentialStore,
@PartialCloudSqlConfigs ImmutableMap<String, String> cloudSqlConfigs) {
SqlCredential credential =
credentialStore.getCredential(new RobotUser(RobotId.SCHEMA_DEPLOYER));
String user = credential.login();
String password = credential.password();
return () -> createJdbcConnection(user, password, cloudSqlConfigs);
}
private static Connection createJdbcConnection(
String user, String password, ImmutableMap<String, String> cloudSqlConfigs) {
Properties properties = new Properties();
properties.put("user", user);
properties.put("password", password);
properties.put("cloudSqlInstance", cloudSqlConfigs.get(HIKARI_DS_CLOUD_SQL_INSTANCE));
properties.put("socketFactory", cloudSqlConfigs.get(HIKARI_DS_SOCKET_FACTORY));
try {
return DriverManager.getConnection(cloudSqlConfigs.get(Environment.URL), properties);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Provides
@Singleton
@AppEngineJpaTm
@@ -378,6 +413,11 @@ public abstract class PersistenceModule {
}
}
/** Dagger qualifier for JDBC {@link Connection} with schema management privilege. */
@Qualifier
@Documented
public @interface SchemaManagerConnection {}
/** Dagger qualifier for {@link JpaTransactionManager} used for App Engine application. */
@Qualifier
@Documented
@@ -17,6 +17,8 @@ package google.registry.persistence.transaction;
import google.registry.persistence.VKey;
import java.util.function.Supplier;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
/** Sub-interface of {@link TransactionManager} which defines JPA related methods. */
public interface JpaTransactionManager extends TransactionManager {
@@ -24,6 +26,22 @@ public interface JpaTransactionManager extends TransactionManager {
/** Returns the {@link EntityManager} for the current request. */
EntityManager getEntityManager();
/**
* Creates a JPA SQL query for the given query string and result class.
*
* <p>This is a convenience method for the longer <code>
* jpaTm().getEntityManager().createQuery(...)</code>.
*/
<T> TypedQuery<T> query(String sqlString, Class<T> resultClass);
/**
* Creates a JPA SQL query for the given query string (which does not return results).
*
* <p>This is a convenience method for the longer <code>
* jpaTm().getEntityManager().createQuery(...)</code>.
*/
Query query(String sqlString);
/** Executes the work in a transaction with no retries and returns the result. */
<T> T transactNoRetry(Supplier<T> work);
@@ -104,6 +104,16 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
return transactionInfo.get().entityManager;
}
@Override
public <T> TypedQuery<T> query(String sqlString, Class<T> resultClass) {
return getEntityManager().createQuery(sqlString, resultClass);
}
@Override
public Query query(String sqlString) {
return getEntityManager().createQuery(sqlString);
}
@Override
public boolean inTransaction() {
return transactionInfo.get().inTransaction;
@@ -365,8 +375,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
private boolean exists(String entityName, ImmutableSet<EntityId> entityIds) {
assertInTransaction();
TypedQuery<Integer> query =
getEntityManager()
.createQuery(
query(
String.format("SELECT 1 FROM %s WHERE %s", entityName, getAndClause(entityIds)),
Integer.class)
.setMaxResults(1);
@@ -470,7 +479,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
// TODO(b/179158393): use Criteria for query to leave not doubt about sql injection risk.
String sql =
String.format("DELETE FROM %s WHERE %s", entityType.getName(), getAndClause(entityIds));
Query query = getEntityManager().createQuery(sql);
Query query = query(sql);
entityIds.forEach(entityId -> query.setParameter(entityId.name, entityId.value));
transactionInfo.get().addDelete(key);
return query.executeUpdate();
@@ -522,6 +531,11 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
// This is an intended no-op method as there is no session cache in Postgresql.
}
@Override
public boolean isOfy() {
return false;
}
@Override
public <T> void assertDelete(VKey<T> key) {
if (internalDelete(key) != 1) {
@@ -17,7 +17,7 @@ package google.registry.persistence.transaction;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import google.registry.model.ofy.DatastoreTransactionManager;
import google.registry.model.annotations.InCrossTld;
import google.registry.persistence.VKey;
import java.util.NoSuchElementException;
import java.util.Optional;
@@ -130,7 +130,7 @@ public interface TransactionManager {
void putAll(ImmutableCollection<?> entities);
/**
* Persists a new entity or update the existing entity in the database without writing any backup
* Persists a new entity or update the existing entity in the database without writing commit logs
* if the underlying database is Datastore.
*
* <p>This method is for the sake of keeping a single code path when replacing ofy() with tm() in
@@ -142,8 +142,8 @@ public interface TransactionManager {
void putWithoutBackup(Object entity);
/**
* Persists all new entities or update the existing entities in the database without writing any
* backup if the underlying database is Datastore.
* Persists all new entities or update the existing entities in the database without writing
* commit logs if the underlying database is Datastore.
*
* <p>This method is for the sake of keeping a single code path when replacing ofy() with tm() in
* the application code. When the method is invoked with Datastore, it won't write the commit log
@@ -160,7 +160,7 @@ public interface TransactionManager {
void updateAll(ImmutableCollection<?> entities);
/**
* Updates an entity in the database without writing any backup if the underlying database is
* Updates an entity in the database without writing commit logs if the underlying database is
* Datastore.
*
* <p>This method is for the sake of keeping a single code path when replacing ofy() with tm() in
@@ -240,7 +240,9 @@ public interface TransactionManager {
/**
* Returns a stream of all entities of the given type that exist in the database.
*
* <p>The resulting stream is empty if there are no entities of this type.
* <p>The resulting stream is empty if there are no entities of this type. In Datastore mode, if
* the class is a member of the cross-TLD entity group (i.e. if it has the {@link InCrossTld}
* annotation, then the correct ancestor query will automatically be applied.
*/
<T> ImmutableList<T> loadAllOf(Class<T> clazz);
@@ -254,19 +256,19 @@ public interface TransactionManager {
void delete(Object entity);
/**
* Deletes the entity by its id without writing any backup if the underlying database is
* Deletes the entity by its id without writing commit logs if the underlying database is
* Datastore.
*/
void deleteWithoutBackup(VKey<?> key);
/**
* Deletes the set of entities by their key id without writing any backup if the underlying
* Deletes the set of entities by their key id without writing commit logs if the underlying
* database is Datastore.
*/
void deleteWithoutBackup(Iterable<? extends VKey<?>> keys);
/**
* Deletes the given entity from the database without writing any backup if the underlying
* Deletes the given entity from the database without writing commit logs if the underlying
* database is Datastore.
*/
void deleteWithoutBackup(Object entity);
@@ -274,8 +276,6 @@ public interface TransactionManager {
/** Clears the session cache if the underlying database is Datastore, otherwise it is a no-op. */
void clearSessionCache();
/** Returns true if the transaction manager is DatastoreTransactionManager. */
default boolean isOfy() {
return this instanceof DatastoreTransactionManager;
}
/** Returns true if the transaction manager is DatastoreTransactionManager, false otherwise. */
boolean isOfy();
}
@@ -49,6 +49,7 @@ public abstract class SqlUser {
/** Enumerates the {@link RobotUser RobotUsers} in the system. */
public enum RobotId {
NOMULUS,
SCHEMA_DEPLOYER,
/**
* Credential for RegistryTool. This is temporary, and will be removed when tool users are
* assigned their personal credentials.
@@ -26,6 +26,7 @@ import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
@@ -489,40 +490,33 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
.map(VKey::from)
.collect(toImmutableSet());
} else {
// Hibernate does not allow us to query @Converted array fields directly, either
// in the CriteriaQuery or the raw text format. However, Postgres does -- so we
// use native queries to find hosts where any of the inetAddresses match.
StringBuilder queryBuilder =
new StringBuilder(
"SELECT h.repo_id FROM \"Host\" h WHERE :address = ANY(h.inet_addresses) AND "
+ "h.deletion_time = CAST(:endOfTime AS timestamptz)");
ImmutableMap.Builder<String, String> parameters =
new ImmutableMap.Builder<String, String>()
.put("address", InetAddresses.toAddrString(inetAddress))
.put("endOfTime", END_OF_TIME.toString());
if (desiredRegistrar.isPresent()) {
queryBuilder.append(" AND h.current_sponsor_registrar_id = :desiredRegistrar");
parameters.put("desiredRegistrar", desiredRegistrar.get());
}
hostKeys =
jpaTm()
.transact(
() -> {
// Hibernate does not allow us to query @Converted array fields directly, either
// in the CriteriaQuery or the raw text format. However, Postgres does -- so we
// use native queries to find hosts where any of the inetAddresses match.
javax.persistence.Query query;
if (desiredRegistrar.isPresent()) {
query =
jpaTm()
.getEntityManager()
.createNativeQuery(
"SELECT h.repo_id FROM \"Host\" h WHERE :address = "
+ "ANY(h.inet_addresses) AND "
+ "h.current_sponsor_registrar_id = :desiredRegistrar AND "
+ "h.deletion_time = CAST(:endOfTime AS timestamptz)")
.setParameter("desiredRegistrar", desiredRegistrar.get());
} else {
query =
jpaTm()
.getEntityManager()
.createNativeQuery(
"SELECT h.repo_id FROM \"Host\" h WHERE :address = "
+ "ANY(h.inet_addresses) AND "
+ "h.deletion_time = CAST(:endOfTime AS timestamptz)");
}
javax.persistence.Query query =
jpaTm()
.getEntityManager()
.createNativeQuery(queryBuilder.toString())
.setMaxResults(maxNameserversInFirstStage);
parameters.build().forEach(query::setParameter);
@SuppressWarnings("unchecked")
Stream<String> resultStream =
query
.setParameter("address", InetAddresses.toAddrString(inetAddress))
.setParameter("endOfTime", END_OF_TIME.toString())
.setMaxResults(maxNameserversInFirstStage)
.getResultStream();
Stream<String> resultStream = query.getResultStream();
return resultStream
.map(repoId -> VKey.create(HostResource.class, repoId))
.collect(toImmutableSet());
@@ -15,9 +15,12 @@
package google.registry.rdap;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.net.InetAddresses;
@@ -25,6 +28,7 @@ import com.google.common.primitives.Booleans;
import com.googlecode.objectify.cmd.Query;
import google.registry.model.domain.DomainBase;
import google.registry.model.host.HostResource;
import google.registry.persistence.transaction.CriteriaQueryBuilder;
import google.registry.rdap.RdapJsonFormatter.OutputDataType;
import google.registry.rdap.RdapMetrics.EndpointType;
import google.registry.rdap.RdapMetrics.SearchType;
@@ -216,33 +220,90 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
private NameserverSearchResponse searchByNameUsingPrefix(RdapSearchPattern partialStringQuery) {
// Add 1 so we can detect truncation.
int querySizeLimit = getStandardQuerySizeLimit();
Query<HostResource> query =
queryItems(
HostResource.class,
"fullyQualifiedHostName",
partialStringQuery,
cursorString,
getDeletedItemHandling(),
querySizeLimit);
return makeSearchResults(
getMatchingResources(query, shouldIncludeDeleted(), querySizeLimit), CursorType.NAME);
if (isDatastore()) {
Query<HostResource> query =
queryItems(
HostResource.class,
"fullyQualifiedHostName",
partialStringQuery,
cursorString,
getDeletedItemHandling(),
querySizeLimit);
return makeSearchResults(
getMatchingResources(query, shouldIncludeDeleted(), querySizeLimit), CursorType.NAME);
} else {
return jpaTm()
.transact(
() -> {
CriteriaQueryBuilder<HostResource> queryBuilder =
queryItemsSql(
HostResource.class,
"fullyQualifiedHostName",
partialStringQuery,
cursorString,
getDeletedItemHandling());
return makeSearchResults(
getMatchingResourcesSql(queryBuilder, shouldIncludeDeleted(), querySizeLimit),
CursorType.NAME);
});
}
}
/** Searches for nameservers by IP address, returning a JSON array of nameserver info maps. */
private NameserverSearchResponse searchByIp(InetAddress inetAddress) {
// Add 1 so we can detect truncation.
int querySizeLimit = getStandardQuerySizeLimit();
Query<HostResource> query =
queryItems(
HostResource.class,
"inetAddresses",
inetAddress.getHostAddress(),
Optional.empty(),
cursorString,
getDeletedItemHandling(),
querySizeLimit);
return makeSearchResults(
getMatchingResources(query, shouldIncludeDeleted(), querySizeLimit), CursorType.ADDRESS);
RdapResultSet<HostResource> rdapResultSet;
if (isDatastore()) {
Query<HostResource> query =
queryItems(
HostResource.class,
"inetAddresses",
inetAddress.getHostAddress(),
Optional.empty(),
cursorString,
getDeletedItemHandling(),
querySizeLimit);
rdapResultSet = getMatchingResources(query, shouldIncludeDeleted(), querySizeLimit);
} else {
// Hibernate does not allow us to query @Converted array fields directly, either in the
// CriteriaQuery or the raw text format. However, Postgres does -- so we use native queries to
// find hosts where any of the inetAddresses match.
StringBuilder queryBuilder =
new StringBuilder("SELECT * FROM \"Host\" WHERE :address = ANY(inet_addresses)");
ImmutableMap.Builder<String, String> parameters =
new ImmutableMap.Builder<String, String>()
.put("address", InetAddresses.toAddrString(inetAddress));
if (getDeletedItemHandling().equals(DeletedItemHandling.EXCLUDE)) {
queryBuilder.append(" AND deletion_time = CAST(:endOfTime AS timestamptz)");
parameters.put("endOfTime", END_OF_TIME.toString());
}
if (cursorString.isPresent()) {
// cursorString here must be the repo ID
queryBuilder.append(" AND repo_id > :repoId");
parameters.put("repoId", cursorString.get());
}
if (getDesiredRegistrar().isPresent()) {
queryBuilder.append(" AND current_sponsor_registrar_id = :desiredRegistrar");
parameters.put("desiredRegistrar", getDesiredRegistrar().get());
}
queryBuilder.append(" ORDER BY repo_id ASC");
rdapResultSet =
jpaTm()
.transact(
() -> {
javax.persistence.Query query =
jpaTm()
.getEntityManager()
.createNativeQuery(queryBuilder.toString(), HostResource.class)
.setMaxResults(querySizeLimit);
parameters.build().forEach(query::setParameter);
@SuppressWarnings("unchecked")
List<HostResource> resultList = query.getResultList();
return filterResourcesByVisibility(resultList, querySizeLimit);
});
}
return makeSearchResults(rdapResultSet, CursorType.ADDRESS);
}
/** Output JSON for a lists of hosts contained in an {@link RdapResultSet}. */
@@ -214,7 +214,7 @@ public abstract class RdapSearchActionBase extends RdapActionBase {
}
}
private <T extends EppResource> RdapResultSet<T> filterResourcesByVisibility(
protected <T extends EppResource> RdapResultSet<T> filterResourcesByVisibility(
List<T> queryResult, int querySizeLimit) {
// If we are including deleted resources, we need to check that we're authorized for each one.
List<T> resources = new ArrayList<>();
@@ -15,7 +15,7 @@
package google.registry.rde;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.schema.cursor.CursorDao.loadAndCompare;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.flogger.FluentLogger;
import google.registry.model.common.Cursor;
@@ -24,7 +24,6 @@ import google.registry.model.registry.Registry;
import google.registry.request.HttpException.NoContentException;
import google.registry.request.HttpException.ServiceUnavailableException;
import google.registry.request.lock.LockHandler;
import google.registry.schema.cursor.CursorDao;
import google.registry.util.Clock;
import java.util.concurrent.Callable;
import javax.inject.Inject;
@@ -92,7 +91,6 @@ class EscrowTaskRunner {
logger.atInfo().log("TLD: %s", registry.getTld());
DateTime startOfToday = clock.nowUtc().withTimeAtStartOfDay();
Cursor cursor = ofy().load().key(Cursor.createKey(cursorType, registry)).now();
loadAndCompare(cursor, registry.getTldStr());
final DateTime nextRequiredRun = (cursor == null ? startOfToday : cursor.getCursorTime());
if (nextRequiredRun.isAfter(startOfToday)) {
throw new NoContentException("Already completed");
@@ -101,7 +99,7 @@ class EscrowTaskRunner {
task.runWithLock(nextRequiredRun);
DateTime nextRun = nextRequiredRun.plus(interval);
logger.atInfo().log("Rolling cursor forward to %s.", nextRun);
CursorDao.saveCursor(Cursor.create(cursorType, nextRun, registry), registry.getTldStr());
tm().transact(() -> tm().put(Cursor.create(cursorType, nextRun, registry)));
return null;
};
String lockName = String.format("EscrowTaskRunner %s", task.getClass().getSimpleName());
@@ -17,7 +17,6 @@ package google.registry.rde;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.schema.cursor.CursorDao.loadAndCompare;
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import com.google.common.collect.ImmutableSetMultimap;
@@ -28,7 +27,6 @@ import google.registry.model.rde.RdeMode;
import google.registry.model.registry.Registries;
import google.registry.model.registry.Registry;
import google.registry.model.registry.Registry.TldType;
import google.registry.schema.cursor.CursorDao;
import google.registry.util.Clock;
import javax.inject.Inject;
import org.joda.time.DateTime;
@@ -92,12 +90,12 @@ public final class PendingDepositChecker {
}
// Avoid creating a transaction unless absolutely necessary.
Cursor cursor = ofy().load().key(Cursor.createKey(cursorType, registry)).now();
loadAndCompare(cursor, registry.getTldStr());
DateTime cursorValue = (cursor != null ? cursor.getCursorTime() : startingPoint);
if (isBeforeOrAt(cursorValue, now)) {
DateTime watermark = (cursor != null
? cursor.getCursorTime()
: transactionallyInitializeCursor(registry, cursorType, startingPoint));
DateTime watermark =
(cursor != null
? cursor.getCursorTime()
: transactionallyInitializeCursor(registry, cursorType, startingPoint));
if (isBeforeOrAt(watermark, now)) {
builder.put(tld, PendingDeposit.create(tld, watermark, mode, cursorType, interval));
}
@@ -107,18 +105,14 @@ public final class PendingDepositChecker {
}
private DateTime transactionallyInitializeCursor(
final Registry registry,
final CursorType cursorType,
final DateTime initialValue) {
final Registry registry, final CursorType cursorType, final DateTime initialValue) {
return tm().transact(
() -> {
Cursor cursor = ofy().load().key(Cursor.createKey(cursorType, registry)).now();
loadAndCompare(cursor, registry.getTldStr());
if (cursor != null) {
return cursor.getCursorTime();
}
CursorDao.saveCursor(
Cursor.create(cursorType, initialValue, registry), registry.getTldStr());
tm().put(Cursor.create(cursorType, initialValue, registry));
return initialValue;
});
}
@@ -20,7 +20,6 @@ import static google.registry.model.common.Cursor.getCursorTimeOrStartOfTime;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.rde.RdeMode.FULL;
import static google.registry.request.Action.Method.POST;
import static google.registry.schema.cursor.CursorDao.loadAndCompare;
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import com.google.appengine.tools.cloudstorage.GcsFilename;
@@ -79,7 +78,6 @@ public final class RdeReportAction implements Runnable, EscrowTask {
public void runWithLock(DateTime watermark) throws Exception {
Cursor cursor =
ofy().load().key(Cursor.createKey(CursorType.RDE_UPLOAD, Registry.get(tld))).now();
loadAndCompare(cursor, tld);
DateTime cursorTime = getCursorTimeOrStartOfTime(cursor);
if (isBeforeOrAt(cursorTime, watermark)) {
throw new NoContentException(
@@ -22,7 +22,6 @@ import static com.google.common.base.Verify.verify;
import static google.registry.model.common.Cursor.getCursorTimeOrStartOfTime;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.schema.cursor.CursorDao.loadAndCompare;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.appengine.tools.cloudstorage.GcsFilename;
@@ -41,7 +40,6 @@ import google.registry.model.rde.RdeRevision;
import google.registry.model.registry.Registry;
import google.registry.request.RequestParameters;
import google.registry.request.lock.LockHandler;
import google.registry.schema.cursor.CursorDao;
import google.registry.tldconfig.idn.IdnTableEnum;
import google.registry.util.TaskQueueUtils;
import google.registry.xjc.rdeheader.XjcRdeHeader;
@@ -213,7 +211,6 @@ public final class RdeStagingReducer extends Reducer<PendingDeposit, DepositFrag
() -> {
Registry registry = Registry.get(tld);
Cursor cursor = ofy().load().key(Cursor.createKey(key.cursor(), registry)).now();
loadAndCompare(cursor, tld);
DateTime position = getCursorTimeOrStartOfTime(cursor);
checkState(key.interval() != null, "Interval must be present");
DateTime newPosition = key.watermark().plus(key.interval());
@@ -226,8 +223,7 @@ public final class RdeStagingReducer extends Reducer<PendingDeposit, DepositFrag
"Partial ordering of RDE deposits broken: %s %s",
position,
key);
CursorDao.saveCursor(
Cursor.create(key.cursor(), newPosition, registry), registry.getTldStr());
tm().put(Cursor.create(key.cursor(), newPosition, registry));
logger.atInfo().log(
"Rolled forward %s on %s cursor to %s", key.cursor(), tld, newPosition);
RdeRevision.saveRevision(tld, watermark, mode, revision);
@@ -24,7 +24,6 @@ import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.rde.RdeMode.FULL;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static google.registry.schema.cursor.CursorDao.loadAndCompare;
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import static java.util.Arrays.asList;
@@ -53,7 +52,6 @@ import google.registry.request.Parameter;
import google.registry.request.RequestParameters;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.schema.cursor.CursorDao;
import google.registry.util.Clock;
import google.registry.util.Retrier;
import google.registry.util.TaskQueueUtils;
@@ -135,7 +133,6 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
logger.atInfo().log("Verifying readiness to upload the RDE deposit.");
Cursor cursor =
ofy().load().key(Cursor.createKey(CursorType.RDE_STAGING, Registry.get(tld))).now();
loadAndCompare(cursor, tld);
DateTime stagingCursorTime = getCursorTimeOrStartOfTime(cursor);
if (isBeforeOrAt(stagingCursorTime, watermark)) {
throw new NoContentException(
@@ -146,7 +143,6 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
}
Cursor sftpCursor =
ofy().load().key(Cursor.createKey(RDE_UPLOAD_SFTP, Registry.get(tld))).now();
loadAndCompare(sftpCursor, tld);
DateTime sftpCursorTime = getCursorTimeOrStartOfTime(sftpCursor);
Duration timeSinceLastSftp = new Duration(sftpCursorTime, clock.nowUtc());
if (timeSinceLastSftp.isShorterThan(sftpCooldown)) {
@@ -176,11 +172,10 @@ public final class RdeUploadAction implements Runnable, EscrowTask {
logger.atInfo().log(
"Updating RDE cursor '%s' for TLD '%s' following successful upload.", RDE_UPLOAD_SFTP, tld);
tm().transact(
() -> {
CursorDao.saveCursor(
Cursor.create(RDE_UPLOAD_SFTP, tm().getTransactionTime(), Registry.get(tld)),
tld);
});
() ->
tm().put(
Cursor.create(
RDE_UPLOAD_SFTP, tm().getTransactionTime(), Registry.get(tld))));
response.setContentType(PLAIN_TEXT_UTF_8);
response.setPayload(String.format("OK %s %s\n", tld, watermark));
}
@@ -106,14 +106,17 @@ public class IcannReportingStager {
throws ExecutionException, InterruptedException {
// Later views depend on the results of earlier ones, so query everything synchronously
logger.atInfo().log("Generating intermediary view %s", queryName);
bigquery.query(
query,
bigquery.buildDestinationTable(queryName)
.description(String.format(
"An intermediary view to generate %s reports for this month.", reportType))
.type(TableType.VIEW)
.build()
).get();
bigquery
.startQuery(
query,
bigquery
.buildDestinationTable(queryName)
.description(
String.format(
"An intermediary view to generate %s reports for this month.", reportType))
.type(TableType.VIEW)
.build())
.get();
}
private Iterable<String> getHeaders(ImmutableSet<TableFieldSchema> fields) {
@@ -20,7 +20,6 @@ import static google.registry.model.common.Cursor.getCursorTimeOrStartOfTime;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static google.registry.schema.cursor.CursorDao.loadAndCompareAll;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.appengine.tools.cloudstorage.GcsFilename;
@@ -42,7 +41,6 @@ import google.registry.request.HttpException.ServiceUnavailableException;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.request.lock.LockHandler;
import google.registry.schema.cursor.CursorDao;
import google.registry.util.Clock;
import google.registry.util.EmailMessage;
import google.registry.util.Retrier;
@@ -50,7 +48,6 @@ import google.registry.util.SendEmailService;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -188,9 +185,7 @@ public final class IcannReportingUploadAction implements Runnable {
cursorType,
cursorTime.withTimeAtStartOfDay().withDayOfMonth(1).plusMonths(1),
Registry.get(tldStr));
CursorDao.saveCursor(
newCursor,
Optional.ofNullable(tldStr).orElse(google.registry.schema.cursor.Cursor.GLOBAL));
tm().transact(() -> tm().put(newCursor));
}
}
@@ -258,7 +253,6 @@ public final class IcannReportingUploadAction implements Runnable {
}
cursors.put(cursor, registry.getTldStr());
});
loadAndCompareAll(cursors.build(), type);
return cursors.build();
}
@@ -17,6 +17,7 @@ package google.registry.request.auth;
import static google.registry.request.auth.AuthLevel.APP;
import static google.registry.request.auth.AuthLevel.NONE;
import com.google.appengine.api.users.UserService;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
@@ -24,29 +25,15 @@ import javax.servlet.http.HttpServletRequest;
* Authentication mechanism which uses the X-AppEngine-QueueName header set by App Engine for
* internal requests.
*
* <p>
* Task queue push task requests set this header value to the actual queue name. Cron requests set
* this header value to __cron, since that's actually the name of the hidden queue used for cron
* <p>Task queue push task requests set this header value to the actual queue name. Cron requests
* set this header value to __cron, since that's actually the name of the hidden queue used for cron
* requests. Cron also sets the header X-AppEngine-Cron, which we could check, but it's simpler just
* to check the one.
*
* <p>
* App Engine allows app admins to set these headers for testing purposes. This means that this auth
* method is somewhat unreliable - any app admin can access any internal endpoint and pretend to be
* the app itself by setting these headers, which would circumvent any finer-grained authorization
* if we added it in the future (assuming we did not apply it to the app itself). And App Engine's
* concept of an "admin" includes all project owners, editors and viewers. So anyone with access to
* the project will be able to access anything the app itself can access.
*
* <p>
* For now, it's probably okay to allow this behavior, especially since it could indeed be
* convenient for testing. If we wanted to revisit this decision in the future, we have a couple
* options for locking this down:
*
* <ul>
* <li>1. Always include the result of UserService.getCurrentUser() as the active user</li>
* <li>2. Validate that the requests came from special AppEngine internal IPs</li>
* </ul>
* <p>App Engine allows app admins to set these headers for testing purposes. Because of this, we
* also need to verify that the current user is null, indicating that there is no user, to prevent
* access by someone with "admin" privileges. If the user is an admin, UserService presumably must
* return a User object.
*
* <p>See <a href=
* "https://cloud.google.com/appengine/docs/java/taskqueue/push/creating-handlers#reading_request_headers">task
@@ -57,12 +44,16 @@ public class AppEngineInternalAuthenticationMechanism implements AuthenticationM
// As defined in the App Engine request header documentation.
private static final String QUEUE_NAME_HEADER = "X-AppEngine-QueueName";
private UserService userService;
@Inject
public AppEngineInternalAuthenticationMechanism() {}
public AppEngineInternalAuthenticationMechanism(UserService userService) {
this.userService = userService;
}
@Override
public AuthResult authenticate(HttpServletRequest request) {
if (request.getHeader(QUEUE_NAME_HEADER) == null) {
if (request.getHeader(QUEUE_NAME_HEADER) == null || userService.getCurrentUser() != null) {
return AuthResult.create(NONE);
} else {
return AuthResult.create(APP);
@@ -1,124 +0,0 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.schema.cursor;
import static com.google.appengine.api.search.checkers.Preconditions.checkNotNull;
import google.registry.model.ImmutableObject;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.model.common.Cursor.CursorType;
import google.registry.schema.cursor.Cursor.CursorId;
import google.registry.schema.replay.DatastoreEntity;
import google.registry.schema.replay.SqlEntity;
import google.registry.util.DateTimeUtils;
import java.io.Serializable;
import java.time.ZonedDateTime;
import java.util.Optional;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import org.joda.time.DateTime;
/**
* Shared entity for date cursors. This uses a compound primary key as defined in {@link CursorId}.
*/
@Entity
@Table
@IdClass(CursorId.class)
public class Cursor implements SqlEntity {
@Enumerated(EnumType.STRING)
@Column(nullable = false)
@Id
private CursorType type;
@Column @Id private String scope;
@Column(nullable = false)
private ZonedDateTime cursorTime;
@Column(nullable = false)
private UpdateAutoTimestamp lastUpdateTime = UpdateAutoTimestamp.create(null);
/** 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";
private Cursor(CursorType type, String scope, DateTime cursorTime) {
this.type = type;
this.scope = scope;
this.cursorTime = DateTimeUtils.toZonedDateTime(cursorTime);
}
// Hibernate requires a default constructor.
private Cursor() {}
/** Constructs a {@link Cursor} object. */
public static Cursor create(CursorType type, String scope, DateTime cursorTime) {
checkNotNull(
scope, "Scope cannot be null. To create a global cursor, use the createGlobal method");
return new Cursor(type, scope, cursorTime);
}
/** Constructs a {@link Cursor} object with a {@link GLOBAL} scope. */
public static Cursor createGlobal(CursorType type, DateTime cursorTime) {
return new Cursor(type, GLOBAL, cursorTime);
}
/** Returns the type of the cursor. */
public CursorType getType() {
return type;
}
/**
* Returns the scope of the cursor. The scope will typically be the tld the cursor is referring
* to. If the cursor is a global cursor, the scope will be {@link GLOBAL}.
*/
public String getScope() {
return scope;
}
/** Returns the time the cursor is set to. */
public DateTime getCursorTime() {
return DateTimeUtils.toJodaDateTime(cursorTime);
}
/** Returns the last time the cursor was updated. */
public DateTime getLastUpdateTime() {
return lastUpdateTime.getTimestamp();
}
@Override
public Optional<DatastoreEntity> toDatastoreEntity() {
return Optional.empty(); // Cursors are not converted since they are ephemeral
}
static class CursorId extends ImmutableObject implements Serializable {
public CursorType type;
public String scope;
private CursorId() {}
public CursorId(CursorType type, String scope) {
this.type = type;
this.scope = scope;
}
}
}
@@ -1,208 +0,0 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.schema.cursor;
import static com.google.appengine.api.search.checkers.Preconditions.checkNotNull;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import google.registry.model.common.Cursor.CursorType;
import google.registry.schema.cursor.Cursor.CursorId;
import java.util.List;
import javax.annotation.Nullable;
/** Data access object class for {@link Cursor}. */
public class CursorDao {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static void save(Cursor cursor) {
jpaTm()
.transact(
() -> {
jpaTm().getEntityManager().merge(cursor);
});
}
public static void saveAll(ImmutableSet<Cursor> cursors) {
jpaTm()
.transact(
() -> {
for (Cursor cursor : cursors) {
jpaTm().getEntityManager().merge(cursor);
}
});
}
public static Cursor load(CursorType type, String scope) {
checkNotNull(scope, "The scope of the cursor to load cannot be null");
checkNotNull(type, "The type of the cursor to load cannot be null");
return jpaTm()
.transact(() -> jpaTm().getEntityManager().find(Cursor.class, new CursorId(type, scope)));
}
/** If no scope is given, use {@link Cursor#GLOBAL} as the scope. */
public static Cursor load(CursorType type) {
checkNotNull(type, "The type of the cursor to load must be specified");
return load(type, Cursor.GLOBAL);
}
public static List<Cursor> loadAll() {
return jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery("SELECT cursor FROM Cursor cursor", Cursor.class)
.getResultList());
}
public static List<Cursor> loadByType(CursorType type) {
checkNotNull(type, "The type of the cursors to load must be specified");
return jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
"SELECT cursor FROM Cursor cursor WHERE cursor.type = :type", Cursor.class)
.setParameter("type", type)
.getResultList());
}
/**
* This writes the given cursor to Datastore. If the save to Datastore succeeds, then a new
* Schema/Cursor object is created and attempted to save to Cloud SQL. If the save to Cloud SQL
* fails, the exception is logged, but does not cause the method to fail.
*/
public static void saveCursor(google.registry.model.common.Cursor cursor, String scope) {
tm().transact(() -> ofy().save().entity(cursor));
CursorType type = cursor.getType();
checkArgumentNotNull(scope, "The scope of the cursor cannot be null");
Cursor cloudSqlCursor = Cursor.create(type, scope, cursor.getCursorTime());
try {
save(cloudSqlCursor);
logger.atInfo().log(
"Rolled forward CloudSQL cursor for %s to %s.", scope, cursor.getCursorTime());
} catch (Exception e) {
logger.atSevere().withCause(e).log("Error saving cursor to Cloud SQL: %s.", cloudSqlCursor);
}
}
/**
* This takes in multiple cursors and saves them to Datastore. If those saves succeed, it attempts
* to save the cursors to Cloud SQL. If the save to Cloud SQL fails, the exception is logged, but
* does not cause the method to fail.
*/
public static void saveCursors(
ImmutableMap<google.registry.model.common.Cursor, String> cursors) {
// Save the cursors to Datastore
tm().transact(
() -> {
for (google.registry.model.common.Cursor cursor : cursors.keySet()) {
ofy().save().entity(cursor);
}
});
// Try to save the cursors to Cloud SQL
try {
ImmutableSet.Builder<Cursor> cloudSqlCursors = new ImmutableSet.Builder<>();
cursors
.keySet()
.forEach(
cursor ->
cloudSqlCursors.add(
Cursor.create(
cursor.getType(), cursors.get(cursor), cursor.getCursorTime())));
saveAll(cloudSqlCursors.build());
} catch (Exception e) {
logger.atSevere().withCause(e).log("Error saving cursor to Cloud SQL.");
}
}
/**
* Loads in cursor from Cloud SQL and compares it to the Datastore cursor
*
* <p>This takes in a cursor from Datastore and checks to see if it exists in Cloud SQL and has
* the same value. If a difference is detected, or the Cloud SQL cursor does not exist, a warning
* is logged.
*/
public static void loadAndCompare(
@Nullable google.registry.model.common.Cursor datastoreCursor, String scope) {
if (datastoreCursor == null) {
return;
}
try {
// Load the corresponding cursor from Cloud SQL
Cursor cloudSqlCursor = load(datastoreCursor.getType(), scope);
compare(datastoreCursor, cloudSqlCursor, scope);
} catch (Throwable t) {
logger.atSevere().withCause(t).log("Error comparing cursors.");
}
}
/**
* Loads in all cursors of a given type from Cloud SQL and compares them to Datastore
*
* <p>This takes in cursors from Datastore and checks to see if they exists in Cloud SQL and have
* the same value. If a difference is detected, or a Cloud SQL cursor does not exist, a warning is
* logged.
*/
public static void loadAndCompareAll(
ImmutableMap<google.registry.model.common.Cursor, String> cursors, CursorType type) {
try {
// Load all the cursors of that type from Cloud SQL
List<Cursor> cloudSqlCursors = loadByType(type);
// Create a map of each TLD to its cursor if one exists.
ImmutableMap<String, Cursor> cloudSqlCursorMap =
Maps.uniqueIndex(cloudSqlCursors, Cursor::getScope);
// Compare each Datastore cursor with its corresponding Cloud SQL cursor.
for (google.registry.model.common.Cursor cursor : cursors.keySet()) {
Cursor cloudSqlCursor = cloudSqlCursorMap.get(cursors.get(cursor));
compare(cursor, cloudSqlCursor, cursors.get(cursor));
}
} catch (Throwable t) {
logger.atSevere().withCause(t).log("Error comparing cursors.");
}
}
private static void compare(
google.registry.model.common.Cursor datastoreCursor,
@Nullable Cursor cloudSqlCursor,
String scope) {
if (cloudSqlCursor == null) {
logger.atWarning().log(
String.format(
"Cursor of type %s with the scope %s was not found in Cloud SQL.",
datastoreCursor.getType().name(), scope));
} else if (!datastoreCursor.getCursorTime().equals(cloudSqlCursor.getCursorTime())) {
logger.atWarning().log(
String.format(
"This cursor of type %s with the scope %s has a cursorTime of %s in Datastore and %s"
+ " in Cloud SQL.",
datastoreCursor.getType(),
scope,
datastoreCursor.getCursorTime(),
cloudSqlCursor.getCursorTime()));
}
}
}
@@ -14,7 +14,6 @@
package google.registry.schema.replay;
import com.google.common.flogger.FluentLogger;
import google.registry.persistence.VKey;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -27,8 +26,6 @@ import java.lang.reflect.Method;
* to invoke special class methods if they are present.
*/
public class ReplaySpecializer {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static void beforeSqlDelete(VKey<?> key) {
try {
Method method = key.getKind().getMethod("beforeSqlDelete", VKey.class);
@@ -57,8 +57,7 @@ class ReplicateToDatastoreAction implements Runnable {
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
.query(
"SELECT txn FROM TransactionEntity txn WHERE id >"
+ " :lastId ORDER BY id")
.setParameter("lastId", lastSqlTxnBeforeBatch.getTransactionId())
@@ -39,8 +39,7 @@ public class SqlReplayCheckpoint implements SqlEntity {
public static DateTime get() {
jpaTm().assertInTransaction();
return jpaTm()
.getEntityManager()
.createQuery("FROM SqlReplayCheckpoint", SqlReplayCheckpoint.class)
.query("FROM SqlReplayCheckpoint", SqlReplayCheckpoint.class)
.setMaxResults(1)
.getResultStream()
.findFirst()
@@ -33,7 +33,7 @@ public class LockDao {
jpaTm()
.transact(
() -> {
jpaTm().getEntityManager().merge(lock);
jpaTm().put(lock);
});
}
@@ -68,7 +68,7 @@ public class LockDao {
() -> {
Optional<Lock> loadedLock = load(resourceName, tld);
if (loadedLock.isPresent()) {
jpaTm().getEntityManager().remove(loadedLock.get());
jpaTm().delete(loadedLock.get());
}
});
}
@@ -159,7 +159,7 @@ public class PremiumListSqlDao {
}
public static PremiumList save(PremiumList premiumList) {
jpaTm().transact(() -> jpaTm().getEntityManager().persist(premiumList));
jpaTm().transact(() -> jpaTm().insert(premiumList));
premiumListCache.invalidate(premiumList.getName());
return premiumList;
}
@@ -174,8 +174,7 @@ public class PremiumListSqlDao {
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
.query(
"FROM PremiumList WHERE name = :name ORDER BY revisionId DESC",
PremiumList.class)
.setParameter("name", premiumListName)
@@ -194,8 +193,7 @@ public class PremiumListSqlDao {
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
.query(
"FROM PremiumEntry pe WHERE pe.revisionId = :revisionId",
PremiumEntry.class)
.setParameter("revisionId", premiumList.getRevisionId())
@@ -211,8 +209,7 @@ public class PremiumListSqlDao {
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
.query(
"SELECT pe.price FROM PremiumEntry pe WHERE pe.revisionId = :revisionId"
+ " AND pe.domainLabel = :label",
BigDecimal.class)
@@ -18,6 +18,7 @@ import static google.registry.request.Action.Method.POST;
import com.google.common.flogger.FluentLogger;
import google.registry.keyring.api.KeyModule.Key;
import google.registry.model.tmch.ClaimsListDualDatabaseDao;
import google.registry.model.tmch.ClaimsListShard;
import google.registry.request.Action;
import google.registry.request.auth.Auth;
@@ -55,9 +56,9 @@ public final class TmchDnlAction implements Runnable {
throw new RuntimeException(e);
}
ClaimsListShard claims = ClaimsListParser.parse(lines);
claims.save();
ClaimsListDualDatabaseDao.save(claims);
logger.atInfo().log(
"Inserted %,d claims into Datastore, created at %s",
"Inserted %,d claims into the DB(s), created at %s",
claims.size(), claims.getTmdbGenerationTime());
}
}
@@ -45,7 +45,7 @@ public final class TmchSmdrlAction implements Runnable {
@Inject @Key("marksdbSmdrlLoginAndPassword") Optional<String> marksdbSmdrlLoginAndPassword;
@Inject TmchSmdrlAction() {}
/** Synchronously fetches latest signed mark revocation list and saves it to Datastore. */
/** Synchronously fetches latest signed mark revocation list and saves it to the database. */
@Override
public void run() {
List<String> lines;
@@ -57,7 +57,7 @@ public final class TmchSmdrlAction implements Runnable {
SignedMarkRevocationList smdrl = SmdrlCsvParser.parse(lines);
smdrl.save();
logger.atInfo().log(
"Inserted %,d smd revocations into Datastore, created at %s",
"Inserted %,d smd revocations into the database, created at %s",
smdrl.size(), smdrl.getCreationTime());
}
}
@@ -21,13 +21,14 @@ import com.beust.jcommander.Parameters;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import google.registry.model.registry.label.ReservedList;
import google.registry.model.registry.label.ReservedListDualDatabaseDao;
/**
* Command to delete a {@link ReservedList} in Datastore. This command will fail if the reserved
* list is currently in use on a tld.
* Command to delete a {@link ReservedList} from the database. This command will fail if the
* reserved list is currently in use on a tld.
*/
@Parameters(separators = " =", commandDescription = "Deletes a ReservedList in Datastore.")
final class DeleteReservedListCommand extends MutatingCommand {
@Parameters(separators = " =", commandDescription = "Deletes a ReservedList from the database.")
final class DeleteReservedListCommand extends ConfirmingCommand {
@Parameter(
names = {"-n", "--name"},
@@ -47,6 +48,12 @@ final class DeleteReservedListCommand extends MutatingCommand {
tldsUsedOn.isEmpty(),
"Cannot delete reserved list because it is used on these tld(s): %s",
Joiner.on(", ").join(tldsUsedOn));
stageEntityChange(existing, null);
}
@Override
protected String execute() {
ReservedList existing = ReservedList.get(name).get();
ReservedListDualDatabaseDao.delete(existing);
return String.format("Deleted reserved list: %s", name);
}
}
@@ -16,6 +16,7 @@ package google.registry.tools;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.beust.jcommander.Parameter;
@@ -61,13 +62,7 @@ final class DeleteTldCommand extends ConfirmingCommand implements CommandWithRem
"Cannot delete TLD because registrar %s lists it as an allowed TLD",
registrar.getClientId());
}
int count = ofy().load()
.type(DomainBase.class)
.filter("tld", tld)
.limit(1)
.count();
checkState(count == 0, "Cannot delete TLD because a domain is defined on it");
checkState(!tldContainsDomains(tld), "Cannot delete TLD because a domain is defined on it");
}
@Override
@@ -77,8 +72,25 @@ final class DeleteTldCommand extends ConfirmingCommand implements CommandWithRem
@Override
protected String execute() {
tm().transactNew(() -> ofy().delete().entity(registry).now());
tm().transactNew(() -> tm().delete(registry));
registry.invalidateInCache();
return String.format("Deleted TLD '%s'.\n", tld);
}
private boolean tldContainsDomains(String tld) {
if (tm().isOfy()) {
return ofy().load().type(DomainBase.class).filter("tld", tld).limit(1).count() > 0;
} else {
return jpaTm()
.transact(
() ->
jpaTm()
.query("FROM Domain WHERE tld = :tld", DomainBase.class)
.setParameter("tld", tld)
.setMaxResults(1)
.getResultStream()
.findFirst()
.isPresent());
}
}
}
@@ -21,6 +21,7 @@ import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.base.Joiner;
import com.google.common.io.Files;
import google.registry.model.tmch.ClaimsListDualDatabaseDao;
import google.registry.model.tmch.ClaimsListShard;
import google.registry.tools.params.PathParameter;
import java.nio.file.Path;
@@ -43,7 +44,7 @@ final class GetClaimsListCommand implements CommandWithRemoteApi {
@Override
public void run() throws Exception {
ClaimsListShard cl = checkNotNull(ClaimsListShard.get(), "Couldn't load ClaimsList");
ClaimsListShard cl = checkNotNull(ClaimsListDualDatabaseDao.get(), "Couldn't load ClaimsList");
String csv = Joiner.on('\n').withKeyValueSeparator(",").join(cl.getLabelsToKeys()) + "\n";
Files.asCharSink(output.toFile(), UTF_8).write(csv);
}
@@ -17,6 +17,7 @@ package google.registry.tools;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.base.Ascii;
import google.registry.config.RegistryConfig.Config;
import google.registry.privileges.secretmanager.SecretManagerClient.SecretManagerException;
import google.registry.privileges.secretmanager.SqlCredential;
import google.registry.privileges.secretmanager.SqlCredentialStore;
@@ -31,13 +32,19 @@ import javax.inject.Inject;
/**
* Command to get a Cloud SQL credential in the Secret Manager.
*
* <p>This command is a short-term tool that will be deprecated by the planned privilege server.
* <p>The schema deployment process will use this command's output. Coordinate with <a
* href="https://github.com/google/nomulus/release/cloudbuild-schema-deploy.yaml">the schema
* deployment script</a> before making changes to the output.
*/
@Parameters(separators = " =", commandDescription = "Get the Cloud SQL Credential for a given user")
public class GetSqlCredentialCommand implements Command {
@Inject SqlCredentialStore store;
@Inject
@Config("cloudSqlInstanceConnectionName")
String cloudSqlInstanceConnectionName;
@Parameter(names = "--user", description = "The Cloud SQL user.", required = true)
private String user;
@@ -62,12 +69,17 @@ public class GetSqlCredentialCommand implements Command {
return;
}
// Output format is important. Check class level javadoc before making changes.
String outputText =
String.format(
"%s %s %s", cloudSqlInstanceConnectionName, credential.login(), credential.password());
if (outputPath == null) {
System.out.printf("[%s]\n", credential.toFormattedString());
System.out.print(outputText);
return;
}
try (FileOutputStream out = new FileOutputStream(outputPath.toFile())) {
out.write(credential.toFormattedString().getBytes(StandardCharsets.UTF_8));
out.write(outputText.getBytes(StandardCharsets.UTF_8));
}
}
}
@@ -15,18 +15,19 @@
package google.registry.tools;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.schema.cursor.CursorDao.loadAndCompare;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.base.Strings;
import com.googlecode.objectify.Key;
import com.google.common.collect.ImmutableMap;
import google.registry.model.common.Cursor;
import google.registry.model.common.Cursor.CursorType;
import google.registry.model.registry.Registries;
import google.registry.model.registry.Registry;
import google.registry.model.registry.Registry.TldType;
import google.registry.persistence.VKey;
import java.util.Map;
import java.util.Optional;
@@ -51,20 +52,18 @@ final class ListCursorsCommand implements CommandWithRemoteApi {
@Override
public void run() {
Map<Registry, Key<Cursor>> registries =
Registries.getTlds()
.stream()
Map<Registry, VKey<Cursor>> registries =
Registries.getTlds().stream()
.map(Registry::get)
.filter(r -> r.getTldType() == filterTldType)
.filter(r -> !filterEscrowEnabled || r.getEscrowEnabled())
.collect(toImmutableMap(r -> r, r -> Cursor.createKey(cursorType, r)));
Map<Key<Cursor>, Cursor> cursors = ofy().load().keys(registries.values());
.collect(toImmutableMap(r -> r, r -> Cursor.createVKey(cursorType, r.getTldStr())));
ImmutableMap<VKey<? extends Cursor>, Cursor> cursors =
transactIfJpaTm(() -> tm().loadByKeysIfPresent(registries.values()));
if (!registries.isEmpty()) {
String header = String.format(OUTPUT_FMT, "TLD", "Cursor Time", "Last Update Time");
System.out.printf("%s\n%s\n", header, Strings.repeat("-", header.length()));
registries
.entrySet()
.stream()
registries.entrySet().stream()
.map(
e ->
renderLine(
@@ -75,9 +74,6 @@ final class ListCursorsCommand implements CommandWithRemoteApi {
}
private static String renderLine(String tld, Optional<Cursor> cursor) {
if (cursor.isPresent()) {
loadAndCompare(cursor.get(), tld);
}
return String.format(
OUTPUT_FMT,
tld,
@@ -25,7 +25,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import google.registry.bigquery.BigqueryUtils.SourceFormat;
import google.registry.export.ExportConstants;
import google.registry.export.AnnotatedEntities;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -48,7 +48,7 @@ final class LoadSnapshotCommand extends BigqueryCommand {
@Parameter(
names = "--kinds",
description = "List of Datastore kinds for which to import snapshot data.")
private List<String> kindNames = new ArrayList<>(ExportConstants.getReportingKinds());
private List<String> kindNames = new ArrayList<>(AnnotatedEntities.getReportingKinds());
/** Runs the main snapshot import logic. */
@Override
@@ -79,12 +79,14 @@ final class LoadSnapshotCommand extends BigqueryCommand {
/** Starts a load job for the specified kind name, sourcing data from the given GCS file. */
private ListenableFuture<?> loadSnapshotFile(String filename, String kindName) {
return bigquery().load(
bigquery().buildDestinationTable(kindName)
.description("Datastore snapshot import for " + kindName + ".")
.build(),
SourceFormat.DATASTORE_BACKUP,
ImmutableList.of(filename));
return bigquery()
.startLoad(
bigquery()
.buildDestinationTable(kindName)
.description("Datastore snapshot import for " + kindName + ".")
.build(),
SourceFormat.DATASTORE_BACKUP,
ImmutableList.of(filename));
}
/**
@@ -14,15 +14,15 @@
package google.registry.tools;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableList;
import google.registry.model.common.Cursor;
import google.registry.model.common.Cursor.CursorType;
import google.registry.model.registry.Registry;
import google.registry.schema.cursor.CursorDao;
import google.registry.tools.params.DateTimeParameter;
import java.util.List;
import org.joda.time.DateTime;
@@ -34,10 +34,7 @@ final class UpdateCursorsCommand extends ConfirmingCommand implements CommandWit
@Parameter(description = "TLDs on which to operate. Omit for global cursors.")
private List<String> tlds;
@Parameter(
names = "--type",
description = "Which cursor to update.",
required = true)
@Parameter(names = "--type", description = "Which cursor to update.", required = true)
private CursorType cursorType;
@Parameter(
@@ -47,28 +44,25 @@ final class UpdateCursorsCommand extends ConfirmingCommand implements CommandWit
required = true)
private DateTime newTimestamp;
ImmutableMap<Cursor, String> cursorsToUpdate;
ImmutableList<Cursor> cursorsToUpdate;
@Override
protected void init() {
ImmutableMap.Builder<Cursor, String> cursorsToUpdateBuilder = new ImmutableMap.Builder<>();
ImmutableList.Builder<Cursor> result = new ImmutableList.Builder<>();
if (isNullOrEmpty(tlds)) {
cursorsToUpdateBuilder.put(
Cursor.createGlobal(cursorType, newTimestamp),
google.registry.schema.cursor.Cursor.GLOBAL);
result.add(Cursor.createGlobal(cursorType, newTimestamp));
} else {
for (String tld : tlds) {
Registry registry = Registry.get(tld);
cursorsToUpdateBuilder.put(
Cursor.create(cursorType, newTimestamp, registry), registry.getTldStr());
result.add(Cursor.create(cursorType, newTimestamp, registry));
}
}
cursorsToUpdate = cursorsToUpdateBuilder.build();
cursorsToUpdate = result.build();
}
@Override
protected String execute() throws Exception {
CursorDao.saveCursors(cursorsToUpdate);
protected String execute() {
tm().transact(() -> tm().putAll(cursorsToUpdate));
return String.format("Updated %d cursors.\n", cursorsToUpdate.size());
}
@@ -79,14 +73,13 @@ final class UpdateCursorsCommand extends ConfirmingCommand implements CommandWit
if (cursorsToUpdate.isEmpty()) {
return "No cursor changes to apply.";
}
cursorsToUpdate.entrySet().stream()
.forEach(entry -> changes.append(getChangeString(entry.getKey(), entry.getValue())));
cursorsToUpdate.forEach(cursor -> changes.append(getChangeString(cursor)));
return changes.toString();
}
private String getChangeString(Cursor cursor, String scope) {
private String getChangeString(Cursor cursor) {
return String.format(
"Change cursorTime of %s for Scope:%s to %s\n",
cursor.getType(), scope, cursor.getCursorTime());
cursor.getType(), cursor.getScope(), cursor.getCursorTime());
}
}
@@ -21,6 +21,7 @@ import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.base.Joiner;
import com.google.common.io.Files;
import google.registry.model.tmch.ClaimsListDualDatabaseDao;
import google.registry.model.tmch.ClaimsListShard;
import google.registry.tmch.ClaimsListParser;
import java.io.File;
@@ -56,7 +57,7 @@ final class UploadClaimsListCommand extends ConfirmingCommand implements Command
@Override
public String execute() {
claimsList.save();
ClaimsListDualDatabaseDao.save(claimsList);
return String.format("Successfully uploaded claims list %s", claimsListFilename);
}
}
@@ -207,8 +207,7 @@ public class BackfillSpec11ThreatMatchesCommand extends ConfirmingCommand
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
.query(
"SELECT DISTINCT stm.checkDate FROM Spec11ThreatMatch stm", LocalDate.class)
.getResultStream()
.collect(toImmutableSet()));
@@ -15,13 +15,13 @@
package google.registry.tools.server;
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static java.util.Comparator.comparing;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import google.registry.model.EppResourceUtils;
import google.registry.model.host.HostResource;
import google.registry.request.Action;
@@ -51,7 +51,7 @@ public final class ListHostsAction extends ListObjectsAction<HostResource> {
@Override
public ImmutableSet<HostResource> loadObjects() {
final DateTime now = clock.nowUtc();
return Streams.stream(ofy().load().type(HostResource.class))
return transactIfJpaTm(() -> tm().loadAllOf(HostResource.class)).stream()
.filter(host -> EppResourceUtils.isActive(host, now))
.collect(toImmutableSortedSet(comparing(HostResource::getHostName)));
}
@@ -14,16 +14,21 @@
package google.registry.tools.server;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import com.google.common.collect.ImmutableSet;
import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.PremiumListDualDao;
import google.registry.request.Action;
import google.registry.request.auth.Auth;
import java.util.Comparator;
import java.util.Optional;
import javax.inject.Inject;
import org.hibernate.Hibernate;
/**
* An action that lists premium lists, for use by the {@code nomulus list_premium_lists} command.
@@ -46,7 +51,14 @@ public final class ListPremiumListsAction extends ListObjectsAction<PremiumList>
@Override
public ImmutableSet<PremiumList> loadObjects() {
return ImmutableSet.copyOf(
ofy().load().type(PremiumList.class).ancestor(getCrossTldKey()).list());
return transactIfJpaTm(
() ->
tm().loadAllOf(PremiumList.class).stream()
.map(PremiumList::getName)
.map(PremiumListDualDao::getLatestRevision)
.filter(Optional::isPresent)
.map(Optional::get)
.peek(list -> Hibernate.initialize(list.getLabelsToPrices()))
.collect(toImmutableSortedSet(Comparator.comparing(PremiumList::getName))));
}
}
@@ -14,15 +14,19 @@
package google.registry.tools.server;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import com.google.common.collect.ImmutableSet;
import google.registry.model.registry.label.ReservedList;
import google.registry.model.registry.label.ReservedListDualDatabaseDao;
import google.registry.request.Action;
import google.registry.request.auth.Auth;
import java.util.Comparator;
import java.util.Optional;
import javax.inject.Inject;
/** A that lists reserved lists, for use by the {@code nomulus list_reserved_lists} command. */
@@ -44,7 +48,13 @@ public final class ListReservedListsAction extends ListObjectsAction<ReservedLis
@Override
public ImmutableSet<ReservedList> loadObjects() {
return ImmutableSet.copyOf(
ofy().load().type(ReservedList.class).ancestor(getCrossTldKey()).list());
return transactIfJpaTm(
() ->
tm().loadAllOf(ReservedList.class).stream()
.map(ReservedList::getName)
.map(ReservedListDualDatabaseDao::getLatestRevision)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(toImmutableSortedSet(Comparator.comparing(ReservedList::getName))));
}
}
@@ -41,6 +41,7 @@
<class>google.registry.model.billing.BillingEvent$Cancellation</class>
<class>google.registry.model.billing.BillingEvent$OneTime</class>
<class>google.registry.model.billing.BillingEvent$Recurring</class>
<class>google.registry.model.common.Cursor</class>
<class>google.registry.model.contact.ContactHistory</class>
<class>google.registry.model.contact.ContactResource</class>
<class>google.registry.model.domain.DomainBase</class>
@@ -69,7 +70,6 @@
<class>google.registry.model.tmch.ClaimsListShard</class>
<class>google.registry.model.tmch.TmchCrl</class>
<class>google.registry.persistence.transaction.TransactionEntity</class>
<class>google.registry.schema.cursor.Cursor</class>
<class>google.registry.schema.domain.RegistryLock</class>
<class>google.registry.schema.replay.SqlReplayCheckpoint</class>
<class>google.registry.schema.server.Lock</class>
@@ -0,0 +1,20 @@
{
"name": "Bulk Delete Cloud Datastore",
"description": "An Apache Beam batch pipeline that deletes Cloud Datastore in bulk. This is easier to use than the GCP-provided template.",
"parameters": [
{
"name": "kindsToDelete",
"label": "The data KINDs to delete.",
"helpText": "The Datastore KINDs to be deleted. The format may be: the list of kinds to be deleted as a comma-separated string; or '*', which causes all kinds to be deleted."
},
{
"name": "getNumOfKindsHint",
"label": "An estimate of the number of KINDs to be deleted.",
"helpText": "An estimate of the number of KINDs to be deleted. This is recommended if --kindsToDelete is '*' and the default value is too low.",
"is_optional": true,
"regexes": [
"^[1-9][0-9]*$"
]
}
]
}

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