1
0
mirror of https://github.com/google/nomulus synced 2026-05-17 13:21:48 +00:00

Compare commits

...

3 Commits

Author SHA1 Message Date
sarahcaseybot
cf1a148208 Add Java changes for new breakglass_mode column on Tld (#2053)
* Add Java changes for new breakglass_mode column on Tld

* Add generated sql schema
2023-06-22 12:55:42 -04:00
sarahcaseybot
6b54b69163 Add batching to the RefreshDnsForAllDomainsAction (#2037)
* Add an includeDeleted option to RefreshDnsForAllDomainsAction

* Add batching to the query

* Some refactoring

* Make batch size configurable

* Set status to ok

* Combine into one transaction

* Remove smear mintes parameter

* Only pass in lastInPreviousBatch
2023-06-22 12:54:40 -04:00
Weimin Yu
a839ec434e Add CurlCommand option to connect to canary (#2060)
Add a --canary option (default to false) to the CurlCommand that allows
connection to the canary endpoints.

During canary analysis, only the DEFAULT-canary receives traffic. This
new flag allows use to test other canary services manually using the
curl command.
2023-06-22 11:20:41 -04:00
10 changed files with 227 additions and 81 deletions

View File

@@ -475,6 +475,9 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
/** An allowlist of hosts allowed to be used on domains on this TLD (ignored if empty). */
@Nullable Set<String> allowedFullyQualifiedHostNames;
@Column(nullable = false)
boolean breakglassMode = false;
/**
* References to allocation tokens that can be used on the TLD if no other token is passed in on a
* domain create.
@@ -701,6 +704,10 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
return nullToEmptyImmutableCopy(idnTables);
}
public boolean getBreakglassMode() {
return breakglassMode;
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
@@ -1004,6 +1011,11 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
return this;
}
public Builder setBreakglassMode(boolean breakglassMode) {
getInstance().breakglassMode = breakglassMode;
return this;
}
@Override
public Tld build() {
final Tld instance = getInstance();

View File

@@ -75,6 +75,11 @@ class CurlCommand implements CommandWithConnection {
required = true)
private Service service;
@Parameter(
names = {"--canary"},
description = "If set, use the canary end-point; otherwise use the regular end-point.")
private Boolean canary = Boolean.FALSE;
@Override
public void setConnection(ServiceConnection connection) {
this.connection = connection;
@@ -90,7 +95,7 @@ class CurlCommand implements CommandWithConnection {
throw new IllegalArgumentException("You may not specify a body for a get method.");
}
ServiceConnection connectionToService = connection.withService(service);
ServiceConnection connectionToService = connection.withService(service, canary);
String response =
(method == Method.GET)
? connectionToService.sendGetRequest(path, ImmutableMap.<String, String>of())

View File

@@ -15,6 +15,8 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.base.Verify.verify;
import static com.google.common.net.HttpHeaders.X_REQUESTED_WITH;
import static com.google.common.net.MediaType.JSON_UTF_8;
import static google.registry.security.JsonHttp.JSON_SAFETY_PREFIX;
@@ -26,6 +28,7 @@ import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.CharStreams;
@@ -36,6 +39,7 @@ import google.registry.config.RegistryConfig;
import google.registry.request.Action.Service;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import javax.annotation.Nullable;
@@ -55,20 +59,23 @@ public class ServiceConnection {
@Inject HttpRequestFactory requestFactory;
private final Service service;
private final boolean useCanary;
@Inject
ServiceConnection() {
service = Service.TOOLS;
useCanary = false;
}
private ServiceConnection(Service service, HttpRequestFactory requestFactory) {
private ServiceConnection(Service service, HttpRequestFactory requestFactory, boolean useCanary) {
this.service = service;
this.requestFactory = requestFactory;
this.useCanary = useCanary;
}
/** Returns a copy of this connection that talks to a different service. */
public ServiceConnection withService(Service service) {
return new ServiceConnection(service, requestFactory);
/** Returns a copy of this connection that talks to a different service endpoint. */
public ServiceConnection withService(Service service, boolean isCanary) {
return new ServiceConnection(service, requestFactory, isCanary);
}
/** Returns the contents of the title tag in the given HTML, or null if not found. */
@@ -85,7 +92,7 @@ public class ServiceConnection {
private String internalSend(
String endpoint, Map<String, ?> params, MediaType contentType, @Nullable byte[] payload)
throws IOException {
GenericUrl url = new GenericUrl(String.format("%s%s", getServer(service), endpoint));
GenericUrl url = new GenericUrl(String.format("%s%s", getServer(), endpoint));
url.putAll(params);
HttpRequest request =
(payload != null)
@@ -120,6 +127,20 @@ public class ServiceConnection {
}
}
@VisibleForTesting
URL getServer() {
URL url = getServer(service);
if (useCanary) {
verify(!isNullOrEmpty(url.getHost()), "Null host in url");
try {
return new URL(url.getProtocol(), "nomulus-dot-" + url.getHost(), url.getFile());
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
return url;
}
public String sendPostRequest(
String endpoint, Map<String, ?> params, MediaType contentType, byte[] payload)
throws IOException {

View File

@@ -15,20 +15,25 @@
package google.registry.tools.server;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Iterables.getLast;
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
import static google.registry.model.tld.Tlds.assertTldsExist;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.RequestParameters.PARAM_TLDS;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import java.util.Optional;
import java.util.Random;
import javax.inject.Inject;
import org.apache.arrow.util.VisibleForTesting;
import org.apache.http.HttpStatus;
import org.joda.time.Duration;
@@ -43,10 +48,8 @@ import org.joda.time.Duration;
* run internally, or by pretending to be internal by setting the X-AppEngine-QueueName header,
* which only admin users can do.
*
* <p>You must pass in a number of {@code smearMinutes} as a URL parameter so that the DNS queue
* doesn't get overloaded. A rough rule of thumb for Cloud DNS is 1 minute per every 1,000 domains.
* This smears the updates out over the next N minutes. For small TLDs consisting of fewer than
* 1,000 domains, passing in 1 is fine (which will execute all the updates immediately).
* <p>You may pass in a {@code batchSize} for the batched read of domains from the database. This is
* recommended to be somewhere between 200 and 500. The default value is 250.
*/
@Action(
service = Action.Service.TOOLS,
@@ -56,47 +59,78 @@ public class RefreshDnsForAllDomainsAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject Response response;
private static final int DEFAULT_BATCH_SIZE = 250;
private final Response response;
private final ImmutableSet<String> tlds;
// Recommended value for batch size is between 200 and 500
private final int batchSize;
private final Random random;
@Inject
@Parameter(PARAM_TLDS)
ImmutableSet<String> tlds;
@Inject
@Parameter("smearMinutes")
int smearMinutes;
@Inject Clock clock;
@Inject Random random;
@Inject
RefreshDnsForAllDomainsAction() {}
RefreshDnsForAllDomainsAction(
Response response,
@Parameter(PARAM_TLDS) ImmutableSet<String> tlds,
@Parameter("batchSize") Optional<Integer> batchSize,
Random random) {
this.response = response;
this.tlds = tlds;
this.batchSize = batchSize.orElse(DEFAULT_BATCH_SIZE);
this.random = random;
}
@Override
public void run() {
assertTldsExist(tlds);
checkArgument(smearMinutes > 0, "Must specify a positive number of smear minutes");
tm().transact(
() ->
tm().query(
"SELECT domainName FROM Domain "
+ "WHERE tld IN (:tlds) "
+ "AND deletionTime > :now",
String.class)
.setParameter("tlds", tlds)
.setParameter("now", clock.nowUtc())
.getResultStream()
.forEach(
domainName -> {
try {
// Smear the task execution time over the next N minutes.
requestDomainDnsRefresh(
domainName, Duration.standardMinutes(random.nextInt(smearMinutes)));
} catch (Throwable t) {
logger.atSevere().withCause(t).log(
"Error while enqueuing DNS refresh for domain '%s'.", domainName);
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
}
}));
checkArgument(batchSize > 0, "Must specify a positive number for batch size");
int smearMinutes = tm().transact(this::calculateSmearMinutes);
ImmutableList<String> previousBatch = ImmutableList.of("");
do {
String lastInPreviousBatch = getLast(previousBatch);
previousBatch = tm().transact(() -> refreshBatch(lastInPreviousBatch, smearMinutes));
} while (previousBatch.size() == batchSize);
}
/**
* Calculates the number of smear minutes to enqueue refreshes so that the DNS queue does not get
* overloaded.
*/
private int calculateSmearMinutes() {
Long activeDomains =
tm().query(
"SELECT COUNT(*) FROM Domain WHERE tld IN (:tlds) AND deletionTime = :endOfTime",
Long.class)
.setParameter("tlds", tlds)
.setParameter("endOfTime", END_OF_TIME)
.getSingleResult();
return Math.max(activeDomains.intValue() / 1000, 1);
}
private ImmutableList<String> getBatch(String lastInPreviousBatch) {
return tm().query(
"SELECT domainName FROM Domain WHERE tld IN (:tlds) AND"
+ " deletionTime = :endOfTime AND domainName >"
+ " :lastInPreviousBatch ORDER BY domainName ASC",
String.class)
.setParameter("tlds", tlds)
.setParameter("endOfTime", END_OF_TIME)
.setParameter("lastInPreviousBatch", lastInPreviousBatch)
.setMaxResults(batchSize)
.getResultStream()
.collect(toImmutableList());
}
@VisibleForTesting
ImmutableList<String> refreshBatch(String lastInPreviousBatch, int smearMinutes) {
ImmutableList<String> domainBatch = getBatch(lastInPreviousBatch);
try {
// Smear the task execution time over the next N minutes.
requestDomainDnsRefresh(domainBatch, Duration.standardMinutes(random.nextInt(smearMinutes)));
} catch (Throwable t) {
logger.atSevere().withCause(t).log("Error while enqueuing DNS refresh batch");
response.setStatus(HttpStatus.SC_OK);
}
return domainBatch;
}
}

View File

@@ -16,6 +16,7 @@ package google.registry.tools.server;
import static com.google.common.base.Strings.emptyToNull;
import static google.registry.request.RequestParameters.extractIntParameter;
import static google.registry.request.RequestParameters.extractOptionalIntParameter;
import static google.registry.request.RequestParameters.extractOptionalParameter;
import static google.registry.request.RequestParameters.extractRequiredParameter;
@@ -76,8 +77,8 @@ public class ToolsServerModule {
}
@Provides
@Parameter("smearMinutes")
static int provideSmearMinutes(HttpServletRequest req) {
return extractIntParameter(req, "smearMinutes");
@Parameter("batchSize")
static Optional<Integer> provideBatchSize(HttpServletRequest req) {
return extractOptionalIntParameter(req, "batchSize");
}
}

View File

@@ -1357,5 +1357,16 @@ public final class DatabaseHelper {
.isEqualTo(1);
}
public static void assertDnsRequestsWithRequestTime(DateTime requestTime, int numOfDomains) {
assertThat(
tm().transact(
() ->
tm().createQueryComposer(DnsRefreshRequest.class)
.where("type", EQ, DnsUtils.TargetType.DOMAIN)
.where("requestTime", EQ, requestTime)
.count()))
.isEqualTo(numOfDomains);
}
private DatabaseHelper() {}
}

View File

@@ -22,6 +22,7 @@ import static google.registry.request.Action.Service.TOOLS;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -29,6 +30,7 @@ import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableMap;
import com.google.common.net.MediaType;
import google.registry.request.Action.Service;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -46,7 +48,7 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
@BeforeEach
void beforeEach() {
command.setConnection(connection);
when(connection.withService(any())).thenReturn(connectionForService);
when(connection.withService(any(Service.class), anyBoolean())).thenReturn(connectionForService);
}
@Captor ArgumentCaptor<ImmutableMap<String, String>> urlParamCaptor;
@@ -54,7 +56,7 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
@Test
void testGetInvocation() throws Exception {
runCommand("--path=/foo/bar?a=1&b=2", "--service=TOOLS");
verify(connection).withService(TOOLS);
verify(connection).withService(eq(TOOLS), eq(false));
verifyNoMoreInteractions(connection);
verify(connectionForService)
.sendGetRequest(eq("/foo/bar?a=1&b=2"), eq(ImmutableMap.<String, String>of()));
@@ -63,7 +65,7 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
@Test
void testExplicitGetInvocation() throws Exception {
runCommand("--path=/foo/bar?a=1&b=2", "--request=GET", "--service=BACKEND");
verify(connection).withService(BACKEND);
verify(connection).withService(eq(BACKEND), eq(false));
verifyNoMoreInteractions(connection);
verify(connectionForService)
.sendGetRequest(eq("/foo/bar?a=1&b=2"), eq(ImmutableMap.<String, String>of()));
@@ -72,7 +74,7 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
@Test
void testPostInvocation() throws Exception {
runCommand("--path=/foo/bar?a=1&b=2", "--data=some data", "--service=DEFAULT");
verify(connection).withService(DEFAULT);
verify(connection).withService(eq(DEFAULT), eq(false));
verifyNoMoreInteractions(connection);
verify(connectionForService)
.sendPostRequest(
@@ -89,7 +91,7 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
"--data=some data",
"--service=DEFAULT",
"--content-type=application/json");
verify(connection).withService(DEFAULT);
verify(connection).withService(eq(DEFAULT), eq(false));
verifyNoMoreInteractions(connection);
verify(connectionForService)
.sendPostRequest(
@@ -118,7 +120,7 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
void testMultiDataPost() throws Exception {
runCommand(
"--path=/foo/bar?a=1&b=2", "--data=first=100", "-d", "second=200", "--service=PUBAPI");
verify(connection).withService(PUBAPI);
verify(connection).withService(eq(PUBAPI), eq(false));
verifyNoMoreInteractions(connection);
verify(connectionForService)
.sendPostRequest(
@@ -132,7 +134,7 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
void testDataDoesntSplit() throws Exception {
runCommand(
"--path=/foo/bar?a=1&b=2", "--data=one,two", "--service=PUBAPI");
verify(connection).withService(PUBAPI);
verify(connection).withService(eq(PUBAPI), eq(false));
verifyNoMoreInteractions(connection);
verify(connectionForService)
.sendPostRequest(
@@ -145,7 +147,20 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
@Test
void testExplicitPostInvocation() throws Exception {
runCommand("--path=/foo/bar?a=1&b=2", "--request=POST", "--service=TOOLS");
verify(connection).withService(TOOLS);
verify(connection).withService(eq(TOOLS), eq(false));
verifyNoMoreInteractions(connection);
verify(connectionForService)
.sendPostRequest(
eq("/foo/bar?a=1&b=2"),
eq(ImmutableMap.<String, String>of()),
eq(MediaType.PLAIN_TEXT_UTF_8),
eq("".getBytes(UTF_8)));
}
@Test
void testCanaryInvocation() throws Exception {
runCommand("--path=/foo/bar?a=1&b=2", "--request=POST", "--service=TOOLS", "--canary");
verify(connection).withService(eq(TOOLS), eq(true));
verifyNoMoreInteractions(connection);
verify(connectionForService)
.sendPostRequest(

View File

@@ -0,0 +1,38 @@
// Copyright 2023 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;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.request.Action.Service.DEFAULT;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link google.registry.tools.ServiceConnection}. */
public class ServiceConnectionTest {
@Test
void testServerUrl_notCanary() {
ServiceConnection connection = new ServiceConnection().withService(DEFAULT, false);
String serverUrl = connection.getServer().toString();
assertThat(serverUrl).isEqualTo("https://localhost"); // See default-config.yaml
}
@Test
void testServerUrl_canary() {
ServiceConnection connection = new ServiceConnection().withService(DEFAULT, true);
String serverUrl = connection.getServer().toString();
assertThat(serverUrl).isEqualTo("https://nomulus-dot-localhost");
}
}

View File

@@ -15,18 +15,24 @@
package google.registry.tools.server;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.QueryComposer.Comparator.EQ;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.assertDnsRequestsWithRequestTime;
import static google.registry.testing.DatabaseHelper.assertDomainDnsRequestWithRequestTime;
import static google.registry.testing.DatabaseHelper.assertNoDnsRequestsExcept;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistDeletedDomain;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.dns.DnsUtils;
import google.registry.model.common.DnsRefreshRequest;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import java.util.Optional;
import java.util.Random;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -46,20 +52,16 @@ public class RefreshDnsForAllDomainsActionTest {
@BeforeEach
void beforeEach() {
action = new RefreshDnsForAllDomainsAction();
action.smearMinutes = 1;
action.random = new Random();
action.random.setSeed(123L);
action.clock = clock;
action.response = response;
createTld("bar");
action =
new RefreshDnsForAllDomainsAction(
response, ImmutableSet.of("bar"), Optional.of(10), new Random());
}
@Test
void test_runAction_successfullyEnqueuesDnsRefreshes() throws Exception {
persistActiveDomain("foo.bar");
persistActiveDomain("low.bar");
action.tlds = ImmutableSet.of("bar");
action.run();
assertDomainDnsRequestWithRequestTime("foo.bar", clock.nowUtc());
assertDomainDnsRequestWithRequestTime("low.bar", clock.nowUtc());
@@ -69,18 +71,27 @@ public class RefreshDnsForAllDomainsActionTest {
void test_runAction_smearsOutDnsRefreshes() throws Exception {
persistActiveDomain("foo.bar");
persistActiveDomain("low.bar");
action.tlds = ImmutableSet.of("bar");
action.smearMinutes = 1000;
action.run();
assertDomainDnsRequestWithRequestTime("foo.bar", clock.nowUtc().plusMinutes(450));
assertDomainDnsRequestWithRequestTime("low.bar", clock.nowUtc().plusMinutes(782));
// Set batch size to 1 since each batch will be enqueud at the same time
action =
new RefreshDnsForAllDomainsAction(
response, ImmutableSet.of("bar"), Optional.of(1), new Random());
tm().transact(() -> action.refreshBatch("", 1000));
tm().transact(() -> action.refreshBatch("", 1000));
ImmutableList<DnsRefreshRequest> refreshRequests =
tm().transact(
() ->
tm().createQueryComposer(DnsRefreshRequest.class)
.where("type", EQ, DnsUtils.TargetType.DOMAIN)
.list());
assertThat(refreshRequests.size()).isEqualTo(2);
assertThat(refreshRequests.get(0).getRequestTime())
.isNotEqualTo(refreshRequests.get(1).getRequestTime());
}
@Test
void test_runAction_doesntRefreshDeletedDomain() throws Exception {
persistActiveDomain("foo.bar");
persistDeletedDomain("deleted.bar", clock.nowUtc().minusYears(1));
action.tlds = ImmutableSet.of("bar");
action.run();
assertDomainDnsRequestWithRequestTime("foo.bar", clock.nowUtc());
assertNoDnsRequestsExcept("foo.bar");
@@ -92,7 +103,6 @@ public class RefreshDnsForAllDomainsActionTest {
persistActiveDomain("foo.bar");
persistActiveDomain("low.bar");
persistActiveDomain("ignore.baz");
action.tlds = ImmutableSet.of("bar");
action.run();
assertDomainDnsRequestWithRequestTime("foo.bar", clock.nowUtc());
assertDomainDnsRequestWithRequestTime("low.bar", clock.nowUtc());
@@ -100,13 +110,11 @@ public class RefreshDnsForAllDomainsActionTest {
}
@Test
void test_smearMinutesMustBeSpecified() {
action.tlds = ImmutableSet.of("bar");
action.smearMinutes = 0;
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> action.run());
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Must specify a positive number of smear minutes");
void test_successfullyBatchesNames() {
for (int i = 0; i <= 10; i++) {
persistActiveDomain(String.format("test%s.bar", i));
}
action.run();
assertDnsRequestsWithRequestTime(clock.nowUtc(), 11);
}
}

View File

@@ -704,6 +704,7 @@
anchor_tenant_add_grace_period_length interval not null,
auto_renew_grace_period_length interval not null,
automatic_transfer_length interval not null,
breakglass_mode boolean not null,
claims_period_end timestamptz not null,
create_billing_cost_amount numeric(19, 2),
create_billing_cost_currency text,