() {
+ @Override
+ public Void call() throws Exception {
+ dnsQueue.addDomainRefreshTask(domain.getFullyQualifiedDomainName());
+ return null;
+ }}, TransientFailureException.class);
+ logger.infofmt(
+ "Enqueued DNS refresh for domain %s referenced by host %s.",
+ domain.getFullyQualifiedDomainName(), referencingHostKey);
+ getContext().incrementCounter("domains refreshed");
+ } else {
+ getContext().incrementCounter("domains not refreshed");
+ }
+
+ // Don't catch errors -- we allow the mapreduce to terminate on any errors that can't be
+ // resolved by retrying the transaction. The reducer only fires if the mapper completes
+ // without errors, meaning that it is acceptable to delete all tasks.
+ }
+ }
+
+ /**
+ * A reducer that always fires exactly once.
+ *
+ * This is really a reducer in name only; what it's really doing is waiting for all of the
+ * mapper tasks to finish, and then delete the pull queue tasks. Note that this only happens if
+ * the mapper completes execution without errors.
+ */
+ public static class RefreshDnsOnHostRenameReducer extends Reducer {
+
+ private static final long serialVersionUID = -2850944843275790412L;
+
+ private final Retrier retrier;
+ private final List tasks;
+
+ RefreshDnsOnHostRenameReducer(List tasks, Retrier retrier) {
+ this.tasks = tasks;
+ this.retrier = retrier;
+ }
+
+ @Override
+ public void reduce(Boolean key, ReducerInput values) {
+ deleteTasksWithRetry(tasks, getQueue(QUEUE_ASYNC_HOST_RENAME), retrier);
+ }
+ }
+
+ /** Deletes a list of tasks from the given queue using a retrier. */
+ private static void deleteTasksWithRetry(
+ final List tasks, final Queue queue, Retrier retrier) {
+ if (tasks.isEmpty()) {
+ return;
+ }
+ retrier.callWithRetry(
+ new Callable() {
+ @Override
+ public Void call() throws Exception {
+ queue.deleteTask(tasks);
+ return null;
+ }}, TransientFailureException.class);
+ }
+
+ /** A class that encapsulates the values of a request to refresh DNS for a renamed host. */
+ @AutoValue
+ abstract static class DnsRefreshRequest implements Serializable {
+
+ private static final long serialVersionUID = 2188894914017230887L;
+
+ abstract Key hostKey();
+ abstract DateTime lastUpdateTime();
+
+ /**
+ * Returns a packaged-up DnsRefreshRequest parsed from a task queue task, or absent if the host
+ * specified is already deleted.
+ */
+ static Optional createFromTask(TaskHandle task, DateTime now)
+ throws Exception {
+ ImmutableMap params = ImmutableMap.copyOf(task.extractParams());
+ Key hostKey =
+ Key.create(checkNotNull(params.get(PARAM_HOST_KEY), "Host to refresh not specified"));
+ HostResource host =
+ checkNotNull(ofy().load().key(hostKey).now(), "Host to refresh doesn't exist");
+ if (isDeleted(host, latestOf(now, host.getUpdateAutoTimestamp().getTimestamp()))) {
+ logger.infofmt("Host %s is already deleted, not refreshing DNS.", hostKey);
+ return Optional.absent();
+ }
+ return Optional.of(
+ new AutoValue_RefreshDnsOnHostRenameAction_DnsRefreshRequest(
+ hostKey, host.getUpdateAutoTimestamp().getTimestamp()));
+ }
+ }
+}
diff --git a/java/google/registry/flows/host/HostUpdateFlow.java b/java/google/registry/flows/host/HostUpdateFlow.java
index 276aff0e9..511bf2134 100644
--- a/java/google/registry/flows/host/HostUpdateFlow.java
+++ b/java/google/registry/flows/host/HostUpdateFlow.java
@@ -230,7 +230,7 @@ public final class HostUpdateFlow extends LoggedInFlow implements TransactionalF
}
// We must also enqueue updates for all domains that use this host as their nameserver so
// that their NS records can be updated to point at the new name.
- asyncFlowEnqueuer.enqueueAsyncDnsRefresh(existingResource);
+ asyncFlowEnqueuer.enqueueLegacyAsyncDnsRefresh(existingResource);
}
}
diff --git a/java/google/registry/module/backend/BackendRequestComponent.java b/java/google/registry/module/backend/BackendRequestComponent.java
index cb59be455..f035725fa 100644
--- a/java/google/registry/module/backend/BackendRequestComponent.java
+++ b/java/google/registry/module/backend/BackendRequestComponent.java
@@ -44,6 +44,7 @@ import google.registry.export.sheet.SyncRegistrarsSheetAction;
import google.registry.flows.async.AsyncFlowsModule;
import google.registry.flows.async.DeleteContactsAndHostsAction;
import google.registry.flows.async.DnsRefreshForHostRenameAction;
+import google.registry.flows.async.RefreshDnsOnHostRenameAction;
import google.registry.mapreduce.MapreduceModule;
import google.registry.monitoring.whitebox.MetricsExportAction;
import google.registry.monitoring.whitebox.VerifyEntityIntegrityAction;
@@ -109,6 +110,7 @@ interface BackendRequestComponent {
RdeUploadAction rdeUploadAction();
RdeReporter rdeReporter();
RefreshDnsAction refreshDnsAction();
+ RefreshDnsOnHostRenameAction refreshDnsOnHostRenameAction();
RestoreCommitLogsAction restoreCommitLogsAction();
SyncGroupMembersAction syncGroupMembersAction();
SyncRegistrarsSheetAction syncRegistrarsSheetAction();
diff --git a/javatests/google/registry/flows/async/RefreshDnsOnHostRenameActionTest.java b/javatests/google/registry/flows/async/RefreshDnsOnHostRenameActionTest.java
new file mode 100644
index 000000000..14e03ec5f
--- /dev/null
+++ b/javatests/google/registry/flows/async/RefreshDnsOnHostRenameActionTest.java
@@ -0,0 +1,172 @@
+// Copyright 2016 The Domain Registry 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.flows.async;
+
+import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
+import static google.registry.flows.async.RefreshDnsOnHostRenameAction.QUEUE_ASYNC_HOST_RENAME;
+import static google.registry.model.ofy.ObjectifyService.ofy;
+import static google.registry.testing.DatastoreHelper.createTld;
+import static google.registry.testing.DatastoreHelper.newDomainApplication;
+import static google.registry.testing.DatastoreHelper.newDomainResource;
+import static google.registry.testing.DatastoreHelper.newHostResource;
+import static google.registry.testing.DatastoreHelper.persistActiveHost;
+import static google.registry.testing.DatastoreHelper.persistDeletedHost;
+import static google.registry.testing.DatastoreHelper.persistResource;
+import static google.registry.testing.TaskQueueHelper.assertDnsTasksEnqueued;
+import static google.registry.testing.TaskQueueHelper.assertNoDnsTasksEnqueued;
+import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued;
+import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
+import static google.registry.util.DateTimeUtils.START_OF_TIME;
+import static org.joda.time.Duration.millis;
+import static org.joda.time.Duration.standardHours;
+import static org.joda.time.Duration.standardSeconds;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableSet;
+import com.googlecode.objectify.Key;
+import google.registry.mapreduce.MapreduceRunner;
+import google.registry.model.host.HostResource;
+import google.registry.testing.ExceptionRule;
+import google.registry.testing.FakeClock;
+import google.registry.testing.FakeResponse;
+import google.registry.testing.FakeSleeper;
+import google.registry.testing.InjectRule;
+import google.registry.testing.TaskQueueHelper.TaskMatcher;
+import google.registry.testing.mapreduce.MapreduceTestCase;
+import google.registry.util.Retrier;
+import google.registry.util.Sleeper;
+import google.registry.util.SystemSleeper;
+import org.joda.time.DateTime;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link RefreshDnsOnHostRenameAction}. */
+@RunWith(JUnit4.class)
+public class RefreshDnsOnHostRenameActionTest
+ extends MapreduceTestCase {
+
+ @Rule
+ public final ExceptionRule thrown = new ExceptionRule();
+
+ @Rule
+ public InjectRule inject = new InjectRule();
+
+ private AsyncFlowEnqueuer enqueuer;
+ private final FakeClock clock = new FakeClock(DateTime.parse("2015-01-15T11:22:33Z"));
+
+ @Before
+ public void setup() throws Exception {
+ createTld("tld");
+
+ enqueuer = new AsyncFlowEnqueuer();
+ enqueuer.asyncDnsRefreshPullQueue = getQueue(QUEUE_ASYNC_HOST_RENAME);
+ enqueuer.retrier = new Retrier(new FakeSleeper(clock), 1);
+
+ action = new RefreshDnsOnHostRenameAction();
+ action.clock = clock;
+ action.mrRunner = new MapreduceRunner(Optional.of(5), Optional.absent());
+ action.pullQueue = getQueue(QUEUE_ASYNC_HOST_RENAME);
+ action.response = new FakeResponse();
+ action.retrier = new Retrier(new FakeSleeper(clock), 1);
+ }
+
+ private void runMapreduce() throws Exception {
+ clock.advanceOneMilli();
+ // Use hard sleeps to ensure that the tasks are enqueued properly and will be leased.
+ Sleeper sleeper = new SystemSleeper();
+ sleeper.sleep(millis(50));
+ action.run();
+ sleeper.sleep(millis(50));
+ executeTasksUntilEmpty("mapreduce", clock);
+ sleeper.sleep(millis(50));
+ clock.advanceBy(standardSeconds(5));
+ ofy().clearSessionCache();
+ }
+
+ @Test
+ public void testSuccess_dnsUpdateEnqueued() throws Exception {
+ HostResource host = persistActiveHost("ns1.example.tld");
+ persistResource(
+ newDomainApplication("notadomain.tld")
+ .asBuilder()
+ .setNameservers(ImmutableSet.of(Key.create(host)))
+ .build());
+ persistResource(newDomainResource("example.tld", host));
+ persistResource(newDomainResource("otherexample.tld", host));
+ persistResource(newDomainResource("untouched.tld", persistActiveHost("ns2.example.tld")));
+
+ enqueuer.enqueueAsyncDnsRefresh(host);
+ runMapreduce();
+ assertDnsTasksEnqueued("example.tld", "otherexample.tld");
+ assertNoTasksEnqueued(QUEUE_ASYNC_HOST_RENAME);
+ }
+
+ @Test
+ public void testSuccess_multipleHostsProcessedInBatch() throws Exception {
+ HostResource host1 = persistActiveHost("ns1.example.tld");
+ HostResource host2 = persistActiveHost("ns2.example.tld");
+ HostResource host3 = persistActiveHost("ns3.example.tld");
+ persistResource(newDomainResource("example1.tld", host1));
+ persistResource(newDomainResource("example2.tld", host2));
+ persistResource(newDomainResource("example3.tld", host3));
+
+ enqueuer.enqueueAsyncDnsRefresh(host1);
+ enqueuer.enqueueAsyncDnsRefresh(host2);
+ enqueuer.enqueueAsyncDnsRefresh(host3);
+ runMapreduce();
+ assertDnsTasksEnqueued("example1.tld", "example2.tld", "example3.tld");
+ assertNoTasksEnqueued(QUEUE_ASYNC_HOST_RENAME);
+ }
+
+ @Test
+ public void testSuccess_deletedHost_doesntTriggerDnsRefresh() throws Exception {
+ HostResource host = persistDeletedHost("ns11.fakesss.tld", clock.nowUtc().minusDays(4));
+ persistResource(newDomainResource("example1.tld", host));
+ enqueuer.enqueueAsyncDnsRefresh(host);
+ runMapreduce();
+ assertNoDnsTasksEnqueued();
+ assertNoTasksEnqueued(QUEUE_ASYNC_HOST_RENAME);
+ }
+
+ @Test
+ public void testSuccess_noDnsTasksForDeletedDomain() throws Exception {
+ HostResource renamedHost = persistActiveHost("ns1.example.tld");
+ persistResource(
+ newDomainResource("example.tld", renamedHost)
+ .asBuilder()
+ .setDeletionTime(START_OF_TIME)
+ .build());
+ enqueuer.enqueueAsyncDnsRefresh(renamedHost);
+ runMapreduce();
+ assertNoDnsTasksEnqueued();
+ assertNoTasksEnqueued(QUEUE_ASYNC_HOST_RENAME);
+ }
+
+ @Test
+ public void testRun_hostDoesntExist_delaysTask() throws Exception {
+ HostResource host = newHostResource("ns1.example.tld");
+ enqueuer.enqueueAsyncDnsRefresh(host);
+ runMapreduce();
+ assertNoDnsTasksEnqueued();
+ assertTasksEnqueued(
+ QUEUE_ASYNC_HOST_RENAME,
+ new TaskMatcher()
+ .payload("hostKey=" + Key.create(host).getString())
+ .etaDelta(standardHours(23), standardHours(25)));
+ }
+}
diff --git a/javatests/google/registry/testing/DatastoreHelper.java b/javatests/google/registry/testing/DatastoreHelper.java
index 23ab47cce..4e71b3536 100644
--- a/javatests/google/registry/testing/DatastoreHelper.java
+++ b/javatests/google/registry/testing/DatastoreHelper.java
@@ -126,6 +126,13 @@ public class DatastoreHelper {
domainName, generateNewDomainRoid(getTldFromDomainName(domainName)), contact);
}
+ public static DomainResource newDomainResource(String domainName, HostResource host) {
+ return newDomainResource(domainName)
+ .asBuilder()
+ .setNameservers(ImmutableSet.of(Key.create(host)))
+ .build();
+ }
+
public static DomainResource newDomainResource(
String domainName, String repoId, ContactResource contact) {
Key contactKey = Key.create(contact);
diff --git a/javatests/google/registry/testing/FakeClock.java b/javatests/google/registry/testing/FakeClock.java
index 78d5b454e..cc0085a9a 100644
--- a/javatests/google/registry/testing/FakeClock.java
+++ b/javatests/google/registry/testing/FakeClock.java
@@ -19,6 +19,7 @@ import static org.joda.time.DateTimeZone.UTC;
import static org.joda.time.Duration.millis;
import google.registry.util.Clock;
+import java.io.Serializable;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.concurrent.ThreadSafe;
import org.joda.time.DateTime;
@@ -27,7 +28,9 @@ import org.joda.time.ReadableInstant;
/** A mock clock for testing purposes that supports telling, setting, and advancing the time. */
@ThreadSafe
-public final class FakeClock implements Clock {
+public final class FakeClock implements Clock, Serializable {
+
+ private static final long serialVersionUID = 675054721685304599L;
// Clock isn't a thread synchronization primitive, but tests involving
// threads should see a consistent flow.
diff --git a/javatests/google/registry/testing/FakeSleeper.java b/javatests/google/registry/testing/FakeSleeper.java
index 9e7a6addd..690429aa2 100644
--- a/javatests/google/registry/testing/FakeSleeper.java
+++ b/javatests/google/registry/testing/FakeSleeper.java
@@ -18,12 +18,15 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import google.registry.util.Sleeper;
+import java.io.Serializable;
import javax.annotation.concurrent.ThreadSafe;
import org.joda.time.ReadableDuration;
/** Sleeper implementation for unit tests that advances {@link FakeClock} rather than sleep. */
@ThreadSafe
-public final class FakeSleeper implements Sleeper {
+public final class FakeSleeper implements Sleeper, Serializable {
+
+ private static final long serialVersionUID = -8975804222581077291L;
private final FakeClock clock;