1 //
2 // Copyright 2022 gRPC authors.
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 //
16
17 #include "src/core/load_balancing/weighted_round_robin/static_stride_scheduler.h"
18
19 #include <limits>
20 #include <vector>
21
22 #include "absl/types/optional.h"
23 #include "gmock/gmock.h"
24 #include "gtest/gtest.h"
25
26 namespace grpc_core {
27 namespace {
28
29 using ::testing::ElementsAre;
30 using ::testing::UnorderedElementsAre;
31
TEST(StaticStrideSchedulerTest,EmptyWeightsIsNullopt)32 TEST(StaticStrideSchedulerTest, EmptyWeightsIsNullopt) {
33 uint32_t sequence = 0;
34 const std::vector<float> weights = {};
35 ASSERT_FALSE(StaticStrideScheduler::Make(absl::MakeSpan(weights), [&] {
36 return sequence++;
37 }).has_value());
38 }
39
TEST(StaticStrideSchedulerTest,OneZeroWeightIsNullopt)40 TEST(StaticStrideSchedulerTest, OneZeroWeightIsNullopt) {
41 uint32_t sequence = 0;
42 const std::vector<float> weights = {0};
43 ASSERT_FALSE(StaticStrideScheduler::Make(absl::MakeSpan(weights), [&] {
44 return sequence++;
45 }).has_value());
46 }
47
TEST(StaticStrideSchedulerTest,AllZeroWeightsIsNullopt)48 TEST(StaticStrideSchedulerTest, AllZeroWeightsIsNullopt) {
49 uint32_t sequence = 0;
50 const std::vector<float> weights = {0, 0, 0, 0};
51 ASSERT_FALSE(StaticStrideScheduler::Make(absl::MakeSpan(weights), [&] {
52 return sequence++;
53 }).has_value());
54 }
55
TEST(StaticStrideSchedulerTest,OneWeightsIsNullopt)56 TEST(StaticStrideSchedulerTest, OneWeightsIsNullopt) {
57 uint32_t sequence = 0;
58 const std::vector<float> weights = {1};
59 ASSERT_FALSE(StaticStrideScheduler::Make(absl::MakeSpan(weights), [&] {
60 return sequence++;
61 }).has_value());
62 }
63
TEST(StaticStrideSchedulerTest,PicksAreWeightedExactly)64 TEST(StaticStrideSchedulerTest, PicksAreWeightedExactly) {
65 uint32_t sequence = 0;
66 const std::vector<float> weights = {1, 2, 3};
67 const absl::optional<StaticStrideScheduler> scheduler =
68 StaticStrideScheduler::Make(absl::MakeSpan(weights),
69 [&] { return sequence++; });
70 ASSERT_TRUE(scheduler.has_value());
71
72 std::vector<int> picks(weights.size());
73 for (int i = 0; i < 6; ++i) {
74 ++picks[scheduler->Pick()];
75 }
76 EXPECT_THAT(picks, ElementsAre(1, 2, 3));
77 }
78
TEST(StaticStrideSchedulerTest,ZeroWeightUsesMean)79 TEST(StaticStrideSchedulerTest, ZeroWeightUsesMean) {
80 uint32_t sequence = 0;
81 const std::vector<float> weights = {3, 0, 1};
82 const absl::optional<StaticStrideScheduler> scheduler =
83 StaticStrideScheduler::Make(absl::MakeSpan(weights),
84 [&] { return sequence++; });
85 ASSERT_TRUE(scheduler.has_value());
86
87 std::vector<int> picks(weights.size());
88 for (int i = 0; i < 6; ++i) {
89 ++picks[scheduler->Pick()];
90 }
91 EXPECT_THAT(picks, ElementsAre(3, 2, 1));
92 }
93
TEST(StaticStrideSchedulerTest,AllWeightsEqualIsRoundRobin)94 TEST(StaticStrideSchedulerTest, AllWeightsEqualIsRoundRobin) {
95 uint32_t sequence = 0;
96 const std::vector<float> weights = {300, 300, 0};
97 const absl::optional<StaticStrideScheduler> scheduler =
98 StaticStrideScheduler::Make(absl::MakeSpan(weights),
99 [&] { return sequence++; });
100 ASSERT_TRUE(scheduler.has_value());
101
102 std::vector<size_t> picks(weights.size());
103 for (int i = 0; i < 3; ++i) {
104 picks[i] = scheduler->Pick();
105 }
106
107 // Each backend is selected exactly once.
108 EXPECT_THAT(picks, UnorderedElementsAre(0, 1, 2));
109
110 // And continues to be picked in the original order, whatever it may be.
111 for (int i = 0; i < 1000; ++i) {
112 EXPECT_EQ(scheduler->Pick(), picks[i % 3]);
113 }
114 }
115
TEST(StaticStrideSchedulerTest,PicksAreDeterministic)116 TEST(StaticStrideSchedulerTest, PicksAreDeterministic) {
117 uint32_t sequence = 0;
118 const std::vector<float> weights = {1, 2, 3};
119 const absl::optional<StaticStrideScheduler> scheduler =
120 StaticStrideScheduler::Make(absl::MakeSpan(weights),
121 [&] { return sequence++; });
122 ASSERT_TRUE(scheduler.has_value());
123
124 const int n = 100;
125 std::vector<size_t> picks;
126 picks.reserve(n);
127 for (int i = 0; i < n; ++i) {
128 picks.push_back(scheduler->Pick());
129 }
130 for (int i = 0; i < 5; ++i) {
131 sequence = 0;
132 for (int j = 0; j < n; ++j) {
133 EXPECT_EQ(scheduler->Pick(), picks[j]);
134 }
135 }
136 }
137
TEST(StaticStrideSchedulerTest,RebuildGiveSamePicks)138 TEST(StaticStrideSchedulerTest, RebuildGiveSamePicks) {
139 uint32_t sequence = 0;
140 const std::vector<float> weights = {1, 2, 3};
141 const absl::optional<StaticStrideScheduler> scheduler =
142 StaticStrideScheduler::Make(absl::MakeSpan(weights),
143 [&] { return sequence++; });
144 ASSERT_TRUE(scheduler.has_value());
145
146 const int n = 100;
147 std::vector<size_t> picks;
148 picks.reserve(n);
149 for (int i = 0; i < n; ++i) {
150 picks.push_back(scheduler->Pick());
151 }
152
153 // Rewind and make each pick with a new scheduler instance. This should give
154 // identical picks.
155 sequence = 0;
156 for (int i = 0; i < n; ++i) {
157 const absl::optional<StaticStrideScheduler> rebuild =
158 StaticStrideScheduler::Make(absl::MakeSpan(weights),
159 [&] { return sequence++; });
160 ASSERT_TRUE(rebuild.has_value());
161
162 EXPECT_EQ(rebuild->Pick(), picks[i]);
163 }
164 }
165
166 // This tests an internal implementation detail of StaticStrideScheduler --
167 // the highest weighted element will be picked on all `kMaxWeight` generations.
168 // The number of picks required to run through all values of the sequence is
169 // mean(weights) * kMaxWeight. It is worth testing this property because it can
170 // catch rounding and off-by-one errors.
TEST(StaticStrideSchedulerTest,LargestIsPickedEveryGeneration)171 TEST(StaticStrideSchedulerTest, LargestIsPickedEveryGeneration) {
172 uint32_t sequence = 0;
173 const std::vector<float> weights = {1, 2, 3};
174 const int mean = 2;
175 const absl::optional<StaticStrideScheduler> scheduler =
176 StaticStrideScheduler::Make(absl::MakeSpan(weights),
177 [&] { return sequence++; });
178 ASSERT_TRUE(scheduler.has_value());
179
180 const int kMaxWeight = std::numeric_limits<uint16_t>::max();
181 int largest_weight_pick_count = 0;
182 for (int i = 0; i < kMaxWeight * mean; ++i) {
183 if (scheduler->Pick() == 2) {
184 ++largest_weight_pick_count;
185 }
186 }
187 EXPECT_EQ(largest_weight_pick_count, kMaxWeight);
188 }
189
TEST(StaticStrideSchedulerTest,MaxIsClampedForHighRatio)190 TEST(StaticStrideSchedulerTest, MaxIsClampedForHighRatio) {
191 uint32_t sequence = 0;
192 const std::vector<float> weights{81, 1, 1, 1, 1, 1, 1, 1, 1, 1,
193 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
194 const absl::optional<StaticStrideScheduler> scheduler =
195 StaticStrideScheduler::Make(absl::MakeSpan(weights),
196 [&] { return sequence++; });
197 ASSERT_TRUE(scheduler.has_value());
198
199 // max gets clamped to mean*maxRatio = 50 for this set of weights. So if we
200 // pick 50 + 19 times we should get all possible picks.
201 std::vector<int> picks(weights.size());
202 for (int i = 0; i < 69; ++i) {
203 ++picks[scheduler->Pick()];
204 }
205 EXPECT_THAT(picks, ElementsAre(50, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
206 1, 1, 1, 1, 1));
207 }
208
TEST(StaticStrideSchedulerTest,MinIsClampedForHighRatio)209 TEST(StaticStrideSchedulerTest, MinIsClampedForHighRatio) {
210 uint32_t sequence = 0;
211 const std::vector<float> weights{100, 1e-10};
212 const absl::optional<StaticStrideScheduler> scheduler =
213 StaticStrideScheduler::Make(absl::MakeSpan(weights),
214 [&] { return sequence++; });
215 ASSERT_TRUE(scheduler.has_value());
216
217 // We pick 201 elements and ensure that the second channel (with epsilon
218 // weight) also gets picked. The math is: mean value of elements is ~50, so
219 // the first channel keeps its weight of 100, but the second element's weight
220 // gets capped from below to 50*0.01 = 0.5.
221 std::vector<int> picks(weights.size());
222 for (int i = 0; i < 201; ++i) {
223 ++picks[scheduler->Pick()];
224 }
225 EXPECT_THAT(picks, ElementsAre(200, 1));
226 }
227
228 } // namespace
229 } // namespace grpc_core
230
main(int argc,char ** argv)231 int main(int argc, char** argv) {
232 ::testing::InitGoogleTest(&argc, argv);
233 return RUN_ALL_TESTS();
234 }
235