• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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