Add email notification of BSA job status (#2368)

This commit is contained in:
Weimin Yu 2024-03-13 15:14:02 -04:00 committed by GitHub
parent cd95be4776
commit 9af006836c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 342 additions and 23 deletions

View file

@ -0,0 +1,41 @@
// Copyright 2024 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.bsa;
import google.registry.config.RegistryConfig.Config;
import google.registry.groups.GmailClient;
import google.registry.util.EmailMessage;
import javax.inject.Inject;
import javax.mail.internet.InternetAddress;
/** Sends BSA-related email notifications. */
class BsaEmailSender {
private final InternetAddress alertRecipientAddress;
private final GmailClient gmailClient;
@Inject
BsaEmailSender(
GmailClient gmailClient,
@Config("newAlertRecipientEmailAddress") InternetAddress alertRecipientAddress) {
this.alertRecipientAddress = alertRecipientAddress;
this.gmailClient = gmailClient;
}
/** Sends an email to the configured alert recipient. */
void sendNotification(String subject, String body) {
this.gmailClient.sendEmail(EmailMessage.create(subject, body, alertRecipientAddress));
}
}

View file

@ -14,6 +14,7 @@
package google.registry.bsa;
import static com.google.common.base.Throwables.getStackTraceAsString;
import static google.registry.bsa.BsaStringUtils.LINE_SPLITTER;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
@ -55,6 +56,7 @@ public class BsaRefreshAction implements Runnable {
private final BsaReportSender bsaReportSender;
private final int transactionBatchSize;
private final Duration domainCreateTxnCommitTimeLag;
private final BsaEmailSender emailSender;
private final BsaLock bsaLock;
private final Clock clock;
private final Response response;
@ -66,6 +68,7 @@ public class BsaRefreshAction implements Runnable {
BsaReportSender bsaReportSender,
@Config("bsaTxnBatchSize") int transactionBatchSize,
@Config("domainCreateTxnCommitTimeLag") Duration domainCreateTxnCommitTimeLag,
BsaEmailSender emailSender,
BsaLock bsaLock,
Clock clock,
Response response) {
@ -74,6 +77,7 @@ public class BsaRefreshAction implements Runnable {
this.bsaReportSender = bsaReportSender;
this.transactionBatchSize = transactionBatchSize;
this.domainCreateTxnCommitTimeLag = domainCreateTxnCommitTimeLag;
this.emailSender = emailSender;
this.bsaLock = bsaLock;
this.clock = clock;
this.response = response;
@ -83,11 +87,15 @@ public class BsaRefreshAction implements Runnable {
public void run() {
try {
if (!bsaLock.executeWithLock(this::runWithinLock)) {
logger.atInfo().log("Job is being executed by another worker.");
String message = "BSA refresh did not run: another BSA related task is running";
logger.atInfo().log("%s.", message);
emailSender.sendNotification(message, /* body= */ "");
} else {
emailSender.sendNotification("BSA refreshed successfully", "");
}
} catch (Throwable throwable) {
// TODO(12/31/2023): consider sending an alert email.
logger.atWarning().withCause(throwable).log("Failed to update block lists.");
logger.atWarning().withCause(throwable).log("Failed to refresh BSA data.");
emailSender.sendNotification("BSA refresh aborted", getStackTraceAsString(throwable));
}
// Always return OK. No need to use a retrier on `runWithinLock`. Its individual steps are
// implicitly retried. If action fails, the next cron will continue at checkpoint.

View file

@ -15,7 +15,8 @@
package google.registry.bsa;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.bsa.persistence.DownloadScheduler.fetchMostRecentDownloadJobIdIfCompleted;
import static com.google.common.base.Throwables.getStackTraceAsString;
import static google.registry.bsa.BsaTransactions.bsaQuery;
import static google.registry.bsa.persistence.Queries.batchReadBsaLabelText;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
@ -28,6 +29,7 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.google.common.flogger.FluentLogger;
import google.registry.bsa.persistence.DownloadScheduler;
import google.registry.config.RegistryConfig.Config;
import google.registry.request.Action;
import google.registry.request.Response;
@ -48,6 +50,7 @@ public class BsaValidateAction implements Runnable {
static final String PATH = "/_dr/task/bsaValidate";
private final GcsClient gcsClient;
private final BsaEmailSender emailSender;
private final int transactionBatchSize;
private final BsaLock bsaLock;
private final Response response;
@ -55,10 +58,12 @@ public class BsaValidateAction implements Runnable {
@Inject
BsaValidateAction(
GcsClient gcsClient,
BsaEmailSender emailSender,
@Config("bsaTxnBatchSize") int transactionBatchSize,
BsaLock bsaLock,
Response response) {
this.gcsClient = gcsClient;
this.emailSender = emailSender;
this.transactionBatchSize = transactionBatchSize;
this.bsaLock = bsaLock;
this.response = response;
@ -68,12 +73,13 @@ public class BsaValidateAction implements Runnable {
public void run() {
try {
if (!bsaLock.executeWithLock(this::runWithinLock)) {
logger.atInfo().log("Cannot execute action. Other BSA related task is executing.");
// TODO(blocked by go/r3pr/2354): send email
String message = "BSA validation did not run: another BSA related task is running";
logger.atInfo().log("%s.", message);
emailSender.sendNotification(message, /* body= */ "");
}
} catch (Throwable throwable) {
logger.atWarning().withCause(throwable).log("Failed to update block lists.");
// TODO(blocked by go/r3pr/2354): send email
logger.atWarning().withCause(throwable).log("Failed to validate block lists.");
emailSender.sendNotification("BSA validation aborted", getStackTraceAsString(throwable));
}
// Always return OK. No need to retry since all queries and GCS accesses are already
// implicitly retried.
@ -82,23 +88,35 @@ public class BsaValidateAction implements Runnable {
/** Executes the validation action while holding the BSA lock. */
Void runWithinLock() {
Optional<String> downloadJobName = fetchMostRecentDownloadJobIdIfCompleted();
Optional<String> downloadJobName =
bsaQuery(DownloadScheduler::fetchMostRecentDownloadJobIdIfCompleted);
if (downloadJobName.isEmpty()) {
logger.atInfo().log("Cannot validate: latest download not found or unfinished.");
logger.atInfo().log("Cannot validate: block list downloads not found.");
emailSender.sendNotification(
"BSA validation does not run: block list downloads not found", "");
return null;
}
logger.atInfo().log("Validating BSA with latest download: %s", downloadJobName.get());
ImmutableList.Builder<String> errors = new ImmutableList.Builder();
errors.addAll(checkBsaLabels(downloadJobName.get()));
ImmutableList.Builder<String> errorsBuilder = new ImmutableList.Builder<>();
errorsBuilder.addAll(checkBsaLabels(downloadJobName.get()));
emailValidationResults(downloadJobName.get(), errors.build());
ImmutableList<String> errors = errorsBuilder.build();
String resultSummary =
errors.isEmpty()
? "BSA validation completed: no errors found"
: "BSA validation completed with errors";
emailValidationResults(resultSummary, downloadJobName.get(), errors);
logger.atInfo().log("Finished validating BSA with latest download: %s", downloadJobName.get());
return null;
}
void emailValidationResults(String job, ImmutableList<String> errors) {
// TODO(blocked by go/r3pr/2354): send email
void emailValidationResults(String subject, String jobName, ImmutableList<String> results) {
String body =
String.format("Most recent download is %s.\n\n", jobName) + Joiner.on('\n').join(results);
emailSender.sendNotification(subject, body);
}
ImmutableList<String> checkBsaLabels(String jobName) {

View file

@ -91,6 +91,7 @@ public class UploadBsaUnavailableDomainsAction implements Runnable {
String gcsBucket;
String apiUrl;
BsaEmailSender emailSender;
google.registry.request.Response response;
@ -99,6 +100,7 @@ public class UploadBsaUnavailableDomainsAction implements Runnable {
Clock clock,
BsaCredential bsaCredential,
GcsUtils gcsUtils,
BsaEmailSender emailSender,
@Config("bsaUnavailableDomainsGcsBucket") String gcsBucket,
@Config("bsaUploadUnavailableDomainsUrl") String apiUrl,
google.registry.request.Response response) {
@ -107,6 +109,7 @@ public class UploadBsaUnavailableDomainsAction implements Runnable {
this.gcsUtils = gcsUtils;
this.gcsBucket = gcsBucket;
this.apiUrl = apiUrl;
this.emailSender = emailSender;
this.response = response;
}
@ -118,26 +121,36 @@ public class UploadBsaUnavailableDomainsAction implements Runnable {
String unavailableDomains = Joiner.on("\n").join(getUnavailableDomains(runTime));
if (unavailableDomains.isEmpty()) {
logger.atWarning().log("No unavailable domains found; terminating.");
emailSender.sendNotification(
"BSA daily upload found no domains to upload", "This is unexpected. Please investigate.");
} else {
uploadToGcs(unavailableDomains, runTime);
uploadToBsa(unavailableDomains, runTime);
boolean isGcsSuccess = uploadToGcs(unavailableDomains, runTime);
boolean isBsaSuccess = uploadToBsa(unavailableDomains, runTime);
if (isBsaSuccess && isGcsSuccess) {
emailSender.sendNotification("BSA daily upload completed successfully", "");
} else {
emailSender.sendNotification(
"BSA daily upload completed with errors", "Please see logs for details.");
}
}
}
/** Uploads the unavailable domains list to GCS in the unavailable domains bucket. */
void uploadToGcs(String unavailableDomains, DateTime runTime) {
boolean uploadToGcs(String unavailableDomains, DateTime runTime) {
logger.atInfo().log("Uploading unavailable names file to GCS in bucket %s", gcsBucket);
BlobId blobId = BlobId.of(gcsBucket, createFilename(runTime));
try (OutputStream gcsOutput = gcsUtils.openOutputStream(blobId);
Writer osWriter = new OutputStreamWriter(gcsOutput, US_ASCII)) {
osWriter.write(unavailableDomains);
return true;
} catch (Exception e) {
logger.atSevere().withCause(e).log(
"Error writing BSA unavailable domains to GCS; skipping to BSA upload ...");
return false;
}
}
void uploadToBsa(String unavailableDomains, DateTime runTime) {
boolean uploadToBsa(String unavailableDomains, DateTime runTime) {
try {
byte[] gzippedContents = gzipUnavailableDomains(unavailableDomains);
String sha512Hash = ByteSource.wrap(gzippedContents).hash(Hashing.sha512()).toString();
@ -174,10 +187,12 @@ public class UploadBsaUnavailableDomainsAction implements Runnable {
uploadResponse.code(),
uploadResponse.body() == null ? "(none)" : uploadResponse.body().string());
}
return true;
} catch (IOException e) {
logger.atSevere().withCause(e).log("Error while attempting to upload to BSA, aborting.");
response.setStatus(HttpStatusCodes.STATUS_CODE_SERVER_ERROR);
response.setPayload("Error while attempting to upload to BSA: " + e.getMessage());
return false;
}
}

View file

@ -19,6 +19,7 @@ import dagger.Component;
import dagger.Lazy;
import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.groups.GmailModule;
import google.registry.keyring.KeyringModule;
import google.registry.keyring.secretmanager.SecretManagerKeyringModule;
import google.registry.module.bsa.BsaRequestComponent.BsaRequestComponentModule;
@ -39,6 +40,7 @@ import javax.inject.Singleton;
BsaRequestComponentModule.class,
ConfigModule.class,
CredentialModule.class,
GmailModule.class,
GsonModule.class,
PersistenceModule.class,
KeyringModule.class,

View file

@ -0,0 +1,109 @@
// Copyright 2024 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.bsa;
import static com.google.common.base.Throwables.getStackTraceAsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import google.registry.bsa.api.BsaReportSender;
import google.registry.bsa.persistence.RefreshScheduler;
import google.registry.groups.GmailClient;
import google.registry.request.Response;
import google.registry.testing.FakeClock;
import google.registry.util.EmailMessage;
import javax.mail.internet.InternetAddress;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
/** Unit tests for {@link BsaRefreshAction}. */
@ExtendWith(MockitoExtension.class)
public class BsaRefreshActionTest {
FakeClock fakeClock = new FakeClock(DateTime.parse("2023-11-09T02:08:57.880Z"));
@Mock RefreshScheduler scheduler;
@Mock GmailClient gmailClient;
@Mock private InternetAddress emailRecipient;
@Mock Response response;
@Mock private BsaLock bsaLock;
@Mock private GcsClient gcsClient;
@Mock private BsaReportSender bsaReportSender;
BsaRefreshAction action;
@BeforeEach
void setup() {
action =
new BsaRefreshAction(
scheduler,
gcsClient,
bsaReportSender,
/* transactionBatchSize= */ 5,
/* domainCreateTxnCommitTimeLag= */ Duration.millis(1),
new BsaEmailSender(gmailClient, emailRecipient),
bsaLock,
fakeClock,
response);
}
@Test
void notificationSent_cannotAcquireLock() {
when(bsaLock.executeWithLock(any())).thenReturn(false);
action.run();
verify(gmailClient, times(1))
.sendEmail(
EmailMessage.create(
"BSA refresh did not run: another BSA related task is running",
"",
emailRecipient));
}
@Test
void notificationSent_abortedByException() {
RuntimeException throwable = new RuntimeException("Error");
when(bsaLock.executeWithLock(any())).thenThrow(throwable);
action.run();
verify(gmailClient, times(1))
.sendEmail(
EmailMessage.create(
"BSA refresh aborted", getStackTraceAsString(throwable), emailRecipient));
}
@Test
void notificationSent_success() {
when(bsaLock.executeWithLock(any()))
.thenAnswer(
args -> {
return true;
});
action.run();
verify(gmailClient, times(1))
.sendEmail(EmailMessage.create("BSA refreshed successfully", "", emailRecipient));
}
}

View file

@ -86,6 +86,8 @@ class BsaRefreshFunctionalTest {
@Mock BsaReportSender bsaReportSender;
@Mock BsaEmailSender emailSender;
private GcsClient gcsClient;
private Response response;
private BsaRefreshAction action;
@ -102,6 +104,7 @@ class BsaRefreshFunctionalTest {
bsaReportSender,
/* transactionBatchSize= */ 5,
/* domainCreateTxnCommitTimeLag= */ Duration.millis(1),
emailSender,
new BsaLock(
new FakeLockHandler(/* lockSucceeds= */ true), Duration.standardSeconds(30)),
fakeClock,
@ -145,6 +148,8 @@ class BsaRefreshFunctionalTest {
verify(bsaReportSender, never()).removeUnblockableDomainsUpdates(anyString());
verify(bsaReportSender, times(1))
.addUnblockableDomainsUpdates("{\n \"reserved\": [\n \"blocked1.app\"\n ]\n}");
verify(emailSender, times(1)).sendNotification("BSA refreshed successfully", "");
}
@Test

View file

@ -14,22 +14,38 @@
package google.registry.bsa;
import static com.google.common.base.Throwables.getStackTraceAsString;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.bsa.persistence.BsaTestingUtils.persistDownloadSchedule;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import google.registry.bsa.persistence.BsaTestingUtils;
import google.registry.gcs.GcsUtils;
import google.registry.groups.GmailClient;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationWithCoverageExtension;
import google.registry.request.Response;
import google.registry.testing.FakeClock;
import google.registry.util.EmailMessage;
import java.util.concurrent.Callable;
import javax.mail.internet.InternetAddress;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ -45,19 +61,31 @@ public class BsaValidateActionTest {
final JpaIntegrationWithCoverageExtension jpa =
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
@Mock BsaLock bsaLock;
@Mock GmailClient gmailClient;
@Mock Response response;
@Mock private BsaLock bsaLock;
@Mock private InternetAddress emailRecipient;
@Captor ArgumentCaptor<EmailMessage> emailCaptor = ArgumentCaptor.forClass(EmailMessage.class);
private GcsClient gcsClient;
private BsaValidateAction action;
@BeforeEach
void setup() {
void setup() throws Exception {
gcsClient =
new GcsClient(new GcsUtils(LocalStorageHelper.getOptions()), "my-bucket", "SHA-256");
action = new BsaValidateAction(gcsClient, /* transactionBatchSize= */ 500, bsaLock, response);
action =
new BsaValidateAction(
gcsClient,
new BsaEmailSender(gmailClient, emailRecipient),
/* transactionBatchSize= */ 500,
bsaLock,
response);
}
static void createBlockList(GcsClient gcsClient, BlockListType blockListType, String content)
@ -152,4 +180,83 @@ public class BsaValidateActionTest {
assertThat(allErrors).contains("Found 1 missing labels in the DB. Examples: [test1]");
assertThat(allErrors).contains("Found 1 unexpected labels in the DB. Examples: [test3]");
}
@Test
void notificationSent_cannotAcquireLock() {
when(bsaLock.executeWithLock(any())).thenReturn(false);
action.run();
verify(gmailClient, times(1))
.sendEmail(
EmailMessage.create(
"BSA validation did not run: another BSA related task is running",
"",
emailRecipient));
}
@Test
void notificationSent_abortedByException() {
RuntimeException throwable = new RuntimeException("Error");
when(bsaLock.executeWithLock(any())).thenThrow(throwable);
action.run();
verify(gmailClient, times(1))
.sendEmail(
EmailMessage.create(
"BSA validation aborted", getStackTraceAsString(throwable), emailRecipient));
}
@Test
void notificationSent_noDownloads() {
when(bsaLock.executeWithLock(any()))
.thenAnswer(
args -> {
args.getArgument(0, Callable.class).call();
return true;
});
action.run();
verify(gmailClient, times(1))
.sendEmail(
EmailMessage.create(
"BSA validation does not run: block list downloads not found", "", emailRecipient));
}
@Test
void notificationSent_withValidationError() {
when(bsaLock.executeWithLock(any()))
.thenAnswer(
args -> {
args.getArgument(0, Callable.class).call();
return true;
});
persistDownloadSchedule(DownloadStage.DONE);
action = spy(action);
doReturn(ImmutableList.of("Error line 1.", "Error line 2"))
.when(action)
.checkBsaLabels(anyString());
action.run();
verify(gmailClient, times(1)).sendEmail(emailCaptor.capture());
EmailMessage message = emailCaptor.getValue();
assertThat(message.subject()).isEqualTo("BSA validation completed with errors");
assertThat(message.body()).startsWith("Most recent download is");
assertThat(message.body())
.isEqualTo(
"Most recent download is 2023-11-09t020857.880z.\n\n" + "Error line 1.\nError line 2");
}
@Test
void notificationSent_noError() {
when(bsaLock.executeWithLock(any()))
.thenAnswer(
args -> {
args.getArgument(0, Callable.class).call();
return true;
});
persistDownloadSchedule(DownloadStage.DONE);
action = spy(action);
doReturn(ImmutableList.of()).when(action).checkBsaLabels(anyString());
action.run();
verify(gmailClient, times(1)).sendEmail(emailCaptor.capture());
EmailMessage message = emailCaptor.getValue();
assertThat(message.subject()).isEqualTo("BSA validation completed: no errors found");
assertThat(message.body()).isEqualTo("Most recent download is 2023-11-09t020857.880z.\n\n");
}
}

View file

@ -22,6 +22,8 @@ import static google.registry.testing.DatabaseHelper.persistReservedList;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
@ -64,6 +66,8 @@ public class UploadBsaUnavailableDomainsActionTest {
@Mock BsaCredential bsaCredential;
@Mock BsaEmailSender emailSender;
private final GcsUtils gcsUtils = new GcsUtils(LocalStorageHelper.getOptions());
private final FakeResponse response = new FakeResponse();
@ -86,7 +90,7 @@ public class UploadBsaUnavailableDomainsActionTest {
.build());
action =
new UploadBsaUnavailableDomainsAction(
clock, bsaCredential, gcsUtils, BUCKET, API_URL, response);
clock, bsaCredential, gcsUtils, emailSender, BUCKET, API_URL, response);
}
@Test
@ -101,6 +105,10 @@ public class UploadBsaUnavailableDomainsActionTest {
assertThat(blockList).isEqualTo("ace.tld\nflagrant.tld\nfoobar.tld\njimmy.tld\ntine.tld");
assertThat(blockList).doesNotContain("not-blocked.tld");
// This test currently fails in the upload-to-bsa step.
verify(emailSender, times(1))
.sendNotification("BSA daily upload completed with errors", "Please see logs for details.");
// TODO(mcilwain): Add test of BSA API upload as well.
}
}

View file

@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableList;
import google.registry.bsa.DownloadStage;
import google.registry.bsa.api.UnblockableDomain;
import google.registry.util.Clock;
import org.joda.time.DateTime;
@ -42,6 +43,11 @@ public final class BsaTestingUtils {
tm().transact(() -> tm().put(BsaUnblockableDomain.of(unblockableDomain)));
}
public static void persistDownloadSchedule(DownloadStage stage) {
BsaDownload bsaDownload = new BsaDownload().setStage(stage);
tm().transact(() -> tm().put(bsaDownload));
}
public static DownloadScheduler createDownloadScheduler(Clock clock) {
return new DownloadScheduler(DEFAULT_DOWNLOAD_INTERVAL, DEFAULT_NOP_INTERVAL, clock);
}