<commit_before>// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.maintenance;

import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.LoadBalancer;
import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget;
import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
import com.yahoo.vespa.hosted.controller.application.Endpoint;
import com.yahoo.vespa.hosted.controller.application.EndpointId;
import com.yahoo.vespa.hosted.controller.application.RoutingId;
import com.yahoo.vespa.hosted.controller.application.RoutingPolicy;
import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue.Priority;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Updates routing policies and their associated DNS records based on an deployment's load balancers.
 *
 * @author mortent
 * @author mpolden
 */
public class RoutingPolicies {

    private final Controller controller;
    private final CuratorDb db;

    public RoutingPolicies(Controller controller) {
        this.controller = Objects.requireNonNull(controller, "controller must be non-null");
        this.db = controller.curator();
        try (var lock = db.lockRoutingPolicies()) { // Update serialized format
            for (var policy : db.readRoutingPolicies().entrySet()) {
                db.writeRoutingPolicies(policy.getKey(), policy.getValue());
            }
        }
    }

    /** Read all known routing policies for given instance */
    public Set<RoutingPolicy> get(ApplicationId application) {
        return db.readRoutingPolicies(application);
    }

    /** Read all known routing policies for given deployment */
    public Set<RoutingPolicy> get(DeploymentId deployment) {
        return get(deployment.applicationId(), deployment.zoneId());
    }

    /** Read all known routing policies for given deployment */
    public Set<RoutingPolicy> get(ApplicationId application, ZoneId zone) {
        return db.readRoutingPolicies(application).stream()
                 .filter(policy -> policy.zone().equals(zone))
                 .collect(Collectors.toUnmodifiableSet());
    }

    /**
     * Refresh routing policies for application in given zone. This is idempotent and changes will only be performed if
     * load balancers for given application have changed.
     */
    public void refresh(ApplicationId application, DeploymentSpec deploymentSpec, ZoneId zone) {
        if (!controller.zoneRegistry().zones().directlyRouted().ids().contains(zone)) return;
        var lbs = new AllocatedLoadBalancers(application, zone, controller.serviceRegistry().configServer().getLoadBalancers(application, zone),
                                             deploymentSpec);
        try (var lock = db.lockRoutingPolicies()) {
            removeObsoleteEndpointsFromDns(lbs, lock);
            storePoliciesOf(lbs, lock);
            removeObsoletePolicies(lbs, lock);
            registerEndpointsInDns(lbs, lock);
        }
    }

    /** Create global endpoints for given route, if any */
    private void registerEndpointsInDns(AllocatedLoadBalancers loadBalancers, @SuppressWarnings("unused") Lock lock) {
        Map<RoutingId, List<RoutingPolicy>> routingTable = routingTableFrom(get(loadBalancers.application));

        // Create DNS record for each routing ID
        for (Map.Entry<RoutingId, List<RoutingPolicy>> routeEntry : routingTable.entrySet()) {
            Endpoint endpoint = RoutingPolicy.endpointOf(routeEntry.getKey().application(), routeEntry.getKey().endpointId(),
                                                         controller.system());
            Set<AliasTarget> targets = routeEntry.getValue()
                                                 .stream()
                                                 .filter(policy -> policy.dnsZone().isPresent())
                                                 .map(policy -> new AliasTarget(policy.canonicalName(),
                                                                                policy.dnsZone().get(),
                                                                                policy.zone()))
                                                 .collect(Collectors.toSet());
            controller.nameServiceForwarder().createAlias(RecordName.from(endpoint.dnsName()), targets, Priority.normal);
        }
    }

    /** Store routing policies for given route */
    private void storePoliciesOf(AllocatedLoadBalancers loadBalancers, @SuppressWarnings("unused") Lock lock) {
        var policies = new LinkedHashSet<>(get(loadBalancers.application));
        for (LoadBalancer loadBalancer : loadBalancers.list) {
            var endpointIds = loadBalancers.endpointIdsOf(loadBalancer);
            var policy = createPolicy(loadBalancers.application, loadBalancers.zone, loadBalancer, endpointIds);
            if (!policies.add(policy)) {
                // Update existing policy
                policies.remove(policy);
                policies.add(policy);
            }
        }
        db.writeRoutingPolicies(loadBalancers.application, policies);
    }

