summaryrefslogtreecommitdiffstats
path: root/config-model-api/src/main/java/com/yahoo/config/application/api/Bcp.java
blob: af369dc2672969deeedf413c080efb11d7a09493 (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
package com.yahoo.config.application.api;

import com.yahoo.config.provision.RegionName;

import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Defines the BCP structure for an instance in a deployment spec:
 * A list of region groups where each group contains a set of regions
 * which will handle the traffic of a member in the group when it becomes unreachable.
 *
 * This is used to make bcp-aware autoscaling decisions. If no explicit BCP spec
 * is provided, it is assumed that a regions traffic will be divided equally over all
 * the other regions when it becomes unreachable - i.e a single BCP group is implicitly
 * defined having all defined production regions as members with fraction 1.0.
 *
 * It is assumed that the traffic of the unreachable region is distributed
 * evenly to the other members of the group.
 *
 * A region can be a fractional member of a group, in which case it is assumed that
 * region will only handle that fraction of its share of the unreachable regions traffic,
 * and symmetrically that the other members of the group will only handle that fraction
 * of the fraction regions traffic if it becomes unreachable.
 *
 * Each production region defined in the instance must have fractional memberships in groups that sums to exactly one.
 *
 * If a group has one member it will not set aside any capacity for BCP.
 * If a group has more than two members, the system will attempt to provision capacity
 * for BCP also when a region is unreachable. That is, if there are three member regions, A, B and C,
 * each handling 100 qps, then they each aim to handle 150 in case one goes down. If C goes down,
 * A and B will now handle 150 each, but will each aim to handle 300 each in case the other goes down.
 *
 * @author bratseth
 */
public class Bcp {

    private static final Bcp empty = new Bcp(List.of());

    private final List<Group> groups;

    public Bcp(List<Group> groups) {
        totalMembershipSumsToOne(groups);
        this.groups = List.copyOf(groups);
    }

    public List<Group> groups() { return groups; }

    /** Returns the set of regions declared in the groups of this. */
    public Set<RegionName> regions() {
        return groups.stream().flatMap(group -> group.members().stream()).map(member -> member.region()).collect(Collectors.toSet());
    }

    public boolean isEmpty() { return groups.isEmpty(); }

    /** Returns this bcp spec, or if it is empty, the given bcp spec. */
    public Bcp orElse(Bcp other) {
        return this.isEmpty() ? other : this;
    }

    private void totalMembershipSumsToOne(List<Group> groups) {
        Map<RegionName, Double> totalMembership = new HashMap<>();
        for (var group : groups) {
            for (var member : group.members())
                totalMembership.compute(member.region(), (__, fraction) -> fraction == null ? member.fraction()
                                                                                            : fraction + member.fraction());
        }
        for (var entry : totalMembership.entrySet()) {
            if (entry.getValue() != 1.0)
                throw new IllegalArgumentException("Illegal BCP spec: All regions must have total membership fractions summing to 1.0, but " +
                                                   entry.getKey() + " sums to " + entry.getValue());
        }
    }

    public static Bcp empty() { return empty; }

    @Override
    public String toString() {
        if (isEmpty()) return "empty BCP";
        return "BCP of " + groups;
    }

    public static class Group {

        private final Duration deadline;
        private final List<RegionMember> members;

        public Group(List<RegionMember> members, Duration deadline) {
            this.members = List.copyOf(members);
            this.deadline = deadline;
        }

        public List<RegionMember> members() { return members; }

        /**
         * Returns the max time until the other regions must be able to handle the additional traffic
         * when a region becomes unreachable, which by default is Duration.ZERO.
         */
        public Duration deadline() { return deadline; }

        @Override
        public String toString() {
            return "BCP group of " + members;
        }

    }

    public record RegionMember(RegionName region, double fraction) {

        public RegionMember {
            if (fraction < 0 || fraction > 1)
                throw new IllegalArgumentException("Fraction must be a number between 0.0 and 1.0, but got " + fraction);
        }


    }

}