1
0
mirror of https://github.com/google/nomulus synced 2026-05-25 17:20:32 +00:00

Compare commits

...

27 Commits

Author SHA1 Message Date
sarahcaseybot
fcc027e0c8 Add Cloud SQL read to Spec11Pipeline (#1173)
* Add Cloud SQL read to Spec11Pipeline

* Add database option

* Add database parameter

* Add a test of the full pipeline

* Use DatabaseHelper in tests

* restore the original tm

* More test fixes
2021-06-11 14:25:20 -04:00
Weimin Yu
c3a4887845 Fix timestamp inversion error in a test (#1207)
* Fix timestamp inversion error in a test
2021-06-11 11:05:10 -04:00
Ben McIlwain
a0b6437f4c Add reason/registrar request options when creating/updating domains (#1202)
* Add reason/registrar_request options when creating/updating domains
2021-06-11 10:50:32 -04:00
Lai Jiang
a7210a26b4 Make RefreshDnsForAllDomains SQL-aware (#1197)
Also marks a few mapreduce actions as @Deprecated as they are no longer
needed in SQL.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1197)
<!-- Reviewable:end -->
2021-06-10 21:09:19 -04:00
Lai Jiang
c7096a1b71 Fix a flaky test (#1204)
In testSuccess_expandSingleEvent_notIdempotentforDifferentRecurring(),
two Recurring entities are created with the only difference being their IDs. If
we don't order the Recurrings by ID when loading them there is no guarantee
which one is expanded first. In this test the expected OneTime entities are
created with the assumption that the first loaded DomainHistory (parent of a
OneTime) corresponds to the expanding the Recurring with the smaller ID (2L).
Since the DomainHistory entities are loaded in order of IDs, and the IDs are
created monotonically in time in tests, we need to load the Recurrings in
order of their IDs to ensure that the first DomainHistory is the result of
expanding the Recurring with ID of 2L. This should impose minimum performance
penalty as we are ordering by the primary key.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1204)
<!-- Reviewable:end -->
2021-06-10 14:06:05 -04:00
gbrodman
30634ff404 Convert EppResourceUtils::loadAtPointInTime to SQL+DS (#1194)
* Convert EppResourceUtils::loadAtPointInTime to SQL+DS

This required the following changes:
- The branching / conversion logic itself, where we load the most recent
history object for the resource in question (or just return the resource
itself)
- For simplicity's sake, adding a method in the *History objects that
returns the generic resource -- this means that it can be called when we
don't know or care which subclass it is.
- Populating the domain's dsData and gracePeriods fields from the
DomainHistory fields, and adding factories in the relevant classes to
allow us to do the conversions nicely (the history classes are almost
the same as the regular ones, but not quite).
- Change the tests to use the clocks properly and to allow comparison of
e.g. DomainContent to DomainBase. The objects aren't the same (one is a
superclass of the other) but the fields are.

Note as well a slight behavioral change: commit logs only allow us
24-hour granularity, so two updates in the same day mean that the
earlier update is ignored and inaccessible. This is not the case for
*History objects in SQL; all versions are accessible.
2021-06-10 12:25:06 -04:00
Lai Jiang
4f71d780ab Make ExportDomainListsAction SQL-aware (#1195)
<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1195)
<!-- Reviewable:end -->
2021-06-10 12:03:17 -04:00
Michael Muller
14ad56a392 Fix Datastore "count" queries (#1201)
* Fix Datastore "count" queries

The objectify "count()" method doesn't work for result sets larger than 1000
elements, use the original trick from "count domains" that fetches the keys
and counts them.

* Added an SO link
2021-06-08 15:23:25 -04:00
gbrodman
a1b56b0521 Convert remaining ofy() calls to auditedOfy() (#1200)
* Convert remaining ofy() calls to auditedOfy()
2021-06-08 13:52:13 -04:00
gbrodman
3f41f7f444 Start the DS->SQL replay cron job in non-prod environments (#1199)
* Start the DS->SQL replay in non-prod environments

This should be a no-op since we haven't enabled it but this means that
when we set the schedule, we'll start replaying
2021-06-08 11:35:47 -04:00
gbrodman
4f6bcea63f Fix a test flake in SetDatabaseMigrationScheduleCommandTest (#1198)
* Fix a test flake in SetDatabaseMigrationScheduleCommandTest

The cache is static so some odd state may stick around between tests --
we should clear it
2021-06-08 11:35:29 -04:00
Lai Jiang
bd0ef626a1 Fix a few test annotations (#1196) 2021-06-08 00:40:58 -04:00
Lai Jiang
68304133c4 Make RefreshDnsOnHostRenameAction SQL-aware (#1190)
<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1190)
<!-- Reviewable:end -->
2021-06-07 10:24:49 -04:00
Weimin Yu
16392c3808 Fix access to a nullable field in HistoryEntry (#1193)
* Fix access to a nullable field in HistoryEntry
2021-06-04 16:30:25 -04:00
gbrodman
5f479488fa Use DB migration state to determine running async replay SQL->DS (#1191)
* Use DB migration state to determine running async replay SQL->DS

The SQL->DS replay likely could use more work (locking, returning the
right codes, things like that) but that's outside the scope of this PR.
2021-06-04 16:18:25 -04:00
Michael Muller
886a970ed6 Use detaching queries for all criteria queries (#1192)
* Make all criteria queries use jpaTm().query()

This causes all criteria queries to detach-on-load.

* Detach results of criteria queries

Wrap the criteria queries in DetachingTypedQuery now that the latter is
merged.
2021-06-04 14:37:53 -04:00
Michael Muller
d7f7568761 Fix copy causing premature hash calculation (#1189)
* Fix copy causing premature hash calculation

The creation of a builder to set the DomainContent repo id in DomainHistory
triggers an equality check which causes the hash code of an associated
transfer data object to be calculated prematurely, before the Ofy keys are
reconstituted.  Replace this with a simple setter, which is acceptible in this
case because the object is being loaded and is considered to be not fully
constructed yet.

* Do setRepoId() in Contact and Host history

Not essential for these as far as we know, but it's safer and more consistent.

* Fixed typos
2021-06-04 11:38:42 -04:00
gbrodman
2017930a8f Add commands to set and check the database migration state (#1174) 2021-06-04 09:57:08 -04:00
gbrodman
ed07fc8181 Use DB migration state to determine running async replay DS->SQL (#1175)
* Use DB migration state to determine running async replay DS->SQL
2021-06-03 11:43:26 -04:00
Lai Jiang
aa2898ebfc Make ExpandRecurringBillingEventAction SQL-aware (#1181)
There is some complication regarding how the
CancellationMatchingBillingEvent of the generated OneTime can be
reconstructed when loading from SQL. I decided to only address it in
testing as there is no real value to fully reconstruct this VKey in
production where we are either in SQL or Ofy mode, both never in both.
Therefore the VKey in a particular mode only needs to contain the
corresponding key in order to function.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1181)
<!-- Reviewable:end -->
2021-06-03 10:21:16 -04:00
gbrodman
586189d7ee Use a TimedTransitionProperty for the DB migration schedule (#1186)
This includes the following changes:
- Convert the single-valued database migration state to a timed
transition property, meaning that we can switch all instances over at
the same time and schedule it in advance
- Use a "cache" (technically an expiring memoized supplier) when
retrieving the database migration state value
- Delete the old DatabaseTransitionSchedule because it is no longer
necessary. We took the idea from that and used it for the new
DatabaseMigrationStateSchedule, though we cannot reuse the entity itself
because the structure is fundamentally different.
- Removed references to the DatabaseTransitionSchedule, mainly in the
getter/setter commands+tests and a few odd references elsewhere.
2021-06-02 14:06:28 -04:00
Lai Jiang
275f364dcb Handle cases where periodYears is NULL in a OneTime (#1187)
There are cases where periodYears is not set when creating a OneTime
billing event, for example when performing a registry lock (default cost = $0)
or when performing a server status update, such as applying the
serverUpdateProhibited status (default cost = $20). This is not currently
handled currently in the billing pipeline because the parseFromRecord
method checks for nullness for all fields. Even if it does not validate
the fields, the null periodYears will still cause problem when the
billing event is converted to CSV files.

This PR alters the BigQuery SQL file to convert a NULL to 0 when
creating the BillingEvent in the invoicing pipeline. It also sets the EndDate
in the invoice CSV to an empty string when periodYears is 0. Note that when the
cost is also 0, the billing event is filtered out in the invoice CSV so only
the non-free OneTime with null periodYear will have an impact on the output.
For detailed reports all billing events are included and the zero
periodYears is printed as is.

Setting the EndDate to empty is the correct behavior per
go/manual-integration-csv#end-date.
2021-06-02 11:52:47 -04:00
Weimin Yu
66867e4397 Use SecretManager for nomulus-tool-cloudbuild cred (#1188)
* Use SecretManager for nomulus-tool-cloudbuild cred

Store cloudbuild's nomulus-tool credential in SecretManager and make the
deployment pipeline load it from the SecretManager.

The tool-credential.json.enc file in the
gs://domain-registry-dev-deploy/secrets folder is no longer needed.
2021-06-02 09:32:57 -04:00
Weimin Yu
3fa56dec45 Make keyring use SecretManager as sole storage (#1185)
* Make keyring use SecretManager as sole storage

The Keyring will only use the SecretManager as storage. Accesses to the
Datastore are removed.

Also consolidated KmsKeyringTest into KmsKeyingUpdaterTest. The latter
is left with its original name to facilitate code reviews. It will be
renamed in planned cleanups.

Additional cleanup is left for a future PR. These include:

- Remove KmsConnection and its associated injection modules

- Remove KmsSecretRevision from SQL schema and code

- Rename relevant files to more appropriate names.
2021-06-01 15:28:22 -04:00
Michael Muller
92f5f8989b Detach entities loaded by loadSingleton() (#1184)
* Detach entities loaded by loadSingleton()

* Reformatted
2021-06-01 14:22:57 -04:00
Michael Muller
810adf0158 Detach result objects obtained through jpaTm().query() (#1183)
* Added TransformingTypedQuery class

Added class to wrap TypedQuery so that we can detach all objects on load.

* Don't detach non-entity results; complete tests

* Changes for review

* Make non-static and call detach directly
2021-06-01 14:20:04 -04:00
gbrodman
f6004181f8 Convert DeleteExpiredDomainsAction to QueryComposer (#1180)
I think this one needed to wait until the detach-on-load PR went in, but
now we should be all set.
2021-06-01 13:32:25 -04:00
115 changed files with 7241 additions and 6295 deletions

View File

@@ -66,6 +66,8 @@ import org.joda.time.Duration;
service = Action.Service.BACKEND,
path = "/_dr/task/deleteOldCommitLogs",
auth = Auth.AUTH_INTERNAL_OR_ADMIN)
// No longer needed in SQL. Subject to future removal.
@Deprecated
public final class DeleteOldCommitLogsAction implements Runnable {
private static final int NUM_MAP_SHARDS = 20;

View File

@@ -27,7 +27,9 @@ import com.google.appengine.tools.cloudstorage.GcsFileMetadata;
import com.google.appengine.tools.cloudstorage.GcsService;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import google.registry.model.common.DatabaseMigrationStateSchedule.ReplayDirection;
import google.registry.model.server.Lock;
import google.registry.model.translators.VKeyTranslatorFactory;
import google.registry.persistence.VKey;
@@ -39,6 +41,7 @@ import google.registry.schema.replay.DatastoreOnlyEntity;
import google.registry.schema.replay.NonReplicatedEntity;
import google.registry.schema.replay.ReplaySpecializer;
import google.registry.schema.replay.SqlReplayCheckpoint;
import google.registry.util.Clock;
import google.registry.util.RequestStatusChecker;
import java.io.IOException;
import java.io.InputStream;
@@ -69,15 +72,19 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
@Inject Response response;
@Inject RequestStatusChecker requestStatusChecker;
@Inject GcsDiffFileLister diffLister;
@Inject Clock clock;
@Inject
ReplayCommitLogsToSqlAction() {}
@Override
public void run() {
if (!RegistryConfig.getCloudSqlReplayCommitLogs()) {
String message = "ReplayCommitLogsToSqlAction was called but disabled in the config.";
logger.atWarning().log(message);
MigrationState state = DatabaseMigrationStateSchedule.getValueAtTime(clock.nowUtc());
if (!state.getReplayDirection().equals(ReplayDirection.DATASTORE_TO_SQL)) {
String message =
String.format(
"Skipping ReplayCommitLogsToSqlAction because we are in migration phase %s.", state);
logger.atInfo().log(message);
// App Engine will retry on any non-2xx status code, which we don't want in this case.
response.setStatus(SC_NO_CONTENT);
response.setPayload(message);

View File

@@ -17,8 +17,8 @@ package google.registry.batch;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static google.registry.flows.FlowUtils.marshalWithLenientRetry;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.ResourceUtils.readResourceUtf8;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -36,6 +36,7 @@ import google.registry.flows.StatelessRequestSessionMetadata;
import google.registry.model.domain.DomainBase;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.model.eppoutput.EppOutput;
import google.registry.persistence.transaction.QueryComposer.Comparator;
import google.registry.request.Action;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
@@ -128,12 +129,15 @@ public class DeleteExpiredDomainsAction implements Runnable {
logger.atInfo().log(
"Deleting non-renewing domains with autorenew end times up through %s.", runTime);
// Note: This query is (and must be) non-transactional, and thus, is only eventually consistent.
// Note: in Datastore, this query is (and must be) non-transactional, and thus, is only
// eventually consistent.
ImmutableList<DomainBase> domainsToDelete =
ofy().load().type(DomainBase.class).filter("autorenewEndTime <=", runTime).list().stream()
// Datastore can't do two inequalities in one query, so the second happens in-memory.
.filter(d -> d.getDeletionTime().isEqual(END_OF_TIME))
.collect(toImmutableList());
transactIfJpaTm(
() ->
tm().createQueryComposer(DomainBase.class)
.where("autorenewEndTime", Comparator.LTE, runTime)
.where("deletionTime", Comparator.EQ, END_OF_TIME)
.list());
if (domainsToDelete.isEmpty()) {
logger.atInfo().log("Found 0 domains to delete.");
response.setPayload("Found 0 domains to delete.");

View File

@@ -21,9 +21,12 @@ import static google.registry.mapreduce.MapreduceRunner.PARAM_DRY_RUN;
import static google.registry.mapreduce.inputs.EppResourceInputs.createChildEntityInput;
import static google.registry.model.common.Cursor.CursorType.RECURRING_BILLING;
import static google.registry.model.domain.Period.Unit.YEARS;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_AUTORENEW;
import static google.registry.persistence.transaction.QueryComposer.Comparator.EQ;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost;
import static google.registry.util.CollectionUtils.union;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -38,10 +41,8 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Range;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.googlecode.objectify.Key;
import google.registry.mapreduce.MapreduceRunner;
import google.registry.mapreduce.inputs.NullInput;
import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
@@ -54,7 +55,7 @@ import google.registry.model.domain.Period;
import google.registry.model.registry.Registry;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
@@ -92,31 +93,88 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
@Override
public void run() {
Cursor cursor =
tm().loadByKeyIfPresent(Cursor.createGlobalVKey(RECURRING_BILLING)).orElse(null);
DateTime executeTime = clock.nowUtc();
DateTime persistedCursorTime = (cursor == null ? START_OF_TIME : cursor.getCursorTime());
DateTime persistedCursorTime =
transactIfJpaTm(
() ->
tm().loadByKeyIfPresent(Cursor.createGlobalVKey(RECURRING_BILLING))
.orElse(Cursor.createGlobal(RECURRING_BILLING, START_OF_TIME))
.getCursorTime());
DateTime cursorTime = cursorTimeParam.orElse(persistedCursorTime);
checkArgument(
cursorTime.isBefore(executeTime),
"Cursor time must be earlier than execution time.");
cursorTime.isBefore(executeTime), "Cursor time must be earlier than execution time.");
logger.atInfo().log(
"Running Recurring billing event expansion for billing time range [%s, %s).",
cursorTime, executeTime);
mrRunner
.setJobName("Expand Recurring billing events into synthetic OneTime events.")
.setModuleName("backend")
.runMapreduce(
new ExpandRecurringBillingEventsMapper(isDryRun, cursorTime, clock.nowUtc()),
new ExpandRecurringBillingEventsReducer(isDryRun, persistedCursorTime),
// Add an extra shard that maps over a null recurring event (see the mapper for why).
ImmutableList.of(
new NullInput<>(),
createChildEntityInput(
ImmutableSet.of(DomainBase.class), ImmutableSet.of(Recurring.class))))
.sendLinkToMapreduceConsole(response);
}
if (tm().isOfy()) {
mrRunner
.setJobName("Expand Recurring billing events into synthetic OneTime events.")
.setModuleName("backend")
.runMapreduce(
new ExpandRecurringBillingEventsMapper(isDryRun, cursorTime, clock.nowUtc()),
new ExpandRecurringBillingEventsReducer(isDryRun, persistedCursorTime),
// Add an extra shard that maps over a null recurring event (see the mapper for why).
ImmutableList.of(
new NullInput<>(),
createChildEntityInput(
ImmutableSet.of(DomainBase.class), ImmutableSet.of(Recurring.class))))
.sendLinkToMapreduceConsole(response);
} else {
int numBillingEventsSaved =
jpaTm()
.transact(
() ->
jpaTm()
.query(
"FROM BillingRecurrence "
+ "WHERE eventTime <= :executeTime "
+ "AND eventTime < recurrenceEndTime "
+ "ORDER BY id ASC",
Recurring.class)
.setParameter("executeTime", executeTime)
// Need to get a list from the transaction and then convert it to a stream
// for further processing. If we get a stream directly, each elements gets
// processed downstream eagerly but Hibernate returns a
// ScrollableResultsIterator that cannot be advanced outside the
// transaction, resulting in an exception.
.getResultList())
.stream()
.map(
recurring ->
jpaTm()
.transact(
() ->
expandBillingEvent(recurring, executeTime, cursorTime, isDryRun)))
.reduce(0, Integer::sum);
if (!isDryRun) {
logger.atInfo().log("Saved OneTime billing events", numBillingEventsSaved);
} else {
logger.atInfo().log("Generated OneTime billing events (dry run)", numBillingEventsSaved);
}
logger.atInfo().log(
"Recurring event expansion %s complete for billing event range [%s, %s).",
isDryRun ? "(dry run) " : "", cursorTime, executeTime);
tm().transact(
() -> {
// Check for the unlikely scenario where the cursor has been altered during the
// expansion.
DateTime currentCursorTime =
tm().loadByKeyIfPresent(Cursor.createGlobalVKey(RECURRING_BILLING))
.orElse(Cursor.createGlobal(RECURRING_BILLING, START_OF_TIME))
.getCursorTime();
if (!currentCursorTime.equals(persistedCursorTime)) {
throw new IllegalStateException(
String.format(
"Current cursor position %s does not match persisted cursor position %s.",
currentCursorTime, persistedCursorTime));
}
if (!isDryRun) {
tm().put(Cursor.createGlobal(RECURRING_BILLING, executeTime));
}
});
}
}
/** Mapper to expand {@link Recurring} billing events into synthetic {@link OneTime} events. */
public static class ExpandRecurringBillingEventsMapper
extends Mapper<Recurring, DateTime, DateTime> {
@@ -155,100 +213,7 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
try {
numBillingEventsSaved =
tm().transactNew(
() -> {
ImmutableSet.Builder<OneTime> syntheticOneTimesBuilder =
new ImmutableSet.Builder<>();
final Registry tld =
Registry.get(getTldFromDomainName(recurring.getTargetId()));
// Determine the complete set of times at which this recurring event should
// occur (up to and including the runtime of the mapreduce).
Iterable<DateTime> eventTimes =
recurring
.getRecurrenceTimeOfYear()
.getInstancesInRange(
Range.closed(
recurring.getEventTime(),
earliestOf(recurring.getRecurrenceEndTime(), executeTime)));
// Convert these event times to billing times
final ImmutableSet<DateTime> billingTimes =
getBillingTimesInScope(eventTimes, cursorTime, executeTime, tld);
Key<? extends EppResource> domainKey = recurring.getParentKey().getParent();
Iterable<OneTime> oneTimesForDomain =
ofy().load().type(OneTime.class).ancestor(domainKey);
// Determine the billing times that already have OneTime events persisted.
ImmutableSet<DateTime> existingBillingTimes =
getExistingBillingTimes(oneTimesForDomain, recurring);
ImmutableSet.Builder<HistoryEntry> historyEntriesBuilder =
new ImmutableSet.Builder<>();
// Create synthetic OneTime events for all billing times that do not yet have
// an event persisted.
for (DateTime billingTime : difference(billingTimes, existingBillingTimes)) {
// Construct a new HistoryEntry that parents over the OneTime
DomainHistory historyEntry =
new DomainHistory.Builder()
.setBySuperuser(false)
.setClientId(recurring.getClientId())
.setModificationTime(tm().getTransactionTime())
// TODO (jianglai): modify this to use setDomain instead when
// converting this action to be SQL-aware.
.setDomainRepoId(domainKey.getName())
.setPeriod(Period.create(1, YEARS))
.setReason(
"Domain autorenewal by ExpandRecurringBillingEventsAction")
.setRequestedByRegistrar(false)
.setType(DOMAIN_AUTORENEW)
// Don't write a domain transaction record if the recurrence was
// ended prior to the billing time (i.e. a domain was deleted
// during the autorenew grace period).
.setDomainTransactionRecords(
recurring.getRecurrenceEndTime().isBefore(billingTime)
? ImmutableSet.of()
: ImmutableSet.of(
DomainTransactionRecord.create(
tld.getTldStr(),
// We report this when the autorenew grace period
// ends
billingTime,
TransactionReportField.netRenewsFieldFromYears(1),
1)))
.build();
historyEntriesBuilder.add(historyEntry);
DateTime eventTime = billingTime.minus(tld.getAutoRenewGracePeriodLength());
// Determine the cost for a one-year renewal.
Money renewCost = getDomainRenewCost(recurring.getTargetId(), eventTime, 1);
syntheticOneTimesBuilder.add(
new OneTime.Builder()
.setBillingTime(billingTime)
.setClientId(recurring.getClientId())
.setCost(renewCost)
.setEventTime(eventTime)
.setFlags(union(recurring.getFlags(), Flag.SYNTHETIC))
.setParent(historyEntry)
.setPeriodYears(1)
.setReason(recurring.getReason())
.setSyntheticCreationTime(executeTime)
.setCancellationMatchingBillingEvent(recurring.createVKey())
.setTargetId(recurring.getTargetId())
.build());
}
Set<HistoryEntry> historyEntries = historyEntriesBuilder.build();
Set<OneTime> syntheticOneTimes = syntheticOneTimesBuilder.build();
if (!isDryRun) {
ImmutableSet<ImmutableObject> entitiesToSave =
new ImmutableSet.Builder<ImmutableObject>()
.addAll(historyEntries)
.addAll(syntheticOneTimes)
.build();
ofy().save().entities(entitiesToSave).now();
}
return syntheticOneTimes.size();
});
() -> expandBillingEvent(recurring, executeTime, cursorTime, isDryRun));
} catch (Throwable t) {
getContext().incrementCounter("error: " + t.getClass().getSimpleName());
getContext().incrementCounter(ERROR_COUNTER);
@@ -260,45 +225,12 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
if (!isDryRun) {
getContext().incrementCounter("Saved OneTime billing events", numBillingEventsSaved);
} else {
getContext().incrementCounter(
"Generated OneTime billing events (dry run)", numBillingEventsSaved);
getContext()
.incrementCounter("Generated OneTime billing events (dry run)", numBillingEventsSaved);
}
}
/**
* Filters a set of {@link DateTime}s down to event times that are in scope for a particular
* mapreduce run, given the cursor time and the mapreduce execution time.
*/
private ImmutableSet<DateTime> getBillingTimesInScope(
Iterable<DateTime> eventTimes,
DateTime cursorTime,
DateTime executeTime,
final Registry tld) {
return Streams.stream(eventTimes)
.map(eventTime -> eventTime.plus(tld.getAutoRenewGracePeriodLength()))
.filter(Range.closedOpen(cursorTime, executeTime))
.collect(toImmutableSet());
}
/**
* Determines an {@link ImmutableSet} of {@link DateTime}s that have already been persisted
* for a given recurring billing event.
*/
private ImmutableSet<DateTime> getExistingBillingTimes(
Iterable<BillingEvent.OneTime> oneTimesForDomain,
final BillingEvent.Recurring recurringEvent) {
return Streams.stream(oneTimesForDomain)
.filter(
billingEvent ->
recurringEvent
.createVKey()
.equals(billingEvent.getCancellationMatchingBillingEvent()))
.map(OneTime::getBillingTime)
.collect(toImmutableSet());
}
}
/**
* "Reducer" to advance the cursor after all map jobs have been completed. The NullInput into the
* mapper will cause the mapper to emit one timestamp pair (current cursor and execution time),
@@ -331,7 +263,8 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
isDryRun ? "(dry run) " : "", cursorTime, executionTime);
tm().transact(
() -> {
Cursor cursor = ofy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now();
Cursor cursor =
auditedOfy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now();
DateTime currentCursorTime =
(cursor == null ? START_OF_TIME : cursor.getCursorTime());
if (!currentCursorTime.equals(expectedPersistedCursorTime)) {
@@ -346,4 +279,135 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
});
}
}
private static int expandBillingEvent(
Recurring recurring, DateTime executeTime, DateTime cursorTime, boolean isDryRun) {
ImmutableSet.Builder<OneTime> syntheticOneTimesBuilder = new ImmutableSet.Builder<>();
final Registry tld = Registry.get(getTldFromDomainName(recurring.getTargetId()));
// Determine the complete set of times at which this recurring event should
// occur (up to and including the runtime of the mapreduce).
Iterable<DateTime> eventTimes =
recurring
.getRecurrenceTimeOfYear()
.getInstancesInRange(
Range.closed(
recurring.getEventTime(),
earliestOf(recurring.getRecurrenceEndTime(), executeTime)));
// Convert these event times to billing times
final ImmutableSet<DateTime> billingTimes =
getBillingTimesInScope(eventTimes, cursorTime, executeTime, tld);
VKey<DomainBase> domainKey =
VKey.create(
DomainBase.class, recurring.getDomainRepoId(), recurring.getParentKey().getParent());
Iterable<OneTime> oneTimesForDomain;
if (tm().isOfy()) {
oneTimesForDomain = auditedOfy().load().type(OneTime.class).ancestor(domainKey.getOfyKey());
} else {
oneTimesForDomain =
tm().createQueryComposer(OneTime.class)
.where("domainRepoId", EQ, recurring.getDomainRepoId())
.list();
}
// Determine the billing times that already have OneTime events persisted.
ImmutableSet<DateTime> existingBillingTimes =
getExistingBillingTimes(oneTimesForDomain, recurring);
ImmutableSet.Builder<DomainHistory> historyEntriesBuilder = new ImmutableSet.Builder<>();
// Create synthetic OneTime events for all billing times that do not yet have
// an event persisted.
for (DateTime billingTime : difference(billingTimes, existingBillingTimes)) {
// Construct a new HistoryEntry that parents over the OneTime
DomainHistory historyEntry =
new DomainHistory.Builder()
.setBySuperuser(false)
.setClientId(recurring.getClientId())
.setModificationTime(tm().getTransactionTime())
.setDomain(tm().loadByKey(domainKey))
.setPeriod(Period.create(1, YEARS))
.setReason("Domain autorenewal by ExpandRecurringBillingEventsAction")
.setRequestedByRegistrar(false)
.setType(DOMAIN_AUTORENEW)
// Don't write a domain transaction record if the recurrence was
// ended prior to the billing time (i.e. a domain was deleted
// during the autorenew grace period).
.setDomainTransactionRecords(
recurring.getRecurrenceEndTime().isBefore(billingTime)
? ImmutableSet.of()
: ImmutableSet.of(
DomainTransactionRecord.create(
tld.getTldStr(),
// We report this when the autorenew grace period
// ends
billingTime,
TransactionReportField.netRenewsFieldFromYears(1),
1)))
.build();
historyEntriesBuilder.add(historyEntry);
DateTime eventTime = billingTime.minus(tld.getAutoRenewGracePeriodLength());
// Determine the cost for a one-year renewal.
Money renewCost = getDomainRenewCost(recurring.getTargetId(), eventTime, 1);
syntheticOneTimesBuilder.add(
new OneTime.Builder()
.setBillingTime(billingTime)
.setClientId(recurring.getClientId())
.setCost(renewCost)
.setEventTime(eventTime)
.setFlags(union(recurring.getFlags(), Flag.SYNTHETIC))
.setParent(historyEntry)
.setPeriodYears(1)
.setReason(recurring.getReason())
.setSyntheticCreationTime(executeTime)
.setCancellationMatchingBillingEvent(recurring.createVKey())
.setTargetId(recurring.getTargetId())
.build());
}
Set<DomainHistory> historyEntries = historyEntriesBuilder.build();
Set<OneTime> syntheticOneTimes = syntheticOneTimesBuilder.build();
if (!isDryRun) {
ImmutableSet<ImmutableObject> entitiesToSave =
new ImmutableSet.Builder<ImmutableObject>()
.addAll(historyEntries)
.addAll(syntheticOneTimes)
.build();
tm().putAll(entitiesToSave);
}
return syntheticOneTimes.size();
}
/**
* Filters a set of {@link DateTime}s down to event times that are in scope for a particular
* mapreduce run, given the cursor time and the mapreduce execution time.
*/
protected static ImmutableSet<DateTime> getBillingTimesInScope(
Iterable<DateTime> eventTimes,
DateTime cursorTime,
DateTime executeTime,
final Registry tld) {
return Streams.stream(eventTimes)
.map(eventTime -> eventTime.plus(tld.getAutoRenewGracePeriodLength()))
.filter(Range.closedOpen(cursorTime, executeTime))
.collect(toImmutableSet());
}
/**
* Determines an {@link ImmutableSet} of {@link DateTime}s that have already been persisted for a
* given recurring billing event.
*/
private static ImmutableSet<DateTime> getExistingBillingTimes(
Iterable<BillingEvent.OneTime> oneTimesForDomain,
final BillingEvent.Recurring recurringEvent) {
return Streams.stream(oneTimesForDomain)
.filter(
billingEvent ->
recurringEvent
.createVKey()
.equals(billingEvent.getCancellationMatchingBillingEvent()))
.map(OneTime::getBillingTime)
.collect(toImmutableSet());
}
}

View File

@@ -23,6 +23,7 @@ import static google.registry.batch.AsyncTaskEnqueuer.PARAM_REQUESTED_TIME;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_HOST_RENAME;
import static google.registry.batch.AsyncTaskMetrics.OperationType.DNS_REFRESH;
import static google.registry.mapreduce.inputs.EppResourceInputs.createEntityInput;
import static google.registry.model.EppResourceUtils.getLinkedDomainKeys;
import static google.registry.model.EppResourceUtils.isActive;
import static google.registry.model.EppResourceUtils.isDeleted;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -69,6 +70,7 @@ import java.util.logging.Level;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.http.HttpStatus;
import org.joda.time.DateTime;
import org.joda.time.Duration;
@@ -86,6 +88,8 @@ public class RefreshDnsOnHostRenameAction implements Runnable {
@Inject Clock clock;
@Inject MapreduceRunner mrRunner;
@Inject @Named(QUEUE_ASYNC_HOST_RENAME) Queue pullQueue;
@Inject DnsQueue dnsQueue;
@Inject RequestStatusChecker requestStatusChecker;
@Inject Response response;
@Inject Retrier retrier;
@@ -153,7 +157,39 @@ public class RefreshDnsOnHostRenameAction implements Runnable {
} else {
logger.atInfo().log(
"Processing asynchronous DNS refresh for renamed hosts: %s", hostKeys.build());
runMapreduce(refreshRequests, lock);
if (tm().isOfy()) {
runMapreduce(refreshRequests, lock);
} else {
try {
refreshRequests.stream()
.flatMap(
request ->
getLinkedDomainKeys(request.hostKey(), request.lastUpdateTime(), null)
.stream())
.distinct()
.map(domainKey -> tm().transact(() -> tm().loadByKey(domainKey).getDomainName()))
.forEach(
domainName -> {
retrier.callWithRetry(
() -> dnsQueue.addDomainRefreshTask(domainName),
TransientFailureException.class);
logger.atInfo().log("Enqueued DNS refresh for domain %s.", domainName);
});
deleteTasksWithRetry(
refreshRequests,
getQueue(QUEUE_ASYNC_HOST_RENAME),
asyncTaskMetrics,
retrier,
OperationResult.SUCCESS);
} catch (Throwable t) {
String message = "Error refreshing DNS on host rename.";
logger.atSevere().withCause(t).log(message);
response.setPayload(message);
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
} finally {
lock.get().release();
}
}
}
}

View File

@@ -54,6 +54,8 @@ import javax.inject.Inject;
service = Action.Service.BACKEND,
path = "/_dr/task/resaveAllEppResources",
auth = Auth.AUTH_INTERNAL_OR_ADMIN)
// No longer needed in SQL. Subject to future removal.
@Deprecated
public class ResaveAllEppResourcesAction implements Runnable {
@Inject MapreduceRunner mrRunner;

View File

@@ -251,7 +251,14 @@ public abstract class BillingEvent implements Serializable {
InvoiceGroupingKey getInvoiceGroupingKey() {
return new AutoValue_BillingEvent_InvoiceGroupingKey(
billingTime().toLocalDate().withDayOfMonth(1).toString(),
billingTime().toLocalDate().withDayOfMonth(1).plusYears(years()).minusDays(1).toString(),
years() == 0
? ""
: billingTime()
.toLocalDate()
.withDayOfMonth(1)
.plusYears(years())
.minusDays(1)
.toString(),
billingId(),
String.format("%s - %s", registrarId(), tld()),
String.format("%s | TLD: %s | TERM: %d-year", action(), tld(), years()),

View File

@@ -23,8 +23,10 @@ import dagger.Component;
import dagger.Module;
import dagger.Provides;
import google.registry.beam.common.RegistryJpaIO;
import google.registry.beam.common.RegistryJpaIO.Read;
import google.registry.beam.spec11.SafeBrowsingTransforms.EvaluateSafeBrowsingFn;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.model.domain.DomainBase;
import google.registry.model.reporting.Spec11ThreatMatch;
import google.registry.model.reporting.Spec11ThreatMatch.ThreatType;
import google.registry.util.Retrier;
@@ -97,20 +99,9 @@ public class Spec11Pipeline implements Serializable {
void setupPipeline(Pipeline pipeline) {
PCollection<Subdomain> domains =
pipeline.apply(
"Read active domains from BigQuery",
BigQueryIO.read(Subdomain::parseFromRecord)
.fromQuery(
SqlTemplate.create(getQueryFromFile(Spec11Pipeline.class, "subdomains.sql"))
.put("PROJECT_ID", options.getProject())
.put("DATASTORE_EXPORT_DATASET", "latest_datastore_export")
.put("REGISTRAR_TABLE", "Registrar")
.put("DOMAIN_BASE_TABLE", "DomainBase")
.build())
.withCoder(SerializableCoder.of(Subdomain.class))
.usingStandardSql()
.withoutValidation()
.withTemplateCompatibility());
options.getDatabase().equals("DATASTORE")
? readFromBigQuery(options, pipeline)
: readFromCloudSql(pipeline);
PCollection<KV<Subdomain, ThreatMatch>> threatMatches =
domains.apply("Run through SafeBrowsing API", ParDo.of(safeBrowsingFn));
@@ -119,6 +110,47 @@ public class Spec11Pipeline implements Serializable {
saveToGcs(threatMatches, options);
}
static PCollection<Subdomain> readFromCloudSql(Pipeline pipeline) {
Read<Object[], Subdomain> 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()",
Spec11Pipeline::parseRow);
return pipeline.apply("Read active domains from Cloud SQL", read);
}
static PCollection<Subdomain> readFromBigQuery(Spec11PipelineOptions options, Pipeline pipeline) {
return pipeline.apply(
"Read active domains from BigQuery",
BigQueryIO.read(Subdomain::parseFromRecord)
.fromQuery(
SqlTemplate.create(getQueryFromFile(Spec11Pipeline.class, "subdomains.sql"))
.put("PROJECT_ID", options.getProject())
.put("DATASTORE_EXPORT_DATASET", "latest_datastore_export")
.put("REGISTRAR_TABLE", "Registrar")
.put("DOMAIN_BASE_TABLE", "DomainBase")
.build())
.withCoder(SerializableCoder.of(Subdomain.class))
.usingStandardSql()
.withoutValidation()
.withTemplateCompatibility());
}
private static Subdomain parseRow(Object[] row) {
DomainBase domainBase = (DomainBase) row[0];
String emailAddress = (String) row[1];
if (emailAddress == null) {
emailAddress = "";
}
return Subdomain.create(
domainBase.getDomainName(),
domainBase.getRepoId(),
domainBase.getCurrentSponsorClientId(),
emailAddress);
}
static void saveToSql(
PCollection<KV<Subdomain, ThreatMatch>> threatMatches, Spec11PipelineOptions options) {
String transformId = "Spec11 Threat Matches";

View File

@@ -34,4 +34,9 @@ public interface Spec11PipelineOptions extends RegistryPipelineOptions {
String getReportingBucketUrl();
void setReportingBucketUrl(String value);
@Description("The database to read data from.")
String getDatabase();
void setDatabase(String value);
}

View File

@@ -1502,22 +1502,6 @@ public final class RegistryConfig {
CONFIG_SETTINGS.get().cloudSql.replicateTransactions = replicateTransactions;
}
/**
* Returns whether or not to replay commit logs to the SQL database after export to GCS.
*
* <p>If true, we will trigger the {@link google.registry.backup.ReplayCommitLogsToSqlAction}
* after the {@link google.registry.backup.ExportCommitLogDiffAction} to load the commit logs and
* replay them to SQL.
*/
public static boolean getCloudSqlReplayCommitLogs() {
return CONFIG_SETTINGS.get().cloudSql.replayCommitLogs;
}
@VisibleForTesting
public static void overrideCloudSqlReplayCommitLogs(boolean replayCommitLogs) {
CONFIG_SETTINGS.get().cloudSql.replayCommitLogs = replayCommitLogs;
}
/** Returns the roid suffix to be used for the roids of all contacts and hosts. */
public static String getContactAndHostRoidSuffix() {
return CONFIG_SETTINGS.get().registryPolicy.contactAndHostRoidSuffix;

View File

@@ -127,7 +127,6 @@ public class RegistryConfigSettings {
public String username;
public String instanceConnectionName;
public boolean replicateTransactions;
public boolean replayCommitLogs;
}
/** Configuration for Apache Beam (Cloud Dataflow). */

View File

@@ -230,8 +230,6 @@ cloudSql:
# Set this to true to replicate cloud SQL transactions to datastore in the
# background.
replicateTransactions: false
# Set this to true to enable replay of commit logs to SQL
replayCommitLogs: false
cloudDns:
# Set both properties to null in Production.

View File

@@ -199,6 +199,15 @@
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/task/replayCommitLogsToSql]]></url>
<description>
Replays recent commit logs from Datastore to the SQL secondary backend.
</description>
<schedule>every 3 minutes</schedule>
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/cron/readDnsQueue?jitterSeconds=45]]></url>
<description>

View File

@@ -41,6 +41,11 @@
<property name="nsHosts" direction="asc"/>
<property name="deletionTime" direction="asc"/>
</datastore-index>
<!-- For deleting expired not-previously-deleted domains. -->
<datastore-index kind="DomainBase" ancestor="false" source="manual">
<property name="deletionTime" direction="asc"/>
<property name="autorenewEndTime" direction="asc"/>
</datastore-index>
<!-- For RDAP searches by linked nameserver. -->
<datastore-index kind="DomainBase" ancestor="false" source="manual">
<property name="nsHosts" direction="asc"/>

View File

@@ -200,4 +200,12 @@
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/task/replayCommitLogsToSql]]></url>
<description>
Replays recent commit logs from Datastore to the SQL secondary backend.
</description>
<schedule>every 3 minutes</schedule>
<target>backend</target>
</cron>
</cronentries>

View File

@@ -100,4 +100,12 @@
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/task/replayCommitLogsToSql]]></url>
<description>
Replays recent commit logs from Datastore to the SQL secondary backend.
</description>
<schedule>every 3 minutes</schedule>
<target>backend</target>
</cron>
</cronentries>

View File

@@ -229,4 +229,12 @@
<target>backend</target>
</cron>
<cron>
<url><![CDATA[/_dr/task/replayCommitLogsToSql]]></url>
<description>
Replays recent commit logs from Datastore to the SQL secondary backend.
</description>
<schedule>every 3 minutes</schedule>
<target>backend</target>
</cron>
</cronentries>

View File

@@ -19,9 +19,10 @@ import static com.google.common.base.Verify.verifyNotNull;
import static google.registry.mapreduce.inputs.EppResourceInputs.createEntityInput;
import static google.registry.model.EppResourceUtils.isActive;
import static google.registry.model.registry.Registries.getTldsOfType;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.joda.time.DateTimeZone.UTC;
import com.google.appengine.tools.cloudstorage.GcsFilename;
import com.google.appengine.tools.cloudstorage.RetryParams;
@@ -45,12 +46,14 @@ import google.registry.request.Action;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.storage.drive.DriveConnection;
import google.registry.util.Clock;
import google.registry.util.NonFinalForTesting;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.List;
import java.util.function.Supplier;
import javax.inject.Inject;
import org.joda.time.DateTime;
@@ -70,9 +73,13 @@ public class ExportDomainListsAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final int MAX_NUM_REDUCE_SHARDS = 100;
public static final String REGISTERED_DOMAINS_FILENAME = "registered_domains.txt";
@Inject MapreduceRunner mrRunner;
@Inject Response response;
@Inject Clock clock;
@Inject DriveConnection driveConnection;
@Inject @Config("domainListsGcsBucket") String gcsBucket;
@Inject @Config("gcsBufferSize") int gcsBufferSize;
@Inject ExportDomainListsAction() {}
@@ -81,15 +88,99 @@ public class ExportDomainListsAction implements Runnable {
public void run() {
ImmutableSet<String> realTlds = getTldsOfType(TldType.REAL);
logger.atInfo().log("Exporting domain lists for tlds %s", realTlds);
mrRunner
.setJobName("Export domain lists")
.setModuleName("backend")
.setDefaultReduceShards(Math.min(realTlds.size(), MAX_NUM_REDUCE_SHARDS))
.runMapreduce(
new ExportDomainListsMapper(DateTime.now(UTC), realTlds),
new ExportDomainListsReducer(gcsBucket, gcsBufferSize),
ImmutableList.of(createEntityInput(DomainBase.class)))
.sendLinkToMapreduceConsole(response);
if (tm().isOfy()) {
mrRunner
.setJobName("Export domain lists")
.setModuleName("backend")
.setDefaultReduceShards(Math.min(realTlds.size(), MAX_NUM_REDUCE_SHARDS))
.runMapreduce(
new ExportDomainListsMapper(clock.nowUtc(), realTlds),
new ExportDomainListsReducer(gcsBucket, gcsBufferSize),
ImmutableList.of(createEntityInput(DomainBase.class)))
.sendLinkToMapreduceConsole(response);
} else {
realTlds.forEach(
tld -> {
List<String> domains =
tm().transact(
() ->
// Note that if we had "creationTime <= :now" in the condition (not
// necessary as there is no pending creation, the order of deletionTime
// and creationTime in the query would have been significant and it
// should come after deletionTime. When Hibernate substitutes "now" it
// will first validate that the **first** field that is to be compared
// with it (deletionTime) is assignable from the substituted Java object
// (click.nowUtc()). Since creationTime is a CreateAutoTimestamp, if it
// comes first, we will need to substitute "now" with
// CreateAutoTimestamp.create(clock.nowUtc()). This might look a bit
// strange as the Java object type is clearly incompatible between the
// two fields deletionTime (DateTime) and creationTime, yet they are
// compared with the same "now". It is actually OK because in the end
// Hibernate converts everything to SQL types (and Java field names to
// SQL column names) to run the query. Both CreateAutoTimestamp and
// DateTime are persisted as timestamp_z in SQL. It is only the
// validation that compares the Java types, and only with the first
// field that compares with the substituted value.
jpaTm()
.query(
"SELECT fullyQualifiedDomainName FROM Domain "
+ "WHERE tld = :tld "
+ "AND deletionTime > :now "
+ "ORDER by fullyQualifiedDomainName ASC",
String.class)
.setParameter("tld", tld)
.setParameter("now", clock.nowUtc())
.getResultList());
String domainsList = Joiner.on("\n").join(domains);
logger.atInfo().log(
"Exporting %d domains for TLD %s to GCS and Drive.", domains.size(), tld);
exportToGcs(tld, domainsList, gcsBucket, gcsBufferSize);
exportToDrive(tld, domainsList, driveConnection);
});
}
}
protected static boolean exportToDrive(
String tld, String domains, DriveConnection driveConnection) {
verifyNotNull(driveConnection, "Expecting non-null driveConnection");
try {
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",
tld);
} else {
String resultMsg =
driveConnection.createOrUpdateFile(
REGISTERED_DOMAINS_FILENAME,
MediaType.PLAIN_TEXT_UTF_8,
registry.getDriveFolderId(),
domains.getBytes(UTF_8));
logger.atInfo().log(
"Exporting registered domains succeeded for TLD %s, response was: %s", tld, resultMsg);
}
} catch (Throwable e) {
logger.atSevere().withCause(e).log(
"Error exporting registered domains for TLD %s to Drive, skipping...", tld);
return false;
}
return true;
}
protected static boolean exportToGcs(
String tld, String domains, String gcsBucket, int gcsBufferSize) {
GcsFilename filename = new GcsFilename(gcsBucket, tld + ".txt");
GcsUtils cloudStorage =
new GcsUtils(createGcsService(RetryParams.getDefaultInstance()), gcsBufferSize);
try (OutputStream gcsOutput = cloudStorage.openOutputStream(filename);
Writer osWriter = new OutputStreamWriter(gcsOutput, UTF_8)) {
osWriter.write(domains);
} catch (Throwable e) {
logger.atSevere().withCause(e).log(
"Error exporting registered domains for TLD %s to GCS, skipping...", tld);
return false;
}
return true;
}
static class ExportDomainListsMapper extends Mapper<DomainBase, String, String> {
@@ -122,9 +213,6 @@ public class ExportDomainListsAction implements Runnable {
private static Supplier<DriveConnection> driveConnectionSupplier =
Suppliers.memoize(() -> DaggerDriveModule_DriveComponent.create().driveConnection());
static final String REGISTERED_DOMAINS_FILENAME = "registered_domains.txt";
static final MediaType EXPORT_MIME_TYPE = MediaType.PLAIN_TEXT_UTF_8;
private final String gcsBucket;
private final int gcsBufferSize;
@@ -147,53 +235,21 @@ public class ExportDomainListsAction implements Runnable {
driveConnection = driveConnectionSupplier.get();
}
private void exportToDrive(String tld, String domains) {
verifyNotNull(driveConnection, "expecting non-null driveConnection");
try {
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",
tld);
} else {
String resultMsg =
driveConnection.createOrUpdateFile(
REGISTERED_DOMAINS_FILENAME,
EXPORT_MIME_TYPE,
registry.getDriveFolderId(),
domains.getBytes(UTF_8));
logger.atInfo().log(
"Exporting registered domains succeeded for TLD %s, response was: %s",
tld, resultMsg);
}
} catch (Throwable e) {
logger.atSevere().withCause(e).log(
"Error exporting registered domains for TLD %s to Drive", tld);
}
getContext().incrementCounter("domain lists written out to Drive");
}
private void exportToGcs(String tld, String domains) {
GcsFilename filename = new GcsFilename(gcsBucket, tld + ".txt");
GcsUtils cloudStorage =
new GcsUtils(createGcsService(RetryParams.getDefaultInstance()), gcsBufferSize);
try (OutputStream gcsOutput = cloudStorage.openOutputStream(filename);
Writer osWriter = new OutputStreamWriter(gcsOutput, UTF_8)) {
osWriter.write(domains);
} catch (IOException e) {
logger.atSevere().withCause(e).log(
"Error exporting registered domains for TLD %s to GCS.", tld);
}
getContext().incrementCounter("domain lists written out to GCS");
}
@Override
public void reduce(String tld, ReducerInput<String> fqdns) {
ImmutableList<String> domains = ImmutableList.sortedCopyOf(() -> fqdns);
String domainsList = Joiner.on('\n').join(domains);
logger.atInfo().log("Exporting %d domains for TLD %s to GCS and Drive.", domains.size(), tld);
exportToGcs(tld, domainsList);
exportToDrive(tld, domainsList);
if (exportToGcs(tld, domainsList, gcsBucket, gcsBufferSize)) {
getContext().incrementCounter("domain lists successful written out to GCS");
} else {
getContext().incrementCounter("domain lists failed to write out to GCS");
}
if (exportToDrive(tld, domainsList, driveConnection)) {
getContext().incrementCounter("domain lists successfully written out to Drive");
} else {
getContext().incrementCounter("domain lists failed to write out to Drive");
}
}
@VisibleForTesting

View File

@@ -16,24 +16,12 @@ package google.registry.keyring.kms;
import static com.google.common.base.CaseFormat.LOWER_HYPHEN;
import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.flogger.FluentLogger;
import com.googlecode.objectify.Key;
import google.registry.config.RegistryConfig.Config;
import google.registry.keyring.api.KeySerializer;
import google.registry.keyring.api.Keyring;
import google.registry.keyring.api.KeyringException;
import google.registry.model.server.KmsSecret;
import google.registry.model.server.KmsSecretRevision;
import google.registry.model.server.KmsSecretRevisionSqlDao;
import google.registry.privileges.secretmanager.KeyringSecretStore;
import java.io.IOException;
import java.util.Arrays;
import java.util.Optional;
import javax.inject.Inject;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPKeyPair;
@@ -47,11 +35,9 @@ import org.bouncycastle.openpgp.PGPPublicKey;
* @see <a href="https://cloud.google.com/kms/docs/">Google Cloud Key Management Service
* Documentation</a>
*/
// TODO(2021-06-01): rename this class to SecretManagerKeyring and delete KmsSecretRevision
// TODO(2021-07-01): rename this class to SecretManagerKeyring and delete KmsSecretRevision
public class KmsKeyring implements Keyring {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Key labels for private key secrets. */
enum PrivateKeyLabel {
BRDA_SIGNING_PRIVATE,
@@ -92,13 +78,10 @@ public class KmsKeyring implements Keyring {
}
}
private final KmsConnection kmsConnection;
private final KeyringSecretStore secretStore;
@Inject
KmsKeyring(
@Config("defaultKmsConnection") KmsConnection kmsConnection, KeyringSecretStore secretStore) {
this.kmsConnection = kmsConnection;
KmsKeyring(KeyringSecretStore secretStore) {
this.secretStore = secretStore;
}
@@ -201,44 +184,11 @@ public class KmsKeyring implements Keyring {
return getKeyPair(keyLabel).getPrivateKey();
}
private byte[] getDecryptedDataFromDatastore(String keyName) {
String encryptedData;
if (tm().isOfy()) {
KmsSecret secret =
auditedOfy().load().key(Key.create(getCrossTldKey(), KmsSecret.class, keyName)).now();
checkState(secret != null, "Requested secret '%s' does not exist.", keyName);
encryptedData = auditedOfy().load().key(secret.getLatestRevision()).now().getEncryptedValue();
} else {
Optional<KmsSecretRevision> revision =
tm().transact(() -> KmsSecretRevisionSqlDao.getLatestRevision(keyName));
checkState(revision.isPresent(), "Requested secret '%s' does not exist.", keyName);
encryptedData = revision.get().getEncryptedValue();
}
try {
return kmsConnection.decrypt(keyName, encryptedData);
} catch (Exception e) {
return new byte[0];
}
}
private byte[] getDataFromSecretStore(String keyName) {
private byte[] getDecryptedData(String keyName) {
try {
return secretStore.getSecret(keyName);
} catch (Exception e) {
throw new KeyringException("Failed to retrieve secret for " + keyName, e);
}
}
private byte[] getDecryptedData(String keyName) {
byte[] dsData = getDecryptedDataFromDatastore(keyName);
byte[] secretStoreData = getDataFromSecretStore(keyName);
if (Arrays.equals(dsData, secretStoreData)) {
logger.atInfo().log("Values for %s in Datastore and Secret Manager match.", keyName);
return secretStoreData;
}
logger.atWarning().log("Values for %s in Datastore and Secret Manager do not match.", keyName);
return secretStoreData;
}
}

View File

@@ -32,18 +32,13 @@ import static google.registry.keyring.kms.KmsKeyring.StringKeyLabel.MARKSDB_SMDR
import static google.registry.keyring.kms.KmsKeyring.StringKeyLabel.RDE_SSH_CLIENT_PRIVATE_STRING;
import static google.registry.keyring.kms.KmsKeyring.StringKeyLabel.RDE_SSH_CLIENT_PUBLIC_STRING;
import static google.registry.keyring.kms.KmsKeyring.StringKeyLabel.SAFE_BROWSING_API_KEY;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.keyring.api.KeySerializer;
import google.registry.keyring.kms.KmsKeyring.PrivateKeyLabel;
import google.registry.keyring.kms.KmsKeyring.PublicKeyLabel;
import google.registry.keyring.kms.KmsKeyring.StringKeyLabel;
import google.registry.model.server.KmsSecret;
import google.registry.model.server.KmsSecretRevision;
import google.registry.privileges.secretmanager.KeyringSecretStore;
import java.io.IOException;
import java.util.HashMap;
@@ -62,14 +57,11 @@ import org.bouncycastle.openpgp.PGPPublicKey;
public final class KmsUpdater {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final KmsConnection kmsConnection;
private final KeyringSecretStore secretStore;
private final HashMap<String, byte[]> secretValues;
@Inject
public KmsUpdater(
@Config("defaultKmsConnection") KmsConnection kmsConnection, KeyringSecretStore secretStore) {
this.kmsConnection = kmsConnection;
public KmsUpdater(KeyringSecretStore secretStore) {
this.secretStore = secretStore;
// Use LinkedHashMap to preserve insertion order on update() to simplify testing and debugging
@@ -150,24 +142,6 @@ public final class KmsUpdater {
+ "Please check the status of Secret Manager and re-run the command.",
e);
}
// TODO(2021-06-01): remove the writes to Datastore
persistEncryptedValues(encryptValues(secretValues));
}
/**
* Encrypts updated secrets using KMS. If the configured {@code KeyRing} or {@code CryptoKey}
* associated with a secret doesn't exist, they will first be created.
*
* @see google.registry.config.RegistryConfigSettings.Kms
*/
private ImmutableMap<String, EncryptResponse> encryptValues(Map<String, byte[]> keyValues) {
ImmutableMap.Builder<String, EncryptResponse> encryptedValues = new ImmutableMap.Builder<>();
for (Map.Entry<String, byte[]> entry : keyValues.entrySet()) {
String secretName = entry.getKey();
encryptedValues.put(secretName, kmsConnection.encrypt(secretName, entry.getValue()));
}
return encryptedValues.build();
}
private KmsUpdater setString(String key, StringKeyLabel stringKeyLabel) {
@@ -195,32 +169,6 @@ public final class KmsUpdater {
return this;
}
/**
* Persists encrypted secrets to Datastore as {@link KmsSecretRevision} entities and makes them
* primary. {@link KmsSecret} entities point to the latest {@link KmsSecretRevision}.
*
* <p>The changes are committed transactionally; if an error occurs, all existing {@link
* KmsSecretRevision} entities will remain primary.
*/
private static void persistEncryptedValues(
final ImmutableMap<String, EncryptResponse> encryptedValues) {
tm().transact(
() -> {
for (Map.Entry<String, EncryptResponse> entry : encryptedValues.entrySet()) {
String secretName = entry.getKey();
EncryptResponse revisionData = entry.getValue();
KmsSecretRevision secretRevision =
new KmsSecretRevision.Builder()
.setEncryptedValue(revisionData.ciphertext())
.setKmsCryptoKeyVersionName(revisionData.cryptoKeyVersionName())
.setParent(secretName)
.build();
tm().putAll(secretRevision, KmsSecret.create(secretName, secretRevision));
}
});
}
private void setSecret(String secretName, byte[] value) {
checkArgument(!secretValues.containsKey(secretName), "Attempted to set %s twice", secretName);
secretValues.put(secretName, value);

View File

@@ -1,55 +0,0 @@
// Copyright 2020 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 static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryEnvironment;
import google.registry.model.common.DatabaseTransitionSchedule;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
/** Utility methods related to migrating dual-read/dual-write entities. */
public class DatabaseMigrationUtils {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Throws exceptions only in unit tests, otherwise only logs exceptions. */
public static void suppressExceptionUnlessInTest(Runnable work, String message) {
try {
work.run();
} catch (Exception e) {
if (RegistryEnvironment.get().equals(RegistryEnvironment.UNITTEST)) {
throw e;
}
logger.atWarning().withCause(e).log(message);
}
}
/** Gets the value for the database currently considered primary. */
public static PrimaryDatabase getPrimaryDatabase(TransitionId transitionId) {
return DatabaseTransitionSchedule.getCached(transitionId)
.map(DatabaseTransitionSchedule::getPrimaryDatabase)
.orElse(PrimaryDatabase.DATASTORE);
}
public static boolean isDatastore(TransitionId transitionId) {
return tm().transactNew(() -> DatabaseMigrationUtils.getPrimaryDatabase(transitionId))
.equals(PrimaryDatabase.DATASTORE);
}
private DatabaseMigrationUtils() {}
}

View File

@@ -17,8 +17,7 @@ package google.registry.model;
import com.google.common.collect.ImmutableSet;
import google.registry.model.billing.BillingEvent;
import google.registry.model.common.Cursor;
import google.registry.model.common.DatabaseMigrationStateWrapper;
import google.registry.model.common.DatabaseTransitionSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.EntityGroupRoot;
import google.registry.model.common.GaeUserIdConverter;
import google.registry.model.contact.ContactHistory;
@@ -76,8 +75,7 @@ public final class EntityClasses {
ContactHistory.class,
ContactResource.class,
Cursor.class,
DatabaseMigrationStateWrapper.class,
DatabaseTransitionSchedule.class,
DatabaseMigrationStateSchedule.class,
DomainBase.class,
DomainHistory.class,
EntityGroupRoot.class,

View File

@@ -155,9 +155,16 @@ public abstract class EppResource extends BackupGroupRoot implements Buildable {
return repoId;
}
/** This method exists solely to satisfy Hibernate. Use {@link Builder} instead. */
@SuppressWarnings("UnusedMethod")
private void setRepoId(String repoId) {
/**
* Sets the repository ID.
*
* <p>This should only be used for restoring the repo id of an object being loaded in a PostLoad
* method (effectively, when it is still under construction by Hibernate). In all other cases, the
* object should be regarded as immutable and changes should go through a Builder.
*
* <p>In addition to this special case use, this method must exist to satisfy Hibernate.
*/
public void setRepoId(String repoId) {
this.repoId = repoId;
}

View File

@@ -16,10 +16,11 @@ package google.registry.model;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.DateTimeUtils.isAtOrAfter;
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import static google.registry.util.DateTimeUtils.latestOf;
@@ -42,10 +43,13 @@ import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.ofy.CommitLogManifest;
import google.registry.model.ofy.CommitLogMutation;
import google.registry.model.registry.Registry;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.HistoryEntryDao;
import google.registry.model.transfer.DomainTransferData;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey;
import java.util.Comparator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
@@ -265,26 +269,43 @@ public final class EppResourceUtils {
* Rewinds an {@link EppResource} object to a given point in time.
*
* <p>This method costs nothing if {@code resource} is already current. Otherwise it needs to
* perform a single asynchronous key fetch operation.
* perform a single fetch operation.
*
* <p><b>Warning:</b> A resource can only be rolled backwards in time, not forwards; therefore
* {@code resource} should be whatever's currently in Datastore.
*
* <p><b>Warning:</b> Revisions are granular to 24-hour periods. It's recommended that
* {@code timestamp} be set to midnight. Otherwise you must take into consideration that under
* certain circumstances, a resource might be restored to a revision on the previous day, even if
* there were revisions made earlier on the same date as {@code timestamp}; however, a resource
* will never be restored to a revision occurring after {@code timestamp}. This behavior is due to
* the way {@link google.registry.model.translators.CommitLogRevisionsTranslatorFactory
* <p><b>Warning:</b> In Datastore, revisions are granular to 24-hour periods. It's recommended
* that {@code timestamp} be set to midnight. If you don't use midnight, you must take into
* consideration that under certain circumstances, a resource might be restored to a revision on
* the previous day, even if there were revisions made earlier on the same date as {@code
* timestamp}; however, a resource will never be restored to a revision occurring after {@code
* timestamp}. This behavior is due to the way {@link
* google.registry.model.translators.CommitLogRevisionsTranslatorFactory
* CommitLogRevisionsTranslatorFactory} manages the {@link EppResource#revisions} field. Please
* note however that the creation and deletion times of a resource are granular to the
* millisecond.
*
* <p>Example: a resource in Datastore has three revisions A, B, and C
*
* <ul>
* <li>A: Day 0, 1pm
* <li>B: Day 1, 1pm
* <li>C: Day 1, 3pm
* </ul>
*
* <p>If one requests the resource as of day 1 at 2pm, we will return revision A because as far as
* the commit logs are concerned, revision C completely overwrites the existence of revision B.
*
* <p>When using the SQL backend (post-Registry-3.0-migration) this restriction goes away and
* objects can be restored to any revision.
*
* <p>TODO(b/177567432): Once Datastore is completely removed, remove the Result wrapping.
*
* @return an asynchronous operation returning resource at {@code timestamp} or {@code null} if
* resource is deleted or not yet created
*/
public static <T extends EppResource>
Result<T> loadAtPointInTime(final T resource, final DateTime timestamp) {
public static <T extends EppResource> Result<T> loadAtPointInTime(
final T resource, final DateTime timestamp) {
// If we're before the resource creation time, don't try to find a "most recent revision".
if (timestamp.isBefore(resource.getCreationTime())) {
return new ResultNow<>(null);
@@ -299,7 +320,8 @@ public final class EppResourceUtils {
: loadMostRecentRevisionAtTime(resource, timestamp);
return () -> {
T loadedResource = loadResult.now();
return (loadedResource == null) ? null
return (loadedResource == null)
? null
: (isActive(loadedResource, timestamp)
? cloneProjectedAtTime(loadedResource, timestamp)
: null);
@@ -307,26 +329,43 @@ public final class EppResourceUtils {
}
/**
* Returns an asynchronous result holding the most recent Datastore revision of a given
* EppResource before or at the provided timestamp using the EppResource revisions map, falling
* back to using the earliest revision or the resource as-is if there are no revisions.
* Returns an asynchronous result holding the most recent revision of a given EppResource before
* or at the provided timestamp, falling back to using the resource as-is if there are no
* revisions.
*
* @see #loadAtPointInTime(EppResource, DateTime)
*/
private static <T extends EppResource> Result<T> loadMostRecentRevisionAtTime(
final T resource, final DateTime timestamp) {
if (tm().isOfy()) {
return loadMostRecentRevisionAtTimeDatastore(resource, timestamp);
} else {
return loadMostRecentRevisionAtTimeSql(resource, timestamp);
}
}
/**
* Returns an asynchronous result holding the most recent Datastore revision of a given
* EppResource before or at the provided timestamp using the EppResource revisions map, falling
* back to using the resource as-is if there are no revisions.
*
* @see #loadAtPointInTime(EppResource, DateTime)
*/
private static <T extends EppResource> Result<T> loadMostRecentRevisionAtTimeDatastore(
final T resource, final DateTime timestamp) {
final Key<T> resourceKey = Key.create(resource);
final Key<CommitLogManifest> revision = findMostRecentRevisionAtTime(resource, timestamp);
final Key<CommitLogManifest> revision =
findMostRecentDatastoreRevisionAtTime(resource, timestamp);
if (revision == null) {
logger.atSevere().log("No revision found for %s, falling back to resource.", resourceKey);
return new ResultNow<>(resource);
}
final Result<CommitLogMutation> mutationResult =
ofy().load().key(CommitLogMutation.createKey(revision, resourceKey));
auditedOfy().load().key(CommitLogMutation.createKey(revision, resourceKey));
return () -> {
CommitLogMutation mutation = mutationResult.now();
if (mutation != null) {
return ofy().load().fromEntity(mutation.getEntity());
return auditedOfy().load().fromEntity(mutation.getEntity());
}
logger.atSevere().log(
"Couldn't load mutation for revision at %s for %s, falling back to resource."
@@ -336,9 +375,37 @@ public final class EppResourceUtils {
};
}
/**
* Returns an asynchronous result holding the most recent SQL revision of a given EppResource
* before or at the provided timestamp using *History objects, falling back to using the resource
* as-is if there are no revisions.
*
* @see #loadAtPointInTime(EppResource, DateTime)
*/
@SuppressWarnings("unchecked")
private static <T extends EppResource> Result<T> loadMostRecentRevisionAtTimeSql(
T resource, DateTime timestamp) {
T resourceAtPointInTime =
(T)
HistoryEntryDao.loadHistoryObjectsForResource(
resource.createVKey(), START_OF_TIME, timestamp)
.stream()
.max(Comparator.comparing(HistoryEntry::getModificationTime))
.flatMap(HistoryEntry::getResourceAtPointInTime)
.orElse(null);
if (resourceAtPointInTime == null) {
logger.atSevere().log(
"Couldn't load resource at % for key %s, falling back to resource %s.",
timestamp, resource.createVKey(), resource);
return new ResultNow<>(resource);
}
return new ResultNow<>(resourceAtPointInTime);
}
@Nullable
private static <T extends EppResource> Key<CommitLogManifest>
findMostRecentRevisionAtTime(final T resource, final DateTime timestamp) {
private static <T extends EppResource>
Key<CommitLogManifest> findMostRecentDatastoreRevisionAtTime(
final T resource, final DateTime timestamp) {
final Key<T> resourceKey = Key.create(resource);
Entry<?, Key<CommitLogManifest>> revision = resource.getRevisions().floorEntry(timestamp);
if (revision != null) {
@@ -366,27 +433,26 @@ public final class EppResourceUtils {
*
* @param key the referent key
* @param now the logical time of the check
* @param limit the maximum number of returned keys
* @param limit the maximum number of returned keys, unlimited if null
*/
public static ImmutableSet<VKey<DomainBase>> getLinkedDomainKeys(
VKey<? extends EppResource> key, DateTime now, int limit) {
VKey<? extends EppResource> key, DateTime now, @Nullable Integer limit) {
checkArgument(
key.getKind().equals(ContactResource.class) || key.getKind().equals(HostResource.class),
"key must be either VKey<ContactResource> or VKey<HostResource>, but it is %s",
key);
boolean isContactKey = key.getKind().equals(ContactResource.class);
if (tm().isOfy()) {
return ofy()
.load()
.type(DomainBase.class)
.filter(isContactKey ? "allContacts.contact" : "nsHosts", key.getOfyKey())
.filter("deletionTime >", now)
.limit(limit)
.keys()
.list()
.stream()
.map(DomainBase::createVKey)
.collect(toImmutableSet());
com.googlecode.objectify.cmd.Query<DomainBase> query =
auditedOfy()
.load()
.type(DomainBase.class)
.filter(isContactKey ? "allContacts.contact" : "nsHosts", key.getOfyKey())
.filter("deletionTime >", now);
if (limit != null) {
query.limit(limit);
}
return query.keys().list().stream().map(DomainBase::createVKey).collect(toImmutableSet());
} else {
return tm().transact(
() -> {
@@ -405,11 +471,13 @@ public final class EppResourceUtils {
.setParameter("fkRepoId", key.getSqlKey())
.setParameter("now", now.toDate());
}
if (limit != null) {
query.setMaxResults(limit);
}
@SuppressWarnings("unchecked")
ImmutableSet<VKey<DomainBase>> domainBaseKeySet =
(ImmutableSet<VKey<DomainBase>>)
query
.setMaxResults(limit)
.getResultStream()
.map(
repoId ->

View File

@@ -324,13 +324,22 @@ public abstract class BillingEvent extends ImmutableObject
DateTime syntheticCreationTime;
/**
* For {@link Flag#SYNTHETIC} events, a {@link Key} to the {@link BillingEvent} from which this
* OneTime was created. This is needed in order to properly match billing events against {@link
* Cancellation}s.
* For {@link Flag#SYNTHETIC} events, a {@link Key} to the {@link Recurring} from which this
* {@link OneTime} was created. This is needed in order to properly match billing events against
* {@link Cancellation}s.
*/
@Column(name = "cancellation_matching_billing_recurrence_id")
VKey<Recurring> cancellationMatchingBillingEvent;
/**
* For {@link Flag#SYNTHETIC} events, the {@link DomainHistory} revision ID of the the {@link
* Recurring} from which this {@link OneTime} was created. This is needed in order to recreate
* the {@link VKey} when reading from SQL.
*/
@Ignore
@Column(name = "recurrence_history_revision_id")
Long recurringEventHistoryRevisionId;
/**
* The {@link AllocationToken} used in the creation of this event, or null if one was not used.
*/
@@ -354,10 +363,14 @@ public abstract class BillingEvent extends ImmutableObject
return syntheticCreationTime;
}
public VKey<? extends BillingEvent> getCancellationMatchingBillingEvent() {
public VKey<Recurring> getCancellationMatchingBillingEvent() {
return cancellationMatchingBillingEvent;
}
public Long getRecurringEventHistoryRevisionId() {
return recurringEventHistoryRevisionId;
}
public Optional<VKey<AllocationToken>> getAllocationToken() {
return Optional.ofNullable(allocationToken);
}
@@ -376,6 +389,28 @@ public abstract class BillingEvent extends ImmutableObject
return new Builder(clone(this));
}
@Override
void onLoad() {
super.onLoad();
if (cancellationMatchingBillingEvent != null) {
recurringEventHistoryRevisionId =
cancellationMatchingBillingEvent.getOfyKey().getParent().getId();
}
}
@Override
void postLoad() {
super.postLoad();
if (cancellationMatchingBillingEvent != null) {
cancellationMatchingBillingEvent =
cancellationMatchingBillingEvent.restoreOfy(
DomainBase.class,
domainRepoId,
DomainHistory.class,
recurringEventHistoryRevisionId);
}
}
/** A builder for {@link OneTime} since it is immutable. */
public static class Builder extends BillingEvent.Builder<OneTime, Builder> {
@@ -410,6 +445,8 @@ public abstract class BillingEvent extends ImmutableObject
public Builder setCancellationMatchingBillingEvent(
VKey<Recurring> cancellationMatchingBillingEvent) {
getInstance().cancellationMatchingBillingEvent = cancellationMatchingBillingEvent;
getInstance().recurringEventHistoryRevisionId =
cancellationMatchingBillingEvent.getOfyKey().getParent().getId();
return this;
}
@@ -444,6 +481,11 @@ public abstract class BillingEvent extends ImmutableObject
== (instance.cancellationMatchingBillingEvent != null),
"Cancellation matching billing event must be set if and only if the SYNTHETIC flag "
+ "is set.");
checkState(
!instance.getFlags().contains(Flag.SYNTHETIC)
|| (instance.cancellationMatchingBillingEvent.getOfyKey().getParent().getId()
== instance.recurringEventHistoryRevisionId),
"Cancellation matching billing event and its history revision ID does not match.");
return super.build();
}
}

View File

@@ -0,0 +1,252 @@
// 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.common;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSortedMap;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Mapify;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.common.TimedTransitionProperty.TimeMapper;
import google.registry.model.common.TimedTransitionProperty.TimedTransition;
import google.registry.schema.replay.DatastoreOnlyEntity;
import java.time.Duration;
import org.joda.time.DateTime;
/**
* A wrapper object representing the stage-to-time mapping of the Registry 3.0 Cloud SQL migration.
*
* <p>The entity is stored in Datastore throughout the entire migration so as to have a single point
* of access.
*/
@Entity
@InCrossTld
public class DatabaseMigrationStateSchedule extends CrossTldSingleton
implements DatastoreOnlyEntity {
public enum PrimaryDatabase {
CLOUD_SQL,
DATASTORE
}
public enum ReplayDirection {
NO_REPLAY,
DATASTORE_TO_SQL,
SQL_TO_DATASTORE
}
/**
* The current phase of the migration plus information about which database to use and whether or
* not the phase is read-only.
*/
public enum MigrationState {
DATASTORE_ONLY(PrimaryDatabase.DATASTORE, false, ReplayDirection.NO_REPLAY),
DATASTORE_PRIMARY(PrimaryDatabase.DATASTORE, false, ReplayDirection.DATASTORE_TO_SQL),
DATASTORE_PRIMARY_READ_ONLY(PrimaryDatabase.DATASTORE, true, ReplayDirection.DATASTORE_TO_SQL),
SQL_PRIMARY_READ_ONLY(PrimaryDatabase.CLOUD_SQL, true, ReplayDirection.SQL_TO_DATASTORE),
SQL_PRIMARY(PrimaryDatabase.CLOUD_SQL, false, ReplayDirection.SQL_TO_DATASTORE),
SQL_ONLY(PrimaryDatabase.CLOUD_SQL, false, ReplayDirection.NO_REPLAY);
private final PrimaryDatabase primaryDatabase;
private final boolean isReadOnly;
private final ReplayDirection replayDirection;
public PrimaryDatabase getPrimaryDatabase() {
return primaryDatabase;
}
public boolean isReadOnly() {
return isReadOnly;
}
public ReplayDirection getReplayDirection() {
return replayDirection;
}
MigrationState(
PrimaryDatabase primaryDatabase, boolean isReadOnly, ReplayDirection replayDirection) {
this.primaryDatabase = primaryDatabase;
this.isReadOnly = isReadOnly;
this.replayDirection = replayDirection;
}
}
@Embed
public static class MigrationStateTransition extends TimedTransition<MigrationState> {
private MigrationState migrationState;
@Override
protected MigrationState getValue() {
return migrationState;
}
@Override
protected void setValue(MigrationState migrationState) {
this.migrationState = migrationState;
}
}
/**
* Cache of the current migration schedule. The key is meaningless; this is essentially a memoized
* Supplier that can be reset for testing purposes and after writes.
*/
@VisibleForTesting
public static final LoadingCache<
Class<DatabaseMigrationStateSchedule>,
TimedTransitionProperty<MigrationState, MigrationStateTransition>>
// Each instance should cache the migration schedule for five minutes before reloading
CACHE =
CacheBuilder.newBuilder()
.expireAfterWrite(Duration.ofMinutes(5))
.build(
new CacheLoader<
Class<DatabaseMigrationStateSchedule>,
TimedTransitionProperty<MigrationState, MigrationStateTransition>>() {
@Override
public TimedTransitionProperty<MigrationState, MigrationStateTransition> load(
Class<DatabaseMigrationStateSchedule> unused) {
return DatabaseMigrationStateSchedule.getUncached();
}
});
// Restrictions on the state transitions, e.g. no going from DATASTORE_ONLY to SQL_ONLY
private static final ImmutableMultimap<MigrationState, MigrationState> VALID_STATE_TRANSITIONS =
createValidStateTransitions();
/**
* The valid state transitions. Generally, one can advance the state one step or move backward any
* number of steps, as long as the step we're moving back to has the same primary database as the
* one we're in. Otherwise, we must move to the corresponding READ_ONLY stage first.
*/
private static ImmutableMultimap<MigrationState, MigrationState> createValidStateTransitions() {
ImmutableMultimap.Builder<MigrationState, MigrationState> builder =
new ImmutableMultimap.Builder<MigrationState, MigrationState>()
.put(MigrationState.DATASTORE_ONLY, MigrationState.DATASTORE_PRIMARY)
.putAll(
MigrationState.DATASTORE_PRIMARY,
MigrationState.DATASTORE_ONLY,
MigrationState.DATASTORE_PRIMARY_READ_ONLY)
.putAll(
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
MigrationState.DATASTORE_ONLY,
MigrationState.DATASTORE_PRIMARY,
MigrationState.SQL_PRIMARY_READ_ONLY,
MigrationState.SQL_PRIMARY)
.putAll(
MigrationState.SQL_PRIMARY_READ_ONLY,
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
MigrationState.SQL_PRIMARY)
.putAll(
MigrationState.SQL_PRIMARY,
MigrationState.SQL_PRIMARY_READ_ONLY,
MigrationState.SQL_ONLY)
.putAll(
MigrationState.SQL_ONLY,
MigrationState.SQL_PRIMARY_READ_ONLY,
MigrationState.SQL_PRIMARY);
// In addition, we can always transition from a state to itself (useful when updating the map).
for (MigrationState migrationState : MigrationState.values()) {
builder.put(migrationState, migrationState);
}
return builder.build();
}
// Default map to return if we have never saved any -- only use Datastore.
@VisibleForTesting
public static final TimedTransitionProperty<MigrationState, MigrationStateTransition>
DEFAULT_TRANSITION_MAP =
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(START_OF_TIME, MigrationState.DATASTORE_ONLY),
MigrationStateTransition.class);
@VisibleForTesting
@Mapify(TimeMapper.class)
public TimedTransitionProperty<MigrationState, MigrationStateTransition> migrationTransitions =
TimedTransitionProperty.forMapify(
MigrationState.DATASTORE_ONLY, MigrationStateTransition.class);
// Required for Objectify initialization
private DatabaseMigrationStateSchedule() {}
@VisibleForTesting
DatabaseMigrationStateSchedule(
TimedTransitionProperty<MigrationState, MigrationStateTransition> migrationTransitions) {
this.migrationTransitions = migrationTransitions;
}
/** Sets and persists to Datastore the provided migration transition schedule. */
public static void set(ImmutableSortedMap<DateTime, MigrationState> migrationTransitionMap) {
ofyTm().assertInTransaction();
TimedTransitionProperty<MigrationState, MigrationStateTransition> transitions =
TimedTransitionProperty.make(
migrationTransitionMap,
MigrationStateTransition.class,
VALID_STATE_TRANSITIONS,
"validStateTransitions",
MigrationState.DATASTORE_ONLY,
"migrationTransitionMap must start with DATASTORE_ONLY");
validateTransitionAtCurrentTime(transitions);
ofyTm().put(new DatabaseMigrationStateSchedule(transitions));
CACHE.invalidateAll();
}
/** Loads the currently-set migration schedule from the cache, or the default if none exists. */
public static TimedTransitionProperty<MigrationState, MigrationStateTransition> get() {
return CACHE.getUnchecked(DatabaseMigrationStateSchedule.class);
}
/** Returns the database migration status at the given time. */
public static MigrationState getValueAtTime(DateTime dateTime) {
return get().getValueAtTime(dateTime);
}
/** Loads the currently-set migration schedule from Datastore, or the default if none exists. */
@VisibleForTesting
static TimedTransitionProperty<MigrationState, MigrationStateTransition> getUncached() {
return ofyTm()
.transact(
() ->
ofyTm()
.loadSingleton(DatabaseMigrationStateSchedule.class)
.map(s -> s.migrationTransitions)
.orElse(DEFAULT_TRANSITION_MAP));
}
/**
* A provided map of transitions may be valid by itself (i.e. it shifts states properly, doesn't
* skip states, and doesn't backtrack incorrectly) while still being invalid. In addition to the
* transitions in the map being valid, the single transition from the current map at the current
* time to the new map at the current time time must also be valid.
*/
private static void validateTransitionAtCurrentTime(
TimedTransitionProperty<MigrationState, MigrationStateTransition> newTransitions) {
MigrationState currentValue = getUncached().getValueAtTime(ofyTm().getTransactionTime());
MigrationState nextCurrentValue = newTransitions.getValueAtTime(ofyTm().getTransactionTime());
checkArgument(
VALID_STATE_TRANSITIONS.get(currentValue).contains(nextCurrentValue),
"Cannot transition from current state-as-of-now %s to new state-as-of-now %s",
currentValue,
nextCurrentValue);
}
}

View File

@@ -1,122 +0,0 @@
// 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.common;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.annotation.Entity;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
import google.registry.schema.replay.DatastoreOnlyEntity;
/**
* A wrapper object representing the current stage of the Registry 3.0 Cloud SQL migration.
*
* <p>The entity is stored in Datastore throughout the entire migration so as to have a single point
* of access (avoiding a two-phase commit problem).
*/
@Entity
@InCrossTld
public class DatabaseMigrationStateWrapper extends CrossTldSingleton
implements DatastoreOnlyEntity {
/**
* The current phase of the migration plus information about which database to use and whether or
* not the phase is read-only.
*/
public enum MigrationState {
DATASTORE_ONLY(PrimaryDatabase.DATASTORE, false),
DATASTORE_PRIMARY(PrimaryDatabase.DATASTORE, false),
DATASTORE_PRIMARY_READ_ONLY(PrimaryDatabase.DATASTORE, true),
SQL_PRIMARY(PrimaryDatabase.CLOUD_SQL, false),
SQL_ONLY(PrimaryDatabase.CLOUD_SQL, false);
private final PrimaryDatabase primaryDatabase;
private final boolean readOnly;
public PrimaryDatabase getPrimaryDatabase() {
return primaryDatabase;
}
public boolean isReadOnly() {
return readOnly;
}
MigrationState(PrimaryDatabase primaryDatabase, boolean readOnly) {
this.primaryDatabase = primaryDatabase;
this.readOnly = readOnly;
}
}
private MigrationState migrationState;
// Required for Objectify initialization
private DatabaseMigrationStateWrapper() {}
DatabaseMigrationStateWrapper(MigrationState migrationState) {
this.migrationState = migrationState;
}
// The valid state transitions. Basically, at state N, state N+1 is valid as well as all previous
// states, with one type of exception: when in either of the SQL states, we can only move back
// one step so that we can make sure that any modifications have been replayed back to Datastore.
private static final ImmutableMap<MigrationState, ImmutableSet<MigrationState>>
VALID_STATE_TRANSITIONS =
ImmutableMap.of(
MigrationState.DATASTORE_ONLY,
ImmutableSet.of(MigrationState.DATASTORE_PRIMARY),
MigrationState.DATASTORE_PRIMARY,
ImmutableSet.of(
MigrationState.DATASTORE_ONLY, MigrationState.DATASTORE_PRIMARY_READ_ONLY),
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
ImmutableSet.of(
MigrationState.DATASTORE_ONLY,
MigrationState.DATASTORE_PRIMARY,
MigrationState.SQL_PRIMARY),
MigrationState.SQL_PRIMARY,
ImmutableSet.of(MigrationState.DATASTORE_PRIMARY_READ_ONLY, MigrationState.SQL_ONLY),
MigrationState.SQL_ONLY,
ImmutableSet.of(MigrationState.SQL_PRIMARY));
private static boolean isValidStateTransition(MigrationState from, MigrationState to) {
return VALID_STATE_TRANSITIONS.get(from).contains(to);
}
/** Sets and persists to Datastore the current database migration state. */
public static void set(MigrationState newState) {
MigrationState currentState = get();
checkArgument(
isValidStateTransition(currentState, newState),
"Moving from migration state %s to %s is not a valid transition",
currentState,
newState);
DatabaseMigrationStateWrapper wrapper = new DatabaseMigrationStateWrapper(newState);
ofyTm().transact(() -> ofyTm().put(wrapper));
}
/** Retrieves the current state of the migration (or DATASTORE_ONLY if it hasn't started). */
public static MigrationState get() {
return ofyTm()
.transact(
() ->
ofyTm()
.loadSingleton(DatabaseMigrationStateWrapper.class)
.map(s -> s.migrationState)
.orElse(MigrationState.DATASTORE_ONLY));
}
}

View File

@@ -1,166 +0,0 @@
// 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.common;
import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.config.RegistryConfig.getSingletonCacheRefreshDuration;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableSortedMap;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Mapify;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.ImmutableObject;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.common.TimedTransitionProperty.TimeMapper;
import google.registry.model.common.TimedTransitionProperty.TimedTransition;
import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.ReservedList;
import google.registry.model.smd.SignedMarkRevocationList;
import google.registry.model.tmch.ClaimsList;
import google.registry.persistence.VKey;
import google.registry.schema.replay.DatastoreOnlyEntity;
import java.util.Optional;
import javax.annotation.concurrent.Immutable;
import org.joda.time.DateTime;
@Entity
@Immutable
@InCrossTld
public class DatabaseTransitionSchedule extends ImmutableObject implements DatastoreOnlyEntity {
/**
* The name of the database to be treated as the primary database. The first entry in the schedule
* will always be Datastore.
*/
public enum PrimaryDatabase {
CLOUD_SQL,
DATASTORE
}
/** The id of the transition schedule. */
public enum TransitionId {
/** The schedule for migration of {@link ClaimsList} entities. */
CLAIMS_LIST,
/** The schedule for the migration of {@link PremiumList} and {@link ReservedList}. */
DOMAIN_LABEL_LISTS,
/** The schedule for the migration of the {@link SignedMarkRevocationList} entity. */
SIGNED_MARK_REVOCATION_LIST,
/** The schedule for all asynchronously-replayed entities, ones not dually-written. */
REPLAYED_ENTITIES,
}
/**
* The transition to a specified primary database at a specific point in time, for use in a
* TimedTransitionProperty.
*/
@Embed
public static class PrimaryDatabaseTransition extends TimedTransition<PrimaryDatabase> {
private PrimaryDatabase primaryDatabase;
@Override
protected PrimaryDatabase getValue() {
return primaryDatabase;
}
@Override
protected void setValue(PrimaryDatabase primaryDatabase) {
this.primaryDatabase = primaryDatabase;
}
}
@Parent Key<EntityGroupRoot> parent = getCrossTldKey();
@Id String transitionId;
/** An automatically managed timestamp of when this schedule was last written to Datastore. */
UpdateAutoTimestamp lastUpdateTime = UpdateAutoTimestamp.create(null);
/** A property that tracks the primary database for a dual-read/dual-write database migration. */
@Mapify(TimeMapper.class)
TimedTransitionProperty<PrimaryDatabase, PrimaryDatabaseTransition> databaseTransitions =
TimedTransitionProperty.forMapify(PrimaryDatabase.DATASTORE, PrimaryDatabaseTransition.class);
/** A cache that loads the {@link DatabaseTransitionSchedule} for a given id. */
private static final LoadingCache<TransitionId, Optional<DatabaseTransitionSchedule>> CACHE =
CacheBuilder.newBuilder()
.expireAfterWrite(
java.time.Duration.ofMillis(getSingletonCacheRefreshDuration().getMillis()))
.build(
new CacheLoader<TransitionId, Optional<DatabaseTransitionSchedule>>() {
@Override
public Optional<DatabaseTransitionSchedule> load(TransitionId transitionId) {
return DatabaseTransitionSchedule.get(transitionId);
}
});
public static DatabaseTransitionSchedule create(
TransitionId transitionId,
TimedTransitionProperty<PrimaryDatabase, PrimaryDatabaseTransition> databaseTransitions) {
checkNotNull(transitionId, "Id cannot be null");
checkNotNull(databaseTransitions, "databaseTransitions cannot be null");
databaseTransitions.checkValidity();
DatabaseTransitionSchedule instance = new DatabaseTransitionSchedule();
instance.transitionId = transitionId.name();
instance.databaseTransitions = databaseTransitions;
return instance;
}
/** Returns the database that is indicated as primary at the given time. */
public PrimaryDatabase getPrimaryDatabase() {
return databaseTransitions.getValueAtTime(tm().getTransactionTime());
}
/** Returns the database transitions as a map of start time to primary database. */
public ImmutableSortedMap<DateTime, PrimaryDatabase> getDatabaseTransitions() {
return databaseTransitions.toValueMap();
}
/**
* Returns the current cached schedule for the given id.
*
* <p>WARNING: The schedule returned by this method could be up to 10 minutes out of date.
*/
public static Optional<DatabaseTransitionSchedule> getCached(TransitionId id) {
return CACHE.getUnchecked(id);
}
/** Returns the schedule for a given id. */
public static Optional<DatabaseTransitionSchedule> get(TransitionId transitionId) {
VKey<DatabaseTransitionSchedule> key =
VKey.create(
DatabaseTransitionSchedule.class,
transitionId,
Key.create(getCrossTldKey(), DatabaseTransitionSchedule.class, transitionId.name()));
return ofyTm().transact(() -> ofyTm().loadByKeyIfPresent(key));
}
@Override
public String toString() {
return String.format(
"%s(last updated at %s): %s",
transitionId, lastUpdateTime.getTimestamp(), databaseTransitions.toValueMap());
}
}

View File

@@ -18,6 +18,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
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.contact.ContactHistory.ContactHistoryId;
import google.registry.model.reporting.HistoryEntry;
@@ -107,6 +108,11 @@ public class ContactHistory extends HistoryEntry implements SqlEntity {
return (VKey<ContactHistory>) createVKey(Key.create(this));
}
@Override
public Optional<? extends EppResource> getResourceAtPointInTime() {
return getContactBase();
}
@PostLoad
void postLoad() {
// Normally Hibernate would see that the contact fields are all null and would fill contactBase
@@ -115,7 +121,10 @@ public class ContactHistory extends HistoryEntry implements SqlEntity {
contactBase = null;
}
if (contactBase != null && contactBase.getRepoId() == null) {
contactBase = contactBase.asBuilder().setRepoId(parent.getName()).build();
// contactBase hasn't been fully constructed yet, so it's ok to go in and mutate it. Though
// the use of the Builder is not necessarily problematic in this case, this is still safer as
// the Builder can do things like comparisons that compute the hash code.
contactBase.setRepoId(parent.getName());
}
}

View File

@@ -21,9 +21,11 @@ import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.common.collect.ImmutableSet;
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.domain.DomainHistory.DomainHistoryId;
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;
@@ -248,17 +250,31 @@ public class DomainHistory extends HistoryEntry implements SqlEntity {
return (VKey<DomainHistory>) createVKey(Key.create(this));
}
@Override
public Optional<? extends EppResource> getResourceAtPointInTime() {
return getDomainContent();
}
@PostLoad
void postLoad() {
if (domainContent != null) {
domainContent.nsHosts = nullToEmptyImmutableCopy(nsHosts);
domainContent.gracePeriods =
gracePeriodHistories.stream()
.map(GracePeriod::createFromHistory)
.collect(toImmutableSet());
domainContent.dsData =
dsDataHistories.stream().map(DelegationSignerData::create).collect(toImmutableSet());
// Normally Hibernate would see that the domain fields are all null and would fill
// domainContent with a null object. Unfortunately, the updateTimestamp is never null in SQL.
if (domainContent.getDomainName() == null) {
domainContent = null;
} else {
if (domainContent.getRepoId() == null) {
domainContent = domainContent.asBuilder().setRepoId(parent.getName()).build();
// domainContent still hasn't been fully constructed yet, so it's ok to go in and mutate
// it. In fact, we have to because going through the builder causes the hash codes of
// contained objects to be calculated prematurely.
domainContent.setRepoId(parent.getName());
}
}
}

View File

@@ -115,6 +115,17 @@ public class GracePeriod extends GracePeriodBase implements DatastoreAndSqlEntit
type, domainRepoId, expirationTime, clientId, billingEventOneTime, null, gracePeriodId);
}
public static GracePeriod createFromHistory(GracePeriodHistory history) {
return createInternal(
history.type,
history.domainRepoId,
history.expirationTime,
history.clientId,
history.billingEventOneTime == null ? null : history.billingEventOneTime.createVKey(),
history.billingEventRecurring == null ? null : history.billingEventRecurring.createVKey(),
history.gracePeriodId);
}
/** Creates a GracePeriod for a Recurring billing event. */
public static GracePeriod createForRecurring(
GracePeriodStatus type,

View File

@@ -114,6 +114,15 @@ public class DelegationSignerData extends DomainDsDataBase {
return create(keyTag, algorithm, digestType, DatatypeConverter.parseHexBinary(digestAsHex));
}
public static DelegationSignerData create(DomainDsDataHistory history) {
return create(
history.keyTag,
history.algorithm,
history.digestType,
history.digest,
history.domainRepoId);
}
/** Class to represent the composite primary key of {@link DelegationSignerData} entity. */
static class DomainDsDataId extends ImmutableObject implements Serializable {

View File

@@ -18,6 +18,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
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.host.HostHistory.HostHistoryId;
import google.registry.model.reporting.HistoryEntry;
@@ -108,6 +109,11 @@ public class HostHistory extends HistoryEntry implements SqlEntity {
return (VKey<HostHistory>) createVKey(Key.create(this));
}
@Override
public Optional<? extends EppResource> getResourceAtPointInTime() {
return getHostBase();
}
@PostLoad
void postLoad() {
// Normally Hibernate would see that the host fields are all null and would fill hostBase
@@ -116,7 +122,10 @@ public class HostHistory extends HistoryEntry implements SqlEntity {
hostBase = null;
}
if (hostBase != null && hostBase.getRepoId() == null) {
hostBase = hostBase.asBuilder().setRepoId(parent.getName()).build();
// hostBase hasn't been fully constructed yet, so it's ok to go in and mutate it. Though the
// use of the Builder is not necessarily problematic in this case, this is still safer as the
// Builder can do things like comparisons that compute the hash code.
hostBase.setRepoId(parent.getName());
}
}

View File

@@ -228,8 +228,7 @@ public abstract class ForeignKeyIndex<E extends EppResource> extends BackupGroup
tm().transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
.query(
CriteriaQueryBuilder.create(clazz)
.whereFieldIsIn(property, foreignKeys)
.build())

View File

@@ -480,7 +480,13 @@ public class DatastoreTransactionManager implements TransactionManager {
@Override
public long count() {
return buildQuery().count();
// Objectify provides a count() function, but unfortunately that doesn't work if there are
// more than 1000 (the default response page size?) entries in the result set. We also use
// chunkAll() here as it provides a nice performance boost.
//
// There is some information on this issue on SO, see:
// https://stackoverflow.com/questions/751124/how-does-one-get-a-count-of-rows-in-a-datastore-model-in-google-app-engine
return Iterables.size(buildQuery().chunkAll().keys());
}
@Override

View File

@@ -51,7 +51,7 @@ import google.registry.model.translators.VKeyTranslatorFactory;
import java.util.concurrent.atomic.AtomicLong;
/**
* An instance of Ofy, obtained via {@code #ofy()}, should be used to access all persistable
* An instance of Ofy, obtained via {@code #auditedOfy()}, should be used to access all persistable
* objects. The class contains a static initializer to call factory().register(...) on all
* persistable objects in this package.
*/

View File

@@ -61,6 +61,7 @@ import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.MappedSuperclass;
import javax.persistence.Transient;
import org.apache.commons.lang3.BooleanUtils;
import org.joda.time.DateTime;
/**
@@ -289,6 +290,17 @@ public class HistoryEntry extends ImmutableObject implements Buildable, Datastor
return nullToEmptyImmutableCopy(domainTransactionRecords);
}
/**
* Throws an error when attempting to retrieve the EppResource at this point in time.
*
* <p>Subclasses must override this to return the resource; it is non-abstract for legacy reasons
* and objects created prior to the Registry 3.0 migration.
*/
public Optional<? extends EppResource> getResourceAtPointInTime() {
throw new UnsupportedOperationException(
"Raw HistoryEntry objects do not store the resource at that point in time.");
}
/** This method exists solely to satisfy Hibernate. Use the {@link Builder} instead. */
@SuppressWarnings("UnusedMethod")
private void setPeriod(Period period) {
@@ -435,7 +447,8 @@ public class HistoryEntry extends ImmutableObject implements Buildable, Datastor
checkArgumentNotNull(getInstance().modificationTime, "Modification time must be specified");
checkArgumentNotNull(getInstance().clientId, "Registrar ID must be specified");
checkArgument(
!getInstance().type.equals(Type.SYNTHETIC) || !getInstance().requestedByRegistrar,
!getInstance().type.equals(Type.SYNTHETIC)
|| BooleanUtils.isNotTrue(getInstance().requestedByRegistrar),
"Synthetic history entries cannot be requested by a registrar");
return super.build();
}

View File

@@ -164,8 +164,7 @@ public class HistoryEntryDao {
private static <T extends HistoryEntry> Stream<T> loadHistoryObjectFromSqlByRegistrars(
Class<T> historyClass, ImmutableCollection<String> registrarIds) {
return jpaTm()
.getEntityManager()
.createQuery(
.query(
CriteriaQueryBuilder.create(historyClass)
.whereFieldIsIn("clientId", registrarIds)
.build())
@@ -184,11 +183,12 @@ public class HistoryEntryDao {
.where("modificationTime", criteriaBuilder::greaterThanOrEqualTo, afterTime)
.where("modificationTime", criteriaBuilder::lessThanOrEqualTo, beforeTime)
.where(repoIdFieldName, criteriaBuilder::equal, parentKey.getSqlKey().toString())
.orderByAsc("id")
.build();
return ImmutableList.sortedCopyOf(
Comparator.comparing(HistoryEntry::getModificationTime),
jpaTm().getEntityManager().createQuery(criteriaQuery).getResultList());
jpaTm().query(criteriaQuery).getResultList());
}
private static Class<? extends HistoryEntry> getHistoryClassFromParent(
@@ -215,8 +215,7 @@ public class HistoryEntryDao {
Class<T> historyClass, DateTime afterTime, DateTime beforeTime) {
CriteriaBuilder criteriaBuilder = jpaTm().getEntityManager().getCriteriaBuilder();
return jpaTm()
.getEntityManager()
.createQuery(
.query(
CriteriaQueryBuilder.create(historyClass)
.where("modificationTime", criteriaBuilder::greaterThanOrEqualTo, afterTime)
.where("modificationTime", criteriaBuilder::lessThanOrEqualTo, beforeTime)

View File

@@ -164,7 +164,6 @@ public class DomainTransferData extends TransferData<DomainTransferData.Builder>
serverApproveEntities = null;
postLoad();
}
hashCode = null; // reset the hash code since we may have changed the entities
}
/**

View File

@@ -19,6 +19,7 @@ import java.util.function.Supplier;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery;
/** Sub-interface of {@link TransactionManager} which defines JPA related methods. */
public interface JpaTransactionManager extends TransactionManager {
@@ -34,11 +35,18 @@ public interface JpaTransactionManager extends TransactionManager {
*/
<T> TypedQuery<T> query(String sqlString, Class<T> resultClass);
/** Creates a JPA SQU query for the given criteria query. */
<T> TypedQuery<T> query(CriteriaQuery<T> criteriaQuery);
/**
* Creates a JPA SQL query for the given query string (which does not return results).
* Creates a JPA SQL query for the given query string.
*
* <p>This is a convenience method for the longer <code>
* jpaTm().getEntityManager().createQuery(...)</code>.
*
* <p>Note that while this method can legally be used for queries that return results, <u>it
* should not be</u>, as it does not correctly detach entities as must be done for nomulus model
* objects.
*/
Query query(String sqlString);

View File

@@ -23,6 +23,7 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static java.util.AbstractMap.SimpleEntry;
import static java.util.stream.Collectors.joining;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -43,8 +44,11 @@ import google.registry.persistence.VKey;
import google.registry.util.Clock;
import google.registry.util.Retrier;
import google.registry.util.SystemSleeper;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
@@ -58,9 +62,14 @@ import javax.annotation.Nullable;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Parameter;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.persistence.TemporalType;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.SingularAttribute;
import org.joda.time.DateTime;
@@ -116,7 +125,12 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public <T> TypedQuery<T> query(String sqlString, Class<T> resultClass) {
return getEntityManager().createQuery(sqlString, resultClass);
return new DetachingTypedQuery(getEntityManager().createQuery(sqlString, resultClass));
}
@Override
public <T> TypedQuery<T> query(CriteriaQuery<T> criteriaQuery) {
return new DetachingTypedQuery(getEntityManager().createQuery(criteriaQuery));
}
@Override
@@ -496,7 +510,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
elements.size() <= 1,
"Expected at most one entity of type %s, found at least two",
clazz.getSimpleName());
return elements.stream().findFirst();
return elements.stream().findFirst().map(this::detach);
}
private int internalDelete(VKey<?> key) {
@@ -665,6 +679,33 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
}
@Nullable
private <T> T detachIfEntity(@Nullable T object) {
if (object == null) {
return null;
}
// Check if the object is an array, if so we'll want to recurse through the elements.
if (object.getClass().isArray()) {
for (int i = 0; i < Array.getLength(object); ++i) {
detachIfEntity(Array.get(object, i));
}
return object;
}
// Check to see if it is an entity (queries can return raw column values or counts, so this
// could be String, Long, ...).
try {
getEntityManager().getMetamodel().entity(object.getClass());
} catch (IllegalArgumentException e) {
// The object is not an entity. Return without detaching.
return object;
}
// At this point, object must be an entity.
return detach(object);
}
/** Detach the entity, suitable for use in Optional.map(). */
@Nullable
private <T> T detach(@Nullable T entity) {
@@ -758,6 +799,207 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
}
/**
* Typed query wrapper that applies a transform to all result objects.
*
* <p>This is used to detach objects upon load.
*/
@VisibleForTesting
class DetachingTypedQuery<T> implements TypedQuery<T> {
TypedQuery<T> delegate;
public DetachingTypedQuery(TypedQuery<T> delegate) {
this.delegate = delegate;
}
@Override
public List<T> getResultList() {
return delegate
.getResultStream()
.map(JpaTransactionManagerImpl.this::detachIfEntity)
.collect(toImmutableList());
}
@Override
public Stream<T> getResultStream() {
return delegate.getResultStream().map(JpaTransactionManagerImpl.this::detachIfEntity);
}
@Override
public T getSingleResult() {
return detachIfEntity(delegate.getSingleResult());
}
@Override
public TypedQuery<T> setMaxResults(int maxResults) {
delegate.setMaxResults(maxResults);
return this;
}
@Override
public TypedQuery<T> setFirstResult(int startPosition) {
delegate.setFirstResult(startPosition);
return this;
}
@Override
public TypedQuery<T> setHint(String hintName, Object value) {
delegate.setHint(hintName, value);
return this;
}
@Override
public <U> TypedQuery<T> setParameter(Parameter<U> param, U value) {
delegate.setParameter(param, value);
return this;
}
@Override
public TypedQuery<T> setParameter(
Parameter<Calendar> param, Calendar value, TemporalType temporalType) {
delegate.setParameter(param, value, temporalType);
return this;
}
@Override
public TypedQuery<T> setParameter(
Parameter<Date> param, Date value, TemporalType temporalType) {
delegate.setParameter(param, value, temporalType);
return this;
}
@Override
public TypedQuery<T> setParameter(String name, Object value) {
delegate.setParameter(name, value);
return this;
}
@Override
public TypedQuery<T> setParameter(String name, Calendar value, TemporalType temporalType) {
delegate.setParameter(name, value, temporalType);
return this;
}
@Override
public TypedQuery<T> setParameter(String name, Date value, TemporalType temporalType) {
delegate.setParameter(name, value, temporalType);
return this;
}
@Override
public TypedQuery<T> setParameter(int position, Object value) {
delegate.setParameter(position, value);
return this;
}
@Override
public TypedQuery<T> setParameter(int position, Calendar value, TemporalType temporalType) {
delegate.setParameter(position, value, temporalType);
return this;
}
@Override
public TypedQuery<T> setParameter(int position, Date value, TemporalType temporalType) {
delegate.setParameter(position, value, temporalType);
return this;
}
@Override
public TypedQuery<T> setFlushMode(FlushModeType flushMode) {
delegate.setFlushMode(flushMode);
return this;
}
@Override
public TypedQuery<T> setLockMode(LockModeType lockMode) {
delegate.setLockMode(lockMode);
return this;
}
// Query interface
@Override
public int executeUpdate() {
return delegate.executeUpdate();
}
@Override
public int getMaxResults() {
return delegate.getMaxResults();
}
@Override
public int getFirstResult() {
return delegate.getFirstResult();
}
@Override
public Map<String, Object> getHints() {
return delegate.getHints();
}
@Override
public Set<Parameter<?>> getParameters() {
return delegate.getParameters();
}
@Override
public Parameter<?> getParameter(String name) {
return delegate.getParameter(name);
}
@Override
public <U> Parameter<U> getParameter(String name, Class<U> type) {
return delegate.getParameter(name, type);
}
@Override
public Parameter<?> getParameter(int position) {
return delegate.getParameter(position);
}
@Override
public <U> Parameter<U> getParameter(int position, Class<U> type) {
return delegate.getParameter(position, type);
}
@Override
public boolean isBound(Parameter<?> param) {
return delegate.isBound(param);
}
@Override
public <U> U getParameterValue(Parameter<U> param) {
return delegate.getParameterValue(param);
}
@Override
public Object getParameterValue(String name) {
return delegate.getParameterValue(name);
}
@Override
public Object getParameterValue(int position) {
return delegate.getParameterValue(position);
}
@Override
public FlushModeType getFlushMode() {
return delegate.getFlushMode();
}
@Override
public LockModeType getLockMode() {
return delegate.getLockMode();
}
@Override
public <U> U unwrap(Class<U> cls) {
return delegate.unwrap(cls);
}
}
private class JpaQueryComposerImpl<T> extends QueryComposer<T> {
private static final int DEFAULT_FETCH_SIZE = 1000;

View File

@@ -17,7 +17,6 @@ package google.registry.rdap;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Actions.getPathForAction;
import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
@@ -29,10 +28,7 @@ import com.google.common.net.MediaType;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.DatabaseMigrationUtils;
import google.registry.model.EppResource;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
import google.registry.model.registrar.Registrar;
import google.registry.rdap.RdapMetrics.EndpointType;
import google.registry.rdap.RdapObjectClasses.ErrorResponse;
@@ -261,10 +257,4 @@ public abstract class RdapActionBase implements Runnable {
return rdapJsonFormatter.getRequestTime();
}
static boolean isDatastore() {
return tm().transact(
() ->
DatabaseMigrationUtils.getPrimaryDatabase(TransitionId.REPLAYED_ENTITIES)
.equals(PrimaryDatabase.DATASTORE));
}
}

View File

@@ -19,6 +19,7 @@ import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
@@ -204,7 +205,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
// need it.
int querySizeLimit = RESULT_SET_SIZE_SCALING_FACTOR * rdapResultSetMaxSize;
RdapResultSet<DomainBase> resultSet;
if (isDatastore()) {
if (tm().isOfy()) {
Query<DomainBase> query =
auditedOfy()
.load()
@@ -260,7 +261,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
// Don't use queryItems, because it doesn't handle pending deletes.
int querySizeLimit = RESULT_SET_SIZE_SCALING_FACTOR * rdapResultSetMaxSize;
RdapResultSet<DomainBase> resultSet;
if (isDatastore()) {
if (tm().isOfy()) {
Query<DomainBase> query = auditedOfy().load().type(DomainBase.class).filter("tld", tld);
if (cursorString.isPresent()) {
query = query.filter("fullyQualifiedDomainName >", cursorString.get());
@@ -337,7 +338,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
// incomplete result set if a search asks for something like "ns*", but we need to enforce a
// limit in order to avoid arbitrarily long-running queries.
Optional<String> desiredRegistrar = getDesiredRegistrar();
if (isDatastore()) {
if (tm().isOfy()) {
Query<HostResource> query =
queryItems(
HostResource.class,
@@ -472,7 +473,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
private DomainSearchResponse searchByNameserverIp(final InetAddress inetAddress) {
Optional<String> desiredRegistrar = getDesiredRegistrar();
ImmutableSet<VKey<HostResource>> hostKeys;
if (isDatastore()) {
if (tm().isOfy()) {
Query<HostResource> query =
queryItems(
HostResource.class,
@@ -544,7 +545,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
int numHostKeysSearched = 0;
for (List<VKey<HostResource>> chunk : Iterables.partition(hostKeys, 30)) {
numHostKeysSearched += chunk.size();
if (isDatastore()) {
if (tm().isOfy()) {
Query<DomainBase> query =
auditedOfy()
.load()
@@ -588,8 +589,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
cursorString.get());
}
jpaTm()
.getEntityManager()
.createQuery(queryBuilder.build())
.query(queryBuilder.build())
.getResultStream()
.filter(this::isAuthorized)
.forEach(

View File

@@ -262,7 +262,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
|| (cursorType == CursorType.REGISTRAR)) {
resultSet = RdapResultSet.create(ImmutableList.of());
} else {
if (isDatastore()) {
if (tm().isOfy()) {
Query<ContactResource> query =
queryItems(
ContactResource.class,
@@ -386,7 +386,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
if (subtype == Subtype.REGISTRARS) {
contactResultSet = RdapResultSet.create(ImmutableList.of());
} else {
if (isDatastore()) {
if (tm().isOfy()) {
contactResultSet =
getMatchingResources(
queryItemsByKey(

View File

@@ -16,6 +16,7 @@ package google.registry.rdap;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
@@ -220,7 +221,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
private NameserverSearchResponse searchByNameUsingPrefix(RdapSearchPattern partialStringQuery) {
// Add 1 so we can detect truncation.
int querySizeLimit = getStandardQuerySizeLimit();
if (isDatastore()) {
if (tm().isOfy()) {
Query<HostResource> query =
queryItems(
HostResource.class,
@@ -254,7 +255,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
// Add 1 so we can detect truncation.
int querySizeLimit = getStandardQuerySizeLimit();
RdapResultSet<HostResource> rdapResultSet;
if (isDatastore()) {
if (tm().isOfy()) {
Query<HostResource> query =
queryItems(
HostResource.class,

View File

@@ -202,11 +202,7 @@ public abstract class RdapSearchActionBase extends RdapActionBase {
desiredRegistrar.get());
}
List<T> queryResult =
jpaTm()
.getEntityManager()
.createQuery(builder.build())
.setMaxResults(querySizeLimit)
.getResultList();
jpaTm().query(builder.build()).setMaxResults(querySizeLimit).getResultList();
if (checkForVisibility) {
return filterResourcesByVisibility(queryResult, querySizeLimit);
} else {

View File

@@ -14,6 +14,7 @@
package google.registry.reporting;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.RequestParameters.extractOptionalParameter;
import static google.registry.request.RequestParameters.extractRequiredParameter;
@@ -55,6 +56,9 @@ public class ReportingModule {
/** The request parameter specifying the jobId for a running Dataflow pipeline. */
public static final String PARAM_JOB_ID = "jobId";
/** The request parameter for specifying which database reporting actions should read from. */
public static final String DATABASE = "database";
/** Provides the Cloud Dataflow jobId for a pipeline. */
@Provides
@Parameter(PARAM_JOB_ID)
@@ -62,6 +66,14 @@ public class ReportingModule {
return extractRequiredParameter(req, PARAM_JOB_ID);
}
/** Provides the database for the pipeline to read from. */
@Provides
@Parameter(DATABASE)
static String provideDatabase(HttpServletRequest req) {
Optional<String> optionalDatabase = extractOptionalParameter(req, DATABASE);
return optionalDatabase.orElse(tm().isOfy() ? "DATASTORE" : "CLOUD_SQL");
}
/** Extracts an optional YearMonth in yyyy-MM format from the request. */
@Provides
@Parameter(PARAM_YEAR_MONTH)

View File

@@ -15,6 +15,8 @@
package google.registry.reporting.spec11;
import static google.registry.beam.BeamUtils.createJobName;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.reporting.ReportingModule.DATABASE;
import static google.registry.reporting.ReportingModule.PARAM_DATE;
import static google.registry.reporting.ReportingUtils.enqueueBeamReportingTask;
import static google.registry.request.Action.Method.POST;
@@ -69,6 +71,7 @@ public class GenerateSpec11ReportAction implements Runnable {
private final Clock clock;
private final Response response;
private final Dataflow dataflow;
private final String database;
@Inject
GenerateSpec11ReportAction(
@@ -78,15 +81,20 @@ public class GenerateSpec11ReportAction implements Runnable {
@Config("reportingBucketUrl") String reportingBucketUrl,
@Key("safeBrowsingAPIKey") String apiKey,
@Parameter(PARAM_DATE) LocalDate date,
@Parameter(DATABASE) String database,
Clock clock,
Response response,
Dataflow dataflow) {
this.projectId = projectId;
this.jobRegion = jobRegion;
this.stagingBucketUrl = stagingBucketUrl;
if (tm().isOfy() && database.equals("CLOUD_SQL")) {
reportingBucketUrl = reportingBucketUrl.concat("-sql");
}
this.reportingBucketUrl = reportingBucketUrl;
this.apiKey = apiKey;
this.date = date;
this.database = database;
this.clock = clock;
this.response = response;
this.dataflow = dataflow;
@@ -105,6 +113,8 @@ public class GenerateSpec11ReportAction implements Runnable {
ImmutableMap.of(
"safeBrowsingApiKey",
apiKey,
"database",
database,
ReportingModule.PARAM_DATE,
date.toString(),
"reportingBucketUrl",

View File

@@ -22,12 +22,17 @@ import static google.registry.request.Action.Method.GET;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import google.registry.model.common.DatabaseMigrationStateSchedule.ReplayDirection;
import google.registry.persistence.transaction.Transaction;
import google.registry.persistence.transaction.TransactionEntity;
import google.registry.request.Action;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import java.io.IOException;
import java.util.List;
import javax.inject.Inject;
import javax.persistence.NoResultException;
/** Cron task to replicate from Cloud SQL to datastore. */
@@ -48,6 +53,13 @@ class ReplicateToDatastoreAction implements Runnable {
*/
public static final int BATCH_SIZE = 200;
private final Clock clock;
@Inject
public ReplicateToDatastoreAction(Clock clock) {
this.clock = clock;
}
@VisibleForTesting
List<TransactionEntity> getTransactionBatch() {
// Get the next batch of transactions that we haven't replicated.
@@ -59,7 +71,8 @@ class ReplicateToDatastoreAction implements Runnable {
jpaTm()
.query(
"SELECT txn FROM TransactionEntity txn WHERE id >"
+ " :lastId ORDER BY id")
+ " :lastId ORDER BY id",
TransactionEntity.class)
.setParameter("lastId", lastSqlTxnBeforeBatch.getTransactionId())
.setMaxResults(BATCH_SIZE)
.getResultList());
@@ -69,7 +82,7 @@ class ReplicateToDatastoreAction implements Runnable {
}
/**
* Apply a transaction to datastore, returns true if there was a fatal error and the batch should
* Apply a transaction to Datastore, returns true if there was a fatal error and the batch should
* be aborted.
*/
@VisibleForTesting
@@ -122,6 +135,13 @@ class ReplicateToDatastoreAction implements Runnable {
@Override
public void run() {
MigrationState state = DatabaseMigrationStateSchedule.getValueAtTime(clock.nowUtc());
if (!state.getReplayDirection().equals(ReplayDirection.SQL_TO_DATASTORE)) {
logger.atInfo().log(
String.format(
"Skipping ReplicateToDatastoreAction because we are in migration phase %s.", state));
return;
}
// TODO(b/181758163): Deal with objects that don't exist in Cloud SQL, e.g. ForeignKeyIndex,
// EppResourceIndex.
logger.atInfo().log("Processing transaction replay batch Cloud SQL -> Cloud Datastore");

View File

@@ -80,8 +80,7 @@ final class CreateDomainCommand extends CreateOrUpdateDomainCommand
}
setSoyTemplate(DomainCreateSoyInfo.getInstance(), DomainCreateSoyInfo.DOMAINCREATE);
addSoyRecord(
clientId,
SoyMapData soyMapData =
new SoyMapData(
"domain", domain,
"period", period,
@@ -92,7 +91,12 @@ final class CreateDomainCommand extends CreateOrUpdateDomainCommand
"password", password,
"currency", currency,
"price", cost,
"dsRecords", DsRecord.convertToSoy(dsRecords)));
"dsRecords", DsRecord.convertToSoy(dsRecords),
"reason", reason);
if (requestedByRegistrar != null) {
soyMapData.put("requestedByRegistrar", requestedByRegistrar.toString());
}
addSoyRecord(clientId, soyMapData);
}
}
}

View File

@@ -78,6 +78,17 @@ abstract class CreateOrUpdateDomainCommand extends MutatingEppToolCommand {
converter = DsRecord.Converter.class)
List<DsRecord> dsRecords = new ArrayList<>();
@Parameter(
names = {"--reason"},
description = "Reason for the change.")
String reason;
@Parameter(
names = {"--registrar_request"},
description = "Whether the change was requested by a registrar.",
arity = 1)
Boolean requestedByRegistrar;
Set<String> domains;
@Override

View File

@@ -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.tools;
import com.beust.jcommander.Parameters;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationStateTransition;
import google.registry.model.common.TimedTransitionProperty;
/** A command to check the current Registry 3.0 migration state of the database. */
@Parameters(separators = " =", commandDescription = "Check current Registry 3.0 migration state")
public class GetDatabaseMigrationStateCommand implements CommandWithRemoteApi {
@Override
public void run() throws Exception {
TimedTransitionProperty<MigrationState, MigrationStateTransition> migrationSchedule =
DatabaseMigrationStateSchedule.get();
System.out.println(
String.format("Current migration schedule: %s", migrationSchedule.toValueMap()));
}
}

View File

@@ -1,44 +0,0 @@
// 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.tools;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import google.registry.model.common.DatabaseTransitionSchedule;
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
import java.util.List;
/** Command to show the {@link DatabaseTransitionSchedule} for a transition id. */
@Parameters(separators = " =", commandDescription = "Show database transition schedule")
final class GetDatabaseTransitionScheduleCommand implements CommandWithRemoteApi {
@Parameter(description = "Transition id(s) for the schedules to get", required = true)
private List<TransitionId> mainParameters;
@Override
public void run() {
for (TransitionId transitionId : mainParameters) {
DatabaseTransitionSchedule schedule =
checkArgumentPresent(
DatabaseTransitionSchedule.get(transitionId),
"A database transition schedule for %s does not exist",
transitionId);
System.out.println(schedule);
}
}
}

View File

@@ -69,7 +69,7 @@ public final class RegistryTool {
.put("get_allocation_token", GetAllocationTokenCommand.class)
.put("get_claims_list", GetClaimsListCommand.class)
.put("get_contact", GetContactCommand.class)
.put("get_database_transition_schedule", GetDatabaseTransitionScheduleCommand.class)
.put("get_database_migration_state", GetDatabaseMigrationStateCommand.class)
.put("get_domain", GetDomainCommand.class)
.put("get_history_entries", GetHistoryEntriesCommand.class)
.put("get_host", GetHostCommand.class)
@@ -111,7 +111,7 @@ public final class RegistryTool {
.put("resave_epp_resource", ResaveEppResourceCommand.class)
.put("save_sql_credential", SaveSqlCredentialCommand.class)
.put("send_escrow_report_to_icann", SendEscrowReportToIcannCommand.class)
.put("set_database_transition_schedule", SetDatabaseTransitionScheduleCommand.class)
.put("set_database_migration_state", SetDatabaseMigrationStateCommand.class)
.put("set_num_instances", SetNumInstancesCommand.class)
.put("set_sql_replay_checkpoint", SetSqlReplayCheckpointCommand.class)
.put("setup_ote", SetupOteCommand.class)

View File

@@ -0,0 +1,70 @@
// 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.tools;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import google.registry.tools.params.TransitionListParameter.MigrationStateTransitions;
import org.joda.time.DateTime;
/** Command to set the Registry 3.0 database migration state schedule. */
@Parameters(
separators = " =",
commandDescription = "Set the current database migration state schedule.")
public class SetDatabaseMigrationStateCommand extends ConfirmingCommand
implements CommandWithRemoteApi {
private static final String WARNING_MESSAGE =
"Attempting to change the schedule with an effect that would take place within the next 10 "
+ "minutes. The cache expiration duration is 5 minutes so this MAY BE DANGEROUS.\n";
@Parameter(
names = "--migration_schedule",
converter = MigrationStateTransitions.class,
validateWith = MigrationStateTransitions.class,
required = true,
description =
"Comma-delimited list of database transitions, of the form"
+ " <time>=<migration-state>[,<time>=<migration-state>]*")
ImmutableSortedMap<DateTime, MigrationState> transitionSchedule;
@Override
protected String prompt() {
return ofyTm()
.transact(
() -> {
StringBuilder result = new StringBuilder();
DateTime now = ofyTm().getTransactionTime();
DateTime nextTransition = transitionSchedule.ceilingKey(now);
if (nextTransition != null && nextTransition.isBefore(now.plusMinutes(10))) {
result.append(WARNING_MESSAGE);
}
return result
.append(String.format("Set new migration state schedule %s?", transitionSchedule))
.toString();
});
}
@Override
protected String execute() {
ofyTm().transact(() -> DatabaseMigrationStateSchedule.set(transitionSchedule));
return String.format("Successfully set new migration state schedule %s", transitionSchedule);
}
}

View File

@@ -1,69 +0,0 @@
// 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.tools;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.common.DatabaseTransitionSchedule;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabaseTransition;
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.tools.params.TransitionListParameter.PrimaryDatabaseTransitions;
import org.joda.time.DateTime;
/** Command to update {@link DatabaseTransitionSchedule}. */
@Parameters(
separators = " =",
commandDescription = "Set the database transition schedule for transition id.")
public class SetDatabaseTransitionScheduleCommand extends ConfirmingCommand
implements CommandWithRemoteApi {
@Parameter(
names = "--transition_schedule",
converter = PrimaryDatabaseTransitions.class,
validateWith = PrimaryDatabaseTransitions.class,
description =
"Comma-delimited list of database transitions, of the form"
+ " <time>=<primary-database>[,<time>=<primary-database>]*")
ImmutableSortedMap<DateTime, PrimaryDatabase> transitionSchedule;
@Parameter(
names = "--transition_id",
required = true,
description = "Transition id string for the schedule being updated")
private TransitionId transitionId;
@Override
protected String prompt() {
return String.format(
"Insert new schedule %s for transition ID %s?", transitionSchedule, transitionId);
}
@Override
protected String execute() {
DatabaseTransitionSchedule newSchedule =
DatabaseTransitionSchedule.create(
transitionId,
TimedTransitionProperty.fromValueMap(
transitionSchedule, PrimaryDatabaseTransition.class));
ofyTm().transact(() -> ofyTm().put(newSchedule));
return String.format(
"Inserted new schedule %s for transition ID %s.", transitionSchedule, transitionId);
}
}

View File

@@ -315,10 +315,14 @@ final class UpdateDomainCommand extends CreateOrUpdateDomainCommand {
"secdns", secDns,
"addDsRecords", DsRecord.convertToSoy(addDsRecords),
"removeDsRecords", DsRecord.convertToSoy(removeDsRecords),
"removeAllDsRecords", clearDsRecords);
"removeAllDsRecords", clearDsRecords,
"reason", reason);
if (autorenews != null) {
soyMapData.put("autorenews", autorenews.toString());
}
if (requestedByRegistrar != null) {
soyMapData.put("requestedByRegistrar", requestedByRegistrar.toString());
}
addSoyRecord(clientId, soyMapData);
}

View File

@@ -14,7 +14,7 @@
package google.registry.tools.javascrap;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.appengine.tools.mapreduce.Mapper;
@@ -111,7 +111,7 @@ public class CreateSyntheticHistoryEntriesAction implements Runnable {
public final void map(final Key<EppResource> resourceKey) {
tm().transact(
() -> {
EppResource eppResource = ofy().load().key(resourceKey).now();
EppResource eppResource = auditedOfy().load().key(resourceKey).now();
tm().put(
HistoryEntry.createBuilderForResource(eppResource)
.setClientId(registryAdminRegistrarId)

View File

@@ -1,23 +0,0 @@
// 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.tools.params;
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
/**
* {@link TransitionId} CLI parameter converter/validator. Required to support multi-value
* TransitionId parameters.
*/
public final class TransitionIdParameter extends EnumParameter<TransitionId> {}

View File

@@ -19,7 +19,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Ordering;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.registry.Registry.TldState;
import org.joda.money.Money;
@@ -74,11 +74,11 @@ public abstract class TransitionListParameter<V> extends KeyValueMapParameter<Da
}
}
/** Converter-validator for primary database transitions. */
public static class PrimaryDatabaseTransitions extends TransitionListParameter<PrimaryDatabase> {
/** Converter-validator for states of the Registry 3.0 database migration. */
public static class MigrationStateTransitions extends TransitionListParameter<MigrationState> {
@Override
protected PrimaryDatabase parseValue(String value) {
return PrimaryDatabase.valueOf(value);
protected MigrationState parseValue(String value) {
return MigrationState.valueOf(value);
}
}
}

View File

@@ -49,6 +49,8 @@ import javax.inject.Inject;
path = "/_dr/task/killAllCommitLogs",
method = POST,
auth = Auth.AUTH_INTERNAL_OR_ADMIN)
// No longer needed in SQL. Subject to future removal.
@Deprecated
public class KillAllCommitLogsAction implements Runnable {
@Inject MapreduceRunner mrRunner;

View File

@@ -18,6 +18,8 @@ import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.mapreduce.inputs.EppResourceInputs.createEntityInput;
import static google.registry.model.EppResourceUtils.isActive;
import static google.registry.model.registry.Registries.assertTldsExist;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.RequestParameters.PARAM_TLDS;
import com.google.appengine.tools.mapreduce.Mapper;
@@ -32,11 +34,12 @@ import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.NonFinalForTesting;
import java.util.Random;
import javax.inject.Inject;
import org.apache.http.HttpStatus;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Duration;
/**
@@ -74,6 +77,8 @@ public class RefreshDnsForAllDomainsAction implements Runnable {
@Parameter("smearMinutes")
int smearMinutes;
@Inject DnsQueue dnsQueue;
@Inject Clock clock;
@Inject Random random;
@Inject
@@ -83,14 +88,41 @@ public class RefreshDnsForAllDomainsAction implements Runnable {
public void run() {
assertTldsExist(tlds);
checkArgument(smearMinutes > 0, "Must specify a positive number of smear minutes");
mrRunner
.setJobName("Refresh DNS for all domains")
.setModuleName("tools")
.setDefaultMapShards(10)
.runMapOnly(
new RefreshDnsForAllDomainsActionMapper(tlds, smearMinutes, random),
ImmutableList.of(createEntityInput(DomainBase.class)))
.sendLinkToMapreduceConsole(response);
if (tm().isOfy()) {
mrRunner
.setJobName("Refresh DNS for all domains")
.setModuleName("tools")
.setDefaultMapShards(10)
.runMapOnly(
new RefreshDnsForAllDomainsActionMapper(tlds, smearMinutes, random, clock.nowUtc()),
ImmutableList.of(createEntityInput(DomainBase.class)))
.sendLinkToMapreduceConsole(response);
} else {
tm().transact(
() ->
jpaTm()
.query(
"SELECT fullyQualifiedDomainName FROM Domain "
+ "WHERE tld IN (:tlds) "
+ "AND deletionTime > :now",
String.class)
.setParameter("tlds", tlds)
.setParameter("now", clock.nowUtc())
.getResultStream()
.forEach(
domainName -> {
try {
// Smear the task execution time over the next N minutes.
dnsQueue.addDomainRefreshTask(
domainName,
Duration.standardMinutes(random.nextInt(smearMinutes)));
} catch (Throwable t) {
logger.atSevere().withCause(t).log(
"Error while enqueuing DNS refresh for domain %s", domainName);
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
}
}));
}
}
/** Mapper to refresh DNS for all active domain resources. */
@@ -104,19 +136,21 @@ public class RefreshDnsForAllDomainsAction implements Runnable {
private final ImmutableSet<String> tlds;
private final int smearMinutes;
private final Random random;
private final DateTime now;
RefreshDnsForAllDomainsActionMapper(
ImmutableSet<String> tlds, int smearMinutes, Random random) {
ImmutableSet<String> tlds, int smearMinutes, Random random, DateTime now) {
this.tlds = tlds;
this.smearMinutes = smearMinutes;
this.random = random;
this.now = now;
}
@Override
public void map(final DomainBase domain) {
String domainName = domain.getDomainName();
if (tlds.contains(domain.getTld())) {
if (isActive(domain, DateTime.now(DateTimeZone.UTC))) {
if (isActive(domain, now)) {
try {
// Smear the task execution time over the next N minutes.
dnsQueue.addDomainRefreshTask(
@@ -124,7 +158,7 @@ public class RefreshDnsForAllDomainsAction implements Runnable {
getContext().incrementCounter("active domains refreshed");
} catch (Throwable t) {
logger.atSevere().withCause(t).log(
"Error while refreshing DNS for domain %s", domainName);
"Error while enqueuing DNS refresh for domain %s", domainName);
getContext().incrementCounter("active domains errored");
}
} else {

View File

@@ -42,6 +42,8 @@ import javax.inject.Inject;
service = Action.Service.TOOLS,
path = "/_dr/task/resaveAllHistoryEntries",
auth = Auth.AUTH_INTERNAL_OR_ADMIN)
// No longer needed in SQL. Subject to future removal.
@Deprecated
public class ResaveAllHistoryEntriesAction implements Runnable {
@Inject MapreduceRunner mrRunner;

View File

@@ -27,7 +27,7 @@ SELECT
reason as action,
targetId as domain,
BillingEvent.domainRepoId as repositoryId,
periodYears as years,
IFNULL(periodYears, 0) as years,
BillingEvent.currency AS currency,
BillingEvent.amount as amount,
-- We'll strip out non-useful flags downstream

View File

@@ -61,6 +61,14 @@
"regexes": [
"^gs:\\/\\/[^\\n\\r]+$"
]
},
{
"name": "database",
"label": "Database to read from.",
"helpText": "DATASTORE or CLOUD_SQL.",
"regexes": [
"^DATASTORE|CLOUD_SQL$"
]
}
]
}

View File

@@ -27,6 +27,8 @@
{@param? currency: string}
{@param? price: string}
{@param dsRecords: list<[keyTag:int, alg:int, digestType:int, digest:string]>}
{@param? reason: string}
{@param? requestedByRegistrar: string}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
@@ -54,7 +56,7 @@
</domain:authInfo>
</domain:create>
</create>
{if length($dsRecords) > 0 or $price != null}
{if length($dsRecords) > 0 or $price != null or $reason or $requestedByRegistrar}
<extension>
{if $price != null}
<fee:create xmlns:fee="urn:ietf:params:xml:ns:fee-0.12">
@@ -74,6 +76,16 @@
{/for}
</secDNS:create>
{/if}
{if $reason or $requestedByRegistrar}
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
{if $reason}
<metadata:reason>{$reason}</metadata:reason>
{/if}
{if $requestedByRegistrar}
<metadata:requestedByRegistrar>{$requestedByRegistrar}</metadata:requestedByRegistrar>
{/if}
</metadata:metadata>
{/if}
</extension>
{/if}
<clTRID>RegistryTool</clTRID>

View File

@@ -36,6 +36,8 @@
{@param removeDsRecords: list<[keyTag:int, alg:int, digestType:int, digest:string]>}
{@param removeAllDsRecords: bool}
{@param? autorenews: string}
{@param? reason: string}
{@param? requestedByRegistrar: string}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
@@ -97,7 +99,7 @@
{/if}
</domain:update>
</update>
{if $secdns or $autorenews}
{if $secdns or $autorenews or $reason or $requestedByRegistrar}
<extension>
{if $secdns}
<secDNS:update xmlns:secDNS="urn:ietf:params:xml:ns:secDNS-1.1">
@@ -137,6 +139,16 @@
<superuser:autorenews>{$autorenews}</superuser:autorenews>
</superuser:domainUpdate>
{/if}
{if $reason or $requestedByRegistrar}
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
{if $reason}
<metadata:reason>{$reason}</metadata:reason>
{/if}
{if $requestedByRegistrar}
<metadata:requestedByRegistrar>{$requestedByRegistrar}</metadata:requestedByRegistrar>
{/if}
</metadata:metadata>
{/if}
</extension>
{/if}
<clTRID>RegistryTool</clTRID>

View File

@@ -22,7 +22,7 @@ import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Lists.partition;
import static google.registry.backup.BackupUtils.serializeEntity;
import static google.registry.model.ofy.CommitLogBucket.getBucketKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.DateTimeUtils.isAtOrAfter;
@@ -80,7 +80,7 @@ public final class CommitLogExports {
public static CommitLogCheckpoint computeCheckpoint(Clock clock) {
CommitLogCheckpointStrategy strategy = new CommitLogCheckpointStrategy();
strategy.clock = clock;
strategy.ofy = ofy();
strategy.ofy = auditedOfy();
CommitLogCheckpoint checkpoint = strategy.computeCheckpoint();
tm().transact(
@@ -90,7 +90,7 @@ public final class CommitLogExports {
checkpoint.getCheckpointTime().isAfter(lastWrittenTime),
"Newer checkpoint already written at time: %s",
lastWrittenTime);
ofy()
auditedOfy()
.saveWithoutBackup()
.entities(
checkpoint, CommitLogCheckpointRoot.create(checkpoint.getCheckpointTime()));
@@ -135,17 +135,17 @@ public final class CommitLogExports {
// asynchronously load the entities for the next one.
List<List<Key<CommitLogManifest>>> keyChunks = partition(sortedKeys, EXPORT_DIFF_BATCH_SIZE);
// Objectify's map return type is asynchronous. Calling .values() will block until it loads.
Map<?, CommitLogManifest> nextChunkToExport = ofy().load().keys(keyChunks.get(0));
Map<?, CommitLogManifest> nextChunkToExport = auditedOfy().load().keys(keyChunks.get(0));
for (int i = 0; i < keyChunks.size(); i++) {
// Force the async load to finish.
Collection<CommitLogManifest> chunkValues = nextChunkToExport.values();
// 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.
ofy().clearSessionCache();
auditedOfy().clearSessionCache();
// Kick off the next async load, which can happen in parallel to the current GCS export.
if (i + 1 < keyChunks.size()) {
nextChunkToExport = ofy().load().keys(keyChunks.get(i + 1));
nextChunkToExport = auditedOfy().load().keys(keyChunks.get(i + 1));
}
exportChunk(commitLogStream, chunkValues);
}
@@ -205,7 +205,7 @@ public final class CommitLogExports {
return ImmutableSet.of();
}
Key<CommitLogBucket> bucketKey = getBucketKey(bucketNum);
return ofy()
return auditedOfy()
.load()
.type(CommitLogManifest.class)
.ancestor(bucketKey)
@@ -222,7 +222,7 @@ public final class CommitLogExports {
new ImmutableList.Builder<>();
for (CommitLogManifest manifest : chunk) {
entities.add(ImmutableList.of(manifest));
entities.add(ofy().load().type(CommitLogMutation.class).ancestor(manifest));
entities.add(auditedOfy().load().type(CommitLogMutation.class).ancestor(manifest));
}
for (ImmutableObject entity : concat(entities.build())) {
serializeEntity(entity, gcsStream);

View File

@@ -21,13 +21,16 @@ import static google.registry.backup.RestoreCommitLogsActionTest.GCS_BUCKET;
import static google.registry.backup.RestoreCommitLogsActionTest.createCheckpoint;
import static google.registry.backup.RestoreCommitLogsActionTest.saveDiffFile;
import static google.registry.backup.RestoreCommitLogsActionTest.saveDiffFileNotToRestore;
import static google.registry.model.common.DatabaseMigrationStateSchedule.DEFAULT_TRANSITION_MAP;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.CommitLogBucket.getBucketKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.newDomainBase;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.mockito.ArgumentMatchers.any;
@@ -40,9 +43,11 @@ import com.google.appengine.tools.cloudstorage.GcsServiceFactory;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.truth.Truth8;
import com.googlecode.objectify.Key;
import google.registry.config.RegistryConfig;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.GracePeriod;
@@ -122,11 +127,20 @@ public class ReplayCommitLogsToSqlActionTest {
action.gcsService = gcsService;
action.response = response;
action.requestStatusChecker = requestStatusChecker;
action.clock = fakeClock;
action.diffLister = new GcsDiffFileLister();
action.diffLister.gcsService = gcsService;
action.diffLister.gcsBucket = GCS_BUCKET;
action.diffLister.executor = newDirectExecutorService();
RegistryConfig.overrideCloudSqlReplayCommitLogs(true);
ofyTm()
.transact(
() ->
DatabaseMigrationStateSchedule.set(
ImmutableSortedMap.of(
START_OF_TIME,
MigrationState.DATASTORE_ONLY,
START_OF_TIME.plusMinutes(1),
MigrationState.DATASTORE_PRIMARY)));
TestObject.beforeSqlSaveCallCount = 0;
TestObject.beforeSqlDeleteCallCount = 0;
}
@@ -329,7 +343,7 @@ public class ReplayCommitLogsToSqlActionTest {
ContactResource contactWithEdit =
contact.asBuilder().setEmailAddress("replay@example.tld").build();
CommitLogMutation contactMutation =
tm().transact(() -> CommitLogMutation.create(manifestKey, contactWithEdit));
ofyTm().transact(() -> CommitLogMutation.create(manifestKey, contactWithEdit));
jpaTm().transact(() -> SqlReplayCheckpoint.set(now.minusMinutes(1).minusMillis(1)));
@@ -421,11 +435,13 @@ public class ReplayCommitLogsToSqlActionTest {
@Test
void testFailure_notEnabled() {
RegistryConfig.overrideCloudSqlReplayCommitLogs(false);
ofyTm().transact(() -> DatabaseMigrationStateSchedule.set(DEFAULT_TRANSITION_MAP.toValueMap()));
action.run();
assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT);
assertThat(response.getPayload())
.isEqualTo("ReplayCommitLogsToSqlAction was called but disabled in the config.");
.isEqualTo(
"Skipping ReplayCommitLogsToSqlAction because we are in migration phase"
+ " DATASTORE_ONLY.");
}
@Test

View File

@@ -14,12 +14,14 @@
package google.registry.batch;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.eppcommon.StatusValue.PENDING_DELETE;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.newDomainBase;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -38,27 +40,35 @@ import google.registry.model.ofy.Ofy;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.monitoring.whitebox.EppMetric;
import google.registry.persistence.transaction.QueryComposer.Comparator;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeLockHandler;
import google.registry.testing.FakeResponse;
import google.registry.testing.InjectExtension;
import google.registry.testing.TestOfyAndSql;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link DeleteExpiredDomainsAction}. */
@DualDatabaseTest
class DeleteExpiredDomainsActionTest {
private final FakeClock clock = new FakeClock(DateTime.parse("2016-06-13T20:21:22Z"));
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().withTaskQueue().build();
AppEngineExtension.builder()
.withDatastoreAndCloudSql()
.withClock(clock)
.withTaskQueue()
.build();
@RegisterExtension public final InjectExtension inject = new InjectExtension();
private final FakeClock clock = new FakeClock(DateTime.parse("2016-06-13T20:21:22Z"));
private final FakeResponse response = new FakeResponse();
private DeleteExpiredDomainsAction action;
@@ -78,7 +88,7 @@ class DeleteExpiredDomainsActionTest {
eppController, "NewRegistrar", clock, new FakeLockHandler(true), response);
}
@Test
@TestOfyAndSql
void test_deletesOnlyExpiredDomain() {
// A normal, active autorenewing domain that shouldn't be touched.
DomainBase activeDomain = persistActiveDomain("foo.tld");
@@ -105,7 +115,7 @@ class DeleteExpiredDomainsActionTest {
// to operate on.)
DomainBase pendingExpirationDomain = persistNonAutorenewingDomain("fizz.tld");
assertThat(tm().loadByEntity(pendingExpirationDomain).getStatusValues())
assertThat(loadByEntity(pendingExpirationDomain).getStatusValues())
.doesNotContain(PENDING_DELETE);
// action.run() does not use any test helper that can advance the fake clock. We manually
// advance the clock to emulate the actual behavior. This works because the action only has
@@ -113,17 +123,17 @@ class DeleteExpiredDomainsActionTest {
clock.advanceOneMilli();
action.run();
DomainBase reloadedActiveDomain = tm().loadByEntity(activeDomain);
DomainBase reloadedActiveDomain = loadByEntity(activeDomain);
assertThat(reloadedActiveDomain).isEqualTo(activeDomain);
assertThat(reloadedActiveDomain.getStatusValues()).doesNotContain(PENDING_DELETE);
assertThat(tm().loadByEntity(alreadyDeletedDomain)).isEqualTo(alreadyDeletedDomain);
assertThat(tm().loadByEntity(notYetExpiredDomain)).isEqualTo(notYetExpiredDomain);
DomainBase reloadedExpiredDomain = tm().loadByEntity(pendingExpirationDomain);
assertThat(loadByEntity(alreadyDeletedDomain)).isEqualTo(alreadyDeletedDomain);
assertThat(loadByEntity(notYetExpiredDomain)).isEqualTo(notYetExpiredDomain);
DomainBase reloadedExpiredDomain = loadByEntity(pendingExpirationDomain);
assertThat(reloadedExpiredDomain.getStatusValues()).contains(PENDING_DELETE);
assertThat(reloadedExpiredDomain.getDeletionTime()).isEqualTo(clock.nowUtc().plusDays(35));
}
@Test
@TestOfyAndSql
void test_deletesThreeDomainsInOneRun() throws Exception {
DomainBase domain1 = persistNonAutorenewingDomain("ecck1.tld");
DomainBase domain2 = persistNonAutorenewingDomain("veee2.tld");
@@ -135,14 +145,14 @@ class DeleteExpiredDomainsActionTest {
int maxRetries = 5;
while (true) {
ImmutableSet<String> matchingDomains =
ofy()
.load()
.type(DomainBase.class)
.filter("autorenewEndTime <=", clock.nowUtc())
.list()
.stream()
.map(DomainBase::getDomainName)
.collect(ImmutableSet.toImmutableSet());
transactIfJpaTm(
() ->
tm()
.createQueryComposer(DomainBase.class)
.where("autorenewEndTime", Comparator.LTE, clock.nowUtc())
.stream()
.map(DomainBase::getDomainName)
.collect(toImmutableSet()));
if (matchingDomains.containsAll(ImmutableSet.of("ecck1.tld", "veee2.tld", "tarm3.tld"))) {
break;
}
@@ -158,9 +168,9 @@ class DeleteExpiredDomainsActionTest {
action.run();
clock.disableAutoIncrement();
assertThat(tm().loadByEntity(domain1).getStatusValues()).contains(PENDING_DELETE);
assertThat(tm().loadByEntity(domain2).getStatusValues()).contains(PENDING_DELETE);
assertThat(tm().loadByEntity(domain3).getStatusValues()).contains(PENDING_DELETE);
assertThat(loadByEntity(domain1).getStatusValues()).contains(PENDING_DELETE);
assertThat(loadByEntity(domain2).getStatusValues()).contains(PENDING_DELETE);
assertThat(loadByEntity(domain3).getStatusValues()).contains(PENDING_DELETE);
}
private DomainBase persistNonAutorenewingDomain(String domainName) {

View File

@@ -21,6 +21,7 @@ import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_AUTORENEW;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
import static google.registry.testing.DatabaseHelper.assertBillingEvents;
import static google.registry.testing.DatabaseHelper.assertBillingEventsForResource;
import static google.registry.testing.DatabaseHelper.createTld;
@@ -52,9 +53,13 @@ import google.registry.model.registry.Registry;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.testing.InjectExtension;
import google.registry.testing.ReplayExtension;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import google.registry.testing.mapreduce.MapreduceTestCase;
import java.util.ArrayList;
import java.util.List;
@@ -62,17 +67,25 @@ import java.util.Optional;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link ExpandRecurringBillingEventsAction}. */
@DualDatabaseTest
public class ExpandRecurringBillingEventsActionTest
extends MapreduceTestCase<ExpandRecurringBillingEventsAction> {
@RegisterExtension public final InjectExtension inject = new InjectExtension();
private DateTime currentTestTime = DateTime.parse("1999-01-05T00:00:00Z");
private final FakeClock clock = new FakeClock(currentTestTime);
private final DateTime beginningOfTest = DateTime.parse("2000-10-02T00:00:00Z");
private final FakeClock clock = new FakeClock(beginningOfTest);
@Order(Order.DEFAULT - 1)
@RegisterExtension
public final InjectExtension inject =
new InjectExtension().withStaticFieldOverride(Ofy.class, "clock", clock);
@Order(Order.DEFAULT - 2)
@RegisterExtension
public final ReplayExtension replayExtension = ReplayExtension.createWithCompare(clock);
private DomainBase domain;
private DomainHistory historyEntry;
@@ -80,7 +93,6 @@ public class ExpandRecurringBillingEventsActionTest
@BeforeEach
void beforeEach() {
inject.setStaticField(Ofy.class, "clock", clock);
action = new ExpandRecurringBillingEventsAction();
action.mrRunner = makeDefaultRunner();
action.clock = clock;
@@ -111,27 +123,37 @@ public class ExpandRecurringBillingEventsActionTest
.setRecurrenceEndTime(END_OF_TIME)
.setTargetId(domain.getDomainName())
.build();
currentTestTime = clock.nowUtc();
clock.setTo(DateTime.parse("2000-10-02T00:00:00Z"));
}
private void saveCursor(final DateTime cursorTime) {
tm().transact(() -> tm().put(Cursor.createGlobal(RECURRING_BILLING, cursorTime)));
}
private void runMapreduce() throws Exception {
private void runAction() throws Exception {
action.response = new FakeResponse();
action.run();
// Need to save the current test time before running the mapreduce, which increments the clock.
// The execution time (e. g. transaction time) is captured when the action starts running so
// the passage of time afterward does not affect the timestamp stored in the billing events.
currentTestTime = clock.nowUtc();
executeTasksUntilEmpty("mapreduce", clock);
auditedOfy().clearSessionCache();
}
private void assertCursorAt(DateTime expectedCursorTime) {
Cursor cursor = auditedOfy().load().key(Cursor.createGlobalKey(RECURRING_BILLING)).now();
Cursor cursor =
transactIfJpaTm(() -> tm().loadByKey(Cursor.createGlobalVKey(RECURRING_BILLING)));
assertThat(cursor).isNotNull();
assertThat(cursor.getCursorTime()).isEqualTo(expectedCursorTime);
}
private void assertHistoryEntryMatches(
DomainBase domain, HistoryEntry actual, String clientId, DateTime billingTime,
DomainBase domain,
HistoryEntry actual,
String clientId,
DateTime billingTime,
boolean shouldHaveTxRecord) {
assertThat(actual.getBySuperuser()).isFalse();
assertThat(actual.getClientId()).isEqualTo(clientId);
@@ -145,10 +167,7 @@ public class ExpandRecurringBillingEventsActionTest
assertThat(actual.getDomainTransactionRecords())
.containsExactly(
DomainTransactionRecord.create(
"tld",
billingTime,
TransactionReportField.NET_RENEWS_1_YR,
1));
"tld", billingTime, TransactionReportField.NET_RENEWS_1_YR, 1));
} else {
assertThat(actual.getDomainTransactionRecords()).isEmpty();
}
@@ -163,28 +182,26 @@ public class ExpandRecurringBillingEventsActionTest
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC))
.setPeriodYears(1)
.setReason(Reason.RENEW)
.setSyntheticCreationTime(beginningOfTest)
.setSyntheticCreationTime(currentTestTime)
.setCancellationMatchingBillingEvent(recurring.createVKey())
.setTargetId(domain.getDomainName());
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent() throws Exception {
persistResource(recurring);
action.cursorTimeParam = Optional.of(START_OF_TIME);
runMapreduce();
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true);
BillingEvent.OneTime expected = defaultOneTimeBuilder()
.setParent(persistedEntry)
.build();
BillingEvent.OneTime expected = defaultOneTimeBuilder().setParent(persistedEntry).build();
assertBillingEventsForResource(domain, expected, recurring);
assertCursorAt(beginningOfTest);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_deletedDomain() throws Exception {
DateTime deletionTime = DateTime.parse("2000-08-01T00:00:00Z");
DomainBase deletedDomain = persistDeletedDomain("deleted.tld", deletionTime);
@@ -209,7 +226,7 @@ public class ExpandRecurringBillingEventsActionTest
.setTargetId(deletedDomain.getDomainName())
.build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runMapreduce();
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(deletedDomain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
@@ -224,69 +241,69 @@ public class ExpandRecurringBillingEventsActionTest
.setTargetId(deletedDomain.getDomainName())
.build();
assertBillingEventsForResource(deletedDomain, expected, recurring);
assertCursorAt(beginningOfTest);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_idempotentForDuplicateRuns() throws Exception {
persistResource(recurring);
action.cursorTimeParam = Optional.of(START_OF_TIME);
runMapreduce();
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true);
BillingEvent.OneTime expected = defaultOneTimeBuilder().setParent(persistedEntry).build();
assertCursorAt(beginningOfTest);
assertCursorAt(currentTestTime);
DateTime beginningOfSecondRun = clock.nowUtc();
action.response = new FakeResponse();
runMapreduce();
runAction();
assertCursorAt(beginningOfSecondRun);
assertBillingEventsForResource(domain, expected, recurring);
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_idempotentForExistingOneTime() throws Exception {
persistResource(recurring);
BillingEvent.OneTime persisted = persistResource(defaultOneTimeBuilder()
.setParent(historyEntry)
.build());
BillingEvent.OneTime persisted =
persistResource(defaultOneTimeBuilder().setParent(historyEntry).build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runMapreduce();
runAction();
// No new history entries should be generated
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
assertCursorAt(beginningOfTest);
assertCursorAt(currentTestTime);
// No additional billing events should be generated
assertBillingEventsForResource(domain, persisted, recurring);
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_notIdempotentForDifferentBillingTime() throws Exception {
persistResource(recurring);
action.cursorTimeParam = Optional.of(START_OF_TIME);
runMapreduce();
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true);
BillingEvent.OneTime expected = defaultOneTimeBuilder().setParent(persistedEntry).build();
// Persist an otherwise identical billing event that differs only in billing time.
BillingEvent.OneTime persisted = persistResource(expected.asBuilder()
.setBillingTime(DateTime.parse("1999-02-19T00:00:00Z"))
.setEventTime(DateTime.parse("1999-01-05T00:00:00Z"))
.build());
assertCursorAt(beginningOfTest);
BillingEvent.OneTime persisted =
persistResource(
expected
.asBuilder()
.setBillingTime(DateTime.parse("1999-02-19T00:00:00Z"))
.setEventTime(DateTime.parse("1999-01-05T00:00:00Z"))
.build());
assertCursorAt(currentTestTime);
assertBillingEventsForResource(domain, persisted, expected, recurring);
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_notIdempotentForDifferentRecurring() throws Exception {
persistResource(recurring);
BillingEvent.Recurring recurring2 = persistResource(recurring.asBuilder()
.setId(3L)
.build());
BillingEvent.Recurring recurring2 = persistResource(recurring.asBuilder().setId(3L).build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runMapreduce();
runAction();
List<DomainHistory> persistedEntries =
getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
for (HistoryEntry persistedEntry : persistedEntries) {
@@ -294,9 +311,8 @@ public class ExpandRecurringBillingEventsActionTest
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true);
}
assertThat(persistedEntries).hasSize(2);
BillingEvent.OneTime expected = defaultOneTimeBuilder()
.setParent(persistedEntries.get(0))
.build();
BillingEvent.OneTime expected =
defaultOneTimeBuilder().setParent(persistedEntries.get(0)).build();
// Persist an otherwise identical billing event that differs only in recurring event key.
BillingEvent.OneTime persisted =
expected
@@ -304,164 +320,166 @@ public class ExpandRecurringBillingEventsActionTest
.setParent(persistedEntries.get(1))
.setCancellationMatchingBillingEvent(recurring2.createVKey())
.build();
assertCursorAt(beginningOfTest);
assertCursorAt(currentTestTime);
assertBillingEventsForResource(domain, persisted, expected, recurring, recurring2);
}
@Test
@TestOfyAndSql
void testSuccess_ignoreRecurringBeforeWindow() throws Exception {
recurring = persistResource(recurring.asBuilder()
.setEventTime(DateTime.parse("1997-01-05T00:00:00Z"))
.setRecurrenceEndTime(DateTime.parse("1999-10-05T00:00:00Z"))
.build());
recurring =
persistResource(
recurring
.asBuilder()
.setEventTime(DateTime.parse("1997-01-05T00:00:00Z"))
.setRecurrenceEndTime(DateTime.parse("1999-10-05T00:00:00Z"))
.build());
action.cursorTimeParam = Optional.of(DateTime.parse("2000-01-01T00:00:00Z"));
runMapreduce();
runAction();
// No new history entries should be generated
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
assertBillingEventsForResource(domain, recurring);
assertCursorAt(beginningOfTest);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_ignoreRecurringAfterWindow() throws Exception {
recurring = persistResource(recurring.asBuilder()
.setEventTime(clock.nowUtc().plusYears(2))
.build());
recurring =
persistResource(recurring.asBuilder().setEventTime(clock.nowUtc().plusYears(2)).build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runMapreduce();
runAction();
// No new history entries should be generated
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
assertBillingEventsForResource(domain, recurring);
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_billingTimeAtCursorTime() throws Exception {
persistResource(recurring);
action.cursorTimeParam = Optional.of(DateTime.parse("2000-02-19T00:00:00Z"));
runMapreduce();
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true);
BillingEvent.OneTime expected = defaultOneTimeBuilder().setParent(persistedEntry).build();
assertBillingEventsForResource(domain, expected, recurring);
assertCursorAt(beginningOfTest);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_cursorTimeBetweenEventAndBillingTime() throws Exception {
persistResource(recurring);
action.cursorTimeParam = Optional.of(DateTime.parse("2000-01-12T00:00:00Z"));
runMapreduce();
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true);
BillingEvent.OneTime expected = defaultOneTimeBuilder().setParent(persistedEntry).build();
assertBillingEventsForResource(domain, expected, recurring);
assertCursorAt(beginningOfTest);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_billingTimeAtExecutionTime() throws Exception {
DateTime testTime = DateTime.parse("2000-02-19T00:00:00Z").minusMillis(1);
clock.setTo(currentTestTime);
persistResource(recurring);
action.cursorTimeParam = Optional.of(START_OF_TIME);
// Clock is advanced one milli in runMapreduce()
clock.setTo(testTime);
runMapreduce();
clock.setTo(DateTime.parse("2000-02-19T00:00:00Z"));
runAction();
// No new history entries should be generated
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
// A candidate billing event is set to be billed exactly on 2/19/00 @ 00:00,
// but these should not be generated as the interval is closed on cursorTime, open on
// executeTime.
assertBillingEventsForResource(domain, recurring);
assertCursorAt(testTime);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_multipleYearCreate() throws Exception {
DateTime testTime = beginningOfTest.plusYears(2);
action.cursorTimeParam = Optional.of(recurring.getEventTime());
recurring =
persistResource(
recurring.asBuilder().setEventTime(recurring.getEventTime().plusYears(2)).build());
clock.setTo(testTime);
runMapreduce();
clock.setTo(DateTime.parse("2002-10-02T00:00:00Z"));
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2002-02-19T00:00:00Z"), true);
BillingEvent.OneTime expected = defaultOneTimeBuilder()
.setBillingTime(DateTime.parse("2002-02-19T00:00:00Z"))
.setEventTime(DateTime.parse("2002-01-05T00:00:00Z"))
.setParent(persistedEntry)
.setSyntheticCreationTime(testTime)
.build();
BillingEvent.OneTime expected =
defaultOneTimeBuilder()
.setBillingTime(DateTime.parse("2002-02-19T00:00:00Z"))
.setEventTime(DateTime.parse("2002-01-05T00:00:00Z"))
.setParent(persistedEntry)
.build();
assertBillingEventsForResource(domain, expected, recurring);
assertCursorAt(testTime);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_withCursor() throws Exception {
persistResource(recurring);
saveCursor(START_OF_TIME);
runMapreduce();
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true);
BillingEvent.OneTime expected = defaultOneTimeBuilder().setParent(persistedEntry).build();
assertBillingEventsForResource(domain, expected, recurring);
assertCursorAt(beginningOfTest);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_withCursorPastExpected() throws Exception {
persistResource(recurring);
// Simulate a quick second run of the mapreduce (this should be a no-op).
saveCursor(clock.nowUtc().minusSeconds(1));
runMapreduce();
runAction();
// No new history entries should be generated
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
assertBillingEventsForResource(domain, recurring);
assertCursorAt(beginningOfTest);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_recurrenceEndBeforeEvent() throws Exception {
// This can occur when a domain is transferred or deleted before a domain comes up for renewal.
recurring = persistResource(recurring.asBuilder()
.setRecurrenceEndTime(recurring.getEventTime().minusDays(5))
.build());
recurring =
persistResource(
recurring
.asBuilder()
.setRecurrenceEndTime(recurring.getEventTime().minusDays(5))
.build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runMapreduce();
runAction();
// No new history entries should be generated
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
assertBillingEventsForResource(domain, recurring);
assertCursorAt(beginningOfTest);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_dryRun() throws Exception {
persistResource(recurring);
action.isDryRun = true;
saveCursor(START_OF_TIME); // Need a saved cursor to verify that it didn't move.
runMapreduce();
runAction();
// No new history entries should be generated
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
assertBillingEventsForResource(domain, recurring);
assertCursorAt(START_OF_TIME); // Cursor doesn't move on a dry run.
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_multipleYears() throws Exception {
DateTime testTime = clock.nowUtc().plusYears(5);
clock.setTo(testTime);
clock.setTo(clock.nowUtc().plusYears(5));
List<BillingEvent> expectedEvents = new ArrayList<>();
expectedEvents.add(persistResource(recurring));
action.cursorTimeParam = Optional.of(START_OF_TIME);
runMapreduce();
runAction();
List<DomainHistory> persistedEntries =
getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertThat(persistedEntries).hasSize(6);
@@ -470,29 +488,25 @@ public class ExpandRecurringBillingEventsActionTest
// Expecting events for '00, '01, '02, '03, '04, '05.
for (int year = 0; year < 6; year++) {
assertHistoryEntryMatches(
domain,
persistedEntries.get(year),
"TheRegistrar",
billingDate.plusYears(year), true);
expectedEvents.add(defaultOneTimeBuilder()
.setBillingTime(billingDate.plusYears(year))
.setEventTime(eventDate.plusYears(year))
.setParent(persistedEntries.get(year))
.setSyntheticCreationTime(testTime)
.build());
domain, persistedEntries.get(year), "TheRegistrar", billingDate.plusYears(year), true);
expectedEvents.add(
defaultOneTimeBuilder()
.setBillingTime(billingDate.plusYears(year))
.setEventTime(eventDate.plusYears(year))
.setParent(persistedEntries.get(year))
.build());
}
assertBillingEventsForResource(domain, Iterables.toArray(expectedEvents, BillingEvent.class));
assertCursorAt(testTime);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_multipleYears_cursorInBetweenYears() throws Exception {
DateTime testTime = clock.nowUtc().plusYears(5);
clock.setTo(testTime);
clock.setTo(clock.nowUtc().plusYears(5));
List<BillingEvent> expectedEvents = new ArrayList<>();
expectedEvents.add(persistResource(recurring));
saveCursor(DateTime.parse("2003-10-02T00:00:00Z"));
runMapreduce();
runAction();
List<DomainHistory> persistedEntries =
getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertThat(persistedEntries).hasSize(2);
@@ -502,139 +516,153 @@ public class ExpandRecurringBillingEventsActionTest
for (int year = 0; year < 2; year++) {
assertHistoryEntryMatches(
domain, persistedEntries.get(year), "TheRegistrar", billingDate.plusYears(year), true);
expectedEvents.add(defaultOneTimeBuilder()
.setBillingTime(billingDate.plusYears(year))
.setParent(persistedEntries.get(year))
.setEventTime(eventDate.plusYears(year))
.setSyntheticCreationTime(testTime)
.build());
expectedEvents.add(
defaultOneTimeBuilder()
.setBillingTime(billingDate.plusYears(year))
.setParent(persistedEntries.get(year))
.setEventTime(eventDate.plusYears(year))
.build());
}
assertBillingEventsForResource(domain, Iterables.toArray(expectedEvents, BillingEvent.class));
assertCursorAt(testTime);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_singleEvent_beforeRenewal() throws Exception {
DateTime testTime = DateTime.parse("2000-01-04T00:00:00Z");
clock.setTo(testTime);
// Need to restore to the time before the clock was advanced so that the commit log's timestamp
// is not inverted when the clock is later reverted.
clock.setTo(currentTestTime);
persistResource(recurring);
clock.setTo(DateTime.parse("2000-01-04T00:00:00Z"));
action.cursorTimeParam = Optional.of(START_OF_TIME);
runMapreduce();
runAction();
// No new history entries should be generated
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
assertBillingEventsForResource(domain, recurring);
assertCursorAt(testTime);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_singleEvent_afterRecurrenceEnd_inAutorenewGracePeriod() throws Exception {
// The domain creation date is 1999-01-05, and the first renewal date is thus 2000-01-05.
DateTime testTime = DateTime.parse("2001-02-06T00:00:00Z");
clock.setTo(testTime);
recurring = persistResource(recurring.asBuilder()
// The domain deletion date is 2000-01-29, which is within the 45 day autorenew grace period
// from the renewal date.
.setRecurrenceEndTime(DateTime.parse("2000-01-29T00:00:00Z"))
.setEventTime(domain.getCreationTime().plusYears(1))
.build());
clock.setTo(DateTime.parse("2001-02-06T00:00:00Z"));
recurring =
persistResource(
recurring
.asBuilder()
// The domain deletion date is 2000-01-29, which is within the 45 day autorenew
// grace period
// from the renewal date.
.setRecurrenceEndTime(DateTime.parse("2000-01-29T00:00:00Z"))
.setEventTime(domain.getCreationTime().plusYears(1))
.build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runMapreduce();
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), false);
BillingEvent.OneTime expected = defaultOneTimeBuilder()
.setBillingTime(DateTime.parse("2000-02-19T00:00:00Z"))
.setParent(persistedEntry)
.setSyntheticCreationTime(testTime)
.build();
BillingEvent.OneTime expected =
defaultOneTimeBuilder()
.setBillingTime(DateTime.parse("2000-02-19T00:00:00Z"))
.setParent(persistedEntry)
.build();
assertBillingEventsForResource(domain, recurring, expected);
assertCursorAt(testTime);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_singleEvent_afterRecurrenceEnd_outsideAutorenewGracePeriod() throws Exception {
// The domain creation date is 1999-01-05, and the first renewal date is thus 2000-01-05.
DateTime testTime = DateTime.parse("2001-02-06T00:00:00Z");
clock.setTo(testTime);
recurring = persistResource(recurring.asBuilder()
// The domain deletion date is 2000-04-05, which is not within the 45 day autorenew grace
// period from the renewal date.
.setRecurrenceEndTime(DateTime.parse("2000-04-05T00:00:00Z"))
.setEventTime(domain.getCreationTime().plusYears(1))
.build());
clock.setTo(DateTime.parse("2001-02-06T00:00:00Z"));
recurring =
persistResource(
recurring
.asBuilder()
// The domain deletion date is 2000-04-05, which is not within the 45 day autorenew
// grace
// period from the renewal date.
.setRecurrenceEndTime(DateTime.parse("2000-04-05T00:00:00Z"))
.setEventTime(domain.getCreationTime().plusYears(1))
.build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runMapreduce();
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true);
BillingEvent.OneTime expected = defaultOneTimeBuilder()
.setBillingTime(DateTime.parse("2000-02-19T00:00:00Z"))
.setParent(persistedEntry)
.setSyntheticCreationTime(testTime)
.build();
BillingEvent.OneTime expected =
defaultOneTimeBuilder()
.setBillingTime(DateTime.parse("2000-02-19T00:00:00Z"))
.setParent(persistedEntry)
.build();
assertBillingEventsForResource(domain, recurring, expected);
assertCursorAt(testTime);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_billingTimeOnLeapYear() throws Exception {
recurring =
persistResource(
recurring.asBuilder().setEventTime(DateTime.parse("2000-01-15T00:00:00Z")).build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runMapreduce();
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-29T00:00:00Z"), true);
BillingEvent.OneTime expected = defaultOneTimeBuilder()
.setBillingTime(DateTime.parse("2000-02-29T00:00:00Z"))
.setEventTime(DateTime.parse("2000-01-15T00:00:00Z"))
.setParent(persistedEntry)
.build();
BillingEvent.OneTime expected =
defaultOneTimeBuilder()
.setBillingTime(DateTime.parse("2000-02-29T00:00:00Z"))
.setEventTime(DateTime.parse("2000-01-15T00:00:00Z"))
.setParent(persistedEntry)
.build();
assertBillingEventsForResource(domain, expected, recurring);
assertCursorAt(beginningOfTest);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_expandSingleEvent_billingTimeNotOnLeapYear() throws Exception {
DateTime testTime = DateTime.parse("2001-12-01T00:00:00Z");
recurring =
persistResource(
recurring.asBuilder().setEventTime(DateTime.parse("2001-01-15T00:00:00Z")).build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
clock.setTo(testTime);
runMapreduce();
clock.setTo(DateTime.parse("2001-12-01T00:00:00Z"));
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2001-03-01T00:00:00Z"), true);
BillingEvent.OneTime expected = defaultOneTimeBuilder()
.setBillingTime(DateTime.parse("2001-03-01T00:00:00Z"))
.setEventTime(DateTime.parse("2001-01-15T00:00:00Z"))
.setParent(persistedEntry)
.setSyntheticCreationTime(testTime)
.build();
BillingEvent.OneTime expected =
defaultOneTimeBuilder()
.setBillingTime(DateTime.parse("2001-03-01T00:00:00Z"))
.setEventTime(DateTime.parse("2001-01-15T00:00:00Z"))
.setParent(persistedEntry)
.build();
assertBillingEventsForResource(domain, expected, recurring);
assertCursorAt(testTime);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_expandMultipleEvents() throws Exception {
persistResource(recurring);
BillingEvent.Recurring recurring2 = persistResource(recurring.asBuilder()
.setEventTime(recurring.getEventTime().plusMonths(3))
.setId(3L)
.build());
BillingEvent.Recurring recurring2 =
persistResource(
recurring
.asBuilder()
.setEventTime(recurring.getEventTime().plusMonths(3))
.setId(3L)
.build());
action.cursorTimeParam = Optional.of(START_OF_TIME);
runMapreduce();
runAction();
List<DomainHistory> persistedEntries =
getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertThat(persistedEntries).hasSize(2);
assertHistoryEntryMatches(
domain, persistedEntries.get(0), "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"),
domain,
persistedEntries.get(0),
"TheRegistrar",
DateTime.parse("2000-02-19T00:00:00Z"),
true);
BillingEvent.OneTime expected =
defaultOneTimeBuilder()
@@ -642,7 +670,10 @@ public class ExpandRecurringBillingEventsActionTest
.setCancellationMatchingBillingEvent(recurring.createVKey())
.build();
assertHistoryEntryMatches(
domain, persistedEntries.get(1), "TheRegistrar", DateTime.parse("2000-05-20T00:00:00Z"),
domain,
persistedEntries.get(1),
"TheRegistrar",
DateTime.parse("2000-05-20T00:00:00Z"),
true);
BillingEvent.OneTime expected2 =
defaultOneTimeBuilder()
@@ -652,10 +683,10 @@ public class ExpandRecurringBillingEventsActionTest
.setCancellationMatchingBillingEvent(recurring2.createVKey())
.build();
assertBillingEventsForResource(domain, expected, expected2, recurring, recurring2);
assertCursorAt(beginningOfTest);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_premiumDomain() throws Exception {
persistResource(
Registry.get("tld")
@@ -664,86 +695,87 @@ public class ExpandRecurringBillingEventsActionTest
.build());
persistResource(recurring);
action.cursorTimeParam = Optional.of(START_OF_TIME);
runMapreduce();
runAction();
DomainHistory persistedEntry =
getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertHistoryEntryMatches(
domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true);
BillingEvent.OneTime expected = defaultOneTimeBuilder()
.setParent(persistedEntry)
.setCost(Money.of(USD, 100))
.build();
BillingEvent.OneTime expected =
defaultOneTimeBuilder().setParent(persistedEntry).setCost(Money.of(USD, 100)).build();
assertBillingEventsForResource(domain, expected, recurring);
assertCursorAt(beginningOfTest);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testSuccess_varyingRenewPrices() throws Exception {
DateTime testTime = beginningOfTest.plusYears(1);
clock.setTo(currentTestTime);
persistResource(
Registry.get("tld")
.asBuilder()
.setRenewBillingCostTransitions(
ImmutableSortedMap.of(
START_OF_TIME, Money.of(USD, 8),
DateTime.parse("2000-06-01T00:00:00Z"), Money.of(USD, 10)))
START_OF_TIME,
Money.of(USD, 8),
DateTime.parse("2000-06-01T00:00:00Z"),
Money.of(USD, 10)))
.build());
clock.setTo(testTime);
clock.setTo(DateTime.parse("2001-10-02T00:00:00Z"));
persistResource(recurring);
action.cursorTimeParam = Optional.of(START_OF_TIME);
runMapreduce();
runAction();
List<DomainHistory> persistedEntries =
getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class);
assertThat(persistedEntries).hasSize(2);
DateTime eventDate = DateTime.parse("2000-01-05T00:00:00Z");
DateTime billingDate = DateTime.parse("2000-02-19T00:00:00Z");
assertHistoryEntryMatches(domain, persistedEntries.get(0), "TheRegistrar", billingDate, true);
BillingEvent.OneTime cheaper = defaultOneTimeBuilder()
.setBillingTime(billingDate)
.setEventTime(eventDate)
.setParent(persistedEntries.get(0))
.setCost(Money.of(USD, 8))
.setSyntheticCreationTime(testTime)
.build();
BillingEvent.OneTime cheaper =
defaultOneTimeBuilder()
.setBillingTime(billingDate)
.setEventTime(eventDate)
.setParent(persistedEntries.get(0))
.setCost(Money.of(USD, 8))
.build();
assertHistoryEntryMatches(
domain, persistedEntries.get(1), "TheRegistrar", billingDate.plusYears(1), true);
BillingEvent.OneTime expensive = cheaper.asBuilder()
.setCost(Money.of(USD, 10))
.setBillingTime(billingDate.plusYears(1))
.setEventTime(eventDate.plusYears(1))
.setParent(persistedEntries.get(1))
.build();
BillingEvent.OneTime expensive =
cheaper
.asBuilder()
.setCost(Money.of(USD, 10))
.setBillingTime(billingDate.plusYears(1))
.setEventTime(eventDate.plusYears(1))
.setParent(persistedEntries.get(1))
.build();
assertBillingEventsForResource(domain, recurring, cheaper, expensive);
assertCursorAt(testTime);
assertCursorAt(currentTestTime);
}
@Test
@TestOfyAndSql
void testFailure_cursorAfterExecutionTime() {
action.cursorTimeParam = Optional.of(clock.nowUtc().plusYears(1));
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, this::runMapreduce);
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, this::runAction);
assertThat(thrown)
.hasMessageThat()
.contains("Cursor time must be earlier than execution time.");
}
@Test
@TestOfyAndSql
void testFailure_cursorAtExecutionTime() {
// The clock advances one milli on runMapreduce.
action.cursorTimeParam = Optional.of(clock.nowUtc().plusMillis(1));
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, this::runMapreduce);
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, this::runAction);
assertThat(thrown)
.hasMessageThat()
.contains("Cursor time must be earlier than execution time.");
}
@Test
@TestOfyOnly
void testFailure_mapperException_doesNotMoveCursor() throws Exception {
saveCursor(START_OF_TIME); // Need a saved cursor to verify that it didn't move.
clock.advanceOneMilli();
// Set target to a TLD that doesn't exist.
recurring = persistResource(recurring.asBuilder().setTargetId("domain.junk").build());
runMapreduce();
runAction();
// No new history entries should be generated
assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty();
assertBillingEvents(recurring); // only the bogus one in Datastore

View File

@@ -17,6 +17,8 @@ package google.registry.batch;
import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_HOST_KEY;
import static google.registry.batch.AsyncTaskEnqueuer.PARAM_REQUESTED_TIME;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_HOST_RENAME;
import static google.registry.batch.AsyncTaskMetrics.OperationType.DNS_REFRESH;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -45,6 +47,7 @@ import static org.mockito.Mockito.when;
import com.googlecode.objectify.Key;
import google.registry.batch.AsyncTaskMetrics.OperationResult;
import google.registry.batch.RefreshDnsOnHostRenameAction.RefreshDnsOnHostRenameReducer;
import google.registry.dns.DnsQueue;
import google.registry.model.host.HostResource;
import google.registry.model.server.Lock;
import google.registry.testing.DualDatabaseTest;
@@ -55,6 +58,7 @@ import google.registry.testing.InjectExtension;
import google.registry.testing.TaskQueueHelper.TaskMatcher;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import google.registry.testing.TestSqlOnly;
import google.registry.testing.mapreduce.MapreduceTestCase;
import google.registry.util.AppEngineServiceUtils;
import google.registry.util.RequestStatusChecker;
@@ -62,6 +66,7 @@ import google.registry.util.Retrier;
import google.registry.util.Sleeper;
import google.registry.util.SystemSleeper;
import java.util.Optional;
import org.apache.http.HttpStatus;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
@@ -94,6 +99,7 @@ public class RefreshDnsOnHostRenameActionTest
action.clock = clock;
action.mrRunner = makeDefaultRunner();
action.pullQueue = getQueue(QUEUE_ASYNC_HOST_RENAME);
action.dnsQueue = DnsQueue.createForTesting(clock);
action.requestStatusChecker = requestStatusChecker;
action.response = fakeResponse;
action.retrier = new Retrier(new FakeSleeper(clock), 1);
@@ -102,7 +108,7 @@ public class RefreshDnsOnHostRenameActionTest
.thenThrow(new AssertionError("Should not be called"));
}
private void runMapreduce() throws Exception {
private void runAction() throws Exception {
clock.advanceOneMilli();
// Use hard sleeps to ensure that the tasks are enqueued properly and will be leased.
Sleeper sleeper = new SystemSleeper();
@@ -123,10 +129,32 @@ public class RefreshDnsOnHostRenameActionTest
tm().clearSessionCache();
}
// TODO(b/181662306) None of the map reduce tests work with SQL since our map-reduce setup is
// inherently Datastore oriented, but this is a bigger task.
@TestSqlOnly
void testFailure_dnsUpdateEnqueueFailed() throws Exception {
HostResource host = persistActiveHost("ns1.example.tld");
persistResource(newDomainBase("example.tld", host));
persistResource(newDomainBase("otherexample.tld", host));
persistResource(newDomainBase("untouched.tld", persistActiveHost("ns2.example.tld")));
DateTime timeEnqueued = clock.nowUtc();
enqueuer.enqueueAsyncDnsRefresh(host, timeEnqueued);
DnsQueue mockedQueue = mock(DnsQueue.class);
action.dnsQueue = mockedQueue;
when(mockedQueue.addDomainRefreshTask(anyString()))
.thenThrow(new RuntimeException("Cannot enqueue task."));
runAction();
assertNoDnsTasksEnqueued();
assertTasksEnqueued(
QUEUE_ASYNC_HOST_RENAME,
new TaskMatcher()
.param(PARAM_HOST_KEY, host.createVKey().getOfyKey().getString())
.param(PARAM_REQUESTED_TIME, timeEnqueued.toString()));
verify(action.asyncTaskMetrics).recordDnsRefreshBatchSize(1L);
verifyNoMoreInteractions(action.asyncTaskMetrics);
assertThat(fakeResponse.getStatus()).isEqualTo(HttpStatus.SC_INTERNAL_SERVER_ERROR);
assertThat(acquireLock()).isPresent();
}
@TestOfyOnly
@TestOfyAndSql
void testSuccess_dnsUpdateEnqueued() throws Exception {
HostResource host = persistActiveHost("ns1.example.tld");
persistResource(newDomainBase("example.tld", host));
@@ -134,21 +162,22 @@ public class RefreshDnsOnHostRenameActionTest
persistResource(newDomainBase("untouched.tld", persistActiveHost("ns2.example.tld")));
DateTime timeEnqueued = clock.nowUtc();
enqueuer.enqueueAsyncDnsRefresh(host, timeEnqueued);
runMapreduce();
runAction();
assertDnsTasksEnqueued("example.tld", "otherexample.tld");
assertNoTasksEnqueued(QUEUE_ASYNC_HOST_RENAME);
verify(action.asyncTaskMetrics).recordDnsRefreshBatchSize(1L);
verify(action.asyncTaskMetrics)
.recordAsyncFlowResult(DNS_REFRESH, OperationResult.SUCCESS, timeEnqueued);
verifyNoMoreInteractions(action.asyncTaskMetrics);
assertThat(acquireLock()).isPresent();
}
@TestOfyOnly
@TestOfyAndSql
void testSuccess_multipleHostsProcessedInBatch() throws Exception {
HostResource host1 = persistActiveHost("ns1.example.tld");
HostResource host2 = persistActiveHost("ns2.example.tld");
HostResource host3 = persistActiveHost("ns3.example.tld");
persistResource(newDomainBase("example1.tld", host1));
persistResource(newDomainBase("example1.tld", host1, host2));
persistResource(newDomainBase("example2.tld", host2));
persistResource(newDomainBase("example3.tld", host3));
DateTime timeEnqueued = clock.nowUtc();
@@ -156,7 +185,7 @@ public class RefreshDnsOnHostRenameActionTest
enqueuer.enqueueAsyncDnsRefresh(host1, timeEnqueued);
enqueuer.enqueueAsyncDnsRefresh(host2, timeEnqueued);
enqueuer.enqueueAsyncDnsRefresh(host3, laterTimeEnqueued);
runMapreduce();
runAction();
assertDnsTasksEnqueued("example1.tld", "example2.tld", "example3.tld");
assertNoTasksEnqueued(QUEUE_ASYNC_HOST_RENAME);
verify(action.asyncTaskMetrics).recordDnsRefreshBatchSize(3L);
@@ -165,21 +194,23 @@ public class RefreshDnsOnHostRenameActionTest
verify(action.asyncTaskMetrics)
.recordAsyncFlowResult(DNS_REFRESH, OperationResult.SUCCESS, laterTimeEnqueued);
verifyNoMoreInteractions(action.asyncTaskMetrics);
assertThat(acquireLock()).isPresent();
}
@TestOfyOnly
@TestOfyAndSql
void testSuccess_deletedHost_doesntTriggerDnsRefresh() throws Exception {
HostResource host = persistDeletedHost("ns11.fakesss.tld", clock.nowUtc().minusDays(4));
persistResource(newDomainBase("example1.tld", host));
DateTime timeEnqueued = clock.nowUtc();
enqueuer.enqueueAsyncDnsRefresh(host, timeEnqueued);
runMapreduce();
runAction();
assertNoDnsTasksEnqueued();
assertNoTasksEnqueued(QUEUE_ASYNC_HOST_RENAME);
verify(action.asyncTaskMetrics).recordDnsRefreshBatchSize(1L);
verify(action.asyncTaskMetrics)
.recordAsyncFlowResult(DNS_REFRESH, OperationResult.STALE, timeEnqueued);
verifyNoMoreInteractions(action.asyncTaskMetrics);
assertThat(acquireLock()).isPresent();
}
@TestOfyAndSql
@@ -191,9 +222,10 @@ public class RefreshDnsOnHostRenameActionTest
.setDeletionTime(START_OF_TIME)
.build());
enqueuer.enqueueAsyncDnsRefresh(renamedHost, clock.nowUtc());
runMapreduce();
runAction();
assertNoDnsTasksEnqueued();
assertNoTasksEnqueued(QUEUE_ASYNC_HOST_RENAME);
assertThat(acquireLock()).isPresent();
}
@TestOfyAndSql
@@ -217,9 +249,10 @@ public class RefreshDnsOnHostRenameActionTest
enqueueMapreduceOnly();
assertThat(fakeResponse.getPayload()).isEqualTo("Can't acquire lock; aborting.");
assertNoDnsTasksEnqueued();
assertThat(acquireLock()).isEmpty();
}
@TestOfyAndSql
@TestOfyOnly
void test_mapreduceHasWorkToDo_lockIsAcquired() {
HostResource host = persistActiveHost("ns1.example.tld");
enqueuer.enqueueAsyncDnsRefresh(host, clock.nowUtc());

View File

@@ -185,6 +185,19 @@ class BillingEventTest {
+ "myRegistrar - test,3,RENEW | TLD: test | TERM: 5-year,20.50,USD,");
}
@Test
void testConvertInvoiceGroupingKey_zeroYears_toCsv() {
GenericRecord record = schemaAndRecord.getRecord();
record.put("years", 0);
schemaAndRecord = new SchemaAndRecord(record, null);
BillingEvent event = BillingEvent.parseFromRecord(schemaAndRecord);
InvoiceGroupingKey invoiceKey = event.getInvoiceGroupingKey();
assertThat(invoiceKey.toCsv(3L))
.isEqualTo(
"2017-10-01,,12345-CRRHELLO,61.50,USD,10125,1,PURCHASE,"
+ "myRegistrar - test,3,RENEW | TLD: test | TERM: 0-year,20.50,USD,");
}
@Test
void testInvoiceGroupingKeyCoder_deterministicSerialization() throws IOException {
InvoiceGroupingKey invoiceKey =

View File

@@ -119,7 +119,37 @@ class InvoicingPipelineTest {
1,
"USD",
0,
"SUNRISE ANCHOR_TENANT"));
"SUNRISE ANCHOR_TENANT"),
BillingEvent.create(
1,
ZonedDateTime.of(2017, 10, 4, 0, 0, 0, 0, ZoneId.of("UTC")),
ZonedDateTime.of(2017, 10, 4, 0, 0, 0, 0, ZoneId.of("UTC")),
"theRegistrar",
"234",
"",
"test",
"SERVER_STATUS",
"locked.test",
"REPO-ID",
0,
"USD",
0,
""),
BillingEvent.create(
1,
ZonedDateTime.of(2017, 10, 4, 0, 0, 0, 0, ZoneId.of("UTC")),
ZonedDateTime.of(2017, 10, 4, 0, 0, 0, 0, ZoneId.of("UTC")),
"theRegistrar",
"234",
"",
"test",
"SERVER_STATUS",
"update-prohibited.test",
"REPO-ID",
0,
"USD",
20,
""));
private static final ImmutableMap<String, ImmutableList<String>> EXPECTED_DETAILED_REPORT_MAP =
ImmutableMap.of(
@@ -128,7 +158,11 @@ class InvoicingPipelineTest {
"1,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00 UTC,theRegistrar,234,,"
+ "test,RENEW,mydomain2.test,REPO-ID,3,USD,20.50,",
"1,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00 UTC,theRegistrar,234,,"
+ "test,RENEW,mydomain.test,REPO-ID,3,USD,20.50,"),
+ "test,RENEW,mydomain.test,REPO-ID,3,USD,20.50,",
"1,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00 UTC,theRegistrar,234,,"
+ "test,SERVER_STATUS,update-prohibited.test,REPO-ID,0,USD,20.00,",
"1,2017-10-04 00:00:00 UTC,2017-10-04 00:00:00 UTC,theRegistrar,234,,"
+ "test,SERVER_STATUS,locked.test,REPO-ID,0,USD,0.00,"),
"invoice_details_2017-10_theRegistrar_hello.csv",
ImmutableList.of(
"1,2017-10-02 00:00:00 UTC,2017-09-29 00:00:00 UTC,theRegistrar,234,,"
@@ -148,6 +182,8 @@ class InvoicingPipelineTest {
+ "RENEW | TLD: test | TERM: 3-year,20.50,USD,",
"2017-10-01,2022-09-30,234,70.75,JPY,10125,1,PURCHASE,theRegistrar - hello,1,"
+ "CREATE | TLD: hello | TERM: 5-year,70.75,JPY,",
"2017-10-01,,234,20.00,USD,10125,1,PURCHASE,theRegistrar - test,1,"
+ "SERVER_STATUS | TLD: test | TERM: 0-year,20.00,USD,",
"2017-10-01,2018-09-30,456,20.50,USD,10125,1,PURCHASE,bestdomains - test,1,"
+ "RENEW | TLD: test | TERM: 1-year,20.50,USD,116688");

View File

@@ -164,7 +164,7 @@ class SafeBrowsingTransformsTest {
* A serializable {@link Answer} that returns a mock HTTP response based on the HTTP request's
* content.
*/
private static class HttpResponder implements Answer<CloseableHttpResponse>, Serializable {
static class HttpResponder implements Answer<CloseableHttpResponse>, Serializable {
@Override
public CloseableHttpResponse answer(InvocationOnMock invocation) throws Throwable {
return getMockResponse(

View File

@@ -18,33 +18,60 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.immutableObjectCorrespondence;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.AppEngineExtension.makeRegistrar1;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistNewRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.google.common.truth.Correspondence;
import com.google.common.truth.Correspondence.BinaryPredicate;
import google.registry.beam.TestPipelineExtension;
import google.registry.beam.spec11.SafeBrowsingTransforms.EvaluateSafeBrowsingFn;
import google.registry.beam.spec11.SafeBrowsingTransformsTest.HttpResponder;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainBase;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
import google.registry.model.registrar.Registrar;
import google.registry.model.reporting.Spec11ThreatMatch;
import google.registry.model.reporting.Spec11ThreatMatch.ThreatType;
import google.registry.model.reporting.Spec11ThreatMatchDao;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationTestExtension;
import google.registry.persistence.transaction.TransactionManager;
import google.registry.persistence.transaction.TransactionManagerFactory;
import google.registry.testing.DatastoreEntityExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeSleeper;
import google.registry.util.ResourceUtils;
import google.registry.util.Retrier;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.beam.sdk.coders.KvCoder;
import org.apache.beam.sdk.coders.SerializableCoder;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.testing.PAssert;
import org.apache.beam.sdk.transforms.Create;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollection;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.json.JSONObject;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
@@ -60,9 +87,14 @@ import org.junit.jupiter.api.io.TempDir;
*/
class Spec11PipelineTest {
private static final DateTime START_TIME = DateTime.parse("2020-01-27T00:00:00.0Z");
private final FakeClock fakeClock = new FakeClock(START_TIME);
private static final String DATE = "2020-01-27";
private static final String SAFE_BROWSING_API_KEY = "api-key";
private static final String REPORTING_BUCKET_URL = "reporting_bucket";
private final CloseableHttpClient mockHttpClient =
mock(CloseableHttpClient.class, withSettings().serializable());
private static final ImmutableList<Subdomain> SUBDOMAINS =
ImmutableList.of(
@@ -103,12 +135,18 @@ class Spec11PipelineTest {
private File reportingBucketUrl;
private PCollection<KV<Subdomain, ThreatMatch>> threatMatches;
ImmutableSet<Spec11ThreatMatch> sqlThreatMatches;
TransactionManager tm;
@BeforeEach
void beforeEach() throws Exception {
tm = tm();
TransactionManagerFactory.setTm(jpaTm());
reportingBucketUrl = Files.createDirectory(tmpDir.resolve(REPORTING_BUCKET_URL)).toFile();
options.setDate(DATE);
options.setSafeBrowsingApiKey(SAFE_BROWSING_API_KEY);
options.setReportingBucketUrl(reportingBucketUrl.getAbsolutePath());
options.setDatabase("DATASTORE");
threatMatches =
pipeline.apply(
Create.of(
@@ -118,11 +156,8 @@ class Spec11PipelineTest {
KvCoder.of(
SerializableCoder.of(Subdomain.class),
SerializableCoder.of(ThreatMatch.class))));
}
@Test
void testSuccess_saveToSql() {
ImmutableSet<Spec11ThreatMatch> sqlThreatMatches =
sqlThreatMatches =
ImmutableSet.of(
new Spec11ThreatMatch.Builder()
.setDomainName("111.com")
@@ -159,25 +194,98 @@ class Spec11PipelineTest {
.setCheckDate(new LocalDate(2020, 1, 27))
.setThreatTypes(ImmutableSet.of(ThreatType.UNWANTED_SOFTWARE))
.build());
}
@AfterEach
void afterEach() {
TransactionManagerFactory.setTm(tm);
}
@Test
void testSuccess_fullSqlPipeline() throws Exception {
setupCloudSql();
options.setDatabase("CLOUD_SQL");
EvaluateSafeBrowsingFn safeBrowsingFn =
new EvaluateSafeBrowsingFn(
SAFE_BROWSING_API_KEY,
new Retrier(new FakeSleeper(new FakeClock()), 1),
Suppliers.ofInstance(mockHttpClient));
when(mockHttpClient.execute(any(HttpPost.class))).thenAnswer(new HttpResponder());
Spec11Pipeline spec11Pipeline = new Spec11Pipeline(options, safeBrowsingFn);
spec11Pipeline.setupPipeline(pipeline);
pipeline.run(options).waitUntilFinish();
verifySaveToGcs();
verifySaveToCloudSql();
}
@Test
void testSuccess_saveToSql() {
Spec11Pipeline.saveToSql(threatMatches, options);
pipeline.run().waitUntilFinish();
assertThat(
jpaTm()
.transact(
() ->
Spec11ThreatMatchDao.loadEntriesByDate(
jpaTm(), new LocalDate(2020, 1, 27))))
.comparingElementsUsing(immutableObjectCorrespondence("id"))
.containsExactlyElementsIn(sqlThreatMatches);
verifySaveToCloudSql();
}
@Test
void testSuccess_saveToGcs() throws Exception {
Spec11Pipeline.saveToGcs(threatMatches, options);
pipeline.run().waitUntilFinish();
verifySaveToGcs();
}
@Test
void testSuccess_readFromCloudSql() throws Exception {
setupCloudSql();
PCollection<Subdomain> subdomains = Spec11Pipeline.readFromCloudSql(pipeline);
PAssert.that(subdomains).containsInAnyOrder(SUBDOMAINS);
pipeline.run().waitUntilFinish();
}
private void setupCloudSql() {
persistNewRegistrar("TheRegistrar");
persistNewRegistrar("NewRegistrar");
Registrar registrar1 =
persistResource(
makeRegistrar1()
.asBuilder()
.setClientId("hello-registrar")
.setEmailAddress("email@hello.net")
.build());
Registrar registrar2 =
persistResource(
makeRegistrar1()
.asBuilder()
.setClientId("kitty-registrar")
.setEmailAddress("contact@kit.ty")
.build());
Registrar registrar3 =
persistResource(
makeRegistrar1()
.asBuilder()
.setClientId("cool-registrar")
.setEmailAddress("cool@aid.net")
.build());
createTld("com");
createTld("net");
createTld("bank");
createTld("dev");
ContactResource contact1 = persistActiveContact(registrar1.getClientId());
ContactResource contact2 = persistActiveContact(registrar2.getClientId());
ContactResource contact3 = persistActiveContact(registrar3.getClientId());
persistResource(createDomain("111.com", "123456789-COM", registrar1, contact1));
persistResource(createDomain("party-night.net", "2244AABBC-NET", registrar2, contact2));
persistResource(createDomain("bitcoin.bank", "1C3D5E7F9-BANK", registrar1, contact1));
persistResource(createDomain("no-email.com", "2A4BA9BBC-COM", registrar2, contact2));
persistResource(
createDomain("anti-anti-anti-virus.dev", "555666888-DEV", registrar3, contact3));
}
private void verifySaveToGcs() throws Exception {
ImmutableList<String> expectedFileContents =
ImmutableList.copyOf(
ResourceUtils.readResourceUtf8(this.getClass(), "test_output.txt").split("\n"));
Spec11Pipeline.saveToGcs(threatMatches, options);
pipeline.run().waitUntilFinish();
ImmutableList<String> resultFileContents = resultFileContents();
assertThat(resultFileContents.size()).isEqualTo(expectedFileContents.size());
assertThat(resultFileContents.get(0)).isEqualTo(expectedFileContents.get(0));
@@ -188,6 +296,34 @@ class Spec11PipelineTest {
.containsExactlyElementsIn(expectedFileContents.subList(1, expectedFileContents.size()));
}
private void verifySaveToCloudSql() {
jpaTm()
.transact(
() -> {
ImmutableList<Spec11ThreatMatch> sqlThreatMatches =
Spec11ThreatMatchDao.loadEntriesByDate(jpaTm(), new LocalDate(2020, 1, 27));
assertThat(sqlThreatMatches)
.comparingElementsUsing(immutableObjectCorrespondence("id"))
.containsExactlyElementsIn(sqlThreatMatches);
});
}
private DomainBase createDomain(
String domainName, String repoId, Registrar registrar, ContactResource contact) {
return new DomainBase.Builder()
.setDomainName(domainName)
.setRepoId(repoId)
.setCreationClientId(registrar.getClientId())
.setLastEppUpdateTime(fakeClock.nowUtc())
.setLastEppUpdateClientId(registrar.getClientId())
.setLastTransferTime(fakeClock.nowUtc())
.setRegistrant(contact.createVKey())
.setPersistedCurrentSponsorClientId(registrar.getClientId())
.setRegistrationExpirationTime(fakeClock.nowUtc().plusYears(1))
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("password")))
.build();
}
/** Returns the text contents of a file under the beamBucket/results directory. */
private ImmutableList<String> resultFileContents() throws Exception {
File resultFile =

View File

@@ -16,8 +16,7 @@ package google.registry.export;
import static com.google.appengine.tools.cloudstorage.GcsServiceFactory.createGcsService;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.export.ExportDomainListsAction.ExportDomainListsReducer.EXPORT_MIME_TYPE;
import static google.registry.export.ExportDomainListsAction.ExportDomainListsReducer.REGISTERED_DOMAINS_FILENAME;
import static google.registry.export.ExportDomainListsAction.REGISTERED_DOMAINS_FILENAME;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistDeletedDomain;
@@ -34,25 +33,40 @@ import com.google.appengine.tools.cloudstorage.GcsFilename;
import com.google.appengine.tools.cloudstorage.GcsService;
import com.google.appengine.tools.cloudstorage.ListOptions;
import com.google.appengine.tools.cloudstorage.ListResult;
import com.google.common.net.MediaType;
import google.registry.export.ExportDomainListsAction.ExportDomainListsReducer;
import google.registry.model.ofy.Ofy;
import google.registry.model.registry.Registry;
import google.registry.model.registry.Registry.TldType;
import google.registry.storage.drive.DriveConnection;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.testing.InjectExtension;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import google.registry.testing.mapreduce.MapreduceTestCase;
import java.io.FileNotFoundException;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentCaptor;
/** Unit tests for {@link ExportDomainListsAction}. */
@DualDatabaseTest
class ExportDomainListsActionTest extends MapreduceTestCase<ExportDomainListsAction> {
private GcsService gcsService;
private DriveConnection driveConnection = mock(DriveConnection.class);
private ArgumentCaptor<byte[]> bytesExportedToDrive = ArgumentCaptor.forClass(byte[].class);
private final FakeResponse response = new FakeResponse();
private final FakeClock clock = new FakeClock(DateTime.parse("2020-02-02T02:02:02Z"));
@Order(Order.DEFAULT - 1)
@RegisterExtension
public final InjectExtension inject =
new InjectExtension().withStaticFieldOverride(Ofy.class, "clock", clock);
@BeforeEach
void beforeEach() {
@@ -68,10 +82,12 @@ class ExportDomainListsActionTest extends MapreduceTestCase<ExportDomainListsAct
action.response = response;
action.gcsBucket = "outputbucket";
action.gcsBufferSize = 500;
action.clock = clock;
action.driveConnection = driveConnection;
gcsService = createGcsService();
}
private void runMapreduce() throws Exception {
private void runAction() throws Exception {
action.run();
executeTasksUntilEmpty("mapreduce");
}
@@ -80,27 +96,27 @@ class ExportDomainListsActionTest extends MapreduceTestCase<ExportDomainListsAct
verify(driveConnection)
.createOrUpdateFile(
eq(REGISTERED_DOMAINS_FILENAME),
eq(EXPORT_MIME_TYPE),
eq(MediaType.PLAIN_TEXT_UTF_8),
eq(folderId),
bytesExportedToDrive.capture());
assertThat(new String(bytesExportedToDrive.getValue(), UTF_8)).isEqualTo(domains);
}
@Test
@TestOfyOnly
void test_writesLinkToMapreduceConsoleToResponse() throws Exception {
runMapreduce();
runAction();
assertThat(response.getPayload())
.startsWith(
"Mapreduce console: https://backend-dot-projectid.appspot.com"
+ "/_ah/pipeline/status.html?root=");
}
@Test
@TestOfyAndSql
void test_outputsOnlyActiveDomains() throws Exception {
persistActiveDomain("onetwo.tld");
persistActiveDomain("rudnitzky.tld");
persistDeletedDomain("mortuary.tld", DateTime.parse("2001-03-14T10:11:12Z"));
runMapreduce();
runAction();
GcsFilename existingFile = new GcsFilename("outputbucket", "tld.txt");
String tlds = new String(readGcsFile(gcsService, existingFile), UTF_8);
// Check that it only contains the active domains, not the dead one.
@@ -109,12 +125,12 @@ class ExportDomainListsActionTest extends MapreduceTestCase<ExportDomainListsAct
verifyNoMoreInteractions(driveConnection);
}
@Test
@TestOfyAndSql
void test_outputsOnlyDomainsOnRealTlds() throws Exception {
persistActiveDomain("onetwo.tld");
persistActiveDomain("rudnitzky.tld");
persistActiveDomain("wontgo.testtld");
runMapreduce();
runAction();
GcsFilename existingFile = new GcsFilename("outputbucket", "tld.txt");
String tlds = new String(readGcsFile(gcsService, existingFile), UTF_8).trim();
// Check that it only contains the domains on the real TLD, and not the test one.
@@ -130,7 +146,7 @@ class ExportDomainListsActionTest extends MapreduceTestCase<ExportDomainListsAct
verifyNoMoreInteractions(driveConnection);
}
@Test
@TestOfyAndSql
void test_outputsDomainsFromDifferentTldsToMultipleFiles() throws Exception {
createTld("tldtwo");
persistResource(Registry.get("tldtwo").asBuilder().setDriveFolderId("hooray").build());
@@ -143,7 +159,7 @@ class ExportDomainListsActionTest extends MapreduceTestCase<ExportDomainListsAct
persistActiveDomain("santa.tldtwo");
persistActiveDomain("buddy.tldtwo");
persistActiveDomain("cupid.tldthree");
runMapreduce();
runAction();
GcsFilename firstTldFile = new GcsFilename("outputbucket", "tld.txt");
String tlds = new String(readGcsFile(gcsService, firstTldFile), UTF_8).trim();
assertThat(tlds).isEqualTo("dasher.tld\nprancer.tld");

View File

@@ -16,8 +16,11 @@ package google.registry.flows;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.EppResourceUtils.loadAtPointInTime;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadAllOf;
import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -25,31 +28,38 @@ import static org.joda.time.DateTimeZone.UTC;
import static org.joda.time.Duration.standardDays;
import com.google.common.collect.ImmutableMap;
import com.googlecode.objectify.Key;
import com.google.common.collect.Iterables;
import google.registry.flows.EppTestComponent.FakesAndMocksModule;
import google.registry.model.domain.DomainBase;
import google.registry.model.ofy.Ofy;
import google.registry.monitoring.whitebox.EppMetric;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.EppLoader;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeHttpSession;
import google.registry.testing.InjectExtension;
import google.registry.testing.TestOfyAndSql;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Test that domain flows create the commit logs needed to reload at points in the past. */
class EppCommitLogsTest {
/** Test that we can reload EPP resources as they were in the past. */
@DualDatabaseTest
class EppPointInTimeTest {
private final FakeClock clock = new FakeClock(DateTime.now(UTC));
@RegisterExtension
final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().withTaskQueue().build();
AppEngineExtension.builder()
.withDatastoreAndCloudSql()
.withClock(clock)
.withTaskQueue()
.build();
@RegisterExtension final InjectExtension inject = new InjectExtension();
private final FakeClock clock = new FakeClock(DateTime.now(UTC));
private EppLoader eppLoader;
@BeforeEach
@@ -81,7 +91,7 @@ class EppCommitLogsTest {
.run(EppMetric.builder());
}
@Test
@TestOfyAndSql
void testLoadAtPointInTime() throws Exception {
clock.setTo(DateTime.parse("1984-12-18T12:30Z")); // not midnight
@@ -95,64 +105,75 @@ class EppCommitLogsTest {
clock.setTo(timeAtCreate);
eppLoader = new EppLoader(this, "domain_create.xml", ImmutableMap.of("DOMAIN", "example.tld"));
runFlow();
auditedOfy().clearSessionCache();
Key<DomainBase> key = Key.create(auditedOfy().load().type(DomainBase.class).first().now());
DomainBase domainAfterCreate = auditedOfy().load().key(key).now();
tm().clearSessionCache();
DomainBase domainAfterCreate = Iterables.getOnlyElement(loadAllOf(DomainBase.class));
assertThat(domainAfterCreate.getDomainName()).isEqualTo("example.tld");
clock.advanceBy(standardDays(2));
DateTime timeAtFirstUpdate = clock.nowUtc();
eppLoader = new EppLoader(this, "domain_update_dsdata_add.xml");
runFlow();
auditedOfy().clearSessionCache();
tm().clearSessionCache();
DomainBase domainAfterFirstUpdate = auditedOfy().load().key(key).now();
DomainBase domainAfterFirstUpdate = loadByEntity(domainAfterCreate);
assertThat(domainAfterCreate).isNotEqualTo(domainAfterFirstUpdate);
clock.advanceOneMilli(); // same day as first update
DateTime timeAtSecondUpdate = clock.nowUtc();
eppLoader = new EppLoader(this, "domain_update_dsdata_rem.xml");
runFlow();
auditedOfy().clearSessionCache();
DomainBase domainAfterSecondUpdate = auditedOfy().load().key(key).now();
tm().clearSessionCache();
DomainBase domainAfterSecondUpdate = loadByEntity(domainAfterCreate);
clock.advanceBy(standardDays(2));
DateTime timeAtDelete = clock.nowUtc(); // before 'add' grace period ends
eppLoader = new EppLoader(this, "domain_delete.xml", ImmutableMap.of("DOMAIN", "example.tld"));
runFlow();
auditedOfy().clearSessionCache();
tm().clearSessionCache();
assertThat(domainAfterFirstUpdate).isNotEqualTo(domainAfterSecondUpdate);
// Point-in-time can only rewind an object from the current version, not roll forward.
DomainBase latest = auditedOfy().load().key(key).now();
DomainBase latest = loadByEntity(domainAfterCreate);
// Creation time has millisecond granularity due to isActive() check.
auditedOfy().clearSessionCache();
tm().clearSessionCache();
assertThat(loadAtPointInTime(latest, timeAtCreate.minusMillis(1)).now()).isNull();
assertThat(loadAtPointInTime(latest, timeAtCreate).now()).isNotNull();
assertThat(loadAtPointInTime(latest, timeAtCreate.plusMillis(1)).now()).isNotNull();
auditedOfy().clearSessionCache();
assertThat(loadAtPointInTime(latest, timeAtCreate.plusDays(1)).now())
.isEqualTo(domainAfterCreate);
tm().clearSessionCache();
assertAboutImmutableObjects()
.that(loadAtPointInTime(latest, timeAtCreate.plusDays(1)).now())
.hasFieldsEqualTo(domainAfterCreate);
// Both updates happened on the same day. Since the revisions field has day granularity, the
// key to the first update should have been overwritten by the second, and its timestamp rolled
// forward. So we have to fall back to the last revision before midnight.
auditedOfy().clearSessionCache();
assertThat(loadAtPointInTime(latest, timeAtFirstUpdate).now()).isEqualTo(domainAfterCreate);
tm().clearSessionCache();
if (tm().isOfy()) {
// Both updates happened on the same day. Since the revisions field has day granularity in
// Datastore, the key to the first update should have been overwritten by the second, and its
// timestamp rolled forward. So we have to fall back to the last revision before midnight.
assertThat(loadAtPointInTime(latest, timeAtFirstUpdate).now()).isEqualTo(domainAfterCreate);
} else {
// In SQL, however, we are not limited by the day granularity, so when we request the object
// at timeAtFirstUpdate we should receive the object at that first update, even though the
// second update occurred one millisecond later.
assertAboutImmutableObjects()
.that(loadAtPointInTime(latest, timeAtFirstUpdate).now())
.hasFieldsEqualTo(domainAfterFirstUpdate);
}
auditedOfy().clearSessionCache();
assertThat(loadAtPointInTime(latest, timeAtSecondUpdate).now())
.isEqualTo(domainAfterSecondUpdate);
tm().clearSessionCache();
assertAboutImmutableObjects()
.that(loadAtPointInTime(latest, timeAtSecondUpdate).now())
.hasFieldsEqualTo(domainAfterSecondUpdate);
auditedOfy().clearSessionCache();
assertThat(loadAtPointInTime(latest, timeAtSecondUpdate.plusDays(1)).now())
.isEqualTo(domainAfterSecondUpdate);
tm().clearSessionCache();
assertAboutImmutableObjects()
.that(loadAtPointInTime(latest, timeAtSecondUpdate.plusDays(1)).now())
.hasFieldsEqualTo(domainAfterSecondUpdate);
// Deletion time has millisecond granularity due to isActive() check.
auditedOfy().clearSessionCache();
tm().clearSessionCache();
assertThat(loadAtPointInTime(latest, timeAtDelete.minusMillis(1)).now()).isNotNull();
assertThat(loadAtPointInTime(latest, timeAtDelete).now()).isNull();
assertThat(loadAtPointInTime(latest, timeAtDelete.plusMillis(1)).now()).isNull();

View File

@@ -1,45 +0,0 @@
// Copyright 2017 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.keyring.kms;
import com.google.common.io.BaseEncoding;
import org.bouncycastle.util.Arrays;
class FakeKmsConnection implements KmsConnection {
FakeKmsConnection() {}
/**
* Returns a dummy {@link EncryptResponse}.
*
* <p>The "encrypted value" in the response is the provided value reversed and then base64-encoded
* and the name of the cryptoKeyVersion is {@code cryptoKeyName + "/foo"}.
*/
@Override
public EncryptResponse encrypt(String cryptoKeyName, byte[] plaintext) {
return EncryptResponse.create(
BaseEncoding.base64().encode(Arrays.reverse(plaintext)), cryptoKeyName + "/foo");
}
/**
* Returns a "decrypted" plaintext.
*
* <p>The plaintext is the encodedCiphertext base64-decoded and then reversed.
*/
@Override
public byte[] decrypt(String cryptoKeyName, String encodedCiphertext) {
return Arrays.reverse(BaseEncoding.base64().decode(encodedCiphertext));
}
}

View File

@@ -1,187 +0,0 @@
// Copyright 2017 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.keyring.kms;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import google.registry.keyring.api.KeySerializer;
import google.registry.model.server.KmsSecret;
import google.registry.model.server.KmsSecretRevision;
import google.registry.privileges.secretmanager.FakeSecretManagerClient;
import google.registry.privileges.secretmanager.KeyringSecretStore;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.BouncyCastleProviderExtension;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import org.bouncycastle.openpgp.PGPKeyPair;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link KmsKeyring}. */
@DualDatabaseTest
class KmsKeyringTest {
@RegisterExtension
final BouncyCastleProviderExtension bouncy = new BouncyCastleProviderExtension();
@RegisterExtension
final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().build();
private KmsKeyring keyring;
private KeyringSecretStore fakeSecretStore =
new KeyringSecretStore(new FakeSecretManagerClient());
@BeforeEach
void beforeEach() {
keyring = new KmsKeyring(new FakeKmsConnection(), fakeSecretStore);
}
@TestOfyAndSql
void test_getRdeSigningKey() throws Exception {
saveKeyPairSecret("rde-signing-public", "rde-signing-private");
PGPKeyPair rdeSigningKey = keyring.getRdeSigningKey();
assertThat(KeySerializer.serializeKeyPair(rdeSigningKey))
.isEqualTo(KeySerializer.serializeKeyPair(KmsTestHelper.getKeyPair()));
}
@TestOfyAndSql
void test_getRdeStagingEncryptionKey() throws Exception {
savePublicKeySecret("rde-staging-public");
PGPPublicKey rdeStagingEncryptionKey = keyring.getRdeStagingEncryptionKey();
assertThat(rdeStagingEncryptionKey.getFingerprint())
.isEqualTo(KmsTestHelper.getPublicKey().getFingerprint());
}
@TestOfyAndSql
void test_getRdeStagingDecryptionKey() throws Exception {
savePrivateKeySecret("rde-staging-private");
savePublicKeySecret("rde-staging-public");
PGPPrivateKey rdeStagingDecryptionKey = keyring.getRdeStagingDecryptionKey();
PGPPublicKey rdeStagingEncryptionKey = keyring.getRdeStagingEncryptionKey();
PGPKeyPair keyPair = new PGPKeyPair(rdeStagingEncryptionKey, rdeStagingDecryptionKey);
assertThat(KeySerializer.serializeKeyPair(keyPair))
.isEqualTo(KeySerializer.serializeKeyPair(KmsTestHelper.getKeyPair()));
}
@TestOfyAndSql
void test_getRdeReceiverKey() throws Exception {
savePublicKeySecret("rde-receiver-public");
PGPPublicKey rdeReceiverKey = keyring.getRdeReceiverKey();
assertThat(rdeReceiverKey.getFingerprint())
.isEqualTo(KmsTestHelper.getPublicKey().getFingerprint());
}
@TestOfyAndSql
void test_getBrdaSigningKey() throws Exception {
saveKeyPairSecret("brda-signing-public", "brda-signing-private");
PGPKeyPair brdaSigningKey = keyring.getBrdaSigningKey();
assertThat(KeySerializer.serializeKeyPair(brdaSigningKey))
.isEqualTo(KeySerializer.serializeKeyPair(KmsTestHelper.getKeyPair()));
}
@TestOfyAndSql
void test_getBrdaReceiverKey() throws Exception {
savePublicKeySecret("brda-receiver-public");
PGPPublicKey brdaReceiverKey = keyring.getBrdaReceiverKey();
assertThat(brdaReceiverKey.getFingerprint())
.isEqualTo(KmsTestHelper.getPublicKey().getFingerprint());
}
@TestOfyAndSql
void test_getRdeSshClientPublicKey() {
saveCleartextSecret("rde-ssh-client-public-string");
String rdeSshClientPublicKey = keyring.getRdeSshClientPublicKey();
assertThat(rdeSshClientPublicKey).isEqualTo("rde-ssh-client-public-stringmoo");
}
@TestOfyAndSql
void test_getRdeSshClientPrivateKey() {
saveCleartextSecret("rde-ssh-client-private-string");
String rdeSshClientPrivateKey = keyring.getRdeSshClientPrivateKey();
assertThat(rdeSshClientPrivateKey).isEqualTo("rde-ssh-client-private-stringmoo");
}
@TestOfyAndSql
void test_getIcannReportingPassword() {
saveCleartextSecret("icann-reporting-password-string");
String icannReportingPassword = keyring.getIcannReportingPassword();
assertThat(icannReportingPassword).isEqualTo("icann-reporting-password-stringmoo");
}
@TestOfyAndSql
void test_getMarksdbDnlLoginAndPassword() {
saveCleartextSecret("marksdb-dnl-login-string");
String marksdbDnlLoginAndPassword = keyring.getMarksdbDnlLoginAndPassword();
assertThat(marksdbDnlLoginAndPassword).isEqualTo("marksdb-dnl-login-stringmoo");
}
@TestOfyAndSql
void test_getMarksdbLordnPassword() {
saveCleartextSecret("marksdb-lordn-password-string");
String marksdbLordnPassword = keyring.getMarksdbLordnPassword();
assertThat(marksdbLordnPassword).isEqualTo("marksdb-lordn-password-stringmoo");
}
@TestOfyAndSql
void test_getMarksdbSmdrlLoginAndPassword() {
saveCleartextSecret("marksdb-smdrl-login-string");
String marksdbSmdrlLoginAndPassword = keyring.getMarksdbSmdrlLoginAndPassword();
assertThat(marksdbSmdrlLoginAndPassword).isEqualTo("marksdb-smdrl-login-stringmoo");
}
@TestOfyAndSql
void test_getJsonCredential() {
saveCleartextSecret("json-credential-string");
String jsonCredential = keyring.getJsonCredential();
assertThat(jsonCredential).isEqualTo("json-credential-stringmoo");
}
private void persistSecret(String secretName, byte[] secretValue) {
KmsConnection kmsConnection = new FakeKmsConnection();
KmsSecretRevision secretRevision =
new KmsSecretRevision.Builder()
.setEncryptedValue(kmsConnection.encrypt(secretName, secretValue).ciphertext())
.setKmsCryptoKeyVersionName(KmsTestHelper.DUMMY_CRYPTO_KEY_VERSION)
.setParent(secretName)
.build();
KmsSecret secret = KmsSecret.create(secretName, secretRevision);
tm().transact(() -> tm().putAll(secretRevision, secret));
fakeSecretStore.createOrUpdateSecret(secretName, secretValue);
}
private void saveCleartextSecret(String secretName) {
persistSecret(secretName, KeySerializer.serializeString(secretName + "moo"));
}
private void savePublicKeySecret(String publicKeyName) throws Exception {
persistSecret(publicKeyName, KeySerializer.serializePublicKey(KmsTestHelper.getPublicKey()));
}
private void savePrivateKeySecret(String privateKeyName) throws Exception {
persistSecret(privateKeyName, KeySerializer.serializeKeyPair(KmsTestHelper.getKeyPair()));
}
private void saveKeyPairSecret(String publicKeyName, String privateKeyName) throws Exception {
savePublicKeySecret(publicKeyName);
savePrivateKeySecret(privateKeyName);
}
}

View File

@@ -26,7 +26,7 @@ import org.bouncycastle.openpgp.bc.BcPGPSecretKeyRing;
import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder;
import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider;
/** Stores dummy values for test use in {@link KmsUpdaterTest} and {@link KmsKeyringTest}. */
/** Stores dummy values for test use in {@link KmsUpdaterTest}. */
final class KmsTestHelper {
static final String DUMMY_CRYPTO_KEY_VERSION = "cheeseburger";

View File

@@ -15,226 +15,185 @@
package google.registry.keyring.kms;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.googlecode.objectify.Key;
import google.registry.keyring.api.KeySerializer;
import google.registry.model.server.KmsSecret;
import google.registry.model.server.KmsSecretRevision;
import google.registry.model.server.KmsSecretRevisionSqlDao;
import google.registry.persistence.VKey;
import google.registry.privileges.secretmanager.FakeSecretManagerClient;
import google.registry.privileges.secretmanager.KeyringSecretStore;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.BouncyCastleProviderExtension;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.bouncycastle.openpgp.PGPKeyPair;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link KmsUpdater} */
@DualDatabaseTest
/** Unit tests for {@link KmsKeyring} and {@link KmsUpdater} */
// TODO(2021-07-01): Rename this class along with KmsKeyring
public class KmsUpdaterTest {
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().build();
@RegisterExtension
public final BouncyCastleProviderExtension bouncy = new BouncyCastleProviderExtension();
private KeyringSecretStore secretStore;
private KmsUpdater updater;
private KmsKeyring keyring;
@BeforeEach
void beforeEach() {
updater =
new KmsUpdater(
new FakeKmsConnection(), new KeyringSecretStore(new FakeSecretManagerClient()));
secretStore = new KeyringSecretStore(new FakeSecretManagerClient());
updater = new KmsUpdater(secretStore);
keyring = new KmsKeyring(secretStore);
}
@TestOfyAndSql
void test_setMultipleSecrets() {
@Test
void setAndReadMultiple() {
String secretPrefix = "setAndReadMultiple_";
updater
.setMarksdbDnlLoginAndPassword("value1")
.setIcannReportingPassword("value2")
.setJsonCredential("value3")
.setMarksdbDnlLoginAndPassword(secretPrefix + "marksdb")
.setIcannReportingPassword(secretPrefix + "icann")
.setJsonCredential(secretPrefix + "json")
.update();
verifySecretAndSecretRevisionWritten(
"marksdb-dnl-login-string",
"marksdb-dnl-login-string/foo",
getCiphertext("value1"));
verifySecretAndSecretRevisionWritten(
"icann-reporting-password-string",
"icann-reporting-password-string/foo",
getCiphertext("value2"));
verifySecretAndSecretRevisionWritten(
"json-credential-string", "json-credential-string/foo", getCiphertext("value3"));
assertThat(keyring.getMarksdbDnlLoginAndPassword()).isEqualTo(secretPrefix + "marksdb");
assertThat(keyring.getIcannReportingPassword()).isEqualTo(secretPrefix + "icann");
assertThat(keyring.getJsonCredential()).isEqualTo(secretPrefix + "json");
verifyPersistedSecret("marksdb-dnl-login-string", secretPrefix + "marksdb");
verifyPersistedSecret("icann-reporting-password-string", secretPrefix + "icann");
verifyPersistedSecret("json-credential-string", secretPrefix + "json");
}
@TestOfyAndSql
void test_setBrdaReceiverKey() throws Exception {
updater.setBrdaReceiverPublicKey(KmsTestHelper.getPublicKey()).update();
@Test
void brdaReceiverKey() throws Exception {
PGPPublicKey publicKey = KmsTestHelper.getPublicKey();
updater.setBrdaReceiverPublicKey(publicKey).update();
verifySecretAndSecretRevisionWritten(
"brda-receiver-public",
"brda-receiver-public/foo",
getCiphertext(KmsTestHelper.getPublicKey()));
assertThat(keyring.getBrdaReceiverKey().getFingerprint()).isEqualTo(publicKey.getFingerprint());
verifyPersistedSecret("brda-receiver-public", serializePublicKey(publicKey));
}
@TestOfyAndSql
void test_setBrdaSigningKey() throws Exception {
updater.setBrdaSigningKey(KmsTestHelper.getKeyPair()).update();
@Test
void brdaSigningKey() throws Exception {
PGPKeyPair keyPair = KmsTestHelper.getKeyPair();
updater.setBrdaSigningKey(keyPair).update();
verifySecretAndSecretRevisionWritten(
"brda-signing-private",
"brda-signing-private/foo",
getCiphertext(KmsTestHelper.getKeyPair()));
verifySecretAndSecretRevisionWritten(
"brda-signing-public",
"brda-signing-public/foo",
getCiphertext(KmsTestHelper.getPublicKey()));
assertThat(serializeKeyPair(keyring.getBrdaSigningKey())).isEqualTo(serializeKeyPair(keyPair));
verifyPersistedSecret("brda-signing-private", serializeKeyPair(KmsTestHelper.getKeyPair()));
verifyPersistedSecret("brda-signing-public", serializePublicKey(KmsTestHelper.getPublicKey()));
}
@TestOfyAndSql
void test_setIcannReportingPassword() {
updater.setIcannReportingPassword("value1").update();
@Test
void icannReportingPassword() {
String secret = "icannReportingPassword";
updater.setIcannReportingPassword(secret).update();
verifySecretAndSecretRevisionWritten(
"icann-reporting-password-string",
"icann-reporting-password-string/foo",
getCiphertext("value1"));
assertThat(keyring.getIcannReportingPassword()).isEqualTo(secret);
verifyPersistedSecret("icann-reporting-password-string", secret);
}
@TestOfyAndSql
void test_setJsonCredential() {
updater.setJsonCredential("value1").update();
@Test
void jsonCredential() {
String secret = "jsonCredential";
updater.setJsonCredential(secret).update();
verifySecretAndSecretRevisionWritten(
"json-credential-string", "json-credential-string/foo", getCiphertext("value1"));
assertThat(keyring.getJsonCredential()).isEqualTo(secret);
verifyPersistedSecret("json-credential-string", secret);
}
@TestOfyAndSql
void test_setMarksdbDnlLoginAndPassword() {
updater.setMarksdbDnlLoginAndPassword("value1").update();
@Test
void marksdbDnlLoginAndPassword() {
String secret = "marksdbDnlLoginAndPassword";
updater.setMarksdbDnlLoginAndPassword(secret).update();
verifySecretAndSecretRevisionWritten(
"marksdb-dnl-login-string", "marksdb-dnl-login-string/foo", getCiphertext("value1"));
assertThat(keyring.getMarksdbDnlLoginAndPassword()).isEqualTo(secret);
verifyPersistedSecret("marksdb-dnl-login-string", secret);
}
@TestOfyAndSql
void test_setMarksdbLordnPassword() {
updater.setMarksdbLordnPassword("value1").update();
@Test
void marksdbLordnPassword() {
String secret = "marksdbLordnPassword";
updater.setMarksdbLordnPassword(secret).update();
verifySecretAndSecretRevisionWritten(
"marksdb-lordn-password-string",
"marksdb-lordn-password-string/foo",
getCiphertext("value1"));
assertThat(keyring.getMarksdbLordnPassword()).isEqualTo(secret);
verifyPersistedSecret("marksdb-lordn-password-string", secret);
}
@TestOfyAndSql
void test_setMarksdbSmdrlLoginAndPassword() {
updater.setMarksdbSmdrlLoginAndPassword("value1").update();
@Test
void marksdbSmdrlLoginAndPassword() {
String secret = "marksdbSmdrlLoginAndPassword";
updater.setMarksdbSmdrlLoginAndPassword(secret).update();
verifySecretAndSecretRevisionWritten(
"marksdb-smdrl-login-string", "marksdb-smdrl-login-string/foo", getCiphertext("value1"));
assertThat(keyring.getMarksdbSmdrlLoginAndPassword()).isEqualTo(secret);
verifyPersistedSecret("marksdb-smdrl-login-string", secret);
}
@TestOfyAndSql
void test_setRdeReceiverKey() throws Exception {
updater.setRdeReceiverPublicKey(KmsTestHelper.getPublicKey()).update();
@Test
void rdeReceiverKey() throws Exception {
PGPPublicKey publicKey = KmsTestHelper.getPublicKey();
updater.setRdeReceiverPublicKey(publicKey).update();
verifySecretAndSecretRevisionWritten(
"rde-receiver-public",
"rde-receiver-public/foo",
getCiphertext(
KeySerializer.serializePublicKey(KmsTestHelper.getPublicKey())));
assertThat(keyring.getRdeReceiverKey().getFingerprint()).isEqualTo(publicKey.getFingerprint());
verifyPersistedSecret("rde-receiver-public", serializePublicKey(KmsTestHelper.getPublicKey()));
}
@TestOfyAndSql
void test_setRdeSigningKey() throws Exception {
updater.setRdeSigningKey(KmsTestHelper.getKeyPair()).update();
@Test
void rdeSigningKey() throws Exception {
PGPKeyPair keyPair = KmsTestHelper.getKeyPair();
updater.setRdeSigningKey(keyPair).update();
verifySecretAndSecretRevisionWritten(
"rde-signing-private",
"rde-signing-private/foo",
getCiphertext(KmsTestHelper.getKeyPair()));
verifySecretAndSecretRevisionWritten(
"rde-signing-public",
"rde-signing-public/foo",
getCiphertext(KmsTestHelper.getPublicKey()));
assertThat(serializeKeyPair(keyring.getRdeSigningKey())).isEqualTo(serializeKeyPair(keyPair));
verifyPersistedSecret("rde-signing-private", serializeKeyPair(keyPair));
verifyPersistedSecret("rde-signing-public", serializePublicKey(keyPair.getPublicKey()));
}
@TestOfyAndSql
void test_setRdeSshClientPrivateKey() {
updater.setRdeSshClientPrivateKey("value1").update();
@Test
void rdeSshClientPrivateKey() {
String secret = "rdeSshClientPrivateKey";
updater.setRdeSshClientPrivateKey(secret).update();
verifySecretAndSecretRevisionWritten(
"rde-ssh-client-private-string",
"rde-ssh-client-private-string/foo",
getCiphertext("value1"));
assertThat(keyring.getRdeSshClientPrivateKey()).isEqualTo(secret);
verifyPersistedSecret("rde-ssh-client-private-string", secret);
}
@TestOfyAndSql
void test_setRdeSshClientPublicKey() {
updater.setRdeSshClientPublicKey("value1").update();
@Test
void rdeSshClientPublicKey() {
String secret = "rdeSshClientPublicKey";
updater.setRdeSshClientPublicKey(secret).update();
verifySecretAndSecretRevisionWritten(
"rde-ssh-client-public-string",
"rde-ssh-client-public-string/foo",
getCiphertext("value1"));
assertThat(keyring.getRdeSshClientPublicKey()).isEqualTo(secret);
verifyPersistedSecret("rde-ssh-client-public-string", secret);
}
@TestOfyAndSql
void test_setRdeStagingKey() throws Exception {
updater.setRdeStagingKey(KmsTestHelper.getKeyPair()).update();
@Test
void rdeStagingKey() throws Exception {
PGPKeyPair keyPair = KmsTestHelper.getKeyPair();
updater.setRdeStagingKey(keyPair).update();
verifySecretAndSecretRevisionWritten(
"rde-staging-private",
"rde-staging-private/foo",
getCiphertext(KmsTestHelper.getKeyPair()));
verifySecretAndSecretRevisionWritten(
"rde-staging-public",
"rde-staging-public/foo",
getCiphertext(KmsTestHelper.getPublicKey()));
assertThat(serializePublicKey(keyring.getRdeStagingEncryptionKey()))
.isEqualTo(serializePublicKey(keyPair.getPublicKey()));
// Since we do not have dedicated tools to compare private keys, we leverage key-pair
// serialization util to compare private keys.
assertThat(
serializeKeyPair(
new PGPKeyPair(
keyring.getRdeStagingEncryptionKey(), keyring.getRdeStagingDecryptionKey())))
.isEqualTo(serializeKeyPair(keyPair));
verifyPersistedSecret("rde-staging-private", serializeKeyPair(keyPair));
verifyPersistedSecret("rde-staging-public", serializePublicKey(KmsTestHelper.getPublicKey()));
}
private static void verifySecretAndSecretRevisionWritten(
String secretName, String expectedCryptoKeyVersionName, String expectedEncryptedValue) {
KmsSecretRevision secretRevision;
if (tm().isOfy()) {
KmsSecret secret =
tm().loadByKey(
VKey.createOfy(
KmsSecret.class, Key.create(getCrossTldKey(), KmsSecret.class, secretName)));
assertThat(secret).isNotNull();
secretRevision =
tm().loadByKey(VKey.createOfy(KmsSecretRevision.class, secret.getLatestRevision()));
} else {
secretRevision =
tm().transact(() -> KmsSecretRevisionSqlDao.getLatestRevision(secretName).get());
}
assertThat(secretRevision.getKmsCryptoKeyVersionName()).isEqualTo(expectedCryptoKeyVersionName);
assertThat(secretRevision.getEncryptedValue()).isEqualTo(expectedEncryptedValue);
private void verifyPersistedSecret(String secretName, String expectedPlainTextValue) {
assertThat(new String(secretStore.getSecret(secretName), StandardCharsets.UTF_8))
.isEqualTo(expectedPlainTextValue);
}
private static String getCiphertext(byte[] plaintext) {
return new FakeKmsConnection().encrypt("blah", plaintext).ciphertext();
private static String serializePublicKey(PGPPublicKey publicKey) throws IOException {
return new String(KeySerializer.serializePublicKey(publicKey), StandardCharsets.UTF_8);
}
private static String getCiphertext(String plaintext) {
return getCiphertext(KeySerializer.serializeString(plaintext));
}
private static String getCiphertext(PGPPublicKey publicKey) throws IOException {
return getCiphertext(KeySerializer.serializePublicKey(publicKey));
}
private static String getCiphertext(PGPKeyPair keyPair) throws Exception {
return getCiphertext(KeySerializer.serializeKeyPair(keyPair));
private static String serializeKeyPair(PGPKeyPair keyPair) throws Exception {
return new String(KeySerializer.serializeKeyPair(keyPair), StandardCharsets.UTF_8);
}
}

View File

@@ -41,14 +41,18 @@ import org.junit.jupiter.api.extension.RegisterExtension;
@DualDatabaseTest
class EppResourceUtilsTest {
private final FakeClock clock = new FakeClock(DateTime.now(UTC));
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().withTaskQueue().build();
AppEngineExtension.builder()
.withDatastoreAndCloudSql()
.withClock(clock)
.withTaskQueue()
.build();
@RegisterExtension public final InjectExtension inject = new InjectExtension();
private final FakeClock clock = new FakeClock(DateTime.now(UTC));
@BeforeEach
void beforeEach() {
createTld("tld");

View File

@@ -0,0 +1,173 @@
// 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.common;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.DATASTORE_ONLY;
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.DATASTORE_PRIMARY;
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.DATASTORE_PRIMARY_READ_ONLY;
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.SQL_ONLY;
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.SQL_PRIMARY;
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.SQL_PRIMARY_READ_ONLY;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.EntityTestCase;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
@BeforeEach
void beforeEach() {
fakeClock.setAutoIncrementByOneMilli();
}
@Test
void testEmpty_returnsDatastoreOnlyMap() {
assertThat(DatabaseMigrationStateSchedule.getUncached())
.isEqualTo(DatabaseMigrationStateSchedule.DEFAULT_TRANSITION_MAP);
}
@Test
void testValidTransitions() {
// First, verify that no-ops are safe
for (MigrationState migrationState : MigrationState.values()) {
runValidTransition(migrationState, migrationState);
}
// Next, the transitions that will actually cause a change
runValidTransition(DATASTORE_ONLY, DATASTORE_PRIMARY);
runValidTransition(DATASTORE_PRIMARY, DATASTORE_ONLY);
runValidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_READ_ONLY);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_ONLY);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_PRIMARY);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_PRIMARY_READ_ONLY);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_PRIMARY);
runValidTransition(SQL_PRIMARY_READ_ONLY, DATASTORE_PRIMARY_READ_ONLY);
runValidTransition(SQL_PRIMARY_READ_ONLY, SQL_PRIMARY);
runValidTransition(SQL_PRIMARY, SQL_PRIMARY_READ_ONLY);
runValidTransition(SQL_PRIMARY, SQL_ONLY);
runValidTransition(SQL_ONLY, SQL_PRIMARY);
}
@Test
void testInvalidTransitions() {
runInvalidTransition(DATASTORE_ONLY, DATASTORE_PRIMARY_READ_ONLY);
runInvalidTransition(DATASTORE_ONLY, SQL_PRIMARY_READ_ONLY);
runInvalidTransition(DATASTORE_ONLY, SQL_PRIMARY);
runInvalidTransition(DATASTORE_ONLY, SQL_ONLY);
runInvalidTransition(DATASTORE_PRIMARY, SQL_PRIMARY_READ_ONLY);
runInvalidTransition(DATASTORE_PRIMARY, SQL_PRIMARY);
runInvalidTransition(DATASTORE_PRIMARY, SQL_ONLY);
runInvalidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_ONLY);
runInvalidTransition(SQL_PRIMARY_READ_ONLY, DATASTORE_ONLY);
runInvalidTransition(SQL_PRIMARY_READ_ONLY, DATASTORE_PRIMARY);
runInvalidTransition(SQL_PRIMARY_READ_ONLY, SQL_ONLY);
runInvalidTransition(SQL_PRIMARY, DATASTORE_ONLY);
runInvalidTransition(SQL_PRIMARY, DATASTORE_PRIMARY);
runInvalidTransition(SQL_PRIMARY, DATASTORE_PRIMARY_READ_ONLY);
runInvalidTransition(SQL_ONLY, DATASTORE_ONLY);
runInvalidTransition(SQL_ONLY, DATASTORE_PRIMARY);
runInvalidTransition(SQL_ONLY, DATASTORE_PRIMARY_READ_ONLY);
}
@Test
void testFailure_newMapImpliesInvalidChangeNow() {
DateTime startTime = fakeClock.nowUtc();
fakeClock.advanceBy(Duration.standardHours(6));
// The new map is valid by itself, but not with the current state of DATASTORE_ONLY because the
// new map implies that the current state is DATASTORE_PRIMARY_READ_ONLY
ImmutableSortedMap<DateTime, MigrationState> nowInvalidMap =
ImmutableSortedMap.<DateTime, MigrationState>naturalOrder()
.put(START_OF_TIME, DATASTORE_ONLY)
.put(startTime.plusHours(1), DATASTORE_PRIMARY)
.put(startTime.plusHours(2), DATASTORE_PRIMARY_READ_ONLY)
.build();
assertThat(
assertThrows(
IllegalArgumentException.class,
() -> ofyTm().transact(() -> DatabaseMigrationStateSchedule.set(nowInvalidMap))))
.hasMessageThat()
.isEqualTo(
"Cannot transition from current state-as-of-now DATASTORE_ONLY "
+ "to new state-as-of-now DATASTORE_PRIMARY_READ_ONLY");
}
@Test
void testFailure_notInTransaction() {
assertThat(
assertThrows(
IllegalStateException.class,
() ->
DatabaseMigrationStateSchedule.set(
DatabaseMigrationStateSchedule.DEFAULT_TRANSITION_MAP.toValueMap())))
.hasMessageThat()
.isEqualTo("Must be called in a transaction");
}
private void runValidTransition(MigrationState from, MigrationState to) {
ImmutableSortedMap<DateTime, MigrationState> transitions =
createMapEndingWithTransition(from, to);
ofyTm().transact(() -> DatabaseMigrationStateSchedule.set(transitions));
assertThat(DatabaseMigrationStateSchedule.getUncached().toValueMap())
.containsExactlyEntriesIn(transitions);
}
private void runInvalidTransition(MigrationState from, MigrationState to) {
ImmutableSortedMap<DateTime, MigrationState> transitions =
createMapEndingWithTransition(from, to);
assertThat(
assertThrows(
IllegalArgumentException.class,
() -> ofyTm().transact(() -> DatabaseMigrationStateSchedule.set(transitions))))
.hasMessageThat()
.isEqualTo(
String.format("validStateTransitions map cannot transition from %s to %s.", from, to));
}
// Create a transition map that is valid up to the "from" transition, then add the "to" transition
private ImmutableSortedMap<DateTime, MigrationState> createMapEndingWithTransition(
MigrationState from, MigrationState to) {
ImmutableSortedMap.Builder<DateTime, MigrationState> builder =
ImmutableSortedMap.naturalOrder();
builder.put(START_OF_TIME, DATASTORE_ONLY);
MigrationState[] allMigrationStates = MigrationState.values();
for (int i = 0; i < allMigrationStates.length; i++) {
builder.put(fakeClock.nowUtc().plusMinutes(i), allMigrationStates[i]);
if (allMigrationStates[i].equals(from)) {
break;
}
}
builder.put(fakeClock.nowUtc().plusDays(1), to);
return builder.build();
}
}

View File

@@ -1,111 +0,0 @@
// 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.common;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.DatabaseMigrationStateWrapper.MigrationState.DATASTORE_ONLY;
import static google.registry.model.common.DatabaseMigrationStateWrapper.MigrationState.DATASTORE_PRIMARY;
import static google.registry.model.common.DatabaseMigrationStateWrapper.MigrationState.DATASTORE_PRIMARY_READ_ONLY;
import static google.registry.model.common.DatabaseMigrationStateWrapper.MigrationState.SQL_ONLY;
import static google.registry.model.common.DatabaseMigrationStateWrapper.MigrationState.SQL_PRIMARY;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.model.common.DatabaseMigrationStateWrapper.MigrationState;
import google.registry.testing.AppEngineExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
public class DatabaseMigrationStateWrapperTest {
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().build();
@Test
void testEmpty_returnsDatastore() {
assertThat(DatabaseMigrationStateWrapper.get()).isEqualTo(DATASTORE_ONLY);
}
@Test
void testEmpty_canChangeToDatastorePrimary() {
DatabaseMigrationStateWrapper.set(DATASTORE_PRIMARY);
assertThat(DatabaseMigrationStateWrapper.get()).isEqualTo(DATASTORE_PRIMARY);
}
@Test
void testValidTransitions() {
runValidTransition(DATASTORE_ONLY, DATASTORE_PRIMARY);
runValidTransition(DATASTORE_PRIMARY, DATASTORE_ONLY);
runValidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_READ_ONLY);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_ONLY);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_PRIMARY);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_PRIMARY);
runValidTransition(SQL_PRIMARY, DATASTORE_PRIMARY_READ_ONLY);
runValidTransition(SQL_PRIMARY, SQL_ONLY);
runValidTransition(SQL_ONLY, SQL_PRIMARY);
}
@Test
void testInvalidTransitions() {
runInvalidTransition(DATASTORE_ONLY, DATASTORE_ONLY);
runInvalidTransition(DATASTORE_ONLY, DATASTORE_PRIMARY_READ_ONLY);
runInvalidTransition(DATASTORE_ONLY, SQL_PRIMARY);
runInvalidTransition(DATASTORE_ONLY, SQL_ONLY);
runInvalidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY);
runInvalidTransition(DATASTORE_PRIMARY, SQL_PRIMARY);
runInvalidTransition(DATASTORE_PRIMARY, SQL_ONLY);
runInvalidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_PRIMARY_READ_ONLY);
runInvalidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_ONLY);
runInvalidTransition(SQL_PRIMARY, DATASTORE_ONLY);
runInvalidTransition(SQL_PRIMARY, DATASTORE_PRIMARY);
runInvalidTransition(SQL_PRIMARY, SQL_PRIMARY);
runInvalidTransition(SQL_ONLY, DATASTORE_ONLY);
runInvalidTransition(SQL_ONLY, DATASTORE_PRIMARY);
runInvalidTransition(SQL_ONLY, DATASTORE_PRIMARY_READ_ONLY);
runInvalidTransition(SQL_ONLY, SQL_ONLY);
}
private static void runValidTransition(MigrationState from, MigrationState to) {
setStateForced(from);
DatabaseMigrationStateWrapper.set(to);
assertThat(DatabaseMigrationStateWrapper.get()).isEqualTo(to);
}
private static void runInvalidTransition(MigrationState from, MigrationState to) {
setStateForced(from);
assertThat(
assertThrows(
IllegalArgumentException.class, () -> DatabaseMigrationStateWrapper.set(to)))
.hasMessageThat()
.isEqualTo(
String.format(
"Moving from migration state %s to %s is not a valid transition", from, to));
}
private static void setStateForced(MigrationState migrationState) {
DatabaseMigrationStateWrapper wrapper = new DatabaseMigrationStateWrapper(migrationState);
ofyTm().transact(() -> ofyTm().put(wrapper));
assertThat(DatabaseMigrationStateWrapper.get()).isEqualTo(migrationState);
}
}

View File

@@ -1,79 +0,0 @@
// 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.common;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.EntityTestCase;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabaseTransition;
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
import org.joda.time.Duration;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link DatabaseTransitionSchedule}. */
public class DatabaseTransitionScheduleTest extends EntityTestCase {
@Test
void testSuccess_persistence() {
TimedTransitionProperty<PrimaryDatabase, PrimaryDatabaseTransition> databaseTransitions =
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(START_OF_TIME, PrimaryDatabase.DATASTORE),
PrimaryDatabaseTransition.class);
DatabaseTransitionSchedule schedule =
DatabaseTransitionSchedule.create(
TransitionId.SIGNED_MARK_REVOCATION_LIST, databaseTransitions);
ofyTm().transactNew(() -> ofyTm().put(schedule));
assertThat(
DatabaseTransitionSchedule.get(TransitionId.SIGNED_MARK_REVOCATION_LIST)
.get()
.databaseTransitions)
.isEqualTo(databaseTransitions);
}
@Test
void testFailure_scheduleWithNoStartOfTime() {
assertThrows(
IllegalArgumentException.class,
() ->
DatabaseTransitionSchedule.create(
TransitionId.SIGNED_MARK_REVOCATION_LIST,
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(fakeClock.nowUtc(), PrimaryDatabase.DATASTORE),
PrimaryDatabaseTransition.class)));
}
@Test
void testSuccess_getPrimaryDatabase() {
DatabaseTransitionSchedule schedule =
DatabaseTransitionSchedule.create(
TransitionId.SIGNED_MARK_REVOCATION_LIST,
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(
START_OF_TIME,
PrimaryDatabase.DATASTORE,
fakeClock.nowUtc().plusDays(1),
PrimaryDatabase.CLOUD_SQL),
PrimaryDatabaseTransition.class));
assertThat(ofyTm().transact(schedule::getPrimaryDatabase)).isEqualTo(PrimaryDatabase.DATASTORE);
fakeClock.advanceBy(Duration.standardDays(5));
assertThat(ofyTm().transact(schedule::getPrimaryDatabase)).isEqualTo(PrimaryDatabase.CLOUD_SQL);
}
}

View File

@@ -16,7 +16,7 @@ package google.registry.model.index;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.config.RegistryConfig.getEppResourceIndexBucketCount;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -44,7 +44,7 @@ class EppResourceIndexTest extends EntityTestCase {
@Test
void testPersistence() {
EppResourceIndex loadedIndex = Iterables.getOnlyElement(getEppResourceIndexObjects());
assertThat(ofy().load().key(loadedIndex.reference).now()).isEqualTo(contact);
assertThat(auditedOfy().load().key(loadedIndex.reference).now()).isEqualTo(contact);
}
@Test
@@ -56,7 +56,7 @@ class EppResourceIndexTest extends EntityTestCase {
void testIdempotentOnUpdate() {
contact = persistResource(contact.asBuilder().setEmailAddress("abc@def.fake").build());
EppResourceIndex loadedIndex = Iterables.getOnlyElement(getEppResourceIndexObjects());
assertThat(ofy().load().key(loadedIndex.reference).now()).isEqualTo(contact);
assertThat(auditedOfy().load().key(loadedIndex.reference).now()).isEqualTo(contact);
}
/**
@@ -65,9 +65,11 @@ class EppResourceIndexTest extends EntityTestCase {
private static ImmutableList<EppResourceIndex> getEppResourceIndexObjects() {
ImmutableList.Builder<EppResourceIndex> indexEntities = new ImmutableList.Builder<>();
for (int i = 0; i < getEppResourceIndexBucketCount(); i++) {
indexEntities.addAll(ofy().load()
.type(EppResourceIndex.class)
.ancestor(Key.create(EppResourceIndexBucket.class, i + 1)));
indexEntities.addAll(
auditedOfy()
.load()
.type(EppResourceIndex.class)
.ancestor(Key.create(EppResourceIndexBucket.class, i + 1)));
}
return indexEntities.build();
}

View File

@@ -59,8 +59,7 @@ class CriteriaQueryBuilderTest {
.transact(
() ->
jpaTm()
.getEntityManager()
.createQuery(
.query(
CriteriaQueryBuilder.create(CriteriaQueryBuilderTestEntity.class)
.build())
.getResultList()))
@@ -77,10 +76,11 @@ class CriteriaQueryBuilderTest {
CriteriaQuery<CriteriaQueryBuilderTestEntity> query =
CriteriaQueryBuilder.create(CriteriaQueryBuilderTestEntity.class)
.where(
"data", jpaTm().getEntityManager().getCriteriaBuilder()::equal,
"data",
jpaTm().getEntityManager().getCriteriaBuilder()::equal,
"zztz")
.build();
return jpaTm().getEntityManager().createQuery(query).getResultList();
return jpaTm().query(query).getResultList();
});
assertThat(result).containsExactly(entity2);
}
@@ -96,7 +96,7 @@ class CriteriaQueryBuilderTest {
.where(
"data", jpaTm().getEntityManager().getCriteriaBuilder()::like, "a%")
.build();
return jpaTm().getEntityManager().createQuery(query).getResultList();
return jpaTm().query(query).getResultList();
});
assertThat(result).containsExactly(entity3);
}
@@ -112,7 +112,7 @@ class CriteriaQueryBuilderTest {
.where(
"data", jpaTm().getEntityManager().getCriteriaBuilder()::like, "%a%")
.build();
return jpaTm().getEntityManager().createQuery(query).getResultList();
return jpaTm().query(query).getResultList();
});
assertThat(result).containsExactly(entity1, entity3).inOrder();
}
@@ -132,7 +132,7 @@ class CriteriaQueryBuilderTest {
.where(
"data", jpaTm().getEntityManager().getCriteriaBuilder()::like, "%t%")
.build();
return jpaTm().getEntityManager().createQuery(query).getResultList();
return jpaTm().query(query).getResultList();
});
assertThat(result).containsExactly(entity1);
}
@@ -147,7 +147,7 @@ class CriteriaQueryBuilderTest {
CriteriaQueryBuilder.create(CriteriaQueryBuilderTestEntity.class)
.whereFieldIsIn("data", ImmutableList.of("aaa", "bbb"))
.build();
return jpaTm().getEntityManager().createQuery(query).getResultList();
return jpaTm().query(query).getResultList();
});
assertThat(result).containsExactly(entity3).inOrder();
}
@@ -162,7 +162,7 @@ class CriteriaQueryBuilderTest {
CriteriaQueryBuilder.create(CriteriaQueryBuilderTestEntity.class)
.whereFieldIsIn("data", ImmutableList.of("aaa", "bbb", "data"))
.build();
return jpaTm().getEntityManager().createQuery(query).getResultList();
return jpaTm().query(query).getResultList();
});
assertThat(result).containsExactly(entity1, entity3).inOrder();
}
@@ -179,7 +179,7 @@ class CriteriaQueryBuilderTest {
.where(
"data", jpaTm().getEntityManager().getCriteriaBuilder()::like, "%a%")
.build();
return jpaTm().getEntityManager().createQuery(query).getResultList();
return jpaTm().query(query).getResultList();
});
assertThat(result).containsExactly(entity3, entity1).inOrder();
}
@@ -194,7 +194,7 @@ class CriteriaQueryBuilderTest {
CriteriaQueryBuilder.create(CriteriaQueryBuilderTestEntity.class)
.orderByDesc("data")
.build();
return jpaTm().getEntityManager().createQuery(query).getResultList();
return jpaTm().query(query).getResultList();
});
assertThat(result).containsExactly(entity2, entity1, entity3).inOrder();
}

View File

@@ -508,6 +508,19 @@ class JpaTransactionManagerImplTest {
assertThat(persisted).containsExactlyElementsIn(moreEntities);
}
@Test
void loadSingleton_detaches() {
jpaTm().transact(() -> jpaTm().insert(theEntity));
jpaTm()
.transact(
() ->
assertThat(
jpaTm()
.getEntityManager()
.contains(jpaTm().loadSingleton(TestEntity.class).get())))
.isFalse();
}
@Test
void delete_succeeds() {
jpaTm().transact(() -> jpaTm().insert(theEntity));
@@ -572,6 +585,29 @@ class JpaTransactionManagerImplTest {
.contains("Inserted/updated object reloaded: ");
}
@Test
void cqQuery_detaches() {
jpaTm().transact(() -> jpaTm().insertAll(moreEntities));
jpaTm()
.transact(
() ->
assertThat(
jpaTm()
.getEntityManager()
.contains(
jpaTm()
.query(
CriteriaQueryBuilder.create(TestEntity.class)
.where(
"name",
jpaTm().getEntityManager().getCriteriaBuilder()
::equal,
"entity1")
.build())
.getSingleResult()))
.isFalse());
}
@Test
void loadAfterPut_fails() {
assertThat(
@@ -588,6 +624,36 @@ class JpaTransactionManagerImplTest {
.contains("Inserted/updated object reloaded: ");
}
@Test
void query_detachesResults() {
jpaTm().transact(() -> jpaTm().insertAll(moreEntities));
jpaTm()
.transact(
() ->
jpaTm().query("FROM TestEntity", TestEntity.class).getResultList().stream()
.forEach(e -> assertThat(jpaTm().getEntityManager().contains(e)).isFalse()));
jpaTm()
.transact(
() ->
jpaTm()
.query("FROM TestEntity", TestEntity.class)
.getResultStream()
.forEach(e -> assertThat(jpaTm().getEntityManager().contains(e)).isFalse()));
jpaTm()
.transact(
() ->
assertThat(
jpaTm()
.getEntityManager()
.contains(
jpaTm()
.query(
"FROM TestEntity WHERE name = 'entity1'", TestEntity.class)
.getSingleResult()))
.isFalse());
}
private void insertPerson(int age) {
jpaTm()
.getEntityManager()

View File

@@ -50,6 +50,7 @@ class GenerateSpec11ReportActionTest extends BeamActionTestBase {
"gs://reporting-project/reporting-bucket/",
"api_key/a",
clock.nowUtc().toLocalDate(),
"DATASTORE",
clock,
response,
dataflow);
@@ -71,6 +72,7 @@ class GenerateSpec11ReportActionTest extends BeamActionTestBase {
"gs://reporting-project/reporting-bucket/",
"api_key/a",
clock.nowUtc().toLocalDate(),
"DATASTORE",
clock,
response,
dataflow);

View File

@@ -18,19 +18,30 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.testing.LogsSubject.assertAboutLogs;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.testing.TestLogHandler;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import google.registry.config.RegistryConfig;
import google.registry.model.ImmutableObject;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import google.registry.model.ofy.CommitLogBucket;
import google.registry.model.ofy.Ofy;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.TransactionEntity;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.InjectExtension;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -38,29 +49,51 @@ import org.junit.jupiter.api.extension.RegisterExtension;
public class ReplicateToDatastoreActionTest {
private final FakeClock fakeClock = new FakeClock(DateTime.parse("2000-01-01TZ"));
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder()
.withDatastoreAndCloudSql()
.withOfyTestEntities(TestEntity.class)
.withJpaUnitTestEntities(TestEntity.class)
.withClock(fakeClock)
.build();
ReplicateToDatastoreAction task = new ReplicateToDatastoreAction();
@RegisterExtension final InjectExtension injectExtension = new InjectExtension();
TestLogHandler logHandler;
public ReplicateToDatastoreActionTest() {}
private final ReplicateToDatastoreAction task = new ReplicateToDatastoreAction(fakeClock);
private final TestLogHandler logHandler = new TestLogHandler();
@BeforeEach
public void setUp() {
injectExtension.setStaticField(Ofy.class, "clock", fakeClock);
// Use a single bucket to expose timestamp inversion problems.
injectExtension.setStaticField(
CommitLogBucket.class, "bucketIdSupplier", Suppliers.ofInstance(1));
fakeClock.setAutoIncrementByOneMilli();
RegistryConfig.overrideCloudSqlReplicateTransactions(true);
logHandler = new TestLogHandler();
Logger.getLogger(ReplicateToDatastoreAction.class.getCanonicalName()).addHandler(logHandler);
DateTime now = fakeClock.nowUtc();
ofyTm()
.transact(
() ->
DatabaseMigrationStateSchedule.set(
ImmutableSortedMap.of(
START_OF_TIME,
MigrationState.DATASTORE_ONLY,
now,
MigrationState.DATASTORE_PRIMARY,
now.plusHours(1),
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
now.plusHours(2),
MigrationState.SQL_PRIMARY)));
fakeClock.advanceBy(Duration.standardDays(1));
}
@AfterEach
public void tearDown() {
fakeClock.disableAutoIncrement();
RegistryConfig.overrideCloudSqlReplicateTransactions(false);
}
@@ -170,6 +203,36 @@ public class ReplicateToDatastoreActionTest {
"Missing transaction: last transaction id = -1, next available transaction = 1");
}
@Test
void testNotInMigrationState_doesNothing() {
// set a schedule that backtracks the current status to DATASTORE_PRIMARY_READ_ONLY
DateTime now = fakeClock.nowUtc();
ofyTm()
.transact(
() ->
DatabaseMigrationStateSchedule.set(
ImmutableSortedMap.<DateTime, MigrationState>naturalOrder()
.put(START_OF_TIME, MigrationState.DATASTORE_ONLY)
.put(START_OF_TIME.plusHours(1), MigrationState.DATASTORE_PRIMARY)
.put(START_OF_TIME.plusHours(2), MigrationState.DATASTORE_PRIMARY_READ_ONLY)
.put(START_OF_TIME.plusHours(3), MigrationState.SQL_PRIMARY)
.put(now.plusHours(1), MigrationState.SQL_PRIMARY_READ_ONLY)
.put(now.plusHours(2), MigrationState.DATASTORE_PRIMARY_READ_ONLY)
.build()));
fakeClock.advanceBy(Duration.standardDays(1));
jpaTm().transact(() -> jpaTm().insert(new TestEntity("foo")));
task.run();
// Replication shouldn't have happened
assertThat(ofyTm().loadAllOf(TestEntity.class)).isEmpty();
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(
Level.INFO,
"Skipping ReplicateToDatastoreAction because we are in migration phase "
+ "DATASTORE_PRIMARY_READ_ONLY.");
}
@Entity(name = "ReplicationTestEntity")
@javax.persistence.Entity(name = "TestEntity")
private static class TestEntity extends ImmutableObject {

View File

@@ -17,12 +17,10 @@ package google.registry.testing;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.io.Files.asCharSink;
import static com.google.common.truth.Truth.assertWithMessage;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.persistSimpleResources;
import static google.registry.testing.DualDatabaseTestInvocationContextProvider.injectTmForDualDatabaseTest;
import static google.registry.testing.DualDatabaseTestInvocationContextProvider.restoreTmAfterDualDatabaseTest;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static google.registry.util.ResourceUtils.readResourceUtf8;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -42,16 +40,10 @@ import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.ObjectifyFilter;
import google.registry.model.common.DatabaseTransitionSchedule;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabaseTransition;
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.model.ofy.ObjectifyService;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.Registrar.State;
@@ -402,18 +394,8 @@ public final class AppEngineExtension implements BeforeEachCallback, AfterEachCa
if (withDatastore && !withoutCannedData) {
loadInitialData();
}
} else {
// If we're using SQL, set replayed entities to use SQL
DatabaseTransitionSchedule schedule =
DatabaseTransitionSchedule.create(
TransitionId.REPLAYED_ENTITIES,
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(START_OF_TIME, PrimaryDatabase.CLOUD_SQL),
PrimaryDatabaseTransition.class));
tm().transactNew(() -> auditedOfy().saveWithoutBackup().entity(schedule).now());
if (withCloudSql && !withJpaUnitTest && !withoutCannedData) {
loadInitialData();
}
} else if (withCloudSql && !withJpaUnitTest && !withoutCannedData) {
loadInitialData();
}
}

View File

@@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Suppliers.memoize;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Iterables.toArray;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
@@ -110,6 +111,7 @@ import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey;
import google.registry.schema.tld.PremiumListDao;
import google.registry.tmch.LordnTaskUtils;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
@@ -182,11 +184,10 @@ public class DatabaseHelper {
domainName, generateNewDomainRoid(getTldFromDomainName(domainName)), contact);
}
public static DomainBase newDomainBase(String domainName, HostResource host) {
return newDomainBase(domainName)
.asBuilder()
.setNameservers(ImmutableSet.of(host.createVKey()))
.build();
public static DomainBase newDomainBase(String domainName, HostResource... hosts) {
ImmutableSet<VKey<HostResource>> hostKeys =
Arrays.stream(hosts).map(HostResource::createVKey).collect(toImmutableSet());
return newDomainBase(domainName).asBuilder().setNameservers(hostKeys).build();
}
public static DomainBase newDomainBase(

View File

@@ -162,6 +162,20 @@ class CreateDomainCommandTest extends EppToolCommandTestCase<CreateDomainCommand
+ "sending total cost for 1 year(s) of USD 877.00.");
}
@Test
void testSuccess_reasonAndRegistrarRequest() throws Exception {
createTld("tld");
runCommandForced(
"--client=NewRegistrar",
"--registrant=crr-admin",
"--admins=crr-admin",
"--techs=crr-tech",
"--reason=\"Creating test domain\"",
"--registrar_request=false",
"example.tld");
eppVerifier.verifySent("domain_create_metadata.xml");
}
@Test
void testFailure_duplicateDomains() {
IllegalArgumentException thrown =
@@ -175,7 +189,7 @@ class CreateDomainCommandTest extends EppToolCommandTestCase<CreateDomainCommand
"--techs=crr-tech",
"example.tld",
"example.tld"));
assertThat(thrown).hasMessageThat().contains("Duplicate arguments found: \'example.tld\'");
assertThat(thrown).hasMessageThat().contains("Duplicate arguments found: 'example.tld'");
}
@Test

View File

@@ -0,0 +1,65 @@
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.tools;
import static google.registry.model.common.DatabaseMigrationStateSchedule.DEFAULT_TRANSITION_MAP;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
/** Tests for {@link GetDatabaseMigrationStateCommand}. */
@DualDatabaseTest
public class GetDatabaseMigrationStateCommandTest
extends CommandTestCase<GetDatabaseMigrationStateCommand> {
@BeforeEach
void beforeEach() {
ofyTm().transact(() -> DatabaseMigrationStateSchedule.set(DEFAULT_TRANSITION_MAP.toValueMap()));
}
@TestOfyAndSql
void testInitial_returnsDatastoreOnly() throws Exception {
runCommand();
assertStdoutIs(
String.format("Current migration schedule: %s\n", DEFAULT_TRANSITION_MAP.toValueMap()));
}
@TestOfyAndSql
void testFullSchedule() throws Exception {
DateTime now = fakeClock.nowUtc();
ImmutableSortedMap<DateTime, MigrationState> transitions =
ImmutableSortedMap.of(
START_OF_TIME,
MigrationState.DATASTORE_ONLY,
now.plusHours(1),
MigrationState.DATASTORE_PRIMARY,
now.plusHours(2),
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
now.plusHours(3),
MigrationState.SQL_PRIMARY,
now.plusHours(4),
MigrationState.SQL_ONLY);
ofyTm().transact(() -> DatabaseMigrationStateSchedule.set(transitions));
runCommand();
assertStdoutIs(String.format("Current migration schedule: %s\n", transitions));
}
}

View File

@@ -1,132 +0,0 @@
// 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.tools;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.beust.jcommander.ParameterException;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.common.DatabaseTransitionSchedule;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabaseTransition;
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.model.ofy.Ofy;
import google.registry.testing.InjectExtension;
import google.registry.testing.SetClockExtension;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link GetDatabaseTransitionScheduleCommand} */
public class GetDatabaseTransitionScheduleCommandTest
extends CommandTestCase<GetDatabaseTransitionScheduleCommand> {
@Order(value = Order.DEFAULT - 3)
@RegisterExtension
final SetClockExtension setClockExtension =
new SetClockExtension(fakeClock, "1984-12-21T06:07:08.789Z");
@RegisterExtension public final InjectExtension inject = new InjectExtension();
@BeforeEach
void beforeEach() {
inject.setStaticField(Ofy.class, "clock", fakeClock);
}
@Test
void testSuccess() throws Exception {
TimedTransitionProperty<PrimaryDatabase, PrimaryDatabaseTransition> databaseTransitions =
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(START_OF_TIME, PrimaryDatabase.DATASTORE),
PrimaryDatabaseTransition.class);
DatabaseTransitionSchedule schedule =
DatabaseTransitionSchedule.create(
TransitionId.SIGNED_MARK_REVOCATION_LIST, databaseTransitions);
ofyTm().transactNew(() -> ofyTm().put(schedule));
runCommand("SIGNED_MARK_REVOCATION_LIST");
assertStdoutIs(
"SIGNED_MARK_REVOCATION_LIST(last updated at 1984-12-21T06:07:08.789Z):"
+ " {1970-01-01T00:00:00.000Z=DATASTORE}\n");
}
@Test
void testSuccess_multipleArguments() throws Exception {
TimedTransitionProperty<PrimaryDatabase, PrimaryDatabaseTransition> databaseTransitions =
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(START_OF_TIME, PrimaryDatabase.DATASTORE),
PrimaryDatabaseTransition.class);
DatabaseTransitionSchedule schedule =
DatabaseTransitionSchedule.create(TransitionId.DOMAIN_LABEL_LISTS, databaseTransitions);
ofyTm().transactNew(() -> ofyTm().put(schedule));
fakeClock.advanceOneMilli(); // Now 1984-12-21T06:07:08.790Z
TimedTransitionProperty<PrimaryDatabase, PrimaryDatabaseTransition> databaseTransitions2 =
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(
START_OF_TIME,
PrimaryDatabase.DATASTORE,
DateTime.parse("2020-10-01T00:00:00Z"),
PrimaryDatabase.CLOUD_SQL),
PrimaryDatabaseTransition.class);
DatabaseTransitionSchedule schedule2 =
DatabaseTransitionSchedule.create(
TransitionId.SIGNED_MARK_REVOCATION_LIST, databaseTransitions2);
ofyTm().transactNew(() -> ofyTm().put(schedule2));
runCommand("DOMAIN_LABEL_LISTS", "SIGNED_MARK_REVOCATION_LIST");
assertStdoutIs(
"DOMAIN_LABEL_LISTS(last updated at 1984-12-21T06:07:08.789Z):"
+ " {1970-01-01T00:00:00.000Z=DATASTORE}\n"
+ "SIGNED_MARK_REVOCATION_LIST(last updated at 1984-12-21T06:07:08.790Z):"
+ " {1970-01-01T00:00:00.000Z=DATASTORE, 2020-10-01T00:00:00.000Z=CLOUD_SQL}\n");
}
@Test
void testFailure_scheduleDoesNotExist() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class, () -> runCommand("SIGNED_MARK_REVOCATION_LIST"));
assertThat(thrown)
.hasMessageThat()
.contains("A database transition schedule for SIGNED_MARK_REVOCATION_LIST does not exist");
}
@Test
void testFailure_noIdGiven() {
assertThrows(ParameterException.class, this::runCommand);
}
@Test
void testFailure_oneScheduleDoesNotExist() {
TimedTransitionProperty<PrimaryDatabase, PrimaryDatabaseTransition> databaseTransitions =
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(START_OF_TIME, PrimaryDatabase.DATASTORE),
PrimaryDatabaseTransition.class);
DatabaseTransitionSchedule schedule =
DatabaseTransitionSchedule.create(TransitionId.DOMAIN_LABEL_LISTS, databaseTransitions);
ofyTm().transactNew(() -> ofyTm().put(schedule));
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> runCommand("DOMAIN_LABEL_LISTS", "SIGNED_MARK_REVOCATION_LIST"));
assertThat(thrown)
.hasMessageThat()
.contains("A database transition schedule for SIGNED_MARK_REVOCATION_LIST does not exist");
}
}

View File

@@ -0,0 +1,178 @@
// 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.tools;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.model.common.DatabaseMigrationStateSchedule.DEFAULT_TRANSITION_MAP;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.beust.jcommander.ParameterException;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.TestOfyAndSql;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
/** Tests for {@link SetDatabaseMigrationStateCommand}. */
@DualDatabaseTest
public class SetDatabaseMigrationStateCommandTest
extends CommandTestCase<SetDatabaseMigrationStateCommand> {
@BeforeEach
void beforeEach() {
// clear out any static state that may have been persisted
ofyTm()
.transact(
() ->
ofyTm()
.loadSingleton(DatabaseMigrationStateSchedule.class)
.ifPresent(ofyTm()::delete));
DatabaseMigrationStateSchedule.CACHE.invalidateAll();
}
@TestOfyAndSql
void testSuccess_setsBasicSchedule() throws Exception {
assertThat(DatabaseMigrationStateSchedule.get()).isEqualTo(DEFAULT_TRANSITION_MAP);
assertThat(ofyTm().transact(() -> ofyTm().loadSingleton(DatabaseMigrationStateSchedule.class)))
.isEmpty();
runCommandForced("--migration_schedule=1970-01-01T00:00:00.000Z=DATASTORE_ONLY");
// use a raw ofy call to check what's in the DB
ofyTm()
.transact(
() ->
assertThat(
ofyTm()
.loadSingleton(DatabaseMigrationStateSchedule.class)
.get()
.migrationTransitions)
.isEqualTo(DEFAULT_TRANSITION_MAP));
assertThat(DatabaseMigrationStateSchedule.get()).isEqualTo(DEFAULT_TRANSITION_MAP);
}
@TestOfyAndSql
void testSuccess_fullSchedule() throws Exception {
DateTime now = fakeClock.nowUtc();
DateTime datastorePrimary = now.plusHours(1);
DateTime datastorePrimaryReadOnly = now.plusHours(2);
DateTime sqlPrimary = now.plusHours(3);
DateTime sqlOnly = now.plusHours(4);
runCommandForced(
String.format(
"--migration_schedule=%s=DATASTORE_ONLY,%s=DATASTORE_PRIMARY,"
+ "%s=DATASTORE_PRIMARY_READ_ONLY,%s=SQL_PRIMARY,%s=SQL_ONLY",
START_OF_TIME, datastorePrimary, datastorePrimaryReadOnly, sqlPrimary, sqlOnly));
assertThat(DatabaseMigrationStateSchedule.get().toValueMap())
.containsExactlyEntriesIn(
ImmutableSortedMap.of(
START_OF_TIME,
MigrationState.DATASTORE_ONLY,
datastorePrimary,
MigrationState.DATASTORE_PRIMARY,
datastorePrimaryReadOnly,
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
sqlPrimary,
MigrationState.SQL_PRIMARY,
sqlOnly,
MigrationState.SQL_ONLY));
}
@TestOfyAndSql
void testSuccess_warnsOnChangeSoon() throws Exception {
DateTime now = fakeClock.nowUtc();
runCommandForced(
String.format(
"--migration_schedule=%s=DATASTORE_ONLY,%s=DATASTORE_PRIMARY",
START_OF_TIME, now.plusMinutes(1)));
assertThat(DatabaseMigrationStateSchedule.get().toValueMap())
.containsExactlyEntriesIn(
ImmutableSortedMap.of(
START_OF_TIME,
MigrationState.DATASTORE_ONLY,
now.plusMinutes(1),
MigrationState.DATASTORE_PRIMARY));
assertInStdout("MAY BE DANGEROUS");
}
@TestOfyAndSql
void testSuccess_goesBackward() throws Exception {
DateTime now = fakeClock.nowUtc();
runCommandForced(
String.format(
"--migration_schedule=%s=DATASTORE_ONLY,%s=DATASTORE_PRIMARY,"
+ "%s=DATASTORE_PRIMARY_READ_ONLY,%s=DATASTORE_PRIMARY",
START_OF_TIME, now.plusHours(1), now.plusHours(2), now.plusHours(3)));
assertThat(DatabaseMigrationStateSchedule.get().toValueMap())
.containsExactlyEntriesIn(
ImmutableSortedMap.of(
START_OF_TIME,
MigrationState.DATASTORE_ONLY,
now.plusHours(1),
MigrationState.DATASTORE_PRIMARY,
now.plusHours(2),
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
now.plusHours(3),
MigrationState.DATASTORE_PRIMARY));
}
@TestOfyAndSql
void testFailure_invalidTransition() {
assertThat(
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
String.format(
"--migration_schedule=%s=DATASTORE_ONLY,%s=DATASTORE_PRIMARY_READ_ONLY",
START_OF_TIME, START_OF_TIME.plusHours(1)))))
.hasMessageThat()
.isEqualTo(
"validStateTransitions map cannot transition from DATASTORE_ONLY "
+ "to DATASTORE_PRIMARY_READ_ONLY.");
}
@TestOfyAndSql
void testFailure_invalidTransitionFromOldToNew() {
// The map we pass in is valid by itself, but we can't go from DATASTORE_ONLY now to
// DATASTORE_PRIMARY_READ_ONLY now
DateTime now = fakeClock.nowUtc();
assertThat(
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
String.format(
"--migration_schedule=%s=DATASTORE_ONLY,"
+ "%s=DATASTORE_PRIMARY,%s=DATASTORE_PRIMARY_READ_ONLY",
START_OF_TIME, now.minusHours(2), now.minusHours(1)))))
.hasMessageThat()
.isEqualTo(
"Cannot transition from current state-as-of-now DATASTORE_ONLY "
+ "to new state-as-of-now DATASTORE_PRIMARY_READ_ONLY");
}
@TestOfyAndSql
void testFailure_invalidParams() {
assertThrows(ParameterException.class, this::runCommandForced);
assertThrows(ParameterException.class, () -> runCommandForced("--migration_schedule=FOOBAR"));
assertThrows(
ParameterException.class,
() -> runCommandForced("--migration_schedule=1970-01-01T00:00:00.000Z=FOOBAR"));
}
}

View File

@@ -1,134 +0,0 @@
// 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.tools;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.ofyTm;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.beust.jcommander.ParameterException;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.truth.Truth8;
import com.googlecode.objectify.Key;
import google.registry.model.common.DatabaseTransitionSchedule;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabaseTransition;
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.persistence.VKey;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link SetDatabaseTransitionScheduleCommand}. */
public class SetDatabaseTransitionScheduleCommandTest
extends CommandTestCase<SetDatabaseTransitionScheduleCommand> {
@BeforeEach
void setup() {
fakeClock.setTo(DateTime.parse("2020-12-01T00:00:00Z"));
}
@Test
void testFailure_noTransitionId() throws Exception {
ParameterException thrown =
assertThrows(
ParameterException.class,
() -> runCommandForced("--transition_schedule=2021-04-14T00:00:00.000Z=DATASTORE"));
assertThat(thrown).hasMessageThat().contains("--transition_id");
}
@Test
void testSuccess_currentScheduleIsEmpty() throws Exception {
Truth8.assertThat(
ofyTm()
.loadByKeyIfPresent(
VKey.createOfy(
DatabaseTransitionSchedule.class,
Key.create(getCrossTldKey(), DatabaseTransitionSchedule.class, "test"))))
.isEmpty();
runCommandForced(
"--transition_id=SIGNED_MARK_REVOCATION_LIST",
"--transition_schedule=1970-01-01T00:00:00.000Z=DATASTORE");
assertThat(
ofyTm()
.transact(
() ->
DatabaseTransitionSchedule.get(TransitionId.SIGNED_MARK_REVOCATION_LIST)
.get()
.getPrimaryDatabase()))
.isEqualTo(PrimaryDatabase.DATASTORE);
assertThat(command.prompt())
.isEqualTo(
"Insert new schedule {1970-01-01T00:00:00.000Z=DATASTORE} "
+ "for transition ID SIGNED_MARK_REVOCATION_LIST?");
}
@Test
void testSuccess() throws Exception {
ImmutableSortedMap<DateTime, PrimaryDatabase> transitionMap =
ImmutableSortedMap.of(
START_OF_TIME,
PrimaryDatabase.DATASTORE,
fakeClock.nowUtc().minusDays(1),
PrimaryDatabase.CLOUD_SQL);
persistResource(
DatabaseTransitionSchedule.create(
TransitionId.SIGNED_MARK_REVOCATION_LIST,
TimedTransitionProperty.fromValueMap(transitionMap, PrimaryDatabaseTransition.class)));
assertThat(
DatabaseTransitionSchedule.get(TransitionId.SIGNED_MARK_REVOCATION_LIST)
.get()
.getDatabaseTransitions())
.isEqualTo(transitionMap);
runCommandForced(
"--transition_id=SIGNED_MARK_REVOCATION_LIST",
"--transition_schedule=1970-01-01T00:00:00.000Z=DATASTORE,"
+ "2020-11-30T00:00:00.000Z=CLOUD_SQL,2020-12-06T00:00:00.000Z=DATASTORE");
ImmutableSortedMap<DateTime, PrimaryDatabase> retrievedTransitionMap =
ofyTm()
.transact(
() ->
DatabaseTransitionSchedule.get(TransitionId.SIGNED_MARK_REVOCATION_LIST)
.get()
.getDatabaseTransitions());
assertThat(retrievedTransitionMap)
.containsExactly(
START_OF_TIME,
PrimaryDatabase.DATASTORE,
fakeClock.nowUtc().minusDays(1),
PrimaryDatabase.CLOUD_SQL,
fakeClock.nowUtc().plusDays(5),
PrimaryDatabase.DATASTORE);
fakeClock.advanceBy(Duration.standardDays(5));
assertThat(
ofyTm()
.transact(
() ->
DatabaseTransitionSchedule.get(TransitionId.SIGNED_MARK_REVOCATION_LIST)
.get()
.getPrimaryDatabase()))
.isEqualTo(PrimaryDatabase.DATASTORE);
assertThat(command.prompt())
.isEqualTo(
"Insert new schedule {1970-01-01T00:00:00.000Z=DATASTORE, "
+ "2020-11-30T00:00:00.000Z=CLOUD_SQL, 2020-12-06T00:00:00.000Z=DATASTORE} "
+ "for transition ID SIGNED_MARK_REVOCATION_LIST?");
}
}

View File

@@ -197,6 +197,18 @@ class UpdateDomainCommandTest extends EppToolCommandTestCase<UpdateDomainCommand
eppVerifier.verifySent("domain_update_change.xml");
}
@TestOfyAndSql
void testSuccess_change_reasonAndRegistrarRequest() throws Exception {
runCommandForced(
"--client=NewRegistrar",
"--registrant=crr-admin",
"--password=2fooBAR",
"--reason=\"Testing domain update\"",
"--registrar_request=false",
"example.tld");
eppVerifier.verifySent("domain_update_change_metadata.xml");
}
@TestOfyAndSql
void testSuccess_setNameservers() throws Exception {
HostResource host1 = persistActiveHost("ns1.zdns.google");

View File

@@ -21,7 +21,6 @@ import static com.google.common.collect.Iterables.filter;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.newContactResource;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -78,7 +77,9 @@ class KillAllCommitLogsActionTest extends MapreduceTestCase<KillAllCommitLogsAct
START_OF_TIME.plusDays(1),
ImmutableMap.of(1, bucketTime, 2, bucketTime, 3, bucketTime)));
for (Class<?> clazz : AFFECTED_TYPES) {
assertWithMessage("entities of type " + clazz).that(ofy().load().type(clazz)).isNotEmpty();
assertWithMessage("entities of type " + clazz)
.that(auditedOfy().load().type(clazz))
.isNotEmpty();
}
ImmutableList<?> otherStuff =
Streams.stream(auditedOfy().load())

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