diff --git a/java/google/registry/env/common/tools/WEB-INF/web.xml b/java/google/registry/env/common/tools/WEB-INF/web.xml
index 32a6f2ca8..1bc691d46 100644
--- a/java/google/registry/env/common/tools/WEB-INF/web.xml
+++ b/java/google/registry/env/common/tools/WEB-INF/web.xml
@@ -128,6 +128,11 @@
+
+ tools-servlet
+ /_dr/task/pollMapreduce
+
+
diff --git a/java/google/registry/module/tools/ToolsRequestComponent.java b/java/google/registry/module/tools/ToolsRequestComponent.java
index 189dba605..d345e9d67 100644
--- a/java/google/registry/module/tools/ToolsRequestComponent.java
+++ b/java/google/registry/module/tools/ToolsRequestComponent.java
@@ -42,6 +42,7 @@ import google.registry.tools.server.ListPremiumListsAction;
import google.registry.tools.server.ListRegistrarsAction;
import google.registry.tools.server.ListReservedListsAction;
import google.registry.tools.server.ListTldsAction;
+import google.registry.tools.server.PollMapreduceAction;
import google.registry.tools.server.RefreshDnsForAllDomainsAction;
import google.registry.tools.server.ResaveAllEppResourcesAction;
import google.registry.tools.server.ToolsServerModule;
@@ -77,6 +78,7 @@ interface ToolsRequestComponent {
ListReservedListsAction listReservedListsAction();
ListTldsAction listTldsAction();
LoadTestAction loadTestAction();
+ PollMapreduceAction pollMapReduceAction();
PublishDetailReportAction publishDetailReportAction();
RefreshDnsForAllDomainsAction refreshDnsForAllDomainsAction();
ResaveAllEppResourcesAction resaveAllEppResourcesAction();
diff --git a/java/google/registry/tools/server/BUILD b/java/google/registry/tools/server/BUILD
index 9630bf4a1..451797a8d 100644
--- a/java/google/registry/tools/server/BUILD
+++ b/java/google/registry/tools/server/BUILD
@@ -25,6 +25,7 @@ java_library(
"@com_google_appengine_api_1_0_sdk",
"@com_google_appengine_tools_appengine_gcs_client",
"@com_google_appengine_tools_appengine_mapreduce",
+ "@com_google_appengine_tools_appengine_pipeline",
"@com_google_code_findbugs_jsr305",
"@com_google_dagger",
"@com_google_guava",
diff --git a/java/google/registry/tools/server/PollMapreduceAction.java b/java/google/registry/tools/server/PollMapreduceAction.java
new file mode 100644
index 000000000..9cc3d60fe
--- /dev/null
+++ b/java/google/registry/tools/server/PollMapreduceAction.java
@@ -0,0 +1,58 @@
+// Copyright 2017 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.tools.server;
+
+import static com.google.appengine.tools.pipeline.PipelineServiceFactory.newPipelineService;
+import static google.registry.request.Action.Method.POST;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+
+import com.google.appengine.tools.mapreduce.MapReduceResult;
+import com.google.appengine.tools.pipeline.JobInfo;
+import com.google.appengine.tools.pipeline.NoSuchObjectException;
+import com.google.common.collect.ImmutableMap;
+import google.registry.request.Action;
+import google.registry.request.HttpException.InternalServerErrorException;
+import google.registry.request.JsonResponse;
+import google.registry.request.Parameter;
+import google.registry.request.auth.Auth;
+import javax.inject.Inject;
+
+/** Action to poll the status of a mapreduce job. */
+@Action(path = PollMapreduceAction.PATH, method = POST, auth = Auth.AUTH_INTERNAL_ONLY)
+public class PollMapreduceAction implements Runnable {
+
+ public static final String PATH = "/_dr/task/pollMapreduce";
+
+ @Inject @Parameter("jobId") String jobId;
+ @Inject JsonResponse response;
+ @Inject PollMapreduceAction() {}
+
+ @Override
+ public void run() {
+ JobInfo jobInfo;
+ try {
+ jobInfo = newPipelineService().getJobInfo(jobId);
+ } catch (NoSuchObjectException e) {
+ throw new InternalServerErrorException("Job not found: " + e);
+ }
+ ImmutableMap.Builder json = new ImmutableMap.Builder<>();
+ json.put("state", jobInfo.getJobState().toString());
+ if (jobInfo.getJobState() == JobInfo.State.COMPLETED_SUCCESSFULLY) {
+ json.put("output", ((MapReduceResult>) jobInfo.getOutput()).getOutputResult().toString());
+ }
+ response.setPayload(json.build());
+ response.setStatus(SC_OK);
+ }
+}
diff --git a/java/google/registry/tools/server/ToolsServerModule.java b/java/google/registry/tools/server/ToolsServerModule.java
index 662b65959..54a681324 100644
--- a/java/google/registry/tools/server/ToolsServerModule.java
+++ b/java/google/registry/tools/server/ToolsServerModule.java
@@ -95,4 +95,10 @@ public class ToolsServerModule {
static String provideRawKeys(HttpServletRequest req) {
return extractRequiredParameter(req, "rawKeys");
}
+
+ @Provides
+ @Parameter("jobId")
+ String provideJobId(HttpServletRequest req) {
+ return extractRequiredParameter(req, "jobId");
+ }
}
diff --git a/javatests/google/registry/module/tools/testdata/tools_routing.txt b/javatests/google/registry/module/tools/testdata/tools_routing.txt
index af3f5918d..f19dd9b94 100644
--- a/javatests/google/registry/module/tools/testdata/tools_routing.txt
+++ b/javatests/google/registry/module/tools/testdata/tools_routing.txt
@@ -16,6 +16,7 @@ PATH CLASS METHODS OK AUTH
/_dr/task/generateZoneFiles GenerateZoneFilesAction POST n INTERNAL,API APP ADMIN
/_dr/task/killAllCommitLogs KillAllCommitLogsAction POST n INTERNAL APP IGNORED
/_dr/task/killAllEppResources KillAllEppResourcesAction POST n INTERNAL APP IGNORED
+/_dr/task/pollMapreduce PollMapreduceAction POST n INTERNAL APP IGNORED
/_dr/task/refreshDnsForAllDomains RefreshDnsForAllDomainsAction GET n INTERNAL,API APP ADMIN
/_dr/task/resaveAllEppResources ResaveAllEppResourcesAction GET n INTERNAL,API APP ADMIN
/_dr/task/restoreCommitLogs RestoreCommitLogsAction POST y INTERNAL,API APP ADMIN
diff --git a/javatests/google/registry/tools/server/BUILD b/javatests/google/registry/tools/server/BUILD
index 298cb3c9d..bde2a9690 100644
--- a/javatests/google/registry/tools/server/BUILD
+++ b/javatests/google/registry/tools/server/BUILD
@@ -26,6 +26,7 @@ java_library(
"@com_google_appengine_api_1_0_sdk//:testonly",
"@com_google_appengine_tools_appengine_gcs_client",
"@com_google_appengine_tools_appengine_mapreduce",
+ "@com_google_appengine_tools_appengine_pipeline",
"@com_google_guava",
"@com_google_re2j",
"@com_google_truth",
diff --git a/javatests/google/registry/tools/server/PollMapreduceActionTest.java b/javatests/google/registry/tools/server/PollMapreduceActionTest.java
new file mode 100644
index 000000000..3f55c371e
--- /dev/null
+++ b/javatests/google/registry/tools/server/PollMapreduceActionTest.java
@@ -0,0 +1,91 @@
+// Copyright 2017 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.tools.server;
+
+import static com.google.appengine.tools.pipeline.PipelineServiceFactory.newPipelineService;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.appengine.tools.mapreduce.Counters;
+import com.google.appengine.tools.mapreduce.MapReduceResult;
+import com.google.appengine.tools.pipeline.Job0;
+import com.google.appengine.tools.pipeline.JobInfo.State;
+import com.google.appengine.tools.pipeline.Value;
+import google.registry.testing.FakeJsonResponse;
+import google.registry.testing.mapreduce.MapreduceTestCase;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link PollMapreduceAction}.*/
+@RunWith(JUnit4.class)
+public class PollMapreduceActionTest extends MapreduceTestCase {
+
+ @Before
+ public void init() throws Exception {
+ action = new PollMapreduceAction();
+ }
+
+ @Test
+ public void testPollUntilSuccess() throws Exception {
+ action.jobId = newPipelineService().startNewPipeline(new SuccessfulJob());
+ assertThat(poll()).containsExactly("state", State.RUNNING.toString());
+ executeTasksUntilEmpty("default");
+ assertThat(poll()).containsExactly(
+ "state", State.COMPLETED_SUCCESSFULLY.toString(), "output", "foobar");
+ }
+
+ @Test
+ public void testPollUntilFailure() throws Exception {
+ action.jobId = newPipelineService().startNewPipeline(new ThrowingJob());
+ assertThat(poll()).containsExactly("state", State.RUNNING.toString());
+ executeTasksUntilEmpty("default");
+ assertThat(poll()).containsExactly("state", State.STOPPED_BY_ERROR.toString());
+ }
+
+ Map poll() {
+ action.response = new FakeJsonResponse();
+ action.run();
+ return ((FakeJsonResponse) action.response).getResponseMap();
+ }
+
+ /** A job that returns a string. */
+ static class SuccessfulJob extends Job0> {
+ @Override
+ public Value> run() {
+ MapReduceResult result =
+ new MapReduceResult() {
+ @Override
+ public Counters getCounters() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getOutputResult() {
+ return "foobar";
+ }};
+ return immediate(result);
+ }
+ }
+
+ /** A job that throws a RunTimeException when run. */
+ static class ThrowingJob extends Job0 {
+ @Override
+ public Value run() {
+ throw new RuntimeException("expected");
+ }
+ }
+}