mirror of
https://github.com/google/nomulus.git
synced 2025-05-14 00:17:20 +02:00
Better handle deletion of prober domains
This implements a two-part deletion process for prober domains that were not deleted properly by the prober (which is usually caused by a transient network failure). The first time the mapreduce is run, such domains are soft-deleted, so that their DNS entries can be removed correctly, and then they are hard-deleted in the subsequent run. Currently, all domains are hard-deleted the first time this mapreduce runs, even if they were never soft-deleted correctly, which means that their published DNS entries won't be correctly handled. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165612973
This commit is contained in:
parent
b0c254dc65
commit
46f175e078
2 changed files with 167 additions and 10 deletions
|
@ -14,29 +14,40 @@
|
||||||
|
|
||||||
package google.registry.batch;
|
package google.registry.batch;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
import static google.registry.flows.ResourceFlowUtils.updateForeignKeyIndexDeletionTime;
|
||||||
import static google.registry.mapreduce.MapreduceRunner.PARAM_DRY_RUN;
|
import static google.registry.mapreduce.MapreduceRunner.PARAM_DRY_RUN;
|
||||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||||
import static google.registry.model.registry.Registries.getTldsOfType;
|
import static google.registry.model.registry.Registries.getTldsOfType;
|
||||||
|
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_DELETE;
|
||||||
import static google.registry.request.Action.Method.POST;
|
import static google.registry.request.Action.Method.POST;
|
||||||
|
import static org.joda.time.DateTimeZone.UTC;
|
||||||
|
|
||||||
import com.google.appengine.tools.mapreduce.Mapper;
|
import com.google.appengine.tools.mapreduce.Mapper;
|
||||||
import com.google.common.base.Function;
|
import com.google.common.base.Function;
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.base.Splitter;
|
import com.google.common.base.Splitter;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.FluentIterable;
|
import com.google.common.collect.FluentIterable;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.googlecode.objectify.Key;
|
import com.googlecode.objectify.Key;
|
||||||
|
import com.googlecode.objectify.VoidWork;
|
||||||
import com.googlecode.objectify.Work;
|
import com.googlecode.objectify.Work;
|
||||||
|
import google.registry.config.RegistryConfig.Config;
|
||||||
|
import google.registry.dns.DnsQueue;
|
||||||
import google.registry.mapreduce.MapreduceRunner;
|
import google.registry.mapreduce.MapreduceRunner;
|
||||||
import google.registry.mapreduce.inputs.EppResourceInputs;
|
import google.registry.mapreduce.inputs.EppResourceInputs;
|
||||||
|
import google.registry.model.EppResourceUtils;
|
||||||
import google.registry.model.domain.DomainApplication;
|
import google.registry.model.domain.DomainApplication;
|
||||||
import google.registry.model.domain.DomainBase;
|
import google.registry.model.domain.DomainBase;
|
||||||
|
import google.registry.model.domain.DomainResource;
|
||||||
import google.registry.model.index.EppResourceIndex;
|
import google.registry.model.index.EppResourceIndex;
|
||||||
import google.registry.model.index.ForeignKeyIndex;
|
import google.registry.model.index.ForeignKeyIndex;
|
||||||
import google.registry.model.registry.Registry;
|
import google.registry.model.registry.Registry;
|
||||||
import google.registry.model.registry.Registry.TldType;
|
import google.registry.model.registry.Registry.TldType;
|
||||||
|
import google.registry.model.reporting.HistoryEntry;
|
||||||
import google.registry.request.Action;
|
import google.registry.request.Action;
|
||||||
import google.registry.request.Parameter;
|
import google.registry.request.Parameter;
|
||||||
import google.registry.request.Response;
|
import google.registry.request.Response;
|
||||||
|
@ -45,6 +56,7 @@ import google.registry.util.FormattingLogger;
|
||||||
import google.registry.util.PipelineUtils;
|
import google.registry.util.PipelineUtils;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import org.joda.time.DateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes all prober DomainResources and their subordinate history entries, poll messages, and
|
* Deletes all prober DomainResources and their subordinate history entries, poll messages, and
|
||||||
|
@ -62,17 +74,21 @@ public class DeleteProberDataAction implements Runnable {
|
||||||
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
||||||
|
|
||||||
@Inject @Parameter(PARAM_DRY_RUN) boolean isDryRun;
|
@Inject @Parameter(PARAM_DRY_RUN) boolean isDryRun;
|
||||||
|
@Inject @Config("registryAdminClientId") String registryAdminClientId;
|
||||||
@Inject MapreduceRunner mrRunner;
|
@Inject MapreduceRunner mrRunner;
|
||||||
@Inject Response response;
|
@Inject Response response;
|
||||||
@Inject DeleteProberDataAction() {}
|
@Inject DeleteProberDataAction() {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
checkState(
|
||||||
|
!Strings.isNullOrEmpty(registryAdminClientId),
|
||||||
|
"Registry admin client ID must be configured for prober data deletion to work");
|
||||||
response.sendJavaScriptRedirect(PipelineUtils.createJobPath(mrRunner
|
response.sendJavaScriptRedirect(PipelineUtils.createJobPath(mrRunner
|
||||||
.setJobName("Delete prober data")
|
.setJobName("Delete prober data")
|
||||||
.setModuleName("backend")
|
.setModuleName("backend")
|
||||||
.runMapOnly(
|
.runMapOnly(
|
||||||
new DeleteProberDataMapper(getProberRoidSuffixes(), isDryRun),
|
new DeleteProberDataMapper(getProberRoidSuffixes(), isDryRun, registryAdminClientId),
|
||||||
ImmutableList.of(EppResourceInputs.createKeyInput(DomainBase.class)))));
|
ImmutableList.of(EppResourceInputs.createKeyInput(DomainBase.class)))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,14 +113,18 @@ public class DeleteProberDataAction implements Runnable {
|
||||||
/** Provides the map method that runs for each existing DomainBase entity. */
|
/** Provides the map method that runs for each existing DomainBase entity. */
|
||||||
public static class DeleteProberDataMapper extends Mapper<Key<DomainBase>, Void, Void> {
|
public static class DeleteProberDataMapper extends Mapper<Key<DomainBase>, Void, Void> {
|
||||||
|
|
||||||
private static final long serialVersionUID = 1737761271804180412L;
|
private static final DnsQueue dnsQueue = DnsQueue.create();
|
||||||
|
private static final long serialVersionUID = -7724537393697576369L;
|
||||||
|
|
||||||
private final ImmutableSet<String> proberRoidSuffixes;
|
private final ImmutableSet<String> proberRoidSuffixes;
|
||||||
private final Boolean isDryRun;
|
private final Boolean isDryRun;
|
||||||
|
private final String registryAdminClientId;
|
||||||
|
|
||||||
public DeleteProberDataMapper(ImmutableSet<String> proberRoidSuffixes, Boolean isDryRun) {
|
public DeleteProberDataMapper(
|
||||||
|
ImmutableSet<String> proberRoidSuffixes, Boolean isDryRun, String registryAdminClientId) {
|
||||||
this.proberRoidSuffixes = proberRoidSuffixes;
|
this.proberRoidSuffixes = proberRoidSuffixes;
|
||||||
this.isDryRun = isDryRun;
|
this.isDryRun = isDryRun;
|
||||||
|
this.registryAdminClientId = registryAdminClientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -123,22 +143,47 @@ public class DeleteProberDataAction implements Runnable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteDomain(final Key<DomainBase> domainKey) {
|
private void deleteDomain(final Key<DomainBase> domainKey) {
|
||||||
final DomainBase domain = ofy().load().key(domainKey).now();
|
final DomainBase domainBase = ofy().load().key(domainKey).now();
|
||||||
if (domain == null) {
|
if (domainBase == null) {
|
||||||
// Depending on how stale Datastore indexes are, we can get keys to resources that are
|
// Depending on how stale Datastore indexes are, we can get keys to resources that are
|
||||||
// already deleted (e.g. by a recent previous invocation of this mapreduce). So ignore them.
|
// already deleted (e.g. by a recent previous invocation of this mapreduce). So ignore them.
|
||||||
getContext().incrementCounter("already deleted");
|
getContext().incrementCounter("already deleted");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (domain instanceof DomainApplication) {
|
if (domainBase instanceof DomainApplication) {
|
||||||
// Cover the case where we somehow have a domain application with a prober ROID suffix.
|
// Cover the case where we somehow have a domain application with a prober ROID suffix.
|
||||||
getContext().incrementCounter("skipped, domain application");
|
getContext().incrementCounter("skipped, domain application");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DomainResource domain = (DomainResource) domainBase;
|
||||||
if (domain.getFullyQualifiedDomainName().equals("nic." + domain.getTld())) {
|
if (domain.getFullyQualifiedDomainName().equals("nic." + domain.getTld())) {
|
||||||
getContext().incrementCounter("skipped, NIC domain");
|
getContext().incrementCounter("skipped, NIC domain");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (domain.getCreationTime().isAfter(DateTime.now(UTC).minusHours(1))) {
|
||||||
|
getContext().incrementCounter("skipped, domain too new");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!domain.getSubordinateHosts().isEmpty()) {
|
||||||
|
logger.warningfmt("Cannot delete domain %s because it has subordinate hosts.", domainKey);
|
||||||
|
getContext().incrementCounter("skipped, had subordinate host(s)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 mapreduce is run.
|
||||||
|
if (EppResourceUtils.isActive(domain, DateTime.now(UTC))) {
|
||||||
|
if (isDryRun) {
|
||||||
|
logger.infofmt("Would soft-delete the active domain: %s", domainKey);
|
||||||
|
} else {
|
||||||
|
softDeleteDomain(domain);
|
||||||
|
}
|
||||||
|
getContext().incrementCounter("domains soft-deleted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final Key<EppResourceIndex> eppIndex = Key.create(EppResourceIndex.create(domainKey));
|
final Key<EppResourceIndex> eppIndex = Key.create(EppResourceIndex.create(domainKey));
|
||||||
final Key<? extends ForeignKeyIndex<?>> fki = ForeignKeyIndex.createKey(domain);
|
final Key<? extends ForeignKeyIndex<?>> fki = ForeignKeyIndex.createKey(domain);
|
||||||
|
@ -155,15 +200,42 @@ public class DeleteProberDataAction implements Runnable {
|
||||||
.addAll(domainAndDependentKeys)
|
.addAll(domainAndDependentKeys)
|
||||||
.build();
|
.build();
|
||||||
if (isDryRun) {
|
if (isDryRun) {
|
||||||
logger.infofmt("Would delete the following entities: %s", allKeys);
|
logger.infofmt("Would hard-delete the following entities: %s", allKeys);
|
||||||
} else {
|
} else {
|
||||||
ofy().deleteWithoutBackup().keys(allKeys);
|
ofy().deleteWithoutBackup().keys(allKeys);
|
||||||
}
|
}
|
||||||
return allKeys.size();
|
return allKeys.size();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
getContext().incrementCounter("domains deleted");
|
getContext().incrementCounter("domains hard-deleted");
|
||||||
getContext().incrementCounter("total entities deleted", entitiesDeleted);
|
getContext().incrementCounter("total entities hard-deleted", entitiesDeleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void softDeleteDomain(final DomainResource domain) {
|
||||||
|
ofy().transactNew(new VoidWork() {
|
||||||
|
@Override
|
||||||
|
public void vrun() {
|
||||||
|
DomainResource deletedDomain = domain
|
||||||
|
.asBuilder()
|
||||||
|
.setDeletionTime(ofy().getTransactionTime())
|
||||||
|
.setStatusValues(null)
|
||||||
|
.build();
|
||||||
|
HistoryEntry historyEntry = new HistoryEntry.Builder()
|
||||||
|
.setParent(domain)
|
||||||
|
.setType(DOMAIN_DELETE)
|
||||||
|
.setModificationTime(ofy().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.
|
||||||
|
ofy().save().entities(deletedDomain, historyEntry);
|
||||||
|
updateForeignKeyIndexDeletionTime(deletedDomain);
|
||||||
|
dnsQueue.addDomainRefreshTask(deletedDomain.getFullyQualifiedDomainName());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,21 @@
|
||||||
package google.registry.batch;
|
package google.registry.batch;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static com.google.common.truth.Truth.assert_;
|
||||||
|
import static google.registry.model.EppResourceUtils.loadByForeignKey;
|
||||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||||
import static google.registry.testing.DatastoreHelper.createTld;
|
import static google.registry.testing.DatastoreHelper.createTld;
|
||||||
|
import static google.registry.testing.DatastoreHelper.newDomainResource;
|
||||||
import static google.registry.testing.DatastoreHelper.persistActiveDomain;
|
import static google.registry.testing.DatastoreHelper.persistActiveDomain;
|
||||||
|
import static google.registry.testing.DatastoreHelper.persistActiveHost;
|
||||||
import static google.registry.testing.DatastoreHelper.persistDeletedDomain;
|
import static google.registry.testing.DatastoreHelper.persistDeletedDomain;
|
||||||
|
import static google.registry.testing.DatastoreHelper.persistDomainAsDeleted;
|
||||||
import static google.registry.testing.DatastoreHelper.persistResource;
|
import static google.registry.testing.DatastoreHelper.persistResource;
|
||||||
import static google.registry.testing.DatastoreHelper.persistSimpleResource;
|
import static google.registry.testing.DatastoreHelper.persistSimpleResource;
|
||||||
|
import static google.registry.testing.TaskQueueHelper.assertDnsTasksEnqueued;
|
||||||
|
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||||
|
import static org.joda.time.DateTimeZone.UTC;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.googlecode.objectify.Key;
|
import com.googlecode.objectify.Key;
|
||||||
|
@ -75,6 +83,7 @@ public class DeleteProberDataActionTest extends MapreduceTestCase<DeleteProberDa
|
||||||
action.mrRunner = makeDefaultRunner();
|
action.mrRunner = makeDefaultRunner();
|
||||||
action.response = new FakeResponse();
|
action.response = new FakeResponse();
|
||||||
action.isDryRun = false;
|
action.isDryRun = false;
|
||||||
|
action.registryAdminClientId = "TheRegistrar";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runMapreduce() throws Exception {
|
private void runMapreduce() throws Exception {
|
||||||
|
@ -107,14 +116,90 @@ public class DeleteProberDataActionTest extends MapreduceTestCase<DeleteProberDa
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSuccess_dryRunDoesntDeleteData() throws Exception {
|
public void testDryRun_doesntDeleteData() throws Exception {
|
||||||
Set<ImmutableObject> tldEntities = persistLotsOfDomains("tld");
|
Set<ImmutableObject> tldEntities = persistLotsOfDomains("tld");
|
||||||
Set<ImmutableObject> oaEntities = persistLotsOfDomains("oa-canary.test");
|
Set<ImmutableObject> oaEntities = persistLotsOfDomains("oa-canary.test");
|
||||||
action.isDryRun = true;
|
action.isDryRun = true;
|
||||||
|
runMapreduce();
|
||||||
assertNotDeleted(tldEntities);
|
assertNotDeleted(tldEntities);
|
||||||
assertNotDeleted(oaEntities);
|
assertNotDeleted(oaEntities);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_activeDomain_isSoftDeleted() throws Exception {
|
||||||
|
DomainResource domain = persistResource(
|
||||||
|
newDomainResource("blah.ib-any.test")
|
||||||
|
.asBuilder()
|
||||||
|
.setCreationTimeForTest(DateTime.now(UTC).minusYears(1))
|
||||||
|
.build());
|
||||||
|
runMapreduce();
|
||||||
|
DateTime timeAfterDeletion = DateTime.now(UTC);
|
||||||
|
assertThat(loadByForeignKey(DomainResource.class, "blah.ib-any.test", timeAfterDeletion))
|
||||||
|
.isNull();
|
||||||
|
assertThat(ofy().load().entity(domain).now().getDeletionTime()).isLessThan(timeAfterDeletion);
|
||||||
|
assertDnsTasksEnqueued("blah.ib-any.test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_recentlyCreatedDomain_isntDeletedYet() throws Exception {
|
||||||
|
persistResource(
|
||||||
|
newDomainResource("blah.ib-any.test")
|
||||||
|
.asBuilder()
|
||||||
|
.setCreationTimeForTest(DateTime.now(UTC).minusSeconds(1))
|
||||||
|
.build());
|
||||||
|
runMapreduce();
|
||||||
|
DomainResource domain =
|
||||||
|
loadByForeignKey(DomainResource.class, "blah.ib-any.test", DateTime.now(UTC));
|
||||||
|
assertThat(domain).isNotNull();
|
||||||
|
assertThat(domain.getDeletionTime()).isEqualTo(END_OF_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDryRun_doesntSoftDeleteData() throws Exception {
|
||||||
|
DomainResource domain = persistResource(
|
||||||
|
newDomainResource("blah.ib-any.test")
|
||||||
|
.asBuilder()
|
||||||
|
.setCreationTimeForTest(DateTime.now(UTC).minusYears(1))
|
||||||
|
.build());
|
||||||
|
action.isDryRun = true;
|
||||||
|
runMapreduce();
|
||||||
|
assertThat(ofy().load().entity(domain).now().getDeletionTime()).isEqualTo(END_OF_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_domainWithSubordinateHosts_isSkipped() throws Exception {
|
||||||
|
persistActiveHost("ns1.blah.ib-any.test");
|
||||||
|
DomainResource nakedDomain =
|
||||||
|
persistDeletedDomain("todelete.ib-any.test", DateTime.now(UTC).minusYears(1));
|
||||||
|
DomainResource domainWithSubord =
|
||||||
|
persistDomainAsDeleted(
|
||||||
|
newDomainResource("blah.ib-any.test")
|
||||||
|
.asBuilder()
|
||||||
|
.setSubordinateHosts(ImmutableSet.of("ns1.blah.ib-any.test"))
|
||||||
|
.build(),
|
||||||
|
DateTime.now(UTC).minusYears(1));
|
||||||
|
runMapreduce();
|
||||||
|
assertThat(ofy().load().entity(domainWithSubord).now()).isNotNull();
|
||||||
|
assertThat(ofy().load().entity(nakedDomain).now()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_registryAdminClientId_isRequiredForSoftDeletion() throws Exception {
|
||||||
|
persistResource(
|
||||||
|
newDomainResource("blah.ib-any.test")
|
||||||
|
.asBuilder()
|
||||||
|
.setCreationTimeForTest(DateTime.now(UTC).minusYears(1))
|
||||||
|
.build());
|
||||||
|
action.registryAdminClientId = null;
|
||||||
|
try {
|
||||||
|
runMapreduce();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
assertThat(e).hasMessageThat().contains("Registry admin client ID must be configured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert_().fail("Expected IllegalStateException");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persists and returns a domain and a descendant history entry, billing event, and poll message,
|
* Persists and returns a domain and a descendant history entry, billing event, and poll message,
|
||||||
* along with the ForeignKeyIndex and EppResourceIndex.
|
* along with the ForeignKeyIndex and EppResourceIndex.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue