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 eb4f4aeb8..17b35cbf3 100644 --- a/java/google/registry/env/common/tools/WEB-INF/web.xml +++ b/java/google/registry/env/common/tools/WEB-INF/web.xml @@ -110,6 +110,12 @@ /_dr/task/refreshAllDomains + + + tools-servlet + /_dr/task/purgeSyntheticBillingEvents + + tools-servlet diff --git a/java/google/registry/module/tools/ToolsRequestComponent.java b/java/google/registry/module/tools/ToolsRequestComponent.java index 735663b64..afda53c48 100644 --- a/java/google/registry/module/tools/ToolsRequestComponent.java +++ b/java/google/registry/module/tools/ToolsRequestComponent.java @@ -44,6 +44,7 @@ import google.registry.tools.server.ResaveAllEppResourcesAction; import google.registry.tools.server.ToolsServerModule; import google.registry.tools.server.UpdatePremiumListAction; import google.registry.tools.server.VerifyOteAction; +import google.registry.tools.server.javascrap.PurgeSyntheticBillingEventsAction; import google.registry.tools.server.javascrap.RefreshAllDomainsAction; /** Dagger component with per-request lifetime for "tools" App Engine module. */ @@ -75,6 +76,7 @@ interface ToolsRequestComponent { ListTldsAction listTldsAction(); LoadTestAction loadTestAction(); PublishDetailReportAction publishDetailReportAction(); + PurgeSyntheticBillingEventsAction purgeSyntheticBillingEventsAction(); RefreshAllDomainsAction refreshAllDomainsAction(); ResaveAllEppResourcesAction resaveAllEppResourcesAction(); UpdatePremiumListAction updatePremiumListAction(); diff --git a/java/google/registry/tools/server/javascrap/PurgeSyntheticBillingEventsAction.java b/java/google/registry/tools/server/javascrap/PurgeSyntheticBillingEventsAction.java new file mode 100644 index 000000000..c751ca53b --- /dev/null +++ b/java/google/registry/tools/server/javascrap/PurgeSyntheticBillingEventsAction.java @@ -0,0 +1,117 @@ +// Copyright 2016 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.javascrap; + +import static google.registry.mapreduce.MapreduceRunner.PARAM_DRY_RUN; +import static google.registry.mapreduce.inputs.EppResourceInputs.createChildEntityInput; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.util.PipelineUtils.createJobPath; + +import com.google.appengine.tools.mapreduce.Mapper; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.googlecode.objectify.VoidWork; +import google.registry.mapreduce.MapreduceRunner; +import google.registry.mapreduce.inputs.NullInput; +import google.registry.model.billing.BillingEvent.Flag; +import google.registry.model.billing.BillingEvent.OneTime; +import google.registry.model.domain.DomainResource; +import google.registry.request.Action; +import google.registry.request.Parameter; +import google.registry.request.Response; +import google.registry.util.Clock; +import google.registry.util.FormattingLogger; +import javax.inject.Inject; +import org.joda.time.DateTime; + +/** + * A mapreduce that purges {@link Flag#SYNTHETIC} {@link OneTime} billing events, in the event + * the recurring billing event mapreduce goes south. + */ +@Action(path = "/_dr/task/purgeSyntheticBillingEvents") +public class PurgeSyntheticBillingEventsAction implements Runnable { + + // TODO(b/27562876): Delete once ExpandRecurringBillingEventsAction is verified in production. + + private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); + + @Inject Clock clock; + @Inject MapreduceRunner mrRunner; + @Inject @Parameter(PARAM_DRY_RUN) boolean isDryRun; + @Inject Response response; + @Inject PurgeSyntheticBillingEventsAction() {} + + @Override + public void run() { + DateTime executeTime = clock.nowUtc(); + logger.infofmt("Running synthetic billing event purge at %s.", executeTime); + response.sendJavaScriptRedirect(createJobPath(mrRunner + .setJobName("Purge synthetic billing events.") + .setModuleName("tools") + .runMapOnly( + new PurgeSyntheticBillingEventsMapper(isDryRun), + ImmutableList.of( + new NullInput(), + createChildEntityInput( + ImmutableSet.>of(DomainResource.class), + ImmutableSet.>of(OneTime.class)))))); + } + + /** Mapper to purge {@link Flag#SYNTHETIC} {@link OneTime} billing events. */ + public static class PurgeSyntheticBillingEventsMapper extends Mapper { + + private static final long serialVersionUID = 8376442755556228455L; + + private final boolean isDryRun; + private final String syntheticCounterName; + private static final String ONETIME_COUNTER = "OneTime billing events encountered"; + + public PurgeSyntheticBillingEventsMapper(boolean isDryRun) { + this.isDryRun = isDryRun; + this.syntheticCounterName = isDryRun + ? "Synthetic OneTime billing events (dry run)" + : "Synthetic OneTime billing events deleted"; + } + + @Override + public final void map(final OneTime oneTime) { + // A null input ensures the mapper gets called at least once, and initialize the counters. + if (oneTime == null) { + getContext().getCounter(syntheticCounterName); + getContext().getCounter(ONETIME_COUNTER); + return; + } + getContext().incrementCounter("OneTime billing events encountered"); + try { + ofy().transactNew(new VoidWork() { + @Override + public void vrun() { + if (oneTime.getFlags().contains(Flag.SYNTHETIC)) { + if (!isDryRun) { + ofy().delete().entity(oneTime).now(); + } + getContext().incrementCounter(syntheticCounterName); + } + } + }); + } catch (Throwable t) { + logger.severefmt( + t, "Error while deleting synthetic OneTime billing event %s", oneTime.getId()); + getContext().incrementCounter("error: " + t.getClass().getSimpleName()); + throw t; + } + } + } +}