mirror of
https://github.com/google/nomulus
synced 2026-06-09 16:33:02 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1dcb1299f | |||
| d46594c610 | |||
| 0a9fa8cf23 | |||
| db4bf90538 | |||
| d6127e4c0c | |||
| 447bfa162b | |||
| c9efa61198 | |||
| 054c0625a8 | |||
| b03639d7fc | |||
| bd9af0de84 |
@@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-8
@@ -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;
|
||||
|
||||
+62
-12
@@ -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;
|
||||
|
||||
|
||||
+320
@@ -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);
|
||||
}
|
||||
}
|
||||
+203
@@ -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();
|
||||
}
|
||||
}
|
||||
+200
@@ -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);
|
||||
}
|
||||
|
||||
+18
-3
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+78
-8
@@ -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);
|
||||
|
||||
+19
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
+90
-2
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user