Add reporting retry, emailing and better logging

This change:
- Adds retries to the staging action
- Emails domain-registry-eng@ upon completion of either action
- Simplifies logging to be more useful

TODO: fix up Module @Inject naming conventions and yearMonth injection

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=173294822
This commit is contained in:
larryruili 2017-10-24 12:29:40 -07:00 committed by jianglai
parent 7bc2d6badd
commit 2f539d6008
18 changed files with 374 additions and 147 deletions

View file

@ -54,7 +54,7 @@ public final class BigqueryJobFailureException extends RuntimeException {
@Nullable @Nullable
private final GoogleJsonError jsonError; private final GoogleJsonError jsonError;
private BigqueryJobFailureException( public BigqueryJobFailureException(
String message, String message,
@Nullable Throwable cause, @Nullable Throwable cause,
@Nullable JobStatus jobStatus, @Nullable JobStatus jobStatus,

View file

@ -292,7 +292,7 @@ public final class RegistryConfig {
@Provides @Provides
@Config("dnsDefaultATtl") @Config("dnsDefaultATtl")
public static Duration provideDnsDefaultATtl() { public static Duration provideDnsDefaultATtl() {
return Duration.standardSeconds(180); return Duration.standardMinutes(3);
} }
/** /**
@ -303,7 +303,7 @@ public final class RegistryConfig {
@Provides @Provides
@Config("dnsDefaultNsTtl") @Config("dnsDefaultNsTtl")
public static Duration provideDnsDefaultNsTtl() { public static Duration provideDnsDefaultNsTtl() {
return Duration.standardSeconds(180); return Duration.standardMinutes(3);
} }
/** /**
@ -314,7 +314,7 @@ public final class RegistryConfig {
@Provides @Provides
@Config("dnsDefaultDsTtl") @Config("dnsDefaultDsTtl")
public static Duration provideDnsDefaultDsTtl() { public static Duration provideDnsDefaultDsTtl() {
return Duration.standardSeconds(180); return Duration.standardMinutes(3);
} }
/** /**
@ -497,6 +497,30 @@ public final class RegistryConfig {
return config.icannReporting.icannActivityReportingUploadUrl; return config.icannReporting.icannActivityReportingUploadUrl;
} }
/**
* Returns the email address from which we send ICANN reporting email summaries from.
*
* @see google.registry.reporting.ReportingEmailUtils
*/
@Provides
@Config("icannReportingSenderEmailAddress")
public static String provideIcannReportingEmailSenderAddress(
@Config("projectId") String projectId, RegistryConfigSettings config) {
return String.format(
"%s@%s", projectId, config.icannReporting.icannReportingEmailSenderDomain);
}
/**
* Returns the email address from which we send ICANN reporting email summaries to.
*
* @see google.registry.reporting.ReportingEmailUtils
*/
@Provides
@Config("icannReportingRecipientEmailAddress")
public static String provideIcannReportingEmailRecipientAddress(RegistryConfigSettings config) {
return config.icannReporting.icannReportingEmailRecipient;
}
/** /**
* Returns the Google Cloud Storage bucket for staging escrow deposits pending upload. * Returns the Google Cloud Storage bucket for staging escrow deposits pending upload.
* *
@ -552,7 +576,7 @@ public final class RegistryConfig {
@Provides @Provides
@Config("rdeReportLockTimeout") @Config("rdeReportLockTimeout")
public static Duration provideRdeReportLockTimeout() { public static Duration provideRdeReportLockTimeout() {
return Duration.standardSeconds(60); return Duration.standardMinutes(1);
} }
/** /**

View file

@ -115,6 +115,8 @@ public class RegistryConfigSettings {
public static class IcannReporting { public static class IcannReporting {
public String icannTransactionsReportingUploadUrl; public String icannTransactionsReportingUploadUrl;
public String icannActivityReportingUploadUrl; public String icannActivityReportingUploadUrl;
public String icannReportingEmailSenderDomain;
public String icannReportingEmailRecipient;
} }
/** Configuration for Registry Data Escrow (RDE). */ /** Configuration for Registry Data Escrow (RDE). */

View file

@ -160,6 +160,12 @@ icannReporting:
# URL we PUT monthly ICANN activity reports to. # URL we PUT monthly ICANN activity reports to.
icannActivityReportingUploadUrl: https://ry-api.icann.org/report/registry-functions-activity icannActivityReportingUploadUrl: https://ry-api.icann.org/report/registry-functions-activity
# Domain for the email address we send reporting pipeline summary emails from.
icannReportingEmailSenderDomain: appspotmail.com
# Address we send reporting pipeline summary emails to.
icannReportingEmailRecipient: email@example.com
rde: rde:
# URL prefix of ICANN's server to upload RDE reports to. Nomulus adds /TLD/ID # URL prefix of ICANN's server to upload RDE reports to. Nomulus adds /TLD/ID
# to the end of this to construct the full URL. # to the end of this to construct the full URL.

View file

@ -21,6 +21,7 @@ java_library(
"//java/google/registry/xml", "//java/google/registry/xml",
"@com_google_api_client", "@com_google_api_client",
"@com_google_apis_google_api_services_bigquery", "@com_google_apis_google_api_services_bigquery",
"@com_google_appengine_api_1_0_sdk",
"@com_google_appengine_tools_appengine_gcs_client", "@com_google_appengine_tools_appengine_gcs_client",
"@com_google_code_findbugs_jsr305", "@com_google_code_findbugs_jsr305",
"@com_google_dagger", "@com_google_dagger",

View file

@ -31,7 +31,6 @@ import com.google.common.io.ByteStreams;
import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryConfig.Config;
import google.registry.keyring.api.KeyModule.Key; import google.registry.keyring.api.KeyModule.Key;
import google.registry.reporting.IcannReportingModule.ReportType; import google.registry.reporting.IcannReportingModule.ReportType;
import google.registry.request.HttpException.InternalServerErrorException;
import google.registry.util.FormattingLogger; import google.registry.util.FormattingLogger;
import google.registry.xjc.XjcXmlTransformer; import google.registry.xjc.XjcXmlTransformer;
import google.registry.xjc.iirdea.XjcIirdeaResponseElement; import google.registry.xjc.iirdea.XjcIirdeaResponseElement;
@ -66,8 +65,8 @@ public class IcannHttpReporter {
@Inject @Config("icannActivityReportingUploadUrl") String icannActivityUrl; @Inject @Config("icannActivityReportingUploadUrl") String icannActivityUrl;
@Inject IcannHttpReporter() {} @Inject IcannHttpReporter() {}
/** Uploads {@code reportBytes} to ICANN. */ /** Uploads {@code reportBytes} to ICANN, returning whether or not it succeeded. */
public void send(byte[] reportBytes, String reportFilename) throws XmlException, IOException { public boolean send(byte[] reportBytes, String reportFilename) throws XmlException, IOException {
validateReportFilename(reportFilename); validateReportFilename(reportFilename);
GenericUrl uploadUrl = new GenericUrl(makeUrl(reportFilename)); GenericUrl uploadUrl = new GenericUrl(makeUrl(reportFilename));
HttpRequest request = HttpRequest request =
@ -85,6 +84,7 @@ public class IcannHttpReporter {
logger.infofmt( logger.infofmt(
"Sending report to %s with content length %s", "Sending report to %s with content length %s",
uploadUrl.toString(), request.getContent().getLength()); uploadUrl.toString(), request.getContent().getLength());
boolean success = true;
try { try {
response = request.execute(); response = request.execute();
byte[] content; byte[] content;
@ -93,25 +93,28 @@ public class IcannHttpReporter {
} finally { } finally {
response.getContent().close(); response.getContent().close();
} }
logger.infofmt("Received response code %s", response.getStatusCode()); logger.infofmt(
logger.infofmt("Response content: %s", new String(content, UTF_8)); "Received response code %s with content %s",
response.getStatusCode(), new String(content, UTF_8));
XjcIirdeaResult result = parseResult(content); XjcIirdeaResult result = parseResult(content);
if (result.getCode().getValue() != 1000) { if (result.getCode().getValue() != 1000) {
success = false;
logger.warningfmt( logger.warningfmt(
"PUT rejected, status code %s:\n%s\n%s", "PUT rejected, status code %s:\n%s\n%s",
result.getCode(), result.getCode(),
result.getMsg(), result.getMsg(),
result.getDescription()); result.getDescription());
throw new InternalServerErrorException(result.getMsg());
} }
} finally { } finally {
if (response != null) { if (response != null) {
response.disconnect(); response.disconnect();
} else { } else {
success = false;
logger.warningfmt( logger.warningfmt(
"Received null response from ICANN server at %s", uploadUrl.toString()); "Received null response from ICANN server at %s", uploadUrl.toString());
} }
} }
return success;
} }
private XjcIirdeaResult parseResult(byte[] content) throws XmlException, IOException { private XjcIirdeaResult parseResult(byte[] content) throws XmlException, IOException {

View file

@ -28,6 +28,7 @@ import google.registry.bigquery.BigqueryConnection;
import google.registry.request.HttpException.BadRequestException; import google.registry.request.HttpException.BadRequestException;
import google.registry.request.Parameter; import google.registry.request.Parameter;
import google.registry.util.Clock; import google.registry.util.Clock;
import google.registry.util.SendEmailService;
import java.util.Optional; import java.util.Optional;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.joda.time.Duration; import org.joda.time.Duration;
@ -138,5 +139,10 @@ public final class IcannReportingModule {
throw new RuntimeException("Could not initialize BigqueryConnection!", e); throw new RuntimeException("Could not initialize BigqueryConnection!", e);
} }
} }
@Provides
static SendEmailService provideSendEmailService() {
return new SendEmailService();
}
} }

View file

@ -21,6 +21,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.api.services.bigquery.model.TableFieldSchema; import com.google.api.services.bigquery.model.TableFieldSchema;
import com.google.appengine.tools.cloudstorage.GcsFilename; import com.google.appengine.tools.cloudstorage.GcsFilename;
import com.google.common.base.Ascii;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
@ -93,7 +94,6 @@ public class IcannReportingStager {
// Get report headers from the table schema and convert into CSV format // Get report headers from the table schema and convert into CSV format
String headerRow = constructRow(getHeaders(reportTable.columnKeySet())); String headerRow = constructRow(getHeaders(reportTable.columnKeySet()));
logger.infofmt("Headers: %s", headerRow);
return (reportType == ReportType.ACTIVITY) return (reportType == ReportType.ACTIVITY)
? stageActivityReports(headerRow, reportTable.rowMap().values()) ? stageActivityReports(headerRow, reportTable.rowMap().values())
@ -231,7 +231,6 @@ public class IcannReportingStager {
reportCsv.append("\r\n"); reportCsv.append("\r\n");
reportCsv.append(row); reportCsv.append(row);
} }
logger.infofmt("Created report:\n%s", reportCsv.toString());
return reportCsv.toString(); return reportCsv.toString();
} }
@ -240,8 +239,11 @@ public class IcannReportingStager {
throws IOException { throws IOException {
// Upload resulting CSV file to GCS // Upload resulting CSV file to GCS
byte[] reportBytes = reportCsv.getBytes(UTF_8); byte[] reportBytes = reportCsv.getBytes(UTF_8);
String reportFilename = ReportingUtils.createFilename(tld, yearMonth, reportType); String reportFilename =
String reportBucketname = ReportingUtils.createReportingBucketName(reportingBucket, subdir); String.format(
"%s-%s-%s.csv",
tld, Ascii.toLowerCase(reportType.toString()), yearMonth.replace("-", ""));
String reportBucketname = String.format("%s/%s", reportingBucket, subdir);
final GcsFilename gcsFilename = new GcsFilename(reportBucketname, reportFilename); final GcsFilename gcsFilename = new GcsFilename(reportBucketname, reportFilename);
gcsUtils.createFromBytes(gcsFilename, reportBytes); gcsUtils.createFromBytes(gcsFilename, reportBytes);
logger.infofmt( logger.infofmt(
@ -253,7 +255,7 @@ public class IcannReportingStager {
/** Creates and stores a manifest file on GCS, indicating which reports were generated. */ /** Creates and stores a manifest file on GCS, indicating which reports were generated. */
void createAndUploadManifest(ImmutableList<String> filenames) throws IOException { void createAndUploadManifest(ImmutableList<String> filenames) throws IOException {
String reportBucketname = ReportingUtils.createReportingBucketName(reportingBucket, subdir); String reportBucketname = String.format("%s/%s", reportingBucket, subdir);
final GcsFilename gcsFilename = new GcsFilename(reportBucketname, MANIFEST_FILE_NAME); final GcsFilename gcsFilename = new GcsFilename(reportBucketname, MANIFEST_FILE_NAME);
StringBuilder manifestString = new StringBuilder(); StringBuilder manifestString = new StringBuilder();
filenames.forEach((filename) -> manifestString.append(filename).append("\n")); filenames.forEach((filename) -> manifestString.append(filename).append("\n"));

View file

@ -18,16 +18,17 @@ import static google.registry.request.Action.Method.POST;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.base.Throwables; import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.net.MediaType; import com.google.common.net.MediaType;
import google.registry.bigquery.BigqueryJobFailureException;
import google.registry.reporting.IcannReportingModule.ReportType; import google.registry.reporting.IcannReportingModule.ReportType;
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;
import google.registry.request.auth.Auth; import google.registry.request.auth.Auth;
import google.registry.util.FormattingLogger; import google.registry.util.FormattingLogger;
import java.util.Arrays; import google.registry.util.Retrier;
import javax.inject.Inject; import javax.inject.Inject;
/** /**
@ -60,12 +61,15 @@ public final class IcannReportingStagingAction implements Runnable {
ImmutableList<ReportType> reportTypes; ImmutableList<ReportType> reportTypes;
@Inject IcannReportingStager stager; @Inject IcannReportingStager stager;
@Inject Retrier retrier;
@Inject Response response; @Inject Response response;
@Inject ReportingEmailUtils emailUtils;
@Inject IcannReportingStagingAction() {} @Inject IcannReportingStagingAction() {}
@Override @Override
public void run() { public void run() {
try { retrier.callWithRetry(
() -> {
ImmutableList.Builder<String> manifestedFilesBuilder = new ImmutableList.Builder<>(); ImmutableList.Builder<String> manifestedFilesBuilder = new ImmutableList.Builder<>();
for (ReportType reportType : reportTypes) { for (ReportType reportType : reportTypes) {
manifestedFilesBuilder.addAll(stager.stageReports(reportType)); manifestedFilesBuilder.addAll(stager.stageReports(reportType));
@ -74,17 +78,33 @@ public final class IcannReportingStagingAction implements Runnable {
stager.createAndUploadManifest(manifestedFiles); stager.createAndUploadManifest(manifestedFiles);
logger.infofmt("Completed staging %d report files.", manifestedFiles.size()); logger.infofmt("Completed staging %d report files.", manifestedFiles.size());
emailUtils.emailResults(
"ICANN Monthly report staging summary [SUCCESS]",
String.format(
"Completed staging the following %d ICANN reports:\n%s",
manifestedFiles.size(), Joiner.on('\n').join(manifestedFiles)));
response.setStatus(SC_OK); response.setStatus(SC_OK);
response.setContentType(MediaType.PLAIN_TEXT_UTF_8); response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
response.setPayload("Completed staging action."); response.setPayload("Completed staging action.");
} catch (Exception e) { return null;
logger.severe("Reporting staging action failed!"); },
logger.severe(Throwables.getStackTraceAsString(e)); new Retrier.FailureReporter() {
@Override
public void beforeRetry(Throwable thrown, int failures, int maxAttempts) {}
@Override
public void afterFinalFailure(Throwable thrown, int failures) {
emailUtils.emailResults(
"ICANN Monthly report staging summary [FAILURE]",
String.format(
"Staging failed due to %s, check logs for more details.", thrown.toString()));
logger.severefmt("Staging action failed due to %s", thrown.toString());
response.setStatus(SC_INTERNAL_SERVER_ERROR); response.setStatus(SC_INTERNAL_SERVER_ERROR);
response.setContentType(MediaType.PLAIN_TEXT_UTF_8); response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
response.setPayload( response.setPayload(String.format("Staging failed due to %s", thrown.toString()));
String.format("Caught exception:\n%s\n%s", e.getMessage(), }
Arrays.toString(e.getStackTrace()))); },
} BigqueryJobFailureException.class);
} }
} }

View file

@ -23,6 +23,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.appengine.tools.cloudstorage.GcsFilename; import com.google.appengine.tools.cloudstorage.GcsFilename;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteStreams; import com.google.common.io.ByteStreams;
import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryConfig.Config;
import google.registry.gcs.GcsUtils; import google.registry.gcs.GcsUtils;
@ -34,6 +35,7 @@ import google.registry.util.FormattingLogger;
import google.registry.util.Retrier; import google.registry.util.Retrier;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
/** /**
@ -68,29 +70,55 @@ public final class IcannReportingUploadAction implements Runnable {
@Inject IcannHttpReporter icannReporter; @Inject IcannHttpReporter icannReporter;
@Inject Retrier retrier; @Inject Retrier retrier;
@Inject Response response; @Inject Response response;
@Inject ReportingEmailUtils emailUtils;
@Inject @Inject
IcannReportingUploadAction() {} IcannReportingUploadAction() {}
@Override @Override
public void run() { public void run() {
String reportBucketname = ReportingUtils.createReportingBucketName(reportingBucket, subdir); String reportBucketname = String.format("%s/%s", reportingBucket, subdir);
ImmutableList<String> manifestedFiles = getManifestedFiles(reportBucketname); ImmutableList<String> manifestedFiles = getManifestedFiles(reportBucketname);
ImmutableMap.Builder<String, Boolean> reportSummaryBuilder = new ImmutableMap.Builder<>();
// Report on all manifested files // Report on all manifested files
for (String reportFilename : manifestedFiles) { for (String reportFilename : manifestedFiles) {
logger.infofmt("Reading ICANN report %s from bucket %s", reportFilename, reportBucketname); logger.infofmt("Reading ICANN report %s from bucket %s", reportFilename, reportBucketname);
final GcsFilename gcsFilename = new GcsFilename(reportBucketname, reportFilename); final GcsFilename gcsFilename = new GcsFilename(reportBucketname, reportFilename);
verifyFileExists(gcsFilename); verifyFileExists(gcsFilename);
boolean success = false;
try {
success =
retrier.callWithRetry( retrier.callWithRetry(
() -> { () -> {
final byte[] payload = readBytesFromGcs(gcsFilename); final byte[] payload = readBytesFromGcs(gcsFilename);
icannReporter.send(payload, reportFilename); return icannReporter.send(payload, reportFilename);
response.setContentType(PLAIN_TEXT_UTF_8);
response.setPayload(String.format("OK, sending: %s", new String(payload, UTF_8)));
return null;
}, },
IOException.class); IOException.class);
} catch (RuntimeException e) {
logger.warningfmt("Upload to %s failed due to %s", gcsFilename.toString(), e.toString());
} }
reportSummaryBuilder.put(reportFilename, success);
}
emailUploadResults(reportSummaryBuilder.build());
response.setContentType(PLAIN_TEXT_UTF_8);
response.setPayload(
String.format("OK, attempted uploading %d reports", manifestedFiles.size()));
}
private void emailUploadResults(ImmutableMap<String, Boolean> reportSummary) {
emailUtils.emailResults(
String.format(
"ICANN Monthly report upload summary: %d/%d succeeded",
reportSummary.values().stream().filter((b) -> b).count(), reportSummary.size()),
String.format(
"Report Filename - Upload status:\n%s",
reportSummary
.entrySet()
.stream()
.map(
(e) ->
String.format("%s - %s", e.getKey(), e.getValue() ? "SUCCESS" : "FAILURE"))
.collect(Collectors.joining("\n"))));
} }
private ImmutableList<String> getManifestedFiles(String reportBucketname) { private ImmutableList<String> getManifestedFiles(String reportBucketname) {

View file

@ -0,0 +1,48 @@
// 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.reporting;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.FormattingLogger;
import google.registry.util.SendEmailService;
import javax.inject.Inject;
import javax.mail.Message;
import javax.mail.Message.RecipientType;
import javax.mail.internet.InternetAddress;
/** Static utils for emailing reporting results. */
public class ReportingEmailUtils {
@Inject @Config("icannReportingSenderEmailAddress") String sender;
@Inject @Config("icannReportingRecipientEmailAddress") String recipient;
@Inject SendEmailService emailService;
@Inject ReportingEmailUtils() {}
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
void emailResults(String subject, String body) {
try {
Message msg = emailService.createMessage();
logger.infofmt("Emailing %s", recipient);
msg.setFrom(new InternetAddress(sender));
msg.addRecipient(RecipientType.TO, new InternetAddress(recipient));
msg.setSubject(subject);
msg.setText(body);
emailService.sendMessage(msg);
} catch (Exception e) {
logger.warningfmt("E-mail service failed due to %s", e.toString());
}
}
}

View file

@ -1,34 +0,0 @@
// 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.reporting;
import com.google.common.base.Ascii;
import google.registry.reporting.IcannReportingModule.ReportType;
/** Static utils for reporting. */
public final class ReportingUtils {
/** Generates a report filename in accord with ICANN's specifications. */
static String createFilename(String tld, String yearMonth, ReportType reportType) {
// Report files use YYYYMM naming instead of standard YYYY-MM, per ICANN requirements.
return String.format(
"%s-%s-%s.csv", tld, Ascii.toLowerCase(reportType.toString()), yearMonth.replace("-", ""));
}
/** Constructs the bucket name to store/upload reports to. */
static String createReportingBucketName(String reportingBucket, String subdir) {
return String.format("%s/%s", reportingBucket, subdir);
}
}

View file

@ -19,6 +19,7 @@ java_library(
"//java/google/registry/util", "//java/google/registry/util",
"//javatests/google/registry/testing", "//javatests/google/registry/testing",
"@com_google_apis_google_api_services_bigquery", "@com_google_apis_google_api_services_bigquery",
"@com_google_appengine_api_1_0_sdk",
"@com_google_appengine_tools_appengine_gcs_client", "@com_google_appengine_tools_appengine_gcs_client",
"@com_google_code_findbugs_jsr305", "@com_google_code_findbugs_jsr305",
"@com_google_dagger", "@com_google_dagger",

View file

@ -17,7 +17,6 @@ package google.registry.reporting;
import static com.google.common.net.MediaType.CSV_UTF_8; import static com.google.common.net.MediaType.CSV_UTF_8;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8; import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static google.registry.testing.DatastoreHelper.createTld; import static google.registry.testing.DatastoreHelper.createTld;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
@ -29,7 +28,6 @@ import com.google.api.client.testing.http.MockLowLevelHttpResponse;
import com.google.api.client.util.Base64; import com.google.api.client.util.Base64;
import com.google.api.client.util.StringUtils; import com.google.api.client.util.StringUtils;
import com.google.common.io.ByteSource; import com.google.common.io.ByteSource;
import google.registry.request.HttpException.InternalServerErrorException;
import google.registry.testing.AppEngineRule; import google.registry.testing.AppEngineRule;
import google.registry.testing.ExceptionRule; import google.registry.testing.ExceptionRule;
import java.io.IOException; import java.io.IOException;
@ -124,12 +122,7 @@ public class IcannHttpReporterTest {
public void testFail_BadIirdeaResponse() throws Exception { public void testFail_BadIirdeaResponse() throws Exception {
IcannHttpReporter reporter = createReporter(); IcannHttpReporter reporter = createReporter();
reporter.httpTransport = createMockTransport(IIRDEA_BAD_XML); reporter.httpTransport = createMockTransport(IIRDEA_BAD_XML);
try { assertThat(reporter.send(FAKE_PAYLOAD, "test-transactions-201706.csv")).isFalse();
reporter.send(FAKE_PAYLOAD, "test-transactions-201706.csv");
assertWithMessage("Expected InternalServerErrorException to be thrown").fail();
} catch (InternalServerErrorException expected) {
assertThat(expected).hasMessageThat().isEqualTo("The structure of the report is invalid.");
}
} }
@Test @Test

View file

@ -14,14 +14,21 @@
package google.registry.reporting; package google.registry.reporting;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import google.registry.bigquery.BigqueryJobFailureException;
import google.registry.reporting.IcannReportingModule.ReportType; import google.registry.reporting.IcannReportingModule.ReportType;
import google.registry.testing.AppEngineRule; import google.registry.testing.AppEngineRule;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse; import google.registry.testing.FakeResponse;
import google.registry.testing.FakeSleeper;
import google.registry.util.Retrier;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@ -36,6 +43,7 @@ public class IcannReportingStagingActionTest {
FakeResponse response = new FakeResponse(); FakeResponse response = new FakeResponse();
IcannReportingStager stager = mock(IcannReportingStager.class); IcannReportingStager stager = mock(IcannReportingStager.class);
ReportingEmailUtils emailUtils = mock(ReportingEmailUtils.class);
@Rule @Rule
public final AppEngineRule appEngine = AppEngineRule.builder() public final AppEngineRule appEngine = AppEngineRule.builder()
@ -54,6 +62,8 @@ public class IcannReportingStagingActionTest {
action.reportTypes = reportingMode; action.reportTypes = reportingMode;
action.response = response; action.response = response;
action.stager = stager; action.stager = stager;
action.retrier = new Retrier(new FakeSleeper(new FakeClock()), 3);
action.emailUtils = emailUtils;
return action; return action;
} }
@ -63,6 +73,10 @@ public class IcannReportingStagingActionTest {
action.run(); action.run();
verify(stager).stageReports(ReportType.ACTIVITY); verify(stager).stageReports(ReportType.ACTIVITY);
verify(stager).createAndUploadManifest(ImmutableList.of("a", "b")); verify(stager).createAndUploadManifest(ImmutableList.of("a", "b"));
verify(emailUtils)
.emailResults(
"ICANN Monthly report staging summary [SUCCESS]",
"Completed staging the following 2 ICANN reports:\na\nb");
} }
@Test @Test
@ -73,6 +87,48 @@ public class IcannReportingStagingActionTest {
verify(stager).stageReports(ReportType.ACTIVITY); verify(stager).stageReports(ReportType.ACTIVITY);
verify(stager).stageReports(ReportType.TRANSACTIONS); verify(stager).stageReports(ReportType.TRANSACTIONS);
verify(stager).createAndUploadManifest(ImmutableList.of("a", "b", "c", "d")); verify(stager).createAndUploadManifest(ImmutableList.of("a", "b", "c", "d"));
verify(emailUtils)
.emailResults(
"ICANN Monthly report staging summary [SUCCESS]",
"Completed staging the following 4 ICANN reports:\na\nb\nc\nd");
}
@Test
public void testRetryOnBigqueryException() throws Exception {
IcannReportingStagingAction action =
createAction(ImmutableList.of(ReportType.ACTIVITY, ReportType.TRANSACTIONS));
when(stager.stageReports(ReportType.TRANSACTIONS))
.thenThrow(new BigqueryJobFailureException("Expected failure", null, null, null))
.thenReturn(ImmutableList.of("c", "d"));
action.run();
verify(stager, times(2)).stageReports(ReportType.ACTIVITY);
verify(stager, times(2)).stageReports(ReportType.TRANSACTIONS);
verify(stager).createAndUploadManifest(ImmutableList.of("a", "b", "c", "d"));
verify(emailUtils)
.emailResults(
"ICANN Monthly report staging summary [SUCCESS]",
"Completed staging the following 4 ICANN reports:\na\nb\nc\nd");
}
@Test
public void testEmailEng_onMoreThanRetriableFailure() throws Exception {
IcannReportingStagingAction action =
createAction(ImmutableList.of(ReportType.ACTIVITY));
when(stager.stageReports(ReportType.ACTIVITY))
.thenThrow(new BigqueryJobFailureException("Expected failure", null, null, null));
try {
action.run();
assertWithMessage("Expected to encounter a BigqueryJobFailureException").fail();
} catch (BigqueryJobFailureException expected) {
// Expect the exception.
assertThat(expected).hasMessageThat().isEqualTo("Expected failure");
}
verify(stager, times(3)).stageReports(ReportType.ACTIVITY);
verify(emailUtils)
.emailResults(
"ICANN Monthly report staging summary [FAILURE]",
"Staging failed due to BigqueryJobFailureException: Expected failure,"
+ " check logs for more details.");
} }
} }

View file

@ -18,11 +18,11 @@ import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage; import static com.google.common.truth.Truth.assertWithMessage;
import static google.registry.testing.GcsTestingUtils.writeGcsFile; import static google.registry.testing.GcsTestingUtils.writeGcsFile;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.appengine.tools.cloudstorage.GcsFilename; import com.google.appengine.tools.cloudstorage.GcsFilename;
import com.google.appengine.tools.cloudstorage.GcsService; import com.google.appengine.tools.cloudstorage.GcsService;
@ -46,15 +46,14 @@ public class IcannReportingUploadActionTest {
@Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build(); @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build();
private static final byte[] FAKE_PAYLOAD = "test,csv\n13,37".getBytes(UTF_8); private static final byte[] PAYLOAD_SUCCESS = "test,csv\n13,37".getBytes(UTF_8);
private static final byte[] MANIFEST_PAYLOAD = "test-transactions-201706.csv\n".getBytes(UTF_8); private static final byte[] PAYLOAD_FAIL = "ahah,csv\n12,34".getBytes(UTF_8);
private static final byte[] MANIFEST_PAYLOAD =
"test-transactions-201706.csv\na-activity-201706.csv\n".getBytes(UTF_8);
private final IcannHttpReporter mockReporter = mock(IcannHttpReporter.class); private final IcannHttpReporter mockReporter = mock(IcannHttpReporter.class);
private final ReportingEmailUtils emailUtils = mock(ReportingEmailUtils.class);
private final FakeResponse response = new FakeResponse(); private final FakeResponse response = new FakeResponse();
private final GcsService gcsService = GcsServiceFactory.createGcsService(); private final GcsService gcsService = GcsServiceFactory.createGcsService();
private final GcsFilename reportFile =
new GcsFilename("basin/icann/monthly/2017-06", "test-transactions-201706.csv");
private final GcsFilename manifestFile =
new GcsFilename("basin/icann/monthly/2017-06", "MANIFEST.txt");
private IcannReportingUploadAction createAction() { private IcannReportingUploadAction createAction() {
IcannReportingUploadAction action = new IcannReportingUploadAction(); IcannReportingUploadAction action = new IcannReportingUploadAction();
@ -63,40 +62,84 @@ public class IcannReportingUploadActionTest {
action.retrier = new Retrier(new FakeSleeper(new FakeClock()), 3); action.retrier = new Retrier(new FakeSleeper(new FakeClock()), 3);
action.subdir = "icann/monthly/2017-06"; action.subdir = "icann/monthly/2017-06";
action.reportingBucket = "basin"; action.reportingBucket = "basin";
action.emailUtils = emailUtils;
action.response = response; action.response = response;
return action; return action;
} }
@Before @Before
public void before() throws Exception { public void before() throws Exception {
writeGcsFile(gcsService, reportFile, FAKE_PAYLOAD); writeGcsFile(
writeGcsFile(gcsService, manifestFile, MANIFEST_PAYLOAD); gcsService,
new GcsFilename("basin/icann/monthly/2017-06", "test-transactions-201706.csv"),
PAYLOAD_SUCCESS);
writeGcsFile(
gcsService,
new GcsFilename("basin/icann/monthly/2017-06", "a-activity-201706.csv"),
PAYLOAD_FAIL);
writeGcsFile(
gcsService,
new GcsFilename("basin/icann/monthly/2017-06", "MANIFEST.txt"),
MANIFEST_PAYLOAD);
when(mockReporter.send(PAYLOAD_SUCCESS, "test-transactions-201706.csv")).thenReturn(true);
when(mockReporter.send(PAYLOAD_FAIL, "a-activity-201706.csv")).thenReturn(false);
} }
@Test @Test
public void testSuccess() throws Exception { public void testSuccess() throws Exception {
IcannReportingUploadAction action = createAction(); IcannReportingUploadAction action = createAction();
action.run(); action.run();
verify(mockReporter).send(FAKE_PAYLOAD, "test-transactions-201706.csv"); verify(mockReporter).send(PAYLOAD_SUCCESS, "test-transactions-201706.csv");
verify(mockReporter).send(PAYLOAD_FAIL, "a-activity-201706.csv");
verifyNoMoreInteractions(mockReporter); verifyNoMoreInteractions(mockReporter);
assertThat(((FakeResponse) action.response).getPayload()) assertThat(((FakeResponse) action.response).getPayload())
.isEqualTo("OK, sending: test,csv\n13,37"); .isEqualTo("OK, attempted uploading 2 reports");
verify(emailUtils)
.emailResults(
"ICANN Monthly report upload summary: 1/2 succeeded",
"Report Filename - Upload status:\n"
+ "test-transactions-201706.csv - SUCCESS\n"
+ "a-activity-201706.csv - FAILURE");
} }
@Test @Test
public void testSuccess_WithRetry() throws Exception { public void testSuccess_WithRetry() throws Exception {
IcannReportingUploadAction action = createAction(); IcannReportingUploadAction action = createAction();
doThrow(new IOException("Expected exception.")) when(mockReporter.send(PAYLOAD_SUCCESS, "test-transactions-201706.csv"))
.doNothing() .thenThrow(new IOException("Expected exception."))
.when(mockReporter) .thenReturn(true);
.send(FAKE_PAYLOAD, "test-transactions-201706.csv");
action.run(); action.run();
verify(mockReporter, times(2)).send(FAKE_PAYLOAD, "test-transactions-201706.csv"); verify(mockReporter, times(2)).send(PAYLOAD_SUCCESS, "test-transactions-201706.csv");
verify(mockReporter).send(PAYLOAD_FAIL, "a-activity-201706.csv");
verifyNoMoreInteractions(mockReporter); verifyNoMoreInteractions(mockReporter);
assertThat(((FakeResponse) action.response).getPayload()) assertThat(((FakeResponse) action.response).getPayload())
.isEqualTo("OK, sending: test,csv\n13,37"); .isEqualTo("OK, attempted uploading 2 reports");
verify(emailUtils)
.emailResults(
"ICANN Monthly report upload summary: 1/2 succeeded",
"Report Filename - Upload status:\n"
+ "test-transactions-201706.csv - SUCCESS\n"
+ "a-activity-201706.csv - FAILURE");
} }
@Test
public void testFailure_firstUnrecoverable_stillAttemptsUploadingBoth() throws Exception {
IcannReportingUploadAction action = createAction();
when(mockReporter.send(PAYLOAD_SUCCESS, "test-transactions-201706.csv"))
.thenThrow(new IOException("Expected exception"));
action.run();
verify(mockReporter, times(3)).send(PAYLOAD_SUCCESS, "test-transactions-201706.csv");
verify(mockReporter).send(PAYLOAD_FAIL, "a-activity-201706.csv");
verifyNoMoreInteractions(mockReporter);
assertThat(((FakeResponse) action.response).getPayload())
.isEqualTo("OK, attempted uploading 2 reports");
verify(emailUtils)
.emailResults(
"ICANN Monthly report upload summary: 0/2 succeeded",
"Report Filename - Upload status:\n"
+ "test-transactions-201706.csv - FAILURE\n"
+ "a-activity-201706.csv - FAILURE");
}
@Test @Test
public void testFail_FileNotFound() throws Exception { public void testFail_FileNotFound() throws Exception {
IcannReportingUploadAction action = createAction(); IcannReportingUploadAction action = createAction();

View file

@ -0,0 +1,68 @@
// 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.reporting;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import google.registry.util.SendEmailService;
import java.util.Properties;
import javax.mail.Message;
import javax.mail.Message.RecipientType;
import javax.mail.Session;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link ReportingEmailUtils}. */
@RunWith(JUnit4.class)
public class ReportingEmailUtilsTest {
private Message msg;
private final SendEmailService emailService = mock(SendEmailService.class);
@Before
public void setUp() {
msg = new MimeMessage(Session.getDefaultInstance(new Properties(), null));
when(emailService.createMessage()).thenReturn(msg);
}
private ReportingEmailUtils createEmailUtil() {
ReportingEmailUtils emailUtils = new ReportingEmailUtils();
emailUtils.sender = "test-project.appspotmail.com";
emailUtils.recipient = "email@example.com";
emailUtils.emailService = emailService;
return emailUtils;
}
@Test
public void testSuccess_sendsEmail() throws Exception {
ReportingEmailUtils emailUtils = createEmailUtil();
emailUtils.emailResults("Subject", "Body");
assertThat(msg.getFrom()).hasLength(1);
assertThat(msg.getFrom()[0])
.isEqualTo(new InternetAddress("test-project.appspotmail.com"));
assertThat(msg.getRecipients(RecipientType.TO)).hasLength(1);
assertThat(msg.getRecipients(RecipientType.TO)[0])
.isEqualTo(new InternetAddress("email@example.com"));
assertThat(msg.getSubject()).isEqualTo("Subject");
assertThat(msg.getContentType()).isEqualTo("text/plain");
assertThat(msg.getContent().toString()).isEqualTo("Body");
}
}

View file

@ -1,40 +0,0 @@
// 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.reporting;
import static com.google.common.truth.Truth.assertThat;
import google.registry.reporting.IcannReportingModule.ReportType;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link google.registry.reporting.ReportingUtils}. */
@RunWith(JUnit4.class)
public class ReportingUtilsTest {
@Test
public void testCreateFilename_success() {
assertThat(ReportingUtils.createFilename("test", "2017-06", ReportType.ACTIVITY))
.isEqualTo("test-activity-201706.csv");
assertThat(ReportingUtils.createFilename("foo", "2017-06", ReportType.TRANSACTIONS))
.isEqualTo("foo-transactions-201706.csv");
}
@Test
public void testCreateBucketName_success() {
assertThat(ReportingUtils.createReportingBucketName("gs://domain-registry-basin", "my/subdir"))
.isEqualTo("gs://domain-registry-basin/my/subdir");
}
}