    /** Create a policy for given load balancer and register a CNAME for it */
    private RoutingPolicy createPolicy(ApplicationId application, ZoneId zone, LoadBalancer loadBalancer,
                                       Set<EndpointId> endpointIds) {
        var routingPolicy = new RoutingPolicy(application, loadBalancer.cluster(), zone, loadBalancer.hostname(),
                                              loadBalancer.dnsZone(), endpointIds);
        var name = RecordName.from(routingPolicy.endpointIn(controller.system()).dnsName());
        var data = RecordData.fqdn(loadBalancer.hostname().value());
        controller.nameServiceForwarder().createCname(name, data, Priority.normal);
        return routingPolicy;
    }

    /** Remove obsolete policies for given route and their CNAME records */
    private void removeObsoletePolicies(AllocatedLoadBalancers loadBalancers, @SuppressWarnings("unused") Lock lock) {
        var allPolicies = new LinkedHashSet<>(get(loadBalancers.application));
        var removalCandidates = new HashSet<>(allPolicies);
        var activeLoadBalancers = loadBalancers.list.stream()
                                                    .map(LoadBalancer::hostname)
                                                    .collect(Collectors.toSet());
        // Remove active load balancers and irrelevant zones from candidates
        removalCandidates.removeIf(policy -> activeLoadBalancers.contains(policy.canonicalName()) ||
                                             !policy.zone().equals(loadBalancers.zone));
        for (var policy : removalCandidates) {
            var dnsName = policy.endpointIn(controller.system()).dnsName();
            controller.nameServiceForwarder().removeRecords(Record.Type.CNAME, RecordName.from(dnsName), Priority.normal);
            allPolicies.remove(policy);
        }
        db.writeRoutingPolicies(loadBalancers.application, allPolicies);
    }
<commit_msg>Log stale read<commit_after>// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.maintenance;

import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.log.LogLevel;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.LoadBalancer;
import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget;
import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
import com.yahoo.vespa.hosted.controller.application.Endpoint;
import com.yahoo.vespa.hosted.controller.application.EndpointId;
import com.yahoo.vespa.hosted.controller.application.RoutingId;
import com.yahoo.vespa.hosted.controller.application.RoutingPolicy;
import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue.Priority;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
 * Updates routing policies and their associated DNS records based on an deployment's load balancers.
 *
 * @author mortent
 * @author mpolden
 */
public class RoutingPolicies {

    private static final Logger LOGGER = Logger.getLogger(RoutingPolicies.class.getName());

    private final Controller controller;
    private final CuratorDb db;

    public RoutingPolicies(Controller controller) {
        this.controller = Objects.requireNonNull(controller, "controller must be non-null");
        this.db = controller.curator();
        try (var lock = db.lockRoutingPolicies()) { // Update serialized format
            for (var policy : db.readRoutingPolicies().entrySet()) {
                db.writeRoutingPolicies(policy.getKey(), policy.getValue());
            }
        }
    }

    /** Read all known routing policies for given instance */
    public Set<RoutingPolicy> get(ApplicationId application) {
        return db.readRoutingPolicies(application);
    }

    /** Read all known routing policies for given deployment */
    public Set<RoutingPolicy> get(DeploymentId deployment) {
        return get(deployment.applicationId(), deployment.zoneId());
    }

    /** Read all known routing policies for given deployment */
    public Set<RoutingPolicy> get(ApplicationId application, ZoneId zone) {
        return db.readRoutingPolicies(application).stream()
                 .filter(policy -> policy.zone().equals(zone))
                 .collect(Collectors.toUnmodifiableSet());
    }

    /**
     * Refresh routing policies for application in given zone. This is idempotent and changes will only be performed if
     * load balancers for given application have changed.
     */
    public void refresh(ApplicationId application, DeploymentSpec deploymentSpec, ZoneId zone) {
        if (!controller.zoneRegistry().zones().directlyRouted().ids().contains(zone)) return;
        var lbs = new AllocatedLoadBalancers(application, zone, controller.serviceRegistry().configServer().getLoadBalancers(application, zone),
                                             deploymentSpec);
        try (var lock = db.lockRoutingPolicies()) {
            removeObsoleteEndpointsFromDns(lbs, lock);
            var writtenPolicies = storePoliciesOf(lbs, lock);
            removeObsoletePolicies(lbs, writtenPolicies, lock);
            registerEndpointsInDns(lbs, lock);
        }
    }

