All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.yahoo.vespa.model.routing.DocumentProtocol Maven / Gradle / Ivy

// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.model.routing;

import com.yahoo.config.model.ConfigModelRepo;
import com.yahoo.document.select.DocumentSelector;
import com.yahoo.documentapi.messagebus.protocol.DocumentrouteselectorpolicyConfig;
import com.yahoo.documentapi.messagebus.protocol.DocumentProtocolPoliciesConfig;
import com.yahoo.messagebus.routing.ApplicationSpec;
import com.yahoo.messagebus.routing.HopSpec;
import com.yahoo.messagebus.routing.RouteSpec;
import com.yahoo.messagebus.routing.RoutingTableSpec;
import com.yahoo.vespa.config.content.MessagetyperouteselectorpolicyConfig;
import com.yahoo.vespa.model.container.Container;
import com.yahoo.vespa.model.container.ContainerCluster;
import com.yahoo.vespa.model.container.ContainerModel;
import com.yahoo.vespa.model.container.docproc.ContainerDocproc;
import com.yahoo.vespa.model.container.docproc.DocprocChain;
import com.yahoo.vespa.model.content.Content;
import com.yahoo.vespa.model.content.cluster.ContentCluster;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * This class is responsible for generating all hops and routes for the Document protocol running on message bus. All
 * the code within could really be part of {@link Routing}, but it has been partitioned out to allow better readability
 * and also more easily maintainable as the number of protocols increase.
 *
 * @author Simon Thoresen Hult
 */
