diff --git a/java/google/registry/dns/writer/clouddns/CloudDnsWriterModule.java b/java/google/registry/dns/writer/clouddns/CloudDnsWriterModule.java index 099fe94e8..8c54f08d1 100644 --- a/java/google/registry/dns/writer/clouddns/CloudDnsWriterModule.java +++ b/java/google/registry/dns/writer/clouddns/CloudDnsWriterModule.java @@ -61,14 +61,6 @@ public final class CloudDnsWriterModule { return writer; } - // TODO(b/71607306): Remove once large zone resigning is tested - @Provides - @IntoMap - @StringKey(MultiplyingCloudDnsWriter.NAME) - static DnsWriter provideMultiplyingWriter(MultiplyingCloudDnsWriter writer) { - return writer; - } - @Provides @IntoSet @Named("dnsWriterNames") @@ -76,14 +68,6 @@ public final class CloudDnsWriterModule { return CloudDnsWriter.NAME; } - // TODO(b/71607306): Remove once large zone resigning is tested - @Provides - @IntoSet - @Named("dnsWriterNames") - static String provideMultiplyingWriterName() { - return MultiplyingCloudDnsWriter.NAME; - } - @Provides @Named("cloudDns") static RateLimiter provideRateLimiter() { diff --git a/java/google/registry/dns/writer/clouddns/MultiplyingCloudDnsWriter.java b/java/google/registry/dns/writer/clouddns/MultiplyingCloudDnsWriter.java deleted file mode 100644 index 084ebcb3a..000000000 --- a/java/google/registry/dns/writer/clouddns/MultiplyingCloudDnsWriter.java +++ /dev/null @@ -1,464 +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.dns.writer.clouddns; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.collect.ImmutableSet.toImmutableSet; -import static google.registry.model.EppResourceUtils.loadByForeignKey; - -import com.google.api.client.googleapis.json.GoogleJsonError.ErrorInfo; -import com.google.api.client.googleapis.json.GoogleJsonResponseException; -import com.google.api.services.dns.Dns; -import com.google.api.services.dns.model.Change; -import com.google.api.services.dns.model.ResourceRecordSet; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Joiner; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Sets; -import com.google.common.flogger.FluentLogger; -import com.google.common.net.InternetDomainName; -import com.google.common.util.concurrent.RateLimiter; -import google.registry.config.RegistryConfig.Config; -import google.registry.dns.writer.BaseDnsWriter; -import google.registry.dns.writer.DnsWriter; -import google.registry.dns.writer.DnsWriterZone; -import google.registry.model.domain.DomainResource; -import google.registry.model.domain.secdns.DelegationSignerData; -import google.registry.model.host.HostResource; -import google.registry.model.registry.Registries; -import google.registry.util.Clock; -import google.registry.util.Concurrent; -import google.registry.util.Retrier; -import java.io.IOException; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.util.AbstractMap.SimpleImmutableEntry; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import javax.inject.Inject; -import javax.inject.Named; -import org.joda.time.Duration; - -/** - * {@link DnsWriter} implementation that artificially multiplies the number of domains in a dns. - * - * This is a temporary writer, that should be removed once large zone resigning has been tested. - * - * It acts just like {@link CloudDnsWriter}, but in addition to registering the requested domain - * with Cloud DNS, it registers 9 more copies of the domain. - * - * The result is a 10x larger zone on the Cloud DNS side than on the nomulus side. - * - * The reason for this is a combination of the following requirements: - * (a) we want to test how Cloud DNS handles large zones (1M+ domain), and we want to find out ASAP - * so they have time to fix any problems we find, and - * (b) we currently found problems in nomulus preventing us from creating domains at a fast enough - * rate to actually have a 1M domain zone. We managed to get up to 300k. - * - * So this temporary Writer will allow us to have a 1M domain zone in Cloud DNS while only creating - * 100k domains in nomulus. - * - * TODO(b/71607306): Remove once large zone resigning is tested - * - * @see Google Cloud DNS Documentation - */ -public class MultiplyingCloudDnsWriter extends BaseDnsWriter { - - /** - * The name of the pricing engine, as used in {@code Registry.dnsWriter}. Remember to change - * the value on affected Registry objects to prevent runtime failures. - */ - public static final String NAME = "MultiplyingCloudDnsWriter"; - - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - private static final ImmutableSet RETRYABLE_EXCEPTION_REASONS = - ImmutableSet.of("preconditionFailed", "notFound", "alreadyExists"); - - private final Clock clock; - private final RateLimiter rateLimiter; - private final int numThreads; - // TODO(shikhman): This uses @Named("transientFailureRetries") which may not be tuned for this - // application. - private final Retrier retrier; - private final Duration defaultATtl; - private final Duration defaultNsTtl; - private final Duration defaultDsTtl; - private final String projectId; - private final String zoneName; - private final Dns dnsConnection; - private final HashMap> desiredRecords = new HashMap<>(); - - @Inject - MultiplyingCloudDnsWriter( - Dns dnsConnection, - @Config("projectId") String projectId, - @DnsWriterZone String zoneName, - @Config("dnsDefaultATtl") Duration defaultATtl, - @Config("dnsDefaultNsTtl") Duration defaultNsTtl, - @Config("dnsDefaultDsTtl") Duration defaultDsTtl, - @Named("cloudDns") RateLimiter rateLimiter, - @Named("cloudDnsNumThreads") int numThreads, - Clock clock, - Retrier retrier) { - this.dnsConnection = dnsConnection; - this.projectId = projectId; - this.zoneName = zoneName.replace('.', '-'); - this.defaultATtl = defaultATtl; - this.defaultNsTtl = defaultNsTtl; - this.defaultDsTtl = defaultDsTtl; - this.rateLimiter = rateLimiter; - this.clock = clock; - this.retrier = retrier; - this.numThreads = numThreads; - } - - /** Publish the domain and all subordinate hosts. */ - @Override - public void publishDomain(String domainName) { - // Load the target domain. Note that it can be null if this domain was just deleted. - Optional domainResource = - Optional.ofNullable(loadByForeignKey(DomainResource.class, domainName, clock.nowUtc())); - - // Canonicalize name - Set absoluteDomainNames = multiplyAbsoluteName(getAbsoluteHostName(domainName)); - - // Return early if no DNS records should be published. - // desiredRecordsBuilder is populated with an empty set to indicate that all existing records - // should be deleted. - if (!domainResource.isPresent() || !domainResource.get().shouldPublishToDns()) { - absoluteDomainNames.forEach( - absoluteDomainName -> desiredRecords.put(absoluteDomainName, ImmutableSet.of())); - return; - } - - for (String absoluteDomainName : absoluteDomainNames) { - ImmutableSet.Builder domainRecords = new ImmutableSet.Builder<>(); - - // Construct DS records (if any). - Set dsData = domainResource.get().getDsData(); - if (!dsData.isEmpty()) { - HashSet dsRrData = new HashSet<>(); - for (DelegationSignerData ds : dsData) { - dsRrData.add(ds.toRrData()); - } - - if (!dsRrData.isEmpty()) { - domainRecords.add( - new ResourceRecordSet() - .setName(absoluteDomainName) - .setTtl((int) defaultDsTtl.getStandardSeconds()) - .setType("DS") - .setKind("dns#resourceRecordSet") - .setRrdatas(ImmutableList.copyOf(dsRrData))); - } - } - - // Construct NS records (if any). - Set nameserverData = domainResource.get().loadNameserverFullyQualifiedHostNames(); - if (!nameserverData.isEmpty()) { - HashSet nsRrData = new HashSet<>(); - for (String hostName : nameserverData) { - nsRrData.add(getAbsoluteHostName(hostName)); - - // Construct glue records for subordinate NS hostnames (if any) - if (hostName.endsWith(domainName)) { - publishSubordinateHost(hostName); - } - } - - if (!nsRrData.isEmpty()) { - domainRecords.add( - new ResourceRecordSet() - .setName(absoluteDomainName) - .setTtl((int) defaultNsTtl.getStandardSeconds()) - .setType("NS") - .setKind("dns#resourceRecordSet") - .setRrdatas(ImmutableList.copyOf(nsRrData))); - } - } - - desiredRecords.put(absoluteDomainName, domainRecords.build()); - logger.atFine().log( - "Will write %d records for domain %s", domainRecords.build().size(), absoluteDomainName); - } - } - - private void publishSubordinateHost(String hostName) { - logger.atInfo().log("Publishing glue records for %s", hostName); - // Canonicalize name - String absoluteHostName = getAbsoluteHostName(hostName); - - // Load the target host. Note that it can be null if this host was just deleted. - // desiredRecords is populated with an empty set to indicate that all existing records - // should be deleted. - Optional host = - Optional.ofNullable(loadByForeignKey(HostResource.class, hostName, clock.nowUtc())); - - // Return early if the host is deleted. - if (!host.isPresent()) { - desiredRecords.put(absoluteHostName, ImmutableSet.of()); - return; - } - - ImmutableSet.Builder domainRecords = new ImmutableSet.Builder<>(); - - // Construct A and AAAA records (if any). - HashSet aRrData = new HashSet<>(); - HashSet aaaaRrData = new HashSet<>(); - for (InetAddress ip : host.get().getInetAddresses()) { - if (ip instanceof Inet4Address) { - aRrData.add(ip.toString()); - } else { - checkArgument(ip instanceof Inet6Address); - aaaaRrData.add(ip.toString()); - } - } - - if (!aRrData.isEmpty()) { - domainRecords.add( - new ResourceRecordSet() - .setName(absoluteHostName) - .setTtl((int) defaultATtl.getStandardSeconds()) - .setType("A") - .setKind("dns#resourceRecordSet") - .setRrdatas(ImmutableList.copyOf(aRrData))); - } - - if (!aaaaRrData.isEmpty()) { - domainRecords.add( - new ResourceRecordSet() - .setName(absoluteHostName) - .setTtl((int) defaultATtl.getStandardSeconds()) - .setType("AAAA") - .setKind("dns#resourceRecordSet") - .setRrdatas(ImmutableList.copyOf(aaaaRrData))); - } - - desiredRecords.put(absoluteHostName, domainRecords.build()); - } - - /** - * Publish A/AAAA records to Cloud DNS. - * - *

Cloud DNS has no API for glue -- A/AAAA records are automatically matched to their - * corresponding NS records to serve glue. - */ - @Override - public void publishHost(String hostName) { - // Get the superordinate domain name of the host. - InternetDomainName host = InternetDomainName.from(hostName); - Optional tld = Registries.findTldForName(host); - - // Host not managed by our registry, no need to update DNS. - if (!tld.isPresent()) { - logger.atSevere().log("publishHost called for invalid host %s", hostName); - return; - } - - // Extract the superordinate domain name. The TLD and host may have several dots so this - // must calculate a sublist. - ImmutableList hostParts = host.parts(); - ImmutableList tldParts = tld.get().parts(); - ImmutableList domainParts = - hostParts.subList(hostParts.size() - tldParts.size() - 1, hostParts.size()); - String domain = Joiner.on(".").join(domainParts); - - // Refresh the superordinate domain, since we shouldn't be publishing glue records if we are not - // authoritative for the superordinate domain. - publishDomain(domain); - } - - /** - * Sync changes in a zone requested by publishDomain and publishHost to Cloud DNS. - * - *

The zone for the TLD must exist first in Cloud DNS and must be DNSSEC enabled. - * - *

The relevant resource records (including those of all subordinate hosts) will be retrieved - * and the operation will be retried until the state of the retrieved zone data matches the - * representation built via this writer. - */ - @Override - protected void commitUnchecked() { - ImmutableMap> desiredRecordsCopy = - ImmutableMap.copyOf(desiredRecords); - retrier.callWithRetry(() -> mutateZone(desiredRecordsCopy), ZoneStateException.class); - logger.atInfo().log("Wrote to Cloud DNS"); - } - - /** - * Returns the glue records for in-bailiwick nameservers for the given domain+records. - */ - private Stream filterGlueRecords(String domainName, Stream records) { - return records - .filter(record -> record.getType().equals("NS")) - .flatMap(record -> record.getRrdatas().stream()) - .filter(hostName -> hostName.endsWith(domainName) && !hostName.equals(domainName)); - } - - /** - * Mutate the zone with the provided {@code desiredRecords}. - */ - @VisibleForTesting - void mutateZone(ImmutableMap> desiredRecords) { - // Fetch all existing records for names that this writer is trying to modify - ImmutableSet.Builder flattenedExistingRecords = new ImmutableSet.Builder<>(); - - // First, fetch the records for the given domains - Map> domainRecords = - getResourceRecordsForDomains(desiredRecords.keySet()); - - // add the records to the list of exiting records - domainRecords.values().forEach(flattenedExistingRecords::addAll); - - // Get the glue record host names from the given records - ImmutableSet hostsToRead = - domainRecords - .entrySet() - .stream() - .flatMap(entry -> filterGlueRecords(entry.getKey(), entry.getValue().stream())) - .collect(toImmutableSet()); - - // Then fetch and add the records for these hosts - getResourceRecordsForDomains(hostsToRead).values().forEach(flattenedExistingRecords::addAll); - - // Flatten the desired records into one set. - ImmutableSet.Builder flattenedDesiredRecords = new ImmutableSet.Builder<>(); - desiredRecords.values().forEach(flattenedDesiredRecords::addAll); - - // Delete all existing records and add back the desired records - updateResourceRecords(flattenedDesiredRecords.build(), flattenedExistingRecords.build()); - } - - /** - * Fetch the {@link ResourceRecordSet}s for the given domain names under this zone. - * - *

The provided domain should be in absolute form. - */ - private Map> getResourceRecordsForDomains( - Set domainNames) { - logger.atFine().log("Fetching records for %s", domainNames); - // As per Concurrent.transform() - if numThreads or domainNames.size() < 2, it will not use - // threading. - return ImmutableMap.copyOf( - Concurrent.transform( - domainNames, - numThreads, - domainName -> - new SimpleImmutableEntry<>(domainName, getResourceRecordsForDomain(domainName)))); - } - - /** - * Fetch the {@link ResourceRecordSet}s for the given domain name under this zone. - * - *

The provided domain should be in absolute form. - */ - private List getResourceRecordsForDomain(String domainName) { - // TODO(b/70217860): do we want to use a retrier here? - try { - Dns.ResourceRecordSets.List listRecordsRequest = - dnsConnection.resourceRecordSets().list(projectId, zoneName).setName(domainName); - - rateLimiter.acquire(); - return listRecordsRequest.execute().getRrsets(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Update {@link ResourceRecordSet}s under this zone. - * - *

This call should be used in conjunction with {@link #getResourceRecordsForDomains} in a - * get-and-set retry loop. - * - *

See {@link "https://cloud.google.com/dns/troubleshooting"} for a list of errors produced by - * the Google Cloud DNS API. - * - * @throws ZoneStateException if the operation could not be completely successfully because the - * records to delete do not exist, already exist or have been modified with different - * attributes since being queried. - */ - private void updateResourceRecords( - ImmutableSet additions, ImmutableSet deletions) { - // Find records that are both in additions and deletions, so we can remove them from both before - // requesting the change. This is mostly for optimization reasons - not doing so doesn't affect - // the result. - ImmutableSet intersection = - Sets.intersection(additions, deletions).immutableCopy(); - logger.atInfo().log( - "There are %d common items out of the %d items in 'additions' and %d items in 'deletions'", - intersection.size(), additions.size(), deletions.size()); - // Exit early if we have nothing to update - dnsConnection doesn't work on empty changes - if (additions.equals(deletions)) { - logger.atInfo().log("Returning early because additions is the same as deletions"); - return; - } - Change change = - new Change() - .setAdditions(ImmutableList.copyOf(Sets.difference(additions, intersection))) - .setDeletions(ImmutableList.copyOf(Sets.difference(deletions, intersection))); - - rateLimiter.acquire(); - try { - dnsConnection.changes().create(projectId, zoneName, change).execute(); - } catch (GoogleJsonResponseException e) { - List errors = e.getDetails().getErrors(); - // We did something really wrong here, just give up and re-throw - if (errors.size() > 1) { - throw new RuntimeException(e); - } - String errorReason = errors.get(0).getReason(); - - if (RETRYABLE_EXCEPTION_REASONS.contains(errorReason)) { - throw new ZoneStateException(errorReason); - } else { - throw new RuntimeException(e); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Returns the presentation format ending in a dot used for an absolute hostname. - * - * @param hostName the fully qualified hostname - */ - private static String getAbsoluteHostName(String hostName) { - return hostName.endsWith(".") ? hostName : hostName + "."; - } - - private static ImmutableSet multiplyAbsoluteName(String absoluteName) { - return IntStream.range(0, 10) - .mapToObj(i -> i == 0 ? absoluteName : String.format("%d-%s", i, absoluteName)) - .collect(ImmutableSet.toImmutableSet()); - } - - /** Zone state on Cloud DNS does not match the expected state. */ - static class ZoneStateException extends RuntimeException { - public ZoneStateException(String reason) { - super("Zone state on Cloud DNS does not match the expected state: " + reason); - } - } -}