1
0
mirror of https://github.com/google/nomulus synced 2026-01-30 01:22:23 +00:00

Compare commits

...

4 Commits

Author SHA1 Message Date
Rachel Guan
743dea9ca2 Add renewal price behavior to AllocationToken (#1580) 2022-04-04 18:51:49 -04:00
sarahcaseybot
41f9f1ef7d Change use of BillingIdentifier to BillingAccountMap in invoicing pipeline (#1577)
* Change billingIdentifier to BillingAccountMap in invoicing pipeline

* Add a default for billing account map

* Throw error on missing PAK

* Add unit test
2022-04-04 16:16:43 -04:00
Michael Muller
44ede2b022 Check for error suggesting another nomulus running (#1582)
Check for a PSQLException referencing a failed connection to "google:5433",
which likely indicates that there is another nomulus tool instance running.

It's worth giving this hint because in cases like this it's not at all obvious
that the other instance of nomulus is problematic.
2022-04-04 11:14:43 -04:00
Ben McIlwain
e4312322dc Add a no-async actions DB migration phase (#1579)
* Add a no-async actions DB migration phase

This needs to be set several hours prior to entering the READONLY stage. This is
not a read-only stage; all synchronous actions under Datastore (such as domain
creates) will continue to succeed. The only thing that will fail is host
deletes, host renames, and contact deletes, as these three actions require a
mapreduce to run before they are complete, and we don't want mapreduces hanging
around and executing during what is supposed to be a short duration READONLY
period.
2022-04-01 16:55:51 -04:00
25 changed files with 2909 additions and 2634 deletions

View File

@@ -14,6 +14,7 @@
package google.registry.beam.invoicing;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.beam.BeamUtils.getQueryFromFile;
import static org.apache.beam.sdk.values.TypeDescriptors.strings;
@@ -53,6 +54,7 @@ import org.apache.beam.sdk.transforms.PTransform;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.TypeDescriptor;
import org.joda.money.CurrencyUnit;
/**
* Definition of a Dataflow Flex pipeline template, which generates a given month's invoices.
@@ -122,12 +124,19 @@ public class InvoicingPipeline implements Serializable {
google.registry.model.billing.BillingEvent.OneTime oneTime =
(google.registry.model.billing.BillingEvent.OneTime) row[0];
Registrar registrar = (Registrar) row[1];
CurrencyUnit currency = oneTime.getCost().getCurrencyUnit();
checkState(
registrar.getBillingAccountMap().containsKey(currency),
"Registrar %s does not have a product account key for the currency unit: %s",
registrar.getRegistrarId(),
currency);
return BillingEvent.create(
oneTime.getId(),
DateTimeUtils.toZonedDateTime(oneTime.getBillingTime(), ZoneId.of("UTC")),
DateTimeUtils.toZonedDateTime(oneTime.getEventTime(), ZoneId.of("UTC")),
registrar.getRegistrarId(),
registrar.getBillingIdentifier().toString(),
registrar.getBillingAccountMap().get(currency),
registrar.getPoNumber().orElse(""),
DomainNameUtils.getTldFromDomainName(oneTime.getTargetId()),
oneTime.getReason().toString(),

View File

@@ -25,6 +25,7 @@ import static google.registry.model.ResourceTransferUtils.handlePendingTransferO
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
import static google.registry.model.transfer.TransferStatus.SERVER_CANCELLED;
import static google.registry.persistence.transaction.TransactionManagerFactory.assertAsyncActionsAreAllowed;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
@@ -94,6 +95,7 @@ public final class ContactDeleteFlow implements TransactionalFlow {
extensionManager.register(MetadataExtension.class);
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
assertAsyncActionsAreAllowed();
DateTime now = tm().getTransactionTime();
checkLinkedDomains(targetId, now, ContactResource.class, DomainBase::getReferencedContacts);
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);

View File

@@ -22,6 +22,7 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.flows.host.HostFlowUtils.validateHostName;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
import static google.registry.persistence.transaction.TransactionManagerFactory.assertAsyncActionsAreAllowed;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
@@ -96,6 +97,7 @@ public final class HostDeleteFlow implements TransactionalFlow {
extensionManager.register(MetadataExtension.class);
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
assertAsyncActionsAreAllowed();
DateTime now = tm().getTransactionTime();
validateHostName(targetId);
checkLinkedDomains(targetId, now, HostResource.class, DomainBase::getNameservers);

View File

@@ -28,6 +28,7 @@ import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomain
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership;
import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey;
import static google.registry.model.reporting.HistoryEntry.Type.HOST_UPDATE;
import static google.registry.persistence.transaction.TransactionManagerFactory.assertAsyncActionsAreAllowed;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
@@ -136,6 +137,9 @@ public final class HostUpdateFlow implements TransactionalFlow {
validateHostName(targetId);
HostResource existingHost = loadAndVerifyExistence(HostResource.class, targetId, now);
boolean isHostRename = suppliedNewHostName != null;
if (isHostRename) {
assertAsyncActionsAreAllowed();
}
String oldHostName = targetId;
String newHostName = firstNonNull(suppliedNewHostName, oldHostName);
DomainBase oldSuperordinateDomain =

View File

@@ -30,6 +30,7 @@ import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.common.TimedTransitionProperty.TimedTransition;
import google.registry.model.replay.SqlOnlyEntity;
import java.time.Duration;
import java.util.Arrays;
import javax.persistence.Entity;
import javax.persistence.PersistenceException;
import org.joda.time.DateTime;
@@ -62,11 +63,28 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
* not the phase is read-only.
*/
public enum MigrationState {
/** Datastore is the only DB being used. */
DATASTORE_ONLY(PrimaryDatabase.DATASTORE, false, ReplayDirection.NO_REPLAY),
/** Datastore is the primary DB, with changes replicated to Cloud SQL. */
DATASTORE_PRIMARY(PrimaryDatabase.DATASTORE, false, ReplayDirection.DATASTORE_TO_SQL),
/** Datastore is the primary DB, with replication, and async actions are disallowed. */
DATASTORE_PRIMARY_NO_ASYNC(PrimaryDatabase.DATASTORE, false, ReplayDirection.DATASTORE_TO_SQL),
/** Datastore is the primary DB, with replication, and all mutating actions are disallowed. */
DATASTORE_PRIMARY_READ_ONLY(PrimaryDatabase.DATASTORE, true, ReplayDirection.DATASTORE_TO_SQL),
/**
* Cloud SQL is the primary DB, with replication back to Datastore, and all mutating actions are
* disallowed.
*/
SQL_PRIMARY_READ_ONLY(PrimaryDatabase.CLOUD_SQL, true, ReplayDirection.SQL_TO_DATASTORE),
/** Cloud SQL is the primary DB, with changes replicated to Datastore. */
SQL_PRIMARY(PrimaryDatabase.CLOUD_SQL, false, ReplayDirection.SQL_TO_DATASTORE),
/** Cloud SQL is the only DB being used. */
SQL_ONLY(PrimaryDatabase.CLOUD_SQL, false, ReplayDirection.NO_REPLAY);
private final PrimaryDatabase primaryDatabase;
@@ -146,11 +164,17 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
.putAll(
MigrationState.DATASTORE_PRIMARY,
MigrationState.DATASTORE_ONLY,
MigrationState.DATASTORE_PRIMARY_NO_ASYNC)
.putAll(
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
MigrationState.DATASTORE_ONLY,
MigrationState.DATASTORE_PRIMARY,
MigrationState.DATASTORE_PRIMARY_READ_ONLY)
.putAll(
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
MigrationState.DATASTORE_ONLY,
MigrationState.DATASTORE_PRIMARY,
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
MigrationState.SQL_PRIMARY_READ_ONLY,
MigrationState.SQL_PRIMARY)
.putAll(
@@ -165,10 +189,9 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
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);
}
Arrays.stream(MigrationState.values()).forEach(state -> builder.put(state, state));
return builder.build();
}
@@ -246,7 +269,7 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
* 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.
* time to the new map at the current time must also be valid.
*/
private static void validateTransitionAtCurrentTime(
TimedTransitionProperty<MigrationState, MigrationStateTransition> newTransitions) {

View File

@@ -15,6 +15,7 @@
package google.registry.persistence.transaction;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.DATASTORE_PRIMARY_NO_ASYNC;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static org.joda.time.DateTimeZone.UTC;
@@ -23,6 +24,7 @@ import com.google.appengine.api.utils.SystemProperty.Environment.Value;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Suppliers;
import google.registry.config.RegistryEnvironment;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.PrimaryDatabase;
import google.registry.model.ofy.DatastoreTransactionManager;
@@ -198,6 +200,22 @@ public final class TransactionManagerFactory {
}
}
/**
* Asserts that async actions (contact/host deletes and host renames) are allowed.
*
* <p>These are allowed at all times except during the {@link
* DatabaseMigrationStateSchedule.MigrationState#DATASTORE_PRIMARY_NO_ASYNC} stage. Note that
* {@link ReadOnlyModeException} may well be thrown during other read-only stages inside the
* transaction manager; this method specifically checks only async actions.
*/
@DeleteAfterMigration
public static void assertAsyncActionsAreAllowed() {
if (DatabaseMigrationStateSchedule.getValueAtTime(DateTime.now(UTC))
.equals(DATASTORE_PRIMARY_NO_ASYNC)) {
throw new ReadOnlyModeException();
}
}
/** Registry is currently undergoing maintenance and is in read-only mode. */
public static class ReadOnlyModeException extends IllegalStateException {
public ReadOnlyModeException() {

View File

@@ -38,7 +38,9 @@ import java.io.ByteArrayInputStream;
import java.net.URL;
import java.security.Security;
import java.util.Map;
import java.util.Optional;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.postgresql.util.PSQLException;
/** Container class to create and run remote commands against a Datastore instance. */
@Parameters(separators = " =", commandDescription = "Command-line interface to the registry")
@@ -178,14 +180,30 @@ final class RegistryCli implements AutoCloseable, CommandRunner {
try {
runCommand(command);
} catch (RuntimeException ex) {
if (Throwables.getRootCause(ex) instanceof LoginRequiredException) {
} catch (RuntimeException e) {
if (Throwables.getRootCause(e) instanceof LoginRequiredException) {
System.err.println("===================================================================");
System.err.println("You must login using 'nomulus login' prior to running this command.");
System.err.println("===================================================================");
System.exit(1);
} else {
throw ex;
// See if this looks like the error we get when there's another instance of nomulus tool
// running against SQL and give the user some additional guidance if so.
Optional<Throwable> psqlException =
Throwables.getCausalChain(e).stream()
.filter(x -> x instanceof PSQLException)
.findFirst();
if (psqlException.isPresent() && psqlException.get().getMessage().contains("google:5432")) {
e.printStackTrace();
System.err.println("===================================================================");
System.err.println(
"This error is likely the result of having another instance of\n"
+ "nomulus running at the same time. Check your system, shut down\n"
+ "the other instance, and try again.");
System.err.println("===================================================================");
} else {
throw e;
}
}
}
}

View File

@@ -31,7 +31,7 @@ JOIN Domain d ON b.domainRepoId = d.repoId
JOIN Tld t ON t.tldStrId = d.tld
LEFT JOIN BillingCancellation c ON b.id = c.refOneTime.billingId
LEFT JOIN BillingCancellation cr ON b.cancellationMatchingBillingEvent = cr.refRecurring.billingId
WHERE r.billingIdentifier IS NOT NULL
WHERE r.billingAccountMap IS NOT NULL
AND r.type = 'REAL'
AND t.invoicingEnabled IS TRUE
AND b.billingTime BETWEEN CAST('%FIRST_TIMESTAMP_OF_MONTH%' AS timestamp) AND CAST('%LAST_TIMESTAMP_OF_MONTH%' AS timestamp)

View File

@@ -24,6 +24,10 @@ import static google.registry.testing.DatabaseHelper.persistNewRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.CAD;
import static org.joda.money.CurrencyUnit.JPY;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -55,6 +59,7 @@ import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Map.Entry;
import java.util.Optional;
import org.apache.beam.sdk.Pipeline.PipelineExecutionException;
import org.apache.beam.sdk.coders.SerializableCoder;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.testing.PAssert;
@@ -294,6 +299,37 @@ class InvoicingPipelineTest {
pipeline.run().waitUntilFinish();
}
@Test
void testFailure_readFromCloudSqlMissingPAK() throws Exception {
Registrar registrar = persistNewRegistrar("TheRegistrar");
registrar =
registrar
.asBuilder()
.setBillingAccountMap(ImmutableMap.of(USD, "789"))
.setPoNumber(Optional.of("22446688"))
.build();
persistResource(registrar);
Registry test =
newRegistry("test", "_TEST", ImmutableSortedMap.of(START_OF_TIME, GENERAL_AVAILABILITY))
.asBuilder()
.setInvoicingEnabled(true)
.build();
persistResource(test);
DomainBase domain = persistActiveDomain("mycanadiandomain.test");
persistOneTimeBillingEvent(1, domain, registrar, Reason.RENEW, 3, Money.of(CAD, 20.5));
PCollection<BillingEvent> billingEvents = InvoicingPipeline.readFromCloudSql(options, pipeline);
billingEvents = billingEvents.apply(new ChangeDomainRepo());
PAssert.that(billingEvents).empty();
PipelineExecutionException thrown =
assertThrows(PipelineExecutionException.class, () -> pipeline.run().waitUntilFinish());
assertThat(thrown)
.hasMessageThat()
.contains(
"Registrar TheRegistrar does not have a product account key for the currency unit:"
+ " CAD");
}
@Test
void testSuccess_saveInvoiceCsv() throws Exception {
InvoicingPipeline.saveInvoiceCsv(billingEvents, options);
@@ -338,7 +374,7 @@ class InvoicingPipelineTest {
+ "LEFT JOIN BillingCancellation c ON b.id = c.refOneTime.billingId\n"
+ "LEFT JOIN BillingCancellation cr ON b.cancellationMatchingBillingEvent ="
+ " cr.refRecurring.billingId\n"
+ "WHERE r.billingIdentifier IS NOT NULL\n"
+ "WHERE r.billingAccountMap IS NOT NULL\n"
+ "AND r.type = 'REAL'\n"
+ "AND t.invoicingEnabled IS TRUE\n"
+ "AND b.billingTime BETWEEN CAST('2017-10-01' AS timestamp) AND CAST('2017-11-01'"
@@ -362,18 +398,22 @@ class InvoicingPipelineTest {
persistNewRegistrar("NewRegistrar");
persistNewRegistrar("TheRegistrar");
Registrar registrar1 = persistNewRegistrar("theRegistrar");
registrar1 = registrar1.asBuilder().setBillingIdentifier(234L).build();
registrar1 =
registrar1
.asBuilder()
.setBillingAccountMap(ImmutableMap.of(JPY, "234", USD, "234"))
.build();
persistResource(registrar1);
Registrar registrar2 = persistNewRegistrar("bestdomains");
registrar2 =
registrar2
.asBuilder()
.setBillingIdentifier(456L)
.setBillingAccountMap(ImmutableMap.of(USD, "456"))
.setPoNumber(Optional.of("116688"))
.build();
persistResource(registrar2);
Registrar registrar3 = persistNewRegistrar("anotherRegistrar");
registrar3 = registrar3.asBuilder().setBillingIdentifier(789L).build();
registrar3 = registrar3.asBuilder().setBillingAccountMap(ImmutableMap.of(USD, "789")).build();
persistResource(registrar3);
Registry test =
@@ -397,10 +437,8 @@ class InvoicingPipelineTest {
DomainBase domain6 = persistActiveDomain("locked.test");
DomainBase domain7 = persistActiveDomain("update-prohibited.test");
persistOneTimeBillingEvent(
1, domain1, registrar1, Reason.RENEW, 3, Money.of(CurrencyUnit.USD, 20.5));
persistOneTimeBillingEvent(
2, domain2, registrar1, Reason.RENEW, 3, Money.of(CurrencyUnit.USD, 20.5));
persistOneTimeBillingEvent(1, domain1, registrar1, Reason.RENEW, 3, Money.of(USD, 20.5));
persistOneTimeBillingEvent(2, domain2, registrar1, Reason.RENEW, 3, Money.of(USD, 20.5));
persistOneTimeBillingEvent(
3,
domain3,
@@ -410,31 +448,27 @@ class InvoicingPipelineTest {
Money.ofMajor(CurrencyUnit.JPY, 70),
DateTime.parse("2017-09-29T00:00:00.0Z"),
DateTime.parse("2017-10-02T00:00:00.0Z"));
persistOneTimeBillingEvent(
4, domain4, registrar2, Reason.RENEW, 1, Money.of(CurrencyUnit.USD, 20.5));
persistOneTimeBillingEvent(4, domain4, registrar2, Reason.RENEW, 1, Money.of(USD, 20.5));
persistOneTimeBillingEvent(
5,
domain5,
registrar3,
Reason.CREATE,
1,
Money.of(CurrencyUnit.USD, 0),
Money.of(USD, 0),
DateTime.parse("2017-10-04T00:00:00.0Z"),
DateTime.parse("2017-10-04T00:00:00.0Z"),
Flag.SUNRISE,
Flag.ANCHOR_TENANT);
persistOneTimeBillingEvent(
6, domain6, registrar1, Reason.SERVER_STATUS, 0, Money.of(CurrencyUnit.USD, 0));
persistOneTimeBillingEvent(
7, domain7, registrar1, Reason.SERVER_STATUS, 0, Money.of(CurrencyUnit.USD, 20));
persistOneTimeBillingEvent(6, domain6, registrar1, Reason.SERVER_STATUS, 0, Money.of(USD, 0));
persistOneTimeBillingEvent(7, domain7, registrar1, Reason.SERVER_STATUS, 0, Money.of(USD, 20));
// Add billing event for a non-billable registrar
Registrar registrar4 = persistNewRegistrar("noBillRegistrar");
registrar4 = registrar4.asBuilder().setBillingIdentifier(null).build();
registrar4 = registrar4.asBuilder().setBillingAccountMap(null).build();
persistResource(registrar4);
DomainBase domain8 = persistActiveDomain("non-billable.test");
persistOneTimeBillingEvent(
8, domain8, registrar4, Reason.RENEW, 3, Money.of(CurrencyUnit.USD, 20.5));
persistOneTimeBillingEvent(8, domain8, registrar4, Reason.RENEW, 3, Money.of(USD, 20.5));
// Add billing event for a non-real registrar
Registrar registrar5 = persistNewRegistrar("notRealRegistrar");
@@ -442,19 +476,17 @@ class InvoicingPipelineTest {
registrar5
.asBuilder()
.setIanaIdentifier(null)
.setBillingIdentifier(456L)
.setBillingAccountMap(ImmutableMap.of(USD, "456"))
.setType(Registrar.Type.OTE)
.build();
persistResource(registrar5);
DomainBase domain9 = persistActiveDomain("not-real.test");
persistOneTimeBillingEvent(
9, domain9, registrar5, Reason.RENEW, 3, Money.of(CurrencyUnit.USD, 20.5));
persistOneTimeBillingEvent(9, domain9, registrar5, Reason.RENEW, 3, Money.of(USD, 20.5));
// Add billing event for a non-invoicing TLD
createTld("nobill");
DomainBase domain10 = persistActiveDomain("test.nobill");
persistOneTimeBillingEvent(
10, domain10, registrar1, Reason.RENEW, 3, Money.of(CurrencyUnit.USD, 20.5));
persistOneTimeBillingEvent(10, domain10, registrar1, Reason.RENEW, 3, Money.of(USD, 20.5));
// Add billing event before October 2017
DomainBase domain11 = persistActiveDomain("july.test");
@@ -471,8 +503,7 @@ class InvoicingPipelineTest {
// Add a billing event with a corresponding cancellation
DomainBase domain12 = persistActiveDomain("cancel.test");
OneTime oneTime =
persistOneTimeBillingEvent(
12, domain12, registrar1, Reason.RENEW, 3, Money.of(CurrencyUnit.USD, 20.5));
persistOneTimeBillingEvent(12, domain12, registrar1, Reason.RENEW, 3, Money.of(USD, 20.5));
DomainHistory domainHistory = persistDomainHistory(domain12, registrar1);
Cancellation cancellation =
@@ -507,8 +538,7 @@ class InvoicingPipelineTest {
.build();
persistResource(recurring);
OneTime oneTimeRecurring =
persistOneTimeBillingEvent(
13, domain13, registrar1, Reason.RENEW, 3, Money.of(CurrencyUnit.USD, 20.5));
persistOneTimeBillingEvent(13, domain13, registrar1, Reason.RENEW, 3, Money.of(USD, 20.5));
oneTimeRecurring =
oneTimeRecurring
.asBuilder()

View File

@@ -254,6 +254,15 @@ class ContactDeleteFlowTest extends ResourceFlowTestCase<ContactDeleteFlow, Cont
assertIcannReportingActivityFieldLogged("srs-cont-delete");
}
@TestOfyOnly
void testModification_duringNoAsyncPhase() throws Exception {
persistActiveContact(getUniqueIdFromCommand());
DatabaseHelper.setMigrationScheduleToDatastorePrimaryNoAsync(clock);
EppException thrown = assertThrows(ReadOnlyModeEppException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
DatabaseHelper.removeDatabaseMigrationSchedule();
}
@TestOfyOnly
void testModification_duringReadOnlyPhase() throws Exception {
persistActiveContact(getUniqueIdFromCommand());

View File

@@ -840,6 +840,15 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
doSuccessfulTest();
}
@TestOfyOnly
void testSuccess_inNoAsyncPhase() throws Exception {
DatabaseHelper.setMigrationScheduleToDatastorePrimaryNoAsync(clock);
persistContactsAndHosts();
runFlowAssertResponse(
loadFile("domain_create_response_noasync.xml", ImmutableMap.of("DOMAIN", "example.tld")));
DatabaseHelper.removeDatabaseMigrationSchedule();
}
@TestOfyAndSql
void testSuccess_maxNumberOfNameservers() throws Exception {
setEppInput("domain_create_13_nameservers.xml");

View File

@@ -358,6 +358,15 @@ class HostDeleteFlowTest extends ResourceFlowTestCase<HostDeleteFlow, HostResour
DatabaseHelper.removeDatabaseMigrationSchedule();
}
@TestOfyOnly
void testModification_duringNoAsyncPhase() {
persistActiveHost("ns1.example.tld");
DatabaseHelper.setMigrationScheduleToDatastorePrimaryNoAsync(clock);
EppException thrown = assertThrows(ReadOnlyModeEppException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
DatabaseHelper.removeDatabaseMigrationSchedule();
}
private void assertOfyDeleteSuccess(String registrarId, String clientTrid, boolean isSuperuser)
throws Exception {
HostResource deletedHost = reloadResourceByForeignKey();

View File

@@ -1355,7 +1355,43 @@ class HostUpdateFlowTest extends ResourceFlowTestCase<HostUpdateFlow, HostResour
}
@TestOfyOnly
void testModification_duringReadOnlyPhase() throws Exception {
void testSuccess_nonHostRename_inNoAsyncPhase_succeeds() throws Exception {
setEppInput("host_update_name_unchanged.xml");
createTld("tld");
DatabaseHelper.setMigrationScheduleToDatastorePrimaryNoAsync(clock);
DomainBase domain = persistActiveDomain("example.tld");
HostResource oldHost = persistActiveSubordinateHost(oldHostName(), domain);
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("generic_success_response.xml"));
// The example xml doesn't do a host rename, so reloading the host should work.
assertAboutHosts()
.that(reloadResourceByForeignKey())
.hasLastSuperordinateChange(oldHost.getLastSuperordinateChange())
.and()
.hasSuperordinateDomain(domain.createVKey())
.and()
.hasPersistedCurrentSponsorRegistrarId("TheRegistrar")
.and()
.hasLastTransferTime(null)
.and()
.hasOnlyOneHistoryEntryWhich()
.hasType(HistoryEntry.Type.HOST_UPDATE);
assertDnsTasksEnqueued("ns1.example.tld");
DatabaseHelper.removeDatabaseMigrationSchedule();
}
@TestOfyOnly
void testRename_duringNoAsyncPhase_fails() throws Exception {
createTld("tld");
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
DatabaseHelper.setMigrationScheduleToDatastorePrimaryNoAsync(clock);
EppException thrown = assertThrows(ReadOnlyModeEppException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
DatabaseHelper.removeDatabaseMigrationSchedule();
}
@TestOfyOnly
void testModification_duringReadOnlyPhase_fails() throws Exception {
createTld("tld");
persistActiveSubordinateHost(oldHostName(), persistActiveDomain("example.tld"));
DatabaseHelper.setMigrationScheduleToDatastorePrimaryReadOnly(clock);

View File

@@ -17,6 +17,7 @@ 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_NO_ASYNC;
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;
@@ -71,10 +72,12 @@ public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
runValidTransition(DATASTORE_ONLY, DATASTORE_PRIMARY);
runValidTransition(DATASTORE_PRIMARY, DATASTORE_ONLY);
runValidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_READ_ONLY);
runValidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_NO_ASYNC);
runValidTransition(DATASTORE_PRIMARY_NO_ASYNC, DATASTORE_PRIMARY_READ_ONLY);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_ONLY);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_PRIMARY);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, DATASTORE_PRIMARY_NO_ASYNC);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_PRIMARY_READ_ONLY);
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_PRIMARY);
@@ -94,6 +97,7 @@ public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
runInvalidTransition(DATASTORE_ONLY, SQL_PRIMARY);
runInvalidTransition(DATASTORE_ONLY, SQL_ONLY);
runInvalidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_READ_ONLY);
runInvalidTransition(DATASTORE_PRIMARY, SQL_PRIMARY_READ_ONLY);
runInvalidTransition(DATASTORE_PRIMARY, SQL_PRIMARY);
runInvalidTransition(DATASTORE_PRIMARY, SQL_ONLY);
@@ -124,7 +128,8 @@ public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
ImmutableSortedMap.<DateTime, MigrationState>naturalOrder()
.put(START_OF_TIME, DATASTORE_ONLY)
.put(startTime.plusHours(1), DATASTORE_PRIMARY)
.put(startTime.plusHours(2), DATASTORE_PRIMARY_READ_ONLY)
.put(startTime.plusHours(2), DATASTORE_PRIMARY_NO_ASYNC)
.put(startTime.plusHours(3), DATASTORE_PRIMARY_READ_ONLY)
.build();
assertThat(
assertThrows(
@@ -163,7 +168,8 @@ public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
fakeClock.setTo(START_OF_TIME.plusDays(1));
AllocationToken token =
new AllocationToken.Builder().setToken("token").setTokenType(TokenType.SINGLE_USE).build();
runValidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_READ_ONLY);
runValidTransition(DATASTORE_PRIMARY, DATASTORE_PRIMARY_NO_ASYNC);
runValidTransition(DATASTORE_PRIMARY_NO_ASYNC, DATASTORE_PRIMARY_READ_ONLY);
assertThrows(ReadOnlyModeException.class, () -> persistResource(token));
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_PRIMARY_READ_ONLY);
assertThrows(ReadOnlyModeException.class, () -> persistResource(token));

View File

@@ -366,8 +366,9 @@ public class ReplicateToDatastoreActionTest {
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(START_OF_TIME.plusHours(2), MigrationState.DATASTORE_PRIMARY_NO_ASYNC)
.put(START_OF_TIME.plusHours(3), MigrationState.DATASTORE_PRIMARY_READ_ONLY)
.put(START_OF_TIME.plusHours(4), MigrationState.SQL_PRIMARY)
.put(now.plusHours(1), MigrationState.SQL_PRIMARY_READ_ONLY)
.put(now.plusHours(2), MigrationState.DATASTORE_PRIMARY_READ_ONLY)
.put(now.plusHours(3), MigrationState.DATASTORE_PRIMARY)

View File

@@ -47,7 +47,9 @@ public class DatabaseMigrationScheduleTransitionConverterTest {
MigrationState.DATASTORE_ONLY,
DateTime.parse("2001-01-01T00:00:00.0Z"),
MigrationState.DATASTORE_PRIMARY,
DateTime.parse("2002-01-01T00:00:00.0Z"),
DateTime.parse("2002-01-01T01:00:00.0Z"),
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
DateTime.parse("2002-01-01T02:00:00.0Z"),
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
DateTime.parse("2002-01-02T00:00:00.0Z"),
MigrationState.SQL_PRIMARY,

View File

@@ -1429,6 +1429,33 @@ public class DatabaseHelper {
return entity;
}
/**
* Sets a DATASTORE_PRIMARY_NO_ASYNC state on the {@link DatabaseMigrationStateSchedule}.
*
* <p>In order to allow for tests to manipulate the clock how they need, we start the transitions
* one millisecond after the clock's current time (in case the clock's current value is
* START_OF_TIME). We then advance the clock one second so that we're in the
* DATASTORE_PRIMARY_READ_ONLY phase.
*
* <p>We must use the current time, otherwise the setting of the migration state will fail due to
* an invalid transition.
*/
public static void setMigrationScheduleToDatastorePrimaryNoAsync(FakeClock fakeClock) {
DateTime now = fakeClock.nowUtc();
jpaTm()
.transact(
() ->
DatabaseMigrationStateSchedule.set(
ImmutableSortedMap.of(
START_OF_TIME,
MigrationState.DATASTORE_ONLY,
now.plusMillis(1),
MigrationState.DATASTORE_PRIMARY,
now.plusMillis(2),
MigrationState.DATASTORE_PRIMARY_NO_ASYNC)));
fakeClock.advanceBy(Duration.standardSeconds(1));
}
/**
* Sets a DATASTORE_PRIMARY_READ_ONLY state on the {@link DatabaseMigrationStateSchedule}.
*
@@ -1452,6 +1479,8 @@ public class DatabaseHelper {
now.plusMillis(1),
MigrationState.DATASTORE_PRIMARY,
now.plusMillis(2),
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
now.plusMillis(3),
MigrationState.DATASTORE_PRIMARY_READ_ONLY)));
fakeClock.advanceBy(Duration.standardSeconds(1));
}
@@ -1478,8 +1507,10 @@ public class DatabaseHelper {
now.plusMillis(1),
MigrationState.DATASTORE_PRIMARY,
now.plusMillis(2),
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
now.plusMillis(3),
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
now.plusMillis(4),
MigrationState.SQL_PRIMARY)));
fakeClock.advanceBy(Duration.standardSeconds(1));
}

View File

@@ -54,10 +54,12 @@ public class GetDatabaseMigrationStateCommandTest
now.plusHours(1),
MigrationState.DATASTORE_PRIMARY,
now.plusHours(2),
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
now.plusHours(3),
MigrationState.SQL_PRIMARY,
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
now.plusHours(4),
MigrationState.SQL_PRIMARY,
now.plusHours(5),
MigrationState.SQL_ONLY);
jpaTm().transact(() -> DatabaseMigrationStateSchedule.set(transitions));
runCommand();

View File

@@ -64,14 +64,21 @@ public class SetDatabaseMigrationStateCommandTest
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);
DateTime datastorePrimaryNoAsync = now.plusHours(2);
DateTime datastorePrimaryReadOnly = now.plusHours(3);
DateTime sqlPrimary = now.plusHours(4);
DateTime sqlOnly = now.plusHours(5);
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));
+ "%s=DATASTORE_PRIMARY_NO_ASYNC,%s=DATASTORE_PRIMARY_READ_ONLY,"
+ "%s=SQL_PRIMARY,%s=SQL_ONLY",
START_OF_TIME,
datastorePrimary,
datastorePrimaryNoAsync,
datastorePrimaryReadOnly,
sqlPrimary,
sqlOnly));
assertThat(DatabaseMigrationStateSchedule.get().toValueMap())
.containsExactlyEntriesIn(
ImmutableSortedMap.of(
@@ -79,6 +86,8 @@ public class SetDatabaseMigrationStateCommandTest
MigrationState.DATASTORE_ONLY,
datastorePrimary,
MigrationState.DATASTORE_PRIMARY,
datastorePrimaryNoAsync,
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
datastorePrimaryReadOnly,
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
sqlPrimary,
@@ -110,8 +119,9 @@ public class SetDatabaseMigrationStateCommandTest
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)));
+ "%s=DATASTORE_PRIMARY_NO_ASYNC,%s=DATASTORE_PRIMARY_READ_ONLY,"
+ "%s=DATASTORE_PRIMARY",
START_OF_TIME, now.plusHours(1), now.plusHours(2), now.plusHours(3), now.plusHours(4)));
assertThat(DatabaseMigrationStateSchedule.get().toValueMap())
.containsExactlyEntriesIn(
ImmutableSortedMap.of(
@@ -120,8 +130,10 @@ public class SetDatabaseMigrationStateCommandTest
now.plusHours(1),
MigrationState.DATASTORE_PRIMARY,
now.plusHours(2),
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
MigrationState.DATASTORE_PRIMARY_NO_ASYNC,
now.plusHours(3),
MigrationState.DATASTORE_PRIMARY_READ_ONLY,
now.plusHours(4),
MigrationState.DATASTORE_PRIMARY));
}
@@ -152,9 +164,12 @@ public class SetDatabaseMigrationStateCommandTest
() ->
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)))))
"--migration_schedule=%s=DATASTORE_ONLY,%s=DATASTORE_PRIMARY,"
+ "%s=DATASTORE_PRIMARY_NO_ASYNC,%s=DATASTORE_PRIMARY_READ_ONLY",
START_OF_TIME,
now.minusHours(3),
now.minusHours(2),
now.minusHours(1)))))
.hasMessageThat()
.isEqualTo(
"Cannot transition from current state-as-of-now DATASTORE_ONLY "

View File

@@ -0,0 +1,19 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<domain:creData
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN%</domain:name>
<domain:crDate>1999-04-03T22:00:01.0Z</domain:crDate>
<domain:exDate>2001-04-03T22:00:01.0Z</domain:exDate>
</domain:creData>
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View File

@@ -261,11 +261,11 @@ td.section {
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2022-03-30 20:53:20.574091</td>
<td class="property_value">2022-04-01 16:53:26.766163</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
<td id="lastFlywayFile" class="property_value">V115__add_renewal_columns_to_billing_recurrence.sql</td>
<td id="lastFlywayFile" class="property_value">V116__add_renewal_column_to_allocation_token.sql</td>
</tr>
</tbody>
</table>
@@ -284,7 +284,7 @@ td.section {
generated on
</text>
<text text-anchor="start" x="4055.5" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
2022-03-30 20:53:20.574091
2022-04-01 16:53:26.766163
</text>
<polygon fill="none" stroke="#888888" points="3968,-4 3968,-44 4233,-44 4233,-4 3968,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">

File diff suppressed because it is too large Load Diff

View File

@@ -113,3 +113,4 @@ V112__add_billingrecurrence_missing_indexes.sql
V113__add_host_missing_indexes.sql
V114__add_allocation_token_indexes.sql
V115__add_renewal_columns_to_billing_recurrence.sql
V116__add_renewal_column_to_allocation_token.sql

View File

@@ -0,0 +1,15 @@
-- Copyright 2022 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.
alter table "AllocationToken" add column renewal_price_behavior text default 'DEFAULT' not null;

View File

@@ -51,7 +51,8 @@ CREATE TABLE public."AllocationToken" (
redemption_domain_repo_id text,
token_status_transitions public.hstore,
token_type text,
redemption_domain_history_id bigint
redemption_domain_history_id bigint,
renewal_price_behavior text DEFAULT 'DEFAULT'::text NOT NULL
);