aboutsummaryrefslogtreecommitdiffstats
path: root/container-search/src/main/java/com/yahoo/search/searchers/ConnectionControlSearcher.java
blob: 38c4951ff20b03c62a61c3cbd4940264a00a2662 (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
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.search.searchers;

import com.yahoo.component.annotation.Inject;
import com.yahoo.processing.request.CompoundName;
import com.yahoo.search.Query;
import com.yahoo.search.Result;
import com.yahoo.search.Searcher;
import com.yahoo.search.searchchain.Execution;

import java.util.concurrent.TimeUnit;
import java.util.function.LongSupplier;

/**
 * Searcher which can enforce HTTP connection close based on query properties.
 *
 * <p>
 * This searcher informs the client to close a persistent HTTP connection if the
 * connection is older than the configured max lifetime. This is done by adding
 * the "Connection" HTTP header with the value "Close" to the result.
 * </p>
 *
 * <p>
 * The searcher reads the query property "connectioncontrol.maxlifetime", which
 * is an integer number of seconds, to get the value for maximum connection
 * lifetime. Setting it to zero will enforce connection close independently of
 * the age of the connection. Typical usage would be as follows:
 * </p>
 *
 * <ol>
 * <li>Add the ConnectionControlSearcher to the default search chain of your
 * application. (It has no special ordering considerations.)</li>
 *
 * <li>For the default query profile of your application, set a reasonable value
 * for "connectioncontrol.maxlifetime". The definition of reasonable will be
 * highly application dependent, but it should always be less than the grace
 * period when taking the container out of production traffic.</li>
 *
 * <li>Deploy application. The container will now inform clients to close
 * connections/reconnect within the configured time limit.
 * </ol>
 *
 * @author frodelu
 * @author Steinar Knutsen
 */
public class ConnectionControlSearcher extends Searcher {

    private final String simpleName = this.getClass().getSimpleName();

    private final LongSupplier clock;

    private static final CompoundName KEEPALIVE_MAXLIFETIMESECONDS = CompoundName.from("connectioncontrol.maxlifetime");
    private static final String HTTP_CONNECTION_HEADER_NAME = "Connection";
    private static final String HTTP_CONNECTION_CLOSE_ARGUMENT = "Close";

    @Inject
    public ConnectionControlSearcher() {
        this(() -> System.currentTimeMillis());
    }

    private ConnectionControlSearcher(LongSupplier clock) {
        this.clock = clock;
    }

    /**
     * Create a searcher instance suitable for unit tests.
     *
     * @param clock a simulated or real clock behaving similarly to System.currentTimeMillis()
     * @return a fully initialised instance
     */
    public static ConnectionControlSearcher createTestInstance(LongSupplier clock) {
        return new ConnectionControlSearcher(clock);
    }

    @Override
    public Result search(Query query, Execution execution) {
        Result result = execution.search(query);

        query.trace(false, 3, simpleName, " updating headers.");
        keepAliveProcessing(query, result);
        return result;
    }

    /**
     * If the HTTP connection has been alive for too long, set the header
     * "Connection: Close" to tell the client to close the connection after this
     * request.
     */
    private void keepAliveProcessing(Query query, Result result) {
        int maxLifetimeSeconds = query.properties().getInteger(KEEPALIVE_MAXLIFETIMESECONDS, -1);

        if (maxLifetimeSeconds < 0) {
            return;
        } else if (maxLifetimeSeconds == 0) {
            result.getHeaders(true).put(HTTP_CONNECTION_HEADER_NAME, HTTP_CONNECTION_CLOSE_ARGUMENT);
            query.trace(false, 5, simpleName, ": Max HTTP connection lifetime set to 0; adding \"", HTTP_CONNECTION_HEADER_NAME,
                    ": ", HTTP_CONNECTION_CLOSE_ARGUMENT, "\" header");
        } else {
            setCloseIfLifetimeExceeded(query, result, maxLifetimeSeconds);
        }
    }

    private void setCloseIfLifetimeExceeded(Query query, Result result, int maxLifetimeSeconds) {
        if (query.getHttpRequest() == null) {
            query.trace(false, 5, simpleName, " got max lifetime = ", maxLifetimeSeconds,
                    ", but got no JDisc request. Setting no header.");
            return;
        }

        final long connectedAtMillis = query.getHttpRequest().getConnectedAt(TimeUnit.MILLISECONDS);
        final long maxLifeTimeMillis = maxLifetimeSeconds * 1000L;
        if (connectedAtMillis + maxLifeTimeMillis < clock.getAsLong()) {
            result.getHeaders(true).put(HTTP_CONNECTION_HEADER_NAME, HTTP_CONNECTION_CLOSE_ARGUMENT);
            query.trace(false, 5, simpleName, ": Max HTTP connection lifetime (", maxLifetimeSeconds, ") exceeded; adding \"",
                    HTTP_CONNECTION_HEADER_NAME, ": ", HTTP_CONNECTION_CLOSE_ARGUMENT, "\" header");
        }
    }

}