summaryrefslogtreecommitdiffstats
path: root/config-model/src/main/java/com/yahoo/vespa/model/HostResource.java
blob: 46dc287bb10a5a518ea78428011072cacf1ce267 (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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.model;

import com.yahoo.config.model.api.HostInfo;
import com.yahoo.config.provision.ClusterMembership;
import com.yahoo.config.provision.Flavor;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
import java.util.stream.Collectors;

/**
 * A host representation. The identity of this is the identity of its Host.
 * TODO: Merge with {@link Host}
 * Host resources are ordered by their host order.
 *
 * @author lulf
 * @since 5.12
 */
public class HostResource implements Comparable<HostResource> {

    public final static int BASE_PORT = 19100;
    final static int MAX_PORTS = 799;
    private final Host host;

    // Map from "sentinel name" to service
    private final Map<String, Service> services = new LinkedHashMap<>();
    private final Map<Integer, Service> portDB = new LinkedHashMap<>();

    private int allocatedPorts = 0;

    private Set<ClusterMembership> clusterMemberships = new LinkedHashSet<>();

    // Empty for self-hosted Vespa.
    private Optional<Flavor> flavor = Optional.empty();

    /**
     * Create a new {@link HostResource} bound to a specific {@link com.yahoo.vespa.model.Host}.
     *
     * @param host {@link com.yahoo.vespa.model.Host} object to bind to.
     */
    public HostResource(Host host) {
        this.host = host;
    }

    /**
     * Return the currently bounded {@link com.yahoo.vespa.model.Host}.
     * @return the {@link com.yahoo.vespa.model.Host} if bound, null if not.
     */
    public Host getHost() { return host; }

    /**
     * Returns the baseport of the first available port range of length numPorts,
     * or 0 if there is no range of that length available.
     *
     * @param numPorts  The length of the desired port range.
     * @return  The baseport of the first available range, or 0 if no range is available.
     */
    public int nextAvailableBaseport(int numPorts) {
        int range = 0;
        int port = BASE_PORT;
        for (; port < BASE_PORT + MAX_PORTS && (range < numPorts); port++) {
            if (portDB.containsKey(port)) {
                range = 0;
                continue;
            }
            range++;
        }
        return range == numPorts ?
                port - range :
                0;
    }

    /**
     * Adds service and allocates resources for it.
     *
     * @param service The Service to allocate resources for
     * @param wantedPort the wanted port for this service
     * @return  The allocated ports for the Service.
     */
    List<Integer> allocateService(AbstractService service, int wantedPort) {
        List<Integer> ports = allocatePorts(service, wantedPort);
        assert (getService(service.getServiceName()) == null) :
                ("There is already a service with name '" + service.getServiceName() + "' registered on " + this +
                ". Most likely a programming error - all service classes must have unique names, even in different packages!");

        services.put(service.getServiceName(), service);
        return ports;
    }

    private List<Integer> allocatePorts(AbstractService service, int wantedPort) {
        List<Integer> ports = new ArrayList<>();
        if (service.getPortCount() < 1)
            return ports;

        int serviceBasePort = BASE_PORT + allocatedPorts;
        if (wantedPort > 0) {
            if (service.getPortCount() < 1) {
                throw new RuntimeException(service + " wants baseport " + wantedPort +
                        ", but it has not reserved any ports, so it cannot name a desired baseport.");
            }
            if (service.requiresWantedPort() || canUseWantedPort(service, wantedPort, serviceBasePort))
                serviceBasePort = wantedPort;
        }

        reservePort(service, serviceBasePort);
        ports.add(serviceBasePort);

        int remainingPortsStart =  service.requiresConsecutivePorts() ?
                serviceBasePort + 1:
                BASE_PORT + allocatedPorts;
        for (int i = 0; i < service.getPortCount() - 1; i++) {
            int port = remainingPortsStart + i;
            reservePort(service, port);
            ports.add(port);
        }
        return ports;
    }

    private boolean canUseWantedPort(AbstractService service, int wantedPort, int serviceBasePort) {
        for (int i = 0; i < service.getPortCount(); i++) {
            int port = wantedPort + i;
            if (portDB.containsKey(port)) {
                AbstractService s = (AbstractService)portDB.get(port);
                s.getRoot().getDeployState().getDeployLogger().log(Level.WARNING, service.getServiceName() +" cannot reserve port " + port + " on " +
                        this + ": Already reserved for " + s.getServiceName() +
                        ". Using default port range from " + serviceBasePort);
                return false;
            }
            if (!service.requiresConsecutivePorts()) break;
        }
        return true;
    }

    /**
     * Reserves the desired port for the given service, or throws as exception if the port
     * is not available.
     *
     * @param service the service that wishes to reserve the port.
     * @param port the port to be reserved.
     */
    void reservePort(AbstractService service, int port) {
        if (portDB.containsKey(port)) {
            portAlreadyReserved(service, port);
        } else {
            if (inVespasPortRange(port)) {
                allocatedPorts++;
                if (allocatedPorts > MAX_PORTS) {
                    noMoreAvailablePorts();
                }
            }
            portDB.put(port, service);
        }
    }

    private boolean inVespasPortRange(int port) {
        return port >= BASE_PORT &&
                port < BASE_PORT + MAX_PORTS;
    }

    private void portAlreadyReserved(AbstractService service, int port) {
        AbstractService otherService = (AbstractService)portDB.get(port);
        int nextAvailablePort = nextAvailableBaseport(service.getPortCount());
        if (nextAvailablePort == 0) {
            noMoreAvailablePorts();
        }
        String msg = (service.getClass().equals(otherService.getClass()) && service.requiresWantedPort())
                ? "You must set port explicitly for all instances of this service type, except the first one. "
                : "";
        throw new RuntimeException(service.getServiceName() + " cannot reserve port " + port +
                    " on " + this + ": Already reserved for " + otherService.getServiceName() +
                    ". " + msg + "Next available port is: " + nextAvailablePort + " ports used: " + portDB);
    }

    private void noMoreAvailablePorts() {
        throw new RuntimeException
            ("Too many ports are reserved in Vespa's port range (" +
                    BASE_PORT  + ".." + (BASE_PORT+MAX_PORTS) + ") on " + this +
                    ". Move one or more services to another host, or outside this port range.");
    }

    /**
     * Returns the service with the given "sentinel name" on this Host,
     * or null if the name does not match any service.
     *
     * @param sentinelName the sentinel name of the service we want to return
     * @return The service with the given sentinel name
     */
    public Service getService(String sentinelName) {
        return services.get(sentinelName);
    }

    /**
     * Returns a List of all services running on this Host.
     * @return a List of all services running on this Host.
     */
    public List<Service> getServices() {
        return new ArrayList<>(services.values());
    }

    public HostInfo getHostInfo() {
        return new HostInfo(getHostName(), services.values().stream()
                .map(Service::getServiceInfo)
                .collect(Collectors.toSet()));
    }

    public void setFlavor(Optional<Flavor> flavor) { this.flavor = flavor; }

    /** Returns the flavor of this resource. Empty for self-hosted Vespa. */
    public Optional<Flavor> getFlavor() { return flavor; }

    public void addClusterMembership(@Nullable ClusterMembership clusterMembership) {
        if (clusterMembership != null)
            clusterMemberships.add(clusterMembership);
    }

    public Set<ClusterMembership> clusterMemberships() {
        return Collections.unmodifiableSet(clusterMemberships);
    }

    /**
     * Returns the "primary" cluster membership.
     * Content clusters are preferred, then container clusters, and finally admin clusters.
     * If there is more than one cluster of the preferred type, the cluster that was added first will be chosen.
     */
    public Optional<ClusterMembership> primaryClusterMembership() {
        return clusterMemberships().stream()
                .sorted(HostResource::compareClusters)
                .findFirst();
    }

    private static int compareClusters(ClusterMembership cluster1, ClusterMembership cluster2) {
        // This depends on the declared order of enum constants.
        return cluster2.cluster().type().compareTo(cluster1.cluster().type());
    }

    @Override
    public String toString() {
        return "host '" + host.getHostName() + "'";
    }

    public String getHostName() {
        return host.getHostName();
    }

    @Override
    public int hashCode() { return host.hashCode(); }

    @Override
    public boolean equals(Object other) {
        if (other == this) return true;
        if ( ! (other instanceof HostResource)) return false;
        return ((HostResource)other).host.equals(this.host);
    }

    @Override
    public int compareTo(HostResource other) {
        return this.host.compareTo(other.host);
    }

}