Notify registrars of async contact/host deletions

We now send PendingActionNotificationResponses in our poll messages upon completion of an asynchronous contact or host deletion. This is part 1 of 2, which begins logging Trid in all enqueued Host/Contact deletion flows for use in batch deletions, and optionally consuming the resultant Trid info to emit a Host/ContactPendingActionNotifcationResponse.

Part 2 will make this response emission non-optional, which will happen once the queue is cleared of all non-Trid containing tasks.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=153084197
This commit is contained in:
larryruili 2017-04-13 11:32:11 -07:00 committed by Ben McIlwain
parent 478c7576c6
commit 5047d568de
12 changed files with 240 additions and 66 deletions

View file

@ -23,9 +23,11 @@ import static com.googlecode.objectify.Key.getKind;
import static google.registry.flows.ResourceFlowUtils.createResolvedTransferData;
import static google.registry.flows.ResourceFlowUtils.handlePendingTransferOnDelete;
import static google.registry.flows.ResourceFlowUtils.updateForeignKeyIndexDeletionTime;
import static google.registry.flows.async.AsyncFlowEnqueuer.PARAM_CLIENT_TRANSACTION_ID;
import static google.registry.flows.async.AsyncFlowEnqueuer.PARAM_IS_SUPERUSER;
import static google.registry.flows.async.AsyncFlowEnqueuer.PARAM_REQUESTING_CLIENT_ID;
import static google.registry.flows.async.AsyncFlowEnqueuer.PARAM_RESOURCE_KEY;
import static google.registry.flows.async.AsyncFlowEnqueuer.PARAM_SERVER_TRANSACTION_ID;
import static google.registry.flows.async.AsyncFlowEnqueuer.QUEUE_ASYNC_DELETE;
import static google.registry.model.EppResourceUtils.isActive;
import static google.registry.model.EppResourceUtils.isDeleted;
@ -68,7 +70,11 @@ import google.registry.model.annotations.ExternalMessagingName;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.eppoutput.EppResponse.ResponseData;
import google.registry.model.host.HostResource;
import google.registry.model.poll.PendingActionNotificationResponse.ContactPendingActionNotificationResponse;
import google.registry.model.poll.PendingActionNotificationResponse.HostPendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferStatus;
@ -318,6 +324,8 @@ public class DeleteContactsAndHostsAction implements Runnable {
.setMsg(pollMessageText)
.setParent(historyEntry)
.setEventTime(now)
.setResponseData(
getPollMessageResponseData(deletionRequest, resource, deleteAllowed, now))
.build();
EppResource resourceToSave;
@ -348,6 +356,35 @@ public class DeleteContactsAndHostsAction implements Runnable {
deleteAllowed ? Type.DELETED : Type.NOT_DELETED, pollMessageText);
}
private static ImmutableList<? extends ResponseData> getPollMessageResponseData(
DeletionRequest deletionRequest,
EppResource resource,
boolean deleteAllowed,
DateTime now) {
Optional<String> clientTransactionId = deletionRequest.clientTransactionId();
Optional<String> serverTransactionId = deletionRequest.serverTransactionId();
// TODO(b/36402020): Make this unconditional, once older tasks enqueued without Trid data
// have been processed out of the queue.
checkState(
clientTransactionId.isPresent() == serverTransactionId.isPresent(),
"Found one part of TRID without the other!");
if (clientTransactionId.isPresent() && serverTransactionId.isPresent()) {
Trid trid = Trid.create(clientTransactionId.get(), serverTransactionId.get());
if (resource instanceof HostResource) {
return ImmutableList.of(
HostPendingActionNotificationResponse.create(
((HostResource) resource).getFullyQualifiedHostName(), deleteAllowed, trid, now));
} else if (resource instanceof ContactResource) {
return ImmutableList.of(
ContactPendingActionNotificationResponse.create(
((ContactResource) resource).getContactId(), deleteAllowed, trid, now));
} else {
throw new IllegalStateException("EPP resource of unknown type " + Key.create(resource));
}
}
return ImmutableList.of();
}
/**
* Determine the proper history entry type for the delete operation, as a function of
* whether or not the delete was successful.
@ -403,6 +440,13 @@ public class DeleteContactsAndHostsAction implements Runnable {
* the actual current owner of the resource).
*/
abstract String requestingClientId();
/** First half of TRID for the original request, split for serializability. */
abstract Optional<String> clientTransactionId();
/** Second half of TRID for the original request, split for serializability. */
abstract Optional<String> serverTransactionId();
abstract boolean isSuperuser();
abstract TaskHandle task();
@ -411,6 +455,8 @@ public class DeleteContactsAndHostsAction implements Runnable {
abstract Builder setKey(Key<? extends EppResource> key);
abstract Builder setLastUpdateTime(DateTime lastUpdateTime);
abstract Builder setRequestingClientId(String requestingClientId);
abstract Builder setClientTransactionId(Optional<String> clientTransactionId);
abstract Builder setServerTransactionId(Optional<String> serverTransactionId);
abstract Builder setIsSuperuser(boolean isSuperuser);
abstract Builder setTask(TaskHandle task);
abstract DeletionRequest build();
@ -435,10 +481,18 @@ public class DeleteContactsAndHostsAction implements Runnable {
new AutoValue_DeleteContactsAndHostsAction_DeletionRequest.Builder()
.setKey(resourceKey)
.setLastUpdateTime(resource.getUpdateAutoTimestamp().getTimestamp())
.setRequestingClientId(checkNotNull(
params.get(PARAM_REQUESTING_CLIENT_ID), "Requesting client id not specified"))
.setIsSuperuser(Boolean.valueOf(
checkNotNull(params.get(PARAM_IS_SUPERUSER), "Is superuser not specified")))
.setRequestingClientId(
checkNotNull(
params.get(PARAM_REQUESTING_CLIENT_ID), "Requesting client id not specified"))
.setClientTransactionId(
Optional.fromNullable(
params.get(PARAM_CLIENT_TRANSACTION_ID)))
.setServerTransactionId(
Optional.fromNullable(
params.get(PARAM_SERVER_TRANSACTION_ID)))
.setIsSuperuser(
Boolean.valueOf(
checkNotNull(params.get(PARAM_IS_SUPERUSER), "Is superuser not specified")))
.setTask(task)
.build());
}

