mirror of
https://github.com/google/nomulus
synced 2026-05-25 09:10:51 +00:00
Compare commits
85 Commits
nomulus-20
...
nomulus-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf4b6978a7 | ||
|
|
548ae25fac | ||
|
|
8393c75929 | ||
|
|
1764ae0b3f | ||
|
|
d76abfc23a | ||
|
|
6af9299a3c | ||
|
|
a53c127573 | ||
|
|
8dbf4fced9 | ||
|
|
5dc6354ebc | ||
|
|
c84767bd07 | ||
|
|
a59f09e011 | ||
|
|
b4b318f923 | ||
|
|
52550a9251 | ||
|
|
930c4f8cfa | ||
|
|
b4468d83a9 | ||
|
|
4dc4daffe6 | ||
|
|
76458bb3b9 | ||
|
|
2d1a67b01b | ||
|
|
01d3932122 | ||
|
|
2eb8bb3996 | ||
|
|
2218663d55 | ||
|
|
e0dc2e43bb | ||
|
|
7fedd40739 | ||
|
|
f793ca5b68 | ||
|
|
395ed19601 | ||
|
|
cecc1a6cc7 | ||
|
|
77bc072aac | ||
|
|
93a479837f | ||
|
|
1e7aae26a3 | ||
|
|
201b6e8e0b | ||
|
|
43074ea32f | ||
|
|
1a4a31569e | ||
|
|
c7f50dae92 | ||
|
|
7344c424d1 | ||
|
|
969fa2b68c | ||
|
|
9a569198fb | ||
|
|
8a53edd57b | ||
|
|
d25d4073f5 | ||
|
|
6ffe84e93d | ||
|
|
a451524010 | ||
|
|
bb8988ee4e | ||
|
|
2aff72b3b6 | ||
|
|
35fd61f771 | ||
|
|
13cb17e9a4 | ||
|
|
4f1c317bbc | ||
|
|
c8aa32ef05 | ||
|
|
95a1bbf66a | ||
|
|
23aa16469e | ||
|
|
0277c5c25a | ||
|
|
b1b0589281 | ||
|
|
28628564cc | ||
|
|
835f93f555 | ||
|
|
276c188e9d | ||
|
|
34ecc6fbe7 | ||
|
|
0f4156c563 | ||
|
|
e1827ab939 | ||
|
|
51b2887709 | ||
|
|
62eb8801c5 | ||
|
|
f6920454f6 | ||
|
|
9103216a46 | ||
|
|
c6705d1956 | ||
|
|
737f65bd33 | ||
|
|
c8caa8f80b | ||
|
|
65ef18052b | ||
|
|
f7938e80f7 | ||
|
|
d8b3a30a20 | ||
|
|
93715c6f9e | ||
|
|
90cf4519c5 | ||
|
|
3a177f36b1 | ||
|
|
fbbe014e96 | ||
|
|
b05b77cfd1 | ||
|
|
420a0b8b9a | ||
|
|
cc062e3528 | ||
|
|
56a0e35314 | ||
|
|
de434f861f | ||
|
|
3caee5fba7 | ||
|
|
ff3c848def | ||
|
|
f0b3be5bb6 | ||
|
|
18b808bd34 | ||
|
|
d7689539d7 | ||
|
|
c14ce6866b | ||
|
|
3b84542e46 | ||
|
|
fc7db91d70 | ||
|
|
3d8aa85d63 | ||
|
|
e14cd8bfa2 |
67
build.gradle
67
build.gradle
@@ -196,9 +196,41 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.ext {
|
||||
pyver = { exe ->
|
||||
try {
|
||||
ext.execInBash(
|
||||
exe + " -c 'import sys; print(sys.hexversion)' 2>/dev/null",
|
||||
"/") as Integer
|
||||
} catch (org.gradle.process.internal.ExecException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the path to a usable python3 executable.
|
||||
getPythonExecutable = {
|
||||
// Find a python version greater than 3.7.3 (this is somewhat arbitrary, we
|
||||
// know we'd like at least 3.6, but 3.7.3 is the latest that ships with
|
||||
// Debian so it seems like that should be available anywhere).
|
||||
def MIN_PY_VER = 0x3070300
|
||||
if (pyver('python') >= MIN_PY_VER) {
|
||||
return 'python'
|
||||
} else if (pyver('/usr/bin/python3') >= MIN_PY_VER) {
|
||||
return '/usr/bin/python3'
|
||||
} else {
|
||||
throw new GradleException("No usable Python version found (build " +
|
||||
"requires at least python 3.7.3)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task runPresubmits(type: Exec) {
|
||||
executable '/usr/bin/python3'
|
||||
|
||||
args('config/presubmits.py')
|
||||
|
||||
doFirst {
|
||||
executable getPythonExecutable()
|
||||
}
|
||||
}
|
||||
|
||||
def javadocSource = []
|
||||
@@ -412,9 +444,10 @@ rootProject.ext {
|
||||
? "${rootDir}/.."
|
||||
: rootDir
|
||||
def formatDiffScript = "${scriptDir}/google-java-format-git-diff.sh"
|
||||
def pythonExe = getPythonExecutable()
|
||||
|
||||
return ext.execInBash(
|
||||
"${formatDiffScript} ${action}", "${workingDir}")
|
||||
"PYTHON=${pythonExe} ${formatDiffScript} ${action}", "${workingDir}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,18 +455,23 @@ rootProject.ext {
|
||||
// Note that this task checks modified Java files in the entire repository.
|
||||
task javaIncrementalFormatCheck {
|
||||
doLast {
|
||||
def checkResult = invokeJavaDiffFormatScript("check")
|
||||
if (checkResult == 'true') {
|
||||
throw new IllegalStateException(
|
||||
"Some Java files need to be reformatted. You may use the "
|
||||
+ "'javaIncrementalFormatDryRun' task to review\n "
|
||||
+ "the changes, or the 'javaIncrementalFormatApply' task "
|
||||
+ "to reformat.")
|
||||
} else if (checkResult != 'false') {
|
||||
throw new RuntimeException(
|
||||
"Failed to invoke format check script:\n" + checkResult)
|
||||
// We can only do this in a git tree.
|
||||
if (new File("${rootDir}/.git").exists()) {
|
||||
def checkResult = invokeJavaDiffFormatScript("check")
|
||||
if (checkResult == 'true') {
|
||||
throw new IllegalStateException(
|
||||
"Some Java files need to be reformatted. You may use the "
|
||||
+ "'javaIncrementalFormatDryRun' task to review\n "
|
||||
+ "the changes, or the 'javaIncrementalFormatApply' task "
|
||||
+ "to reformat.")
|
||||
} else if (checkResult != 'false') {
|
||||
throw new RuntimeException(
|
||||
"Failed to invoke format check script:\n" + checkResult)
|
||||
}
|
||||
println("Incremental Java format check ok.")
|
||||
} else {
|
||||
println("Omitting format check: not in a git directory.")
|
||||
}
|
||||
println("Incremental Java format check ok.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,7 +501,8 @@ task javadoc(type: Javadoc) {
|
||||
options.addBooleanOption('Xdoclint:all,-missing', true)
|
||||
options.addBooleanOption("-allow-script-in-comments",true)
|
||||
options.tags = ["type:a:Generic Type",
|
||||
"error:a:Expected Error"]
|
||||
"error:a:Expected Error",
|
||||
"invariant:a:Guaranteed Property"]
|
||||
}
|
||||
|
||||
tasks.build.dependsOn(tasks.javadoc)
|
||||
|
||||
@@ -39,7 +39,7 @@ public final class SystemInfo {
|
||||
pid.getOutputStream().close();
|
||||
pid.waitFor();
|
||||
} catch (IOException e) {
|
||||
logger.atWarning().withCause(e).log("%s command not available", cmd);
|
||||
logger.atWarning().withCause(e).log("%s command not available.", cmd);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -140,6 +140,8 @@ PROPERTIES = [
|
||||
'a BEAM pipeline to image. Setting this property to empty string '
|
||||
'will disable image generation.',
|
||||
'/usr/bin/dot'),
|
||||
Property('pipeline',
|
||||
'The name of the Beam pipeline being staged.')
|
||||
]
|
||||
|
||||
GRADLE_FLAGS = [
|
||||
|
||||
@@ -17,9 +17,11 @@ These aren't built in to the static code analysis tools we use (e.g. Checkstyle,
|
||||
Error Prone) so we must write them manually.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import List, Tuple
|
||||
import sys
|
||||
import textwrap
|
||||
import re
|
||||
|
||||
# We should never analyze any generated files
|
||||
@@ -28,6 +30,13 @@ UNIVERSALLY_SKIPPED_PATTERNS = {"/build/", "cloudbuild-caches", "/out/", ".git/"
|
||||
FORBIDDEN = 1
|
||||
REQUIRED = 2
|
||||
|
||||
# The list of expected json packages and their licenses.
|
||||
# These should be one of the allowed licenses in:
|
||||
# config/dependency-license/allowed_licenses.json
|
||||
EXPECTED_JS_PACKAGES = [
|
||||
'google-closure-library', # Owned by Google, Apache 2.0
|
||||
]
|
||||
|
||||
|
||||
class PresubmitCheck:
|
||||
|
||||
@@ -110,9 +119,10 @@ PRESUBMITS = {
|
||||
"AppEngineExtension.register(...) instead.",
|
||||
|
||||
# PostgreSQLContainer instantiation must specify docker tag
|
||||
# TODO(b/204572437): Fix the pattern to pass DatabaseSnapshotTest.java
|
||||
PresubmitCheck(
|
||||
r"[\s\S]*new\s+PostgreSQLContainer(<[\s\S]*>)?\(\s*\)[\s\S]*",
|
||||
"java", {}):
|
||||
"java", {"DatabaseSnapshotTest.java"}):
|
||||
"PostgreSQLContainer instantiation must specify docker tag.",
|
||||
|
||||
# Various Soy linting checks
|
||||
@@ -177,50 +187,6 @@ PRESUBMITS = {
|
||||
{"/node_modules/", "google/registry/ui/js/util.js", "registrar_bin."},
|
||||
):
|
||||
"JavaScript files should not include console logging.",
|
||||
# SQL injection protection rule for java source file:
|
||||
# The sql template passed to createQuery/createNativeQuery methods must be
|
||||
# a variable name in UPPER_CASE_UNDERSCORE format, i.e., a static final
|
||||
# String variable. This forces the use of parameter-binding on all queries
|
||||
# that take parameters.
|
||||
# The rule would forbid invocation of createQuery(Criteria). However, this
|
||||
# can be handled by adding a helper method in an exempted class to make
|
||||
# the calls.
|
||||
# TODO(b/179158393): enable the 'ConstantName' Java style check to ensure
|
||||
# that non-final variables do not use the UPPER_CASE_UNDERSCORE format.
|
||||
PresubmitCheck(
|
||||
# Line 1: the method names we check and the opening parenthesis, which
|
||||
# marks the beginning of the first parameter
|
||||
# Line 2: The first parameter is a match if is NOT any of the following:
|
||||
# - final variable name: \s*([A-Z_]+
|
||||
# - string literal: "([^"]|\\")*"
|
||||
# - concatenation of literals: (\s*\+\s*"([^"]|\\")*")*
|
||||
# Line 3: , or the closing parenthesis, marking the end of the first
|
||||
# parameter
|
||||
r'.*\.(query|createQuery|createNativeQuery)\('
|
||||
r'(?!(\s*([A-Z_]+|"([^"]|\\")*"(\s*\+\s*"([^"]|\\")*")*)'
|
||||
r'(,|\s*\))))',
|
||||
"java",
|
||||
# ActivityReportingQueryBuilder deals with Dremel queries
|
||||
{"src/test", "ActivityReportingQueryBuilder.java",
|
||||
# This class contains helper method to make queries in Beam.
|
||||
"RegistryJpaIO.java",
|
||||
# TODO(b/179158393): Remove everything below, which should be done
|
||||
# using Criteria
|
||||
"JpaTransactionManager.java",
|
||||
"JpaTransactionManagerImpl.java",
|
||||
# CriteriaQueryBuilder is a false positive
|
||||
"CriteriaQueryBuilder.java",
|
||||
"RdapDomainSearchAction.java",
|
||||
"RdapNameserverSearchAction.java",
|
||||
"ReadOnlyCheckingEntityManager.java",
|
||||
"RegistryQuery",
|
||||
},
|
||||
):
|
||||
"The first String parameter to EntityManager.create(Native)Query "
|
||||
"methods must be one of the following:\n"
|
||||
" - A String literal\n"
|
||||
" - Concatenation of String literals only\n"
|
||||
" - The name of a static final String variable"
|
||||
}
|
||||
|
||||
# Note that this regex only works for one kind of Flyway file. If we want to
|
||||
@@ -308,6 +274,26 @@ def verify_flyway_index():
|
||||
return not success
|
||||
|
||||
|
||||
def verify_javascript_deps():
|
||||
"""Verifies that we haven't introduced any new javascript dependencies."""
|
||||
with open('package.json') as f:
|
||||
package = json.load(f)
|
||||
|
||||
deps = list(package['dependencies'].keys())
|
||||
if deps != EXPECTED_JS_PACKAGES:
|
||||
print('Unexpected javascript dependencies. Was expecting '
|
||||
'%s, got %s.' % (EXPECTED_JS_PACKAGES, deps))
|
||||
print(textwrap.dedent("""
|
||||
* If the new dependencies are intentional, please verify that the
|
||||
* license is one of the allowed licenses (see
|
||||
* config/dependency-license/allowed_licenses.json) and add an entry
|
||||
* for the package (with the license in a comment) to the
|
||||
* EXPECTED_JS_PACKAGES variable in config/presubmits.py.
|
||||
"""))
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_files():
|
||||
for root, dirnames, filenames in os.walk("."):
|
||||
for filename in filenames:
|
||||
@@ -315,6 +301,7 @@ def get_files():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print('python version is %s' % sys.version)
|
||||
failed = False
|
||||
for file in get_files():
|
||||
error_messages = []
|
||||
@@ -331,5 +318,8 @@ if __name__ == "__main__":
|
||||
# when we put it here it fails fast before all of the tests are run.
|
||||
failed |= verify_flyway_index()
|
||||
|
||||
# Make sure we haven't introduced any javascript dependencies.
|
||||
failed |= verify_javascript_deps()
|
||||
|
||||
if failed:
|
||||
sys.exit(1)
|
||||
|
||||
@@ -470,7 +470,7 @@ task soyToJava {
|
||||
|
||||
outputs.each { file ->
|
||||
exec {
|
||||
commandLine 'sed', '-i', 's/@link/LINK/g', file.getCanonicalPath()
|
||||
commandLine 'sed', '-i""', '-e', 's/@link/LINK/g', file.getCanonicalPath()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -705,7 +705,11 @@ createToolTask(
|
||||
|
||||
|
||||
createToolTask(
|
||||
'jpaDemoPipeline', 'google.registry.beam.common.JpaDemoPipeline')
|
||||
'jpaDemoPipeline', 'google.registry.beam.common.JpaDemoPipeline')
|
||||
|
||||
createToolTask(
|
||||
'createSyntheticHistoryEntries',
|
||||
'google.registry.tools.javascrap.CreateSyntheticHistoryEntriesPipeline')
|
||||
|
||||
project.tasks.create('initSqlPipeline', JavaExec) {
|
||||
main = 'google.registry.beam.initsql.InitSqlPipeline'
|
||||
@@ -772,50 +776,57 @@ createUberJar('nomulus', 'nomulus', 'google.registry.tools.RegistryTool')
|
||||
// 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',
|
||||
'beamPipelineCommon',
|
||||
'beam_pipeline_common',
|
||||
'')
|
||||
|
||||
// Create beam staging task if environment is alpha or crash.
|
||||
// All other environments use formally released pipelines through CloudBuild.
|
||||
// Create beam staging task if the environment is alpha. Production, sandbox and
|
||||
// qa use formally released pipelines through CloudBuild, whereas crash and
|
||||
// alpha use the pipelines staged on alpha deployment project.
|
||||
//
|
||||
// User should install gcloud and login to GCP before invoking this tasks.
|
||||
if (environment in ['alpha', 'crash']) {
|
||||
if (environment == 'alpha') {
|
||||
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'
|
||||
],
|
||||
[
|
||||
mainClass: 'google.registry.beam.spec11.Spec11Pipeline',
|
||||
metaData: 'google/registry/beam/spec11_pipeline_metadata.json'
|
||||
],
|
||||
[
|
||||
mainClass: 'google.registry.beam.invoicing.InvoicingPipeline',
|
||||
metaData: 'google/registry/beam/invoicing_pipeline_metadata.json'
|
||||
],
|
||||
[
|
||||
mainClass: 'google.registry.beam.rde.RdePipeline',
|
||||
metaData: 'google/registry/beam/rde_pipeline_metadata.json'
|
||||
],
|
||||
initSql :
|
||||
[
|
||||
mainClass: 'google.registry.beam.initsql.InitSqlPipeline',
|
||||
metaData : 'google/registry/beam/init_sql_pipeline_metadata.json'
|
||||
],
|
||||
bulkDeleteDatastore:
|
||||
[
|
||||
mainClass: 'google.registry.beam.datastore.BulkDeleteDatastorePipeline',
|
||||
metaData : 'google/registry/beam/bulk_delete_datastore_pipeline_metadata.json'
|
||||
],
|
||||
spec11 :
|
||||
[
|
||||
mainClass: 'google.registry.beam.spec11.Spec11Pipeline',
|
||||
metaData : 'google/registry/beam/spec11_pipeline_metadata.json'
|
||||
],
|
||||
invoicing :
|
||||
[
|
||||
mainClass: 'google.registry.beam.invoicing.InvoicingPipeline',
|
||||
metaData : 'google/registry/beam/invoicing_pipeline_metadata.json'
|
||||
],
|
||||
rde :
|
||||
[
|
||||
mainClass: 'google.registry.beam.rde.RdePipeline',
|
||||
metaData : 'google/registry/beam/rde_pipeline_metadata.json'
|
||||
],
|
||||
]
|
||||
project.tasks.create("stage_beam_pipelines") {
|
||||
project.tasks.create("stageBeamPipelines") {
|
||||
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
|
||||
if (rootProject.pipeline == ''|| rootProject.pipeline == it.key) {
|
||||
def mainClass = it.value['mainClass']
|
||||
def metaData = it.value['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.beamPipelineCommon.outputs.files.asPath
|
||||
|
||||
def command = "\
|
||||
def command = "\
|
||||
gcloud dataflow flex-template build \
|
||||
gs://${gcpProject}-deploy/live/beam/${metaDataBaseName} \
|
||||
--image-gcr-path ${imageName}:live \
|
||||
@@ -825,10 +836,11 @@ if (environment in ['alpha', 'crash']) {
|
||||
--jar ${uberJarName} \
|
||||
--env FLEX_TEMPLATE_JAVA_MAIN_CLASS=${mainClass} \
|
||||
--project ${gcpProject}".toString()
|
||||
rootProject.ext.execInBash(command, '/tmp')
|
||||
rootProject.ext.execInBash(command, '/tmp')
|
||||
}
|
||||
}
|
||||
}
|
||||
}.dependsOn(tasks.beam_pipeline_common)
|
||||
}.dependsOn(tasks.beamPipelineCommon)
|
||||
}
|
||||
|
||||
// A jar with classes and resources from main sourceSet, excluding internal
|
||||
@@ -1103,6 +1115,10 @@ test {
|
||||
// TODO(weiminyu): Remove dependency on sqlIntegrationTest
|
||||
}.dependsOn(fragileTest, outcastTest, standardTest, registryToolIntegrationTest, sqlIntegrationTest)
|
||||
|
||||
// When we override tests, we also break the cleanTest command.
|
||||
cleanTest.dependsOn(cleanFragileTest, cleanOutcastTest, cleanStandardTest,
|
||||
cleanRegistryToolIntegrationTest, cleanSqlIntegrationTest)
|
||||
|
||||
project.build.dependsOn devtool
|
||||
project.build.dependsOn buildToolImage
|
||||
project.build.dependsOn ':stage'
|
||||
|
||||
@@ -41,11 +41,12 @@ public class BackupUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given {@link ImmutableObject} to a raw Datastore entity and write it to an
|
||||
* {@link OutputStream} in delimited protocol buffer format.
|
||||
* Converts the given {@link ImmutableObject} to a raw Datastore entity and write it to an {@link
|
||||
* OutputStream} in delimited protocol buffer format.
|
||||
*/
|
||||
static void serializeEntity(ImmutableObject entity, OutputStream stream) throws IOException {
|
||||
EntityTranslator.convertToPb(auditedOfy().save().toEntity(entity)).writeDelimitedTo(stream);
|
||||
EntityTranslator.convertToPb(auditedOfy().saveIgnoringReadOnly().toEntity(entity))
|
||||
.writeDelimitedTo(stream);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,21 +14,21 @@
|
||||
|
||||
package google.registry.backup;
|
||||
|
||||
import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
|
||||
import static com.google.appengine.api.taskqueue.TaskOptions.Builder.withUrl;
|
||||
import static google.registry.backup.ExportCommitLogDiffAction.LOWER_CHECKPOINT_TIME_PARAM;
|
||||
import static google.registry.backup.ExportCommitLogDiffAction.UPPER_CHECKPOINT_TIME_PARAM;
|
||||
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
|
||||
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.model.ofy.CommitLogCheckpoint;
|
||||
import google.registry.model.ofy.CommitLogCheckpointRoot;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Action.Service;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.util.TaskQueueUtils;
|
||||
import google.registry.util.CloudTasksUtils;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
@@ -56,7 +56,8 @@ public final class CommitLogCheckpointAction implements Runnable {
|
||||
|
||||
@Inject Clock clock;
|
||||
@Inject CommitLogCheckpointStrategy strategy;
|
||||
@Inject TaskQueueUtils taskQueueUtils;
|
||||
@Inject CloudTasksUtils cloudTasksUtils;
|
||||
|
||||
@Inject CommitLogCheckpointAction() {}
|
||||
|
||||
@Override
|
||||
@@ -73,16 +74,20 @@ public final class CommitLogCheckpointAction implements Runnable {
|
||||
return;
|
||||
}
|
||||
auditedOfy()
|
||||
.saveWithoutBackup()
|
||||
.saveIgnoringReadOnly()
|
||||
.entities(
|
||||
checkpoint, CommitLogCheckpointRoot.create(checkpoint.getCheckpointTime()));
|
||||
// Enqueue a diff task between previous and current checkpoints.
|
||||
taskQueueUtils.enqueue(
|
||||
getQueue(QUEUE_NAME),
|
||||
withUrl(ExportCommitLogDiffAction.PATH)
|
||||
.param(LOWER_CHECKPOINT_TIME_PARAM, lastWrittenTime.toString())
|
||||
.param(
|
||||
UPPER_CHECKPOINT_TIME_PARAM, checkpoint.getCheckpointTime().toString()));
|
||||
cloudTasksUtils.enqueue(
|
||||
QUEUE_NAME,
|
||||
CloudTasksUtils.createPostTask(
|
||||
ExportCommitLogDiffAction.PATH,
|
||||
Service.BACKEND.toString(),
|
||||
ImmutableMultimap.of(
|
||||
LOWER_CHECKPOINT_TIME_PARAM,
|
||||
lastWrittenTime.toString(),
|
||||
UPPER_CHECKPOINT_TIME_PARAM,
|
||||
checkpoint.getCheckpointTime().toString())));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +55,9 @@ public final class CommitLogImports {
|
||||
* represents the changes in one transaction. The {@code CommitLogManifest} contains deleted
|
||||
* entity keys, whereas each {@code CommitLogMutation} contains one whole entity.
|
||||
*/
|
||||
public static ImmutableList<ImmutableList<VersionedEntity>> loadEntitiesByTransaction(
|
||||
static ImmutableList<ImmutableList<VersionedEntity>> loadEntitiesByTransaction(
|
||||
InputStream inputStream) {
|
||||
try (AppEngineEnvironment appEngineEnvironment = new AppEngineEnvironment();
|
||||
InputStream input = new BufferedInputStream(inputStream)) {
|
||||
try (InputStream input = new BufferedInputStream(inputStream)) {
|
||||
Iterator<ImmutableObject> commitLogs = createDeserializingIterator(input, false);
|
||||
checkState(commitLogs.hasNext());
|
||||
checkState(commitLogs.next() instanceof CommitLogCheckpoint);
|
||||
@@ -105,7 +104,7 @@ public final class CommitLogImports {
|
||||
* represents the changes in one transaction. The {@code CommitLogManifest} contains deleted
|
||||
* entity keys, whereas each {@code CommitLogMutation} contains one whole entity.
|
||||
*/
|
||||
public static ImmutableList<VersionedEntity> loadEntities(InputStream inputStream) {
|
||||
static ImmutableList<VersionedEntity> loadEntities(InputStream inputStream) {
|
||||
return loadEntitiesByTransaction(inputStream).stream()
|
||||
.flatMap(ImmutableList::stream)
|
||||
.collect(toImmutableList());
|
||||
|
||||
@@ -93,7 +93,7 @@ public final class DeleteOldCommitLogsAction implements Runnable {
|
||||
public void run() {
|
||||
DateTime deletionThreshold = clock.nowUtc().minus(maxAge);
|
||||
logger.atInfo().log(
|
||||
"Processing asynchronous deletion of unreferenced CommitLogManifests older than %s",
|
||||
"Processing asynchronous deletion of unreferenced CommitLogManifests older than %s.",
|
||||
deletionThreshold);
|
||||
|
||||
mrRunner
|
||||
@@ -208,7 +208,7 @@ public final class DeleteOldCommitLogsAction implements Runnable {
|
||||
getContext().incrementCounter("EPP resources missing pre-threshold revision (SEE LOGS)");
|
||||
logger.atSevere().log(
|
||||
"EPP resource missing old enough revision: "
|
||||
+ "%s (created on %s) has %d revisions between %s and %s, while threshold is %s",
|
||||
+ "%s (created on %s) has %d revisions between %s and %s, while threshold is %s.",
|
||||
Key.create(eppResource),
|
||||
eppResource.getCreationTime(),
|
||||
eppResource.getRevisions().size(),
|
||||
|
||||
@@ -100,7 +100,7 @@ public final class ExportCommitLogDiffAction implements Runnable {
|
||||
|
||||
// Load the keys of all the manifests to include in this diff.
|
||||
List<Key<CommitLogManifest>> sortedKeys = loadAllDiffKeys(lowerCheckpoint, upperCheckpoint);
|
||||
logger.atInfo().log("Found %d manifests to export", sortedKeys.size());
|
||||
logger.atInfo().log("Found %d manifests to export.", sortedKeys.size());
|
||||
// Open an output channel to GCS, wrapped in a stream for convenience.
|
||||
try (OutputStream gcsStream =
|
||||
gcsUtils.openOutputStream(
|
||||
@@ -124,7 +124,7 @@ public final class ExportCommitLogDiffAction implements Runnable {
|
||||
for (int i = 0; i < keyChunks.size(); i++) {
|
||||
// Force the async load to finish.
|
||||
Collection<CommitLogManifest> chunkValues = nextChunkToExport.values();
|
||||
logger.atInfo().log("Loaded %d manifests", chunkValues.size());
|
||||
logger.atInfo().log("Loaded %d manifests.", chunkValues.size());
|
||||
// Since there is no hard bound on how much data this might be, take care not to let the
|
||||
// Objectify session cache fill up and potentially run out of memory. This is the only safe
|
||||
// point to do this since at this point there is no async load in progress.
|
||||
@@ -134,12 +134,12 @@ public final class ExportCommitLogDiffAction implements Runnable {
|
||||
nextChunkToExport = auditedOfy().load().keys(keyChunks.get(i + 1));
|
||||
}
|
||||
exportChunk(gcsStream, chunkValues);
|
||||
logger.atInfo().log("Exported %d manifests", chunkValues.size());
|
||||
logger.atInfo().log("Exported %d manifests.", chunkValues.size());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
logger.atInfo().log("Exported %d manifests in total", sortedKeys.size());
|
||||
logger.atInfo().log("Exported %d total manifests.", sortedKeys.size());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -94,14 +94,14 @@ class GcsDiffFileLister {
|
||||
logger.atInfo().log(
|
||||
"Gap discovered in sequence terminating at %s, missing file: %s",
|
||||
sequence.lastKey(), filename);
|
||||
logger.atInfo().log("Found sequence from %s to %s", checkpointTime, lastTime);
|
||||
logger.atInfo().log("Found sequence from %s to %s.", checkpointTime, lastTime);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
sequence.put(checkpointTime, blobInfo);
|
||||
checkpointTime = getLowerBoundTime(blobInfo);
|
||||
}
|
||||
logger.atInfo().log("Found sequence from %s to %s", checkpointTime, lastTime);
|
||||
logger.atInfo().log("Found sequence from %s to %s.", checkpointTime, lastTime);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ class GcsDiffFileLister {
|
||||
}
|
||||
}
|
||||
if (upperBoundTimesToBlobInfo.isEmpty()) {
|
||||
logger.atInfo().log("No files found");
|
||||
logger.atInfo().log("No files found.");
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ class GcsDiffFileLister {
|
||||
|
||||
logger.atInfo().log(
|
||||
"Actual restore from time: %s", getLowerBoundTime(sequence.firstEntry().getValue()));
|
||||
logger.atInfo().log("Found %d files to restore", sequence.size());
|
||||
logger.atInfo().log("Found %d files to restore.", sequence.size());
|
||||
return ImmutableList.copyOf(sequence.values());
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
|
||||
return;
|
||||
}
|
||||
Optional<Lock> lock =
|
||||
Lock.acquire(
|
||||
Lock.acquireSql(
|
||||
this.getClass().getSimpleName(), null, LEASE_LENGTH, requestStatusChecker, false);
|
||||
if (!lock.isPresent()) {
|
||||
String message = "Can't acquire SQL commit log replay lock, aborting.";
|
||||
@@ -140,7 +140,7 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
response.setPayload(message);
|
||||
} finally {
|
||||
lock.ifPresent(Lock::release);
|
||||
lock.ifPresent(Lock::releaseSql);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
|
||||
ofyPojo.getClass());
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.atSevere().log("Error when replaying object %s", ofyPojo);
|
||||
logger.atSevere().log("Error when replaying object %s.", ofyPojo);
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
@@ -300,7 +300,7 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
|
||||
jpaTm().deleteIgnoringReadOnly(entityVKey);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.atSevere().log("Error when deleting key %s", entityVKey);
|
||||
logger.atSevere().log("Error when deleting key %s.", entityVKey);
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,13 +103,13 @@ public class RestoreCommitLogsAction implements Runnable {
|
||||
!FORBIDDEN_ENVIRONMENTS.contains(RegistryEnvironment.get()),
|
||||
"DO NOT RUN IN PRODUCTION OR SANDBOX.");
|
||||
if (dryRun) {
|
||||
logger.atInfo().log("Running in dryRun mode");
|
||||
logger.atInfo().log("Running in dry-run mode.");
|
||||
}
|
||||
String gcsBucket = gcsBucketOverride.orElse(defaultGcsBucket);
|
||||
logger.atInfo().log("Restoring from %s.", gcsBucket);
|
||||
List<BlobInfo> diffFiles = diffLister.listDiffFiles(gcsBucket, fromTime, toTime);
|
||||
if (diffFiles.isEmpty()) {
|
||||
logger.atInfo().log("Nothing to restore");
|
||||
logger.atInfo().log("Nothing to restore.");
|
||||
return;
|
||||
}
|
||||
Map<Integer, DateTime> bucketTimestamps = new HashMap<>();
|
||||
@@ -143,7 +143,7 @@ public class RestoreCommitLogsAction implements Runnable {
|
||||
.build()),
|
||||
Stream.of(CommitLogCheckpointRoot.create(lastCheckpoint.getCheckpointTime())))
|
||||
.collect(toImmutableList()));
|
||||
logger.atInfo().log("Restore complete");
|
||||
logger.atInfo().log("Restore complete.");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -105,29 +105,29 @@ public abstract class VersionedEntity implements Serializable {
|
||||
* VersionedEntity VersionedEntities}. See {@link CommitLogImports#loadEntities} for more
|
||||
* information.
|
||||
*/
|
||||
public static Stream<VersionedEntity> fromManifest(CommitLogManifest manifest) {
|
||||
static Stream<VersionedEntity> fromManifest(CommitLogManifest manifest) {
|
||||
long commitTimeMillis = manifest.getCommitTime().getMillis();
|
||||
return manifest.getDeletions().stream()
|
||||
.map(com.googlecode.objectify.Key::getRaw)
|
||||
.map(key -> builder().commitTimeMills(commitTimeMillis).key(key).build());
|
||||
.map(key -> newBuilder().commitTimeMills(commitTimeMillis).key(key).build());
|
||||
}
|
||||
|
||||
/* Converts a {@link CommitLogMutation} to a {@link VersionedEntity}. */
|
||||
public static VersionedEntity fromMutation(CommitLogMutation mutation) {
|
||||
static VersionedEntity fromMutation(CommitLogMutation mutation) {
|
||||
return from(
|
||||
com.googlecode.objectify.Key.create(mutation).getParent().getId(),
|
||||
mutation.getEntityProtoBytes());
|
||||
}
|
||||
|
||||
public static VersionedEntity from(long commitTimeMillis, byte[] entityProtoBytes) {
|
||||
return builder()
|
||||
return newBuilder()
|
||||
.entityProtoBytes(entityProtoBytes)
|
||||
.key(EntityTranslator.createFromPbBytes(entityProtoBytes).getKey())
|
||||
.commitTimeMills(commitTimeMillis)
|
||||
.build();
|
||||
}
|
||||
|
||||
static Builder builder() {
|
||||
private static Builder newBuilder() {
|
||||
return new AutoValue_VersionedEntity.Builder();
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ public abstract class VersionedEntity implements Serializable {
|
||||
|
||||
public abstract VersionedEntity build();
|
||||
|
||||
public Builder entityProtoBytes(byte[] bytes) {
|
||||
Builder entityProtoBytes(byte[] bytes) {
|
||||
return entityProtoBytes(new ImmutableBytes(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_HOST_RENAME;
|
||||
import static google.registry.request.RequestParameters.extractIntParameter;
|
||||
import static google.registry.request.RequestParameters.extractLongParameter;
|
||||
import static google.registry.request.RequestParameters.extractOptionalBooleanParameter;
|
||||
import static google.registry.request.RequestParameters.extractOptionalDatetimeParameter;
|
||||
import static google.registry.request.RequestParameters.extractOptionalIntParameter;
|
||||
import static google.registry.request.RequestParameters.extractOptionalParameter;
|
||||
import static google.registry.request.RequestParameters.extractRequiredDatetimeParameter;
|
||||
@@ -106,6 +107,13 @@ public class BatchModule {
|
||||
return extractIntParameter(req, RelockDomainAction.PREVIOUS_ATTEMPTS_PARAM);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter(ExpandRecurringBillingEventsAction.PARAM_CURSOR_TIME)
|
||||
static Optional<DateTime> provideCursorTime(HttpServletRequest req) {
|
||||
return extractOptionalDatetimeParameter(
|
||||
req, ExpandRecurringBillingEventsAction.PARAM_CURSOR_TIME);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named(QUEUE_ASYNC_ACTIONS)
|
||||
static Queue provideAsyncActionsPushQueue() {
|
||||
|
||||
@@ -311,7 +311,7 @@ public class DeleteContactsAndHostsAction implements Runnable {
|
||||
@Override
|
||||
public void reduce(final DeletionRequest deletionRequest, ReducerInput<Boolean> values) {
|
||||
final boolean hasNoActiveReferences = !Iterators.contains(values, true);
|
||||
logger.atInfo().log("Processing async deletion request for %s", deletionRequest.key());
|
||||
logger.atInfo().log("Processing async deletion request for %s.", deletionRequest.key());
|
||||
DeletionResult result =
|
||||
tm()
|
||||
.transactNew(
|
||||
@@ -605,12 +605,12 @@ public class DeleteContactsAndHostsAction implements Runnable {
|
||||
static boolean doesResourceStateAllowDeletion(EppResource resource, DateTime now) {
|
||||
Key<EppResource> key = Key.create(resource);
|
||||
if (isDeleted(resource, now)) {
|
||||
logger.atWarning().log("Cannot asynchronously delete %s because it is already deleted", key);
|
||||
logger.atWarning().log("Cannot asynchronously delete %s because it is already deleted.", key);
|
||||
return false;
|
||||
}
|
||||
if (!resource.getStatusValues().contains(PENDING_DELETE)) {
|
||||
logger.atWarning().log(
|
||||
"Cannot asynchronously delete %s because it is not in PENDING_DELETE", key);
|
||||
"Cannot asynchronously delete %s because it is not in PENDING_DELETE.", key);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -167,7 +167,7 @@ public class DeleteExpiredDomainsAction implements Runnable {
|
||||
|
||||
/** 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());
|
||||
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
|
||||
// in the intervening period since the query above found it.
|
||||
@@ -203,7 +203,7 @@ public class DeleteExpiredDomainsAction implements Runnable {
|
||||
|
||||
if (eppOutput.isPresent()) {
|
||||
if (eppOutput.get().isSuccess()) {
|
||||
logger.atInfo().log("Successfully deleted domain %s", domain.getDomainName());
|
||||
logger.atInfo().log("Successfully deleted domain '%s'.", domain.getDomainName());
|
||||
} else {
|
||||
logger.atSevere().log(
|
||||
"Failed to delete domain %s; EPP response:\n\n%s",
|
||||
|
||||
@@ -142,7 +142,7 @@ public class DeleteLoadTestDataAction implements Runnable {
|
||||
// that are linked to domains (since it would break the foreign keys)
|
||||
if (EppResourceUtils.isLinked(contact.createVKey(), clock.nowUtc())) {
|
||||
logger.atWarning().log(
|
||||
"Cannot delete contact with repo ID %s since it is referenced from a domain",
|
||||
"Cannot delete contact with repo ID %s since it is referenced from a domain.",
|
||||
contact.getRepoId());
|
||||
return;
|
||||
}
|
||||
@@ -177,7 +177,7 @@ public class DeleteLoadTestDataAction implements Runnable {
|
||||
HistoryEntryDao.loadHistoryObjectsForResource(eppResource.createVKey());
|
||||
if (isDryRun) {
|
||||
logger.atInfo().log(
|
||||
"Would delete repo ID %s along with %d history objects",
|
||||
"Would delete repo ID %s along with %d history objects.",
|
||||
eppResource.getRepoId(), historyObjects.size());
|
||||
} else {
|
||||
historyObjects.forEach(tm()::delete);
|
||||
|
||||
@@ -228,7 +228,7 @@ public class DeleteProberDataAction implements Runnable {
|
||||
if (EppResourceUtils.isActive(domain, tm().getTransactionTime())) {
|
||||
if (isDryRun) {
|
||||
logger.atInfo().log(
|
||||
"Would soft-delete the active domain: %s (%s)",
|
||||
"Would soft-delete the active domain: %s (%s).",
|
||||
domain.getDomainName(), domain.getRepoId());
|
||||
} else {
|
||||
softDeleteDomain(domain, registryAdminRegistrarId, dnsQueue);
|
||||
@@ -237,7 +237,7 @@ public class DeleteProberDataAction implements Runnable {
|
||||
} else {
|
||||
if (isDryRun) {
|
||||
logger.atInfo().log(
|
||||
"Would hard-delete the non-active domain: %s (%s) and its dependents",
|
||||
"Would hard-delete the non-active domain: %s (%s) and its dependents.",
|
||||
domain.getDomainName(), domain.getRepoId());
|
||||
} else {
|
||||
domainRepoIdsToHardDelete.add(domain.getRepoId());
|
||||
@@ -331,7 +331,7 @@ public class DeleteProberDataAction implements Runnable {
|
||||
getContext().incrementCounter("skipped, non-prober data");
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.atSevere().withCause(t).log("Error while deleting prober data for key %s", key);
|
||||
logger.atSevere().withCause(t).log("Error while deleting prober data for key %s.", key);
|
||||
getContext().incrementCounter(String.format("error, kind %s", key.getKind()));
|
||||
}
|
||||
}
|
||||
@@ -372,7 +372,7 @@ public class DeleteProberDataAction implements Runnable {
|
||||
if (EppResourceUtils.isActive(domain, now)) {
|
||||
if (isDryRun) {
|
||||
logger.atInfo().log(
|
||||
"Would soft-delete the active domain: %s (%s)", domainName, domainKey);
|
||||
"Would soft-delete the active domain: %s (%s).", domainName, domainKey);
|
||||
} else {
|
||||
tm().transact(() -> softDeleteDomain(domain, registryAdminRegistrarId, dnsQueue));
|
||||
}
|
||||
|
||||
@@ -148,9 +148,9 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
|
||||
.reduce(0, Integer::sum);
|
||||
|
||||
if (!isDryRun) {
|
||||
logger.atInfo().log("Saved OneTime billing events", numBillingEventsSaved);
|
||||
logger.atInfo().log("Saved OneTime billing events.", numBillingEventsSaved);
|
||||
} else {
|
||||
logger.atInfo().log("Generated OneTime billing events (dry run)", numBillingEventsSaved);
|
||||
logger.atInfo().log("Generated OneTime billing events (dry run).", numBillingEventsSaved);
|
||||
}
|
||||
logger.atInfo().log(
|
||||
"Recurring event expansion %s complete for billing event range [%s, %s).",
|
||||
|
||||
@@ -173,7 +173,7 @@ public class RefreshDnsOnHostRenameAction implements Runnable {
|
||||
retrier.callWithRetry(
|
||||
() -> dnsQueue.addDomainRefreshTask(domainName),
|
||||
TransientFailureException.class);
|
||||
logger.atInfo().log("Enqueued DNS refresh for domain %s.", domainName);
|
||||
logger.atInfo().log("Enqueued DNS refresh for domain '%s'.", domainName);
|
||||
});
|
||||
deleteTasksWithRetry(
|
||||
refreshRequests,
|
||||
|
||||
@@ -313,7 +313,7 @@ public class RelockDomainAction implements Runnable {
|
||||
builder.add(new InternetAddress(registryLockEmailAddress));
|
||||
} catch (AddressException e) {
|
||||
// This shouldn't stop any other emails going out, so swallow it
|
||||
logger.atWarning().log("Invalid email address %s", registryLockEmailAddress);
|
||||
logger.atWarning().log("Invalid email address '%s'.", registryLockEmailAddress);
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
|
||||
@@ -55,6 +55,7 @@ import org.joda.time.format.DateTimeFormatter;
|
||||
path = SendExpiringCertificateNotificationEmailAction.PATH,
|
||||
auth = Auth.AUTH_INTERNAL_OR_ADMIN)
|
||||
public class SendExpiringCertificateNotificationEmailAction implements Runnable {
|
||||
|
||||
public static final String PATH = "/_dr/task/sendExpiringCertificateNotificationEmail";
|
||||
/**
|
||||
* Used as an offset when storing the last notification email sent date.
|
||||
@@ -96,8 +97,13 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
|
||||
public void run() {
|
||||
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
||||
try {
|
||||
sendNotificationEmails();
|
||||
int numEmailsSent = sendNotificationEmails();
|
||||
String message =
|
||||
String.format(
|
||||
"Done. Sent %d expiring certificate notification emails in total.", numEmailsSent);
|
||||
logger.atInfo().log(message);
|
||||
response.setStatus(SC_OK);
|
||||
response.setPayload(message);
|
||||
} catch (Exception e) {
|
||||
logger.atWarning().withCause(e).log(
|
||||
"Exception thrown when sending expiring certificate notification emails.");
|
||||
@@ -107,9 +113,11 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of registrars that should receive expiring notification emails. There are two
|
||||
* certificates that should be considered (the main certificate and failOver certificate). The
|
||||
* registrars should receive notifications if one of the certificate checks returns true.
|
||||
* Returns a list of registrars that should receive expiring notification emails.
|
||||
*
|
||||
* <p>There are two certificates that should be considered (the main certificate and failOver
|
||||
* certificate). The registrars should receive notifications if one of the certificate checks
|
||||
* returns true.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
ImmutableList<RegistrarInfo> getRegistrarsWithExpiringCertificates() {
|
||||
@@ -151,15 +159,17 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
|
||||
}
|
||||
try {
|
||||
ImmutableSet<InternetAddress> recipients = getEmailAddresses(registrar, Type.TECH);
|
||||
ImmutableSet<InternetAddress> ccs = getEmailAddresses(registrar, Type.ADMIN);
|
||||
Date expirationDate = certificateChecker.getCertificate(certificate.get()).getNotAfter();
|
||||
logger.atInfo().log(
|
||||
"Registrar %s should receive an email that its %s SSL certificate will expire on %s.",
|
||||
registrar.getRegistrarName(),
|
||||
" %s SSL certificate of registrar '%s' will expire on %s.",
|
||||
certificateType.getDisplayName(),
|
||||
registrar.getRegistrarName(),
|
||||
expirationDate.toString());
|
||||
if (recipients.isEmpty()) {
|
||||
if (recipients.isEmpty() && ccs.isEmpty()) {
|
||||
logger.atWarning().log(
|
||||
"Registrar %s contains no email addresses to receive notification email.",
|
||||
"Registrar %s contains no TECH nor ADMIN email addresses to receive notification"
|
||||
+ " email.",
|
||||
registrar.getRegistrarName());
|
||||
return false;
|
||||
}
|
||||
@@ -174,7 +184,7 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
|
||||
expirationDate,
|
||||
registrar.getRegistrarId()))
|
||||
.setRecipients(recipients)
|
||||
.setCcs(getEmailAddresses(registrar, Type.ADMIN))
|
||||
.setCcs(ccs)
|
||||
.build());
|
||||
/*
|
||||
* A duration time offset is used here to ensure that date comparison between two
|
||||
@@ -243,32 +253,32 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
|
||||
/** Sends notification emails to registrars with expiring certificates. */
|
||||
@VisibleForTesting
|
||||
int sendNotificationEmails() {
|
||||
int emailsSent = 0;
|
||||
int numEmailsSent = 0;
|
||||
for (RegistrarInfo registrarInfo : getRegistrarsWithExpiringCertificates()) {
|
||||
Registrar registrar = registrarInfo.registrar();
|
||||
if (registrarInfo.isCertExpiring()) {
|
||||
sendNotificationEmail(
|
||||
registrar,
|
||||
registrar.getLastExpiringCertNotificationSentDate(),
|
||||
CertificateType.PRIMARY,
|
||||
registrar.getClientCertificate());
|
||||
emailsSent++;
|
||||
if (registrarInfo.isCertExpiring()
|
||||
&& sendNotificationEmail(
|
||||
registrar,
|
||||
registrar.getLastExpiringCertNotificationSentDate(),
|
||||
CertificateType.PRIMARY,
|
||||
registrar.getClientCertificate())) {
|
||||
numEmailsSent++;
|
||||
}
|
||||
if (registrarInfo.isFailOverCertExpiring()) {
|
||||
sendNotificationEmail(
|
||||
registrar,
|
||||
registrar.getLastExpiringFailoverCertNotificationSentDate(),
|
||||
CertificateType.FAILOVER,
|
||||
registrar.getFailoverClientCertificate());
|
||||
emailsSent++;
|
||||
if (registrarInfo.isFailOverCertExpiring()
|
||||
&& sendNotificationEmail(
|
||||
registrar,
|
||||
registrar.getLastExpiringFailoverCertNotificationSentDate(),
|
||||
CertificateType.FAILOVER,
|
||||
registrar.getFailoverClientCertificate())) {
|
||||
numEmailsSent++;
|
||||
}
|
||||
}
|
||||
logger.atInfo().log(
|
||||
"Attempted to send %d expiring certificate notification emails.", emailsSent);
|
||||
return emailsSent;
|
||||
return numEmailsSent;
|
||||
}
|
||||
|
||||
/** Returns a list of email addresses of the registrar that should receive a notification email */
|
||||
/**
|
||||
* Returns a list of email addresses of the registrar that should receive a notification email.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
ImmutableSet<InternetAddress> getEmailAddresses(Registrar registrar, Type contactType) {
|
||||
ImmutableSortedSet<RegistrarContact> contacts = registrar.getContactsOfType(contactType);
|
||||
@@ -327,6 +337,7 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
|
||||
|
||||
@AutoValue
|
||||
public abstract static class RegistrarInfo {
|
||||
|
||||
static RegistrarInfo create(
|
||||
Registrar registrar, boolean isCertExpiring, boolean isFailOverCertExpiring) {
|
||||
return new AutoValue_SendExpiringCertificateNotificationEmailAction_RegistrarInfo(
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
// 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 google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
|
||||
import static org.apache.http.HttpStatus.SC_OK;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.model.contact.ContactHistory;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Action.Service;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Clock;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Stream;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* An action that wipes out Personal Identifiable Information (PII) fields of {@link ContactHistory}
|
||||
* entities.
|
||||
*
|
||||
* <p>ContactHistory entities should be retained in the database for only certain amount of time.
|
||||
* This periodic wipe out action only applies to SQL.
|
||||
*/
|
||||
@Action(
|
||||
service = Service.BACKEND,
|
||||
path = WipeOutContactHistoryPiiAction.PATH,
|
||||
auth = Auth.AUTH_INTERNAL_OR_ADMIN)
|
||||
public class WipeOutContactHistoryPiiAction implements Runnable {
|
||||
|
||||
public static final String PATH = "/_dr/task/wipeOutContactHistoryPii";
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private final Clock clock;
|
||||
private final Response response;
|
||||
private final int minMonthsBeforeWipeOut;
|
||||
private final int wipeOutQueryBatchSize;
|
||||
|
||||
@Inject
|
||||
public WipeOutContactHistoryPiiAction(
|
||||
Clock clock,
|
||||
@Config("minMonthsBeforeWipeOut") int minMonthsBeforeWipeOut,
|
||||
@Config("wipeOutQueryBatchSize") int wipeOutQueryBatchSize,
|
||||
Response response) {
|
||||
this.clock = clock;
|
||||
this.response = response;
|
||||
this.minMonthsBeforeWipeOut = minMonthsBeforeWipeOut;
|
||||
this.wipeOutQueryBatchSize = wipeOutQueryBatchSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
||||
try {
|
||||
int totalNumOfWipedEntities = 0;
|
||||
DateTime wipeOutTime = clock.nowUtc().minusMonths(minMonthsBeforeWipeOut);
|
||||
logger.atInfo().log(
|
||||
"About to wipe out all PII of contact history entities prior to %s.", wipeOutTime);
|
||||
|
||||
int numOfWipedEntities = 0;
|
||||
do {
|
||||
numOfWipedEntities =
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
wipeOutContactHistoryData(
|
||||
getNextContactHistoryEntitiesWithPiiBatch(wipeOutTime)));
|
||||
totalNumOfWipedEntities += numOfWipedEntities;
|
||||
} while (numOfWipedEntities > 0);
|
||||
String msg =
|
||||
String.format(
|
||||
"Done. Wiped out PII of %d ContactHistory entities in total.",
|
||||
totalNumOfWipedEntities);
|
||||
logger.atInfo().log(msg);
|
||||
response.setPayload(msg);
|
||||
response.setStatus(SC_OK);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.atSevere().withCause(e).log(
|
||||
"Exception thrown during the process of wiping out contact history PII.");
|
||||
response.setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||
response.setPayload(
|
||||
String.format(
|
||||
"Exception thrown during the process of wiping out contact history PII with cause"
|
||||
+ ": %s",
|
||||
e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a stream of up to {@link #wipeOutQueryBatchSize} {@link ContactHistory} entities
|
||||
* containing PII that are prior to @param wipeOutTime.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
Stream<ContactHistory> getNextContactHistoryEntitiesWithPiiBatch(DateTime wipeOutTime) {
|
||||
// email is one of the required fields in EPP, meaning it's initially not null.
|
||||
// Therefore, checking if it's null is one way to avoid processing contact history entities
|
||||
// that have been processed previously. Refer to RFC 5733 for more information.
|
||||
return jpaTm()
|
||||
.query(
|
||||
"FROM ContactHistory WHERE modificationTime < :wipeOutTime " + "AND email IS NOT NULL",
|
||||
ContactHistory.class)
|
||||
.setParameter("wipeOutTime", wipeOutTime)
|
||||
.setMaxResults(wipeOutQueryBatchSize)
|
||||
.getResultStream();
|
||||
}
|
||||
|
||||
/** Wipes out the PII of each of the {@link ContactHistory} entities in the stream. */
|
||||
@VisibleForTesting
|
||||
int wipeOutContactHistoryData(Stream<ContactHistory> contactHistoryEntities) {
|
||||
AtomicInteger numOfEntities = new AtomicInteger(0);
|
||||
contactHistoryEntities.forEach(
|
||||
contactHistoryEntity -> {
|
||||
jpaTm().update(contactHistoryEntity.asBuilder().wipeOutPii().build());
|
||||
numOfEntities.incrementAndGet();
|
||||
});
|
||||
logger.atInfo().log(
|
||||
"Wiped out all PII fields of %d ContactHistory entities.", numOfEntities.get());
|
||||
return numOfEntities.get();
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,12 @@ public class WipeoutDatastoreAction implements Runnable {
|
||||
.setJobName(createJobName("bulk-delete-datastore-", clock))
|
||||
.setContainerSpecGcsPath(
|
||||
String.format("%s/%s_metadata.json", stagingBucketUrl, PIPELINE_NAME))
|
||||
.setParameters(ImmutableMap.of("kindsToDelete", "*"));
|
||||
.setParameters(
|
||||
ImmutableMap.of(
|
||||
"kindsToDelete",
|
||||
"*",
|
||||
"registryEnvironment",
|
||||
RegistryEnvironment.get().name()));
|
||||
LaunchFlexTemplateResponse launchResponse =
|
||||
dataflow
|
||||
.projects()
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.beam.common;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import java.util.List;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.EntityTransaction;
|
||||
|
||||
/**
|
||||
* A database snapshot shareable by concurrent queries from multiple database clients. A snapshot is
|
||||
* uniquely identified by its {@link #getSnapshotId snapshotId}, and must stay open until all
|
||||
* concurrent queries to this snapshot have attached to it by calling {@link
|
||||
* google.registry.persistence.transaction.JpaTransactionManager#setDatabaseSnapshot}. However, it
|
||||
* can be closed before those queries complete.
|
||||
*
|
||||
* <p>This feature is <em>Postgresql-only</em>.
|
||||
*
|
||||
* <p>To support large queries, transaction isolation level is fixed at the REPEATABLE_READ to avoid
|
||||
* exhausting predicate locks at the SERIALIZABLE level.
|
||||
*/
|
||||
// TODO(b/193662898): vendor-independent support for richer transaction semantics.
|
||||
public class DatabaseSnapshot implements AutoCloseable {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private String snapshotId;
|
||||
private EntityManager entityManager;
|
||||
private EntityTransaction transaction;
|
||||
|
||||
private DatabaseSnapshot() {}
|
||||
|
||||
public String getSnapshotId() {
|
||||
checkState(entityManager != null, "Snapshot not opened yet.");
|
||||
checkState(entityManager.isOpen(), "Snapshot already closed.");
|
||||
return snapshotId;
|
||||
}
|
||||
|
||||
private DatabaseSnapshot open() {
|
||||
entityManager = jpaTm().getStandaloneEntityManager();
|
||||
transaction = entityManager.getTransaction();
|
||||
transaction.setRollbackOnly();
|
||||
transaction.begin();
|
||||
|
||||
entityManager
|
||||
.createNativeQuery("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ")
|
||||
.executeUpdate();
|
||||
|
||||
List<?> snapshotIds =
|
||||
entityManager.createNativeQuery("SELECT pg_export_snapshot();").getResultList();
|
||||
checkState(snapshotIds.size() == 1, "Unexpected number of snapshots: %s", snapshotIds.size());
|
||||
snapshotId = (String) snapshotIds.get(0);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (transaction != null && transaction.isActive()) {
|
||||
try {
|
||||
transaction.rollback();
|
||||
} catch (Exception e) {
|
||||
logger.atWarning().withCause(e).log("Failed to close a Database Snapshot");
|
||||
}
|
||||
}
|
||||
if (entityManager != null && entityManager.isOpen()) {
|
||||
entityManager.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static DatabaseSnapshot createSnapshot() {
|
||||
return new DatabaseSnapshot().open();
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ package google.registry.beam.common;
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
|
||||
import google.registry.backup.AppEngineEnvironment;
|
||||
import google.registry.model.contact.ContactResource;
|
||||
import google.registry.persistence.transaction.CriteriaQueryBuilder;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
@@ -59,18 +58,16 @@ public class JpaDemoPipeline implements Serializable {
|
||||
public void processElement() {
|
||||
// AppEngineEnvironment is needed as long as JPA entity classes still depends
|
||||
// on Objectify.
|
||||
try (AppEngineEnvironment allowOfyEntity = new AppEngineEnvironment()) {
|
||||
int result =
|
||||
(Integer)
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.getEntityManager()
|
||||
.createNativeQuery("select 1;")
|
||||
.getSingleResult());
|
||||
verify(result == 1, "Expecting 1, got %s.", result);
|
||||
}
|
||||
int result =
|
||||
(Integer)
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.getEntityManager()
|
||||
.createNativeQuery("select 1;")
|
||||
.getSingleResult());
|
||||
verify(result == 1, "Expecting 1, got %s.", result);
|
||||
counter.inc();
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -20,11 +20,9 @@ import static org.apache.beam.sdk.values.TypeDescriptors.integers;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Streams;
|
||||
import google.registry.backup.AppEngineEnvironment;
|
||||
import google.registry.beam.common.RegistryQuery.CriteriaQuerySupplier;
|
||||
import google.registry.model.UpdateAutoTimestamp;
|
||||
import google.registry.model.UpdateAutoTimestamp.DisableAutoUpdateResource;
|
||||
import google.registry.model.ofy.ObjectifyService;
|
||||
import google.registry.model.replay.SqlEntity;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
import google.registry.persistence.transaction.TransactionManagerFactory;
|
||||
@@ -140,6 +138,9 @@ public final class RegistryJpaIO {
|
||||
|
||||
abstract Coder<T> coder();
|
||||
|
||||
@Nullable
|
||||
abstract String snapshotId();
|
||||
|
||||
abstract Builder<R, T> toBuilder();
|
||||
|
||||
@Override
|
||||
@@ -147,7 +148,9 @@ public final class RegistryJpaIO {
|
||||
public PCollection<T> expand(PBegin input) {
|
||||
return input
|
||||
.apply("Starting " + name(), Create.of((Void) null))
|
||||
.apply("Run query for " + name(), ParDo.of(new QueryRunner<>(query(), resultMapper())))
|
||||
.apply(
|
||||
"Run query for " + name(),
|
||||
ParDo.of(new QueryRunner<>(query(), resultMapper(), snapshotId())))
|
||||
.setCoder(coder())
|
||||
.apply("Reshuffle", Reshuffle.viaRandomKey());
|
||||
}
|
||||
@@ -164,6 +167,18 @@ public final class RegistryJpaIO {
|
||||
return toBuilder().coder(coder).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the database snapshot to use for this query.
|
||||
*
|
||||
* <p>This feature is <em>Postgresql-only</em>. User is responsible for keeping the snapshot
|
||||
* available until all JVM workers have started using it by calling {@link
|
||||
* JpaTransactionManager#setDatabaseSnapshot}.
|
||||
*/
|
||||
// TODO(b/193662898): vendor-independent support for richer transaction semantics.
|
||||
public Read<R, T> withSnapshot(String snapshotId) {
|
||||
return toBuilder().snapshotId(snapshotId).build();
|
||||
}
|
||||
|
||||
static <R, T> Builder<R, T> builder() {
|
||||
return new AutoValue_RegistryJpaIO_Read.Builder<R, T>()
|
||||
.name(DEFAULT_NAME)
|
||||
@@ -181,6 +196,8 @@ public final class RegistryJpaIO {
|
||||
|
||||
abstract Builder<R, T> coder(Coder coder);
|
||||
|
||||
abstract Builder<R, T> snapshotId(@Nullable String sharedSnapshotId);
|
||||
|
||||
abstract Read<R, T> build();
|
||||
|
||||
Builder<R, T> criteriaQuery(CriteriaQuerySupplier<R> criteriaQuery) {
|
||||
@@ -203,22 +220,28 @@ public final class RegistryJpaIO {
|
||||
static class QueryRunner<R, T> extends DoFn<Void, T> {
|
||||
private final RegistryQuery<R> query;
|
||||
private final SerializableFunction<R, T> resultMapper;
|
||||
// java.util.Optional is not serializable. Use of Guava Optional is discouraged.
|
||||
@Nullable private final String snapshotId;
|
||||
|
||||
QueryRunner(RegistryQuery<R> query, SerializableFunction<R, T> resultMapper) {
|
||||
QueryRunner(
|
||||
RegistryQuery<R> query,
|
||||
SerializableFunction<R, T> resultMapper,
|
||||
@Nullable String snapshotId) {
|
||||
this.query = query;
|
||||
this.resultMapper = resultMapper;
|
||||
this.snapshotId = snapshotId;
|
||||
}
|
||||
|
||||
@ProcessElement
|
||||
public void processElement(OutputReceiver<T> outputReceiver) {
|
||||
// AppEngineEnvironment is need for handling VKeys, which involve Ofy keys. Unlike
|
||||
// SqlBatchWriter, it is unnecessary to initialize ObjectifyService in this class.
|
||||
try (AppEngineEnvironment env = new AppEngineEnvironment()) {
|
||||
// TODO(b/187210388): JpaTransactionManager should support non-transactional query.
|
||||
jpaTm()
|
||||
.transactNoRetry(
|
||||
() -> query.stream().map(resultMapper::apply).forEach(outputReceiver::output));
|
||||
}
|
||||
jpaTm()
|
||||
.transactNoRetry(
|
||||
() -> {
|
||||
if (snapshotId != null) {
|
||||
jpaTm().setDatabaseSnapshot(snapshotId);
|
||||
}
|
||||
query.stream().map(resultMapper::apply).forEach(outputReceiver::output);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -364,16 +387,6 @@ public final class RegistryJpaIO {
|
||||
this.withAutoTimestamp = withAutoTimestamp;
|
||||
}
|
||||
|
||||
@Setup
|
||||
public void setup() {
|
||||
// AppEngineEnvironment is needed as long as Objectify keys are still involved in the handling
|
||||
// of SQL entities (e.g., in VKeys). ObjectifyService needs to be initialized when conversion
|
||||
// between Ofy entity and Datastore entity is needed.
|
||||
try (AppEngineEnvironment env = new AppEngineEnvironment()) {
|
||||
ObjectifyService.initOfy();
|
||||
}
|
||||
}
|
||||
|
||||
@ProcessElement
|
||||
public void processElement(@Element KV<ShardedKey<Integer>, Iterable<T>> kv) {
|
||||
if (withAutoTimestamp) {
|
||||
@@ -386,19 +399,17 @@ public final class RegistryJpaIO {
|
||||
}
|
||||
|
||||
private void actuallyProcessElement(@Element KV<ShardedKey<Integer>, Iterable<T>> kv) {
|
||||
try (AppEngineEnvironment env = new AppEngineEnvironment()) {
|
||||
ImmutableList<Object> entities =
|
||||
Streams.stream(kv.getValue())
|
||||
.map(this.jpaConverter::apply)
|
||||
// TODO(b/177340730): post migration delete the line below.
|
||||
.filter(Objects::nonNull)
|
||||
.collect(ImmutableList.toImmutableList());
|
||||
try {
|
||||
jpaTm().transact(() -> jpaTm().putAll(entities));
|
||||
counter.inc(entities.size());
|
||||
} catch (RuntimeException e) {
|
||||
processSingly(entities);
|
||||
}
|
||||
ImmutableList<Object> entities =
|
||||
Streams.stream(kv.getValue())
|
||||
.map(this.jpaConverter::apply)
|
||||
// TODO(b/177340730): post migration delete the line below.
|
||||
.filter(Objects::nonNull)
|
||||
.collect(ImmutableList.toImmutableList());
|
||||
try {
|
||||
jpaTm().transact(() -> jpaTm().putAll(entities));
|
||||
counter.inc(entities.size());
|
||||
} catch (RuntimeException e) {
|
||||
processSingly(entities);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import google.registry.config.CredentialModule;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.config.RegistryConfig.ConfigModule;
|
||||
import google.registry.persistence.PersistenceModule;
|
||||
import google.registry.persistence.PersistenceModule.BeamBulkQueryJpaTm;
|
||||
import google.registry.persistence.PersistenceModule.BeamJpaTm;
|
||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
@@ -45,9 +46,19 @@ public interface RegistryPipelineComponent {
|
||||
@Config("projectId")
|
||||
String getProjectId();
|
||||
|
||||
/** Returns the regular {@link JpaTransactionManager} for general use. */
|
||||
@BeamJpaTm
|
||||
Lazy<JpaTransactionManager> getJpaTransactionManager();
|
||||
|
||||
/**
|
||||
* Returns a {@link JpaTransactionManager} optimized for bulk loading multi-level JPA entities
|
||||
* ({@link google.registry.model.domain.DomainBase} and {@link
|
||||
* google.registry.model.domain.DomainHistory}). Please refer to {@link
|
||||
* google.registry.model.bulkquery.BulkQueryEntities} for more information.
|
||||
*/
|
||||
@BeamBulkQueryJpaTm
|
||||
Lazy<JpaTransactionManager> getBulkQueryJpaTransactionManager();
|
||||
|
||||
@Component.Builder
|
||||
interface Builder {
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.beam.common;
|
||||
|
||||
import google.registry.beam.common.RegistryJpaIO.Write;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.persistence.PersistenceModule.JpaTransactionManagerType;
|
||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
import java.util.Objects;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -34,7 +35,6 @@ import org.apache.beam.sdk.options.Description;
|
||||
public interface RegistryPipelineOptions extends GcpOptions {
|
||||
|
||||
@Description("The Registry environment.")
|
||||
@Nullable
|
||||
RegistryEnvironment getRegistryEnvironment();
|
||||
|
||||
void setRegistryEnvironment(RegistryEnvironment environment);
|
||||
@@ -45,6 +45,12 @@ public interface RegistryPipelineOptions extends GcpOptions {
|
||||
|
||||
void setIsolationOverride(TransactionIsolationLevel isolationOverride);
|
||||
|
||||
@Description("The JPA Transaction Manager to use.")
|
||||
@Default.Enum(value = "REGULAR")
|
||||
JpaTransactionManagerType getJpaTransactionManagerType();
|
||||
|
||||
void setJpaTransactionManagerType(JpaTransactionManagerType jpaTransactionManagerType);
|
||||
|
||||
@Description("The number of entities to write to the SQL database in one operation.")
|
||||
@Default.Integer(20)
|
||||
int getSqlWriteBatchSize();
|
||||
|
||||
@@ -20,6 +20,8 @@ import com.google.auto.service.AutoService;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import dagger.Lazy;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.config.SystemPropertySetter;
|
||||
import google.registry.model.AppEngineEnvironment;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
import google.registry.persistence.transaction.TransactionManagerFactory;
|
||||
import org.apache.beam.sdk.harness.JvmInitializer;
|
||||
@@ -35,18 +37,35 @@ import org.apache.beam.sdk.options.PipelineOptions;
|
||||
@AutoService(JvmInitializer.class)
|
||||
public class RegistryPipelineWorkerInitializer implements JvmInitializer {
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
public static final String PROPERTY = "google.registry.beam";
|
||||
|
||||
@Override
|
||||
public void beforeProcessing(PipelineOptions options) {
|
||||
RegistryPipelineOptions registryOptions = options.as(RegistryPipelineOptions.class);
|
||||
RegistryEnvironment environment = registryOptions.getRegistryEnvironment();
|
||||
if (environment == null || environment.equals(RegistryEnvironment.UNITTEST)) {
|
||||
return;
|
||||
throw new RuntimeException(
|
||||
"A registry environment must be specified in the pipeline options.");
|
||||
}
|
||||
logger.atInfo().log("Setting up RegistryEnvironment: %s", environment);
|
||||
logger.atInfo().log("Setting up RegistryEnvironment %s.", environment);
|
||||
environment.setup();
|
||||
Lazy<JpaTransactionManager> transactionManagerLazy =
|
||||
toRegistryPipelineComponent(registryOptions).getJpaTransactionManager();
|
||||
RegistryPipelineComponent registryPipelineComponent =
|
||||
toRegistryPipelineComponent(registryOptions);
|
||||
Lazy<JpaTransactionManager> transactionManagerLazy;
|
||||
switch (registryOptions.getJpaTransactionManagerType()) {
|
||||
case BULK_QUERY:
|
||||
transactionManagerLazy = registryPipelineComponent.getBulkQueryJpaTransactionManager();
|
||||
break;
|
||||
case REGULAR:
|
||||
default:
|
||||
transactionManagerLazy = registryPipelineComponent.getJpaTransactionManager();
|
||||
}
|
||||
TransactionManagerFactory.setJpaTmOnBeamWorker(transactionManagerLazy::get);
|
||||
// Masquerade all threads as App Engine threads so we can create Ofy keys in the pipeline. Also
|
||||
// loads all ofy entities.
|
||||
new AppEngineEnvironment("Beam").setEnvironmentForAllThreads();
|
||||
// Set the system property so that we can call IdService.allocateId() without access to
|
||||
// datastore.
|
||||
SystemPropertySetter.PRODUCTION_IMPL.setProperty(PROPERTY, "true");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.datastore.v1.Entity;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import org.apache.beam.sdk.Pipeline;
|
||||
@@ -308,6 +309,11 @@ public class BulkDeleteDatastorePipeline {
|
||||
|
||||
public interface BulkDeletePipelineOptions extends GcpOptions {
|
||||
|
||||
@Description("The Registry environment.")
|
||||
RegistryEnvironment getRegistryEnvironment();
|
||||
|
||||
void setRegistryEnvironment(RegistryEnvironment environment);
|
||||
|
||||
@Description(
|
||||
"The Datastore KINDs to be deleted. The format may be:\n"
|
||||
+ "\t- The list of kinds to be deleted as a comma-separated string, or\n"
|
||||
|
||||
@@ -169,14 +169,15 @@ public class DatastoreV1 {
|
||||
int numSplits;
|
||||
try {
|
||||
long estimatedSizeBytes = getEstimatedSizeBytes(datastore, query, namespace);
|
||||
logger.atInfo().log("Estimated size bytes for the query is: %s", estimatedSizeBytes);
|
||||
logger.atInfo().log("Estimated size for the query is %d bytes.", estimatedSizeBytes);
|
||||
numSplits =
|
||||
(int)
|
||||
Math.min(
|
||||
NUM_QUERY_SPLITS_MAX,
|
||||
Math.round(((double) estimatedSizeBytes) / DEFAULT_BUNDLE_SIZE_BYTES));
|
||||
} catch (Exception e) {
|
||||
logger.atWarning().log("Failed the fetch estimatedSizeBytes for query: %s", query, e);
|
||||
logger.atWarning().withCause(e).log(
|
||||
"Failed the fetch estimatedSizeBytes for query: %s", query);
|
||||
// Fallback in case estimated size is unavailable.
|
||||
numSplits = NUM_QUERY_SPLITS_MIN;
|
||||
}
|
||||
@@ -215,7 +216,7 @@ public class DatastoreV1 {
|
||||
private static Entity getLatestTableStats(
|
||||
String ourKind, @Nullable String namespace, Datastore datastore) throws DatastoreException {
|
||||
long latestTimestamp = queryLatestStatisticsTimestamp(datastore, namespace);
|
||||
logger.atInfo().log("Latest stats timestamp for kind %s is %s", ourKind, latestTimestamp);
|
||||
logger.atInfo().log("Latest stats timestamp for kind %s is %s.", ourKind, latestTimestamp);
|
||||
|
||||
Query.Builder queryBuilder = Query.newBuilder();
|
||||
if (Strings.isNullOrEmpty(namespace)) {
|
||||
@@ -234,7 +235,7 @@ public class DatastoreV1 {
|
||||
long now = System.currentTimeMillis();
|
||||
RunQueryResponse response = datastore.runQuery(request);
|
||||
logger.atFine().log(
|
||||
"Query for per-kind statistics took %sms", System.currentTimeMillis() - now);
|
||||
"Query for per-kind statistics took %d ms.", System.currentTimeMillis() - now);
|
||||
|
||||
QueryResultBatch batch = response.getBatch();
|
||||
if (batch.getEntityResultsCount() == 0) {
|
||||
@@ -330,7 +331,7 @@ public class DatastoreV1 {
|
||||
logger.atWarning().log(
|
||||
"Failed to translate Gql query '%s': %s", gqlQueryWithZeroLimit, e.getMessage());
|
||||
logger.atWarning().log(
|
||||
"User query might have a limit already set, so trying without zero limit");
|
||||
"User query might have a limit already set, so trying without zero limit.");
|
||||
// Retry without the zero limit.
|
||||
return translateGqlQuery(gql, datastore, namespace);
|
||||
} else {
|
||||
@@ -514,10 +515,10 @@ public class DatastoreV1 {
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext c) throws Exception {
|
||||
String gqlQuery = c.element();
|
||||
logger.atInfo().log("User query: '%s'", gqlQuery);
|
||||
logger.atInfo().log("User query: '%s'.", gqlQuery);
|
||||
Query query =
|
||||
translateGqlQueryWithLimitCheck(gqlQuery, datastore, v1Options.getNamespace());
|
||||
logger.atInfo().log("User gql query translated to Query(%s)", query);
|
||||
logger.atInfo().log("User gql query translated to Query(%s).", query);
|
||||
c.output(query);
|
||||
}
|
||||
}
|
||||
@@ -573,7 +574,7 @@ public class DatastoreV1 {
|
||||
estimatedNumSplits = numSplits;
|
||||
}
|
||||
|
||||
logger.atInfo().log("Splitting the query into %s splits", estimatedNumSplits);
|
||||
logger.atInfo().log("Splitting the query into %d splits.", estimatedNumSplits);
|
||||
List<Query> querySplits;
|
||||
try {
|
||||
querySplits =
|
||||
@@ -647,7 +648,7 @@ public class DatastoreV1 {
|
||||
throw exception;
|
||||
}
|
||||
if (!BackOffUtils.next(sleeper, backoff)) {
|
||||
logger.atSevere().log("Aborting after %s retries.", MAX_RETRIES);
|
||||
logger.atSevere().log("Aborting after %d retries.", MAX_RETRIES);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.backup.AppEngineEnvironment;
|
||||
import google.registry.backup.VersionedEntity;
|
||||
import google.registry.beam.common.RegistryJpaIO;
|
||||
import google.registry.beam.initsql.Transforms.RemoveDomainBaseForeignKeys;
|
||||
@@ -230,9 +229,7 @@ public class InitSqlPipeline implements Serializable {
|
||||
}
|
||||
|
||||
private static ImmutableList<String> toKindStrings(Collection<Class<?>> entityClasses) {
|
||||
try (AppEngineEnvironment env = new AppEngineEnvironment()) {
|
||||
return entityClasses.stream().map(Key::getKind).collect(ImmutableList.toImmutableList());
|
||||
}
|
||||
return entityClasses.stream().map(Key::getKind).collect(ImmutableList.toImmutableList());
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import static google.registry.beam.initsql.BackupPaths.getExportFilePatterns;
|
||||
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
|
||||
import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
|
||||
import static java.util.Comparator.comparing;
|
||||
import static org.apache.beam.sdk.values.TypeDescriptors.kvs;
|
||||
import static org.apache.beam.sdk.values.TypeDescriptors.strings;
|
||||
@@ -261,8 +262,8 @@ public final class Transforms {
|
||||
|
||||
// Production data repair configs go below. See b/185954992.
|
||||
|
||||
// Prober domains in bad state, without associated contacts, hosts, billings, and history.
|
||||
// They can be safely ignored.
|
||||
// Prober domains in bad state, without associated contacts, hosts, billings, and non-synthesized
|
||||
// history. They can be safely ignored.
|
||||
private static final ImmutableSet<String> IGNORED_DOMAINS =
|
||||
ImmutableSet.of("6AF6D2-IQCANT", "2-IQANYT");
|
||||
|
||||
@@ -277,7 +278,7 @@ public final class Transforms {
|
||||
|
||||
// Prober contacts referencing phantom registrars. They and their associated history entries can
|
||||
// be safely ignored.
|
||||
private static final ImmutableSet IGNORED_CONTACTS =
|
||||
private static final ImmutableSet<String> IGNORED_CONTACTS =
|
||||
ImmutableSet.of(
|
||||
"1_WJ0TEST-GOOGLE", "1_WJ1TEST-GOOGLE", "1_WJ2TEST-GOOGLE", "1_WJ3TEST-GOOGLE");
|
||||
|
||||
@@ -298,7 +299,7 @@ public final class Transforms {
|
||||
return !IGNORED_HOSTS.contains(roid);
|
||||
}
|
||||
if (entity.getKind().equals("HistoryEntry")) {
|
||||
// Remove production bad data: History of the contacts to be ignored:
|
||||
// Remove production bad data: Histories of ignored EPP resources:
|
||||
com.google.appengine.api.datastore.Key parentKey = entity.getKey().getParent();
|
||||
if (parentKey.getKind().equals("ContactResource")) {
|
||||
String contactRoid = parentKey.getName();
|
||||
@@ -308,6 +309,10 @@ public final class Transforms {
|
||||
String hostRoid = parentKey.getName();
|
||||
return !IGNORED_HOSTS.contains(hostRoid);
|
||||
}
|
||||
if (parentKey.getKind().equals("DomainBase")) {
|
||||
String domainRoid = parentKey.getName();
|
||||
return !IGNORED_DOMAINS.contains(domainRoid);
|
||||
}
|
||||
}
|
||||
// End of production-specific checks.
|
||||
|
||||
@@ -320,7 +325,8 @@ public final class Transforms {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Entity repairBadData(Entity entity) {
|
||||
@VisibleForTesting
|
||||
static Entity repairBadData(Entity entity) {
|
||||
if (entity.getKind().equals("Cancellation")
|
||||
&& Objects.equals(entity.getProperty("reason"), "AUTO_RENEW")) {
|
||||
// AUTO_RENEW has been moved from 'reason' to flags. Change reason to RENEW and add the
|
||||
@@ -328,6 +334,15 @@ public final class Transforms {
|
||||
// instead of append. See b/185954992.
|
||||
entity.setUnindexedProperty("reason", Reason.RENEW.name());
|
||||
entity.setUnindexedProperty("flags", ImmutableList.of(Flag.AUTO_RENEW.name()));
|
||||
} else if (entity.getKind().equals("DomainBase")) {
|
||||
// Canonicalize old domain/host names from 2016 and earlier before we were enforcing this.
|
||||
entity.setIndexedProperty(
|
||||
"fullyQualifiedDomainName",
|
||||
canonicalizeDomainName((String) entity.getProperty("fullyQualifiedDomainName")));
|
||||
} else if (entity.getKind().equals("HostResource")) {
|
||||
entity.setIndexedProperty(
|
||||
"fullyQualifiedHostName",
|
||||
canonicalizeDomainName((String) entity.getProperty("fullyQualifiedHostName")));
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
@@ -365,7 +380,8 @@ public final class Transforms {
|
||||
* Returns a {@link PTransform} that produces a {@link PCollection} containing all elements in the
|
||||
* given {@link Iterable}.
|
||||
*/
|
||||
static PTransform<PBegin, PCollection<String>> toStringPCollection(Iterable<String> strings) {
|
||||
private static PTransform<PBegin, PCollection<String>> toStringPCollection(
|
||||
Iterable<String> strings) {
|
||||
return Create.of(strings).withCoder(StringUtf8Coder.of());
|
||||
}
|
||||
|
||||
@@ -373,7 +389,7 @@ public final class Transforms {
|
||||
* Returns a {@link PTransform} from file {@link Metadata} to {@link VersionedEntity} using
|
||||
* caller-provided {@code transformer}.
|
||||
*/
|
||||
static PTransform<PCollection<Metadata>, PCollection<VersionedEntity>> processFiles(
|
||||
private static PTransform<PCollection<Metadata>, PCollection<VersionedEntity>> processFiles(
|
||||
DoFn<ReadableFile, VersionedEntity> transformer) {
|
||||
return new PTransform<PCollection<Metadata>, PCollection<VersionedEntity>>() {
|
||||
@Override
|
||||
@@ -389,7 +405,7 @@ public final class Transforms {
|
||||
private final DateTime fromTime;
|
||||
private final DateTime toTime;
|
||||
|
||||
public FilterCommitLogFileByTime(DateTime fromTime, DateTime toTime) {
|
||||
FilterCommitLogFileByTime(DateTime fromTime, DateTime toTime) {
|
||||
checkNotNull(fromTime, "fromTime");
|
||||
checkNotNull(toTime, "toTime");
|
||||
checkArgument(
|
||||
|
||||
@@ -19,10 +19,13 @@ import static com.google.common.base.Verify.verify;
|
||||
import static google.registry.model.common.Cursor.getCursorTimeOrStartOfTime;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
||||
import static google.registry.rde.RdeModule.BRDA_QUEUE;
|
||||
import static google.registry.rde.RdeModule.RDE_UPLOAD_QUEUE;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.cloud.storage.BlobId;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.gcs.GcsUtils;
|
||||
import google.registry.keyring.api.PgpHelper;
|
||||
@@ -31,14 +34,20 @@ import google.registry.model.rde.RdeMode;
|
||||
import google.registry.model.rde.RdeNamingUtils;
|
||||
import google.registry.model.rde.RdeRevision;
|
||||
import google.registry.model.tld.Registry;
|
||||
import google.registry.rde.BrdaCopyAction;
|
||||
import google.registry.rde.DepositFragment;
|
||||
import google.registry.rde.Ghostryde;
|
||||
import google.registry.rde.PendingDeposit;
|
||||
import google.registry.rde.RdeCounter;
|
||||
import google.registry.rde.RdeMarshaller;
|
||||
import google.registry.rde.RdeModule;
|
||||
import google.registry.rde.RdeResourceType;
|
||||
import google.registry.rde.RdeUploadAction;
|
||||
import google.registry.rde.RdeUtil;
|
||||
import google.registry.request.Action.Service;
|
||||
import google.registry.request.RequestParameters;
|
||||
import google.registry.tldconfig.idn.IdnTableEnum;
|
||||
import google.registry.util.CloudTasksUtils;
|
||||
import google.registry.xjc.rdeheader.XjcRdeHeader;
|
||||
import google.registry.xjc.rdeheader.XjcRdeHeaderElement;
|
||||
import google.registry.xml.ValidationMode;
|
||||
@@ -68,6 +77,8 @@ public class RdeIO {
|
||||
|
||||
abstract GcsUtils gcsUtils();
|
||||
|
||||
abstract CloudTasksUtils cloudTasksUtils();
|
||||
|
||||
abstract String rdeBucket();
|
||||
|
||||
// It's OK to return a primitive array because we are only using it to construct the
|
||||
@@ -83,7 +94,9 @@ public class RdeIO {
|
||||
|
||||
@AutoValue.Builder
|
||||
abstract static class Builder {
|
||||
abstract Builder setGcsUtils(GcsUtils gcsUtils);
|
||||
abstract Builder setGcsUtils(GcsUtils value);
|
||||
|
||||
abstract Builder setCloudTasksUtils(CloudTasksUtils value);
|
||||
|
||||
abstract Builder setRdeBucket(String value);
|
||||
|
||||
@@ -100,7 +113,8 @@ public class RdeIO {
|
||||
.apply(
|
||||
"Write to GCS",
|
||||
ParDo.of(new RdeWriter(gcsUtils(), rdeBucket(), stagingKeyBytes(), validationMode())))
|
||||
.apply("Update cursors", ParDo.of(new CursorUpdater()));
|
||||
.apply("Update cursors", ParDo.of(new CursorUpdater()))
|
||||
.apply("Enqueue upload action", ParDo.of(new UploadEnqueuer(cloudTasksUtils())));
|
||||
return PDone.in(input.getPipeline());
|
||||
}
|
||||
}
|
||||
@@ -172,7 +186,7 @@ public class RdeIO {
|
||||
|
||||
// Write a gigantic XML file to GCS. We'll start by opening encrypted out/err file handles.
|
||||
|
||||
logger.atInfo().log("Writing %s and %s", xmlFilename, xmlLengthFilename);
|
||||
logger.atInfo().log("Writing files '%s' and '%s'.", xmlFilename, xmlLengthFilename);
|
||||
try (OutputStream gcsOutput = gcsUtils.openOutputStream(xmlFilename);
|
||||
OutputStream lengthOutput = gcsUtils.openOutputStream(xmlLengthFilename);
|
||||
OutputStream ghostrydeEncoder = Ghostryde.encoder(gcsOutput, stagingKey, lengthOutput);
|
||||
@@ -219,7 +233,7 @@ public class RdeIO {
|
||||
//
|
||||
// This will be sent to ICANN once we're done uploading the big XML to the escrow provider.
|
||||
if (mode == RdeMode.FULL) {
|
||||
logger.atInfo().log("Writing %s", reportFilename);
|
||||
logger.atInfo().log("Writing file '%s'.", reportFilename);
|
||||
try (OutputStream gcsOutput = gcsUtils.openOutputStream(reportFilename);
|
||||
OutputStream ghostrydeEncoder = Ghostryde.encoder(gcsOutput, stagingKey)) {
|
||||
counter.makeReport(id, watermark, header, revision).marshal(ghostrydeEncoder, UTF_8);
|
||||
@@ -229,18 +243,19 @@ public class RdeIO {
|
||||
}
|
||||
// Now that we're done, output roll the cursor forward.
|
||||
if (key.manual()) {
|
||||
logger.atInfo().log("Manual operation; not advancing cursor or enqueuing upload task");
|
||||
logger.atInfo().log("Manual operation; not advancing cursor or enqueuing upload task.");
|
||||
} else {
|
||||
outputReceiver.output(KV.of(key, revision));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class CursorUpdater extends DoFn<KV<PendingDeposit, Integer>, Void> {
|
||||
private static class CursorUpdater extends DoFn<KV<PendingDeposit, Integer>, PendingDeposit> {
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
@ProcessElement
|
||||
public void processElement(@Element KV<PendingDeposit, Integer> input) {
|
||||
public void processElement(
|
||||
@Element KV<PendingDeposit, Integer> input, OutputReceiver<PendingDeposit> outputReceiver) {
|
||||
tm().transact(
|
||||
() -> {
|
||||
PendingDeposit key = input.getKey();
|
||||
@@ -265,9 +280,48 @@ public class RdeIO {
|
||||
key);
|
||||
tm().put(Cursor.create(key.cursor(), newPosition, registry));
|
||||
logger.atInfo().log(
|
||||
"Rolled forward %s on %s cursor to %s", key.cursor(), key.tld(), newPosition);
|
||||
"Rolled forward %s on %s cursor to %s.", key.cursor(), key.tld(), newPosition);
|
||||
RdeRevision.saveRevision(key.tld(), key.watermark(), key.mode(), revision);
|
||||
});
|
||||
outputReceiver.output(input.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
private static class UploadEnqueuer extends DoFn<PendingDeposit, Void> {
|
||||
|
||||
private final CloudTasksUtils cloudTasksUtils;
|
||||
|
||||
private UploadEnqueuer(CloudTasksUtils cloudTasksUtils) {
|
||||
this.cloudTasksUtils = cloudTasksUtils;
|
||||
}
|
||||
|
||||
@ProcessElement
|
||||
public void processElement(@Element PendingDeposit input, PipelineOptions options) {
|
||||
if (input.mode() == RdeMode.FULL) {
|
||||
cloudTasksUtils.enqueue(
|
||||
RDE_UPLOAD_QUEUE,
|
||||
CloudTasksUtils.createPostTask(
|
||||
RdeUploadAction.PATH,
|
||||
Service.BACKEND.getServiceId(),
|
||||
ImmutableMultimap.of(
|
||||
RequestParameters.PARAM_TLD,
|
||||
input.tld(),
|
||||
RdeModule.PARAM_PREFIX,
|
||||
options.getJobName() + '/')));
|
||||
} else {
|
||||
cloudTasksUtils.enqueue(
|
||||
BRDA_QUEUE,
|
||||
CloudTasksUtils.createPostTask(
|
||||
BrdaCopyAction.PATH,
|
||||
Service.BACKEND.getServiceId(),
|
||||
ImmutableMultimap.of(
|
||||
RequestParameters.PARAM_TLD,
|
||||
input.tld(),
|
||||
RdeModule.PARAM_WATERMARK,
|
||||
input.watermark().toString(),
|
||||
RdeModule.PARAM_PREFIX,
|
||||
options.getJobName() + '/')));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.google.common.io.BaseEncoding;
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Component;
|
||||
import google.registry.beam.common.RegistryJpaIO;
|
||||
import google.registry.config.CloudTasksUtilsModule;
|
||||
import google.registry.config.CredentialModule;
|
||||
import google.registry.config.RegistryConfig.ConfigModule;
|
||||
import google.registry.gcs.GcsUtils;
|
||||
@@ -44,6 +45,8 @@ import google.registry.rde.PendingDeposit;
|
||||
import google.registry.rde.PendingDeposit.PendingDepositCoder;
|
||||
import google.registry.rde.RdeFragmenter;
|
||||
import google.registry.rde.RdeMarshaller;
|
||||
import google.registry.util.CloudTasksUtils;
|
||||
import google.registry.util.UtilsModule;
|
||||
import google.registry.xml.ValidationMode;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -66,7 +69,6 @@ import org.apache.beam.sdk.options.PipelineOptionsFactory;
|
||||
import org.apache.beam.sdk.transforms.FlatMapElements;
|
||||
import org.apache.beam.sdk.transforms.Flatten;
|
||||
import org.apache.beam.sdk.transforms.GroupByKey;
|
||||
import org.apache.beam.sdk.transforms.Reshuffle;
|
||||
import org.apache.beam.sdk.values.KV;
|
||||
import org.apache.beam.sdk.values.PCollection;
|
||||
import org.apache.beam.sdk.values.PCollectionList;
|
||||
@@ -93,6 +95,7 @@ public class RdePipeline implements Serializable {
|
||||
private final String rdeBucket;
|
||||
private final byte[] stagingKeyBytes;
|
||||
private final GcsUtils gcsUtils;
|
||||
private final CloudTasksUtils cloudTasksUtils;
|
||||
|
||||
// Registrars to be excluded from data escrow. Not including the sandbox-only OTE type so that
|
||||
// if sneaks into production we would get an extra signal.
|
||||
@@ -111,13 +114,14 @@ public class RdePipeline implements Serializable {
|
||||
}
|
||||
|
||||
@Inject
|
||||
RdePipeline(RdePipelineOptions options, GcsUtils gcsUtils) {
|
||||
RdePipeline(RdePipelineOptions options, GcsUtils gcsUtils, CloudTasksUtils cloudTasksUtils) {
|
||||
this.options = options;
|
||||
this.mode = ValidationMode.valueOf(options.getValidationMode());
|
||||
this.pendings = decodePendings(options.getPendings());
|
||||
this.rdeBucket = options.getGcsBucket();
|
||||
this.rdeBucket = options.getRdeStagingBucket();
|
||||
this.stagingKeyBytes = BaseEncoding.base64Url().decode(options.getStagingKey());
|
||||
this.gcsUtils = gcsUtils;
|
||||
this.cloudTasksUtils = cloudTasksUtils;
|
||||
}
|
||||
|
||||
PipelineResult run() {
|
||||
@@ -140,10 +144,11 @@ public class RdePipeline implements Serializable {
|
||||
|
||||
void persistData(PCollection<KV<PendingDeposit, Iterable<DepositFragment>>> input) {
|
||||
input.apply(
|
||||
"Write to GCS and update cursors",
|
||||
"Write to GCS, update cursors, and enqueue upload tasks",
|
||||
RdeIO.Write.builder()
|
||||
.setRdeBucket(rdeBucket)
|
||||
.setGcsUtils(gcsUtils)
|
||||
.setCloudTasksUtils(cloudTasksUtils)
|
||||
.setValidationMode(mode)
|
||||
.setStagingKeyBytes(stagingKeyBytes)
|
||||
.build());
|
||||
@@ -177,18 +182,13 @@ public class RdePipeline implements Serializable {
|
||||
}));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Reshuffle is still recommended by Dataflow.
|
||||
<T extends EppResource>
|
||||
PCollection<KV<PendingDeposit, DepositFragment>> processNonRegistrarEntities(
|
||||
Pipeline pipeline, Class<T> clazz) {
|
||||
return createInputs(pipeline, clazz)
|
||||
.apply("Marshal " + clazz.getSimpleName() + " into DepositFragment", mapToFragments(clazz))
|
||||
.setCoder(KvCoder.of(PendingDepositCoder.of(), SerializableCoder.of(DepositFragment.class)))
|
||||
.apply(
|
||||
"Reshuffle KV<PendingDeposit, DepositFragment> of "
|
||||
+ clazz.getSimpleName()
|
||||
+ " to prevent fusion",
|
||||
Reshuffle.of());
|
||||
.setCoder(
|
||||
KvCoder.of(PendingDepositCoder.of(), SerializableCoder.of(DepositFragment.class)));
|
||||
}
|
||||
|
||||
<T extends EppResource> PCollection<VKey<T>> createInputs(Pipeline pipeline, Class<T> clazz) {
|
||||
@@ -202,7 +202,7 @@ public class RdePipeline implements Serializable {
|
||||
String.class,
|
||||
// TODO: consider adding coders for entities and pass them directly instead of using
|
||||
// VKeys.
|
||||
x -> VKey.create(clazz, x)));
|
||||
x -> VKey.createSql(clazz, x)));
|
||||
}
|
||||
|
||||
<T extends EppResource>
|
||||
@@ -270,7 +270,7 @@ public class RdePipeline implements Serializable {
|
||||
* Encodes the TLD to pending deposit map in an URL safe string that is sent to the pipeline
|
||||
* worker by the pipeline launcher as a pipeline option.
|
||||
*/
|
||||
static String encodePendings(ImmutableSetMultimap<String, PendingDeposit> pendings)
|
||||
public static String encodePendings(ImmutableSetMultimap<String, PendingDeposit> pendings)
|
||||
throws IOException {
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
ObjectOutputStream oos = new ObjectOutputStream(baos);
|
||||
@@ -282,13 +282,24 @@ public class RdePipeline implements Serializable {
|
||||
|
||||
public static void main(String[] args) throws IOException, ClassNotFoundException {
|
||||
PipelineOptionsFactory.register(RdePipelineOptions.class);
|
||||
RdePipelineOptions options = PipelineOptionsFactory.fromArgs(args).as(RdePipelineOptions.class);
|
||||
RdePipelineOptions options =
|
||||
PipelineOptionsFactory.fromArgs(args).withValidation().as(RdePipelineOptions.class);
|
||||
// RegistryPipelineWorkerInitializer only initializes before pipeline executions, after the
|
||||
// main() function constructed the graph. We need the registry environment set up so that we
|
||||
// can create a CloudTasksUtils which uses the environment-dependent config file.
|
||||
options.getRegistryEnvironment().setup();
|
||||
options.setIsolationOverride(TransactionIsolationLevel.TRANSACTION_READ_COMMITTED);
|
||||
DaggerRdePipeline_RdePipelineComponent.builder().options(options).build().rdePipeline().run();
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Component(modules = {CredentialModule.class, ConfigModule.class})
|
||||
@Component(
|
||||
modules = {
|
||||
CredentialModule.class,
|
||||
ConfigModule.class,
|
||||
CloudTasksUtilsModule.class,
|
||||
UtilsModule.class
|
||||
})
|
||||
interface RdePipelineComponent {
|
||||
RdePipeline rdePipeline();
|
||||
|
||||
|
||||
@@ -31,9 +31,9 @@ public interface RdePipelineOptions extends RegistryPipelineOptions {
|
||||
void setValidationMode(String value);
|
||||
|
||||
@Description("The GCS bucket where the encrypted RDE deposits will be uploaded to.")
|
||||
String getGcsBucket();
|
||||
String getRdeStagingBucket();
|
||||
|
||||
void setGcsBucket(String value);
|
||||
void setRdeStagingBucket(String value);
|
||||
|
||||
@Description("The Base64-encoded PGP public key to encrypt the deposits.")
|
||||
String getStagingKey();
|
||||
|
||||
@@ -218,7 +218,7 @@ public class SafeBrowsingTransforms {
|
||||
throws JSONException, IOException {
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
if (statusCode != SC_OK) {
|
||||
logger.atWarning().log("Got unexpected status code %s from response", statusCode);
|
||||
logger.atWarning().log("Got unexpected status code %s from response.", statusCode);
|
||||
} else {
|
||||
// Unpack the response body
|
||||
JSONObject responseBody =
|
||||
@@ -227,7 +227,7 @@ public class SafeBrowsingTransforms {
|
||||
new InputStreamReader(response.getEntity().getContent(), UTF_8)));
|
||||
logger.atInfo().log("Got response: %s", responseBody.toString());
|
||||
if (responseBody.length() == 0) {
|
||||
logger.atInfo().log("Response was empty, no threats detected");
|
||||
logger.atInfo().log("Response was empty, no threats detected.");
|
||||
} else {
|
||||
// Emit all DomainNameInfos with their API results.
|
||||
JSONArray threatMatches = responseBody.getJSONArray("matches");
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.beam.spec11;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.beam.BeamUtils.getQueryFromFile;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -30,6 +31,7 @@ import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.reporting.Spec11ThreatMatch;
|
||||
import google.registry.model.reporting.Spec11ThreatMatch.ThreatType;
|
||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.SqlTemplate;
|
||||
import google.registry.util.UtilsModule;
|
||||
@@ -41,6 +43,7 @@ import org.apache.beam.sdk.coders.SerializableCoder;
|
||||
import org.apache.beam.sdk.io.TextIO;
|
||||
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO;
|
||||
import org.apache.beam.sdk.options.PipelineOptionsFactory;
|
||||
import org.apache.beam.sdk.transforms.DoFn;
|
||||
import org.apache.beam.sdk.transforms.GroupByKey;
|
||||
import org.apache.beam.sdk.transforms.MapElements;
|
||||
import org.apache.beam.sdk.transforms.ParDo;
|
||||
@@ -113,15 +116,43 @@ public class Spec11Pipeline implements Serializable {
|
||||
}
|
||||
|
||||
static PCollection<DomainNameInfo> readFromCloudSql(Pipeline pipeline) {
|
||||
Read<Object[], DomainNameInfo> read =
|
||||
Read<Object[], KV<String, String>> read =
|
||||
RegistryJpaIO.read(
|
||||
"select d, r.emailAddress from Domain d join Registrar r on"
|
||||
+ " d.currentSponsorClientId = r.clientIdentifier where r.type = 'REAL'"
|
||||
+ " and d.deletionTime > now()",
|
||||
"select d.repoId, r.emailAddress from Domain d join Registrar r on"
|
||||
+ " d.currentSponsorClientId = r.clientIdentifier where r.type = 'REAL' and"
|
||||
+ " d.deletionTime > now()",
|
||||
false,
|
||||
Spec11Pipeline::parseRow);
|
||||
|
||||
return pipeline.apply("Read active domains from Cloud SQL", read);
|
||||
return pipeline
|
||||
.apply("Read active domains from Cloud SQL", read)
|
||||
.apply(
|
||||
"Build DomainNameInfo",
|
||||
ParDo.of(
|
||||
new DoFn<KV<String, String>, DomainNameInfo>() {
|
||||
@ProcessElement
|
||||
public void processElement(
|
||||
@Element KV<String, String> input, OutputReceiver<DomainNameInfo> output) {
|
||||
DomainBase domainBase =
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.loadByKey(
|
||||
VKey.createSql(DomainBase.class, input.getKey())));
|
||||
String emailAddress = input.getValue();
|
||||
if (emailAddress == null) {
|
||||
emailAddress = "";
|
||||
}
|
||||
DomainNameInfo domainNameInfo =
|
||||
DomainNameInfo.create(
|
||||
domainBase.getDomainName(),
|
||||
domainBase.getRepoId(),
|
||||
domainBase.getCurrentSponsorRegistrarId(),
|
||||
emailAddress);
|
||||
output.output(domainNameInfo);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
static PCollection<DomainNameInfo> readFromBigQuery(
|
||||
@@ -142,17 +173,8 @@ public class Spec11Pipeline implements Serializable {
|
||||
.withTemplateCompatibility());
|
||||
}
|
||||
|
||||
private static DomainNameInfo parseRow(Object[] row) {
|
||||
DomainBase domainBase = (DomainBase) row[0];
|
||||
String emailAddress = (String) row[1];
|
||||
if (emailAddress == null) {
|
||||
emailAddress = "";
|
||||
}
|
||||
return DomainNameInfo.create(
|
||||
domainBase.getDomainName(),
|
||||
domainBase.getRepoId(),
|
||||
domainBase.getCurrentSponsorRegistrarId(),
|
||||
emailAddress);
|
||||
private static KV<String, String> parseRow(Object[] row) {
|
||||
return KV.of((String) row[0], (String) row[1]);
|
||||
}
|
||||
|
||||
static void saveToSql(
|
||||
|
||||
@@ -632,7 +632,7 @@ public class BigqueryConnection implements AutoCloseable {
|
||||
private static String summarizeCompletedJob(Job job) {
|
||||
JobStatistics stats = job.getStatistics();
|
||||
return String.format(
|
||||
"Job took %,.3f seconds after a %,.3f second delay and processed %,d bytes (%s)",
|
||||
"Job took %,.3f seconds after a %,.3f second delay and processed %,d bytes (%s).",
|
||||
(stats.getEndTime() - stats.getStartTime()) / 1000.0,
|
||||
(stats.getStartTime() - stats.getCreationTime()) / 1000.0,
|
||||
stats.getTotalBytesProcessed(),
|
||||
@@ -706,17 +706,17 @@ public class BigqueryConnection implements AutoCloseable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper that creates a dataset with this name if it doesn't already exist, and returns true
|
||||
* if creation took place.
|
||||
* Helper that creates a dataset with this name if it doesn't already exist, and returns true if
|
||||
* creation took place.
|
||||
*/
|
||||
public boolean createDatasetIfNeeded(String datasetName) throws IOException {
|
||||
private boolean createDatasetIfNeeded(String datasetName) throws IOException {
|
||||
if (!checkDatasetExists(datasetName)) {
|
||||
bigquery.datasets()
|
||||
.insert(getProjectId(), new Dataset().setDatasetReference(new DatasetReference()
|
||||
.setProjectId(getProjectId())
|
||||
.setDatasetId(datasetName)))
|
||||
.execute();
|
||||
logger.atInfo().log("Created dataset: %s:%s\n", getProjectId(), datasetName);
|
||||
logger.atInfo().log("Created dataset: %s: %s.", getProjectId(), datasetName);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -732,9 +732,8 @@ public class BigqueryConnection implements AutoCloseable {
|
||||
.setDefaultDataset(getDataset())
|
||||
.setDestinationTable(table))));
|
||||
} catch (BigqueryJobFailureException e) {
|
||||
if (e.getReason().equals("duplicate")) {
|
||||
// Table already exists.
|
||||
} else {
|
||||
if (!e.getReason().equals("duplicate")) {
|
||||
// Throw if it failed for any reason other than table already existing.
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ public class CheckedBigquery {
|
||||
.setTableReference(table))
|
||||
.execute();
|
||||
logger.atInfo().log(
|
||||
"Created BigQuery table %s:%s.%s",
|
||||
"Created BigQuery table %s:%s.%s.",
|
||||
table.getProjectId(), table.getDatasetId(), table.getTableId());
|
||||
} catch (IOException e) {
|
||||
// Swallow errors about a table that exists, and throw any other ones.
|
||||
|
||||
@@ -22,10 +22,13 @@ import dagger.Provides;
|
||||
import google.registry.config.CredentialModule.DefaultCredential;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.util.CloudTasksUtils;
|
||||
import google.registry.util.CloudTasksUtils.GcpCloudTasksClient;
|
||||
import google.registry.util.CloudTasksUtils.SerializableCloudTasksClient;
|
||||
import google.registry.util.GoogleCredentialsBundle;
|
||||
import google.registry.util.Retrier;
|
||||
import java.io.IOException;
|
||||
import javax.inject.Provider;
|
||||
import java.io.Serializable;
|
||||
import java.util.function.Supplier;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/**
|
||||
@@ -42,24 +45,35 @@ public abstract class CloudTasksUtilsModule {
|
||||
public static CloudTasksUtils provideCloudTasksUtils(
|
||||
@Config("projectId") String projectId,
|
||||
@Config("locationId") String locationId,
|
||||
// Use a provider so that we can use try-with-resources with the client, which implements
|
||||
// Autocloseable.
|
||||
Provider<CloudTasksClient> clientProvider,
|
||||
SerializableCloudTasksClient client,
|
||||
Retrier retrier) {
|
||||
return new CloudTasksUtils(retrier, projectId, locationId, clientProvider);
|
||||
return new CloudTasksUtils(retrier, projectId, locationId, client);
|
||||
}
|
||||
|
||||
// Provides a supplier instead of using a Dagger @Provider because the latter is not serializable.
|
||||
@Provides
|
||||
public static Supplier<CloudTasksClient> provideCloudTasksClientSupplier(
|
||||
@DefaultCredential GoogleCredentialsBundle credentials) {
|
||||
return (Supplier<CloudTasksClient> & Serializable)
|
||||
() -> {
|
||||
CloudTasksClient client;
|
||||
try {
|
||||
client =
|
||||
CloudTasksClient.create(
|
||||
CloudTasksSettings.newBuilder()
|
||||
.setCredentialsProvider(
|
||||
FixedCredentialsProvider.create(credentials.getGoogleCredentials()))
|
||||
.build());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return client;
|
||||
};
|
||||
}
|
||||
|
||||
@Provides
|
||||
public static CloudTasksClient provideCloudTasksClient(
|
||||
@DefaultCredential GoogleCredentialsBundle credentials) {
|
||||
try {
|
||||
return CloudTasksClient.create(
|
||||
CloudTasksSettings.newBuilder()
|
||||
.setCredentialsProvider(
|
||||
FixedCredentialsProvider.create(credentials.getGoogleCredentials()))
|
||||
.build());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
public static SerializableCloudTasksClient provideSerializableCloudTasksClient(
|
||||
final Supplier<CloudTasksClient> clientSupplier) {
|
||||
return new GcpCloudTasksClient(clientSupplier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1306,6 +1306,18 @@ public final class RegistryConfig {
|
||||
public static ImmutableSet<String> provideAllowedEcdsaCurves(RegistryConfigSettings config) {
|
||||
return ImmutableSet.copyOf(config.sslCertificateValidation.allowedEcdsaCurves);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("minMonthsBeforeWipeOut")
|
||||
public static int provideMinMonthsBeforeWipeOut(RegistryConfigSettings config) {
|
||||
return config.contactHistory.minMonthsBeforeWipeOut;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("wipeOutQueryBatchSize")
|
||||
public static int provideWipeOutQueryBatchSize(RegistryConfigSettings config) {
|
||||
return config.contactHistory.wipeOutQueryBatchSize;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the App Engine project ID, which is based off the environment name. */
|
||||
|
||||
@@ -41,6 +41,7 @@ public class RegistryConfigSettings {
|
||||
public Keyring keyring;
|
||||
public RegistryTool registryTool;
|
||||
public SslCertificateValidation sslCertificateValidation;
|
||||
public ContactHistory contactHistory;
|
||||
|
||||
/** Configuration options that apply to the entire App Engine project. */
|
||||
public static class AppEngine {
|
||||
@@ -234,4 +235,10 @@ public class RegistryConfigSettings {
|
||||
public String expirationWarningEmailBodyText;
|
||||
public String expirationWarningEmailSubjectText;
|
||||
}
|
||||
|
||||
/** Configuration for contact history. */
|
||||
public static class ContactHistory {
|
||||
public int minMonthsBeforeWipeOut;
|
||||
public int wipeOutQueryBatchSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,6 +442,13 @@ registryTool:
|
||||
# OAuth client secret used by the tool.
|
||||
clientSecret: YOUR_CLIENT_SECRET
|
||||
|
||||
# Configuration options for handling contact history.
|
||||
contactHistory:
|
||||
# The number of months that a ContactHistory entity should be stored in the database.
|
||||
minMonthsBeforeWipeOut: 18
|
||||
# The batch size for querying ContactHistory table in the database.
|
||||
wipeOutQueryBatchSize: 500
|
||||
|
||||
# Configuration options for checking SSL certificates.
|
||||
sslCertificateValidation:
|
||||
# A map specifying the maximum amount of days the certificate can be valid.
|
||||
|
||||
@@ -14,18 +14,15 @@
|
||||
|
||||
package google.registry.cron;
|
||||
|
||||
import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
|
||||
|
||||
import com.google.appengine.api.taskqueue.Queue;
|
||||
import com.google.appengine.api.taskqueue.TaskOptions;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import google.registry.model.ofy.CommitLogBucket;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Action.Service;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.TaskQueueUtils;
|
||||
import java.time.Duration;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.util.CloudTasksUtils;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/** Action for fanning out cron tasks for each commit log bucket. */
|
||||
@@ -38,25 +35,27 @@ public final class CommitLogFanoutAction implements Runnable {
|
||||
|
||||
public static final String BUCKET_PARAM = "bucket";
|
||||
|
||||
private static final Random random = new Random();
|
||||
@Inject Clock clock;
|
||||
@Inject CloudTasksUtils cloudTasksUtils;
|
||||
|
||||
@Inject TaskQueueUtils taskQueueUtils;
|
||||
@Inject @Parameter("endpoint") String endpoint;
|
||||
@Inject @Parameter("queue") String queue;
|
||||
@Inject @Parameter("jitterSeconds") Optional<Integer> jitterSeconds;
|
||||
@Inject CommitLogFanoutAction() {}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Queue taskQueue = getQueue(queue);
|
||||
for (int bucketId : CommitLogBucket.getBucketIds()) {
|
||||
long delay =
|
||||
jitterSeconds.map(i -> random.nextInt((int) Duration.ofSeconds(i).toMillis())).orElse(0);
|
||||
TaskOptions taskOptions =
|
||||
TaskOptions.Builder.withUrl(endpoint)
|
||||
.param(BUCKET_PARAM, Integer.toString(bucketId))
|
||||
.countdownMillis(delay);
|
||||
taskQueueUtils.enqueue(taskQueue, taskOptions);
|
||||
cloudTasksUtils.enqueue(
|
||||
queue,
|
||||
CloudTasksUtils.createPostTask(
|
||||
endpoint,
|
||||
Service.BACKEND.toString(),
|
||||
ImmutableMultimap.of(BUCKET_PARAM, Integer.toString(bucketId)),
|
||||
clock,
|
||||
jitterSeconds));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ public final class TldFanoutAction implements Runnable {
|
||||
StringBuilder outputPayload =
|
||||
new StringBuilder(
|
||||
String.format("OK: Launched the following %d tasks in queue %s\n", tlds.size(), queue));
|
||||
logger.atInfo().log("Launching %d tasks in queue %s", tlds.size(), queue);
|
||||
logger.atInfo().log("Launching %d tasks in queue %s.", tlds.size(), queue);
|
||||
if (tlds.isEmpty()) {
|
||||
logger.atWarning().log("No TLDs to fan-out!");
|
||||
}
|
||||
@@ -153,7 +153,7 @@ public final class TldFanoutAction implements Runnable {
|
||||
"- Task: '%s', tld: '%s', endpoint: '%s'\n",
|
||||
createdTask.getName(), tld, createdTask.getAppEngineHttpRequest().getRelativeUri()));
|
||||
logger.atInfo().log(
|
||||
"Task: '%s', tld: '%s', endpoint: '%s'",
|
||||
"Task: '%s', tld: '%s', endpoint: '%s'.",
|
||||
createdTask.getName(), tld, createdTask.getAppEngineHttpRequest().getRelativeUri());
|
||||
}
|
||||
response.setContentType(PLAIN_TEXT_UTF_8);
|
||||
|
||||
@@ -107,7 +107,7 @@ public class DnsQueue {
|
||||
private TaskHandle addToQueue(
|
||||
TargetType targetType, String targetName, String tld, Duration countdown) {
|
||||
logger.atInfo().log(
|
||||
"Adding task type=%s, target=%s, tld=%s to pull queue %s (%d tasks currently on queue)",
|
||||
"Adding task type=%s, target=%s, tld=%s to pull queue %s (%d tasks currently on queue).",
|
||||
targetType, targetName, tld, DNS_PULL_QUEUE_NAME, queue.fetchStatistics().getNumTasks());
|
||||
return queue.add(
|
||||
TaskOptions.Builder.withDefaults()
|
||||
@@ -166,7 +166,7 @@ public class DnsQueue {
|
||||
"There are %d tasks in the DNS queue '%s'.", numTasks, DNS_PULL_QUEUE_NAME);
|
||||
return queue.leaseTasks(leaseDuration.getMillis(), MILLISECONDS, leaseTasksBatchSize);
|
||||
} catch (TransientFailureException | DeadlineExceededException e) {
|
||||
logger.atSevere().withCause(e).log("Failed leasing tasks too fast");
|
||||
logger.atSevere().withCause(e).log("Failed leasing tasks too fast.");
|
||||
return ImmutableList.of();
|
||||
}
|
||||
}
|
||||
@@ -176,7 +176,7 @@ public class DnsQueue {
|
||||
try {
|
||||
queue.deleteTask(tasks);
|
||||
} catch (TransientFailureException | DeadlineExceededException e) {
|
||||
logger.atSevere().withCause(e).log("Failed deleting tasks too fast");
|
||||
logger.atSevere().withCause(e).log("Failed deleting tasks too fast.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
|
||||
new Duration(enqueuedTime, now));
|
||||
logger.atInfo().log(
|
||||
"publishDnsWriter latency statistics: TLD: %s, dnsWriter: %s, actionStatus: %s, "
|
||||
+ "numItems: %d, timeSinceCreation: %s, timeInQueue: %s",
|
||||
+ "numItems: %d, timeSinceCreation: %s, timeInQueue: %s.",
|
||||
tld,
|
||||
dnsWriter,
|
||||
status,
|
||||
@@ -144,7 +144,7 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
|
||||
|
||||
/** Adds all the domains and hosts in the batch back to the queue to be processed later. */
|
||||
private void requeueBatch() {
|
||||
logger.atInfo().log("Requeueing batch for retry");
|
||||
logger.atInfo().log("Requeueing batch for retry.");
|
||||
for (String domain : nullToEmpty(domains)) {
|
||||
dnsQueue.addDomainRefreshTask(domain);
|
||||
}
|
||||
@@ -158,14 +158,14 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
|
||||
// LockIndex should always be within [1, numPublishLocks]
|
||||
if (lockIndex > numPublishLocks || lockIndex <= 0) {
|
||||
logger.atSevere().log(
|
||||
"Lock index should be within [1,%d], got %d instead", numPublishLocks, lockIndex);
|
||||
"Lock index should be within [1,%d], got %d instead.", numPublishLocks, lockIndex);
|
||||
return false;
|
||||
}
|
||||
// Check if the Registry object's num locks has changed since this task was batched
|
||||
int registryNumPublishLocks = Registry.get(tld).getNumDnsPublishLocks();
|
||||
if (registryNumPublishLocks != numPublishLocks) {
|
||||
logger.atWarning().log(
|
||||
"Registry numDnsPublishLocks %d out of sync with parameter %d",
|
||||
"Registry numDnsPublishLocks %d out of sync with parameter %d.",
|
||||
registryNumPublishLocks, numPublishLocks);
|
||||
return false;
|
||||
}
|
||||
@@ -179,7 +179,7 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
|
||||
DnsWriter writer = dnsWriterProxy.getByClassNameForTld(dnsWriter, tld);
|
||||
|
||||
if (writer == null) {
|
||||
logger.atWarning().log("Couldn't get writer %s for TLD %s", dnsWriter, tld);
|
||||
logger.atWarning().log("Couldn't get writer %s for TLD %s.", dnsWriter, tld);
|
||||
recordActionResult(ActionStatus.BAD_WRITER);
|
||||
requeueBatch();
|
||||
return;
|
||||
@@ -190,11 +190,11 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
|
||||
for (String domain : nullToEmpty(domains)) {
|
||||
if (!DomainNameUtils.isUnder(
|
||||
InternetDomainName.from(domain), InternetDomainName.from(tld))) {
|
||||
logger.atSevere().log("%s: skipping domain %s not under tld", tld, domain);
|
||||
logger.atSevere().log("%s: skipping domain %s not under TLD.", tld, domain);
|
||||
domainsRejected += 1;
|
||||
} else {
|
||||
writer.publishDomain(domain);
|
||||
logger.atInfo().log("%s: published domain %s", tld, domain);
|
||||
logger.atInfo().log("%s: published domain %s.", tld, domain);
|
||||
domainsPublished += 1;
|
||||
}
|
||||
}
|
||||
@@ -206,11 +206,11 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
|
||||
for (String host : nullToEmpty(hosts)) {
|
||||
if (!DomainNameUtils.isUnder(
|
||||
InternetDomainName.from(host), InternetDomainName.from(tld))) {
|
||||
logger.atSevere().log("%s: skipping host %s not under tld", tld, host);
|
||||
logger.atSevere().log("%s: skipping host %s not under TLD.", tld, host);
|
||||
hostsRejected += 1;
|
||||
} else {
|
||||
writer.publishHost(host);
|
||||
logger.atInfo().log("%s: published host %s", tld, host);
|
||||
logger.atInfo().log("%s: published host %s.", tld, host);
|
||||
hostsPublished += 1;
|
||||
}
|
||||
}
|
||||
@@ -233,7 +233,7 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
|
||||
tld, dnsWriter, commitStatus, duration, domainsPublished, hostsPublished);
|
||||
logger.atInfo().log(
|
||||
"writer.commit() statistics: TLD: %s, dnsWriter: %s, commitStatus: %s, duration: %s, "
|
||||
+ "domainsPublished: %d, domainsRejected: %d, hostsPublished: %d, hostsRejected: %d",
|
||||
+ "domainsPublished: %d, domainsRejected: %d, hostsPublished: %d, hostsRejected: %d.",
|
||||
tld,
|
||||
dnsWriter,
|
||||
commitStatus,
|
||||
|
||||
@@ -180,11 +180,11 @@ public class CloudDnsWriter extends BaseDnsWriter {
|
||||
|
||||
desiredRecords.put(absoluteDomainName, domainRecords.build());
|
||||
logger.atFine().log(
|
||||
"Will write %d records for domain %s", domainRecords.build().size(), absoluteDomainName);
|
||||
"Will write %d records for domain '%s'.", domainRecords.build().size(), absoluteDomainName);
|
||||
}
|
||||
|
||||
private void publishSubordinateHost(String hostName) {
|
||||
logger.atInfo().log("Publishing glue records for %s", hostName);
|
||||
logger.atInfo().log("Publishing glue records for host '%s'.", hostName);
|
||||
// Canonicalize name
|
||||
String absoluteHostName = getAbsoluteHostName(hostName);
|
||||
|
||||
@@ -250,7 +250,7 @@ public class CloudDnsWriter extends BaseDnsWriter {
|
||||
|
||||
// Host not managed by our registry, no need to update DNS.
|
||||
if (!tld.isPresent()) {
|
||||
logger.atSevere().log("publishHost called for invalid host %s", hostName);
|
||||
logger.atSevere().log("publishHost called for invalid host '%s'.", hostName);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ public class CloudDnsWriter extends BaseDnsWriter {
|
||||
ImmutableMap<String, ImmutableSet<ResourceRecordSet>> desiredRecordsCopy =
|
||||
ImmutableMap.copyOf(desiredRecords);
|
||||
retrier.callWithRetry(() -> mutateZone(desiredRecordsCopy), ZoneStateException.class);
|
||||
logger.atInfo().log("Wrote to Cloud DNS");
|
||||
logger.atInfo().log("Wrote to Cloud DNS.");
|
||||
}
|
||||
|
||||
/** Returns the glue records for in-bailiwick nameservers for the given domain+records. */
|
||||
@@ -329,7 +329,7 @@ public class CloudDnsWriter extends BaseDnsWriter {
|
||||
*/
|
||||
private Map<String, List<ResourceRecordSet>> getResourceRecordsForDomains(
|
||||
Set<String> domainNames) {
|
||||
logger.atFine().log("Fetching records for %s", domainNames);
|
||||
logger.atFine().log("Fetching records for domain '%s'.", domainNames);
|
||||
// As per Concurrent.transform() - if numThreads or domainNames.size() < 2, it will not use
|
||||
// threading.
|
||||
return ImmutableMap.copyOf(
|
||||
@@ -381,11 +381,11 @@ public class CloudDnsWriter extends BaseDnsWriter {
|
||||
ImmutableSet<ResourceRecordSet> intersection =
|
||||
Sets.intersection(additions, deletions).immutableCopy();
|
||||
logger.atInfo().log(
|
||||
"There are %d common items out of the %d items in 'additions' and %d items in 'deletions'",
|
||||
"There are %d common items out of the %d items in 'additions' and %d items in 'deletions'.",
|
||||
intersection.size(), additions.size(), deletions.size());
|
||||
// Exit early if we have nothing to update - dnsConnection doesn't work on empty changes
|
||||
if (additions.equals(deletions)) {
|
||||
logger.atInfo().log("Returning early because additions is the same as deletions");
|
||||
logger.atInfo().log("Returning early because additions are the same as deletions.");
|
||||
return;
|
||||
}
|
||||
Change change =
|
||||
|
||||
@@ -157,6 +157,12 @@
|
||||
<url-pattern>/_dr/cron/readDnsQueue</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<!-- Replicates SQL transactions to Datastore during the Registry 3.0 migration. -->
|
||||
<servlet-mapping>
|
||||
<servlet-name>backend-servlet</servlet-name>
|
||||
<url-pattern>/_dr/cron/replicateToDatastore</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<!-- Publishes DNS updates. -->
|
||||
<servlet-mapping>
|
||||
<servlet-name>backend-servlet</servlet-name>
|
||||
@@ -391,6 +397,13 @@
|
||||
<url-pattern>/_dr/task/relockDomain</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<!-- Background action to wipe out PII fields of ContactHistory entities that
|
||||
have been in the database for a certain period of time. -->
|
||||
<servlet-mapping>
|
||||
<servlet-name>backend-servlet</servlet-name>
|
||||
<url-pattern>/_dr/task/wipeOutContactHistoryPii</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<!-- Action to wipeout Cloud SQL data -->
|
||||
<servlet-mapping>
|
||||
<servlet-name>backend-servlet</servlet-name>
|
||||
|
||||
@@ -348,4 +348,14 @@
|
||||
<schedule>every 3 minutes</schedule>
|
||||
<target>backend</target>
|
||||
</cron>
|
||||
|
||||
<cron>
|
||||
<url><![CDATA[/_dr/task/wipeOutContactHistoryPii]]></url>
|
||||
<description>
|
||||
This job runs weekly to wipe out PII fields of ContactHistory entities
|
||||
that have been in the database for a certain period of time.
|
||||
</description>
|
||||
<schedule>every monday 15:00</schedule>
|
||||
<target>backend</target>
|
||||
</cron>
|
||||
</cronentries>
|
||||
|
||||
@@ -246,4 +246,14 @@
|
||||
<schedule>every 3 minutes</schedule>
|
||||
<target>backend</target>
|
||||
</cron>
|
||||
|
||||
<cron>
|
||||
<url><![CDATA[/_dr/task/wipeOutContactHistoryPii]]></url>
|
||||
<description>
|
||||
This job runs weekly to wipe out PII fields of ContactHistory entities
|
||||
that have been in the database for a certain period of time.
|
||||
</description>
|
||||
<schedule>every monday 15:00</schedule>
|
||||
<target>backend</target>
|
||||
</cron>
|
||||
</cronentries>
|
||||
|
||||
@@ -80,7 +80,7 @@ public class BackupDatastoreAction implements Runnable {
|
||||
logger.atInfo().log(message);
|
||||
response.setPayload(message);
|
||||
} catch (Throwable e) {
|
||||
throw new InternalServerErrorException("Exception occurred while backing up datastore.", e);
|
||||
throw new InternalServerErrorException("Exception occurred while backing up Datastore", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,10 +118,10 @@ public class BigqueryPollJobAction implements Runnable {
|
||||
|
||||
// Check if the job ended with an error.
|
||||
if (job.getStatus().getErrorResult() != null) {
|
||||
logger.atSevere().log("Bigquery job failed - %s - %s", jobRefString, job);
|
||||
logger.atSevere().log("Bigquery job failed - %s - %s.", jobRefString, job);
|
||||
return false;
|
||||
}
|
||||
logger.atInfo().log("Bigquery job succeeded - %s", jobRefString);
|
||||
logger.atInfo().log("Bigquery job succeeded - %s.", jobRefString);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -171,10 +171,10 @@ public class CheckBackupAction implements Runnable {
|
||||
ImmutableSet.copyOf(intersection(backup.getKinds(), kindsToLoad));
|
||||
String message = String.format("Datastore backup %s complete - ", backupName);
|
||||
if (exportedKindsToLoad.isEmpty()) {
|
||||
message += "no kinds to load into BigQuery";
|
||||
message += "no kinds to load into BigQuery.";
|
||||
} else {
|
||||
enqueueUploadBackupTask(backupId, backup.getExportFolderUrl(), exportedKindsToLoad);
|
||||
message += "BigQuery load task enqueued";
|
||||
message += "BigQuery load task enqueued.";
|
||||
}
|
||||
logger.atInfo().log(message);
|
||||
response.setPayload(message);
|
||||
|
||||
@@ -85,7 +85,7 @@ public class ExportDomainListsAction implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
ImmutableSet<String> realTlds = getTldsOfType(TldType.REAL);
|
||||
logger.atInfo().log("Exporting domain lists for tlds %s", realTlds);
|
||||
logger.atInfo().log("Exporting domain lists for TLDs %s.", realTlds);
|
||||
if (tm().isOfy()) {
|
||||
mrRunner
|
||||
.setJobName("Export domain lists")
|
||||
@@ -145,7 +145,7 @@ public class ExportDomainListsAction implements Runnable {
|
||||
Registry registry = Registry.get(tld);
|
||||
if (registry.getDriveFolderId() == null) {
|
||||
logger.atInfo().log(
|
||||
"Skipping registered domains export for TLD %s because Drive folder isn't specified",
|
||||
"Skipping registered domains export for TLD %s because Drive folder isn't specified.",
|
||||
tld);
|
||||
} else {
|
||||
String resultMsg =
|
||||
|
||||
@@ -110,11 +110,11 @@ public class ExportPremiumTermsAction implements Runnable {
|
||||
private Optional<String> checkConfig(Registry registry) {
|
||||
if (isNullOrEmpty(registry.getDriveFolderId())) {
|
||||
logger.atInfo().log(
|
||||
"Skipping premium terms export for TLD %s because Drive folder isn't specified", tld);
|
||||
"Skipping premium terms export for TLD %s because Drive folder isn't specified.", tld);
|
||||
return Optional.of("Skipping export because no Drive folder is associated with this TLD");
|
||||
}
|
||||
if (!registry.getPremiumListName().isPresent()) {
|
||||
logger.atInfo().log("No premium terms to export for TLD %s", tld);
|
||||
logger.atInfo().log("No premium terms to export for TLD '%s'.", tld);
|
||||
return Optional.of("No premium lists configured");
|
||||
}
|
||||
return Optional.empty();
|
||||
|
||||
@@ -65,11 +65,11 @@ public class ExportReservedTermsAction implements Runnable {
|
||||
String resultMsg;
|
||||
if (registry.getReservedListNames().isEmpty() && isNullOrEmpty(registry.getDriveFolderId())) {
|
||||
resultMsg = "No reserved lists configured";
|
||||
logger.atInfo().log("No reserved terms to export for TLD %s", tld);
|
||||
logger.atInfo().log("No reserved terms to export for TLD '%s'.", tld);
|
||||
} else if (registry.getDriveFolderId() == null) {
|
||||
resultMsg = "Skipping export because no Drive folder is associated with this TLD";
|
||||
logger.atInfo().log(
|
||||
"Skipping reserved terms export for TLD %s because Drive folder isn't specified", tld);
|
||||
"Skipping reserved terms export for TLD %s because Drive folder isn't specified.", tld);
|
||||
} else {
|
||||
resultMsg = driveConnection.createOrUpdateFile(
|
||||
RESERVED_TERMS_FILENAME,
|
||||
|
||||
@@ -194,7 +194,7 @@ public final class SyncGroupMembersAction implements Runnable {
|
||||
}
|
||||
}
|
||||
logger.atInfo().log(
|
||||
"Successfully synced contacts for registrar %s: added %d and removed %d",
|
||||
"Successfully synced contacts for registrar %s: added %d and removed %d.",
|
||||
registrar.getRegistrarId(), totalAdded, totalRemoved);
|
||||
} catch (IOException e) {
|
||||
// Package up exception and re-throw with attached additional relevant info.
|
||||
|
||||
@@ -150,8 +150,7 @@ public class UpdateSnapshotViewAction implements Runnable {
|
||||
if (e.getDetails() != null && e.getDetails().getCode() == 404) {
|
||||
bigquery.tables().insert(ref.getProjectId(), ref.getDatasetId(), table).execute();
|
||||
} else {
|
||||
logger.atWarning().withCause(e).log(
|
||||
"UpdateSnapshotViewAction failed, caught exception %s", e.getDetails());
|
||||
logger.atWarning().withCause(e).log("UpdateSnapshotViewAction errored out.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ public class UploadDatastoreBackupAction implements Runnable {
|
||||
String message = uploadBackup(backupId, backupFolderUrl, Splitter.on(',').split(backupKinds));
|
||||
logger.atInfo().log("Loaded backup successfully: %s", message);
|
||||
} catch (Throwable e) {
|
||||
logger.atSevere().withCause(e).log("Error loading backup");
|
||||
logger.atSevere().withCause(e).log("Error loading backup.");
|
||||
if (e instanceof IllegalArgumentException) {
|
||||
throw new BadRequestException("Error calling load backup: " + e.getMessage(), e);
|
||||
} else {
|
||||
@@ -148,12 +148,12 @@ public class UploadDatastoreBackupAction implements Runnable {
|
||||
getQueue(UpdateSnapshotViewAction.QUEUE));
|
||||
|
||||
builder.append(String.format(" - %s:%s\n", projectId, jobId));
|
||||
logger.atInfo().log("Submitted load job %s:%s", projectId, jobId);
|
||||
logger.atInfo().log("Submitted load job %s:%s.", projectId, jobId);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
static String sanitizeForBigquery(String backupId) {
|
||||
private static String sanitizeForBigquery(String backupId) {
|
||||
return backupId.replaceAll("[^a-zA-Z0-9_]", "_");
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ class SheetSynchronizer {
|
||||
BatchUpdateValuesResponse response =
|
||||
sheetsService.spreadsheets().values().batchUpdate(spreadsheetId, updateRequest).execute();
|
||||
Integer cellsUpdated = response.getTotalUpdatedCells();
|
||||
logger.atInfo().log("Updated %d originalVals", cellsUpdated != null ? cellsUpdated : 0);
|
||||
logger.atInfo().log("Updated %d originalVals.", cellsUpdated != null ? cellsUpdated : 0);
|
||||
}
|
||||
|
||||
// Append extra rows if necessary
|
||||
@@ -140,7 +140,7 @@ class SheetSynchronizer {
|
||||
.setInsertDataOption("INSERT_ROWS")
|
||||
.execute();
|
||||
logger.atInfo().log(
|
||||
"Appended %d rows to range %s",
|
||||
"Appended %d rows to range %s.",
|
||||
data.size() - originalVals.size(), appendResponse.getTableRange());
|
||||
// Clear the extra rows if necessary
|
||||
} else if (data.size() < originalVals.size()) {
|
||||
@@ -155,7 +155,7 @@ class SheetSynchronizer {
|
||||
new ClearValuesRequest())
|
||||
.execute();
|
||||
logger.atInfo().log(
|
||||
"Cleared %d rows from range %s",
|
||||
"Cleared %d rows from range %s.",
|
||||
originalVals.size() - data.size(), clearResponse.getClearedRange());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ public class CheckApiAction implements Runnable {
|
||||
return fail(e.getResult().getMsg());
|
||||
} catch (Exception e) {
|
||||
metricBuilder.status(UNKNOWN_ERROR);
|
||||
logger.atWarning().withCause(e).log("Unknown error");
|
||||
logger.atWarning().withCause(e).log("Unknown error.");
|
||||
return fail("Invalid request");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,12 +130,12 @@ public final class EppController {
|
||||
} catch (EppException | EppExceptionInProviderException e) {
|
||||
// The command failed. Send the client an error message, but only log at INFO since many of
|
||||
// these failures are innocuous or due to client error, so there's nothing we have to change.
|
||||
logger.atInfo().withCause(e).log("Flow returned failure response");
|
||||
logger.atInfo().withCause(e).log("Flow returned failure response.");
|
||||
EppException eppEx = (EppException) (e instanceof EppException ? e : e.getCause());
|
||||
return getErrorResponse(eppEx.getResult(), flowComponent.trid());
|
||||
} catch (Throwable e) {
|
||||
// Something bad and unexpected happened. Send the client a generic error, and log at SEVERE.
|
||||
logger.atSevere().withCause(e).log("Unexpected failure in flow execution");
|
||||
logger.atSevere().withCause(e).log("Unexpected failure in flow execution.");
|
||||
return getErrorResponse(Result.create(Code.COMMAND_FAILED), flowComponent.trid());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ public class EppRequestHandler {
|
||||
response.setHeader(ProxyHttpHeaders.LOGGED_IN, "true");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.atWarning().withCause(e).log("handleEppCommand general exception");
|
||||
logger.atWarning().withCause(e).log("handleEppCommand general exception.");
|
||||
response.setStatus(SC_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ public final class ExtensionManager {
|
||||
throw new UndeclaredServiceExtensionException(undeclaredUrisThatError);
|
||||
}
|
||||
logger.atInfo().log(
|
||||
"Client %s is attempting to run %s without declaring URIs %s on login",
|
||||
"Client %s is attempting to run %s without declaring URIs %s on login.",
|
||||
registrarId, flowClass.getSimpleName(), undeclaredUris);
|
||||
}
|
||||
|
||||
|
||||
@@ -371,6 +371,10 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||
String.format(
|
||||
"Domain %s was deleted by registry administrator with final deletion effective: %s",
|
||||
existingDomain.getDomainName(), deletionTime))
|
||||
.setResponseData(
|
||||
ImmutableList.of(
|
||||
DomainPendingActionNotificationResponse.create(
|
||||
existingDomain.getDomainName(), true, trid, now)))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -229,6 +229,15 @@ public final class DomainRenewFlow implements TransactionalFlow {
|
||||
|
||||
private DomainHistory buildDomainHistory(
|
||||
DomainBase newDomain, DateTime now, Period period, Duration renewGracePeriod) {
|
||||
Optional<MetadataExtension> metadataExtensionOpt =
|
||||
eppInput.getSingleExtension(MetadataExtension.class);
|
||||
if (metadataExtensionOpt.isPresent()) {
|
||||
MetadataExtension metadataExtension = metadataExtensionOpt.get();
|
||||
if (metadataExtension.getReason() != null) {
|
||||
historyBuilder.setReason(metadataExtension.getReason());
|
||||
}
|
||||
historyBuilder.setRequestedByRegistrar(metadataExtension.getRequestedByRegistrar());
|
||||
}
|
||||
return historyBuilder
|
||||
.setType(DOMAIN_RENEW)
|
||||
.setPeriod(period)
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.flows.domain;
|
||||
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
|
||||
import static com.google.common.collect.Sets.symmetricDifference;
|
||||
import static com.google.common.collect.Sets.union;
|
||||
import static google.registry.flows.FlowUtils.persistEntityChanges;
|
||||
@@ -43,6 +44,9 @@ import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_UPDATE;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.net.InternetDomainName;
|
||||
import google.registry.dns.DnsQueue;
|
||||
import google.registry.flows.EppException;
|
||||
@@ -76,6 +80,7 @@ import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.eppinput.EppInput;
|
||||
import google.registry.model.eppinput.ResourceCommand;
|
||||
import google.registry.model.eppoutput.EppResponse;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
|
||||
import google.registry.model.tld.Registry;
|
||||
import java.util.Optional;
|
||||
@@ -175,6 +180,9 @@ public final class DomainUpdateFlow implements TransactionalFlow {
|
||||
Optional<BillingEvent.OneTime> statusUpdateBillingEvent =
|
||||
createBillingEventForStatusUpdates(existingDomain, newDomain, domainHistory, now);
|
||||
statusUpdateBillingEvent.ifPresent(entitiesToSave::add);
|
||||
Optional<PollMessage.OneTime> serverStatusUpdatePollMessage =
|
||||
createPollMessageForServerStatusUpdates(existingDomain, newDomain, domainHistory, now);
|
||||
serverStatusUpdatePollMessage.ifPresent(entitiesToSave::add);
|
||||
EntityChanges entityChanges =
|
||||
flowCustomLogic.beforeSave(
|
||||
BeforeSaveParameters.newBuilder()
|
||||
@@ -306,4 +314,50 @@ public final class DomainUpdateFlow implements TransactionalFlow {
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/** Enqueues a poll message iff a superuser is adding/removing server statuses. */
|
||||
private Optional<PollMessage.OneTime> createPollMessageForServerStatusUpdates(
|
||||
DomainBase existingDomain, DomainBase newDomain, DomainHistory historyEntry, DateTime now) {
|
||||
if (registrarId.equals(existingDomain.getPersistedCurrentSponsorRegistrarId())) {
|
||||
// Don't send a poll message when a superuser registrar is updating its own domain.
|
||||
return Optional.empty();
|
||||
}
|
||||
ImmutableSortedSet<String> addedServerStatuses =
|
||||
Sets.difference(newDomain.getStatusValues(), existingDomain.getStatusValues()).stream()
|
||||
.filter(StatusValue::isServerSettable)
|
||||
.map(StatusValue::getXmlName)
|
||||
.collect(toImmutableSortedSet(Ordering.natural()));
|
||||
ImmutableSortedSet<String> removedServerStatuses =
|
||||
Sets.difference(existingDomain.getStatusValues(), newDomain.getStatusValues()).stream()
|
||||
.filter(StatusValue::isServerSettable)
|
||||
.map(StatusValue::getXmlName)
|
||||
.collect(toImmutableSortedSet(Ordering.natural()));
|
||||
|
||||
String msg = "";
|
||||
if (addedServerStatuses.size() > 0 && removedServerStatuses.size() > 0) {
|
||||
msg =
|
||||
String.format(
|
||||
"The registry administrator has added the status(es) %s and removed the status(es)"
|
||||
+ " %s.",
|
||||
addedServerStatuses, removedServerStatuses);
|
||||
} else if (addedServerStatuses.size() > 0) {
|
||||
msg =
|
||||
String.format(
|
||||
"The registry administrator has added the status(es) %s.", addedServerStatuses);
|
||||
} else if (removedServerStatuses.size() > 0) {
|
||||
msg =
|
||||
String.format(
|
||||
"The registry administrator has removed the status(es) %s.", removedServerStatuses);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.ofNullable(
|
||||
new PollMessage.OneTime.Builder()
|
||||
.setParent(historyEntry)
|
||||
.setEventTime(now)
|
||||
.setRegistrarId(existingDomain.getCurrentSponsorRegistrarId())
|
||||
.setMsg(msg)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ public class LoginFlow implements Flow {
|
||||
}
|
||||
|
||||
/** Run the flow without bothering to log errors. The {@link #run} method will do that for us. */
|
||||
public final EppResponse runWithoutLogging() throws EppException {
|
||||
private final EppResponse runWithoutLogging() throws EppException {
|
||||
extensionManager.validate(); // There are no legal extensions for this flow.
|
||||
Login login = (Login) eppInput.getCommandWrapper().getCommand();
|
||||
if (!registrarId.isEmpty()) {
|
||||
|
||||
@@ -141,7 +141,7 @@ public class GcsUtils implements Serializable {
|
||||
Blob blob = storage().get(blobId);
|
||||
return blob != null && blob.getSize() > 0;
|
||||
} catch (StorageException e) {
|
||||
logger.atWarning().withCause(e).log("Failed to check if GCS file exists");
|
||||
logger.atWarning().withCause(e).log("Failure while checking if GCS file exists.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ public class LoadTestAction implements Runnable {
|
||||
}
|
||||
ImmutableList<TaskOptions> taskOptions = tasks.build();
|
||||
enqueue(taskOptions);
|
||||
logger.atInfo().log("Added %d total load test tasks", taskOptions.size());
|
||||
logger.atInfo().log("Added %d total load test tasks.", taskOptions.size());
|
||||
}
|
||||
|
||||
private void validateAndLogRequest() {
|
||||
|
||||
@@ -58,7 +58,7 @@ public class UnlockerOutput<O> extends Output<O, Lock> {
|
||||
|
||||
@Override
|
||||
public Lock finish(Collection<? extends OutputWriter<O>> writers) {
|
||||
logger.atInfo().log("Mapreduce finished; releasing lock: %s", lock);
|
||||
logger.atInfo().log("Mapreduce finished; releasing lock '%s'.", lock);
|
||||
lock.release();
|
||||
return lock;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class EppResourceEntityReader<R extends EppResource> extends EppResourceBaseRead
|
||||
Key<? extends EppResource> key = nextQueryResult().getKey();
|
||||
EppResource resource = auditedOfy().load().key(key).now();
|
||||
if (resource == null) {
|
||||
logger.atSevere().log("EppResourceIndex key %s points at a missing resource", key);
|
||||
logger.atSevere().log("EppResourceIndex key %s points at a missing resource.", key);
|
||||
continue;
|
||||
}
|
||||
// Postfilter to distinguish polymorphic types (e.g. EppResources).
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.backup;
|
||||
package google.registry.model;
|
||||
|
||||
import com.google.apphosting.api.ApiProxy;
|
||||
import com.google.apphosting.api.ApiProxy.Environment;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.io.Closeable;
|
||||
import google.registry.model.ofy.ObjectifyService;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
@@ -38,26 +39,61 @@ import java.lang.reflect.Proxy;
|
||||
* <p>Note that conversion from Objectify objects to Datastore {@code Entities} still requires the
|
||||
* Datastore service.
|
||||
*/
|
||||
public class AppEngineEnvironment implements Closeable {
|
||||
public class AppEngineEnvironment {
|
||||
|
||||
private boolean isPlaceHolderNeeded;
|
||||
private Environment environment;
|
||||
|
||||
/**
|
||||
* Constructor for use by tests.
|
||||
*
|
||||
* <p>All test suites must use the same appId for environments, since when tearing down we do not
|
||||
* clear cached environments in spawned threads. See {@link #unsetEnvironmentForAllThreads} for
|
||||
* more information.
|
||||
*/
|
||||
public AppEngineEnvironment() {
|
||||
this("PlaceholderAppId");
|
||||
/**
|
||||
* Use AppEngineExtension's appId here so that ofy and sql entities can be compared with {@code
|
||||
* Objects#equals()}. The choice of this value does not impact functional correctness.
|
||||
*/
|
||||
this("test");
|
||||
}
|
||||
|
||||
/** Constructor for use by applications, e.g., BEAM pipelines. */
|
||||
public AppEngineEnvironment(String appId) {
|
||||
isPlaceHolderNeeded = ApiProxy.getCurrentEnvironment() == null;
|
||||
// isPlaceHolderNeeded may be true when we are invoked in a test with AppEngineRule.
|
||||
if (isPlaceHolderNeeded) {
|
||||
ApiProxy.setEnvironmentForCurrentThread(createAppEngineEnvironment(appId));
|
||||
}
|
||||
environment = createAppEngineEnvironment(appId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (isPlaceHolderNeeded) {
|
||||
ApiProxy.setEnvironmentForCurrentThread(null);
|
||||
public void setEnvironmentForCurrentThread() {
|
||||
ApiProxy.setEnvironmentForCurrentThread(environment);
|
||||
ObjectifyService.initOfy();
|
||||
}
|
||||
|
||||
public void setEnvironmentForAllThreads() {
|
||||
setEnvironmentForCurrentThread();
|
||||
ApiProxy.setEnvironmentFactory(() -> environment);
|
||||
}
|
||||
|
||||
public void unsetEnvironmentForCurrentThread() {
|
||||
ApiProxy.clearEnvironmentForCurrentThread();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsets the test environment in all threads with best effort.
|
||||
*
|
||||
* <p>This method unsets the environment factory and clears the cached environment in the current
|
||||
* thread (the main test runner thread). We do not clear the cache in spawned threads, even though
|
||||
* they may be reused. This is not a problem as long as the appId stays the same: those threads
|
||||
* are used only in AppEngine or BEAM tests, and expect the presence of an environment.
|
||||
*/
|
||||
public void unsetEnvironmentForAllThreads() {
|
||||
unsetEnvironmentForCurrentThread();
|
||||
|
||||
try {
|
||||
Method method = ApiProxy.class.getDeclaredMethod("clearEnvironmentFactory");
|
||||
method.setAccessible(true);
|
||||
method.invoke(null);
|
||||
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package google.registry.model;
|
||||
|
||||
import google.registry.util.PreconditionsUtils;
|
||||
import javax.persistence.Access;
|
||||
import javax.persistence.AccessType;
|
||||
import javax.persistence.AttributeOverride;
|
||||
@@ -30,7 +31,7 @@ import javax.xml.bind.annotation.XmlTransient;
|
||||
* that we can enforce strictly increasing timestamps.
|
||||
*/
|
||||
@MappedSuperclass
|
||||
public abstract class BackupGroupRoot extends ImmutableObject {
|
||||
public abstract class BackupGroupRoot extends ImmutableObject implements UnsafeSerializable {
|
||||
|
||||
/**
|
||||
* An automatically managed timestamp of when this object was last written to Datastore.
|
||||
@@ -49,4 +50,14 @@ public abstract class BackupGroupRoot extends ImmutableObject {
|
||||
public UpdateAutoTimestamp getUpdateTimestamp() {
|
||||
return updateTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies {@link #updateTimestamp} from another entity.
|
||||
*
|
||||
* <p>This method is for the few cases when {@code updateTimestamp} is copied between different
|
||||
* types of entities. Use {@link #clone} for same-type copying.
|
||||
*/
|
||||
protected void copyUpdateTimestamp(BackupGroupRoot other) {
|
||||
this.updateTimestamp = PreconditionsUtils.checkArgumentNotNull(other, "other").updateTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import org.joda.time.DateTime;
|
||||
*
|
||||
* @see CreateAutoTimestampTranslatorFactory
|
||||
*/
|
||||
public class CreateAutoTimestamp extends ImmutableObject {
|
||||
public class CreateAutoTimestamp extends ImmutableObject implements UnsafeSerializable {
|
||||
|
||||
DateTime timestamp;
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.model.server.Lock;
|
||||
import google.registry.model.server.ServerSecret;
|
||||
import google.registry.model.tld.Registry;
|
||||
import google.registry.model.tmch.TmchCrl;
|
||||
|
||||
/** Sets of classes of the Objectify-registered entities in use throughout the model. */
|
||||
public final class EntityClasses {
|
||||
@@ -85,8 +84,7 @@ public final class EntityClasses {
|
||||
Registrar.class,
|
||||
RegistrarContact.class,
|
||||
Registry.class,
|
||||
ServerSecret.class,
|
||||
TmchCrl.class);
|
||||
ServerSecret.class);
|
||||
|
||||
private EntityClasses() {}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,14 @@ import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import com.google.appengine.api.datastore.DatastoreServiceFactory;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import google.registry.beam.common.RegistryPipelineWorkerInitializer;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* Allocates a globally unique {@link Long} number to use as an Ofy {@code @Id}.
|
||||
*
|
||||
* <p>In non-test environments the Id is generated by Datastore, whereas in tests it's from an
|
||||
* <p>In non-test, non-beam environments the Id is generated by Datastore, otherwise it's from an
|
||||
* atomic long number that's incremented every time this method is called.
|
||||
*/
|
||||
public final class IdService {
|
||||
@@ -35,13 +36,25 @@ public final class IdService {
|
||||
*/
|
||||
private static final String APP_WIDE_ALLOCATION_KIND = "common";
|
||||
|
||||
/** Counts of used ids for use in unit tests. Outside tests this is never used. */
|
||||
private static final AtomicLong nextTestId = new AtomicLong(1); // ids cannot be zero
|
||||
/**
|
||||
* Counts of used ids for use in unit tests or Beam.
|
||||
*
|
||||
* <p>Note that one should only use self-allocate Ids in Beam for entities whose Ids are not
|
||||
* important and are not persisted back to the database, i. e. nowhere the uniqueness of the ID is
|
||||
* required.
|
||||
*/
|
||||
private static final AtomicLong nextSelfAllocatedId = new AtomicLong(1); // ids cannot be zero
|
||||
|
||||
private static final boolean isSelfAllocated() {
|
||||
return RegistryEnvironment.UNITTEST.equals(RegistryEnvironment.get())
|
||||
|| "true".equals(System.getProperty(RegistryPipelineWorkerInitializer.PROPERTY, "false"));
|
||||
}
|
||||
|
||||
/** Allocates an id. */
|
||||
// TODO(b/201547855): Find a way to allocate a unique ID without datastore.
|
||||
public static long allocateId() {
|
||||
return RegistryEnvironment.UNITTEST.equals(RegistryEnvironment.get())
|
||||
? nextTestId.getAndIncrement()
|
||||
return isSelfAllocated()
|
||||
? nextSelfAllocatedId.getAndIncrement()
|
||||
: DatastoreServiceFactory.getDatastoreService()
|
||||
.allocateIds(APP_WIDE_ALLOCATION_KIND, 1)
|
||||
.iterator()
|
||||
@@ -49,13 +62,11 @@ public final class IdService {
|
||||
.getId();
|
||||
}
|
||||
|
||||
/** Resets the global test id counter (i.e. sets the next id to 1). */
|
||||
/** Resets the global self-allocated id counter (i.e. sets the next id to 1). */
|
||||
@VisibleForTesting
|
||||
public static void resetNextTestId() {
|
||||
public static void resetSelfAllocatedId() {
|
||||
checkState(
|
||||
RegistryEnvironment.UNITTEST.equals(RegistryEnvironment.get()),
|
||||
"Can't call resetTestIdCounts() from RegistryEnvironment.%s",
|
||||
RegistryEnvironment.get());
|
||||
nextTestId.set(1); // ids cannot be zero
|
||||
isSelfAllocated(), "Can only call resetSelfAllocatedId() in unit tests or Beam pipelines");
|
||||
nextSelfAllocatedId.set(1); // ids cannot be zero
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@ public abstract class ImmutableObject implements Cloneable {
|
||||
@Target(FIELD)
|
||||
public @interface Insignificant {}
|
||||
|
||||
// Note: if this class is made to implement Serializable, this field must become 'transient' since
|
||||
// hashing is not stable across executions. Also note that @XmlTransient is forbidden on transient
|
||||
// fields and need to be removed if transient is added.
|
||||
@Ignore @XmlTransient protected Integer hashCode;
|
||||
|
||||
private boolean equalsImmutableObject(ImmutableObject other) {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Marker interface for Nomulus entities whose serialization are implemented in a fragile way. These
|
||||
* entities are made {@link Serializable} so that they can be passed between JVMs. The intended use
|
||||
* case is BEAM pipeline-based cross-database data validation between Datastore and Cloud SQL during
|
||||
* the migration. Note that only objects loaded from the SQL database need serialization support.
|
||||
* Objects exported from Datastore can already be serialized as protocol buffers.
|
||||
*
|
||||
* <p>All entities implementing this interface take advantage of the fact that all Java collection
|
||||
* classes we use, either directly or indirectly, including those in Java libraries, Guava,
|
||||
* Objectify, and Hibernate are {@code Serializable}.
|
||||
*
|
||||
* <p>The {@code serialVersionUID} field has also been omitted in the implementing classes, since
|
||||
* they are not used for persistence.
|
||||
*/
|
||||
// TODO(b/203609782): either remove this interface or fix implementors post migration.
|
||||
public interface UnsafeSerializable extends Serializable {}
|
||||
@@ -38,12 +38,12 @@ import org.joda.time.DateTime;
|
||||
* @see UpdateAutoTimestampTranslatorFactory
|
||||
*/
|
||||
@Embeddable
|
||||
public class UpdateAutoTimestamp extends ImmutableObject {
|
||||
public class UpdateAutoTimestamp extends ImmutableObject implements UnsafeSerializable {
|
||||
|
||||
// When set to true, database converters/translators should do the auto update. When set to
|
||||
// false, auto update should be suspended (this exists to allow us to preserve the original value
|
||||
// during a replay).
|
||||
private static ThreadLocal<Boolean> autoUpdateEnabled = ThreadLocal.withInitial(() -> true);
|
||||
private static final ThreadLocal<Boolean> autoUpdateEnabled = ThreadLocal.withInitial(() -> true);
|
||||
|
||||
@Transient DateTime timestamp;
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Id;
|
||||
@@ -39,6 +38,7 @@ import com.googlecode.objectify.annotation.Parent;
|
||||
import com.googlecode.objectify.condition.IfNull;
|
||||
import google.registry.model.Buildable;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.UnsafeSerializable;
|
||||
import google.registry.model.annotations.ReportedOn;
|
||||
import google.registry.model.common.TimeOfYear;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
@@ -72,18 +72,33 @@ import org.joda.time.DateTime;
|
||||
/** A billable event in a domain's lifecycle. */
|
||||
@MappedSuperclass
|
||||
public abstract class BillingEvent extends ImmutableObject
|
||||
implements Buildable, TransferServerApproveEntity {
|
||||
implements Buildable, TransferServerApproveEntity, UnsafeSerializable {
|
||||
|
||||
/** The reason for the bill, which maps 1:1 to skus in go/registry-billing-skus. */
|
||||
public enum Reason {
|
||||
CREATE,
|
||||
CREATE(true),
|
||||
@Deprecated // TODO(b/31676071): remove this legacy value once old data is cleaned up.
|
||||
ERROR,
|
||||
FEE_EARLY_ACCESS,
|
||||
RENEW,
|
||||
RESTORE,
|
||||
SERVER_STATUS,
|
||||
TRANSFER
|
||||
ERROR(false),
|
||||
FEE_EARLY_ACCESS(true),
|
||||
RENEW(true),
|
||||
RESTORE(true),
|
||||
SERVER_STATUS(false),
|
||||
TRANSFER(true);
|
||||
|
||||
private final boolean requiresPeriod;
|
||||
|
||||
Reason(boolean requiresPeriod) {
|
||||
this.requiresPeriod = requiresPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether billing events with this reason have a period years associated with them.
|
||||
*
|
||||
* <p>Note that this is an "if an only if" condition.
|
||||
*/
|
||||
public boolean hasPeriodYears() {
|
||||
return requiresPeriod;
|
||||
}
|
||||
}
|
||||
|
||||
/** Set of flags that can be applied to billing events. */
|
||||
@@ -267,7 +282,7 @@ public abstract class BillingEvent extends ImmutableObject
|
||||
public T build() {
|
||||
T instance = getInstance();
|
||||
checkNotNull(instance.reason, "Reason must be set");
|
||||
checkNotNull(instance.clientId, "Client ID must be set");
|
||||
checkNotNull(instance.clientId, "Registrar ID must be set");
|
||||
checkNotNull(instance.eventTime, "Event time must be set");
|
||||
checkNotNull(instance.targetId, "Target ID must be set");
|
||||
checkNotNull(instance.parent, "Parent must be set");
|
||||
@@ -461,17 +476,14 @@ public abstract class BillingEvent extends ImmutableObject
|
||||
checkNotNull(instance.billingTime);
|
||||
checkNotNull(instance.cost);
|
||||
checkState(!instance.cost.isNegative(), "Costs should be non-negative.");
|
||||
ImmutableSet<Reason> reasonsWithPeriods =
|
||||
Sets.immutableEnumSet(
|
||||
Reason.CREATE,
|
||||
Reason.FEE_EARLY_ACCESS,
|
||||
Reason.RENEW,
|
||||
Reason.RESTORE,
|
||||
Reason.TRANSFER);
|
||||
checkState(
|
||||
reasonsWithPeriods.contains(instance.reason) == (instance.periodYears != null),
|
||||
"Period years must be set if and only if reason is "
|
||||
+ "CREATE, FEE_EARLY_ACCESS, RENEW, RESTORE or TRANSFER.");
|
||||
// TODO(mcilwain): Enforce this check on all billing events (not just more recent ones)
|
||||
// post-migration after we add the missing period years values in SQL.
|
||||
if (instance.eventTime.isAfter(DateTime.parse("2019-01-01T00:00:00Z"))) {
|
||||
checkState(
|
||||
instance.reason.hasPeriodYears() == (instance.periodYears != null),
|
||||
"Period years must be set if and only if reason is "
|
||||
+ "CREATE, FEE_EARLY_ACCESS, RENEW, RESTORE or TRANSFER.");
|
||||
}
|
||||
checkState(
|
||||
instance.getFlags().contains(Flag.SYNTHETIC)
|
||||
== (instance.syntheticCreationTime != null),
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.model.bulkquery;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.DomainContent;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.GracePeriod;
|
||||
import google.registry.model.domain.GracePeriod.GracePeriodHistory;
|
||||
import google.registry.model.domain.secdns.DelegationSignerData;
|
||||
import google.registry.model.domain.secdns.DomainDsDataHistory;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.model.reporting.DomainTransactionRecord;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
|
||||
/**
|
||||
* Utilities for managing an alternative JPA entity model optimized for bulk loading multi-level
|
||||
* entities such as {@link DomainBase} and {@link DomainHistory}.
|
||||
*
|
||||
* <p>In a bulk query for a multi-level JPA entity type, the JPA framework only generates a bulk
|
||||
* query (SELECT * FROM table) for the base table. Then, for each row in the base table, additional
|
||||
* queries are issued to load associated rows in child tables. This can be very slow when an entity
|
||||
* type has multiple child tables.
|
||||
*
|
||||
* <p>We have defined an alternative entity model for {@code DomainBase} and {@code DomainHistory},
|
||||
* where the base table as well as the child tables are mapped to single-level entity types. The
|
||||
* idea is to load each of these types using a bulk query, and assemble them into the target type in
|
||||
* memory in a pipeline. The main use case is Datastore-Cloud SQL validation during the Registry
|
||||
* database migration, where we will need the full database snapshots frequently.
|
||||
*/
|
||||
public class BulkQueryEntities {
|
||||
/**
|
||||
* The JPA entity classes in persistence.xml to replace when creating the {@link
|
||||
* JpaTransactionManager} for bulk query.
|
||||
*/
|
||||
public static final ImmutableMap<String, String> JPA_ENTITIES_REPLACEMENTS =
|
||||
ImmutableMap.of(
|
||||
DomainBase.class.getCanonicalName(),
|
||||
DomainBaseLite.class.getCanonicalName(),
|
||||
DomainHistory.class.getCanonicalName(),
|
||||
DomainHistoryLite.class.getCanonicalName());
|
||||
|
||||
/* The JPA entity classes that are not included in persistence.xml and need to be added to
|
||||
* the {@link JpaTransactionManager} for bulk query.*/
|
||||
public static final ImmutableList<String> JPA_ENTITIES_NEW =
|
||||
ImmutableList.of(
|
||||
DomainHost.class.getCanonicalName(), DomainHistoryHost.class.getCanonicalName());
|
||||
|
||||
public static DomainBase assembleDomainBase(
|
||||
DomainBaseLite domainBaseLite,
|
||||
ImmutableSet<GracePeriod> gracePeriods,
|
||||
ImmutableSet<DelegationSignerData> delegationSignerData,
|
||||
ImmutableSet<VKey<HostResource>> nsHosts) {
|
||||
DomainBase.Builder builder = new DomainBase.Builder();
|
||||
builder.copyFrom(domainBaseLite);
|
||||
builder.setGracePeriods(gracePeriods);
|
||||
builder.setDsData(delegationSignerData);
|
||||
builder.setNameservers(nsHosts);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static DomainHistory assembleDomainHistory(
|
||||
DomainHistoryLite domainHistoryLite,
|
||||
ImmutableSet<DomainDsDataHistory> dsDataHistories,
|
||||
ImmutableSet<VKey<HostResource>> domainHistoryHosts,
|
||||
ImmutableSet<GracePeriodHistory> gracePeriodHistories,
|
||||
ImmutableSet<DomainTransactionRecord> transactionRecords) {
|
||||
DomainHistory.Builder builder = new DomainHistory.Builder();
|
||||
builder.copyFrom(domainHistoryLite);
|
||||
DomainContent rawDomainContent = domainHistoryLite.domainContent;
|
||||
if (rawDomainContent != null) {
|
||||
DomainContent newDomainContent =
|
||||
domainHistoryLite
|
||||
.domainContent
|
||||
.asBuilder()
|
||||
.setNameservers(domainHistoryHosts)
|
||||
.setGracePeriods(
|
||||
gracePeriodHistories.stream()
|
||||
.map(GracePeriod::createFromHistory)
|
||||
.collect(toImmutableSet()))
|
||||
.setDsData(
|
||||
dsDataHistories.stream()
|
||||
.map(DelegationSignerData::create)
|
||||
.collect(toImmutableSet()))
|
||||
.build();
|
||||
builder.setDomain(newDomainContent);
|
||||
}
|
||||
return builder.buildAndAssemble(
|
||||
dsDataHistories, domainHistoryHosts, gracePeriodHistories, transactionRecords);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.model.bulkquery;
|
||||
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.DomainContent;
|
||||
import google.registry.model.replay.SqlOnlyEntity;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.WithStringVKey;
|
||||
import javax.persistence.Access;
|
||||
import javax.persistence.AccessType;
|
||||
import javax.persistence.Entity;
|
||||
|
||||
/**
|
||||
* A 'light' version of {@link DomainBase} with only base table ("Domain") attributes, which allows
|
||||
* fast bulk loading. They are used in in-memory assembly of {@code DomainBase} instances along with
|
||||
* bulk-loaded child entities ({@code GracePeriod} etc). The in-memory assembly achieves much higher
|
||||
* performance than loading {@code DomainBase} directly.
|
||||
*
|
||||
* <p>Please refer to {@link BulkQueryEntities} for more information.
|
||||
*/
|
||||
@Entity(name = "Domain")
|
||||
@WithStringVKey
|
||||
@Access(AccessType.FIELD)
|
||||
public class DomainBaseLite extends DomainContent implements SqlOnlyEntity {
|
||||
|
||||
@Override
|
||||
@javax.persistence.Id
|
||||
@Access(AccessType.PROPERTY)
|
||||
public String getRepoId() {
|
||||
return super.getRepoId();
|
||||
}
|
||||
|
||||
public static VKey<DomainBaseLite> createVKey(String repoId) {
|
||||
return VKey.createSql(DomainBaseLite.class, repoId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.model.bulkquery;
|
||||
|
||||
import google.registry.model.domain.DomainHistory.DomainHistoryId;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.model.replay.SqlOnlyEntity;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.io.Serializable;
|
||||
import javax.persistence.Access;
|
||||
import javax.persistence.AccessType;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.IdClass;
|
||||
|
||||
/**
|
||||
* A name server host referenced by a {@link google.registry.model.domain.DomainHistory} record.
|
||||
* Please refer to {@link BulkQueryEntities} for usage.
|
||||
*/
|
||||
@Entity
|
||||
@Access(AccessType.FIELD)
|
||||
@IdClass(DomainHistoryHost.class)
|
||||
public class DomainHistoryHost implements Serializable, SqlOnlyEntity {
|
||||
|
||||
@Id private Long domainHistoryHistoryRevisionId;
|
||||
@Id private String domainHistoryDomainRepoId;
|
||||
@Id private String hostRepoId;
|
||||
|
||||
private DomainHistoryHost() {}
|
||||
|
||||
public DomainHistoryId getDomainHistoryId() {
|
||||
return new DomainHistoryId(domainHistoryDomainRepoId, domainHistoryHistoryRevisionId);
|
||||
}
|
||||
|
||||
public VKey<HostResource> getHostVKey() {
|
||||
return VKey.create(HostResource.class, hostRepoId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.model.bulkquery;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.DomainContent;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.DomainHistory.DomainHistoryId;
|
||||
import google.registry.model.domain.Period;
|
||||
import google.registry.model.replay.SqlOnlyEntity;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.persistence.VKey;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.Access;
|
||||
import javax.persistence.AccessType;
|
||||
import javax.persistence.AttributeOverride;
|
||||
import javax.persistence.AttributeOverrides;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.IdClass;
|
||||
import javax.persistence.PostLoad;
|
||||
|
||||
/**
|
||||
* A 'light' version of {@link DomainHistory} with only base table ("DomainHistory") attributes,
|
||||
* which allows fast bulk loading. They are used in in-memory assembly of {@code DomainHistory}
|
||||
* instances along with bulk-loaded child entities ({@code GracePeriodHistory} etc). The in-memory
|
||||
* assembly achieves much higher performance than loading {@code DomainHistory} directly.
|
||||
*
|
||||
* <p>Please refer to {@link BulkQueryEntities} for more information.
|
||||
*
|
||||
* <p>This class is adapted from {@link DomainHistory} by removing the {@code dsDataHistories},
|
||||
* {@code gracePeriodHistories}, and {@code nsHosts} fields and associated methods.
|
||||
*/
|
||||
@Entity(name = "DomainHistory")
|
||||
@Access(AccessType.FIELD)
|
||||
@IdClass(DomainHistoryId.class)
|
||||
public class DomainHistoryLite extends HistoryEntry implements SqlOnlyEntity {
|
||||
|
||||
// Store DomainContent instead of DomainBase so we don't pick up its @Id
|
||||
// Nullable for the sake of pre-Registry-3.0 history objects
|
||||
@Nullable DomainContent domainContent;
|
||||
|
||||
@Id
|
||||
@Access(AccessType.PROPERTY)
|
||||
public String getDomainRepoId() {
|
||||
// We need to handle null case here because Hibernate sometimes accesses this method before
|
||||
// parent gets initialized
|
||||
return parent == null ? null : parent.getName();
|
||||
}
|
||||
|
||||
/** This method is private because it is only used by Hibernate. */
|
||||
@SuppressWarnings("unused")
|
||||
private void setDomainRepoId(String domainRepoId) {
|
||||
parent = Key.create(DomainBase.class, domainRepoId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
@Access(AccessType.PROPERTY)
|
||||
@AttributeOverrides({
|
||||
@AttributeOverride(name = "unit", column = @Column(name = "historyPeriodUnit")),
|
||||
@AttributeOverride(name = "value", column = @Column(name = "historyPeriodValue"))
|
||||
})
|
||||
public Period getPeriod() {
|
||||
return super.getPeriod();
|
||||
}
|
||||
|
||||
/**
|
||||
* For transfers, the id of the other registrar.
|
||||
*
|
||||
* <p>For requests and cancels, the other registrar is the losing party (because the registrar
|
||||
* sending the EPP transfer command is the gaining party). For approves and rejects, the other
|
||||
* registrar is the gaining party.
|
||||
*/
|
||||
@Nullable
|
||||
@Access(AccessType.PROPERTY)
|
||||
@Column(name = "historyOtherRegistrarId")
|
||||
@Override
|
||||
public String getOtherRegistrarId() {
|
||||
return super.getOtherRegistrarId();
|
||||
}
|
||||
|
||||
@Id
|
||||
@Column(name = "historyRevisionId")
|
||||
@Access(AccessType.PROPERTY)
|
||||
@Override
|
||||
public long getId() {
|
||||
return super.getId();
|
||||
}
|
||||
|
||||
/** The key to the {@link DomainBase} this is based off of. */
|
||||
public VKey<DomainBase> getParentVKey() {
|
||||
return VKey.create(DomainBase.class, getDomainRepoId());
|
||||
}
|
||||
|
||||
@PostLoad
|
||||
void postLoad() {
|
||||
if (domainContent == null) {
|
||||
return;
|
||||
}
|
||||
// See inline comments in DomainHistory.postLoad for reasons for the following lines.
|
||||
if (domainContent.getDomainName() == null) {
|
||||
domainContent = null;
|
||||
} else if (domainContent.getRepoId() == null) {
|
||||
domainContent.setRepoId(parent.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.model.bulkquery;
|
||||
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.model.replay.SqlOnlyEntity;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.io.Serializable;
|
||||
import javax.persistence.Access;
|
||||
import javax.persistence.AccessType;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.IdClass;
|
||||
|
||||
/** A name server host of a domain. Please refer to {@link BulkQueryEntities} for usage. */
|
||||
@Entity
|
||||
@Access(AccessType.FIELD)
|
||||
@IdClass(DomainHost.class)
|
||||
public class DomainHost implements Serializable, SqlOnlyEntity {
|
||||
|
||||
@Id private String domainRepoId;
|
||||
|
||||
@Id private String hostRepoId;
|
||||
|
||||
DomainHost() {}
|
||||
|
||||
public String getDomainRepoId() {
|
||||
return domainRepoId;
|
||||
}
|
||||
|
||||
public VKey<HostResource> getHostVKey() {
|
||||
return VKey.create(HostResource.class, hostRepoId);
|
||||
}
|
||||
}
|
||||
@@ -27,13 +27,13 @@ import com.googlecode.objectify.annotation.Ignore;
|
||||
import com.googlecode.objectify.annotation.OnLoad;
|
||||
import com.googlecode.objectify.annotation.Parent;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.UnsafeSerializable;
|
||||
import google.registry.model.UpdateAutoTimestamp;
|
||||
import google.registry.model.annotations.InCrossTld;
|
||||
import google.registry.model.common.Cursor.CursorId;
|
||||
import google.registry.model.replay.DatastoreAndSqlEntity;
|
||||
import google.registry.model.tld.Registry;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import javax.persistence.Column;
|
||||
@@ -53,7 +53,7 @@ import org.joda.time.DateTime;
|
||||
@javax.persistence.Entity
|
||||
@IdClass(CursorId.class)
|
||||
@InCrossTld
|
||||
public class Cursor extends ImmutableObject implements DatastoreAndSqlEntity {
|
||||
public class Cursor extends ImmutableObject implements DatastoreAndSqlEntity, UnsafeSerializable {
|
||||
|
||||
/** The scope of a global cursor. A global cursor is a cursor that is not specific to one tld. */
|
||||
public static final String GLOBAL = "GLOBAL";
|
||||
@@ -283,7 +283,7 @@ public class Cursor extends ImmutableObject implements DatastoreAndSqlEntity {
|
||||
return cursorTime;
|
||||
}
|
||||
|
||||
static class CursorId extends ImmutableObject implements Serializable {
|
||||
public static class CursorId extends ImmutableObject implements UnsafeSerializable {
|
||||
|
||||
public CursorType type;
|
||||
public String scope;
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.google.common.collect.Range;
|
||||
import com.googlecode.objectify.annotation.Embed;
|
||||
import com.googlecode.objectify.annotation.Index;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.UnsafeSerializable;
|
||||
import java.util.List;
|
||||
import javax.persistence.Embeddable;
|
||||
import org.joda.time.DateTime;
|
||||
@@ -45,7 +46,7 @@ import org.joda.time.DateTime;
|
||||
*/
|
||||
@Embed
|
||||
@Embeddable
|
||||
public class TimeOfYear extends ImmutableObject {
|
||||
public class TimeOfYear extends ImmutableObject implements UnsafeSerializable {
|
||||
|
||||
/**
|
||||
* The time as "month day millis" with all fields left-padded with zeroes so that lexographic
|
||||
|
||||
@@ -27,7 +27,9 @@ import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.googlecode.objectify.mapper.Mapper;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.UnsafeSerializable;
|
||||
import google.registry.util.TypeUtils;
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
@@ -53,11 +55,12 @@ import org.joda.time.DateTime;
|
||||
* to use for storing the list of transitions. The user is given this choice of subclass so that the
|
||||
* field of the value type stored in the transition can be given a customized name.
|
||||
*/
|
||||
public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedTransition<V>>
|
||||
extends ForwardingMap<DateTime, T> {
|
||||
public class TimedTransitionProperty<
|
||||
V extends Serializable, T extends TimedTransitionProperty.TimedTransition<V>>
|
||||
extends ForwardingMap<DateTime, T> implements UnsafeSerializable {
|
||||
|
||||
/**
|
||||
* A transition to a value of type {@code V} at a certain time. This superclass only has a field
|
||||
* A transition to a value of type {@code V} at a certain time. This superclass only has a field
|
||||
* for the {@code DateTime}, which means that subclasses should supply the field of type {@code V}
|
||||
* and implementations of the abstract getter and setter methods to access that field. This design
|
||||
* is so that subclasses tagged with @Embed can define a custom field name for their value, for
|
||||
@@ -65,11 +68,12 @@ public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedT
|
||||
*
|
||||
* <p>The public visibility of this class exists only so that it can be subclassed; clients should
|
||||
* never call any methods on this class or attempt to access its members, but should instead treat
|
||||
* it as a customizable implementation detail of {@code TimedTransitionProperty}. However, note
|
||||
* it as a customizable implementation detail of {@code TimedTransitionProperty}. However, note
|
||||
* that subclasses must also have public visibility so that they can be instantiated via
|
||||
* reflection in a call to {@code fromValueMap}.
|
||||
*/
|
||||
public abstract static class TimedTransition<V> extends ImmutableObject {
|
||||
public abstract static class TimedTransition<V extends Serializable> extends ImmutableObject
|
||||
implements UnsafeSerializable {
|
||||
/** The time at which this value becomes the active value. */
|
||||
private DateTime transitionTime;
|
||||
|
||||
@@ -89,16 +93,16 @@ public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedT
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided value map into the equivalent transition map, using transition objects
|
||||
* of the given TimedTransition subclass. The value map must be sorted according to the natural
|
||||
* Converts the provided value map into the equivalent transition map, using transition objects of
|
||||
* the given TimedTransition subclass. The value map must be sorted according to the natural
|
||||
* ordering of its DateTime keys, and keys cannot be earlier than START_OF_TIME.
|
||||
*/
|
||||
// NB: The Class<T> parameter could be eliminated by getting the class via reflection, but then
|
||||
// the callsite cannot infer T, so unless you explicitly call this as .<V, T>fromValueMap() it
|
||||
// will default to using just TimedTransition<V>, which fails at runtime.
|
||||
private static <V, T extends TimedTransition<V>> NavigableMap<DateTime, T> makeTransitionMap(
|
||||
ImmutableSortedMap<DateTime, V> valueMap,
|
||||
final Class<T> timedTransitionSubclass) {
|
||||
private static <V extends Serializable, T extends TimedTransition<V>>
|
||||
NavigableMap<DateTime, T> makeTransitionMap(
|
||||
ImmutableSortedMap<DateTime, V> valueMap, final Class<T> timedTransitionSubclass) {
|
||||
checkArgument(
|
||||
Ordering.natural().equals(valueMap.comparator()),
|
||||
"Timed transition value map must have transition time keys in chronological order");
|
||||
@@ -121,9 +125,9 @@ public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedT
|
||||
*
|
||||
* <p>This method should be the normal method for constructing a {@link TimedTransitionProperty}.
|
||||
*/
|
||||
public static <V, T extends TimedTransition<V>> TimedTransitionProperty<V, T> fromValueMap(
|
||||
ImmutableSortedMap<DateTime, V> valueMap,
|
||||
final Class<T> timedTransitionSubclass) {
|
||||
public static <V extends Serializable, T extends TimedTransition<V>>
|
||||
TimedTransitionProperty<V, T> fromValueMap(
|
||||
ImmutableSortedMap<DateTime, V> valueMap, final Class<T> timedTransitionSubclass) {
|
||||
return new TimedTransitionProperty<>(ImmutableSortedMap.copyOf(
|
||||
makeTransitionMap(valueMap, timedTransitionSubclass)));
|
||||
}
|
||||
@@ -175,10 +179,10 @@ public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedT
|
||||
* @param allowedTransitions optional map of all possible state-to-state transitions
|
||||
* @param allowedTransitionMapName optional transition map description string for error messages
|
||||
* @param initialValue optional initial value; if present, the first transition must have this
|
||||
* value
|
||||
* value
|
||||
* @param badInitialValueErrorMessage option error message string if the initial value is wrong
|
||||
*/
|
||||
public static <V, T extends TimedTransitionProperty.TimedTransition<V>>
|
||||
public static <V extends Serializable, T extends TimedTransitionProperty.TimedTransition<V>>
|
||||
TimedTransitionProperty<V, T> make(
|
||||
ImmutableSortedMap<DateTime, V> newTransitions,
|
||||
Class<T> transitionClass,
|
||||
@@ -200,7 +204,7 @@ public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedT
|
||||
* Validates that a transition map is not null or empty, starts at START_OF_TIME, and has
|
||||
* transitions which move from one value to another in allowed ways.
|
||||
*/
|
||||
public static <V, T extends TimedTransitionProperty.TimedTransition<V>>
|
||||
public static <V extends Serializable, T extends TimedTransitionProperty.TimedTransition<V>>
|
||||
void validateTimedTransitionMap(
|
||||
@Nullable NavigableMap<DateTime, V> transitionMap,
|
||||
ImmutableMultimap<V, V> allowedTransitions,
|
||||
@@ -240,8 +244,9 @@ public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedT
|
||||
* annotation. The map for those fields must be mutable so that Objectify can load values from
|
||||
* Datastore into the map, but clients should still never mutate the field's map directly.
|
||||
*/
|
||||
public static <V, T extends TimedTransition<V>> TimedTransitionProperty<V, T> forMapify(
|
||||
ImmutableSortedMap<DateTime, V> valueMap, Class<T> timedTransitionSubclass) {
|
||||
public static <V extends Serializable, T extends TimedTransition<V>>
|
||||
TimedTransitionProperty<V, T> forMapify(
|
||||
ImmutableSortedMap<DateTime, V> valueMap, Class<T> timedTransitionSubclass) {
|
||||
return new TimedTransitionProperty<>(
|
||||
new TreeMap<>(makeTransitionMap(valueMap, timedTransitionSubclass)));
|
||||
}
|
||||
@@ -254,8 +259,9 @@ public class TimedTransitionProperty<V, T extends TimedTransitionProperty.TimedT
|
||||
* annotation. The map for those fields must be mutable so that Objectify can load values from
|
||||
* Datastore into the map, but clients should still never mutate the field's map directly.
|
||||
*/
|
||||
public static <V, T extends TimedTransition<V>> TimedTransitionProperty<V, T> forMapify(
|
||||
V valueAtStartOfTime, Class<T> timedTransitionSubclass) {
|
||||
public static <V extends Serializable, T extends TimedTransition<V>>
|
||||
TimedTransitionProperty<V, T> forMapify(
|
||||
V valueAtStartOfTime, Class<T> timedTransitionSubclass) {
|
||||
return forMapify(
|
||||
ImmutableSortedMap.of(START_OF_TIME, valueAtStartOfTime), timedTransitionSubclass);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.EntitySubclass;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.UnsafeSerializable;
|
||||
import google.registry.model.contact.ContactHistory.ContactHistoryId;
|
||||
import google.registry.model.replay.DatastoreEntity;
|
||||
import google.registry.model.replay.SqlEntity;
|
||||
@@ -59,7 +60,7 @@ import javax.persistence.PostLoad;
|
||||
@EntitySubclass
|
||||
@Access(AccessType.FIELD)
|
||||
@IdClass(ContactHistoryId.class)
|
||||
public class ContactHistory extends HistoryEntry implements SqlEntity {
|
||||
public class ContactHistory extends HistoryEntry implements SqlEntity, UnsafeSerializable {
|
||||
|
||||
// Store ContactBase instead of ContactResource so we don't pick up its @Id
|
||||
// Nullable for the sake of pre-Registry-3.0 history objects
|
||||
@@ -227,5 +228,11 @@ public class ContactHistory extends HistoryEntry implements SqlEntity {
|
||||
getInstance().parent = Key.create(ContactResource.class, contactRepoId);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder wipeOutPii() {
|
||||
getInstance().contactBase =
|
||||
getInstance().getContactBase().get().asBuilder().wipeOut().build();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ import com.google.common.collect.ImmutableList;
|
||||
import com.googlecode.objectify.annotation.Embed;
|
||||
import google.registry.model.Buildable;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.UnsafeSerializable;
|
||||
import google.registry.model.eppcommon.PresenceMarker;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import javax.persistence.Embeddable;
|
||||
import javax.persistence.Embedded;
|
||||
@@ -31,7 +33,7 @@ import javax.xml.bind.annotation.XmlType;
|
||||
@Embed
|
||||
@Embeddable
|
||||
@XmlType(propOrder = {"name", "org", "addr", "voice", "fax", "email"})
|
||||
public class Disclose extends ImmutableObject {
|
||||
public class Disclose extends ImmutableObject implements UnsafeSerializable {
|
||||
|
||||
List<PostalInfoChoice> name;
|
||||
|
||||
@@ -78,7 +80,7 @@ public class Disclose extends ImmutableObject {
|
||||
|
||||
/** The "intLocType" from <a href="http://tools.ietf.org/html/rfc5733">RFC5733</a>. */
|
||||
@Embed
|
||||
public static class PostalInfoChoice extends ImmutableObject {
|
||||
public static class PostalInfoChoice extends ImmutableObject implements Serializable {
|
||||
@XmlAttribute
|
||||
PostalInfo.Type type;
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.googlecode.objectify.annotation.Embed;
|
||||
import google.registry.model.Buildable;
|
||||
import google.registry.model.Buildable.Overlayable;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.UnsafeSerializable;
|
||||
import java.util.Optional;
|
||||
import javax.persistence.Embeddable;
|
||||
import javax.persistence.EnumType;
|
||||
@@ -38,7 +39,8 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
@Embed
|
||||
@Embeddable
|
||||
@XmlType(propOrder = {"name", "org", "address", "type"})
|
||||
public class PostalInfo extends ImmutableObject implements Overlayable<PostalInfo> {
|
||||
public class PostalInfo extends ImmutableObject
|
||||
implements Overlayable<PostalInfo>, UnsafeSerializable {
|
||||
|
||||
/** The type of the address, either localized or international. */
|
||||
public enum Type {
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.googlecode.objectify.annotation.Embed;
|
||||
import com.googlecode.objectify.annotation.Ignore;
|
||||
import com.googlecode.objectify.annotation.Index;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.UnsafeSerializable;
|
||||
import google.registry.model.contact.ContactResource;
|
||||
import google.registry.persistence.VKey;
|
||||
import javax.persistence.Embeddable;
|
||||
@@ -46,7 +47,7 @@ import javax.xml.bind.annotation.XmlEnumValue;
|
||||
*/
|
||||
@Embed
|
||||
@Embeddable
|
||||
public class DesignatedContact extends ImmutableObject {
|
||||
public class DesignatedContact extends ImmutableObject implements UnsafeSerializable {
|
||||
|
||||
/**
|
||||
* XML type for contact types. This can be either: {@code "admin"}, {@code "billing"}, or
|
||||
|
||||
@@ -24,6 +24,7 @@ import google.registry.model.host.HostResource;
|
||||
import google.registry.model.replay.DatastoreAndSqlEntity;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.WithStringVKey;
|
||||
import google.registry.util.DomainNameUtils;
|
||||
import java.util.Set;
|
||||
import javax.persistence.Access;
|
||||
import javax.persistence.AccessType;
|
||||
@@ -164,6 +165,11 @@ public class DomainBase extends DomainContent
|
||||
return cloneDomainProjectedAtTime(this, now);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeSqlSaveOnReplay() {
|
||||
fullyQualifiedDomainName = DomainNameUtils.canonicalizeDomainName(fullyQualifiedDomainName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeDatastoreSaveOnReplay() {
|
||||
saveIndexesToDatastore();
|
||||
@@ -189,6 +195,7 @@ public class DomainBase extends DomainContent
|
||||
}
|
||||
|
||||
public Builder copyFrom(DomainContent domainContent) {
|
||||
this.getInstance().copyUpdateTimestamp(domainContent);
|
||||
return this.setAuthInfo(domainContent.getAuthInfo())
|
||||
.setAutorenewPollMessage(domainContent.getAutorenewPollMessage())
|
||||
.setAutorenewBillingEvent(domainContent.getAutorenewBillingEvent())
|
||||
|
||||
@@ -865,7 +865,8 @@ public class DomainContent extends EppResource
|
||||
public B setDomainName(String domainName) {
|
||||
checkArgument(
|
||||
domainName.equals(canonicalizeDomainName(domainName)),
|
||||
"Domain name must be in puny-coded, lower-case form");
|
||||
"Domain name %s not in puny-coded, lower-case form",
|
||||
domainName);
|
||||
getInstance().fullyQualifiedDomainName = domainName;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import google.registry.model.replay.SqlEntity;
|
||||
import google.registry.model.reporting.DomainTransactionRecord;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.DomainNameUtils;
|
||||
import java.io.Serializable;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
@@ -216,6 +217,10 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
|
||||
return super.getId();
|
||||
}
|
||||
|
||||
public DomainHistoryId getDomainHistoryId() {
|
||||
return new DomainHistoryId(getDomainRepoId(), getId());
|
||||
}
|
||||
|
||||
/** Returns keys to the {@link HostResource} that are the nameservers for the domain. */
|
||||
public Set<VKey<HostResource>> getNsHosts() {
|
||||
return nsHosts;
|
||||
@@ -299,6 +304,8 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
|
||||
public void beforeSqlSaveOnReplay() {
|
||||
if (domainContent == null) {
|
||||
domainContent = jpaTm().getEntityManager().find(DomainBase.class, getDomainRepoId());
|
||||
domainContent.fullyQualifiedDomainName =
|
||||
DomainNameUtils.canonicalizeDomainName(domainContent.fullyQualifiedDomainName);
|
||||
fillAuxiliaryFieldsFromDomain(this);
|
||||
}
|
||||
}
|
||||
@@ -314,6 +321,8 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
|
||||
nullToEmptyImmutableCopy(domainHistory.domainContent.getGracePeriods()).stream()
|
||||
.map(gracePeriod -> GracePeriodHistory.createFrom(domainHistory.id, gracePeriod))
|
||||
.collect(toImmutableSet());
|
||||
} else {
|
||||
domainHistory.nsHosts = ImmutableSet.of();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,8 +402,16 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
|
||||
if (domainContent == null) {
|
||||
return this;
|
||||
}
|
||||
// TODO(b/203609982): if actual type of domainContent is DomainBase, convert to DomainContent
|
||||
// Note: a DomainHistory fetched by JPA has DomainContent in this field. Allowing DomainBase
|
||||
// in the setter makes equality checks messy.
|
||||
getInstance().domainContent = domainContent;
|
||||
return super.setParent(domainContent);
|
||||
if (domainContent instanceof DomainBase) {
|
||||
super.setParent(domainContent);
|
||||
} else {
|
||||
super.setParent(Key.create(DomainBase.class, domainContent.getRepoId()));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDomainRepoId(String domainRepoId) {
|
||||
@@ -412,5 +429,19 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
|
||||
fillAuxiliaryFieldsFromDomain(instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
public DomainHistory buildAndAssemble(
|
||||
ImmutableSet<DomainDsDataHistory> dsDataHistories,
|
||||
ImmutableSet<VKey<HostResource>> domainHistoryHosts,
|
||||
ImmutableSet<GracePeriodHistory> gracePeriodHistories,
|
||||
ImmutableSet<DomainTransactionRecord> transactionRecords) {
|
||||
DomainHistory instance = super.build();
|
||||
instance.dsDataHistories = dsDataHistories;
|
||||
instance.nsHosts = domainHistoryHosts;
|
||||
instance.gracePeriodHistories = gracePeriodHistories;
|
||||
instance.domainTransactionRecords = transactionRecords;
|
||||
instance.hashCode = null;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user