summaryrefslogtreecommitdiffstats
path: root/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/ClusterModelTest.java
blob: f07d52a4a7f45dfe556f37c90e3b59a35fb623d0 (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
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.provision.autoscale;

import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Capacity;
import com.yahoo.config.provision.ClusterResources;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.test.ManualClock;
import com.yahoo.vespa.hosted.provision.applications.Application;
import com.yahoo.vespa.hosted.provision.applications.Cluster;
import com.yahoo.vespa.hosted.provision.applications.Status;
import com.yahoo.vespa.hosted.provision.provisioning.ProvisioningTester;
import org.junit.Test;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.function.IntFunction;

import static org.junit.Assert.assertEquals;

/**
 * @author bratseth
 */
public class ClusterModelTest {

    private static final double delta = 0.001;

    @Test
    public void unit_adjustment_should_cause_no_change() {
        var model = clusterModelWithNoData(); // 5 nodes, 1 group
        assertEquals(Load.one(), model.loadAdjustment());
        var target = model.loadAdjustment().scaled(nodeResources());
        int testingNodes = 5 - 1;
        int currentNodes = 5 - 1;
        assertEquals(nodeResources(), model.loadWith(testingNodes, 1).scaled(Load.one().divide(model.loadWith(currentNodes, 1)).scaled(target)));
    }

    @Test
    public void test_traffic_headroom() {
        // No current traffic share: Ideal load is low but capped
        var model1 = clusterModel(new Status(0.0, 1.0),
                                  t -> t == 0 ? 10000.0 : 100.0, t -> 0.0);
        assertEquals(0.32653061224489793, model1.idealLoad().cpu(), delta);

        // Almost no current traffic share: Ideal load is low but capped
        var model2 = clusterModel(new Status(0.0001, 1.0),
                                  t -> t == 0 ? 10000.0 : 100.0, t -> 0.0);
        assertEquals(0.32653061224489793, model2.idealLoad().cpu(), delta);

        // Almost no traffic: Headroom impact is reduced due to uncertainty
        var model3 = clusterModel(new Status(0.0001, 1.0),
                                  t -> t == 0 ? 10000.0 : 1.0, t -> 0.0);
        assertEquals(0.6465952717720751, model3.idealLoad().cpu(), delta);
    }

    @Test
    public void test_growth_headroom() {
        // No traffic data: Ideal load assumes 2 regions
        var model1 = clusterModel(new Status(0.0, 0.0),
                                  t -> t == 0 ? 10000.0 : 100.0, t -> 0.0);
        assertEquals(0.16326530612244897, model1.idealLoad().cpu(), delta);

        // No traffic: Ideal load is higher since we now know there is only one zone
        var model2 = clusterModel(new Status(0.0, 1.0),
                                  t -> t == 0 ? 10000.0 : 100.0, t -> 0.0);
        assertEquals(0.32653061224489793, model2.idealLoad().cpu(), delta);

        // Almost no current traffic: Similar number as above
        var model3 = clusterModel(new Status(0.0001, 1.0),
                                  t -> t == 0 ? 10000.0 : 100.0, t -> 0.0);
        assertEquals(0.32653061224489793, model3.idealLoad().cpu(), delta);

        // Low query rate: Impact of growth headroom is reduced due to uncertainty
        var model4 = clusterModel(new Status(0.0001, 1.0),
                                  t -> t == 0 ? 100.0 : 1.0, t -> 0.0);
        assertEquals(0.6465952717720751, model4.idealLoad().cpu(), delta);
    }

    private ClusterModel clusterModelWithNoData() {
        return clusterModel(new Status(0.0, 1.0), t -> 0.0, t -> 0.0);
    }

    private ClusterModel clusterModel(Status status, IntFunction<Double> queryRate, IntFunction<Double> writeRate) {
        ManualClock clock = new ManualClock();
        Application application = Application.empty(ApplicationId.from("t1", "a1", "i1"));
        ClusterSpec clusterSpec = clusterSpec();
        Cluster cluster = cluster();
        application = application.with(cluster);
        var nodeRepository = new ProvisioningTester.Builder().build().nodeRepository();
        return new ClusterModel(nodeRepository,
                                application.with(status),
                                clusterSpec, cluster,
                                new AllocatableResources(clusterResources(), clusterSpec, nodeRepository),
                                clock, Duration.ofMinutes(10), Duration.ofMinutes(5),
                                timeseries(cluster,100, queryRate, writeRate, clock),
                                ClusterNodesTimeseries.empty());
    }

    private ClusterResources clusterResources() {
        return new ClusterResources(5, 1, nodeResources());
    }

    private NodeResources nodeResources() {
        return new NodeResources(1, 10, 100, 1);
    }

    private ClusterSpec clusterSpec() {
        return ClusterSpec.specification(ClusterSpec.Type.content, ClusterSpec.Id.from("test"))
                          .group(ClusterSpec.Group.from(0))
                          .vespaVersion("7.1.1")
                          .build();
    }

    private Cluster cluster() {
        return Cluster.create(ClusterSpec.Id.from("test"),
                              false,
                              Capacity.from(clusterResources()));
    }

    /** Creates the given number of measurements, spaced 5 minutes between, using the given function */
    private ClusterTimeseries timeseries(Cluster cluster,
                                         int measurements,
                                         IntFunction<Double> queryRate,
                                         IntFunction<Double> writeRate,
                                         ManualClock clock) {
        List<ClusterMetricSnapshot> snapshots = new ArrayList<>(measurements);
        for (int i = 0; i < measurements; i++) {
            snapshots.add(new ClusterMetricSnapshot(clock.instant(), queryRate.apply(i), writeRate.apply(i)));
            clock.advance(Duration.ofMinutes(5));
        }
        return new ClusterTimeseries(cluster.id(),snapshots);
    }

}