View file

@ -22,6 +22,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.googlecode.objectify.Key;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.EppResource;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.HostResource;
import google.registry.util.FormattingLogger;
import google.registry.util.Retrier;
@ -36,6 +37,8 @@ public final class AsyncFlowEnqueuer {
/** The HTTP parameter names used by async flows. */
public static final String PARAM_RESOURCE_KEY = "resourceKey";
public static final String PARAM_REQUESTING_CLIENT_ID = "requestingClientId";
public static final String PARAM_CLIENT_TRANSACTION_ID = "clientTransactionId";
public static final String PARAM_SERVER_TRANSACTION_ID = "serverTransactionId";
public static final String PARAM_IS_SUPERUSER = "isSuperuser";
public static final String PARAM_HOST_KEY = "hostKey";
@ -70,17 +73,18 @@ public final class AsyncFlowEnqueuer {
/** Enqueues a task to asynchronously delete a contact or host, by key. */
public void enqueueAsyncDelete(
EppResource resourceToDelete, String requestingClientId, boolean isSuperuser) {
EppResource resourceToDelete, String requestingClientId, Trid trid, boolean isSuperuser) {
Key<EppResource> resourceKey = Key.create(resourceToDelete);
logger.infofmt(
"Enqueuing async deletion of %s on behalf of registrar %s.",
resourceKey, requestingClientId);
TaskOptions task =
TaskOptions.Builder
.withMethod(Method.PULL)
TaskOptions.Builder.withMethod(Method.PULL)
.countdownMillis(asyncDeleteDelay.getMillis())
.param(PARAM_RESOURCE_KEY, resourceKey.getString())
.param(PARAM_REQUESTING_CLIENT_ID, requestingClientId)
.param(PARAM_CLIENT_TRANSACTION_ID, trid.getClientTransactionId())
.param(PARAM_SERVER_TRANSACTION_ID, trid.getServerTransactionId())
.param(PARAM_IS_SUPERUSER, Boolean.toString(isSuperuser));
addTaskToQueueWithRetry(asyncDeletePullQueue, task);
}

View file

@ -40,6 +40,7 @@ import google.registry.model.domain.DomainBase;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
@ -78,6 +79,7 @@ public final class ContactDeleteFlow implements TransactionalFlow {
@Inject ExtensionManager extensionManager;
@Inject @ClientId String clientId;
@Inject @TargetId String targetId;
@Inject Trid trid;
@Inject @Superuser boolean isSuperuser;
@Inject Optional<AuthInfo> authInfo;
@Inject HistoryEntry.Builder historyBuilder;
@ -98,7 +100,7 @@ public final class ContactDeleteFlow implements TransactionalFlow {
if (!isSuperuser) {
verifyResourceOwnership(clientId, existingContact);
}
asyncFlowEnqueuer.enqueueAsyncDelete(existingContact, clientId, isSuperuser);
asyncFlowEnqueuer.enqueueAsyncDelete(existingContact, clientId, trid, isSuperuser);
ContactResource newContact =
existingContact.asBuilder().addStatusValue(StatusValue.PENDING_DELETE).build();
historyBuilder

View file

@ -38,6 +38,7 @@ import google.registry.model.EppResource;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.host.HostResource;
import google.registry.model.reporting.HistoryEntry;
@ -80,6 +81,7 @@ public final class HostDeleteFlow implements TransactionalFlow {
@Inject ExtensionManager extensionManager;
@Inject @ClientId String clientId;
@Inject @TargetId String targetId;
@Inject Trid trid;
@Inject @Superuser boolean isSuperuser;
@Inject HistoryEntry.Builder historyBuilder;
@Inject AsyncFlowEnqueuer asyncFlowEnqueuer;
@ -106,7 +108,7 @@ public final class HostDeleteFlow implements TransactionalFlow {
: existingHost;
verifyResourceOwnership(clientId, owningResource);
}
asyncFlowEnqueuer.enqueueAsyncDelete(existingHost, clientId, isSuperuser);
asyncFlowEnqueuer.enqueueAsyncDelete(existingHost, clientId, trid, isSuperuser);
HostResource newHost =
existingHost.asBuilder().addStatusValue(StatusValue.PENDING_DELETE).build();
historyBuilder

View file

@ -60,6 +60,7 @@ import google.registry.model.host.HostInfoData;
import google.registry.model.poll.MessageQueueInfo;
import google.registry.model.poll.PendingActionNotificationResponse.ContactPendingActionNotificationResponse;
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
import google.registry.model.poll.PendingActionNotificationResponse.HostPendingActionNotificationResponse;
import google.registry.model.transfer.TransferResponse.ContactTransferResponse;
import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
import javax.annotation.Nullable;
@ -98,20 +99,22 @@ public class EppResponse extends ImmutableObject implements ResponseOrGreeting {
/** Zero or more response "resData" results. */
@XmlElementRefs({
@XmlElementRef(type = ContactCheckData.class),
@XmlElementRef(type = ContactCreateData.class),
@XmlElementRef(type = ContactInfoData.class),
@XmlElementRef(type = ContactPendingActionNotificationResponse.class),
@XmlElementRef(type = ContactTransferResponse.class),
@XmlElementRef(type = DomainCheckData.class),
@XmlElementRef(type = DomainCreateData.class),
@XmlElementRef(type = DomainInfoData.class),
@XmlElementRef(type = DomainPendingActionNotificationResponse.class),
@XmlElementRef(type = DomainRenewData.class),
@XmlElementRef(type = DomainTransferResponse.class),
@XmlElementRef(type = HostCheckData.class),
@XmlElementRef(type = HostCreateData.class),
@XmlElementRef(type = HostInfoData.class)})
@XmlElementRef(type = ContactCheckData.class),
@XmlElementRef(type = ContactCreateData.class),
@XmlElementRef(type = ContactInfoData.class),
@XmlElementRef(type = ContactPendingActionNotificationResponse.class),
@XmlElementRef(type = ContactTransferResponse.class),
@XmlElementRef(type = DomainCheckData.class),
@XmlElementRef(type = DomainCreateData.class),
@XmlElementRef(type = DomainInfoData.class),
@XmlElementRef(type = DomainPendingActionNotificationResponse.class),
@XmlElementRef(type = DomainRenewData.class),
@XmlElementRef(type = DomainTransferResponse.class),
@XmlElementRef(type = HostCheckData.class),
@XmlElementRef(type = HostCreateData.class),
@XmlElementRef(type = HostInfoData.class),
@XmlElementRef(type = HostPendingActionNotificationResponse.class)
})
@XmlElementWrapper
ImmutableList<? extends ResponseData> resData;

View file

@ -125,4 +125,30 @@ public abstract class PendingActionNotificationResponse
processedDate);
}
}
/** An adapter to output the XML in response to resolving a pending command on a host. */
@Embed
@XmlRootElement(name = "panData", namespace = "urn:ietf:params:xml:ns:domain-1.0")
@XmlType(
propOrder = {"name", "trid", "processedDate"},
namespace = "urn:ietf:params:xml:ns:domain-1.0"
)
public static class HostPendingActionNotificationResponse
extends PendingActionNotificationResponse {
@XmlElement
NameOrId getName() {
return nameOrId;
}
public static HostPendingActionNotificationResponse create(
String fullyQualifiedHostName, boolean actionResult, Trid trid, DateTime processedDate) {
return init(
new HostPendingActionNotificationResponse(),
fullyQualifiedHostName,
actionResult,
trid,
processedDate);
}
}
}