public final class DocumentProtocol implements Protocol,
                                               DocumentrouteselectorpolicyConfig.Producer,
                                               DocumentProtocolPoliciesConfig.Producer {

    private static final String NAME = "document";
    private final ApplicationSpec application;
    private final RoutingTableSpec routingTable;
    private final ConfigModelRepo repo;

    public static String getIndexedRouteName(String configId) {
        return configId + "-index";
    }

    public static String getDirectRouteName(String configId) {
        return configId + "-direct";
    }

    /**
     * Constructs a new document protocol based on the content of the given plugins.
     *
     * @param plugins the plugins to reflect on
     */
    DocumentProtocol(ConfigModelRepo plugins) {
        application = createApplicationSpec(plugins);
        routingTable = createRoutingTable(plugins);
        this.repo = plugins;
    }

    /**
     * Creates a service index based on the plugins loaded. This means to fill the index with all services known by this
     * protocol by traversing the plugins.
     *
     * @param plugins All initialized plugins of the Vespa model
     * @return the index of all known services
     */
    private static ApplicationSpec createApplicationSpec(ConfigModelRepo plugins) {
        ApplicationSpec ret = new ApplicationSpec();

        for (ContentCluster cluster : Content.getContentClusters(plugins)) {
            for (com.yahoo.vespa.model.content.Distributor node : cluster.getDistributorNodes().getChildren().values()) {
                ret.addService(NAME, node.getConfigId() + "/default");
            }
        }

        for (ContainerCluster containerCluster: ContainerModel.containerClusters(plugins)) {
            ContainerDocproc containerDocproc = containerCluster.getDocproc();
            if (containerDocproc != null) {
                createDocprocChainSpec(ret,
                                       containerDocproc.getChains().allChains().allComponents(),
                                       containerCluster.getContainers());
            }
        }

        return ret;
    }

    private static void createDocprocChainSpec(ApplicationSpec spec,
                                               List docprocChains,
                                               List containerNodes) {
        for (DocprocChain chain: docprocChains) {
            for (Container node: containerNodes)
                spec.addService(NAME, node.getConfigId() + "/chain." + chain.getComponentId().stringValue());
        }
    }

    @Override
    public void getConfig(DocumentrouteselectorpolicyConfig.Builder builder) {
        for (ContentCluster cluster : Content.getContentClusters(repo)) {
            addRoute(cluster.getConfigId(), cluster.getRoutingSelector(), builder);
        }
    }

    @Override
    public void getConfig(DocumentProtocolPoliciesConfig.Builder builder) {
        for (ContentCluster cluster : Content.getContentClusters(repo)) {
            DocumentProtocolPoliciesConfig.Cluster.Builder clusterBuilder = new DocumentProtocolPoliciesConfig.Cluster.Builder();
            addSelector(cluster.getConfigId(), cluster.getRoutingSelector(), clusterBuilder);
            addRoutes(getDirectRouteName(cluster.getConfigId()), getIndexedRouteName(cluster.getConfigId()), clusterBuilder);
            builder.cluster(cluster.getConfigId(), clusterBuilder);
        }
    }

    public static void getConfig(MessagetyperouteselectorpolicyConfig.Builder builder, String configId) {
        builder.defaultroute(getDirectRouteName(configId))
                .route(new MessagetyperouteselectorpolicyConfig.Route.Builder()
                        .messagetype(com.yahoo.documentapi.messagebus.protocol.DocumentProtocol.MESSAGE_PUTDOCUMENT)
                        .name(getIndexedRouteName(configId)))
                .route(new MessagetyperouteselectorpolicyConfig.Route.Builder()
                        .messagetype(com.yahoo.documentapi.messagebus.protocol.DocumentProtocol.MESSAGE_REMOVEDOCUMENT)
                        .name(getIndexedRouteName(configId)))
                .route(new MessagetyperouteselectorpolicyConfig.Route.Builder()
                        .messagetype(com.yahoo.documentapi.messagebus.protocol.DocumentProtocol.MESSAGE_UPDATEDOCUMENT)
                        .name(getIndexedRouteName(configId)));
    }

    private static void addRoutes(String directRoute, String indexedRoute, DocumentProtocolPoliciesConfig.Cluster.Builder builder) {
            builder.defaultRoute(directRoute)
                   .route(new DocumentProtocolPoliciesConfig.Cluster.Route.Builder()
                                  .messageType(com.yahoo.documentapi.messagebus.protocol.DocumentProtocol.MESSAGE_PUTDOCUMENT)
                                  .name(indexedRoute))
                   .route(new DocumentProtocolPoliciesConfig.Cluster.Route.Builder()
                                  .messageType(com.yahoo.documentapi.messagebus.protocol.DocumentProtocol.MESSAGE_REMOVEDOCUMENT)
                                  .name(indexedRoute))
                   .route(new DocumentProtocolPoliciesConfig.Cluster.Route.Builder()
                                  .messageType(com.yahoo.documentapi.messagebus.protocol.DocumentProtocol.MESSAGE_UPDATEDOCUMENT)
                                  .name(indexedRoute));
    }

    private static void addSelector(String clusterConfigId, String selector, DocumentProtocolPoliciesConfig.Cluster.Builder builder) {
        try {
            new DocumentSelector(selector);
        } catch (com.yahoo.document.select.parser.ParseException e) {
            throw new IllegalArgumentException("Failed to parse selector '" + selector +
                                               "' for route '" + clusterConfigId +
                                               "' in policy 'DocumentRouteSelector'.");
        }
        builder.selector(selector);
    }

    private static void addRoute(String clusterConfigId, String selector, DocumentrouteselectorpolicyConfig.Builder builder) {
        try {
            new DocumentSelector(selector);
        } catch (com.yahoo.document.select.parser.ParseException e) {
            throw new IllegalArgumentException("Failed to parse selector '" + selector +
                                               "' for route '" + clusterConfigId +
                                               "' in policy 'DocumentRouteSelector'.");
        }
        DocumentrouteselectorpolicyConfig.Route.Builder routeBuilder = new DocumentrouteselectorpolicyConfig.Route.Builder();
        routeBuilder.name(clusterConfigId);
        routeBuilder.selector(selector);
        builder.route(routeBuilder);
    }

    /**
     * This function extrapolates any routes for the document protocol that it can from the vespa model.
     *
     * @param plugins all initialized plugins of the vespa model
     * @return routing table for the document protocol
     */
    private static RoutingTableSpec createRoutingTable(ConfigModelRepo plugins) {
        // Build simple hops and routes.
        List content = Content.getContentClusters(plugins);
        Collection> containerClusters = ContainerModel.containerClusters(plugins);

        RoutingTableSpec table = new RoutingTableSpec(NAME);
        addContainerClusterDocprocHops(containerClusters, table);
        addContentRouting(content, table);

        // Build the indexing hop if it is possible to derive.
        addIndexingHop(content, table);

        // Build the default routes if possible
        addDefaultRoutes(content, containerClusters, table);

        // Return the complete routing table.
        simplifyRouteNames(table);
        return table;
    }

    private static void addContainerClusterDocprocHops(Collection> containerClusters, RoutingTableSpec table) {
        for (ContainerCluster cluster: containerClusters) {
            ContainerDocproc docproc = cluster.getDocproc();

            if (docproc != null) {
                for (DocprocChain chain : docproc.getChains().allChains().allComponents()) {
                    addChainHop(table, cluster.getConfigId(), chain);
                }
            }
        }
    }

    private static void addChainHop(RoutingTableSpec table, String configId, DocprocChain chain) {
        StringBuilder selector = new StringBuilder();
        selector.append("[LoadBalancer:cluster=").append(configId)
                .append(";session=").append(chain.getSessionName())
                .append("]");
        table.addHop(new HopSpec(chain.getServiceName(), selector.toString()));
    }

    /**
     * Create hops to all configured storage nodes for the Document protocol. The "Distributor" policy resolves its
     * recipients using slobrok lookups, so it requires no configured recipients.
     *
     * @param content the storage model from {@link com.yahoo.vespa.model.VespaModel}
     * @param table   the routing table to add to
     */
    private static void addContentRouting(List content, RoutingTableSpec table) {
        for (ContentCluster cluster : content) {
            RouteSpec spec = new RouteSpec(cluster.getConfigId());
            var indexingDocproc = cluster.getSearch().getIndexingDocproc();
            table.addRoute(spec.addHop("[MessageType:" + cluster.getConfigId() + "]"));
            table.addRoute(new RouteSpec(getIndexedRouteName(cluster.getConfigId()))
                                   .addHop(indexingDocproc.getServiceName())
                                   .addHop("[Content:cluster=" + cluster.getName() + "]"));
            table.addRoute(new RouteSpec(getDirectRouteName(cluster.getConfigId()))
                                   .addHop("[Content:cluster=" + cluster.getName() + "]"));
            table.addRoute(new RouteSpec("storage/cluster." + cluster.getName())
                                   .addHop("route:" + cluster.getConfigId()));
        }
    }

    /**
     * Create the "indexing" hop. This hop contains all non-streaming search clusters as recipients, and the routing
     * policy "SearchCluster" will decide which cluster(s) are to receive every document passed through it based on a
     * document select string derived from services.xml.
     *
     * @param table the routing table to add to
     */
    private static void addIndexingHop(List content, RoutingTableSpec table) {
        if (content.isEmpty()) return;

        HopSpec hop = new HopSpec("indexing", "[DocumentRouteSelector]");
        for (ContentCluster cluster : content) {
            hop.addRecipient(cluster.getConfigId());
        }
        if (hop.hasRecipients()) {
            table.addHop(hop);
        }
    }

    /**
     * Create the {@code default} and {@code default-get} routes for the Document protocol. The {@code default}
     * route will be either a route to storage or a route to search. Since recovery from storage is supported,
     * storage takes precedence over search when deciding on the final target of the default route. If there
     * is an unambiguous docproc cluster in the application, the {@code default} route will pass through it.
     * The {@code default-get} route skips the docproc but is otherwise identical to the {@code default} route.
     *
     * @param content the content model from {@link com.yahoo.vespa.model.VespaModel}
     * @param containerClusters a collection of {@link com.yahoo.vespa.model.container.ContainerCluster}s
     * @param table the routing table to add to
     */
    private static void addDefaultRoutes(List content,
                                         Collection> containerClusters,
                                         RoutingTableSpec table) {
        if (content.isEmpty() || !indexingHopExists(table)) return;

        RouteSpec route = new RouteSpec("default");
        String hop = getContainerClustersDocprocHop(containerClusters);
        if (hop != null) {
            route.addHop(hop);
        }
        route.addHop("indexing");
        table.addRoute(route);

        if (content.size() == 1) {
            table.addRoute(new RouteSpec("default-get").addHop("[Content:cluster=" + content.get(0).getConfigId() + "]"));
        } else {
            //TODO This should ideally skip indexing and go directly to correct cluster.
            // But will handle the single cluster for now.
            table.addRoute(new RouteSpec("default-get").addHop("indexing"));
        }
    }

    private static boolean indexingHopExists(RoutingTableSpec table) {
        for (int i = 0, len = table.getNumHops(); i < len; ++i) {
            if (table.getHop(i).getName().equals("indexing")) {
                return true;
            }
        }
        return false;
    }

    private static String getContainerClustersDocprocHop(Collection> containerClusters) {
        DocprocChain result = null;

        for (ContainerCluster containerCluster: containerClusters) {
            DocprocChain defaultChain = getDefaultChain(containerCluster.getDocproc());
            if (defaultChain != null) {
                if (result != null)
                    throw new IllegalArgumentException("Only a single default docproc chain is allowed across all container clusters");
                result = defaultChain;
            }
        }

        return result == null ? null: result.getServiceName();
    }

    private static DocprocChain getDefaultChain(ContainerDocproc docproc) {
        return docproc == null
                ? null
                : docproc.getChains().allChains().getComponent("default");
    }

    /**
     * Attempts to simplify all route names by removing prefixing plugin name and whatever comes before the dot (.) in
     * the second naming element. This can only be done to those routes that do not share primary name elements with
     * other routes (e.g. a search clusters with the same name as a storage cluster).
     *
     * @param table the routing table whose route names are to be simplified
     */
    private static void simplifyRouteNames(RoutingTableSpec table) {
        if (table == null || !table.hasRoutes()) {
            return;
        }

        // Pass 1: Determine which simplifications are in conflict.
        Map> simple = new TreeMap<>();
        List broken = new ArrayList<>();
        for (int i = 0, len = table.getNumRoutes(); i < len; ++i) {
            String before = table.getRoute(i).getName();
            String after = simplifyRouteName(before);
            if (simple.containsKey(after)) {
                Set l = simple.get(after);
                l.add(before);
                if (!(l.contains("content/" + after) && l.contains("storage/cluster." + after) && (l.size() == 2))) {
                    broken.add(after);
                }
            } else {
                Set l = new HashSet<>();
                l.add(before);
                simple.put(after, l);
            }
        }

        // Pass 2: Simplify all non-conflicting route names by alias.
        Set alias = new HashSet<>();
        Set unique = new HashSet<>();
        for (int i = 0; i < table.getNumRoutes(); ) {
            RouteSpec route = table.getRoute(i);
            String before = route.getName();
            String after = simplifyRouteName(before);
            if (!before.equals(after)) {
                if (!broken.contains(after)) {
                    if (route.getNumHops() == 1 && route.getHop(0).equals(route.getName())) {
                        alias.add(new RouteSpec(after).addHop(route.getHop(0))); // full route name is redundant
                        unique.add(after);
                        table.removeRoute(i);
                        continue; // do not increment i
                    } else {
                        if (!unique.contains(after)) {
                            alias.add(new RouteSpec(after).addHop("route:" + before));
                            unique.add(after);
                        }
                    }
                }
            }
            ++i;
        }
        for (RouteSpec rs : alias) {
            table.addRoute(rs);
        }
    }

    /**
     * Returns a simplified version of the given route name. This method will remove the first component of the name as
     * separated by a forward slash, and then remove the first component of the remaining name as separated by a dot.
     *
     * @param name the route name to simplify
     * @return the simplified route name
     */
    private static String simplifyRouteName(String name) {
        String[] foo = name.split("/", 2);
        if (foo.length < 2) {
            return name;
        }
        String[] bar = foo[1].split("\\.", 2);
        if (bar.length < 2) {
            return foo[1];
        }
        return bar[1];
    }

    @Override
    public ApplicationSpec getApplicationSpec() {
        return application;
    }

    @Override
    public RoutingTableSpec getRoutingTableSpec() {
        return routingTable;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy