1
0
mirror of https://github.com/google/nomulus synced 2026-06-09 16:33:02 +00:00

Compare commits

...

10 Commits

Author SHA1 Message Date
Lai Jiang f1dcb1299f Reformat a file (#1305)
* Reformat a file

Constants should be SNAKE_CASE named and static fields usually go before
instance fields.
2021-09-03 16:56:57 -04:00
gbrodman d46594c610 Select correct history entries when creating canceling records (#1309) 2021-09-03 11:25:53 -04:00
gbrodman 0a9fa8cf23 Add RetryingTest annotation to flaky RDE test (#1306)
I'm not sure why this test is failing. It's failing saying that the
listObjects call is failing to include
"soy_2000-01-01_thin_S1_R1.xml.ghostryde" in the results, however the
verifyFiles method that we call right beforehand verifies that file and
its contents
2021-09-03 09:39:55 -04:00
sarahcaseybot db4bf90538 Remove Datastore references from DomainLabelEntry (#1307) 2021-09-02 17:55:55 -04:00
Rachel Guan d6127e4c0c Log registrar and certificate info before sending an email (#1308)
* Log registrar and certificate info before sending an email
2021-09-02 16:58:58 -04:00
sarahcaseybot 447bfa162b Remove Datastore references in BaseDomainLabelList (#1304) 2021-09-02 13:21:45 -04:00
Rachel Guan c9efa61198 Update expiring certificate notification email content (#1294)
* Update expiring certificate notification email content

* Improve test cases
2021-08-30 11:51:05 -04:00
gbrodman 054c0625a8 Add SQL functionality to DeleteProberDataAction (#1218)
This includes a change to how the JPA transaction manager handles
existence and load checks for entities with compound IDs. Previously, we
relied on the fields all being named the same in the ID entity and the
parent entity. This didn't work for History objects (e.g. DomainHistory)
so existence checks were broken. Now, we use the methods the same way
that Hibernate does (if possible).

Note as well that there's a bit of semi-duplicated logic in
DeleteProberDataAction (between the mapper and the SQL logic). The
mapper code will be deleted once we've shifted to SQL, and for now it's
better to keep it in place for logging purposes.
2021-08-27 21:09:08 -04:00
gbrodman b03639d7fc Implement read-only transaction manager modes for R3.0 migration (#1241)
This involves:
- Altering both transaction managers to check for a read-only mode at
the start of standard write actions (e.g. delete, put).
- Altering both raw layers (entity manager, ofy) to throw exceptions on
write actions as well
- Implementing bypass routes for reading / setting / removing the schedule itself
so that we don't get "stuck"
2021-08-27 15:59:16 -04:00
Rachel Guan bd9af0de84 Improve logging for SendExpiringCertificateNotificationEmailAction.java (#1302)
* Improve logging for SendExpiringCertificateNotificationEmailAction.java
2021-08-27 13:11:54 -04:00
30 changed files with 1542 additions and 251 deletions
+1
View File
@@ -215,6 +215,7 @@ PRESUBMITS = {
"RdapDomainSearchAction.java",
"RdapNameserverSearchAction.java",
"RdapSearchActionBase.java",
"ReadOnlyCheckingEntityManager.java",
"RegistryQuery",
},
):
@@ -260,7 +260,7 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
.ifPresent(
sqlEntity -> {
sqlEntity.beforeSqlSaveOnReplay();
jpaTm().put(sqlEntity);
jpaTm().putIgnoringReadOnly(sqlEntity);
});
} else {
// this should never happen, but we shouldn't fail on it
@@ -293,7 +293,7 @@ public class ReplayCommitLogsToSqlAction implements Runnable {
&& !DatastoreOnlyEntity.class.isAssignableFrom(entityClass)
&& entityClass.getAnnotation(javax.persistence.Entity.class) != null) {
ReplaySpecializer.beforeSqlDelete(entityVKey);
jpaTm().delete(entityVKey);
jpaTm().deleteIgnoringReadOnly(entityVKey);
}
} catch (Throwable t) {
logger.atSevere().log("Error when deleting key %s", entityVKey);
@@ -23,6 +23,7 @@ import static google.registry.model.ResourceTransferUtils.updateForeignKeyIndexD
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_DELETE;
import static google.registry.model.tld.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 google.registry.request.RequestParameters.PARAM_TLDS;
@@ -42,6 +43,7 @@ import google.registry.config.RegistryEnvironment;
import google.registry.dns.DnsQueue;
import google.registry.mapreduce.MapreduceRunner;
import google.registry.mapreduce.inputs.EppResourceInputs;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.EppResourceUtils;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
@@ -54,15 +56,18 @@ import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import javax.inject.Inject;
import org.hibernate.CacheMode;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.query.Query;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* Deletes all prober DomainBases and their subordinate history entries, poll messages, and
* billing events, along with their ForeignKeyDomainIndex and EppResourceIndex entities.
*
* <p>See: https://www.youtube.com/watch?v=xuuv0syoHnM
* Deletes all prober DomainBases and their subordinate history entries, poll messages, and billing
* events, along with their ForeignKeyDomainIndex and EppResourceIndex entities.
*/
@Action(
service = Action.Service.BACKEND,
@@ -73,10 +78,51 @@ public class DeleteProberDataAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/**
* The maximum amount of time we allow a prober domain to be in use.
*
* <p>In practice, the prober's connection will time out well before this duration. This includes
* a decent buffer.
*/
private static final Duration DOMAIN_USED_DURATION = Duration.standardHours(1);
/**
* The minimum amount of time we want a domain to be "soft deleted".
*
* <p>The domain has to remain soft deleted for at least enough time for the DNS task to run and
* remove it from DNS itself. This is probably on the order of minutes.
*/
private static final Duration SOFT_DELETE_DELAY = Duration.standardHours(1);
private static final DnsQueue dnsQueue = DnsQueue.create();
// Domains to delete must:
// 1. Be in one of the prober TLDs
// 2. Not be a nic domain
// 3. Have no subordinate hosts
// 4. Not still be used (within an hour of creation time)
// 5. Either be active (creationTime <= now < deletionTime) or have been deleted a while ago (this
// prevents accidental double-map with the same key from immediately deleting active domains)
//
// Note: creationTime must be compared to a Java object (CreateAutoTimestamp) but deletionTime can
// be compared directly to the SQL timestamp (it's a DateTime)
private static final String DOMAIN_QUERY_STRING =
"FROM Domain d WHERE d.tld IN :tlds AND d.fullyQualifiedDomainName NOT LIKE 'nic.%' AND"
+ " (d.subordinateHosts IS EMPTY OR d.subordinateHosts IS NULL) AND d.creationTime <"
+ " :creationTimeCutoff AND ((d.creationTime <= :nowAutoTimestamp AND d.deletionTime >"
+ " current_timestamp()) OR d.deletionTime < :nowMinusSoftDeleteDelay) ORDER BY d.repoId";
/** Number of domains to retrieve and delete per SQL transaction. */
private static final int BATCH_SIZE = 1000;
@Inject @Parameter(PARAM_DRY_RUN) boolean isDryRun;
/** List of TLDs to work on. If empty - will work on all TLDs that end with .test. */
@Inject @Parameter(PARAM_TLDS) ImmutableSet<String> tlds;
@Inject @Config("registryAdminClientId") String registryAdminClientId;
@Inject
@Config("registryAdminClientId")
String registryAdminRegistrarId;
@Inject MapreduceRunner mrRunner;
@Inject Response response;
@Inject DeleteProberDataAction() {}
@@ -84,25 +130,14 @@ public class DeleteProberDataAction implements Runnable {
@Override
public void run() {
checkState(
!Strings.isNullOrEmpty(registryAdminClientId),
!Strings.isNullOrEmpty(registryAdminRegistrarId),
"Registry admin client ID must be configured for prober data deletion to work");
mrRunner
.setJobName("Delete prober data")
.setModuleName("backend")
.runMapOnly(
new DeleteProberDataMapper(getProberRoidSuffixes(), isDryRun, registryAdminClientId),
ImmutableList.of(EppResourceInputs.createKeyInput(DomainBase.class)))
.sendLinkToMapreduceConsole(response);
}
private ImmutableSet<String> getProberRoidSuffixes() {
checkArgument(
!PRODUCTION.equals(RegistryEnvironment.get())
|| tlds.stream().allMatch(tld -> tld.endsWith(".test")),
"On production, can only work on TLDs that end with .test");
ImmutableSet<String> deletableTlds =
getTldsOfType(TldType.TEST)
.stream()
getTldsOfType(TldType.TEST).stream()
.filter(tld -> tlds.isEmpty() ? tld.endsWith(".test") : tlds.contains(tld))
.collect(toImmutableSet());
checkArgument(
@@ -110,10 +145,161 @@ public class DeleteProberDataAction implements Runnable {
"If tlds are given, they must all exist and be TEST tlds. Given: %s, not found: %s",
tlds,
Sets.difference(tlds, deletableTlds));
return deletableTlds
.stream()
.map(tld -> Registry.get(tld).getRoidSuffix())
.collect(toImmutableSet());
ImmutableSet<String> proberRoidSuffixes =
deletableTlds.stream()
.map(tld -> Registry.get(tld).getRoidSuffix())
.collect(toImmutableSet());
if (tm().isOfy()) {
mrRunner
.setJobName("Delete prober data")
.setModuleName("backend")
.runMapOnly(
new DeleteProberDataMapper(proberRoidSuffixes, isDryRun, registryAdminRegistrarId),
ImmutableList.of(EppResourceInputs.createKeyInput(DomainBase.class)))
.sendLinkToMapreduceConsole(response);
} else {
runSqlJob(deletableTlds);
}
}
private void runSqlJob(ImmutableSet<String> deletableTlds) {
AtomicInteger softDeletedDomains = new AtomicInteger();
AtomicInteger hardDeletedDomains = new AtomicInteger();
jpaTm().transact(() -> processDomains(deletableTlds, softDeletedDomains, hardDeletedDomains));
logger.atInfo().log(
"%s %d domains.",
isDryRun ? "Would have soft-deleted" : "Soft-deleted", softDeletedDomains.get());
logger.atInfo().log(
"%s %d domains.",
isDryRun ? "Would have hard-deleted" : "Hard-deleted", hardDeletedDomains.get());
}
private void processDomains(
ImmutableSet<String> deletableTlds,
AtomicInteger softDeletedDomains,
AtomicInteger hardDeletedDomains) {
DateTime now = tm().getTransactionTime();
// Scroll through domains, soft-deleting as necessary (very few will be soft-deleted) and
// keeping track of which domains to hard-delete (there can be many, so we batch them up)
ScrollableResults scrollableResult =
jpaTm()
.query(DOMAIN_QUERY_STRING, DomainBase.class)
.setParameter("tlds", deletableTlds)
.setParameter(
"creationTimeCutoff", CreateAutoTimestamp.create(now.minus(DOMAIN_USED_DURATION)))
.setParameter("nowMinusSoftDeleteDelay", now.minus(SOFT_DELETE_DELAY))
.setParameter("nowAutoTimestamp", CreateAutoTimestamp.create(now))
.unwrap(Query.class)
.setCacheMode(CacheMode.IGNORE)
.scroll(ScrollMode.FORWARD_ONLY);
ImmutableList.Builder<String> domainRepoIdsToHardDelete = new ImmutableList.Builder<>();
ImmutableList.Builder<String> hostNamesToHardDelete = new ImmutableList.Builder<>();
for (int i = 1; scrollableResult.next(); i = (i + 1) % BATCH_SIZE) {
DomainBase domain = (DomainBase) scrollableResult.get(0);
processDomain(
domain,
domainRepoIdsToHardDelete,
hostNamesToHardDelete,
softDeletedDomains,
hardDeletedDomains);
// Batch the deletion and DB flush + session clearing so we don't OOM
if (i == 0) {
hardDeleteDomainsAndHosts(domainRepoIdsToHardDelete.build(), hostNamesToHardDelete.build());
domainRepoIdsToHardDelete = new ImmutableList.Builder<>();
hostNamesToHardDelete = new ImmutableList.Builder<>();
jpaTm().getEntityManager().flush();
jpaTm().getEntityManager().clear();
}
}
// process the remainder
hardDeleteDomainsAndHosts(domainRepoIdsToHardDelete.build(), hostNamesToHardDelete.build());
}
private void processDomain(
DomainBase domain,
ImmutableList.Builder<String> domainRepoIdsToHardDelete,
ImmutableList.Builder<String> hostNamesToHardDelete,
AtomicInteger softDeletedDomains,
AtomicInteger hardDeletedDomains) {
// If the domain is still active, that means that the prober encountered a failure and did not
// successfully soft-delete the domain (thus leaving its DNS entry published). We soft-delete
// it now so that the DNS entry can be handled. The domain will then be hard-deleted the next
// time the job is run.
if (EppResourceUtils.isActive(domain, tm().getTransactionTime())) {
if (isDryRun) {
logger.atInfo().log(
"Would soft-delete the active domain: %s (%s)",
domain.getDomainName(), domain.getRepoId());
} else {
softDeleteDomain(domain, registryAdminRegistrarId, dnsQueue);
}
softDeletedDomains.incrementAndGet();
} else {
if (isDryRun) {
logger.atInfo().log(
"Would hard-delete the non-active domain: %s (%s) and its dependents",
domain.getDomainName(), domain.getRepoId());
} else {
domainRepoIdsToHardDelete.add(domain.getRepoId());
hostNamesToHardDelete.addAll(domain.getSubordinateHosts());
}
hardDeletedDomains.incrementAndGet();
}
}
private void hardDeleteDomainsAndHosts(
ImmutableList<String> domainRepoIds, ImmutableList<String> hostNames) {
jpaTm()
.query("DELETE FROM Host WHERE fullyQualifiedHostName IN :hostNames")
.setParameter("hostNames", hostNames)
.executeUpdate();
jpaTm()
.query("DELETE FROM BillingEvent WHERE domainRepoId IN :repoIds")
.setParameter("repoIds", domainRepoIds)
.executeUpdate();
jpaTm()
.query("DELETE FROM BillingRecurrence WHERE domainRepoId IN :repoIds")
.setParameter("repoIds", domainRepoIds)
.executeUpdate();
jpaTm()
.query("DELETE FROM BillingCancellation WHERE domainRepoId IN :repoIds")
.setParameter("repoIds", domainRepoIds)
.executeUpdate();
jpaTm()
.query("DELETE FROM DomainHistory WHERE domainRepoId IN :repoIds")
.setParameter("repoIds", domainRepoIds)
.executeUpdate();
jpaTm()
.query("DELETE FROM PollMessage WHERE domainRepoId IN :repoIds")
.setParameter("repoIds", domainRepoIds)
.executeUpdate();
jpaTm()
.query("DELETE FROM Domain WHERE repoId IN :repoIds")
.setParameter("repoIds", domainRepoIds)
.executeUpdate();
}
// Take a DNS queue + admin registrar id as input so that it can be called from the mapper as well
private static void softDeleteDomain(
DomainBase domain, String registryAdminRegistrarId, DnsQueue localDnsQueue) {
DomainBase deletedDomain =
domain.asBuilder().setDeletionTime(tm().getTransactionTime()).setStatusValues(null).build();
DomainHistory historyEntry =
new DomainHistory.Builder()
.setDomain(domain)
.setType(DOMAIN_DELETE)
.setModificationTime(tm().getTransactionTime())
.setBySuperuser(true)
.setReason("Deletion of prober data")
.setClientId(registryAdminRegistrarId)
.build();
// Note that we don't bother handling grace periods, billing events, pending transfers, poll
// messages, or auto-renews because those will all be hard-deleted the next time the job runs
// anyway.
tm().putAllWithoutBackup(ImmutableList.of(deletedDomain, historyEntry));
// updating foreign keys is a no-op in SQL
updateForeignKeyIndexDeletionTime(deletedDomain);
localDnsQueue.addDomainRefreshTask(deletedDomain.getDomainName());
}
/** Provides the map method that runs for each existing DomainBase entity. */
@@ -122,32 +308,17 @@ public class DeleteProberDataAction implements Runnable {
private static final DnsQueue dnsQueue = DnsQueue.create();
private static final long serialVersionUID = -7724537393697576369L;
/**
* The maximum amount of time we allow a prober domain to be in use.
*
* In practice, the prober's connection will time out well before this duration. This includes a
* decent buffer.
*
*/
private static final Duration DOMAIN_USED_DURATION = Duration.standardHours(1);
/**
* The minimum amount of time we want a domain to be "soft deleted".
*
* The domain has to remain soft deleted for at least enough time for the DNS task to run and
* remove it from DNS itself. This is probably on the order of minutes.
*/
private static final Duration SOFT_DELETE_DELAY = Duration.standardHours(1);
private final ImmutableSet<String> proberRoidSuffixes;
private final Boolean isDryRun;
private final String registryAdminClientId;
private final String registryAdminRegistrarId;
public DeleteProberDataMapper(
ImmutableSet<String> proberRoidSuffixes, Boolean isDryRun, String registryAdminClientId) {
ImmutableSet<String> proberRoidSuffixes,
Boolean isDryRun,
String registryAdminRegistrarId) {
this.proberRoidSuffixes = proberRoidSuffixes;
this.isDryRun = isDryRun;
this.registryAdminClientId = registryAdminClientId;
this.registryAdminRegistrarId = registryAdminRegistrarId;
}
@Override
@@ -203,7 +374,7 @@ public class DeleteProberDataAction implements Runnable {
logger.atInfo().log(
"Would soft-delete the active domain: %s (%s)", domainName, domainKey);
} else {
softDeleteDomain(domain);
tm().transact(() -> softDeleteDomain(domain, registryAdminRegistrarId, dnsQueue));
}
getContext().incrementCounter("domains soft-deleted");
return;
@@ -223,8 +394,7 @@ public class DeleteProberDataAction implements Runnable {
tm().transact(
() -> {
// This ancestor query selects all descendant HistoryEntries, BillingEvents,
// PollMessages,
// and TLD-specific entities, as well as the domain itself.
// PollMessages, and TLD-specific entities, as well as the domain itself.
List<Key<Object>> domainAndDependentKeys =
auditedOfy().load().ancestor(domainKey).keys().list();
ImmutableSet<Key<?>> allKeys =
@@ -243,32 +413,5 @@ public class DeleteProberDataAction implements Runnable {
getContext().incrementCounter("domains hard-deleted");
getContext().incrementCounter("total entities hard-deleted", entitiesDeleted);
}
private void softDeleteDomain(final DomainBase domain) {
tm().transactNew(
() -> {
DomainBase deletedDomain =
domain
.asBuilder()
.setDeletionTime(tm().getTransactionTime())
.setStatusValues(null)
.build();
DomainHistory historyEntry =
new DomainHistory.Builder()
.setDomain(domain)
.setType(DOMAIN_DELETE)
.setModificationTime(tm().getTransactionTime())
.setBySuperuser(true)
.setReason("Deletion of prober data")
.setClientId(registryAdminClientId)
.build();
// Note that we don't bother handling grace periods, billing events, pending
// transfers, poll messages, or auto-renews because these will all be hard-deleted
// the next time the mapreduce runs anyway.
tm().putAll(deletedDomain, historyEntry);
updateForeignKeyIndexDeletionTime(deletedDomain);
dnsQueue.addDomainRefreshTask(deletedDomain.getDomainName());
});
}
}
}
@@ -113,6 +113,8 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
*/
@VisibleForTesting
ImmutableList<RegistrarInfo> getRegistrarsWithExpiringCertificates() {
logger.atInfo().log(
"Getting a list of registrars that should receive expiring notification emails.");
return Streams.stream(Registrar.loadAllCached())
.map(
registrar ->
@@ -149,6 +151,12 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
}
try {
ImmutableSet<InternetAddress> recipients = getEmailAddresses(registrar, Type.TECH);
Date expirationDate = certificateChecker.getCertificate(certificate.get()).getNotAfter();
logger.atInfo().log(
"Registrar %s should receive an email that its %s SSL certificate will expire on %s.",
registrar.getRegistrarName(),
certificateType.getDisplayName(),
expirationDate.toString());
if (recipients.isEmpty()) {
logger.atWarning().log(
"Registrar %s contains no email addresses to receive notification email.",
@@ -163,7 +171,8 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
getEmailBody(
registrar.getRegistrarName(),
certificateType,
certificateChecker.getCertificate(certificate.get()).getNotAfter()))
expirationDate,
registrar.getClientId()))
.setRecipients(recipients)
.setCcs(getEmailAddresses(registrar, Type.ADMIN))
.build());
@@ -198,17 +207,21 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
newRegistrar.setLastExpiringCertNotificationSentDate(now);
tm().put(newRegistrar.build());
logger.atInfo().log(
"Updated last notification email sent date for %s certificate of "
"Updated last notification email sent date to %s for %s certificate of "
+ "registrar %s.",
certificateType.getDisplayName(), registrar.getRegistrarName());
DATE_FORMATTER.print(now),
certificateType.getDisplayName(),
registrar.getRegistrarName());
break;
case FAILOVER:
newRegistrar.setLastExpiringFailoverCertNotificationSentDate(now);
tm().put(newRegistrar.build());
logger.atInfo().log(
"Updated last notification email sent date for %s certificate of "
"Updated last notification email sent date to %s for %s certificate of "
+ "registrar %s.",
certificateType.getDisplayName(), registrar.getRegistrarName());
DATE_FORMATTER.print(now),
certificateType.getDisplayName(),
registrar.getRegistrarName());
break;
default:
throw new IllegalArgumentException(
@@ -251,7 +264,7 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
}
}
logger.atInfo().log(
"Sent %d expiring certificate notification emails to registrars.", emailsSent);
"Attempted to send %d expiring certificate notification emails.", emailsSent);
return emailsSent;
}
@@ -278,14 +291,17 @@ public class SendExpiringCertificateNotificationEmailAction implements Runnable
*/
@VisibleForTesting
@SuppressWarnings("lgtm[java/dereferenced-value-may-be-null]")
String getEmailBody(String registrarName, CertificateType type, Date expirationDate) {
String getEmailBody(
String registrarName, CertificateType type, Date expirationDate, String registrarId) {
checkArgumentNotNull(expirationDate, "Expiration date cannot be null");
checkArgumentNotNull(type, "Certificate type cannot be null");
checkArgumentNotNull(registrarId, "Registrar Id cannot be null");
return String.format(
expirationWarningEmailBodyText,
registrarName,
type.getDisplayName(),
DATE_FORMATTER.print(new DateTime(expirationDate)));
DATE_FORMATTER.print(new DateTime(expirationDate)),
registrarId);
}
/**
@@ -456,12 +456,53 @@ sslCertificateValidation:
# The minimum number of days between two successive expiring notification emails.
expirationWarningIntervalDays: 15
# Text for expiring certificate notification email subject.
expirationWarningEmailSubjectText: Certificate Expring Within 30 Days.
expirationWarningEmailSubjectText: "[Important] Expiring SSL certificate for Google Registry EPP connection"
# Text for expiring certificate notification email body that accepts 3 parameters:
# registrar name, certificate type, and expiration date, respectively.
expirationWarningEmailBodyText: |
Hello Registrar %s,
The %s certificate is expiring on %s.
expirationWarningEmailBodyText: >
Dear %1$s,
We would like to inform you that your %2$s SSL certificate will expire at
%3$s. Please take note that using expired certificates will prevent
successful Registry login.
Kindly update your production account certificate within the support
console using the following steps:
1. Navigate to support.registry.google and login using your
%4$s@registry.google credentials.
* If this is your first time logging in, you will be prompted to
reset your password, so please keep your new password safe.
* If you are already logged in with some other Google account(s) but
not your %4$s@registry.google account, you need to click on
“Add Account” and login using your %4$s@registry.google credentials.
2. Select “Settings > Security” from the left navigation bar.
3. Click “Edit” on the top left corner.
4. Enter your full certificate string
(including lines -----BEGIN CERTIFICATE----- and
-----END CERTIFICATE-----) in the box.
5. Click “Save”. If there are validation issues with the form, you will
be prompted to fix them and click “Save” again.
A failover SSL certificate can also be added in order to prevent connection
issues once your main certificate expires. Connecting with either of the
certificates will work with our production EPP server.
Further information about our EPP connection requirements can be found in
section 9.2 in the updated Technical Guide in your Google Drive folder.
Note that account certificate changes take a few minutes to become
effective and that the existing connections will remain unaffected by
the change.
If you also would like to update your OT&E account certificate, please send
an email from your primary or technical contact to
registry-support@google.com and include the full certificate string
(including lines -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----).
Regards,
Google Registry
# The minimum number of bits an RSA key must contain.
minimumRsaKeyLength: 2048
# The ECDSA curves that are allowed for public keys.
@@ -1097,10 +1097,11 @@ public class DomainFlowUtils {
} else {
return jpaTm()
.query(
"FROM DomainHistory WHERE modificationTime >= :beginning "
+ "ORDER BY modificationTime ASC",
"FROM DomainHistory WHERE modificationTime >= :beginning AND domainRepoId = "
+ ":repoId ORDER BY modificationTime ASC",
DomainHistory.class)
.setParameter("beginning", now.minus(maxSearchPeriod))
.setParameter("repoId", domainBase.getRepoId())
.getResultList();
}
}
@@ -36,8 +36,8 @@ 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.
* <p>The entity is stored in SQL throughout the entire migration so as to have a single point of
* access.
*/
@Entity
public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements SqlOnlyEntity {
@@ -187,12 +187,12 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
private DatabaseMigrationStateSchedule() {}
@VisibleForTesting
DatabaseMigrationStateSchedule(
public DatabaseMigrationStateSchedule(
TimedTransitionProperty<MigrationState, MigrationStateTransition> migrationTransitions) {
this.migrationTransitions = migrationTransitions;
}
/** Sets and persists to Datastore the provided migration transition schedule. */
/** Sets and persists to SQL the provided migration transition schedule. */
public static void set(ImmutableSortedMap<DateTime, MigrationState> migrationTransitionMap) {
jpaTm().assertInTransaction();
TimedTransitionProperty<MigrationState, MigrationStateTransition> transitions =
@@ -204,7 +204,7 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
MigrationState.DATASTORE_ONLY,
"migrationTransitionMap must start with DATASTORE_ONLY");
validateTransitionAtCurrentTime(transitions);
jpaTm().put(new DatabaseMigrationStateSchedule(transitions));
jpaTm().putIgnoringReadOnly(new DatabaseMigrationStateSchedule(transitions));
CACHE.invalidateAll();
}
@@ -218,7 +218,7 @@ public class DatabaseMigrationStateSchedule extends CrossTldSingleton implements
return get().getValueAtTime(dateTime);
}
/** Loads the currently-set migration schedule from Datastore, or the default if none exists. */
/** Loads the currently-set migration schedule from SQL, or the default if none exists. */
@VisibleForTesting
static TimedTransitionProperty<MigrationState, MigrationStateTransition> getUncached() {
return jpaTm()
@@ -336,7 +336,7 @@ public class DatastoreTransactionManager implements TransactionManager {
@Override
public <T> QueryComposer<T> createQueryComposer(Class<T> entity) {
return new DatastoreQueryComposerImpl(entity);
return new DatastoreQueryComposerImpl<>(entity);
}
@Override
@@ -349,6 +349,16 @@ public class DatastoreTransactionManager implements TransactionManager {
return true;
}
@Override
public void putIgnoringReadOnly(Object entity) {
syncIfTransactionless(getOfy().saveIgnoringReadOnly().entities(toDatastoreEntity(entity)));
}
@Override
public void deleteIgnoringReadOnly(VKey<?> key) {
syncIfTransactionless(getOfy().deleteIgnoringReadOnly().key(key.getOfyKey()));
}
/**
* Executes the given {@link Result} instance synchronously if not in a transaction.
*
@@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Maps.uniqueIndex;
import static com.googlecode.objectify.ObjectifyService.ofy;
import static google.registry.config.RegistryConfig.getBaseOfyRetryDuration;
import static google.registry.persistence.transaction.TransactionManagerFactory.assertNotReadOnlyMode;
import static google.registry.util.CollectionUtils.union;
import com.google.appengine.api.datastore.DatastoreFailureException;
@@ -131,6 +132,7 @@ public class Ofy {
* <p>We only allow this in transactions so commit logs can be written in tandem with the delete.
*/
public Deleter delete() {
assertNotReadOnlyMode();
return new AugmentedDeleter() {
@Override
protected void handleDeletion(Iterable<Key<?>> keys) {
@@ -148,12 +150,8 @@ public class Ofy {
* <p>No backups get written.
*/
public Deleter deleteWithoutBackup() {
return new AugmentedDeleter() {
@Override
protected void handleDeletion(Iterable<Key<?>> keys) {
checkProhibitedAnnotations(keys, VirtualEntity.class);
}
};
assertNotReadOnlyMode();
return deleteIgnoringReadOnly();
}
/**
@@ -163,6 +161,7 @@ public class Ofy {
* <p>We only allow this in transactions so commit logs can be written in tandem with the save.
*/
public Saver save() {
assertNotReadOnlyMode();
return new AugmentedSaver() {
@Override
protected void handleSave(Iterable<?> entities) {
@@ -182,6 +181,12 @@ public class Ofy {
* <p>No backups get written.
*/
public Saver saveWithoutBackup() {
assertNotReadOnlyMode();
return saveIgnoringReadOnly();
}
/** Save, ignoring any backups or any read-only settings. */
public Saver saveIgnoringReadOnly() {
return new AugmentedSaver() {
@Override
protected void handleSave(Iterable<?> entities) {
@@ -190,6 +195,16 @@ public class Ofy {
};
}
/** Delete, ignoring any backups or any read-only settings. */
public Deleter deleteIgnoringReadOnly() {
return new AugmentedDeleter() {
@Override
protected void handleDeletion(Iterable<Key<?>> keys) {
checkProhibitedAnnotations(keys, VirtualEntity.class);
}
};
}
private Clock getClock() {
return injectedClock == null ? clock : injectedClock;
}
@@ -41,6 +41,6 @@ public class SqlReplayCheckpoint extends CrossTldSingleton implements SqlOnlyEnt
SqlReplayCheckpoint checkpoint = new SqlReplayCheckpoint();
checkpoint.lastReplayTime = lastReplayTime;
// this will overwrite the existing object due to the constant revisionId
jpaTm().put(checkpoint);
jpaTm().putIgnoringReadOnly(checkpoint);
}
}
@@ -250,7 +250,7 @@ public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Seri
resourceName, scope, requestStatusChecker.getLogId(), now, leaseLength);
// Locks are not parented under an EntityGroupRoot (so as to avoid write
// contention) and don't need to be backed up.
tm().putWithoutBackup(newLock);
tm().putIgnoringReadOnly(newLock);
return AcquireResult.create(now, lock, newLock, lockState);
});
@@ -269,18 +269,15 @@ public class Lock extends ImmutableObject implements DatastoreAndSqlEntity, Seri
// delete it. If the lock in Datastore was different then this lock is gone already;
// this can happen if release() is called around the expiration time and the lock
// expires underneath us.
Lock loadedLock =
tm().loadByKeyIfPresent(
VKey.create(
Lock.class,
new LockId(resourceName, tld),
Key.create(Lock.class, lockId)))
.orElse(null);
VKey<Lock> key =
VKey.create(
Lock.class, new LockId(resourceName, tld), Key.create(Lock.class, lockId));
Lock loadedLock = tm().loadByKeyIfPresent(key).orElse(null);
if (Lock.this.equals(loadedLock)) {
// Use deleteWithoutBackup() so that we don't create a commit log entry for deleting
// the lock.
logger.atInfo().log("Deleting lock: %s", lockId);
tm().deleteWithoutBackup(Lock.this);
tm().deleteIgnoringReadOnly(key);
lockMetrics.recordRelease(
resourceName, tld, new Duration(acquiredTime, tm().getTransactionTime()));
@@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.tld.Registries.getTlds;
import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
@@ -29,14 +28,8 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multiset;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.common.EntityGroupRoot;
import google.registry.model.tld.Registry;
import java.util.HashMap;
import java.util.List;
@@ -47,37 +40,28 @@ import javax.annotation.Nullable;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Transient;
import org.joda.time.DateTime;
/**
* Base class for {@link ReservedList} and {@link PremiumList} objects stored in Datastore.
* Base class for {@link ReservedList} and {@link PremiumList} objects.
*
* @param <T> The type of the root value being listed, e.g. {@link ReservationType}.
* @param <R> The type of domain label entry being listed, e.g. {@link
* ReservedList.ReservedListEntry} (note, must subclass {@link DomainLabelEntry}.
*/
@MappedSuperclass
@InCrossTld
public abstract class BaseDomainLabelList<T extends Comparable<?>, R extends DomainLabelEntry<T, ?>>
extends ImmutableObject implements Buildable {
@Ignore
@javax.persistence.Id
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long revisionId;
@Id
@Column(nullable = false)
String name;
@Parent @Transient Key<EntityGroupRoot> parent = getCrossTldKey();
// The list in Cloud SQL is immutable, we only have a creation_timestamp field and it should be
// set to the timestamp when the list is created. In Datastore, we have two fields and the
// lastUpdateTime is set to the current timestamp when creating and updating a list. So, we use
// lastUpdateTime as the creation_timestamp column during the dual-write phase for compatibility.
@Column(name = "creation_timestamp")
DateTime creationTimestamp;
@@ -20,10 +20,10 @@ import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.net.InternetDomainName;
import com.googlecode.objectify.annotation.Id;
import google.registry.model.Buildable.GenericBuilder;
import google.registry.model.ImmutableObject;
import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
/**
@@ -36,13 +36,12 @@ public abstract class DomainLabelEntry<T extends Comparable<?>, D extends Domain
extends ImmutableObject implements Comparable<D> {
@Id
@javax.persistence.Id
@Column(name = "domainLabel", nullable = false)
String domainLabel;
/**
* Returns the label of the field, which also happens to be used as the key for the Map object
* that is serialized from Datastore.
* that is serialized from the database.
*/
public String getDomainLabel() {
return domainLabel;
@@ -48,6 +48,8 @@ import google.registry.util.SystemSleeper;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
@@ -73,7 +75,6 @@ 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;
/** Implementation of {@link JpaTransactionManager} for JPA compatible database. */
@@ -119,22 +120,23 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public EntityManager getEntityManager() {
if (transactionInfo.get().entityManager == null) {
EntityManager entityManager = transactionInfo.get().entityManager;
if (entityManager == null) {
throw new PersistenceException(
"No EntityManager has been initialized. getEntityManager() must be invoked in the scope"
+ " of a transaction");
}
return transactionInfo.get().entityManager;
return entityManager;
}
@Override
public <T> TypedQuery<T> query(String sqlString, Class<T> resultClass) {
return new DetachingTypedQuery(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));
return new DetachingTypedQuery<>(getEntityManager().createQuery(criteriaQuery));
}
@Override
@@ -171,7 +173,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
return work.get();
}
TransactionInfo txnInfo = transactionInfo.get();
txnInfo.entityManager = emf.createEntityManager();
txnInfo.entityManager = createReadOnlyCheckingEntityManager();
EntityTransaction txn = txnInfo.entityManager.getTransaction();
try {
txn.begin();
@@ -203,7 +205,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
return work.get();
}
TransactionInfo txnInfo = transactionInfo.get();
txnInfo.entityManager = emf.createEntityManager();
txnInfo.entityManager = createReadOnlyCheckingEntityManager();
EntityTransaction txn = txnInfo.entityManager.getTransaction();
try {
txn.begin();
@@ -594,7 +596,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public <T> QueryComposer<T> createQueryComposer(Class<T> entity) {
return new JpaQueryComposerImpl<T>(entity);
return new JpaQueryComposerImpl<>(entity);
}
@Override
@@ -607,6 +609,38 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
return false;
}
@Override
public void putIgnoringReadOnly(Object entity) {
checkArgumentNotNull(entity);
if (isEntityOfIgnoredClass(entity)) {
return;
}
assertInTransaction();
// Necessary due to the changes in HistoryEntry representation during the migration to SQL
Object toPersist = toSqlEntity(entity);
TransactionInfo txn = transactionInfo.get();
Object merged = txn.entityManager.mergeIgnoringReadOnly(toPersist);
txn.objectsToSave.add(merged);
txn.addUpdate(toPersist);
}
@Override
public void deleteIgnoringReadOnly(VKey<?> key) {
checkArgumentNotNull(key, "key must be specified");
assertInTransaction();
if (IGNORED_ENTITY_CLASSES.contains(key.getKind())) {
return;
}
EntityType<?> entityType = getEntityType(key.getKind());
ImmutableSet<EntityId> entityIds = getEntityIdsFromSqlKey(entityType, key.getSqlKey());
String sql =
String.format("DELETE FROM %s WHERE %s", entityType.getName(), getAndClause(entityIds));
ReadOnlyCheckingQuery query = transactionInfo.get().entityManager.createQuery(sql);
entityIds.forEach(entityId -> query.setParameter(entityId.name, entityId.value));
transactionInfo.get().addDelete(key);
query.executeUpdateIgnoringReadOnly();
}
@Override
public <T> void assertDelete(VKey<T> key) {
if (internalDelete(key) != 1) {
@@ -615,6 +649,10 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
}
private ReadOnlyCheckingEntityManager createReadOnlyCheckingEntityManager() {
return new ReadOnlyCheckingEntityManager(emf.createEntityManager());
}
private <T> EntityType<T> getEntityType(Class<T> clazz) {
return emf.getMetamodel().entity(clazz);
}
@@ -657,10 +695,22 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
private static ImmutableSet<EntityId> getEntityIdsFromIdContainer(
EntityType<?> entityType, Object idContainer) {
return entityType.getIdClassAttributes().stream()
.map(SingularAttribute::getName)
.map(
idName -> {
Object idValue = getFieldValue(idContainer, idName);
attribute -> {
String idName = attribute.getName();
// The object may use either Java getters or field names to represent the ID object.
// Attempt the Java getter, then fall back to the field name if that fails.
String methodName = attribute.getJavaMember().getName();
Object idValue;
try {
Method method = idContainer.getClass().getDeclaredMethod(methodName);
method.setAccessible(true);
idValue = method.invoke(idContainer);
} catch (NoSuchMethodException
| IllegalAccessException
| InvocationTargetException e) {
idValue = getFieldValue(idContainer, idName);
}
return new EntityId(idName, idValue);
})
.collect(toImmutableSet());
@@ -750,7 +800,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
private static class TransactionInfo {
EntityManager entityManager;
ReadOnlyCheckingEntityManager entityManager;
boolean inTransaction = false;
DateTime transactionTime;
@@ -0,0 +1,320 @@
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence.transaction;
import static google.registry.persistence.transaction.TransactionManagerFactory.assertNotReadOnlyMode;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Query;
import javax.persistence.StoredProcedureQuery;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaDelete;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.CriteriaUpdate;
import javax.persistence.metamodel.Metamodel;
/** An {@link EntityManager} that throws exceptions on write actions if in read-only mode. */
public class ReadOnlyCheckingEntityManager implements EntityManager {
private final EntityManager delegate;
public ReadOnlyCheckingEntityManager(EntityManager delegate) {
this.delegate = delegate;
}
@Override
public void persist(Object entity) {
assertNotReadOnlyMode();
delegate.persist(entity);
}
@Override
public <T> T merge(T entity) {
assertNotReadOnlyMode();
return delegate.merge(entity);
}
@Override
public void remove(Object entity) {
assertNotReadOnlyMode();
delegate.remove(entity);
}
@Override
public <T> T find(Class<T> entityClass, Object primaryKey) {
return delegate.find(entityClass, primaryKey);
}
@Override
public <T> T find(Class<T> entityClass, Object primaryKey, Map<String, Object> properties) {
return delegate.find(entityClass, primaryKey, properties);
}
@Override
public <T> T find(Class<T> entityClass, Object primaryKey, LockModeType lockMode) {
return delegate.find(entityClass, primaryKey, lockMode);
}
@Override
public <T> T find(
Class<T> entityClass,
Object primaryKey,
LockModeType lockMode,
Map<String, Object> properties) {
return delegate.find(entityClass, primaryKey, lockMode, properties);
}
@Override
public <T> T getReference(Class<T> entityClass, Object primaryKey) {
return delegate.getReference(entityClass, primaryKey);
}
@Override
public void flush() {
delegate.flush();
}
@Override
public void setFlushMode(FlushModeType flushMode) {
delegate.setFlushMode(flushMode);
}
@Override
public FlushModeType getFlushMode() {
return delegate.getFlushMode();
}
@Override
public void lock(Object entity, LockModeType lockMode) {
assertNotReadOnlyMode();
delegate.lock(entity, lockMode);
}
@Override
public void lock(Object entity, LockModeType lockMode, Map<String, Object> properties) {
assertNotReadOnlyMode();
delegate.lock(entity, lockMode, properties);
}
@Override
public void refresh(Object entity) {
delegate.refresh(entity);
}
@Override
public void refresh(Object entity, Map<String, Object> properties) {
delegate.refresh(entity, properties);
}
@Override
public void refresh(Object entity, LockModeType lockMode) {
delegate.refresh(entity, lockMode);
}
@Override
public void refresh(Object entity, LockModeType lockMode, Map<String, Object> properties) {
delegate.refresh(entity, lockMode, properties);
}
@Override
public void clear() {
delegate.clear();
}
@Override
public void detach(Object entity) {
delegate.detach(entity);
}
@Override
public boolean contains(Object entity) {
return delegate.contains(entity);
}
@Override
public LockModeType getLockMode(Object entity) {
return delegate.getLockMode(entity);
}
@Override
public void setProperty(String propertyName, Object value) {
delegate.setProperty(propertyName, value);
}
@Override
public Map<String, Object> getProperties() {
return delegate.getProperties();
}
@Override
public ReadOnlyCheckingQuery createQuery(String qlString) {
return new ReadOnlyCheckingQuery(delegate.createQuery(qlString));
}
@Override
public <T> TypedQuery<T> createQuery(CriteriaQuery<T> criteriaQuery) {
return new ReadOnlyCheckingTypedQuery<>(delegate.createQuery(criteriaQuery));
}
@Override
public Query createQuery(CriteriaUpdate updateQuery) {
assertNotReadOnlyMode();
return delegate.createQuery(updateQuery);
}
@Override
public Query createQuery(CriteriaDelete deleteQuery) {
assertNotReadOnlyMode();
return delegate.createQuery(deleteQuery);
}
@Override
public <T> TypedQuery<T> createQuery(String qlString, Class<T> resultClass) {
return new ReadOnlyCheckingTypedQuery<>(delegate.createQuery(qlString, resultClass));
}
@Override
public Query createNamedQuery(String name) {
return new ReadOnlyCheckingQuery(delegate.createNamedQuery(name));
}
@Override
public <T> TypedQuery<T> createNamedQuery(String name, Class<T> resultClass) {
return new ReadOnlyCheckingTypedQuery<>(delegate.createNamedQuery(name, resultClass));
}
@Override
public Query createNativeQuery(String sqlString) {
return new ReadOnlyCheckingQuery(delegate.createNativeQuery(sqlString));
}
@Override
public Query createNativeQuery(String sqlString, Class resultClass) {
return new ReadOnlyCheckingQuery(delegate.createNativeQuery(sqlString, resultClass));
}
@Override
public Query createNativeQuery(String sqlString, String resultSetMapping) {
return new ReadOnlyCheckingQuery(delegate.createNativeQuery(sqlString, resultSetMapping));
}
@Override
public StoredProcedureQuery createNamedStoredProcedureQuery(String name) {
assertNotReadOnlyMode();
return delegate.createNamedStoredProcedureQuery(name);
}
@Override
public StoredProcedureQuery createStoredProcedureQuery(String procedureName) {
assertNotReadOnlyMode();
return delegate.createStoredProcedureQuery(procedureName);
}
@Override
public StoredProcedureQuery createStoredProcedureQuery(
String procedureName, Class... resultClasses) {
assertNotReadOnlyMode();
return delegate.createStoredProcedureQuery(procedureName, resultClasses);
}
@Override
public StoredProcedureQuery createStoredProcedureQuery(
String procedureName, String... resultSetMappings) {
assertNotReadOnlyMode();
return delegate.createStoredProcedureQuery(procedureName, resultSetMappings);
}
@Override
public void joinTransaction() {
delegate.joinTransaction();
}
@Override
public boolean isJoinedToTransaction() {
return delegate.isJoinedToTransaction();
}
@Override
public <T> T unwrap(Class<T> cls) {
return delegate.unwrap(cls);
}
@Override
public Object getDelegate() {
return delegate.getDelegate();
}
@Override
public void close() {
delegate.close();
}
@Override
public boolean isOpen() {
return delegate.isOpen();
}
@Override
public EntityTransaction getTransaction() {
return delegate.getTransaction();
}
@Override
public EntityManagerFactory getEntityManagerFactory() {
return delegate.getEntityManagerFactory();
}
@Override
public CriteriaBuilder getCriteriaBuilder() {
return delegate.getCriteriaBuilder();
}
@Override
public Metamodel getMetamodel() {
return delegate.getMetamodel();
}
@Override
public <T> EntityGraph<T> createEntityGraph(Class<T> rootType) {
return delegate.createEntityGraph(rootType);
}
@Override
public EntityGraph<?> createEntityGraph(String graphName) {
return delegate.createEntityGraph(graphName);
}
@Override
public EntityGraph<?> getEntityGraph(String graphName) {
return delegate.getEntityGraph(graphName);
}
@Override
public <T> List<EntityGraph<? super T>> getEntityGraphs(Class<T> entityClass) {
return delegate.getEntityGraphs(entityClass);
}
public <T> T mergeIgnoringReadOnly(T entity) {
return delegate.merge(entity);
}
}
@@ -0,0 +1,203 @@
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence.transaction;
import static google.registry.persistence.transaction.TransactionManagerFactory.assertNotReadOnlyMode;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Parameter;
import javax.persistence.Query;
import javax.persistence.TemporalType;
/** A {@link Query} that throws exceptions on write actions if in read-only mode. */
class ReadOnlyCheckingQuery implements Query {
private final Query delegate;
ReadOnlyCheckingQuery(Query delegate) {
this.delegate = delegate;
}
@Override
public List getResultList() {
return delegate.getResultList();
}
@Override
public Object getSingleResult() {
return delegate.getSingleResult();
}
@Override
public int executeUpdate() {
assertNotReadOnlyMode();
return delegate.executeUpdate();
}
@Override
public Query setMaxResults(int maxResult) {
return delegate.setMaxResults(maxResult);
}
@Override
public int getMaxResults() {
return delegate.getMaxResults();
}
@Override
public Query setFirstResult(int startPosition) {
return delegate.setFirstResult(startPosition);
}
@Override
public int getFirstResult() {
return delegate.getFirstResult();
}
@Override
public Query setHint(String hintName, Object value) {
return delegate.setHint(hintName, value);
}
@Override
public Map<String, Object> getHints() {
return delegate.getHints();
}
@Override
public <T> Query setParameter(Parameter<T> param, T value) {
return delegate.setParameter(param, value);
}
@Override
public Query setParameter(Parameter<Calendar> param, Calendar value, TemporalType temporalType) {
return delegate.setParameter(param, value, temporalType);
}
@Override
public Query setParameter(Parameter<Date> param, Date value, TemporalType temporalType) {
return delegate.setParameter(param, value, temporalType);
}
@Override
public Query setParameter(String name, Object value) {
return delegate.setParameter(name, value);
}
@Override
public Query setParameter(String name, Calendar value, TemporalType temporalType) {
return delegate.setParameter(name, value, temporalType);
}
@Override
public Query setParameter(String name, Date value, TemporalType temporalType) {
return delegate.setParameter(name, value, temporalType);
}
@Override
public Query setParameter(int position, Object value) {
return delegate.setParameter(position, value);
}
@Override
public Query setParameter(int position, Calendar value, TemporalType temporalType) {
return delegate.setParameter(position, value, temporalType);
}
@Override
public Query setParameter(int position, Date value, TemporalType temporalType) {
return delegate.setParameter(position, value, temporalType);
}
@Override
public Set<Parameter<?>> getParameters() {
return delegate.getParameters();
}
@Override
public Parameter<?> getParameter(String name) {
return delegate.getParameter(name);
}
@Override
public <T> Parameter<T> getParameter(String name, Class<T> type) {
return delegate.getParameter(name, type);
}
@Override
public Parameter<?> getParameter(int position) {
return delegate.getParameter(position);
}
@Override
public <T> Parameter<T> getParameter(int position, Class<T> type) {
return delegate.getParameter(position, type);
}
@Override
public boolean isBound(Parameter<?> param) {
return delegate.isBound(param);
}
@Override
public <T> T getParameterValue(Parameter<T> 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 Query setFlushMode(FlushModeType flushMode) {
return delegate.setFlushMode(flushMode);
}
@Override
public FlushModeType getFlushMode() {
return delegate.getFlushMode();
}
@Override
public Query setLockMode(LockModeType lockMode) {
return delegate.setLockMode(lockMode);
}
@Override
public LockModeType getLockMode() {
return delegate.getLockMode();
}
@Override
public <T> T unwrap(Class<T> cls) {
return delegate.unwrap(cls);
}
public int executeUpdateIgnoringReadOnly() {
return delegate.executeUpdate();
}
}
@@ -0,0 +1,200 @@
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence.transaction;
import static google.registry.persistence.transaction.TransactionManagerFactory.assertNotReadOnlyMode;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Parameter;
import javax.persistence.TemporalType;
import javax.persistence.TypedQuery;
/** A {@link TypedQuery <T>} that throws exceptions on write actions if in read-only mode. */
class ReadOnlyCheckingTypedQuery<T> implements TypedQuery<T> {
private final TypedQuery<T> delegate;
ReadOnlyCheckingTypedQuery(TypedQuery<T> delegate) {
this.delegate = delegate;
}
@Override
public List<T> getResultList() {
return delegate.getResultList();
}
@Override
public T getSingleResult() {
return delegate.getSingleResult();
}
@Override
public int executeUpdate() {
assertNotReadOnlyMode();
return delegate.executeUpdate();
}
@Override
public TypedQuery<T> setMaxResults(int maxResult) {
return delegate.setMaxResults(maxResult);
}
@Override
public int getMaxResults() {
return delegate.getMaxResults();
}
@Override
public TypedQuery<T> setFirstResult(int startPosition) {
return delegate.setFirstResult(startPosition);
}
@Override
public int getFirstResult() {
return delegate.getFirstResult();
}
@Override
public TypedQuery<T> setHint(String hintName, Object value) {
return delegate.setHint(hintName, value);
}
@Override
public Map<String, Object> getHints() {
return delegate.getHints();
}
@Override
public <T1> TypedQuery<T> setParameter(Parameter<T1> param, T1 value) {
return delegate.setParameter(param, value);
}
@Override
public TypedQuery<T> setParameter(
Parameter<Calendar> param, Calendar value, TemporalType temporalType) {
return delegate.setParameter(param, value, temporalType);
}
@Override
public TypedQuery<T> setParameter(Parameter<Date> param, Date value, TemporalType temporalType) {
return delegate.setParameter(param, value, temporalType);
}
@Override
public TypedQuery<T> setParameter(String name, Object value) {
return delegate.setParameter(name, value);
}
@Override
public TypedQuery<T> setParameter(String name, Calendar value, TemporalType temporalType) {
return delegate.setParameter(name, value, temporalType);
}
@Override
public TypedQuery<T> setParameter(String name, Date value, TemporalType temporalType) {
return delegate.setParameter(name, value, temporalType);
}
@Override
public TypedQuery<T> setParameter(int position, Object value) {
return delegate.setParameter(position, value);
}
@Override
public TypedQuery<T> setParameter(int position, Calendar value, TemporalType temporalType) {
return delegate.setParameter(position, value, temporalType);
}
@Override
public TypedQuery<T> setParameter(int position, Date value, TemporalType temporalType) {
return delegate.setParameter(position, value, temporalType);
}
@Override
public Set<Parameter<?>> getParameters() {
return delegate.getParameters();
}
@Override
public Parameter<?> getParameter(String name) {
return delegate.getParameter(name);
}
@Override
public <X> Parameter<X> getParameter(String name, Class<X> type) {
return delegate.getParameter(name, type);
}
@Override
public Parameter<?> getParameter(int position) {
return delegate.getParameter(position);
}
@Override
public <X> Parameter<X> getParameter(int position, Class<X> type) {
return delegate.getParameter(position, type);
}
@Override
public boolean isBound(Parameter<?> param) {
return delegate.isBound(param);
}
@Override
public <X> X getParameterValue(Parameter<X> 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 TypedQuery<T> setFlushMode(FlushModeType flushMode) {
return delegate.setFlushMode(flushMode);
}
@Override
public FlushModeType getFlushMode() {
return delegate.getFlushMode();
}
@Override
public TypedQuery<T> setLockMode(LockModeType lockMode) {
return delegate.setLockMode(lockMode);
}
@Override
public LockModeType getLockMode() {
return delegate.getLockMode();
}
@Override
public <X> X unwrap(Class<X> cls) {
return delegate.unwrap(cls);
}
}
@@ -307,4 +307,10 @@ public interface TransactionManager {
/** Returns true if the transaction manager is DatastoreTransactionManager, false otherwise. */
boolean isOfy();
/** Performs the given write ignoring any read-only restrictions, for use only in replay. */
void putIgnoringReadOnly(Object entity);
/** Performs the given delete ignoring any read-only restrictions, for use only in replay. */
void deleteIgnoringReadOnly(VKey<?> key);
}
@@ -86,9 +86,11 @@ public class TransactionManagerFactory {
if (tmForTest.isPresent()) {
return tmForTest.get();
}
PrimaryDatabase primaryDatabase =
DatabaseMigrationStateSchedule.getValueAtTime(DateTime.now(UTC)).getPrimaryDatabase();
return primaryDatabase.equals(PrimaryDatabase.DATASTORE) ? ofyTm() : jpaTm();
return DatabaseMigrationStateSchedule.getValueAtTime(DateTime.now(UTC))
.getPrimaryDatabase()
.equals(PrimaryDatabase.DATASTORE)
? ofyTm()
: jpaTm();
}
/**
@@ -141,4 +143,17 @@ public class TransactionManagerFactory {
public static void removeTmOverrideForTest() {
tmForTest = Optional.empty();
}
public static void assertNotReadOnlyMode() {
if (DatabaseMigrationStateSchedule.getValueAtTime(DateTime.now(UTC)).isReadOnly()) {
throw new ReadOnlyModeException();
}
}
/** Thrown when a write is attempted when the DB is in read-only mode. */
public static class ReadOnlyModeException extends IllegalStateException {
public ReadOnlyModeException() {
super("Registry is currently in read-only mode");
}
}
}
@@ -40,6 +40,7 @@ import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables;
import com.google.common.truth.Truth8;
import com.google.common.util.concurrent.MoreExecutors;
import com.googlecode.objectify.Key;
@@ -357,10 +358,10 @@ public class ReplayCommitLogsToSqlActionTest {
// even though the domain came first in the file
// 2. that the allocation token delete occurred after the insertions
InOrder inOrder = Mockito.inOrder(spy);
inOrder.verify(spy).put(any(ContactResource.class));
inOrder.verify(spy).put(any(DomainBase.class));
inOrder.verify(spy).delete(toDelete.createVKey());
inOrder.verify(spy).put(any(SqlReplayCheckpoint.class));
inOrder.verify(spy).putIgnoringReadOnly(any(ContactResource.class));
inOrder.verify(spy).putIgnoringReadOnly(any(DomainBase.class));
inOrder.verify(spy).deleteIgnoringReadOnly(toDelete.createVKey());
inOrder.verify(spy).putIgnoringReadOnly(any(SqlReplayCheckpoint.class));
}
@Test
@@ -399,8 +400,8 @@ public class ReplayCommitLogsToSqlActionTest {
// deletes have higher weight
ArgumentCaptor<Object> putCaptor = ArgumentCaptor.forClass(Object.class);
InOrder inOrder = Mockito.inOrder(spy);
inOrder.verify(spy).delete(contact.createVKey());
inOrder.verify(spy).put(putCaptor.capture());
inOrder.verify(spy).deleteIgnoringReadOnly(contact.createVKey());
inOrder.verify(spy).putIgnoringReadOnly(putCaptor.capture());
assertThat(putCaptor.getValue().getClass()).isEqualTo(ContactResource.class);
assertThat(jpaTm().transact(() -> jpaTm().loadByKey(contact.createVKey()).getEmailAddress()))
.isEqualTo("replay@example.tld");
@@ -441,9 +442,9 @@ public class ReplayCommitLogsToSqlActionTest {
}
});
runAndAssertSuccess(now.minusMinutes(1), 1, 1);
// jpaTm()::put should only have been called with the checkpoint
verify(spy, times(2)).put(any(SqlReplayCheckpoint.class));
verify(spy, times(2)).put(any());
// jpaTm()::putIgnoringReadOnly should only have been called with the checkpoint
verify(spy, times(2)).putIgnoringReadOnly(any(SqlReplayCheckpoint.class));
verify(spy, times(2)).putIgnoringReadOnly(any());
}
@Test
@@ -556,6 +557,34 @@ public class ReplayCommitLogsToSqlActionTest {
});
}
@Test
void testReplay_duringReadOnly() throws Exception {
DateTime now = fakeClock.nowUtc();
jpaTm()
.transact(
() -> {
jpaTm().insertWithoutBackup(TestObject.create("previous to delete"));
SqlReplayCheckpoint.set(now.minusMinutes(2));
});
Key<CommitLogManifest> manifestKey =
CommitLogManifest.createKey(getBucketKey(1), now.minusMinutes(1));
saveDiffFile(
gcsUtils,
createCheckpoint(now.minusMinutes(1)),
CommitLogManifest.create(
getBucketKey(1),
now.minusMinutes(1),
ImmutableSet.of(Key.create(TestObject.create("previous to delete")))),
CommitLogMutation.create(manifestKey, TestObject.create("a")));
DatabaseHelper.setMigrationScheduleToDatastorePrimaryReadOnly(fakeClock);
runAndAssertSuccess(now.minusMinutes(1), 1, 1);
jpaTm()
.transact(
() ->
assertThat(Iterables.getOnlyElement(jpaTm().loadAllOf(TestObject.class)).getId())
.isEqualTo("a"));
}
@Test
void testReplay_deleteAndResaveCascade_withOtherDeletion_noErrors() throws Exception {
createTld("tld");
@@ -15,10 +15,13 @@
package google.registry.batch;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadByEntitiesIfPresent;
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.persistActiveHost;
@@ -46,18 +49,20 @@ import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tld.Registry;
import google.registry.model.tld.Registry.TldType;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeResponse;
import google.registry.testing.SystemPropertyExtension;
import google.registry.testing.TestOfyAndSql;
import google.registry.testing.mapreduce.MapreduceTestCase;
import java.util.Optional;
import java.util.Set;
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.extension.RegisterExtension;
/** Unit tests for {@link DeleteProberDataAction}. */
@DualDatabaseTest
class DeleteProberDataActionTest extends MapreduceTestCase<DeleteProberDataAction> {
private static final DateTime DELETION_TIME = DateTime.parse("2010-01-01T00:00:00.000Z");
@@ -93,7 +98,7 @@ class DeleteProberDataActionTest extends MapreduceTestCase<DeleteProberDataActio
action.response = new FakeResponse();
action.isDryRun = false;
action.tlds = ImmutableSet.of();
action.registryAdminClientId = "TheRegistrar";
action.registryAdminRegistrarId = "TheRegistrar";
RegistryEnvironment.SANDBOX.setup(systemPropertyExtension);
}
@@ -102,7 +107,7 @@ class DeleteProberDataActionTest extends MapreduceTestCase<DeleteProberDataActio
executeTasksUntilEmpty("mapreduce");
}
@Test
@TestOfyAndSql
void test_deletesAllAndOnlyProberData() throws Exception {
Set<ImmutableObject> tldEntities = persistLotsOfDomains("tld");
Set<ImmutableObject> exampleEntities = persistLotsOfDomains("example");
@@ -110,14 +115,14 @@ class DeleteProberDataActionTest extends MapreduceTestCase<DeleteProberDataActio
Set<ImmutableObject> ibEntities = persistLotsOfDomains("ib-any.test");
Set<ImmutableObject> oaEntities = persistLotsOfDomains("oa-canary.test");
runMapreduce();
assertNotDeleted(tldEntities);
assertNotDeleted(exampleEntities);
assertNotDeleted(notTestEntities);
assertDeleted(ibEntities);
assertDeleted(oaEntities);
assertAllExist(tldEntities);
assertAllExist(exampleEntities);
assertAllExist(notTestEntities);
assertAllAbsent(ibEntities);
assertAllAbsent(oaEntities);
}
@Test
@TestOfyAndSql
void testSuccess_deletesAllAndOnlyGivenTlds() throws Exception {
Set<ImmutableObject> tldEntities = persistLotsOfDomains("tld");
Set<ImmutableObject> exampleEntities = persistLotsOfDomains("example");
@@ -126,14 +131,14 @@ class DeleteProberDataActionTest extends MapreduceTestCase<DeleteProberDataActio
Set<ImmutableObject> oaEntities = persistLotsOfDomains("oa-canary.test");
action.tlds = ImmutableSet.of("example", "ib-any.test");
runMapreduce();
assertNotDeleted(tldEntities);
assertNotDeleted(notTestEntities);
assertNotDeleted(oaEntities);
assertDeleted(exampleEntities);
assertDeleted(ibEntities);
assertAllExist(tldEntities);
assertAllExist(notTestEntities);
assertAllExist(oaEntities);
assertAllAbsent(exampleEntities);
assertAllAbsent(ibEntities);
}
@Test
@TestOfyAndSql
void testFail_givenNonTestTld() {
action.tlds = ImmutableSet.of("not-test.test");
IllegalArgumentException thrown =
@@ -143,7 +148,7 @@ class DeleteProberDataActionTest extends MapreduceTestCase<DeleteProberDataActio
.contains("If tlds are given, they must all exist and be TEST tlds");
}
@Test
@TestOfyAndSql
void testFail_givenNonExistentTld() {
action.tlds = ImmutableSet.of("non-existent.test");
IllegalArgumentException thrown =
@@ -153,7 +158,7 @@ class DeleteProberDataActionTest extends MapreduceTestCase<DeleteProberDataActio
.contains("If tlds are given, they must all exist and be TEST tlds");
}
@Test
@TestOfyAndSql
void testFail_givenNonDotTestTldOnProd() {
action.tlds = ImmutableSet.of("example");
RegistryEnvironment.PRODUCTION.setup(systemPropertyExtension);
@@ -164,44 +169,46 @@ class DeleteProberDataActionTest extends MapreduceTestCase<DeleteProberDataActio
.contains("On production, can only work on TLDs that end with .test");
}
@Test
@TestOfyAndSql
void testSuccess_doesntDeleteNicDomainForProbers() throws Exception {
DomainBase nic = persistActiveDomain("nic.ib-any.test");
ForeignKeyIndex<DomainBase> fkiNic =
ForeignKeyIndex.load(DomainBase.class, "nic.ib-any.test", START_OF_TIME);
Set<ImmutableObject> ibEntities = persistLotsOfDomains("ib-any.test");
runMapreduce();
assertDeleted(ibEntities);
assertNotDeleted(ImmutableSet.of(nic, fkiNic));
assertAllAbsent(ibEntities);
assertAllExist(ImmutableSet.of(nic));
if (tm().isOfy()) {
assertAllExist(ImmutableSet.of(fkiNic));
}
}
@Test
@TestOfyAndSql
void testDryRun_doesntDeleteData() throws Exception {
Set<ImmutableObject> tldEntities = persistLotsOfDomains("tld");
Set<ImmutableObject> oaEntities = persistLotsOfDomains("oa-canary.test");
action.isDryRun = true;
runMapreduce();
assertNotDeleted(tldEntities);
assertNotDeleted(oaEntities);
assertAllExist(tldEntities);
assertAllExist(oaEntities);
}
@Test
@TestOfyAndSql
void testSuccess_activeDomain_isSoftDeleted() throws Exception {
DomainBase domain = persistResource(
newDomainBase("blah.ib-any.test")
.asBuilder()
.setCreationTimeForTest(DateTime.now(UTC).minusYears(1))
.build());
DomainBase domain =
persistResource(
newDomainBase("blah.ib-any.test")
.asBuilder()
.setCreationTimeForTest(DateTime.now(UTC).minusYears(1))
.build());
runMapreduce();
DateTime timeAfterDeletion = DateTime.now(UTC);
assertThat(loadByForeignKey(DomainBase.class, "blah.ib-any.test", timeAfterDeletion))
.isEmpty();
assertThat(auditedOfy().load().entity(domain).now().getDeletionTime())
.isLessThan(timeAfterDeletion);
assertThat(loadByForeignKey(DomainBase.class, "blah.ib-any.test", timeAfterDeletion)).isEmpty();
assertThat(loadByEntity(domain).getDeletionTime()).isLessThan(timeAfterDeletion);
assertDnsTasksEnqueued("blah.ib-any.test");
}
@Test
@TestOfyAndSql
void testSuccess_activeDomain_doubleMapSoftDeletes() throws Exception {
DomainBase domain = persistResource(
newDomainBase("blah.ib-any.test")
@@ -214,12 +221,11 @@ class DeleteProberDataActionTest extends MapreduceTestCase<DeleteProberDataActio
runMapreduce();
assertThat(loadByForeignKey(DomainBase.class, "blah.ib-any.test", timeAfterDeletion))
.isEmpty();
assertThat(auditedOfy().load().entity(domain).now().getDeletionTime())
.isLessThan(timeAfterDeletion);
assertThat(loadByEntity(domain).getDeletionTime()).isLessThan(timeAfterDeletion);
assertDnsTasksEnqueued("blah.ib-any.test");
}
@Test
@TestOfyAndSql
void test_recentlyCreatedDomain_isntDeletedYet() throws Exception {
persistResource(
newDomainBase("blah.ib-any.test")
@@ -233,19 +239,20 @@ class DeleteProberDataActionTest extends MapreduceTestCase<DeleteProberDataActio
assertThat(domain.get().getDeletionTime()).isEqualTo(END_OF_TIME);
}
@Test
@TestOfyAndSql
void testDryRun_doesntSoftDeleteData() throws Exception {
DomainBase domain = persistResource(
newDomainBase("blah.ib-any.test")
.asBuilder()
.setCreationTimeForTest(DateTime.now(UTC).minusYears(1))
.build());
DomainBase domain =
persistResource(
newDomainBase("blah.ib-any.test")
.asBuilder()
.setCreationTimeForTest(DateTime.now(UTC).minusYears(1))
.build());
action.isDryRun = true;
runMapreduce();
assertThat(auditedOfy().load().entity(domain).now().getDeletionTime()).isEqualTo(END_OF_TIME);
assertThat(loadByEntity(domain).getDeletionTime()).isEqualTo(END_OF_TIME);
}
@Test
@TestOfyAndSql
void test_domainWithSubordinateHosts_isSkipped() throws Exception {
persistActiveHost("ns1.blah.ib-any.test");
DomainBase nakedDomain =
@@ -258,18 +265,19 @@ class DeleteProberDataActionTest extends MapreduceTestCase<DeleteProberDataActio
.build(),
DateTime.now(UTC).minusYears(1));
runMapreduce();
assertThat(auditedOfy().load().entity(domainWithSubord).now()).isNotNull();
assertThat(auditedOfy().load().entity(nakedDomain).now()).isNull();
assertAllExist(ImmutableSet.of(domainWithSubord));
assertAllAbsent(ImmutableSet.of(nakedDomain));
}
@Test
@TestOfyAndSql
void testFailure_registryAdminClientId_isRequiredForSoftDeletion() {
persistResource(
newDomainBase("blah.ib-any.test")
.asBuilder()
.setCreationTimeForTest(DateTime.now(UTC).minusYears(1))
.build());
action.registryAdminClientId = null;
action.registryAdminRegistrarId = null;
IllegalStateException thrown = assertThrows(IllegalStateException.class, this::runMapreduce);
assertThat(thrown).hasMessageThat().contains("Registry admin client ID must be configured");
}
@@ -299,19 +307,26 @@ class DeleteProberDataActionTest extends MapreduceTestCase<DeleteProberDataActio
.setEventTime(DELETION_TIME)
.setTargetId(fqdn)
.build());
PollMessage.OneTime pollMessage = persistSimpleResource(
new PollMessage.OneTime.Builder()
.setParent(historyEntry)
.setEventTime(DELETION_TIME)
.setClientId("TheRegistrar")
.setMsg("Domain registered")
.build());
ForeignKeyIndex<DomainBase> fki =
ForeignKeyIndex.load(DomainBase.class, fqdn, START_OF_TIME);
EppResourceIndex eppIndex =
auditedOfy().load().entity(EppResourceIndex.create(Key.create(domain))).now();
return ImmutableSet.of(
domain, historyEntry, billingEvent, pollMessage, fki, eppIndex);
PollMessage.OneTime pollMessage =
persistSimpleResource(
new PollMessage.OneTime.Builder()
.setParent(historyEntry)
.setEventTime(DELETION_TIME)
.setClientId("TheRegistrar")
.setMsg("Domain registered")
.build());
ImmutableSet.Builder<ImmutableObject> builder =
new ImmutableSet.Builder<ImmutableObject>()
.add(domain)
.add(historyEntry)
.add(billingEvent)
.add(pollMessage);
if (tm().isOfy()) {
builder
.add(ForeignKeyIndex.load(DomainBase.class, fqdn, START_OF_TIME))
.add(loadByEntity(EppResourceIndex.create(Key.create(domain))));
}
return builder.build();
}
private static Set<ImmutableObject> persistLotsOfDomains(String tld) {
@@ -322,15 +337,15 @@ class DeleteProberDataActionTest extends MapreduceTestCase<DeleteProberDataActio
return persistedObjects.build();
}
private static void assertNotDeleted(Iterable<ImmutableObject> entities) {
for (ImmutableObject entity : entities) {
assertThat(auditedOfy().load().entity(entity).now()).isNotNull();
}
private static void assertAllExist(Iterable<ImmutableObject> entities) {
assertWithMessage("Expected entities to exist in the DB but they were deleted")
.that(loadByEntitiesIfPresent(entities))
.containsExactlyElementsIn(entities);
}
private static void assertDeleted(Iterable<ImmutableObject> entities) {
for (ImmutableObject entity : entities) {
assertThat(auditedOfy().load().entity(entity).now()).isNull();
}
private static void assertAllAbsent(Iterable<ImmutableObject> entities) {
assertWithMessage("Expected entities to not exist in the DB, but they did")
.that(loadByEntitiesIfPresent(entities))
.isEmpty();
}
}
@@ -55,6 +55,53 @@ import org.junit.jupiter.api.extension.RegisterExtension;
@DualDatabaseTest
class SendExpiringCertificateNotificationEmailActionTest {
private static final String EXPIRATION_WARNING_EMAIL_BODY_TEXT =
" Dear %1$s,\n"
+ "\n"
+ " We would like to inform you that your %2$s SSL certificate will expire at\n"
+ " %3$s. Please take note that using expired certificates will prevent\n"
+ " successful Registry login.\n"
+ "\n"
+ " Kindly update your production account certificate within the support\n"
+ " console using the following steps:\n"
+ "\n"
+ " 1. Navigate to support.registry.google and login using your\n"
+ " %4$s@registry.google credentials.\n"
+ " * If this is your first time logging in, you will be prompted to\n"
+ " reset your password, so please keep your new password safe.\n"
+ " * If you are already logged in with some other Google account(s) but\n"
+ " not your %4$s@registry.google account, you need to click on\n"
+ " “Add Account” and login using your %4$s@registry.google credentials.\n"
+ " 2. Select “Settings > Security” from the left navigation bar.\n"
+ " 3. Click “Edit” on the top left corner.\n"
+ " 4. Enter your full certificate string\n"
+ " (including lines -----BEGIN CERTIFICATE----- and\n"
+ " -----END CERTIFICATE-----) in the box.\n"
+ " 5. Click “Save”. If there are validation issues with the form, you will\n"
+ " be prompted to fix them and click “Save” again.\n"
+ "\n"
+ " A failover SSL certificate can also be added in order to prevent connection\n"
+ " issues once your main certificate expires. Connecting with either of the\n"
+ " certificates will work with our production EPP server.\n"
+ "\n"
+ " Further information about our EPP connection requirements can be found in\n"
+ " section 9.2 in the updated Technical Guide in your Google Drive folder.\n"
+ "\n"
+ " Note that account certificate changes take a few minutes to become\n"
+ " effective and that the existing connections will remain unaffected by\n"
+ " the change.\n"
+ "\n"
+ " If you also would like to update your OT&E account certificate, please send\n"
+ " an email from your primary or technical contact to\n"
+ " registry-support@google.com and include the full certificate string\n"
+ " (including lines -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----).\n"
+ "\n"
+ " Regards,\n"
+ " Google Registry\n";
private static final String EXPIRATION_WARNING_EMAIL_SUBJECT_TEXT =
"[Important] Expiring SSL certificate for Google " + "Registry EPP connection";
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().withTaskQueue().build();
@@ -77,14 +124,11 @@ class SendExpiringCertificateNotificationEmailActionTest {
2048,
ImmutableSet.of("secp256r1", "secp384r1"),
clock);
String expirationWarningEmailBodyText =
" Hello Registrar %s,\n" + " The %s certificate is expiring on %s.";
String expirationWarningEmailSubjectText = "expiring certificate notification email";
action =
new SendExpiringCertificateNotificationEmailAction(
expirationWarningEmailBodyText,
expirationWarningEmailSubjectText,
EXPIRATION_WARNING_EMAIL_BODY_TEXT,
EXPIRATION_WARNING_EMAIL_SUBJECT_TEXT,
new InternetAddress("test@example.com"),
sendEmailService,
certificateChecker,
@@ -578,12 +622,21 @@ class SendExpiringCertificateNotificationEmailActionTest {
String registrarName = "good registrar";
String certExpirationDateStr = "2021-06-15";
CertificateType certificateType = CertificateType.PRIMARY;
String registrarId = "registrarid";
String emailBody =
action.getEmailBody(
registrarName, certificateType, DateTime.parse(certExpirationDateStr).toDate());
registrarName,
certificateType,
DateTime.parse(certExpirationDateStr).toDate(),
registrarId);
assertThat(emailBody).contains(registrarName);
assertThat(emailBody).contains(certificateType.getDisplayName());
assertThat(emailBody).contains(certExpirationDateStr);
assertThat(emailBody).contains(registrarId + "@registry.google");
assertThat(emailBody).doesNotContain("%1$s@registry.google");
assertThat(emailBody).doesNotContain("%2$s@registry.google");
assertThat(emailBody).doesNotContain("%3$s@registry.google");
assertThat(emailBody).doesNotContain("%4$s@registry.google");
}
@TestOfyAndSql
@@ -591,7 +644,9 @@ class SendExpiringCertificateNotificationEmailActionTest {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> action.getEmailBody("good registrar", CertificateType.FAILOVER, null));
() ->
action.getEmailBody(
"good registrar", CertificateType.FAILOVER, null, "registrarId"));
assertThat(thrown).hasMessageThat().contains("Expiration date cannot be null");
}
@@ -601,7 +656,22 @@ class SendExpiringCertificateNotificationEmailActionTest {
assertThrows(
IllegalArgumentException.class,
() ->
action.getEmailBody("good registrar", null, DateTime.parse("2021-06-15").toDate()));
action.getEmailBody(
"good registrar", null, DateTime.parse("2021-06-15").toDate(), "registrarId"));
assertThat(thrown).hasMessageThat().contains("Certificate type cannot be null");
}
@TestOfyAndSql
void getEmailBody_throwsIllegalArgumentException_noRegistrarId() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
action.getEmailBody(
"good registrar",
CertificateType.FAILOVER,
DateTime.parse("2021-06-15").toDate(),
null));
assertThat(thrown).hasMessageThat().contains("Registrar Id cannot be null");
}
}
@@ -91,6 +91,7 @@ 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;
import org.junitpioneer.jupiter.RetryingTest;
/** Unit tests for {@link RdePipeline}. */
public class RdePipelineTest {
@@ -288,7 +289,8 @@ public class RdePipelineTest {
pipeline.run().waitUntilFinish();
}
@Test
// The GCS folder listing can be a bit flaky, so retry if necessary
@RetryingTest(4)
void testSuccess_persistData() throws Exception {
PendingDeposit brdaKey =
PendingDeposit.create("soy", now, THIN, CursorType.BRDA, Duration.standardDays(1));
@@ -314,7 +316,8 @@ public class RdePipelineTest {
assertThat(loadRevision(now, FULL)).isEqualTo(1);
}
@Test
// The GCS folder listing can be a bit flaky, so retry if necessary
@RetryingTest(4)
void testSuccess_persistData_manual() throws Exception {
PendingDeposit brdaKey = PendingDeposit.createInManualOperation("soy", now, THIN, "test/", 0);
PendingDeposit rdeKey = PendingDeposit.createInManualOperation("soy", now, FULL, "test/", 0);
@@ -23,12 +23,17 @@ import static google.registry.model.common.DatabaseMigrationStateSchedule.Migrat
import static google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState.SQL_PRIMARY_READ_ONLY;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistResource;
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 google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenType;
import google.registry.persistence.transaction.TransactionManagerFactory.ReadOnlyModeException;
import google.registry.testing.DatabaseHelper;
import org.joda.time.DateTime;
import org.joda.time.Duration;
@@ -152,6 +157,20 @@ public class DatabaseMigrationStateScheduleTest extends EntityTestCase {
assertThat(tm().isOfy()).isFalse();
}
@Test
void testSuccess_factoryUsesReadOnly() {
createTld("tld");
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);
assertThrows(ReadOnlyModeException.class, () -> persistResource(token));
runValidTransition(DATASTORE_PRIMARY_READ_ONLY, SQL_PRIMARY_READ_ONLY);
assertThrows(ReadOnlyModeException.class, () -> persistResource(token));
runValidTransition(SQL_PRIMARY_READ_ONLY, SQL_PRIMARY);
persistResource(token);
}
private void runValidTransition(MigrationState from, MigrationState to) {
ImmutableSortedMap<DateTime, MigrationState> transitions =
createMapEndingWithTransition(from, to);
@@ -47,6 +47,7 @@ import google.registry.model.domain.DomainBase;
import google.registry.model.eppcommon.Trid;
import google.registry.model.replay.EntityTest.EntityForTesting;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.transaction.TransactionManagerFactory.ReadOnlyModeException;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
@@ -61,9 +62,11 @@ import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for our wrapper around Objectify. */
public class OfyTest {
private final FakeClock fakeClock = new FakeClock(DateTime.parse("2000-01-01TZ"));
@RegisterExtension
public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().build();
AppEngineExtension.builder().withDatastoreAndCloudSql().withClock(fakeClock).build();
/** An entity to use in save and delete tests. */
private HistoryEntry someObject;
@@ -434,4 +437,12 @@ public class OfyTest {
// Test the normal loading again to verify that we've restored the original session unchanged.
assertThat(auditedOfy().load().entity(someObject).now()).isEqualTo(someObject.asHistoryEntry());
}
@Test
void testReadOnly_failsWrite() {
Ofy ofy = new Ofy(fakeClock);
DatabaseHelper.setMigrationScheduleToDatastorePrimaryReadOnly(fakeClock);
assertThrows(ReadOnlyModeException.class, () -> ofy.save().entity(someObject).now());
DatabaseHelper.removeDatabaseMigrationSchedule();
}
}
@@ -212,7 +212,7 @@ public class ReplicateToDatastoreActionTest {
@Test
void testNotInMigrationState_doesNothing() {
// set a schedule that backtracks the current status to DATASTORE_PRIMARY_READ_ONLY
// set a schedule that backtracks the current status to DATASTORE_PRIMARY
DateTime now = fakeClock.nowUtc();
jpaTm()
.transact(
@@ -225,6 +225,7 @@ public class ReplicateToDatastoreActionTest {
.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)
.put(now.plusHours(3), MigrationState.DATASTORE_PRIMARY)
.build()));
fakeClock.advanceBy(Duration.standardDays(1));
@@ -237,6 +238,6 @@ public class ReplicateToDatastoreActionTest {
.hasLogAtLevelWithMessage(
Level.INFO,
"Skipping ReplicateToDatastoreAction because we are in migration phase "
+ "DATASTORE_PRIMARY_READ_ONLY.");
+ "DATASTORE_PRIMARY.");
}
}
@@ -286,7 +286,6 @@ class ReservedListTest {
ReservedList clone = original.asBuilder().build();
assertThat(clone.getName()).isEqualTo("tld-reserved-cloning");
assertThat(clone.creationTimestamp).isEqualTo(original.creationTimestamp);
assertThat(clone.parent).isEqualTo(original.parent);
assertThat(original.getReservedListEntries()).isEqualTo(clone.getReservedListEntries());
}
@@ -75,7 +75,8 @@ class JpaTransactionManagerImplTest {
new JpaTestRules.Builder()
.withInitScript(fileClassPath(getClass(), "test_schema.sql"))
.withClock(fakeClock)
.withEntityClass(TestEntity.class, TestCompoundIdEntity.class)
.withEntityClass(
TestEntity.class, TestCompoundIdEntity.class, TestNamedCompoundIdEntity.class)
.buildUnitTestRule();
@Test
@@ -272,6 +273,24 @@ class JpaTransactionManagerImplTest {
.isEqualTo(compoundIdEntity);
}
@Test
void createNamedCompoundIdEntity_succeeds() {
// Compound IDs should also work even if the field names don't match up exactly
TestNamedCompoundIdEntity entity = new TestNamedCompoundIdEntity("foo", 1);
jpaTm().transact(() -> jpaTm().insert(entity));
jpaTm()
.transact(
() -> {
assertThat(jpaTm().exists(entity)).isTrue();
assertThat(
jpaTm()
.loadByKey(
VKey.createSql(
TestNamedCompoundIdEntity.class, new NamedCompoundId("foo", 1))))
.isEqualTo(entity);
});
}
@Test
void saveAllNew_succeeds() {
moreEntities.forEach(
@@ -638,7 +657,9 @@ class JpaTransactionManagerImplTest {
jpaTm()
.transact(
() ->
jpaTm().query("FROM TestEntity", TestEntity.class).getResultList().stream()
jpaTm()
.query("FROM TestEntity", TestEntity.class)
.getResultList()
.forEach(e -> assertThat(jpaTm().getEntityManager().contains(e)).isFalse()));
jpaTm()
.transact(
@@ -777,4 +798,71 @@ class JpaTransactionManagerImplTest {
this.age = age;
}
}
// An entity should still behave properly if the name fields in the ID are different
@Entity(name = "TestNamedCompoundIdEntity")
@IdClass(NamedCompoundId.class)
private static class TestNamedCompoundIdEntity extends ImmutableObject {
private String name;
private int age;
private TestNamedCompoundIdEntity() {}
private TestNamedCompoundIdEntity(String name, int age) {
this.name = name;
this.age = age;
}
@Id
public String getNameField() {
return name;
}
@Id
public int getAgeField() {
return age;
}
@SuppressWarnings("unused")
private void setNameField(String name) {
this.name = name;
}
@SuppressWarnings("unused")
private void setAgeField(int age) {
this.age = age;
}
}
private static class NamedCompoundId implements Serializable {
String nameField;
int ageField;
private NamedCompoundId() {}
private NamedCompoundId(String nameField, int ageField) {
this.nameField = nameField;
this.ageField = ageField;
}
@SuppressWarnings("unused")
private String getNameField() {
return nameField;
}
@SuppressWarnings("unused")
private int getAgeField() {
return ageField;
}
@SuppressWarnings("unused")
private void setNameField(String nameField) {
this.nameField = nameField;
}
@SuppressWarnings("unused")
private void setAgeField(int ageField) {
this.ageField = ageField;
}
}
}
@@ -31,7 +31,9 @@ import google.registry.model.ImmutableObject;
import google.registry.model.ofy.DatastoreTransactionManager;
import google.registry.model.ofy.Ofy;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.TransactionManagerFactory.ReadOnlyModeException;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.InjectExtension;
@@ -406,6 +408,13 @@ public class TransactionManagerTest {
assertThat(tm().transact(() -> tm().loadByKey(theEntity.key())).data).isEqualTo("foo");
}
@TestOfyAndSql
void testReadOnly_writeFails() {
DatabaseHelper.setMigrationScheduleToDatastorePrimaryReadOnly(fakeClock);
assertThrows(ReadOnlyModeException.class, () -> tm().transact(() -> tm().put(theEntity)));
DatabaseHelper.removeDatabaseMigrationSchedule();
}
private static void assertEntityExists(TestEntity entity) {
assertThat(tm().transact(() -> tm().exists(entity))).isTrue();
}
@@ -443,7 +443,7 @@ public class DatabaseHelper {
* Deletes "domain" and all history records, billing events, poll messages and subordinate hosts.
*/
public static void deleteTestDomain(DomainBase domain, DateTime now) {
Iterable<BillingEvent> billingEvents = getBillingEvents();
Iterable<BillingEvent> billingEvents = getBillingEvents(domain);
Iterable<? extends HistoryEntry> historyEntries =
HistoryEntryDao.loadHistoryObjectsForResource(domain.createVKey());
Iterable<PollMessage> pollMessages = loadAllOf(PollMessage.class);
@@ -791,13 +791,13 @@ public class DatabaseHelper {
return transactIfJpaTm(
() ->
Iterables.concat(
tm().loadAllOf(BillingEvent.OneTime.class).stream()
tm().loadAllOfStream(BillingEvent.OneTime.class)
.filter(oneTime -> oneTime.getDomainRepoId().equals(resource.getRepoId()))
.collect(toImmutableList()),
tm().loadAllOf(BillingEvent.Recurring.class).stream()
tm().loadAllOfStream(BillingEvent.Recurring.class)
.filter(recurring -> recurring.getDomainRepoId().equals(resource.getRepoId()))
.collect(toImmutableList()),
tm().loadAllOf(BillingEvent.Cancellation.class).stream()
tm().loadAllOfStream(BillingEvent.Cancellation.class)
.filter(
cancellation -> cancellation.getDomainRepoId().equals(resource.getRepoId()))
.collect(toImmutableList())));
@@ -1351,7 +1351,25 @@ public class DatabaseHelper {
}
/**
* Asserts that the given entity is detached from the current JPA entity manager.
* Loads all given entities from the database if possible.
*
* <p>If the transaction manager is Cloud SQL, then this creates an inner wrapping transaction for
* convenience, so you don't need to wrap it in a transaction at the callsite.
*
* <p>Nonexistent entities are absent from the resulting list, but no {@link
* NoSuchElementException} will be thrown.
*/
public static <T> ImmutableList<T> loadByEntitiesIfPresent(Iterable<T> entities) {
return transactIfJpaTm(() -> tm().loadByEntitiesIfPresent(entities));
}
/** Returns whether or not the given entity exists in the database. */
public static boolean existsInDatabase(Object object) {
return transactIfJpaTm(() -> tm().exists(object));
}
/**
* In JPA mode, asserts that the given entity is detached from the current entity manager.
*
* <p>Returns the original entity object.
*/
@@ -1360,6 +1378,33 @@ public class DatabaseHelper {
return entity;
}
/**
* Sets a DATASTORE_PRIMARY_READ_ONLY 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 setMigrationScheduleToDatastorePrimaryReadOnly(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_READ_ONLY)));
fakeClock.advanceBy(Duration.standardSeconds(1));
}
/**
* Sets a SQL_PRIMARY state on the {@link DatabaseMigrationStateSchedule}.
*
@@ -1395,8 +1440,9 @@ public class DatabaseHelper {
.transact(
() ->
jpaTm()
.loadSingleton(DatabaseMigrationStateSchedule.class)
.ifPresent(jpaTm()::delete));
.putIgnoringReadOnly(
new DatabaseMigrationStateSchedule(
DatabaseMigrationStateSchedule.DEFAULT_TRANSITION_MAP)));
DatabaseMigrationStateSchedule.CACHE.invalidateAll();
}