View file

@ -39,6 +39,7 @@ import google.registry.model.eppoutput.EppResponse.ResponseData;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
import google.registry.model.poll.PendingActionNotificationResponse.ContactPendingActionNotificationResponse;
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
import google.registry.model.poll.PendingActionNotificationResponse.HostPendingActionNotificationResponse;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData.TransferServerApproveEntity;
import google.registry.model.transfer.TransferResponse.ContactTransferResponse;
@ -189,6 +190,7 @@ public abstract class PollMessage extends ImmutableObject
List<ContactTransferResponse> contactTransferResponses;
List<DomainPendingActionNotificationResponse> domainPendingActionNotificationResponses;
List<DomainTransferResponse> domainTransferResponses;
List<HostPendingActionNotificationResponse> hostPendingActionNotificationResponses;
// Extensions. Objectify cannot persist a base class type, so we must have a separate field
// to hold every possible derived type of ResponseExtensions that we might store.
@ -211,6 +213,7 @@ public abstract class PollMessage extends ImmutableObject
.addAll(nullToEmpty(contactTransferResponses))
.addAll(nullToEmpty(domainPendingActionNotificationResponses))
.addAll(nullToEmpty(domainTransferResponses))
.addAll(nullToEmpty(hostPendingActionNotificationResponses))
.build();
}
@ -240,6 +243,8 @@ public abstract class PollMessage extends ImmutableObject
iterable.filter(DomainPendingActionNotificationResponse.class).toList());
getInstance().domainTransferResponses = forceEmptyToNull(
iterable.filter(DomainTransferResponse.class).toList());
getInstance().hostPendingActionNotificationResponses = forceEmptyToNull(
iterable.filter(HostPendingActionNotificationResponse.class).toList());
return this;
}