aboutsummaryrefslogtreecommitdiffstats
path: root/config-model/src/main/java/com/yahoo/vespa/model/HostPorts.java
blob: f1d3b38e8fff0ef24dd67242f0a372016e1a907f (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
// Copyright Vespa.ai. 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.application.api.DeployLogger;
import com.yahoo.config.provision.NetworkPorts;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;

/**
 * Allocator for network ports on a host
 *
 * @author arnej
 */
public class HostPorts {

    final String hostname;
    public final static int BASE_PORT = 19100;
    final static int MAX_PORTS = 799;

    private DeployLogger deployLogger = (level, message) -> System.err.println("deploy log["+level+"]: "+message);

    private final Map<Integer, NetworkPortRequestor> portDB = new LinkedHashMap<>();

    private int allocatedPorts = 0;

    private PortFinder portFinder = new PortFinder(Collections.emptyList());

    private boolean flushed = false;
    private Optional<NetworkPorts> networkPortsList = Optional.empty();

    public HostPorts(String hostname) {
        this.hostname = hostname;
    }

    /**
     * Get the allocated network ports.
     * Should be called after allocation is complete and flushPortReservations has been called
     */
    public Optional<NetworkPorts> networkPorts() { return networkPortsList; }

    /**
     * Add port allocation from previous deployments.
     * Call this before starting port allocations, to re-use existing ports where possible
     */
    public void addNetworkPorts(NetworkPorts ports) {
        this.networkPortsList = Optional.of(ports);
        this.portFinder = new PortFinder(ports.allocations());
    }

    /**
     * Setup logging in order to send warnings back to the user.
     */
    public void useLogger(DeployLogger logger) {
        this.deployLogger = logger;
    }

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

    private int nextAvailableNetworkPort() {
        int port = BASE_PORT;
        for (; port < BASE_PORT + MAX_PORTS; port++) {
            if (isFree(port)) return port;
        }
        return 0;
    }

    private boolean isFree(int port) {
        return portFinder.isFree(port) && !portDB.containsKey(port);
    }

    /** Allocate a specific port number for a service */
    public int requireNetworkPort(int port, NetworkPortRequestor service, String suffix) {
        reservePort(service, port);
        String servType = service.getServiceType();
        String configId = service.getConfigId();
        portFinder.use(new NetworkPorts.Allocation(port, servType, configId, suffix));
        return port;
    }

    /** Allocate a preferred port number for a service, fall back to using any dynamic port */
    public int wantNetworkPort(int port, NetworkPortRequestor service, String suffix) {
        if (portDB.containsKey(port)) {
            int fallback = nextAvailableNetworkPort();
            NetworkPortRequestor s = portDB.get(port);
            deployLogger.log(Level.WARNING,
                service.getServiceName() +" cannot reserve port " + port + " on " +
                hostname + ": Already reserved for " + s.getServiceName() +
                ". Using default port range from " + fallback);
            return allocateNetworkPort(service, suffix);
        }
        return requireNetworkPort(port, service, suffix);
    }

    /** Allocate a dynamic port number for a service */
    public int allocateNetworkPort(NetworkPortRequestor service, String suffix) {
        String servType = service.getServiceType();
        String configId = service.getConfigId();
        int fallback = nextAvailableNetworkPort();
        int port = portFinder.findPort(new NetworkPorts.Allocation(fallback, servType, configId, suffix), hostname);
        reservePort(service, port);
        portFinder.use(new NetworkPorts.Allocation(port, servType, configId, suffix));
        return port;
    }

    /** Allocate all ports for a service */
    List<Integer> allocatePorts(NetworkPortRequestor service, int wantedPort) {
        PortAllocBridge allocator = new PortAllocBridge(this, service);
        service.allocatePorts(wantedPort, allocator);
        return allocator.result();
    }

    void deallocatePorts(NetworkPortRequestor service) {
        if (flushed)
            throw new IllegalStateException("Cannot deallocate ports after calling flushPortReservations()");
        portDB.entrySet().removeIf(entry -> entry.getValue().getServiceName().equals(service.getServiceName()));
        allocatedPorts--;
    }

    public void flushPortReservations() {
        this.networkPortsList = Optional.of(new NetworkPorts(portFinder.allocations()));
        this.flushed = 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(NetworkPortRequestor service, int port) {
        if (portDB.containsKey(port)) {
            portAlreadyReserved(service, port);
        }
        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(NetworkPortRequestor service, int port) {
        NetworkPortRequestor otherService = 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 IllegalArgumentException(service.getServiceName() + " cannot reserve port " + port +
                                           " on " + hostname + ": 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 " + hostname +
                    ". Move one or more services to another host, or outside this port range.");
    }

    @Override
    public String toString() {
        return "HostPorts{"+hostname+"}";
    }

}