aboutsummaryrefslogtreecommitdiffstats
path: root/routing-generator/src/main/java/com/yahoo/vespa/hosted/routing/status/RoutingStatusClient.java
blob: d982bb06f3225b66e2485d12a2426363d8d93285 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.routing.status;

import com.yahoo.component.annotation.Inject;
import com.yahoo.component.AbstractComponent;
import com.yahoo.lang.CachedSupplier;
import com.yahoo.routing.config.ZoneConfig;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Slime;
import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.athenz.api.AthenzService;
import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider;
import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier;
import com.yahoo.yolean.Exceptions;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.NoopUserTokenHandler;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.time.Duration;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
 * Caching client for the /routing/v1/status API on the config server. That API decides if a deployment (or entire zone)
 * is explicitly disabled in any global endpoints.
 *
 * This caches the status for a brief period to avoid drowning config servers with requests from health check pollers.
 *
 * @author oyving
 * @author andreer
 * @author mpolden
 */
public class RoutingStatusClient extends AbstractComponent implements RoutingStatus {

    private static final Logger log = Logger.getLogger(RoutingStatusClient.class.getName());
    private static final Duration requestTimeout = Duration.ofSeconds(2);
    private static final Duration cacheTtl = Duration.ofSeconds(5);

    private final CloseableHttpClient httpClient;
    private final URI configServerVip;
    private final CachedSupplier<Status> cache = new CachedSupplier<>(this::status, cacheTtl);

    @Inject
    public RoutingStatusClient(ZoneConfig config, ServiceIdentityProvider provider) {
        this(
                HttpClientBuilder.create()
                                 .setDefaultRequestConfig(RequestConfig.custom()
                                                                       .setConnectTimeout((int) requestTimeout.toMillis())
                                                                       .setConnectionRequestTimeout((int) requestTimeout.toMillis())
                                                                       .setSocketTimeout((int) requestTimeout.toMillis())
                                                                       .build())
                                 .setSSLContext(provider.getIdentitySslContext())
                                 .setSSLHostnameVerifier(createHostnameVerifier(config))
                                 // Required to enable connection pooling, which is disabled by default when using mTLS
                                 .setUserTokenHandler(NoopUserTokenHandler.INSTANCE)
                                 .setUserAgent("hosted-vespa-routing-status-client")
                                 .build(),
                URI.create(config.configserverVipUrl())
        );
    }

    public RoutingStatusClient(CloseableHttpClient httpClient, URI configServerVip) {
        this.httpClient = Objects.requireNonNull(httpClient);
        this.configServerVip = Objects.requireNonNull(configServerVip);
    }

    @Override
    public boolean isActive(String upstreamName) {
        try {
            return cache.get().isActive(upstreamName);
        } catch (Exception e) {
            log.log(Level.WARNING, "Failed to get status for '" + upstreamName + "': " + Exceptions.toMessageString(e));
            return true; // Assume IN if cache update fails
        }
    }

    @Override
    public void deconstruct() {
        Exceptions.uncheck(httpClient::close);
    }

    void invalidateCache() {
        cache.invalidate();
    }

    private Status status() {
        Slime slime = get("/routing/v2/status");
        Cursor root = slime.get();
        Set<String> inactiveDeployments = SlimeUtils.entriesStream(root.field("inactiveDeployments"))
                                                    .map(inspector -> inspector.field("upstreamName").asString())
                                                    .collect(Collectors.toUnmodifiableSet());
        boolean zoneActive = root.field("zoneActive").asBool();
        return new Status(zoneActive, inactiveDeployments);
    }

    private Slime get(String path) {
        URI url = configServerVip.resolve(path);
        HttpGet httpGet = new HttpGet(url);
        try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
            String entity = Exceptions.uncheck(() -> EntityUtils.toString(response.getEntity()));
            if (response.getStatusLine().getStatusCode() / 100 != 2) {
                throw new IllegalArgumentException("Got status code " + response.getStatusLine().getStatusCode() +
                                                   " for URL " + url + ", with response: " + entity);
            }
            return SlimeUtils.jsonToSlime(entity);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private static AthenzIdentityVerifier createHostnameVerifier(ZoneConfig config) {
        return new AthenzIdentityVerifier(Set.of(new AthenzService(config.configserverAthenzDomain(),
                                                                   config.configserverAthenzServiceName())));
    }

    private static class Status {

        private final boolean zoneActive;
        private final Set<String> inactiveDeployments;

        public Status(boolean zoneActive, Set<String> inactiveDeployments) {
            this.zoneActive = zoneActive;
            this.inactiveDeployments = Set.copyOf(Objects.requireNonNull(inactiveDeployments));
        }

        public boolean isActive(String upstreamName) {
            return zoneActive && !inactiveDeployments.contains(upstreamName);
        }

    }

}