    /** Create global endpoints for given route, if any */
    private void registerEndpointsInDns(AllocatedLoadBalancers loadBalancers, @SuppressWarnings("unused") Lock lock) {
        Map<RoutingId, List<RoutingPolicy>> routingTable = routingTableFrom(get(loadBalancers.application));

        // Create DNS record for each routing ID
        for (Map.Entry<RoutingId, List<RoutingPolicy>> routeEntry : routingTable.entrySet()) {
            Endpoint endpoint = RoutingPolicy.endpointOf(routeEntry.getKey().application(), routeEntry.getKey().endpointId(),
                                                         controller.system());
            Set<AliasTarget> targets = routeEntry.getValue()
                                                 .stream()
                                                 .filter(policy -> policy.dnsZone().isPresent())
                                                 .map(policy -> new AliasTarget(policy.canonicalName(),
                                                                                policy.dnsZone().get(),
                                                                                policy.zone()))
                                                 .collect(Collectors.toSet());
            controller.nameServiceForwarder().createAlias(RecordName.from(endpoint.dnsName()), targets, Priority.normal);
        }
    }

    /** Store routing policies for given route. Returns the persisted policies. */
    private Set<RoutingPolicy> storePoliciesOf(AllocatedLoadBalancers loadBalancers, @SuppressWarnings("unused") Lock lock) {
        var policies = new LinkedHashSet<>(get(loadBalancers.application));
        for (LoadBalancer loadBalancer : loadBalancers.list) {
            var endpointIds = loadBalancers.endpointIdsOf(loadBalancer);
            var policy = createPolicy(loadBalancers.application, loadBalancers.zone, loadBalancer, endpointIds);
            if (!policies.add(policy)) {
                // Update existing policy
                policies.remove(policy);
                policies.add(policy);
            }
        }
        db.writeRoutingPolicies(loadBalancers.application, policies);
        return policies;
    }

    /** Create a policy for given load balancer and register a CNAME for it */
    private RoutingPolicy createPolicy(ApplicationId application, ZoneId zone, LoadBalancer loadBalancer,
                                       Set<EndpointId> endpointIds) {
        var routingPolicy = new RoutingPolicy(application, loadBalancer.cluster(), zone, loadBalancer.hostname(),
                                              loadBalancer.dnsZone(), endpointIds);
        var name = RecordName.from(routingPolicy.endpointIn(controller.system()).dnsName());
        var data = RecordData.fqdn(loadBalancer.hostname().value());
        controller.nameServiceForwarder().createCname(name, data, Priority.normal);
        return routingPolicy;
    }

    /** Remove obsolete policies for given route and their CNAME records */
    private void removeObsoletePolicies(AllocatedLoadBalancers loadBalancers, Set<RoutingPolicy> writtenPolicies,
                                        @SuppressWarnings("unused") Lock lock) {
        var allPolicies = new LinkedHashSet<>(get(loadBalancers.application));
        if (!writtenPolicies.equals(allPolicies)) {
            LOGGER.log(LogLevel.ERROR, String.format("Stale read! This should not happen. Wrote policies %s, but read %s",
                                                     writtenPolicies, allPolicies));
        }
        var removalCandidates = new HashSet<>(allPolicies);
        var activeLoadBalancers = loadBalancers.list.stream()
                                                    .map(LoadBalancer::hostname)
                                                    .collect(Collectors.toSet());
        // Remove active load balancers and irrelevant zones from candidates
        removalCandidates.removeIf(policy -> activeLoadBalancers.contains(policy.canonicalName()) ||
                                             !policy.zone().equals(loadBalancers.zone));
        for (var policy : removalCandidates) {
            var dnsName = policy.endpointIn(controller.system()).dnsName();
            controller.nameServiceForwarder().removeRecords(Record.Type.CNAME, RecordName.from(dnsName), Priority.normal);
            allPolicies.remove(policy);
        }
        db.writeRoutingPolicies(loadBalancers.application, allPolicies);
    }

{'fraction_non_alphanumeric': 0.09639216165069288, 'fraction_numerical': 0.0007325560100115988, 'mean_word_length': 2.6155374089604946, 'pattern_counts': {'":': 0, '<': 29, '<?xml version=': 0, '>': 37, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 57, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1225072', 'n_tokens_mistral': 4047, 'n_tokens_neox': 4030, 'n_words': 882}