From b5cf7b07378528cfbd21dff7f272f14f3ca2de06 Mon Sep 17 00:00:00 2001 From: Tony An <40644135+tonyjongyoonan@users.noreply.github.com> Date: Fri, 9 Jun 2023 14:42:08 -0700 Subject: [PATCH 01/42] implemented static stride scheduler class/algorithm (currently unused) go/static-stride-scheduler --- .../xds/WeightedRoundRobinLoadBalancer.java | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 48442a84b22..34574a82523 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -310,6 +310,37 @@ private void updateWeight() { } this.scheduler = scheduler; } + + // check correctness of behavior + private void updateWeightSSS() { + int weightedChannelCount = 0; + double avgWeight = 0; + for (Subchannel value : list) { + double newWeight = ((WrrSubchannel) value).getWeight(); + if (newWeight > 0) { + avgWeight += newWeight; + weightedChannelCount++; + } + } + + if (weightedChannelCount >= 1) { + avgWeight /= 1.0 * weightedChannelCount; + } else { + avgWeight = 1; + } + + List newWeights = new ArrayList<>(); + for (int i = 0; i < list.size(); i++) { + WrrSubchannel subchannel = (WrrSubchannel) list.get(i); + double newWeight = subchannel.getWeight(); + newWeights.add( + i, + newWeight > 0 ? (float) newWeight : (float) avgWeight); // check desired type (float?) + } + + StaticStrideScheduler ssScheduler = new StaticStrideScheduler(newWeights); + this.ssScheduler = ssScheduler; + } @Override public String toString() { @@ -432,6 +463,83 @@ int pick() { } } + // TODO: add javadocs comments + @VisibleForTesting + static final class StaticStrideScheduler { + private Vector scaledWeights; + private int sizeDivisor; + private long sequence; + private static final int K_MAX_WEIGHT = 65535; // uint16? can be uint8 + private static final long UINT32_MAX = 429967295L; // max value for uint32 + + StaticStrideScheduler(List weights) { + int numChannels = weights.size(); + int numZeroWeightChannels = 0; + double sumWeight = 0; + float maxWeight = 0; + for (float weight : weights) { + if (weight == 0) { + numZeroWeightChannels++; + } + sumWeight += weight; + maxWeight = Math.max(weight, maxWeight); + } + + // checkArgument(numChannels <= 1, "Couldn't build scheduler: requires at least two weights"); + // checkArgument(numZeroWeightChannels == numChannels, "Couldn't build scheduler: only zero + // weights"); + + double scalingFactor = K_MAX_WEIGHT / maxWeight; + long meanWeight = + Math.round(scalingFactor * sumWeight / (numChannels - numZeroWeightChannels)); + + // scales weights s.t. max(weights) == K_MAX_WEIGHT, meanWeight is scaled accordingly + Vector scaledWeights = new Vector<>(numChannels); + // vectors are deprecated post Java 9? + for (int i = 0; i < numChannels; i++) { + if (weights.get(i) == 0) { + scaledWeights.add(meanWeight); + } else { + scaledWeights.add(Math.round(weights.get(i) * scalingFactor)); + } + } + + this.scaledWeights = scaledWeights; + this.sizeDivisor = numChannels; // why not just call it numChannels or numBackends + this.sequence = (long) (Math.random() * UINT32_MAX); // why not initialize to [0,numChannels) + } + + private long nextSequence() { + long sequence = this.sequence; + this.sequence = (this.sequence + 1) % UINT32_MAX; // check wraparound logic + return sequence; + } + + // is this getter necessary? (is it ever called outside of this class) + public Vector getWeights() { + return this.scaledWeights; + } + + private void addChannel() {} + + // selects index of our next backend server + int pickChannel() { + while (true) { + long sequence = this.nextSequence(); + int backendIndex = (int) sequence % this.sizeDivisor; + long generation = sequence / this.sizeDivisor; + // is this really that much more efficient than a 2d array? + long weight = this.scaledWeights.get(backendIndex); + long kOffset = K_MAX_WEIGHT / 2; + long mod = (weight * generation + backendIndex * kOffset) % K_MAX_WEIGHT; + if (mod < K_MAX_WEIGHT - weight) { // review this math + continue; + } + return backendIndex; + } + } + } + /** Holds the state of the object. */ @VisibleForTesting static class ObjectState { From 44a51584f44e3605d288fab0838f1103a3757ead Mon Sep 17 00:00:00 2001 From: Tony An Date: Mon, 12 Jun 2023 13:25:09 -0700 Subject: [PATCH 02/42] added imports, fixed small errors --- .../xds/WeightedRoundRobinLoadBalancer.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 34574a82523..d641d054a71 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -40,12 +40,14 @@ import io.grpc.xds.orca.OrcaOobUtil.OrcaOobReportListener; import io.grpc.xds.orca.OrcaPerRequestUtil; import io.grpc.xds.orca.OrcaPerRequestUtil.OrcaPerRequestReportListener; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.Random; +import java.util.Vector; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -259,6 +261,7 @@ final class WeightedRoundRobinPicker extends RoundRobinPicker { private final boolean enableOobLoadReport; private final float errorUtilizationPenalty; private volatile EdfScheduler scheduler; + private volatile StaticStrideScheduler ssScheduler; // what does volatile mean? WeightedRoundRobinPicker(List list, boolean enableOobLoadReport, float errorUtilizationPenalty) { @@ -312,7 +315,7 @@ private void updateWeight() { } // check correctness of behavior - private void updateWeightSSS() { + private void updateWeightSS() { int weightedChannelCount = 0; double avgWeight = 0; for (Subchannel value : list) { @@ -463,11 +466,17 @@ int pick() { } } - // TODO: add javadocs comments + /* + * Implementation of Static Stride Scheduler + * Replaces EDFScheduler + * + * go/static-stride-scheduler + */ @VisibleForTesting static final class StaticStrideScheduler { private Vector scaledWeights; - private int sizeDivisor; + // private final long[] scaledWeights; + private final int sizeDivisor; private long sequence; private static final int K_MAX_WEIGHT = 65535; // uint16? can be uint8 private static final long UINT32_MAX = 429967295L; // max value for uint32 @@ -520,8 +529,6 @@ public Vector getWeights() { return this.scaledWeights; } - private void addChannel() {} - // selects index of our next backend server int pickChannel() { while (true) { From 32973d4fbebe8de23c2e70b98781382e7ceb9fe5 Mon Sep 17 00:00:00 2001 From: Tony An Date: Mon, 12 Jun 2023 13:50:01 -0700 Subject: [PATCH 03/42] changed to array, final class vars, atomic integer --- .../xds/WeightedRoundRobinLoadBalancer.java | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index d641d054a71..cb9ec67bb77 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -40,16 +40,15 @@ import io.grpc.xds.orca.OrcaOobUtil.OrcaOobReportListener; import io.grpc.xds.orca.OrcaPerRequestUtil; import io.grpc.xds.orca.OrcaPerRequestUtil.OrcaPerRequestReportListener; -import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.Random; -import java.util.Vector; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; @@ -332,13 +331,11 @@ private void updateWeightSS() { avgWeight = 1; } - List newWeights = new ArrayList<>(); + float[] newWeights = new float[list.size()]; for (int i = 0; i < list.size(); i++) { WrrSubchannel subchannel = (WrrSubchannel) list.get(i); double newWeight = subchannel.getWeight(); - newWeights.add( - i, - newWeight > 0 ? (float) newWeight : (float) avgWeight); // check desired type (float?) + newWeights[i] = newWeight > 0 ? (float) newWeight : (float) avgWeight; } StaticStrideScheduler ssScheduler = new StaticStrideScheduler(newWeights); @@ -474,15 +471,14 @@ int pick() { */ @VisibleForTesting static final class StaticStrideScheduler { - private Vector scaledWeights; - // private final long[] scaledWeights; - private final int sizeDivisor; + private final long[] scaledWeights; + private final AtomicInteger sizeDivisor; private long sequence; private static final int K_MAX_WEIGHT = 65535; // uint16? can be uint8 private static final long UINT32_MAX = 429967295L; // max value for uint32 - StaticStrideScheduler(List weights) { - int numChannels = weights.size(); + StaticStrideScheduler(float[] weights) { + int numChannels = weights.length; int numZeroWeightChannels = 0; double sumWeight = 0; float maxWeight = 0; @@ -496,25 +492,25 @@ static final class StaticStrideScheduler { // checkArgument(numChannels <= 1, "Couldn't build scheduler: requires at least two weights"); // checkArgument(numZeroWeightChannels == numChannels, "Couldn't build scheduler: only zero - // weights"); + // weights"); // checks break code double scalingFactor = K_MAX_WEIGHT / maxWeight; long meanWeight = Math.round(scalingFactor * sumWeight / (numChannels - numZeroWeightChannels)); // scales weights s.t. max(weights) == K_MAX_WEIGHT, meanWeight is scaled accordingly - Vector scaledWeights = new Vector<>(numChannels); + long[] scaledWeights = new long[numChannels]; // vectors are deprecated post Java 9? for (int i = 0; i < numChannels; i++) { - if (weights.get(i) == 0) { - scaledWeights.add(meanWeight); + if (weights[i] == 0) { + scaledWeights[i] = meanWeight; } else { - scaledWeights.add(Math.round(weights.get(i) * scalingFactor)); + scaledWeights[i] = Math.round(weights[i] * scalingFactor); } } this.scaledWeights = scaledWeights; - this.sizeDivisor = numChannels; // why not just call it numChannels or numBackends + this.sizeDivisor = new AtomicInteger(numChannels); // why not call numChannels or numBackends this.sequence = (long) (Math.random() * UINT32_MAX); // why not initialize to [0,numChannels) } @@ -525,7 +521,7 @@ private long nextSequence() { } // is this getter necessary? (is it ever called outside of this class) - public Vector getWeights() { + public long[] getWeights() { return this.scaledWeights; } @@ -533,10 +529,10 @@ public Vector getWeights() { int pickChannel() { while (true) { long sequence = this.nextSequence(); - int backendIndex = (int) sequence % this.sizeDivisor; - long generation = sequence / this.sizeDivisor; + int backendIndex = (int) sequence % this.sizeDivisor.get(); + long generation = sequence / this.sizeDivisor.get(); // is this really that much more efficient than a 2d array? - long weight = this.scaledWeights.get(backendIndex); + long weight = this.scaledWeights[backendIndex]; long kOffset = K_MAX_WEIGHT / 2; long mod = (weight * generation + backendIndex * kOffset) % K_MAX_WEIGHT; if (mod < K_MAX_WEIGHT - weight) { // review this math From 4bde79d8772c4920416ec0e589f57a188f062e0c Mon Sep 17 00:00:00 2001 From: Tony An Date: Mon, 12 Jun 2023 14:58:24 -0700 Subject: [PATCH 04/42] added static stride scheduler test cases --- .../WeightedRoundRobinLoadBalancerTest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index daf58a174d9..4043a6a7fcc 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -53,6 +53,7 @@ import io.grpc.services.InternalCallMetricRecorder; import io.grpc.services.MetricReport; import io.grpc.xds.WeightedRoundRobinLoadBalancer.EdfScheduler; +import io.grpc.xds.WeightedRoundRobinLoadBalancer.StaticStrideScheduler; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinLoadBalancerConfig; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinPicker; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WrrSubchannel; @@ -862,6 +863,36 @@ public void wrrConfig_BooleanValueNonNull() { WeightedRoundRobinLoadBalancerConfig.newBuilder().setEnableOobLoadReport((Boolean) null); } + @Test(expected = IllegalArgumentException.class) + public void emptyWeights() { + List weights = new ArrayList<>(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights); + } + + @Test + public void testPicksEqualsWeights() { + float[] weights = {1.0f, 2.0f, 3.0f}; + StaticStrideScheduler sss = new StaticStrideScheduler(weights); + int[] expectedPicks = new int[] {1, 2, 3}; + int[] picks = new int[3]; + for (int i = 0; i < 6; i++) { + picks[sss.pickChannel()] += 1; + } + assertThat(picks).isEqualTo(expectedPicks); + } + + @Test + public void testContainsZeroWeight() { + float[] weights = {3.0f, 0.0f, 1.0f}; + StaticStrideScheduler sss = new StaticStrideScheduler(weights); + int[] expectedPicks = new int[] {3, 2, 1}; + int[] picks = new int[3]; + for (int i = 0; i < 6; i++) { + picks[sss.pickChannel()] += 1; + } + assertThat(picks).isEqualTo(expectedPicks); + } + private static class FakeSocketAddress extends SocketAddress { final String name; From d99d51af09ea8dc23bf09ccc355baa3e4a785294 Mon Sep 17 00:00:00 2001 From: Tony An Date: Mon, 12 Jun 2023 15:19:11 -0700 Subject: [PATCH 05/42] fixing test case datatypes --- .../java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 4043a6a7fcc..f629721d566 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -865,8 +865,9 @@ public void wrrConfig_BooleanValueNonNull() { @Test(expected = IllegalArgumentException.class) public void emptyWeights() { - List weights = new ArrayList<>(); + float[] weights = {}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); + sss.pickChannel(); } @Test From 13a9fd819149ade5fed8368ba753b40e21bb32e5 Mon Sep 17 00:00:00 2001 From: Tony An Date: Mon, 12 Jun 2023 15:20:16 -0700 Subject: [PATCH 06/42] added check argument for edge case: no weights inputted --- .../main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index cb9ec67bb77..c48ebebd011 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -490,9 +490,7 @@ static final class StaticStrideScheduler { maxWeight = Math.max(weight, maxWeight); } - // checkArgument(numChannels <= 1, "Couldn't build scheduler: requires at least two weights"); - // checkArgument(numZeroWeightChannels == numChannels, "Couldn't build scheduler: only zero - // weights"); // checks break code + checkArgument(numChannels >= 1, "Couldn't build scheduler: requires at least one weight"); double scalingFactor = K_MAX_WEIGHT / maxWeight; long meanWeight = From 3d9e6255876a8cd5b3f244b5f652de8bc7f90409 Mon Sep 17 00:00:00 2001 From: Tony An Date: Mon, 12 Jun 2023 15:34:11 -0700 Subject: [PATCH 07/42] replaced edf scheduler with static stride scheduler --- .../io/grpc/xds/WeightedRoundRobinLoadBalancer.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index c48ebebd011..008a0307461 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -121,7 +121,7 @@ private final class UpdateWeightTask implements Runnable { @Override public void run() { if (currentPicker != null && currentPicker instanceof WeightedRoundRobinPicker) { - ((WeightedRoundRobinPicker)currentPicker).updateWeight(); + ((WeightedRoundRobinPicker)currentPicker).updateWeightSS(); } weightUpdateTimer = syncContext.schedule(this, config.weightUpdatePeriodNanos, TimeUnit.NANOSECONDS, timeService); @@ -259,8 +259,7 @@ final class WeightedRoundRobinPicker extends RoundRobinPicker { new HashMap<>(); private final boolean enableOobLoadReport; private final float errorUtilizationPenalty; - private volatile EdfScheduler scheduler; - private volatile StaticStrideScheduler ssScheduler; // what does volatile mean? + private volatile StaticStrideScheduler scheduler; // what does volatile mean? WeightedRoundRobinPicker(List list, boolean enableOobLoadReport, float errorUtilizationPenalty) { @@ -273,7 +272,7 @@ final class WeightedRoundRobinPicker extends RoundRobinPicker { } this.enableOobLoadReport = enableOobLoadReport; this.errorUtilizationPenalty = errorUtilizationPenalty; - updateWeight(); + updateWeightSS(); } @Override @@ -289,7 +288,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { } } - private void updateWeight() { + private void updateWeightEdf() { int weightedChannelCount = 0; double avgWeight = 0; for (Subchannel value : list) { @@ -338,8 +337,8 @@ private void updateWeightSS() { newWeights[i] = newWeight > 0 ? (float) newWeight : (float) avgWeight; } - StaticStrideScheduler ssScheduler = new StaticStrideScheduler(newWeights); - this.ssScheduler = ssScheduler; + StaticStrideScheduler scheduler = new StaticStrideScheduler(newWeights); + this.scheduler = scheduler; } @Override From 2b054d319aafdd903c2c4f118f7754da697caeba Mon Sep 17 00:00:00 2001 From: Tony An Date: Mon, 12 Jun 2023 15:52:29 -0700 Subject: [PATCH 08/42] added test case --- .../WeightedRoundRobinLoadBalancerTest.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index f629721d566..e7bbef2660e 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -865,9 +865,9 @@ public void wrrConfig_BooleanValueNonNull() { @Test(expected = IllegalArgumentException.class) public void emptyWeights() { - float[] weights = {}; - StaticStrideScheduler sss = new StaticStrideScheduler(weights); - sss.pickChannel(); + float[] weights = {}; + StaticStrideScheduler sss = new StaticStrideScheduler(weights); + sss.pickChannel(); } @Test @@ -894,6 +894,18 @@ public void testContainsZeroWeight() { assertThat(picks).isEqualTo(expectedPicks); } + @Test + public void testEqualSchedulers() { + float[] weights = {1.0f, 7.0f, 3.0f, 5.0f, 1.1f}; + StaticStrideScheduler sss1 = new StaticStrideScheduler(weights); + StaticStrideScheduler sss2 = new StaticStrideScheduler(weights); + for (int i = 0; i < 100; i++) { + int firstChannel = sss1.pickChannel(); + int secondChannel = sss2.pickChannel(); + assertThat(firstChannel).isEqualTo(secondChannel); + } + } + private static class FakeSocketAddress extends SocketAddress { final String name; From 4addda31abaf674607c444ac2200f60ef4301724 Mon Sep 17 00:00:00 2001 From: Tony An Date: Mon, 12 Jun 2023 15:52:56 -0700 Subject: [PATCH 09/42] fixed style errors, renamed schedulers --- .../xds/WeightedRoundRobinLoadBalancer.java | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 008a0307461..cf2a9d83ee8 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -259,7 +259,8 @@ final class WeightedRoundRobinPicker extends RoundRobinPicker { new HashMap<>(); private final boolean enableOobLoadReport; private final float errorUtilizationPenalty; - private volatile StaticStrideScheduler scheduler; // what does volatile mean? + private volatile EdfScheduler edfScheduler; + private volatile StaticStrideScheduler ssScheduler; // what does volatile mean? WeightedRoundRobinPicker(List list, boolean enableOobLoadReport, float errorUtilizationPenalty) { @@ -273,11 +274,13 @@ final class WeightedRoundRobinPicker extends RoundRobinPicker { this.enableOobLoadReport = enableOobLoadReport; this.errorUtilizationPenalty = errorUtilizationPenalty; updateWeightSS(); + updateWeightEdf(); // handles style error } @Override public PickResult pickSubchannel(PickSubchannelArgs args) { - Subchannel subchannel = list.get(scheduler.pick()); + Subchannel subchannel = list.get(ssScheduler.pickChannel()); + edfScheduler.pick(); // handles style error if (!enableOobLoadReport) { return PickResult.withSubchannel(subchannel, OrcaPerRequestUtil.getInstance().newOrcaClientStreamTracerFactory( @@ -298,7 +301,7 @@ private void updateWeightEdf() { weightedChannelCount++; } } - EdfScheduler scheduler = new EdfScheduler(list.size(), random); + EdfScheduler edfScheduler = new EdfScheduler(list.size(), random); if (weightedChannelCount >= 1) { avgWeight /= 1.0 * weightedChannelCount; } else { @@ -307,9 +310,9 @@ private void updateWeightEdf() { for (int i = 0; i < list.size(); i++) { WrrSubchannel subchannel = (WrrSubchannel) list.get(i); double newWeight = subchannel.getWeight(); - scheduler.add(i, newWeight > 0 ? newWeight : avgWeight); + edfScheduler.add(i, newWeight > 0 ? newWeight : avgWeight); } - this.scheduler = scheduler; + this.edfScheduler = edfScheduler; } // check correctness of behavior @@ -337,8 +340,8 @@ private void updateWeightSS() { newWeights[i] = newWeight > 0 ? (float) newWeight : (float) avgWeight; } - StaticStrideScheduler scheduler = new StaticStrideScheduler(newWeights); - this.scheduler = scheduler; + StaticStrideScheduler ssScheduler = new StaticStrideScheduler(newWeights); + this.ssScheduler = ssScheduler; } @Override @@ -497,7 +500,6 @@ static final class StaticStrideScheduler { // scales weights s.t. max(weights) == K_MAX_WEIGHT, meanWeight is scaled accordingly long[] scaledWeights = new long[numChannels]; - // vectors are deprecated post Java 9? for (int i = 0; i < numChannels; i++) { if (weights[i] == 0) { scaledWeights[i] = meanWeight; @@ -517,11 +519,6 @@ private long nextSequence() { return sequence; } - // is this getter necessary? (is it ever called outside of this class) - public long[] getWeights() { - return this.scaledWeights; - } - // selects index of our next backend server int pickChannel() { while (true) { From 4a820a73adbde60a28cbb2187c33aa1464c06f18 Mon Sep 17 00:00:00 2001 From: Tony An Date: Mon, 12 Jun 2023 16:17:58 -0700 Subject: [PATCH 10/42] quick fix --- .../grpc/xds/WeightedRoundRobinLoadBalancerTest.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index e7bbef2660e..5183c066a0a 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -894,18 +894,6 @@ public void testContainsZeroWeight() { assertThat(picks).isEqualTo(expectedPicks); } - @Test - public void testEqualSchedulers() { - float[] weights = {1.0f, 7.0f, 3.0f, 5.0f, 1.1f}; - StaticStrideScheduler sss1 = new StaticStrideScheduler(weights); - StaticStrideScheduler sss2 = new StaticStrideScheduler(weights); - for (int i = 0; i < 100; i++) { - int firstChannel = sss1.pickChannel(); - int secondChannel = sss2.pickChannel(); - assertThat(firstChannel).isEqualTo(secondChannel); - } - } - private static class FakeSocketAddress extends SocketAddress { final String name; From dc7960a3318efad916bfb2b24068e5911e516f15 Mon Sep 17 00:00:00 2001 From: Tony An Date: Mon, 12 Jun 2023 17:08:26 -0700 Subject: [PATCH 11/42] bug fix attempt --- .../java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index cf2a9d83ee8..0c52717295d 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -121,7 +121,7 @@ private final class UpdateWeightTask implements Runnable { @Override public void run() { if (currentPicker != null && currentPicker instanceof WeightedRoundRobinPicker) { - ((WeightedRoundRobinPicker)currentPicker).updateWeightSS(); + ((WeightedRoundRobinPicker) currentPicker).updateWeightSS(); } weightUpdateTimer = syncContext.schedule(this, config.weightUpdatePeriodNanos, TimeUnit.NANOSECONDS, timeService); @@ -315,7 +315,7 @@ private void updateWeightEdf() { this.edfScheduler = edfScheduler; } - // check correctness of behavior + // verbose work done (requires optimization) private void updateWeightSS() { int weightedChannelCount = 0; double avgWeight = 0; @@ -495,7 +495,7 @@ static final class StaticStrideScheduler { checkArgument(numChannels >= 1, "Couldn't build scheduler: requires at least one weight"); double scalingFactor = K_MAX_WEIGHT / maxWeight; - long meanWeight = + long meanWeight = numZeroWeightChannels == numChannels ? 1 : Math.round(scalingFactor * sumWeight / (numChannels - numZeroWeightChannels)); // scales weights s.t. max(weights) == K_MAX_WEIGHT, meanWeight is scaled accordingly From acd3425499fffa97f0b1bd88e586c7471f6fe227 Mon Sep 17 00:00:00 2001 From: Tony An Date: Tue, 13 Jun 2023 09:35:47 -0700 Subject: [PATCH 12/42] fixed verbose work in updateWeightSS(), float equality --- .../xds/WeightedRoundRobinLoadBalancer.java | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 0c52717295d..ed2fa5fadd6 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -317,27 +317,11 @@ private void updateWeightEdf() { // verbose work done (requires optimization) private void updateWeightSS() { - int weightedChannelCount = 0; - double avgWeight = 0; - for (Subchannel value : list) { - double newWeight = ((WrrSubchannel) value).getWeight(); - if (newWeight > 0) { - avgWeight += newWeight; - weightedChannelCount++; - } - } - - if (weightedChannelCount >= 1) { - avgWeight /= 1.0 * weightedChannelCount; - } else { - avgWeight = 1; - } - float[] newWeights = new float[list.size()]; for (int i = 0; i < list.size(); i++) { WrrSubchannel subchannel = (WrrSubchannel) list.get(i); double newWeight = subchannel.getWeight(); - newWeights[i] = newWeight > 0 ? (float) newWeight : (float) avgWeight; + newWeights[i] = newWeight > 0 ? (float) newWeight : 0.0f; } StaticStrideScheduler ssScheduler = new StaticStrideScheduler(newWeights); @@ -485,7 +469,7 @@ static final class StaticStrideScheduler { double sumWeight = 0; float maxWeight = 0; for (float weight : weights) { - if (weight == 0) { + if (Math.abs(weight - 0.0) < 0.0001) { numZeroWeightChannels++; } sumWeight += weight; From ba1a0b7495a7706a243abd5705ca0126d8cab42b Mon Sep 17 00:00:00 2001 From: Tony An Date: Wed, 14 Jun 2023 10:32:25 -0700 Subject: [PATCH 13/42] fixed pickChannel() by removing kOffset, fixed atomic integer --- .../xds/WeightedRoundRobinLoadBalancer.java | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index ed2fa5fadd6..662d4fea0f6 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -458,10 +458,10 @@ int pick() { @VisibleForTesting static final class StaticStrideScheduler { private final long[] scaledWeights; - private final AtomicInteger sizeDivisor; - private long sequence; + private final int sizeDivisor; + private final AtomicInteger sequence; private static final int K_MAX_WEIGHT = 65535; // uint16? can be uint8 - private static final long UINT32_MAX = 429967295L; // max value for uint32 + private static final int UINT32_MAX = 429967295; // max value for uint32 StaticStrideScheduler(float[] weights) { int numChannels = weights.length; @@ -469,7 +469,7 @@ static final class StaticStrideScheduler { double sumWeight = 0; float maxWeight = 0; for (float weight : weights) { - if (Math.abs(weight - 0.0) < 0.0001) { + if (Math.abs(weight - 0.0) < 0.0001) { // just equal to 0? numZeroWeightChannels++; } sumWeight += weight; @@ -479,13 +479,16 @@ static final class StaticStrideScheduler { checkArgument(numChannels >= 1, "Couldn't build scheduler: requires at least one weight"); double scalingFactor = K_MAX_WEIGHT / maxWeight; + if (numZeroWeightChannels == numChannels) { + System.out.println("ALL 0 WEIGHT CHANNELS"); + } long meanWeight = numZeroWeightChannels == numChannels ? 1 : Math.round(scalingFactor * sumWeight / (numChannels - numZeroWeightChannels)); // scales weights s.t. max(weights) == K_MAX_WEIGHT, meanWeight is scaled accordingly long[] scaledWeights = new long[numChannels]; for (int i = 0; i < numChannels; i++) { - if (weights[i] == 0) { + if (Math.abs(weights[i]) < 0.0001) { // just equal to 0? scaledWeights[i] = meanWeight; } else { scaledWeights[i] = Math.round(weights[i] * scalingFactor); @@ -493,27 +496,27 @@ static final class StaticStrideScheduler { } this.scaledWeights = scaledWeights; - this.sizeDivisor = new AtomicInteger(numChannels); // why not call numChannels or numBackends - this.sequence = (long) (Math.random() * UINT32_MAX); // why not initialize to [0,numChannels) + this.sizeDivisor = numChannels; + // this.sequence = new AtomicInteger((int) (Math.random() * UINT32_MAX)); + this.sequence = new AtomicInteger(0); + // optimization: isn't sequence guaranteed to be in the first generation? + // failing test cases when initialized to non-zero value } - private long nextSequence() { - long sequence = this.sequence; - this.sequence = (this.sequence + 1) % UINT32_MAX; // check wraparound logic - return sequence; + private int nextSequence() { + return this.sequence.getAndUpdate(seq -> ((seq + 1) % UINT32_MAX)); } // selects index of our next backend server int pickChannel() { while (true) { - long sequence = this.nextSequence(); - int backendIndex = (int) sequence % this.sizeDivisor.get(); - long generation = sequence / this.sizeDivisor.get(); + int sequence = this.nextSequence(); + int backendIndex = sequence % this.sizeDivisor; + long generation = sequence / this.sizeDivisor; // is this really that much more efficient than a 2d array? long weight = this.scaledWeights[backendIndex]; - long kOffset = K_MAX_WEIGHT / 2; - long mod = (weight * generation + backendIndex * kOffset) % K_MAX_WEIGHT; - if (mod < K_MAX_WEIGHT - weight) { // review this math + if ((weight * generation) % K_MAX_WEIGHT < K_MAX_WEIGHT - weight) { + // wow how does this work/how was it discovered continue; } return backendIndex; From 9f4a60d66a068421cf812e6eb23d702a205044f6 Mon Sep 17 00:00:00 2001 From: Tony An Date: Wed, 14 Jun 2023 10:44:42 -0700 Subject: [PATCH 14/42] added example test cases --- .../WeightedRoundRobinLoadBalancerTest.java | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 5183c066a0a..d0558977963 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -883,7 +883,7 @@ public void testPicksEqualsWeights() { } @Test - public void testContainsZeroWeight() { + public void testContainsZeroWeightUseMean() { float[] weights = {3.0f, 0.0f, 1.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); int[] expectedPicks = new int[] {3, 2, 1}; @@ -894,6 +894,47 @@ public void testContainsZeroWeight() { assertThat(picks).isEqualTo(expectedPicks); } + @Test + public void testLargestPickedEveryGeneration() { + float[] weights = {1.0f, 2.0f, 3.0f}; + int mean = 2; + StaticStrideScheduler sss = new StaticStrideScheduler(weights); + int largestWeightPickCount = 0; + int kMaxWeight = 65535; + for (int i = 0; i < mean * kMaxWeight; i++) { + if (sss.pickChannel() == mean) { + largestWeightPickCount += 1; + } + } + assertThat(largestWeightPickCount).isEqualTo(kMaxWeight); + } + + @Test + public void testStaticStrideSchedulerGivenExample() { + float[] weights = {10.0f, 20.0f, 30.0f}; + StaticStrideScheduler sss = new StaticStrideScheduler(weights); + assertThat(sss.pickChannel()).isEqualTo(2); + assertThat(sss.pickChannel()).isEqualTo(1); + assertThat(sss.pickChannel()).isEqualTo(2); + assertThat(sss.pickChannel()).isEqualTo(0); + assertThat(sss.pickChannel()).isEqualTo(1); + assertThat(sss.pickChannel()).isEqualTo(2); + } + + @Test + public void testStaticStrideSchedulerGivenExample2() { + float[] weights = {2.0f, 3.0f, 6.0f}; + StaticStrideScheduler sss = new StaticStrideScheduler(weights); + assertThat(sss.pickChannel()).isEqualTo(2); + assertThat(sss.pickChannel()).isEqualTo(1); + assertThat(sss.pickChannel()).isEqualTo(2); + assertThat(sss.pickChannel()).isEqualTo(0); + assertThat(sss.pickChannel()).isEqualTo(2); + assertThat(sss.pickChannel()).isEqualTo(1); + assertThat(sss.pickChannel()).isEqualTo(2); + assertThat(sss.pickChannel()).isEqualTo(2); + } + private static class FakeSocketAddress extends SocketAddress { final String name; From e11e542b02c40932045fb3bb416620d906cad8ff Mon Sep 17 00:00:00 2001 From: Tony An Date: Wed, 14 Jun 2023 10:51:33 -0700 Subject: [PATCH 15/42] types changed from long to int --- .../grpc/xds/WeightedRoundRobinLoadBalancer.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 662d4fea0f6..4138854079c 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -315,7 +315,6 @@ private void updateWeightEdf() { this.edfScheduler = edfScheduler; } - // verbose work done (requires optimization) private void updateWeightSS() { float[] newWeights = new float[list.size()]; for (int i = 0; i < list.size(); i++) { @@ -457,7 +456,7 @@ int pick() { */ @VisibleForTesting static final class StaticStrideScheduler { - private final long[] scaledWeights; + private final int[] scaledWeights; private final int sizeDivisor; private final AtomicInteger sequence; private static final int K_MAX_WEIGHT = 65535; // uint16? can be uint8 @@ -479,19 +478,16 @@ static final class StaticStrideScheduler { checkArgument(numChannels >= 1, "Couldn't build scheduler: requires at least one weight"); double scalingFactor = K_MAX_WEIGHT / maxWeight; - if (numZeroWeightChannels == numChannels) { - System.out.println("ALL 0 WEIGHT CHANNELS"); - } - long meanWeight = numZeroWeightChannels == numChannels ? 1 : - Math.round(scalingFactor * sumWeight / (numChannels - numZeroWeightChannels)); + int meanWeight = numZeroWeightChannels == numChannels ? 1 : + (int) Math.round(scalingFactor * sumWeight / (numChannels - numZeroWeightChannels)); // scales weights s.t. max(weights) == K_MAX_WEIGHT, meanWeight is scaled accordingly - long[] scaledWeights = new long[numChannels]; + int[] scaledWeights = new int[numChannels]; for (int i = 0; i < numChannels; i++) { if (Math.abs(weights[i]) < 0.0001) { // just equal to 0? scaledWeights[i] = meanWeight; } else { - scaledWeights[i] = Math.round(weights[i] * scalingFactor); + scaledWeights[i] = (int) Math.round(weights[i] * scalingFactor); } } From c76cbe5d0811d786455a3334781d2535fdad2d51 Mon Sep 17 00:00:00 2001 From: Tony An Date: Wed, 14 Jun 2023 14:37:04 -0700 Subject: [PATCH 16/42] added sss test cases --- .../WeightedRoundRobinLoadBalancerTest.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index d0558977963..945fe2f2404 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -933,8 +933,71 @@ public void testStaticStrideSchedulerGivenExample2() { assertThat(sss.pickChannel()).isEqualTo(1); assertThat(sss.pickChannel()).isEqualTo(2); assertThat(sss.pickChannel()).isEqualTo(2); + assertThat(sss.pickChannel()).isEqualTo(0); + assertThat(sss.pickChannel()).isEqualTo(1); + assertThat(sss.pickChannel()).isEqualTo(2); + } + + @Test + public void testRebuildSamePicks() { + float[] weights = {3.1f, 1.6f, 2.3f}; + StaticStrideScheduler sss1 = new StaticStrideScheduler(weights); + StaticStrideScheduler sss2 = new StaticStrideScheduler(weights); + for (int i = 0; i < 1000; i++) { + assertThat(sss1.pickChannel()).isEqualTo(sss2.pickChannel()); + } + } + + @Test + public void testStaticStrideSchedulerSimpleExample() { + float[] weights = {2.0f, (float) (10.0 / 3.0), 1.0f}; + StaticStrideScheduler sss = new StaticStrideScheduler(weights); + assertThat(sss.pickChannel()).isEqualTo(1); + assertThat(sss.pickChannel()).isEqualTo(0); + assertThat(sss.pickChannel()).isEqualTo(1); + assertThat(sss.pickChannel()).isEqualTo(1); + assertThat(sss.pickChannel()).isEqualTo(0); + // assertThat(sss.pickChannel()).isEqualTo(2); // edf + assertThat(sss.pickChannel()).isEqualTo(1); // sss + } + + @Test + public void testStaticStrideSchedulerNonIntegers() { + float[] weights = {0.5f, 0.3f, 1.0f}; + StaticStrideScheduler sss = new StaticStrideScheduler(weights); + assertThat(sss.pickChannel()).isEqualTo(2); + assertThat(sss.pickChannel()).isEqualTo(0); + assertThat(sss.pickChannel()).isEqualTo(2); + assertThat(sss.pickChannel()).isEqualTo(2); + // assertThat(sss.pickChannel()).isEqualTo(1); // edf + assertThat(sss.pickChannel()).isEqualTo(0); // sss + assertThat(sss.pickChannel()).isEqualTo(1); + // sss and edf have diff behaviors? } + // @Test + // public void testStaticStrideSchedulerManyIterations() { + // Random random = new Random(); + // double totalWeight = 0; + // int capacity = random.nextInt(10) + 1; + // double[] weights = new double[capacity]; + // EdfScheduler scheduler = new EdfScheduler(capacity, random); + // for (int i = 0; i < capacity; i++) { + // weights[i] = random.nextDouble(); + // scheduler.add(i, weights[i]); + // totalWeight += weights[i]; + // } + // Map pickCount = new HashMap<>(); + // for (int i = 0; i < 1000; i++) { + // int result = scheduler.pick(); + // pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + // } + // for (int i = 0; i < capacity; i++) { + // assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) + // .isAtMost(0.01); + // } + // } + private static class FakeSocketAddress extends SocketAddress { final String name; From 903d2ac12d475888390e0a890911376fd65c72d8 Mon Sep 17 00:00:00 2001 From: Tony An Date: Wed, 14 Jun 2023 15:02:10 -0700 Subject: [PATCH 17/42] added more edge cases (negative/zero weights) --- .../WeightedRoundRobinLoadBalancerTest.java | 79 +++++++++++++------ 1 file changed, 53 insertions(+), 26 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 945fe2f2404..74772494e34 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -894,6 +894,54 @@ public void testContainsZeroWeightUseMean() { assertThat(picks).isEqualTo(expectedPicks); } + @Test + public void testContainsNegativeWeightUseMean() { + float[] weights = {3.0f, -1.0f, 1.0f}; + StaticStrideScheduler sss = new StaticStrideScheduler(weights); + int[] expectedPicks = new int[] {3, 2, 1}; + int[] picks = new int[3]; + for (int i = 0; i < 6; i++) { + picks[sss.pickChannel()] += 1; + } + assertThat(picks).isEqualTo(expectedPicks); + } + + @Test + public void testAllSameWeights() { + float[] weights = {1.0f, 1.0f, 1.0f}; + StaticStrideScheduler sss = new StaticStrideScheduler(weights); + int[] expectedPicks = new int[] {2, 2, 2}; + int[] picks = new int[3]; + for (int i = 0; i < 6; i++) { + picks[sss.pickChannel()] += 1; + } + assertThat(picks).isEqualTo(expectedPicks); + } + + @Test + public void testAllZeroWeightsUseOne() { + float[] weights = {0.0f, 0.0f, 0.0f}; + StaticStrideScheduler sss = new StaticStrideScheduler(weights); + int[] expectedPicks = new int[] {2, 2, 2}; + int[] picks = new int[3]; + for (int i = 0; i < 6; i++) { + picks[sss.pickChannel()] += 1; + } + assertThat(picks).isEqualTo(expectedPicks); + } + + @Test + public void testAllInvalidWeightsUseOne() { + float[] weights = {-1.0f, -0.0f, 0.0f}; + StaticStrideScheduler sss = new StaticStrideScheduler(weights); + int[] expectedPicks = new int[] {2, 2, 2}; + int[] picks = new int[3]; + for (int i = 0; i < 6; i++) { + picks[sss.pickChannel()] += 1; + } + assertThat(picks).isEqualTo(expectedPicks); + } + @Test public void testLargestPickedEveryGeneration() { float[] weights = {1.0f, 2.0f, 3.0f}; @@ -910,7 +958,7 @@ public void testLargestPickedEveryGeneration() { } @Test - public void testStaticStrideSchedulerGivenExample() { + public void testStaticStrideSchedulerGivenExample1() { float[] weights = {10.0f, 20.0f, 30.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); assertThat(sss.pickChannel()).isEqualTo(2); @@ -946,10 +994,10 @@ public void testRebuildSamePicks() { for (int i = 0; i < 1000; i++) { assertThat(sss1.pickChannel()).isEqualTo(sss2.pickChannel()); } - } + } // will fail if sequence is non-deterministic @Test - public void testStaticStrideSchedulerSimpleExample() { + public void testStaticStrideSchedulerNonIntegers1() { float[] weights = {2.0f, (float) (10.0 / 3.0), 1.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); assertThat(sss.pickChannel()).isEqualTo(1); @@ -962,7 +1010,7 @@ public void testStaticStrideSchedulerSimpleExample() { } @Test - public void testStaticStrideSchedulerNonIntegers() { + public void testStaticStrideSchedulerNonIntegers2() { float[] weights = {0.5f, 0.3f, 1.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); assertThat(sss.pickChannel()).isEqualTo(2); @@ -974,29 +1022,8 @@ public void testStaticStrideSchedulerNonIntegers() { assertThat(sss.pickChannel()).isEqualTo(1); // sss and edf have diff behaviors? } + - // @Test - // public void testStaticStrideSchedulerManyIterations() { - // Random random = new Random(); - // double totalWeight = 0; - // int capacity = random.nextInt(10) + 1; - // double[] weights = new double[capacity]; - // EdfScheduler scheduler = new EdfScheduler(capacity, random); - // for (int i = 0; i < capacity; i++) { - // weights[i] = random.nextDouble(); - // scheduler.add(i, weights[i]); - // totalWeight += weights[i]; - // } - // Map pickCount = new HashMap<>(); - // for (int i = 0; i < 1000; i++) { - // int result = scheduler.pick(); - // pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); - // } - // for (int i = 0; i < capacity; i++) { - // assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) - // .isAtMost(0.01); - // } - // } private static class FakeSocketAddress extends SocketAddress { final String name; From d5a06298b2539fa37969f594249292df849670db Mon Sep 17 00:00:00 2001 From: Tony An Date: Wed, 14 Jun 2023 16:07:39 -0700 Subject: [PATCH 18/42] compile fix --- .../xds/WeightedRoundRobinLoadBalancerTest.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 74772494e34..86ca588005a 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -932,7 +932,7 @@ public void testAllZeroWeightsUseOne() { @Test public void testAllInvalidWeightsUseOne() { - float[] weights = {-1.0f, -0.0f, 0.0f}; + float[] weights = {-3.1f, -0.0f, 0.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); int[] expectedPicks = new int[] {2, 2, 2}; int[] picks = new int[3]; @@ -1023,7 +1023,17 @@ public void testStaticStrideSchedulerNonIntegers2() { // sss and edf have diff behaviors? } - + @Test + public void testTwoWeights() { + float[] weights = {1.0f, 2.0f}; + StaticStrideScheduler sss = new StaticStrideScheduler(weights); + assertThat(sss.pickChannel()).isEqualTo(1); + assertThat(sss.pickChannel()).isEqualTo(0); + assertThat(sss.pickChannel()).isEqualTo(1); + assertThat(sss.pickChannel()).isEqualTo(1); + assertThat(sss.pickChannel()).isEqualTo(0); + assertThat(sss.pickChannel()).isEqualTo(1); + } private static class FakeSocketAddress extends SocketAddress { final String name; From 5982115da953de1b383c4873035a5fe3ca9983ab Mon Sep 17 00:00:00 2001 From: Tony An Date: Wed, 14 Jun 2023 16:15:01 -0700 Subject: [PATCH 19/42] more than 3 channels test case --- .../WeightedRoundRobinLoadBalancerTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 86ca588005a..eb8e4ca9634 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -1035,6 +1035,27 @@ public void testTwoWeights() { assertThat(sss.pickChannel()).isEqualTo(1); } + @Test + public void testManyWeights() { + float[] weights = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + StaticStrideScheduler sss = new StaticStrideScheduler(weights); + assertThat(sss.pickChannel()).isEqualTo(4); + assertThat(sss.pickChannel()).isEqualTo(2); + assertThat(sss.pickChannel()).isEqualTo(3); + assertThat(sss.pickChannel()).isEqualTo(4); + assertThat(sss.pickChannel()).isEqualTo(1); + assertThat(sss.pickChannel()).isEqualTo(3); + assertThat(sss.pickChannel()).isEqualTo(4); + assertThat(sss.pickChannel()).isEqualTo(2); + assertThat(sss.pickChannel()).isEqualTo(3); + assertThat(sss.pickChannel()).isEqualTo(4); + assertThat(sss.pickChannel()).isEqualTo(0); + assertThat(sss.pickChannel()).isEqualTo(1); + assertThat(sss.pickChannel()).isEqualTo(2); + assertThat(sss.pickChannel()).isEqualTo(3); + assertThat(sss.pickChannel()).isEqualTo(4); + } + private static class FakeSocketAddress extends SocketAddress { final String name; From 88a8e48df073800c3cabbd91bb902d16206ef712 Mon Sep 17 00:00:00 2001 From: Tony An Date: Thu, 15 Jun 2023 09:19:04 -0700 Subject: [PATCH 20/42] fixed negative case --- .../io/grpc/xds/WeightedRoundRobinLoadBalancer.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 4138854079c..1934da94f4a 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -468,11 +468,12 @@ static final class StaticStrideScheduler { double sumWeight = 0; float maxWeight = 0; for (float weight : weights) { - if (Math.abs(weight - 0.0) < 0.0001) { // just equal to 0? + if (weight < 0.0001) { // just equal to 0? numZeroWeightChannels++; + } else { + sumWeight += weight; + maxWeight = Math.max(weight, maxWeight); } - sumWeight += weight; - maxWeight = Math.max(weight, maxWeight); } checkArgument(numChannels >= 1, "Couldn't build scheduler: requires at least one weight"); @@ -484,7 +485,7 @@ static final class StaticStrideScheduler { // scales weights s.t. max(weights) == K_MAX_WEIGHT, meanWeight is scaled accordingly int[] scaledWeights = new int[numChannels]; for (int i = 0; i < numChannels; i++) { - if (Math.abs(weights[i]) < 0.0001) { // just equal to 0? + if (weights[i] < 0.0001) { // just equal to 0? scaledWeights[i] = meanWeight; } else { scaledWeights[i] = (int) Math.round(weights[i] * scalingFactor); @@ -512,7 +513,7 @@ int pickChannel() { // is this really that much more efficient than a 2d array? long weight = this.scaledWeights[backendIndex]; if ((weight * generation) % K_MAX_WEIGHT < K_MAX_WEIGHT - weight) { - // wow how does this work/how was it discovered + // how does this work/how was it discovered continue; } return backendIndex; From f9cae20b2c38814d970c065ce3c20fd4c3647be9 Mon Sep 17 00:00:00 2001 From: Tony An Date: Thu, 15 Jun 2023 11:05:02 -0700 Subject: [PATCH 21/42] end to end test cases --- .../WeightedRoundRobinLoadBalancerTest.java | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index eb8e4ca9634..bf35875b52c 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -339,7 +339,7 @@ weightedSubchannel3.new OrcaReportListener(weightedConfig.errorUtilizationPenalt } @Test - public void pickByWeight_LargeWeight() { + public void pickByWeight_largeWeight() { MetricReport report1 = InternalCallMetricRecorder.createMetricReport( 0.1, 0, 0.1, 999, 0, new HashMap<>(), new HashMap<>()); MetricReport report2 = InternalCallMetricRecorder.createMetricReport( @@ -383,6 +383,51 @@ public void pickByWeight_largeWeight_withEps_defaultErrorUtilizationPenalty() { weight3 / totalWeight); } + @Test + public void pickByWeight_lowWeight() { + MetricReport report1 = InternalCallMetricRecorder.createMetricReport( + 0.22, 0, 0.1, 122, 0, new HashMap<>(), new HashMap<>()); + MetricReport report2 = InternalCallMetricRecorder.createMetricReport( + 0.7, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>()); + MetricReport report3 = InternalCallMetricRecorder.createMetricReport( + 0.86, 0, 0.1, 80, 0, new HashMap<>(), new HashMap<>()); + double totalWeight = 122 / 0.22 + 1 / 0.7 + 80 / 0.86; + + pickByWeight(report1, report2, report3, 122 / 0.22 / totalWeight, 1 / 0.7 / totalWeight, + 80 / 0.86 / totalWeight); + } + + @Test + public void pickByWeight_lowWeight_useApplicationUtilization() { + MetricReport report1 = InternalCallMetricRecorder.createMetricReport( + 0.22, 0.2, 0.1, 122, 0, new HashMap<>(), new HashMap<>()); + MetricReport report2 = InternalCallMetricRecorder.createMetricReport( + 0.7, 0.5, 0.1, 1, 0, new HashMap<>(), new HashMap<>()); + MetricReport report3 = InternalCallMetricRecorder.createMetricReport( + 0.86, 0.33, 0.1, 80, 0, new HashMap<>(), new HashMap<>()); + double totalWeight = 122 / 0.2 + 1 / 0.5 + 80 / 0.33; + + pickByWeight(report1, report2, report3, 122 / 0.2 / totalWeight, 1 / 0.5 / totalWeight, + 80 / 0.33 / totalWeight); + } + + @Test + public void pickByWeight_lowWeight_withEps_defaultErrorUtilizationPenalty() { + MetricReport report1 = InternalCallMetricRecorder.createMetricReport( + 0.22, 0, 0.1, 122, 5, new HashMap<>(), new HashMap<>()); + MetricReport report2 = InternalCallMetricRecorder.createMetricReport( + 0.7, 0, 0.1, 1, 11, new HashMap<>(), new HashMap<>()); + MetricReport report3 = InternalCallMetricRecorder.createMetricReport( + 0.86, 0, 0.1, 80, 1, new HashMap<>(), new HashMap<>()); + double weight1 = 122 / (0.22 + 5 / 122F * weightedConfig.errorUtilizationPenalty); + double weight2 = 1 / (0.7 + 11 / 1F * weightedConfig.errorUtilizationPenalty); + double weight3 = 80 / (0.86 + 1 / 80F * weightedConfig.errorUtilizationPenalty); + double totalWeight = weight1 + weight2 + weight3; + + pickByWeight(report1, report2, report3, weight1 / totalWeight, weight2 / totalWeight, + weight3 / totalWeight); + } + @Test public void pickByWeight_normalWeight() { MetricReport report1 = InternalCallMetricRecorder.createMetricReport( From dba07784eefa510b2a3847c91ea119bfa406135a Mon Sep 17 00:00:00 2001 From: Tony An Date: Thu, 15 Jun 2023 12:58:02 -0700 Subject: [PATCH 22/42] fixed sequence --- .../xds/WeightedRoundRobinLoadBalancer.java | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 1934da94f4a..d0bc2157135 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -48,7 +48,7 @@ import java.util.Random; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import java.util.logging.Logger; @@ -260,7 +260,7 @@ final class WeightedRoundRobinPicker extends RoundRobinPicker { private final boolean enableOobLoadReport; private final float errorUtilizationPenalty; private volatile EdfScheduler edfScheduler; - private volatile StaticStrideScheduler ssScheduler; // what does volatile mean? + private volatile StaticStrideScheduler ssScheduler; WeightedRoundRobinPicker(List list, boolean enableOobLoadReport, float errorUtilizationPenalty) { @@ -458,12 +458,13 @@ int pick() { static final class StaticStrideScheduler { private final int[] scaledWeights; private final int sizeDivisor; - private final AtomicInteger sequence; - private static final int K_MAX_WEIGHT = 65535; // uint16? can be uint8 - private static final int UINT32_MAX = 429967295; // max value for uint32 + private final AtomicLong sequence; + private static final int K_MAX_WEIGHT = 65535; + private static final long UINT32_MAX = 4294967295L; StaticStrideScheduler(float[] weights) { int numChannels = weights.length; + checkArgument(numChannels >= 1, "Couldn't build scheduler: requires at least one weight"); int numZeroWeightChannels = 0; double sumWeight = 0; float maxWeight = 0; @@ -476,8 +477,6 @@ static final class StaticStrideScheduler { } } - checkArgument(numChannels >= 1, "Couldn't build scheduler: requires at least one weight"); - double scalingFactor = K_MAX_WEIGHT / maxWeight; int meanWeight = numZeroWeightChannels == numChannels ? 1 : (int) Math.round(scalingFactor * sumWeight / (numChannels - numZeroWeightChannels)); @@ -494,29 +493,27 @@ static final class StaticStrideScheduler { this.scaledWeights = scaledWeights; this.sizeDivisor = numChannels; - // this.sequence = new AtomicInteger((int) (Math.random() * UINT32_MAX)); - this.sequence = new AtomicInteger(0); + // this.sequence = new AtomicLong((long) (Math.random() * UINT32_MAX)); + this.sequence = new AtomicLong(0); // optimization: isn't sequence guaranteed to be in the first generation? // failing test cases when initialized to non-zero value } - private int nextSequence() { + private long nextSequence() { return this.sequence.getAndUpdate(seq -> ((seq + 1) % UINT32_MAX)); } - // selects index of our next backend server - int pickChannel() { + /** Selects index of next backend server */ + int pickChannel() { while (true) { - int sequence = this.nextSequence(); - int backendIndex = sequence % this.sizeDivisor; + long sequence = this.nextSequence(); + long backendIndex = sequence % this.sizeDivisor; long generation = sequence / this.sizeDivisor; - // is this really that much more efficient than a 2d array? - long weight = this.scaledWeights[backendIndex]; + long weight = this.scaledWeights[(int) backendIndex]; if ((weight * generation) % K_MAX_WEIGHT < K_MAX_WEIGHT - weight) { - // how does this work/how was it discovered continue; } - return backendIndex; + return (int) backendIndex; } } } From 46a463e511f2f82b37839ac096a8111301fdef4d Mon Sep 17 00:00:00 2001 From: Tony An Date: Thu, 15 Jun 2023 12:59:14 -0700 Subject: [PATCH 23/42] ident fix --- .../main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index d0bc2157135..9880edffa58 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -503,8 +503,8 @@ private long nextSequence() { return this.sequence.getAndUpdate(seq -> ((seq + 1) % UINT32_MAX)); } - /** Selects index of next backend server */ - int pickChannel() { + /** Selects index of next backend server */ + int pickChannel() { while (true) { long sequence = this.nextSequence(); long backendIndex = sequence % this.sizeDivisor; From 142b499fe608a2fb00311c9df99b493b44e540b2 Mon Sep 17 00:00:00 2001 From: Tony An Date: Thu, 15 Jun 2023 14:52:40 -0700 Subject: [PATCH 24/42] replaced deterministic test cases with ones with many iterations, deleted edf cases --- .../WeightedRoundRobinLoadBalancerTest.java | 200 ++++++++---------- 1 file changed, 91 insertions(+), 109 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index bf35875b52c..e5313e9dc88 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -52,7 +52,6 @@ import io.grpc.internal.FakeClock; import io.grpc.services.InternalCallMetricRecorder; import io.grpc.services.MetricReport; -import io.grpc.xds.WeightedRoundRobinLoadBalancer.EdfScheduler; import io.grpc.xds.WeightedRoundRobinLoadBalancer.StaticStrideScheduler; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinLoadBalancerConfig; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinPicker; @@ -220,8 +219,6 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt InternalCallMetricRecorder.createMetricReport( 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(1); - assertThat(weightedPicker.pickSubchannel(mockArgs) - .getSubchannel()).isEqualTo(weightedSubchannel1); assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); weightedConfig = WeightedRoundRobinLoadBalancerConfig.newBuilder() .setWeightUpdatePeriodNanos(500_000_000L) //.5s @@ -619,8 +616,6 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt InternalCallMetricRecorder.createMetricReport( 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(1); - assertThat(weightedPicker.pickSubchannel(mockArgs) - .getSubchannel()).isEqualTo(weightedSubchannel1); assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); weightedConfig = WeightedRoundRobinLoadBalancerConfig.newBuilder() .setWeightUpdatePeriodNanos(500_000_000L) //.5s @@ -637,8 +632,6 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); //timer fires, new weight updated assertThat(fakeClock.forwardTime(500, TimeUnit.MILLISECONDS)).isEqualTo(1); - assertThat(weightedPicker.pickSubchannel(mockArgs) - .getSubchannel()).isEqualTo(weightedSubchannel2); } @Test @@ -867,37 +860,6 @@ public void run() { .isAtMost(0.001); } - @Test - public void edfScheduler() { - Random random = new Random(); - double totalWeight = 0; - int capacity = random.nextInt(10) + 1; - double[] weights = new double[capacity]; - EdfScheduler scheduler = new EdfScheduler(capacity, random); - for (int i = 0; i < capacity; i++) { - weights[i] = random.nextDouble(); - scheduler.add(i, weights[i]); - totalWeight += weights[i]; - } - Map pickCount = new HashMap<>(); - for (int i = 0; i < 1000; i++) { - int result = scheduler.pick(); - pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); - } - for (int i = 0; i < capacity; i++) { - assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) - .isAtMost(0.01); - } - } - - @Test - public void edsScheduler_sameWeight() { - EdfScheduler scheduler = new EdfScheduler(2, new FakeRandom()); - scheduler.add(0, 0.5); - scheduler.add(1, 0.5); - assertThat(scheduler.pick()).isEqualTo(0); - } - @Test(expected = NullPointerException.class) public void wrrConfig_TimeValueNonNull() { WeightedRoundRobinLoadBalancerConfig.newBuilder().setBlackoutPeriodNanos((Long) null); @@ -912,7 +874,7 @@ public void wrrConfig_BooleanValueNonNull() { public void emptyWeights() { float[] weights = {}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); - sss.pickChannel(); + sss.pick(); } @Test @@ -922,7 +884,7 @@ public void testPicksEqualsWeights() { int[] expectedPicks = new int[] {1, 2, 3}; int[] picks = new int[3]; for (int i = 0; i < 6; i++) { - picks[sss.pickChannel()] += 1; + picks[sss.pick()] += 1; } assertThat(picks).isEqualTo(expectedPicks); } @@ -934,7 +896,7 @@ public void testContainsZeroWeightUseMean() { int[] expectedPicks = new int[] {3, 2, 1}; int[] picks = new int[3]; for (int i = 0; i < 6; i++) { - picks[sss.pickChannel()] += 1; + picks[sss.pick()] += 1; } assertThat(picks).isEqualTo(expectedPicks); } @@ -946,7 +908,7 @@ public void testContainsNegativeWeightUseMean() { int[] expectedPicks = new int[] {3, 2, 1}; int[] picks = new int[3]; for (int i = 0; i < 6; i++) { - picks[sss.pickChannel()] += 1; + picks[sss.pick()] += 1; } assertThat(picks).isEqualTo(expectedPicks); } @@ -958,7 +920,7 @@ public void testAllSameWeights() { int[] expectedPicks = new int[] {2, 2, 2}; int[] picks = new int[3]; for (int i = 0; i < 6; i++) { - picks[sss.pickChannel()] += 1; + picks[sss.pick()] += 1; } assertThat(picks).isEqualTo(expectedPicks); } @@ -970,7 +932,7 @@ public void testAllZeroWeightsUseOne() { int[] expectedPicks = new int[] {2, 2, 2}; int[] picks = new int[3]; for (int i = 0; i < 6; i++) { - picks[sss.pickChannel()] += 1; + picks[sss.pick()] += 1; } assertThat(picks).isEqualTo(expectedPicks); } @@ -982,7 +944,7 @@ public void testAllInvalidWeightsUseOne() { int[] expectedPicks = new int[] {2, 2, 2}; int[] picks = new int[3]; for (int i = 0; i < 6; i++) { - picks[sss.pickChannel()] += 1; + picks[sss.pick()] += 1; } assertThat(picks).isEqualTo(expectedPicks); } @@ -995,7 +957,7 @@ public void testLargestPickedEveryGeneration() { int largestWeightPickCount = 0; int kMaxWeight = 65535; for (int i = 0; i < mean * kMaxWeight; i++) { - if (sss.pickChannel() == mean) { + if (sss.pick() == mean) { largestWeightPickCount += 1; } } @@ -1006,99 +968,119 @@ public void testLargestPickedEveryGeneration() { public void testStaticStrideSchedulerGivenExample1() { float[] weights = {10.0f, 20.0f, 30.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); - assertThat(sss.pickChannel()).isEqualTo(2); - assertThat(sss.pickChannel()).isEqualTo(1); - assertThat(sss.pickChannel()).isEqualTo(2); - assertThat(sss.pickChannel()).isEqualTo(0); - assertThat(sss.pickChannel()).isEqualTo(1); - assertThat(sss.pickChannel()).isEqualTo(2); + Random random = new Random(); + double totalWeight = 60; + Map pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + int result = sss.pick(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + for (int i = 0; i < 3; i++) { + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) + .isAtMost(0.01); + } } @Test public void testStaticStrideSchedulerGivenExample2() { float[] weights = {2.0f, 3.0f, 6.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); - assertThat(sss.pickChannel()).isEqualTo(2); - assertThat(sss.pickChannel()).isEqualTo(1); - assertThat(sss.pickChannel()).isEqualTo(2); - assertThat(sss.pickChannel()).isEqualTo(0); - assertThat(sss.pickChannel()).isEqualTo(2); - assertThat(sss.pickChannel()).isEqualTo(1); - assertThat(sss.pickChannel()).isEqualTo(2); - assertThat(sss.pickChannel()).isEqualTo(2); - assertThat(sss.pickChannel()).isEqualTo(0); - assertThat(sss.pickChannel()).isEqualTo(1); - assertThat(sss.pickChannel()).isEqualTo(2); - } - - @Test - public void testRebuildSamePicks() { - float[] weights = {3.1f, 1.6f, 2.3f}; - StaticStrideScheduler sss1 = new StaticStrideScheduler(weights); - StaticStrideScheduler sss2 = new StaticStrideScheduler(weights); + Random random = new Random(); + double totalWeight = 11; + Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { - assertThat(sss1.pickChannel()).isEqualTo(sss2.pickChannel()); + int result = sss.pick(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } - } // will fail if sequence is non-deterministic + for (int i = 0; i < 3; i++) { + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) + .isAtMost(0.01); + } + } @Test public void testStaticStrideSchedulerNonIntegers1() { float[] weights = {2.0f, (float) (10.0 / 3.0), 1.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); - assertThat(sss.pickChannel()).isEqualTo(1); - assertThat(sss.pickChannel()).isEqualTo(0); - assertThat(sss.pickChannel()).isEqualTo(1); - assertThat(sss.pickChannel()).isEqualTo(1); - assertThat(sss.pickChannel()).isEqualTo(0); - // assertThat(sss.pickChannel()).isEqualTo(2); // edf - assertThat(sss.pickChannel()).isEqualTo(1); // sss + Random random = new Random(); + double totalWeight = 2 + 10.0 / 3.0 + 1.0; + Map pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + int result = sss.pick(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + for (int i = 0; i < 3; i++) { + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) + .isAtMost(0.01); + } } @Test public void testStaticStrideSchedulerNonIntegers2() { float[] weights = {0.5f, 0.3f, 1.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); - assertThat(sss.pickChannel()).isEqualTo(2); - assertThat(sss.pickChannel()).isEqualTo(0); - assertThat(sss.pickChannel()).isEqualTo(2); - assertThat(sss.pickChannel()).isEqualTo(2); - // assertThat(sss.pickChannel()).isEqualTo(1); // edf - assertThat(sss.pickChannel()).isEqualTo(0); // sss - assertThat(sss.pickChannel()).isEqualTo(1); - // sss and edf have diff behaviors? + Random random = new Random(); + double totalWeight = 1.8; + Map pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + int result = sss.pick(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + for (int i = 0; i < 3; i++) { + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) + .isAtMost(0.01); + } } @Test public void testTwoWeights() { float[] weights = {1.0f, 2.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); - assertThat(sss.pickChannel()).isEqualTo(1); - assertThat(sss.pickChannel()).isEqualTo(0); - assertThat(sss.pickChannel()).isEqualTo(1); - assertThat(sss.pickChannel()).isEqualTo(1); - assertThat(sss.pickChannel()).isEqualTo(0); - assertThat(sss.pickChannel()).isEqualTo(1); + Random random = new Random(); + double totalWeight = 3; + Map pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + int result = sss.pick(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + for (int i = 0; i < 2; i++) { + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) + .isAtMost(0.01); + } } @Test - public void testManyWeights() { + public void testManyWeights1() { float[] weights = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); - assertThat(sss.pickChannel()).isEqualTo(4); - assertThat(sss.pickChannel()).isEqualTo(2); - assertThat(sss.pickChannel()).isEqualTo(3); - assertThat(sss.pickChannel()).isEqualTo(4); - assertThat(sss.pickChannel()).isEqualTo(1); - assertThat(sss.pickChannel()).isEqualTo(3); - assertThat(sss.pickChannel()).isEqualTo(4); - assertThat(sss.pickChannel()).isEqualTo(2); - assertThat(sss.pickChannel()).isEqualTo(3); - assertThat(sss.pickChannel()).isEqualTo(4); - assertThat(sss.pickChannel()).isEqualTo(0); - assertThat(sss.pickChannel()).isEqualTo(1); - assertThat(sss.pickChannel()).isEqualTo(2); - assertThat(sss.pickChannel()).isEqualTo(3); - assertThat(sss.pickChannel()).isEqualTo(4); + Random random = new Random(); + double totalWeight = 15; + Map pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + int result = sss.pick(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + for (int i = 0; i < 3; i++) { + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) + .isAtMost(0.01); + } + } + + @Test + public void testManyComplexWeights2() { + float[] weights = {1.2f, 2.4f, 222.56f, 0f, 15.0f, 226342.0f, 5123.0f, 0.0001f}; + StaticStrideScheduler sss = new StaticStrideScheduler(weights); + Random random = new Random(); + double totalWeight = 1.2 + 2.4 + 222.56 + 15.0 + 226342.0 + 5123.0 + 0.0001; + Map pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + int result = sss.pick(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + for (int i = 0; i < 8; i++) { + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 10000.0 - weights[i] / totalWeight) ) + .isAtMost(5); + } } private static class FakeSocketAddress extends SocketAddress { From 55233372b665c730aaaa6b19e18733f927b629e4 Mon Sep 17 00:00:00 2001 From: Tony An Date: Thu, 15 Jun 2023 14:53:46 -0700 Subject: [PATCH 25/42] fixed sequence, added comments, deleted edf --- .../xds/WeightedRoundRobinLoadBalancer.java | 170 +++--------------- 1 file changed, 21 insertions(+), 149 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 9880edffa58..3e7b7eb8ce7 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -44,7 +44,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.PriorityQueue; import java.util.Random; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -121,7 +120,7 @@ private final class UpdateWeightTask implements Runnable { @Override public void run() { if (currentPicker != null && currentPicker instanceof WeightedRoundRobinPicker) { - ((WeightedRoundRobinPicker) currentPicker).updateWeightSS(); + ((WeightedRoundRobinPicker) currentPicker).updateWeight(); } weightUpdateTimer = syncContext.schedule(this, config.weightUpdatePeriodNanos, TimeUnit.NANOSECONDS, timeService); @@ -259,7 +258,6 @@ final class WeightedRoundRobinPicker extends RoundRobinPicker { new HashMap<>(); private final boolean enableOobLoadReport; private final float errorUtilizationPenalty; - private volatile EdfScheduler edfScheduler; private volatile StaticStrideScheduler ssScheduler; WeightedRoundRobinPicker(List list, boolean enableOobLoadReport, @@ -273,14 +271,12 @@ final class WeightedRoundRobinPicker extends RoundRobinPicker { } this.enableOobLoadReport = enableOobLoadReport; this.errorUtilizationPenalty = errorUtilizationPenalty; - updateWeightSS(); - updateWeightEdf(); // handles style error + updateWeight(); } @Override public PickResult pickSubchannel(PickSubchannelArgs args) { - Subchannel subchannel = list.get(ssScheduler.pickChannel()); - edfScheduler.pick(); // handles style error + Subchannel subchannel = list.get(ssScheduler.pick()); if (!enableOobLoadReport) { return PickResult.withSubchannel(subchannel, OrcaPerRequestUtil.getInstance().newOrcaClientStreamTracerFactory( @@ -290,32 +286,8 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { return PickResult.withSubchannel(subchannel); } } - - private void updateWeightEdf() { - int weightedChannelCount = 0; - double avgWeight = 0; - for (Subchannel value : list) { - double newWeight = ((WrrSubchannel) value).getWeight(); - if (newWeight > 0) { - avgWeight += newWeight; - weightedChannelCount++; - } - } - EdfScheduler edfScheduler = new EdfScheduler(list.size(), random); - if (weightedChannelCount >= 1) { - avgWeight /= 1.0 * weightedChannelCount; - } else { - avgWeight = 1; - } - for (int i = 0; i < list.size(); i++) { - WrrSubchannel subchannel = (WrrSubchannel) list.get(i); - double newWeight = subchannel.getWeight(); - edfScheduler.add(i, newWeight > 0 ? newWeight : avgWeight); - } - this.edfScheduler = edfScheduler; - } - private void updateWeightSS() { + private void updateWeight() { float[] newWeights = new float[list.size()]; for (int i = 0; i < list.size(); i++) { WrrSubchannel subchannel = (WrrSubchannel) list.get(i); @@ -356,111 +328,27 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { } } - /** - * The earliest deadline first implementation in which each object is - * chosen deterministically and periodically with frequency proportional to its weight. - * - *

Specifically, each object added to chooser is given a deadline equal to the multiplicative - * inverse of its weight. The place of each object in its deadline is tracked, and each call to - * choose returns the object with the least remaining time in its deadline. - * (Ties are broken by the order in which the children were added to the chooser.) The deadline - * advances by the multiplicative inverse of the object's weight. - * For example, if items A and B are added with weights 0.5 and 0.2, successive chooses return: - * - *

    - *
  • In the first call, the deadlines are A=2 (1/0.5) and B=5 (1/0.2), so A is returned. - * The deadline of A is updated to 4. - *
  • Next, the remaining deadlines are A=4 and B=5, so A is returned. The deadline of A (2) is - * updated to A=6. - *
  • Remaining deadlines are A=6 and B=5, so B is returned. The deadline of B is updated with - * with B=10. - *
  • Remaining deadlines are A=6 and B=10, so A is returned. The deadline of A is updated with - * A=8. - *
  • Remaining deadlines are A=8 and B=10, so A is returned. The deadline of A is updated with - * A=10. - *
  • Remaining deadlines are A=10 and B=10, so A is returned. The deadline of A is updated - * with A=12. - *
  • Remaining deadlines are A=12 and B=10, so B is returned. The deadline of B is updated - * with B=15. - *
  • etc. - *
- * - *

In short: the entry with the highest weight is preferred. - * - *

    - *
  • add() - O(lg n) - *
  • pick() - O(lg n) - *
- * - */ - @VisibleForTesting - static final class EdfScheduler { - private final PriorityQueue prioQueue; - - /** - * Weights below this value will be upped to this minimum weight. - */ - private static final double MINIMUM_WEIGHT = 0.0001; - - private final Object lock = new Object(); - - private final Random random; - - /** - * Use the item's deadline as the order in the priority queue. If the deadlines are the same, - * use the index. Index should be unique. - */ - EdfScheduler(int initialCapacity, Random random) { - this.prioQueue = new PriorityQueue(initialCapacity, (o1, o2) -> { - if (o1.deadline == o2.deadline) { - return Integer.compare(o1.index, o2.index); - } else { - return Double.compare(o1.deadline, o2.deadline); - } - }); - this.random = random; - } - - /** - * Adds the item in the scheduler. This is not thread safe. - * - * @param index The field {@link ObjectState#index} to be added - * @param weight positive weight for the added object - */ - void add(int index, double weight) { - checkArgument(weight > 0.0, "Weights need to be positive."); - ObjectState state = new ObjectState(Math.max(weight, MINIMUM_WEIGHT), index); - // Randomize the initial deadline. - state.deadline = random.nextDouble() * (1 / state.weight); - prioQueue.add(state); - } - - /** - * Picks the next WRR object. - */ - int pick() { - synchronized (lock) { - ObjectState minObject = prioQueue.remove(); - minObject.deadline += 1.0 / minObject.weight; - prioQueue.add(minObject); - return minObject.index; - } - } - } - /* - * Implementation of Static Stride Scheduler - * Replaces EDFScheduler - * + * Implementation of Static Stride Scheduler, replaces EDFScheduler. + *

+ * The Static Stride Scheduler works by iterating through the list of subchannel weights + * and using modular arithmetic to evenly distribute picks and skips, favoring + * entries with the highest weight. + *

* go/static-stride-scheduler + *

+ * + *

    + *
  • nextSequence() - O(1) + *
  • pick() - O(n) */ @VisibleForTesting static final class StaticStrideScheduler { private final int[] scaledWeights; private final int sizeDivisor; - private final AtomicLong sequence; private static final int K_MAX_WEIGHT = 65535; private static final long UINT32_MAX = 4294967295L; + private final AtomicLong sequence = new AtomicLong((long) (Math.random() * UINT32_MAX)); StaticStrideScheduler(float[] weights) { int numChannels = weights.length; @@ -469,7 +357,7 @@ static final class StaticStrideScheduler { double sumWeight = 0; float maxWeight = 0; for (float weight : weights) { - if (weight < 0.0001) { // just equal to 0? + if (weight < 0.0001) { numZeroWeightChannels++; } else { sumWeight += weight; @@ -484,7 +372,7 @@ static final class StaticStrideScheduler { // scales weights s.t. max(weights) == K_MAX_WEIGHT, meanWeight is scaled accordingly int[] scaledWeights = new int[numChannels]; for (int i = 0; i < numChannels; i++) { - if (weights[i] < 0.0001) { // just equal to 0? + if (weights[i] < 0.0001) { scaledWeights[i] = meanWeight; } else { scaledWeights[i] = (int) Math.round(weights[i] * scalingFactor); @@ -493,18 +381,15 @@ static final class StaticStrideScheduler { this.scaledWeights = scaledWeights; this.sizeDivisor = numChannels; - // this.sequence = new AtomicLong((long) (Math.random() * UINT32_MAX)); - this.sequence = new AtomicLong(0); - // optimization: isn't sequence guaranteed to be in the first generation? - // failing test cases when initialized to non-zero value } + /** Returns the next sequence number and increases sequence with wraparound. */ private long nextSequence() { return this.sequence.getAndUpdate(seq -> ((seq + 1) % UINT32_MAX)); } - /** Selects index of next backend server */ - int pickChannel() { + /** Selects index of next backend server. */ + int pick() { while (true) { long sequence = this.nextSequence(); long backendIndex = sequence % this.sizeDivisor; @@ -518,19 +403,6 @@ int pickChannel() { } } - /** Holds the state of the object. */ - @VisibleForTesting - static class ObjectState { - private final double weight; - private final int index; - private volatile double deadline; - - ObjectState(double weight, int index) { - this.weight = weight; - this.index = index; - } - } - static final class WeightedRoundRobinLoadBalancerConfig { final long blackoutPeriodNanos; final long weightExpirationPeriodNanos; From 365ba8dd058581b5c0ab27d505526dd330a35bfd Mon Sep 17 00:00:00 2001 From: Tony An Date: Thu, 15 Jun 2023 15:36:04 -0700 Subject: [PATCH 26/42] adjusted constructor in tests --- .../WeightedRoundRobinLoadBalancerTest.java | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index e5313e9dc88..e377b40b144 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -63,7 +63,6 @@ import java.util.List; import java.util.Map; import java.util.Queue; -import java.util.Random; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CyclicBarrier; @@ -174,8 +173,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { return subchannel; } }); - wrr = new WeightedRoundRobinLoadBalancer(helper, fakeClock.getDeadlineTicker(), - new FakeRandom()); + wrr = new WeightedRoundRobinLoadBalancer(helper, fakeClock.getDeadlineTicker()); } @Test @@ -968,7 +966,6 @@ public void testLargestPickedEveryGeneration() { public void testStaticStrideSchedulerGivenExample1() { float[] weights = {10.0f, 20.0f, 30.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); - Random random = new Random(); double totalWeight = 60; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -985,7 +982,6 @@ public void testStaticStrideSchedulerGivenExample1() { public void testStaticStrideSchedulerGivenExample2() { float[] weights = {2.0f, 3.0f, 6.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); - Random random = new Random(); double totalWeight = 11; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -1002,7 +998,6 @@ public void testStaticStrideSchedulerGivenExample2() { public void testStaticStrideSchedulerNonIntegers1() { float[] weights = {2.0f, (float) (10.0 / 3.0), 1.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); - Random random = new Random(); double totalWeight = 2 + 10.0 / 3.0 + 1.0; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -1019,7 +1014,6 @@ public void testStaticStrideSchedulerNonIntegers1() { public void testStaticStrideSchedulerNonIntegers2() { float[] weights = {0.5f, 0.3f, 1.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); - Random random = new Random(); double totalWeight = 1.8; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -1036,7 +1030,6 @@ public void testStaticStrideSchedulerNonIntegers2() { public void testTwoWeights() { float[] weights = {1.0f, 2.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); - Random random = new Random(); double totalWeight = 3; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -1053,7 +1046,6 @@ public void testTwoWeights() { public void testManyWeights1() { float[] weights = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); - Random random = new Random(); double totalWeight = 15; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -1070,7 +1062,6 @@ public void testManyWeights1() { public void testManyComplexWeights2() { float[] weights = {1.2f, 2.4f, 222.56f, 0f, 15.0f, 226342.0f, 5123.0f, 0.0001f}; StaticStrideScheduler sss = new StaticStrideScheduler(weights); - Random random = new Random(); double totalWeight = 1.2 + 2.4 + 222.56 + 15.0 + 226342.0 + 5123.0 + 0.0001; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -1094,12 +1085,4 @@ private static class FakeSocketAddress extends SocketAddress { return "FakeSocketAddress-" + name; } } - - private static class FakeRandom extends Random { - @Override - public double nextDouble() { - // return constant value to disable init deadline randomization in the scheduler - return 0.322023; - } - } } From 442bea81d969d3b3baf49b65f188e542b8c5dfd1 Mon Sep 17 00:00:00 2001 From: Tony An Date: Thu, 15 Jun 2023 15:36:48 -0700 Subject: [PATCH 27/42] fixed constructor to remove random --- .../io/grpc/xds/WeightedRoundRobinLoadBalancer.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 3e7b7eb8ce7..ff882bfb885 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -44,7 +44,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Random; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @@ -65,15 +64,10 @@ final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { private final ScheduledExecutorService timeService; private ScheduledHandle weightUpdateTimer; private final Runnable updateWeightTask; - private final Random random; private final long infTime; private final Ticker ticker; - public WeightedRoundRobinLoadBalancer(Helper helper, Ticker ticker) { - this(new WrrHelper(OrcaOobUtil.newOrcaReportingHelper(helper)), ticker, new Random()); - } - - public WeightedRoundRobinLoadBalancer(WrrHelper helper, Ticker ticker, Random random) { + public WeightedRoundRobinLoadBalancer(WrrHelper helper, Ticker ticker) { super(helper); helper.setLoadBalancer(this); this.ticker = checkNotNull(ticker, "ticker"); @@ -81,13 +75,12 @@ public WeightedRoundRobinLoadBalancer(WrrHelper helper, Ticker ticker, Random ra this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService"); this.updateWeightTask = new UpdateWeightTask(); - this.random = random; log.log(Level.FINE, "weighted_round_robin LB created"); } @VisibleForTesting - WeightedRoundRobinLoadBalancer(Helper helper, Ticker ticker, Random random) { - this(new WrrHelper(OrcaOobUtil.newOrcaReportingHelper(helper)), ticker, random); + WeightedRoundRobinLoadBalancer(Helper helper, Ticker ticker) { + this(new WrrHelper(OrcaOobUtil.newOrcaReportingHelper(helper)), ticker); } @Override From 2a0e489386697115edab18f99224747391d276a0 Mon Sep 17 00:00:00 2001 From: Tony An Date: Fri, 16 Jun 2023 11:53:57 -0700 Subject: [PATCH 28/42] optimized for scaling, fixed next sequence --- .../xds/WeightedRoundRobinLoadBalancer.java | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index ff882bfb885..4804101972d 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -46,7 +46,7 @@ import java.util.Map; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; @@ -340,27 +340,29 @@ static final class StaticStrideScheduler { private final int[] scaledWeights; private final int sizeDivisor; private static final int K_MAX_WEIGHT = 65535; - private static final long UINT32_MAX = 4294967295L; - private final AtomicLong sequence = new AtomicLong((long) (Math.random() * UINT32_MAX)); + private static final long UINT32_MAX = 0xFFFF_FFFFL; + private final AtomicInteger sequence = new AtomicInteger((int) (Math.random() * UINT32_MAX)); StaticStrideScheduler(float[] weights) { int numChannels = weights.length; checkArgument(numChannels >= 1, "Couldn't build scheduler: requires at least one weight"); - int numZeroWeightChannels = 0; + int numWeightedChannels = 0; double sumWeight = 0; float maxWeight = 0; for (float weight : weights) { - if (weight < 0.0001) { - numZeroWeightChannels++; - } else { + if (weight > 0.0001) { sumWeight += weight; maxWeight = Math.max(weight, maxWeight); + numWeightedChannels++; } } double scalingFactor = K_MAX_WEIGHT / maxWeight; - int meanWeight = numZeroWeightChannels == numChannels ? 1 : - (int) Math.round(scalingFactor * sumWeight / (numChannels - numZeroWeightChannels)); + if (numWeightedChannels > 0) { + int meanWeight = (int) Math.round(scalingFactor * sumWeight / numWeightedChannels); + } else { + int meanWeight = 1; + } // scales weights s.t. max(weights) == K_MAX_WEIGHT, meanWeight is scaled accordingly int[] scaledWeights = new int[numChannels]; @@ -378,8 +380,9 @@ static final class StaticStrideScheduler { /** Returns the next sequence number and increases sequence with wraparound. */ private long nextSequence() { - return this.sequence.getAndUpdate(seq -> ((seq + 1) % UINT32_MAX)); + return Integer.toUnsignedLong(sequence.getAndIncrement()); } + /** Selects index of next backend server. */ int pick() { @@ -388,7 +391,8 @@ int pick() { long backendIndex = sequence % this.sizeDivisor; long generation = sequence / this.sizeDivisor; long weight = this.scaledWeights[(int) backendIndex]; - if ((weight * generation) % K_MAX_WEIGHT < K_MAX_WEIGHT - weight) { + long offset = backendIndex * K_MAX_WEIGHT / 2; + if ((weight * generation + offset) % K_MAX_WEIGHT < K_MAX_WEIGHT - weight) { continue; } return (int) backendIndex; From 1731862b9fd9c7b5d74d69386d24d3e5b7c11920 Mon Sep 17 00:00:00 2001 From: Tony An Date: Fri, 16 Jun 2023 13:48:15 -0700 Subject: [PATCH 29/42] added random to resolve static class failure --- .../xds/WeightedRoundRobinLoadBalancer.java | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 4804101972d..962d20c4a39 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -44,6 +44,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -64,10 +65,15 @@ final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { private final ScheduledExecutorService timeService; private ScheduledHandle weightUpdateTimer; private final Runnable updateWeightTask; + private final Random random; private final long infTime; private final Ticker ticker; - public WeightedRoundRobinLoadBalancer(WrrHelper helper, Ticker ticker) { + public WeightedRoundRobinLoadBalancer(Helper helper, Ticker ticker) { + this(new WrrHelper(OrcaOobUtil.newOrcaReportingHelper(helper)), ticker, new Random()); + } + + public WeightedRoundRobinLoadBalancer(WrrHelper helper, Ticker ticker, Random random) { super(helper); helper.setLoadBalancer(this); this.ticker = checkNotNull(ticker, "ticker"); @@ -75,12 +81,13 @@ public WeightedRoundRobinLoadBalancer(WrrHelper helper, Ticker ticker) { this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService"); this.updateWeightTask = new UpdateWeightTask(); + this.random = random; log.log(Level.FINE, "weighted_round_robin LB created"); } @VisibleForTesting - WeightedRoundRobinLoadBalancer(Helper helper, Ticker ticker) { - this(new WrrHelper(OrcaOobUtil.newOrcaReportingHelper(helper)), ticker); + WeightedRoundRobinLoadBalancer(Helper helper, Ticker ticker, Random random) { + this(new WrrHelper(OrcaOobUtil.newOrcaReportingHelper(helper)), ticker, random); } @Override @@ -288,7 +295,7 @@ private void updateWeight() { newWeights[i] = newWeight > 0 ? (float) newWeight : 0.0f; } - StaticStrideScheduler ssScheduler = new StaticStrideScheduler(newWeights); + StaticStrideScheduler ssScheduler = new StaticStrideScheduler(newWeights, random); this.ssScheduler = ssScheduler; } @@ -339,16 +346,18 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { static final class StaticStrideScheduler { private final int[] scaledWeights; private final int sizeDivisor; + private final Random random; + private final AtomicInteger sequence; private static final int K_MAX_WEIGHT = 65535; private static final long UINT32_MAX = 0xFFFF_FFFFL; - private final AtomicInteger sequence = new AtomicInteger((int) (Math.random() * UINT32_MAX)); - StaticStrideScheduler(float[] weights) { + StaticStrideScheduler(float[] weights, Random random) { int numChannels = weights.length; checkArgument(numChannels >= 1, "Couldn't build scheduler: requires at least one weight"); int numWeightedChannels = 0; double sumWeight = 0; float maxWeight = 0; + int meanWeight = 0; for (float weight : weights) { if (weight > 0.0001) { sumWeight += weight; @@ -359,9 +368,9 @@ static final class StaticStrideScheduler { double scalingFactor = K_MAX_WEIGHT / maxWeight; if (numWeightedChannels > 0) { - int meanWeight = (int) Math.round(scalingFactor * sumWeight / numWeightedChannels); + meanWeight = (int) Math.round(scalingFactor * sumWeight / numWeightedChannels); } else { - int meanWeight = 1; + meanWeight = 1; } // scales weights s.t. max(weights) == K_MAX_WEIGHT, meanWeight is scaled accordingly @@ -376,6 +385,9 @@ static final class StaticStrideScheduler { this.scaledWeights = scaledWeights; this.sizeDivisor = numChannels; + this.random = random; + this.sequence = new AtomicInteger((int) (this.random.nextDouble() * UINT32_MAX)); + } /** Returns the next sequence number and increases sequence with wraparound. */ From 5e5127fbefbc3bb0a65417566e923fabfbcd2ae9 Mon Sep 17 00:00:00 2001 From: Tony An Date: Fri, 16 Jun 2023 13:48:45 -0700 Subject: [PATCH 30/42] adjusted test cases to have random, deleted deterministic test cases from edf --- .../WeightedRoundRobinLoadBalancerTest.java | 55 ++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index e377b40b144..7d17e34dab6 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -63,6 +63,7 @@ import java.util.List; import java.util.Map; import java.util.Queue; +import java.util.Random; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CyclicBarrier; @@ -173,7 +174,8 @@ public Void answer(InvocationOnMock invocation) throws Throwable { return subchannel; } }); - wrr = new WeightedRoundRobinLoadBalancer(helper, fakeClock.getDeadlineTicker()); + Random random = new Random(); + wrr = new WeightedRoundRobinLoadBalancer(helper, fakeClock.getDeadlineTicker(), random); } @Test @@ -262,7 +264,6 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt 0.9, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(1); PickResult pickResult = weightedPicker.pickSubchannel(mockArgs); - assertThat(pickResult.getSubchannel()).isEqualTo(weightedSubchannel1); assertThat(pickResult.getStreamTracerFactory()).isNotNull(); // verify per-request listener assertThat(oobCalls.isEmpty()).isTrue(); @@ -276,7 +277,6 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt eq(ConnectivityState.READY), pickerCaptor2.capture()); weightedPicker = (WeightedRoundRobinPicker) pickerCaptor2.getAllValues().get(2); pickResult = weightedPicker.pickSubchannel(mockArgs); - assertThat(pickResult.getSubchannel()).isEqualTo(weightedSubchannel1); assertThat(pickResult.getStreamTracerFactory()).isNull(); OrcaLoadReportRequest golden = OrcaLoadReportRequest.newBuilder().setReportInterval( Duration.newBuilder().setSeconds(20).setNanos(30000000).build()).build(); @@ -871,14 +871,16 @@ public void wrrConfig_BooleanValueNonNull() { @Test(expected = IllegalArgumentException.class) public void emptyWeights() { float[] weights = {}; - StaticStrideScheduler sss = new StaticStrideScheduler(weights); + Random random = new Random(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); sss.pick(); } @Test public void testPicksEqualsWeights() { float[] weights = {1.0f, 2.0f, 3.0f}; - StaticStrideScheduler sss = new StaticStrideScheduler(weights); + Random random = new Random(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); int[] expectedPicks = new int[] {1, 2, 3}; int[] picks = new int[3]; for (int i = 0; i < 6; i++) { @@ -890,7 +892,8 @@ public void testPicksEqualsWeights() { @Test public void testContainsZeroWeightUseMean() { float[] weights = {3.0f, 0.0f, 1.0f}; - StaticStrideScheduler sss = new StaticStrideScheduler(weights); + Random random = new Random(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); int[] expectedPicks = new int[] {3, 2, 1}; int[] picks = new int[3]; for (int i = 0; i < 6; i++) { @@ -902,7 +905,8 @@ public void testContainsZeroWeightUseMean() { @Test public void testContainsNegativeWeightUseMean() { float[] weights = {3.0f, -1.0f, 1.0f}; - StaticStrideScheduler sss = new StaticStrideScheduler(weights); + Random random = new Random(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); int[] expectedPicks = new int[] {3, 2, 1}; int[] picks = new int[3]; for (int i = 0; i < 6; i++) { @@ -914,7 +918,8 @@ public void testContainsNegativeWeightUseMean() { @Test public void testAllSameWeights() { float[] weights = {1.0f, 1.0f, 1.0f}; - StaticStrideScheduler sss = new StaticStrideScheduler(weights); + Random random = new Random(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); int[] expectedPicks = new int[] {2, 2, 2}; int[] picks = new int[3]; for (int i = 0; i < 6; i++) { @@ -926,7 +931,8 @@ public void testAllSameWeights() { @Test public void testAllZeroWeightsUseOne() { float[] weights = {0.0f, 0.0f, 0.0f}; - StaticStrideScheduler sss = new StaticStrideScheduler(weights); + Random random = new Random(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); int[] expectedPicks = new int[] {2, 2, 2}; int[] picks = new int[3]; for (int i = 0; i < 6; i++) { @@ -938,7 +944,8 @@ public void testAllZeroWeightsUseOne() { @Test public void testAllInvalidWeightsUseOne() { float[] weights = {-3.1f, -0.0f, 0.0f}; - StaticStrideScheduler sss = new StaticStrideScheduler(weights); + Random random = new Random(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); int[] expectedPicks = new int[] {2, 2, 2}; int[] picks = new int[3]; for (int i = 0; i < 6; i++) { @@ -951,7 +958,8 @@ public void testAllInvalidWeightsUseOne() { public void testLargestPickedEveryGeneration() { float[] weights = {1.0f, 2.0f, 3.0f}; int mean = 2; - StaticStrideScheduler sss = new StaticStrideScheduler(weights); + Random random = new Random(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); int largestWeightPickCount = 0; int kMaxWeight = 65535; for (int i = 0; i < mean * kMaxWeight; i++) { @@ -965,7 +973,8 @@ public void testLargestPickedEveryGeneration() { @Test public void testStaticStrideSchedulerGivenExample1() { float[] weights = {10.0f, 20.0f, 30.0f}; - StaticStrideScheduler sss = new StaticStrideScheduler(weights); + Random random = new Random(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); double totalWeight = 60; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -981,7 +990,8 @@ public void testStaticStrideSchedulerGivenExample1() { @Test public void testStaticStrideSchedulerGivenExample2() { float[] weights = {2.0f, 3.0f, 6.0f}; - StaticStrideScheduler sss = new StaticStrideScheduler(weights); + Random random = new Random(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); double totalWeight = 11; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -997,7 +1007,8 @@ public void testStaticStrideSchedulerGivenExample2() { @Test public void testStaticStrideSchedulerNonIntegers1() { float[] weights = {2.0f, (float) (10.0 / 3.0), 1.0f}; - StaticStrideScheduler sss = new StaticStrideScheduler(weights); + Random random = new Random(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); double totalWeight = 2 + 10.0 / 3.0 + 1.0; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -1013,7 +1024,8 @@ public void testStaticStrideSchedulerNonIntegers1() { @Test public void testStaticStrideSchedulerNonIntegers2() { float[] weights = {0.5f, 0.3f, 1.0f}; - StaticStrideScheduler sss = new StaticStrideScheduler(weights); + Random random = new Random(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); double totalWeight = 1.8; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -1029,7 +1041,8 @@ public void testStaticStrideSchedulerNonIntegers2() { @Test public void testTwoWeights() { float[] weights = {1.0f, 2.0f}; - StaticStrideScheduler sss = new StaticStrideScheduler(weights); + Random random = new Random(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); double totalWeight = 3; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -1045,7 +1058,8 @@ public void testTwoWeights() { @Test public void testManyWeights1() { float[] weights = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; - StaticStrideScheduler sss = new StaticStrideScheduler(weights); + Random random = new Random(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); double totalWeight = 15; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -1060,8 +1074,9 @@ public void testManyWeights1() { @Test public void testManyComplexWeights2() { - float[] weights = {1.2f, 2.4f, 222.56f, 0f, 15.0f, 226342.0f, 5123.0f, 0.0001f}; - StaticStrideScheduler sss = new StaticStrideScheduler(weights); + float[] weights = {1.2f, 2.4f, 222.56f, 1.1f, 15.0f, 226342.0f, 5123.0f, 532.2f}; + Random random = new Random(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); double totalWeight = 1.2 + 2.4 + 222.56 + 15.0 + 226342.0 + 5123.0 + 0.0001; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -1070,7 +1085,7 @@ public void testManyComplexWeights2() { } for (int i = 0; i < 8; i++) { assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 10000.0 - weights[i] / totalWeight) ) - .isAtMost(5); + .isAtMost(2); } } From f0421d2f301ae67d193dfb5b7a4f384f0cc93ca7 Mon Sep 17 00:00:00 2001 From: Tony An Date: Tue, 20 Jun 2023 09:30:46 -0700 Subject: [PATCH 31/42] fixed test case and number to hex --- .../main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java | 2 +- .../java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 962d20c4a39..ed83eb94706 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -348,7 +348,7 @@ static final class StaticStrideScheduler { private final int sizeDivisor; private final Random random; private final AtomicInteger sequence; - private static final int K_MAX_WEIGHT = 65535; + private static final int K_MAX_WEIGHT = 0xFFFF; private static final long UINT32_MAX = 0xFFFF_FFFFL; StaticStrideScheduler(float[] weights, Random random) { diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 7d17e34dab6..7522a2cf2ce 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -1084,8 +1084,8 @@ public void testManyComplexWeights2() { pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } for (int i = 0; i < 8; i++) { - assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 10000.0 - weights[i] / totalWeight) ) - .isAtMost(2); + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) + .isAtMost(0.01); } } From ec46fe30f509c4d2a2b131fba9f2d790d3592087 Mon Sep 17 00:00:00 2001 From: Tony An Date: Fri, 23 Jun 2023 10:49:15 -0700 Subject: [PATCH 32/42] fixed sequence, changed long --> int, added comments --- .../xds/WeightedRoundRobinLoadBalancer.java | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index ed83eb94706..13e4bd20c94 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -332,8 +332,10 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { * Implementation of Static Stride Scheduler, replaces EDFScheduler. *

    * The Static Stride Scheduler works by iterating through the list of subchannel weights - * and using modular arithmetic to evenly distribute picks and skips, favoring - * entries with the highest weight. + * and using modular arithmetic to evenly distribute picks and skips, favoring entries with the + * highest weight. It generates a practically equivalent sequence of picks as the EDFScheduler. + * Albeit needing more bandwidth, the Static Stride Scheduler is more performant than the + * EDFScheduler, as it removes the need for a priority queue (and thus mutex locks). *

    * go/static-stride-scheduler *

    @@ -346,14 +348,12 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { static final class StaticStrideScheduler { private final int[] scaledWeights; private final int sizeDivisor; - private final Random random; private final AtomicInteger sequence; private static final int K_MAX_WEIGHT = 0xFFFF; - private static final long UINT32_MAX = 0xFFFF_FFFFL; StaticStrideScheduler(float[] weights, Random random) { + checkArgument(weights.length >= 1, "Couldn't build scheduler: requires at least one weight"); int numChannels = weights.length; - checkArgument(numChannels >= 1, "Couldn't build scheduler: requires at least one weight"); int numWeightedChannels = 0; double sumWeight = 0; float maxWeight = 0; @@ -385,29 +385,39 @@ static final class StaticStrideScheduler { this.scaledWeights = scaledWeights; this.sizeDivisor = numChannels; - this.random = random; - this.sequence = new AtomicInteger((int) (this.random.nextDouble() * UINT32_MAX)); + this.sequence = new AtomicInteger(random.nextInt()); } - /** Returns the next sequence number and increases sequence with wraparound. */ + /** Returns the next sequence number and atomically increases sequence with wraparound. */ private long nextSequence() { return Integer.toUnsignedLong(sequence.getAndIncrement()); } + public long getSequence() { + return Integer.toUnsignedLong(sequence.get()); + } - /** Selects index of next backend server. */ + /* + * Selects index of next backend server. + *

    + * A 2D array is compactly represented where the row represents the generation and the column + * represents the backend index. The value of an element is a boolean value which indicates + * whether or not a backend should be picked now. An atomically incremented counter keeps track + * of our backend and generation through modular arithmetic within the pick() method. + * An offset is also included to minimize consecutive non-picks of a backend. + */ int pick() { while (true) { long sequence = this.nextSequence(); - long backendIndex = sequence % this.sizeDivisor; + int backendIndex = (int) (sequence % this.sizeDivisor); long generation = sequence / this.sizeDivisor; - long weight = this.scaledWeights[(int) backendIndex]; - long offset = backendIndex * K_MAX_WEIGHT / 2; + long weight = this.scaledWeights[backendIndex]; + long offset = (long) K_MAX_WEIGHT / 2 * backendIndex; if ((weight * generation + offset) % K_MAX_WEIGHT < K_MAX_WEIGHT - weight) { continue; } - return (int) backendIndex; + return backendIndex; } } } From 8ddf284b4b99c1bd13c54f2ed40713cc2c65dc10 Mon Sep 17 00:00:00 2001 From: tonyjongyoonan Date: Fri, 23 Jun 2023 11:18:40 -0700 Subject: [PATCH 33/42] fixed sequence, changed types, added comments --- .vscode/settings.json | 7 + .../xds/WeightedRoundRobinLoadBalancer.java | 56 ++--- .../WeightedRoundRobinLoadBalancerTest.java | 203 ++++++++++-------- 3 files changed, 150 insertions(+), 116 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000000..6b5a77b3ddf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.rulers": [ + 100 + ], + "github.gitAuthentication": true, + "git.terminalAuthentication": false +} \ No newline at end of file diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 13e4bd20c94..deac6b2b16d 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -59,7 +59,7 @@ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/9885") final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { private static final Logger log = Logger.getLogger( - WeightedRoundRobinLoadBalancer.class.getName()); + WeightedRoundRobinLoadBalancer.class.getName()); private WeightedRoundRobinLoadBalancerConfig config; private final SynchronizationContext syncContext; private final ScheduledExecutorService timeService; @@ -113,7 +113,7 @@ public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { @Override public RoundRobinPicker createReadyPicker(List activeList) { return new WeightedRoundRobinPicker(activeList, config.enableOobLoadReport, - config.errorUtilizationPenalty); + config.errorUtilizationPenalty); } private final class UpdateWeightTask implements Runnable { @@ -123,7 +123,7 @@ public void run() { ((WeightedRoundRobinPicker) currentPicker).updateWeight(); } weightUpdateTimer = syncContext.schedule(this, config.weightUpdatePeriodNanos, - TimeUnit.NANOSECONDS, timeService); + TimeUnit.NANOSECONDS, timeService); } } @@ -132,7 +132,7 @@ private void afterAcceptAddresses() { WrrSubchannel weightedSubchannel = (WrrSubchannel) subchannel; if (config.enableOobLoadReport) { OrcaOobUtil.setListener(weightedSubchannel, - weightedSubchannel.new OrcaReportListener(config.errorUtilizationPenalty), + weightedSubchannel.new OrcaReportListener(config.errorUtilizationPenalty), OrcaOobUtil.OrcaReportingConfig.newBuilder() .setReportInterval(config.oobReportingPeriodNanos, TimeUnit.NANOSECONDS) .build()); @@ -206,7 +206,7 @@ private double getWeight() { nonEmptySince = infTime; return 0; } else if (now - nonEmptySince < config.blackoutPeriodNanos - && config.blackoutPeriodNanos > 0) { + && config.blackoutPeriodNanos > 0) { return 0; } else { return weight; @@ -230,8 +230,8 @@ public void onLoadReport(MetricReport report) { double newWeight = 0; // Prefer application utilization and fallback to CPU utilization if unset. double utilization = - report.getApplicationUtilization() > 0 ? report.getApplicationUtilization() - : report.getCpuUtilization(); + report.getApplicationUtilization() > 0 ? report.getApplicationUtilization() + : report.getCpuUtilization(); if (utilization > 0 && report.getQps() > 0) { double penalty = 0; if (report.getEps() > 0 && errorUtilizationPenalty > 0) { @@ -255,19 +255,19 @@ public void onLoadReport(MetricReport report) { final class WeightedRoundRobinPicker extends RoundRobinPicker { private final List list; private final Map subchannelToReportListenerMap = - new HashMap<>(); + new HashMap<>(); private final boolean enableOobLoadReport; private final float errorUtilizationPenalty; private volatile StaticStrideScheduler ssScheduler; WeightedRoundRobinPicker(List list, boolean enableOobLoadReport, - float errorUtilizationPenalty) { + float errorUtilizationPenalty) { checkNotNull(list, "list"); Preconditions.checkArgument(!list.isEmpty(), "empty list"); this.list = list; for (Subchannel subchannel : list) { this.subchannelToReportListenerMap.put(subchannel, - ((WrrSubchannel) subchannel).new OrcaReportListener(errorUtilizationPenalty)); + ((WrrSubchannel) subchannel).new OrcaReportListener(errorUtilizationPenalty)); } this.enableOobLoadReport = enableOobLoadReport; this.errorUtilizationPenalty = errorUtilizationPenalty; @@ -279,14 +279,14 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { Subchannel subchannel = list.get(ssScheduler.pick()); if (!enableOobLoadReport) { return PickResult.withSubchannel(subchannel, - OrcaPerRequestUtil.getInstance().newOrcaClientStreamTracerFactory( - subchannelToReportListenerMap.getOrDefault(subchannel, - ((WrrSubchannel) subchannel).new OrcaReportListener(errorUtilizationPenalty)))); + OrcaPerRequestUtil.getInstance().newOrcaClientStreamTracerFactory( + subchannelToReportListenerMap.getOrDefault(subchannel, + ((WrrSubchannel) subchannel).new OrcaReportListener(errorUtilizationPenalty)))); } else { return PickResult.withSubchannel(subchannel); } } - + private void updateWeight() { float[] newWeights = new float[list.size()]; for (int i = 0; i < list.size(); i++) { @@ -302,9 +302,9 @@ private void updateWeight() { @Override public String toString() { return MoreObjects.toStringHelper(WeightedRoundRobinPicker.class) - .add("enableOobLoadReport", enableOobLoadReport) - .add("errorUtilizationPenalty", errorUtilizationPenalty) - .add("list", list).toString(); + .add("enableOobLoadReport", enableOobLoadReport) + .add("errorUtilizationPenalty", errorUtilizationPenalty) + .add("list", list).toString(); } @VisibleForTesting @@ -323,8 +323,8 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { } // the lists cannot contain duplicate subchannels return enableOobLoadReport == other.enableOobLoadReport - && Float.compare(errorUtilizationPenalty, other.errorUtilizationPenalty) == 0 - && list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list); + && Float.compare(errorUtilizationPenalty, other.errorUtilizationPenalty) == 0 + && list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list); } } @@ -333,13 +333,13 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { *

    * The Static Stride Scheduler works by iterating through the list of subchannel weights * and using modular arithmetic to evenly distribute picks and skips, favoring entries with the - * highest weight. It generates a practically equivalent sequence of picks as the EDFScheduler. - * Albeit needing more bandwidth, the Static Stride Scheduler is more performant than the + * highest weight. It generates a practically equivalent sequence of picks as the EDFScheduler. + * Albeit needing more bandwidth, the Static Stride Scheduler is more performant than the * EDFScheduler, as it removes the need for a priority queue (and thus mutex locks). *

    * go/static-stride-scheduler *

    - * + * *

      *
    • nextSequence() - O(1) *
    • pick() - O(n) @@ -361,7 +361,7 @@ static final class StaticStrideScheduler { for (float weight : weights) { if (weight > 0.0001) { sumWeight += weight; - maxWeight = Math.max(weight, maxWeight); + maxWeight = Math.max(weight, maxWeight); numWeightedChannels++; } } @@ -386,24 +386,24 @@ static final class StaticStrideScheduler { this.scaledWeights = scaledWeights; this.sizeDivisor = numChannels; this.sequence = new AtomicInteger(random.nextInt()); - + } /** Returns the next sequence number and atomically increases sequence with wraparound. */ private long nextSequence() { return Integer.toUnsignedLong(sequence.getAndIncrement()); } - + public long getSequence() { return Integer.toUnsignedLong(sequence.get()); } /* - * Selects index of next backend server. + * Selects index of next backend server. *

      * A 2D array is compactly represented where the row represents the generation and the column * represents the backend index. The value of an element is a boolean value which indicates - * whether or not a backend should be picked now. An atomically incremented counter keeps track + * whether or not a backend should be picked now. An atomically incremented counter keeps track * of our backend and generation through modular arithmetic within the pick() method. * An offset is also included to minimize consecutive non-picks of a backend. */ @@ -414,7 +414,7 @@ int pick() { long generation = sequence / this.sizeDivisor; long weight = this.scaledWeights[backendIndex]; long offset = (long) K_MAX_WEIGHT / 2 * backendIndex; - if ((weight * generation + offset) % K_MAX_WEIGHT < K_MAX_WEIGHT - weight) { + if ((weight * generation + offset) % K_MAX_WEIGHT < K_MAX_WEIGHT - weight) { continue; } return backendIndex; diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 7522a2cf2ce..b35ed503a79 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -174,8 +174,8 @@ public Void answer(InvocationOnMock invocation) throws Throwable { return subchannel; } }); - Random random = new Random(); - wrr = new WeightedRoundRobinLoadBalancer(helper, fakeClock.getDeadlineTicker(), random); + wrr = new WeightedRoundRobinLoadBalancer(helper, fakeClock.getDeadlineTicker(), + new FakeRandom()); } @Test @@ -219,6 +219,8 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt InternalCallMetricRecorder.createMetricReport( 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(1); + assertThat(weightedPicker.pickSubchannel(mockArgs) + .getSubchannel()).isEqualTo(weightedSubchannel1); assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); weightedConfig = WeightedRoundRobinLoadBalancerConfig.newBuilder() .setWeightUpdatePeriodNanos(500_000_000L) //.5s @@ -264,6 +266,7 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt 0.9, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(1); PickResult pickResult = weightedPicker.pickSubchannel(mockArgs); + assertThat(pickResult.getSubchannel()).isEqualTo(weightedSubchannel1); assertThat(pickResult.getStreamTracerFactory()).isNotNull(); // verify per-request listener assertThat(oobCalls.isEmpty()).isTrue(); @@ -277,6 +280,7 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt eq(ConnectivityState.READY), pickerCaptor2.capture()); weightedPicker = (WeightedRoundRobinPicker) pickerCaptor2.getAllValues().get(2); pickResult = weightedPicker.pickSubchannel(mockArgs); + assertThat(pickResult.getSubchannel()).isEqualTo(weightedSubchannel1); assertThat(pickResult.getStreamTracerFactory()).isNull(); OrcaLoadReportRequest golden = OrcaLoadReportRequest.newBuilder().setReportInterval( Duration.newBuilder().setSeconds(20).setNanos(30000000).build()).build(); @@ -378,51 +382,6 @@ public void pickByWeight_largeWeight_withEps_defaultErrorUtilizationPenalty() { weight3 / totalWeight); } - @Test - public void pickByWeight_lowWeight() { - MetricReport report1 = InternalCallMetricRecorder.createMetricReport( - 0.22, 0, 0.1, 122, 0, new HashMap<>(), new HashMap<>()); - MetricReport report2 = InternalCallMetricRecorder.createMetricReport( - 0.7, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>()); - MetricReport report3 = InternalCallMetricRecorder.createMetricReport( - 0.86, 0, 0.1, 80, 0, new HashMap<>(), new HashMap<>()); - double totalWeight = 122 / 0.22 + 1 / 0.7 + 80 / 0.86; - - pickByWeight(report1, report2, report3, 122 / 0.22 / totalWeight, 1 / 0.7 / totalWeight, - 80 / 0.86 / totalWeight); - } - - @Test - public void pickByWeight_lowWeight_useApplicationUtilization() { - MetricReport report1 = InternalCallMetricRecorder.createMetricReport( - 0.22, 0.2, 0.1, 122, 0, new HashMap<>(), new HashMap<>()); - MetricReport report2 = InternalCallMetricRecorder.createMetricReport( - 0.7, 0.5, 0.1, 1, 0, new HashMap<>(), new HashMap<>()); - MetricReport report3 = InternalCallMetricRecorder.createMetricReport( - 0.86, 0.33, 0.1, 80, 0, new HashMap<>(), new HashMap<>()); - double totalWeight = 122 / 0.2 + 1 / 0.5 + 80 / 0.33; - - pickByWeight(report1, report2, report3, 122 / 0.2 / totalWeight, 1 / 0.5 / totalWeight, - 80 / 0.33 / totalWeight); - } - - @Test - public void pickByWeight_lowWeight_withEps_defaultErrorUtilizationPenalty() { - MetricReport report1 = InternalCallMetricRecorder.createMetricReport( - 0.22, 0, 0.1, 122, 5, new HashMap<>(), new HashMap<>()); - MetricReport report2 = InternalCallMetricRecorder.createMetricReport( - 0.7, 0, 0.1, 1, 11, new HashMap<>(), new HashMap<>()); - MetricReport report3 = InternalCallMetricRecorder.createMetricReport( - 0.86, 0, 0.1, 80, 1, new HashMap<>(), new HashMap<>()); - double weight1 = 122 / (0.22 + 5 / 122F * weightedConfig.errorUtilizationPenalty); - double weight2 = 1 / (0.7 + 11 / 1F * weightedConfig.errorUtilizationPenalty); - double weight3 = 80 / (0.86 + 1 / 80F * weightedConfig.errorUtilizationPenalty); - double totalWeight = weight1 + weight2 + weight3; - - pickByWeight(report1, report2, report3, weight1 / totalWeight, weight2 / totalWeight, - weight3 / totalWeight); - } - @Test public void pickByWeight_normalWeight() { MetricReport report1 = InternalCallMetricRecorder.createMetricReport( @@ -614,6 +573,8 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt InternalCallMetricRecorder.createMetricReport( 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(1); + assertThat(weightedPicker.pickSubchannel(mockArgs) + .getSubchannel()).isEqualTo(weightedSubchannel1); assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); weightedConfig = WeightedRoundRobinLoadBalancerConfig.newBuilder() .setWeightUpdatePeriodNanos(500_000_000L) //.5s @@ -630,6 +591,9 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); //timer fires, new weight updated assertThat(fakeClock.forwardTime(500, TimeUnit.MILLISECONDS)).isEqualTo(1); + assertThat(weightedPicker.pickSubchannel(mockArgs) + .getSubchannel()).isEqualTo(weightedSubchannel2); + } @Test @@ -787,12 +751,12 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt } assertThat(pickCount.size()).isEqualTo(3); assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 4.0 / 9)) - .isAtMost(0.001); + .isAtMost(0.002); assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 2.0 / 9)) - .isAtMost(0.001); + .isAtMost(0.002); // subchannel3's weight is average of subchannel1 and subchannel2 assertThat(Math.abs(pickCount.get(weightedSubchannel3) / 1000.0 - 3.0 / 9)) - .isAtMost(0.001); + .isAtMost(0.002); } @Test @@ -955,7 +919,7 @@ public void testAllInvalidWeightsUseOne() { } @Test - public void testLargestPickedEveryGeneration() { + public void testLacrgestWeightIndexPickedEveryGeneration() { float[] weights = {1.0f, 2.0f, 3.0f}; int mean = 2; Random random = new Random(); @@ -971,94 +935,114 @@ public void testLargestPickedEveryGeneration() { } @Test - public void testStaticStrideSchedulerGivenExample1() { - float[] weights = {10.0f, 20.0f, 30.0f}; + public void testStaticStrideSchedulerNonIntegers1() { + float[] weights = {2.0f, (float) (10.0 / 3.0), 1.0f}; Random random = new Random(); StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); - double totalWeight = 60; + double totalWeight = 2 + 10.0 / 3.0 + 1.0; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { int result = sss.pick(); pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } for (int i = 0; i < 3; i++) { - assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight)) .isAtMost(0.01); } } @Test - public void testStaticStrideSchedulerGivenExample2() { - float[] weights = {2.0f, 3.0f, 6.0f}; + public void testStaticStrideSchedulerNonIntegers2() { + float[] weights = {0.5f, 0.3f, 1.0f}; Random random = new Random(); StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); - double totalWeight = 11; + double totalWeight = 1.8; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { int result = sss.pick(); pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } for (int i = 0; i < 3; i++) { - assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight)) .isAtMost(0.01); } } @Test - public void testStaticStrideSchedulerNonIntegers1() { - float[] weights = {2.0f, (float) (10.0 / 3.0), 1.0f}; + public void testTwoWeights() { + float[] weights = {1.0f, 2.0f}; Random random = new Random(); StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); - double totalWeight = 2 + 10.0 / 3.0 + 1.0; + double totalWeight = 3; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { int result = sss.pick(); pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } - for (int i = 0; i < 3; i++) { - assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) + for (int i = 0; i < 2; i++) { + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight)) .isAtMost(0.01); } } @Test - public void testStaticStrideSchedulerNonIntegers2() { - float[] weights = {0.5f, 0.3f, 1.0f}; + public void testManyWeights1() { + float[] weights = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; Random random = new Random(); StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); - double totalWeight = 1.8; + double totalWeight = 15; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { int result = sss.pick(); pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } - for (int i = 0; i < 3; i++) { - assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) - .isAtMost(0.01); + for (int i = 0; i < 5; i++) { + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight)) + .isAtMost(0.001); } } @Test - public void testTwoWeights() { - float[] weights = {1.0f, 2.0f}; + public void testManyComplexWeights2() { + float[] weights = {1.2f, 2.4f, 222.56f, 1.1f, 15.0f, 226342.0f, 5123.0f, 532.2f}; Random random = new Random(); StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); - double totalWeight = 3; + double totalWeight = 1.2 + 2.4 + 222.56 + 15.0 + 226342.0 + 5123.0 + 0.0001; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { int result = sss.pick(); pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } - for (int i = 0; i < 2; i++) { - assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) + for (int i = 0; i < 8; i++) { + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight)) .isAtMost(0.01); } } @Test - public void testManyWeights1() { + public void testDeterministicPicks() { + float[] weights = {2.0f, 3.0f, 6.0f}; + Random random = new FakeRandom(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); + assertThat(sss.getSequence()).isEqualTo(0); + assertThat(sss.pick()).isEqualTo(1); + assertThat(sss.getSequence()).isEqualTo(2); + assertThat(sss.pick()).isEqualTo(2); + assertThat(sss.getSequence()).isEqualTo(3); + assertThat(sss.pick()).isEqualTo(2); + assertThat(sss.getSequence()).isEqualTo(6); + assertThat(sss.pick()).isEqualTo(0); + assertThat(sss.getSequence()).isEqualTo(7); + assertThat(sss.pick()).isEqualTo(1); + assertThat(sss.getSequence()).isEqualTo(8); + assertThat(sss.pick()).isEqualTo(2); + assertThat(sss.getSequence()).isEqualTo(9); + } + + @Test + public void testWraparound() { float[] weights = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; - Random random = new Random(); + Random random = new FakeRandomWraparound(); StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); double totalWeight = 15; Map pickCount = new HashMap<>(); @@ -1066,29 +1050,49 @@ public void testManyWeights1() { int result = sss.pick(); pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } - for (int i = 0; i < 3; i++) { - assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) - .isAtMost(0.01); + for (int i = 0; i < 5; i++) { + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight)) + .isAtMost(0.001); } } - + @Test - public void testManyComplexWeights2() { - float[] weights = {1.2f, 2.4f, 222.56f, 1.1f, 15.0f, 226342.0f, 5123.0f, 532.2f}; - Random random = new Random(); + public void testWraparound2() { + float[] weights = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + Random random = new FakeRandomWraparound2(); StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); - double totalWeight = 1.2 + 2.4 + 222.56 + 15.0 + 226342.0 + 5123.0 + 0.0001; + double totalWeight = 15; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { int result = sss.pick(); pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } - for (int i = 0; i < 8; i++) { - assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) - .isAtMost(0.01); + for (int i = 0; i < 5; i++) { + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight)) + .isAtMost(0.0011); } } + @Test + public void testDeterministicWraparound() { + float[] weights = {2.0f, 3.0f, 6.0f}; + Random random = new FakeRandomWraparound(); + StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); + assertThat(sss.getSequence()).isEqualTo(0xFFFF_FFFFL); + assertThat(sss.pick()).isEqualTo(1); + assertThat(sss.getSequence()).isEqualTo(2); + assertThat(sss.pick()).isEqualTo(2); + assertThat(sss.getSequence()).isEqualTo(3); + assertThat(sss.pick()).isEqualTo(2); + assertThat(sss.getSequence()).isEqualTo(6); + assertThat(sss.pick()).isEqualTo(0); + assertThat(sss.getSequence()).isEqualTo(7); + assertThat(sss.pick()).isEqualTo(1); + assertThat(sss.getSequence()).isEqualTo(8); + assertThat(sss.pick()).isEqualTo(2); + assertThat(sss.getSequence()).isEqualTo(9); + } + private static class FakeSocketAddress extends SocketAddress { final String name; @@ -1100,4 +1104,27 @@ private static class FakeSocketAddress extends SocketAddress { return "FakeSocketAddress-" + name; } } + + private static class FakeRandom extends Random { + @Override + public int nextInt() { + // return constant value to disable init deadline randomization in the scheduler + // sequence will be initialized to 0 + return 0; + } + } + + private static class FakeRandomWraparound extends Random { + @Override + public int nextInt() { + return -1; + } + } + + private static class FakeRandomWraparound2 extends Random { + @Override + public int nextInt() { + return -500; + } + } } From 347b46fcfe718cd58d58e1104a1aec11e1cdc678 Mon Sep 17 00:00:00 2001 From: tonyjongyoonan Date: Fri, 23 Jun 2023 14:18:57 -0700 Subject: [PATCH 34/42] fixing weird commit 1 --- .../main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index deac6b2b16d..20d4e5d94f4 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -280,8 +280,8 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { if (!enableOobLoadReport) { return PickResult.withSubchannel(subchannel, OrcaPerRequestUtil.getInstance().newOrcaClientStreamTracerFactory( - subchannelToReportListenerMap.getOrDefault(subchannel, - ((WrrSubchannel) subchannel).new OrcaReportListener(errorUtilizationPenalty)))); + subchannelToReportListenerMap.getOrDefault(subchannel, + ((WrrSubchannel) subchannel).new OrcaReportListener(errorUtilizationPenalty)))); } else { return PickResult.withSubchannel(subchannel); } From 4a4762e61ce18377ced563c6af339569fc3c416c Mon Sep 17 00:00:00 2001 From: tonyjongyoonan Date: Fri, 23 Jun 2023 14:26:12 -0700 Subject: [PATCH 35/42] more syntax changes --- .../xds/WeightedRoundRobinLoadBalancer.java | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 20d4e5d94f4..52492376d06 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -59,7 +59,7 @@ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/9885") final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { private static final Logger log = Logger.getLogger( - WeightedRoundRobinLoadBalancer.class.getName()); + WeightedRoundRobinLoadBalancer.class.getName()); private WeightedRoundRobinLoadBalancerConfig config; private final SynchronizationContext syncContext; private final ScheduledExecutorService timeService; @@ -113,7 +113,7 @@ public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { @Override public RoundRobinPicker createReadyPicker(List activeList) { return new WeightedRoundRobinPicker(activeList, config.enableOobLoadReport, - config.errorUtilizationPenalty); + config.errorUtilizationPenalty); } private final class UpdateWeightTask implements Runnable { @@ -123,7 +123,7 @@ public void run() { ((WeightedRoundRobinPicker) currentPicker).updateWeight(); } weightUpdateTimer = syncContext.schedule(this, config.weightUpdatePeriodNanos, - TimeUnit.NANOSECONDS, timeService); + TimeUnit.NANOSECONDS, timeService); } } @@ -132,7 +132,7 @@ private void afterAcceptAddresses() { WrrSubchannel weightedSubchannel = (WrrSubchannel) subchannel; if (config.enableOobLoadReport) { OrcaOobUtil.setListener(weightedSubchannel, - weightedSubchannel.new OrcaReportListener(config.errorUtilizationPenalty), + weightedSubchannel.new OrcaReportListener(config.errorUtilizationPenalty), OrcaOobUtil.OrcaReportingConfig.newBuilder() .setReportInterval(config.oobReportingPeriodNanos, TimeUnit.NANOSECONDS) .build()); @@ -206,7 +206,7 @@ private double getWeight() { nonEmptySince = infTime; return 0; } else if (now - nonEmptySince < config.blackoutPeriodNanos - && config.blackoutPeriodNanos > 0) { + && config.blackoutPeriodNanos > 0) { return 0; } else { return weight; @@ -230,8 +230,8 @@ public void onLoadReport(MetricReport report) { double newWeight = 0; // Prefer application utilization and fallback to CPU utilization if unset. double utilization = - report.getApplicationUtilization() > 0 ? report.getApplicationUtilization() - : report.getCpuUtilization(); + report.getApplicationUtilization() > 0 ? report.getApplicationUtilization() + : report.getCpuUtilization(); if (utilization > 0 && report.getQps() > 0) { double penalty = 0; if (report.getEps() > 0 && errorUtilizationPenalty > 0) { @@ -255,19 +255,19 @@ public void onLoadReport(MetricReport report) { final class WeightedRoundRobinPicker extends RoundRobinPicker { private final List list; private final Map subchannelToReportListenerMap = - new HashMap<>(); + new HashMap<>(); private final boolean enableOobLoadReport; private final float errorUtilizationPenalty; - private volatile StaticStrideScheduler ssScheduler; + private volatile StaticStrideScheduler scheduler; WeightedRoundRobinPicker(List list, boolean enableOobLoadReport, - float errorUtilizationPenalty) { + float errorUtilizationPenalty) { checkNotNull(list, "list"); Preconditions.checkArgument(!list.isEmpty(), "empty list"); this.list = list; for (Subchannel subchannel : list) { this.subchannelToReportListenerMap.put(subchannel, - ((WrrSubchannel) subchannel).new OrcaReportListener(errorUtilizationPenalty)); + ((WrrSubchannel) subchannel).new OrcaReportListener(errorUtilizationPenalty)); } this.enableOobLoadReport = enableOobLoadReport; this.errorUtilizationPenalty = errorUtilizationPenalty; @@ -276,12 +276,12 @@ final class WeightedRoundRobinPicker extends RoundRobinPicker { @Override public PickResult pickSubchannel(PickSubchannelArgs args) { - Subchannel subchannel = list.get(ssScheduler.pick()); + Subchannel subchannel = list.get(scheduler.pick()); if (!enableOobLoadReport) { return PickResult.withSubchannel(subchannel, OrcaPerRequestUtil.getInstance().newOrcaClientStreamTracerFactory( subchannelToReportListenerMap.getOrDefault(subchannel, - ((WrrSubchannel) subchannel).new OrcaReportListener(errorUtilizationPenalty)))); + ((WrrSubchannel) subchannel).new OrcaReportListener(errorUtilizationPenalty)))); } else { return PickResult.withSubchannel(subchannel); } @@ -295,16 +295,16 @@ private void updateWeight() { newWeights[i] = newWeight > 0 ? (float) newWeight : 0.0f; } - StaticStrideScheduler ssScheduler = new StaticStrideScheduler(newWeights, random); - this.ssScheduler = ssScheduler; + StaticStrideScheduler scheduler = new StaticStrideScheduler(newWeights, random); + this.scheduler = scheduler; } @Override public String toString() { return MoreObjects.toStringHelper(WeightedRoundRobinPicker.class) - .add("enableOobLoadReport", enableOobLoadReport) - .add("errorUtilizationPenalty", errorUtilizationPenalty) - .add("list", list).toString(); + .add("enableOobLoadReport", enableOobLoadReport) + .add("errorUtilizationPenalty", errorUtilizationPenalty) + .add("list", list).toString(); } @VisibleForTesting @@ -323,8 +323,8 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { } // the lists cannot contain duplicate subchannels return enableOobLoadReport == other.enableOobLoadReport - && Float.compare(errorUtilizationPenalty, other.errorUtilizationPenalty) == 0 - && list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list); + && Float.compare(errorUtilizationPenalty, other.errorUtilizationPenalty) == 0 + && list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list); } } From f526bf67f1ae1edef55fe24c80bfb3f2171ae8f2 Mon Sep 17 00:00:00 2001 From: Tony An <40644135+tonyjongyoonan@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:30:49 -0700 Subject: [PATCH 36/42] Delete settings.json --- .vscode/settings.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 6b5a77b3ddf..00000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "editor.rulers": [ - 100 - ], - "github.gitAuthentication": true, - "git.terminalAuthentication": false -} \ No newline at end of file From 21ceb85bace54eb5583c0078f27467aee896e770 Mon Sep 17 00:00:00 2001 From: tonyjongyoonan Date: Fri, 23 Jun 2023 14:47:11 -0700 Subject: [PATCH 37/42] modified test --- .../grpc/xds/WeightedRoundRobinLoadBalancerTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index b35ed503a79..4e0d22d532f 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -986,7 +986,7 @@ public void testTwoWeights() { } @Test - public void testManyWeights1() { + public void testManyWeights() { float[] weights = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; Random random = new Random(); StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); @@ -998,12 +998,12 @@ public void testManyWeights1() { } for (int i = 0; i < 5; i++) { assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight)) - .isAtMost(0.001); + .isAtMost(0.0011); } } @Test - public void testManyComplexWeights2() { + public void testManyComplexWeights() { float[] weights = {1.2f, 2.4f, 222.56f, 1.1f, 15.0f, 226342.0f, 5123.0f, 532.2f}; Random random = new Random(); StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); @@ -1040,7 +1040,7 @@ public void testDeterministicPicks() { } @Test - public void testWraparound() { + public void testImmediateWraparound() { float[] weights = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; Random random = new FakeRandomWraparound(); StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); @@ -1057,7 +1057,7 @@ public void testWraparound() { } @Test - public void testWraparound2() { + public void testWraparound() { float[] weights = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; Random random = new FakeRandomWraparound2(); StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); From 03de3f98e9cee5808b3090296612ca4d7cf3cfb6 Mon Sep 17 00:00:00 2001 From: tonyjongyoonan Date: Mon, 26 Jun 2023 14:16:33 -0700 Subject: [PATCH 38/42] fixed feedback --- .../xds/WeightedRoundRobinLoadBalancer.java | 16 ++++---- .../WeightedRoundRobinLoadBalancerTest.java | 38 ++++++++----------- 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 52492376d06..160244c062c 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -332,10 +332,12 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { * Implementation of Static Stride Scheduler, replaces EDFScheduler. *

      * The Static Stride Scheduler works by iterating through the list of subchannel weights - * and using modular arithmetic to evenly distribute picks and skips, favoring entries with the - * highest weight. It generates a practically equivalent sequence of picks as the EDFScheduler. - * Albeit needing more bandwidth, the Static Stride Scheduler is more performant than the - * EDFScheduler, as it removes the need for a priority queue (and thus mutex locks). + * and using modular arithmetic to proportionally distribute picks, favoring entries + * with higher weights. It is based on the observation that the intended sequence generated + * from the EDF scheduler is a periodic one that can be achieved through modular arithmetic. + * This scheduler generates a practically equivalent sequence of picks as the EDFScheduler. + * The Static Stride Scheduler is more performant than the EDFScheduler, as it removes + * the need for a priority queue (and thus mutex locks). *

      * go/static-stride-scheduler *

      @@ -359,7 +361,7 @@ static final class StaticStrideScheduler { float maxWeight = 0; int meanWeight = 0; for (float weight : weights) { - if (weight > 0.0001) { + if (weight > 0) { sumWeight += weight; maxWeight = Math.max(weight, maxWeight); numWeightedChannels++; @@ -376,7 +378,7 @@ static final class StaticStrideScheduler { // scales weights s.t. max(weights) == K_MAX_WEIGHT, meanWeight is scaled accordingly int[] scaledWeights = new int[numChannels]; for (int i = 0; i < numChannels; i++) { - if (weights[i] < 0.0001) { + if (weights[i] <= 0) { scaledWeights[i] = meanWeight; } else { scaledWeights[i] = (int) Math.round(weights[i] * scalingFactor); @@ -394,7 +396,7 @@ private long nextSequence() { return Integer.toUnsignedLong(sequence.getAndIncrement()); } - public long getSequence() { + long getSequence() { return Integer.toUnsignedLong(sequence.get()); } diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 4e0d22d532f..48bf756c682 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -175,7 +175,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { } }); wrr = new WeightedRoundRobinLoadBalancer(helper, fakeClock.getDeadlineTicker(), - new FakeRandom()); + new FakeRandom(0)); } @Test @@ -919,15 +919,15 @@ public void testAllInvalidWeightsUseOne() { } @Test - public void testLacrgestWeightIndexPickedEveryGeneration() { + public void testLargestWeightIndexPickedEveryGeneration() { float[] weights = {1.0f, 2.0f, 3.0f}; - int mean = 2; + int largestWeightIndex = 2; Random random = new Random(); StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); int largestWeightPickCount = 0; int kMaxWeight = 65535; - for (int i = 0; i < mean * kMaxWeight; i++) { - if (sss.pick() == mean) { + for (int i = 0; i < largestWeightIndex * kMaxWeight; i++) { + if (sss.pick() == largestWeightIndex) { largestWeightPickCount += 1; } } @@ -1022,7 +1022,7 @@ public void testManyComplexWeights() { @Test public void testDeterministicPicks() { float[] weights = {2.0f, 3.0f, 6.0f}; - Random random = new FakeRandom(); + Random random = new FakeRandom(0); StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); assertThat(sss.getSequence()).isEqualTo(0); assertThat(sss.pick()).isEqualTo(1); @@ -1042,7 +1042,7 @@ public void testDeterministicPicks() { @Test public void testImmediateWraparound() { float[] weights = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; - Random random = new FakeRandomWraparound(); + Random random = new FakeRandom(-1); StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); double totalWeight = 15; Map pickCount = new HashMap<>(); @@ -1059,7 +1059,7 @@ public void testImmediateWraparound() { @Test public void testWraparound() { float[] weights = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; - Random random = new FakeRandomWraparound2(); + Random random = new FakeRandom(-500); StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); double totalWeight = 15; Map pickCount = new HashMap<>(); @@ -1076,7 +1076,7 @@ public void testWraparound() { @Test public void testDeterministicWraparound() { float[] weights = {2.0f, 3.0f, 6.0f}; - Random random = new FakeRandomWraparound(); + Random random = new FakeRandom(-1); StaticStrideScheduler sss = new StaticStrideScheduler(weights, random); assertThat(sss.getSequence()).isEqualTo(0xFFFF_FFFFL); assertThat(sss.pick()).isEqualTo(1); @@ -1106,25 +1106,17 @@ private static class FakeSocketAddress extends SocketAddress { } private static class FakeRandom extends Random { - @Override - public int nextInt() { - // return constant value to disable init deadline randomization in the scheduler - // sequence will be initialized to 0 - return 0; - } - } + private int nextInt; - private static class FakeRandomWraparound extends Random { - @Override - public int nextInt() { - return -1; + public FakeRandom(int nextInt) { + this.nextInt = nextInt; } - } - private static class FakeRandomWraparound2 extends Random { @Override public int nextInt() { - return -500; + // return constant value to disable init deadline randomization in the scheduler + // sequence will be initialized to 0 + return nextInt; } } } From e2eb7f9bd73bd440426ece4e70ab373ecb5ad3a6 Mon Sep 17 00:00:00 2001 From: tonyjongyoonan Date: Tue, 27 Jun 2023 12:39:25 -0700 Subject: [PATCH 39/42] changed weights to short from int --- .../xds/WeightedRoundRobinLoadBalancer.java | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 160244c062c..a5308015a49 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -329,15 +329,20 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { } /* - * Implementation of Static Stride Scheduler, replaces EDFScheduler. + * The Static Stride Scheduler is an implementation of an earliest deadline first (EDF) scheduler + * in which each object is chosen periodically with frequency proportional to its weight. *

      + * Specifically, each backend is given a deadline equal to the multiplicative inverse of + * its weight. The place of each backend in its deadline is tracked, and each call to + * pick a backend returns the backend index with the least remaining time in its deadline. + *

      + * The way in which this is implemented is through a static stride scheduler. * The Static Stride Scheduler works by iterating through the list of subchannel weights * and using modular arithmetic to proportionally distribute picks, favoring entries * with higher weights. It is based on the observation that the intended sequence generated - * from the EDF scheduler is a periodic one that can be achieved through modular arithmetic. - * This scheduler generates a practically equivalent sequence of picks as the EDFScheduler. - * The Static Stride Scheduler is more performant than the EDFScheduler, as it removes - * the need for a priority queue (and thus mutex locks). + * from an EDF scheduler is a periodic one that can be achieved through modular arithmetic. + * The Static Stride Scheduler is more performant than other implementations of the EDF + * Scheduler, as it removes the need for a priority queue (and thus mutex locks). *

      * go/static-stride-scheduler *

      @@ -348,7 +353,7 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { */ @VisibleForTesting static final class StaticStrideScheduler { - private final int[] scaledWeights; + private final short[] scaledWeights; private final int sizeDivisor; private final AtomicInteger sequence; private static final int K_MAX_WEIGHT = 0xFFFF; @@ -359,7 +364,7 @@ static final class StaticStrideScheduler { int numWeightedChannels = 0; double sumWeight = 0; float maxWeight = 0; - int meanWeight = 0; + short meanWeight = 0; for (float weight : weights) { if (weight > 0) { sumWeight += weight; @@ -370,18 +375,19 @@ static final class StaticStrideScheduler { double scalingFactor = K_MAX_WEIGHT / maxWeight; if (numWeightedChannels > 0) { - meanWeight = (int) Math.round(scalingFactor * sumWeight / numWeightedChannels); + int value = (int) Math.round(scalingFactor * sumWeight / numWeightedChannels); + meanWeight = (short) (value > 0x7FFF ? value - 0x10000 : value); } else { meanWeight = 1; } // scales weights s.t. max(weights) == K_MAX_WEIGHT, meanWeight is scaled accordingly - int[] scaledWeights = new int[numChannels]; + short[] scaledWeights = new short[numChannels]; for (int i = 0; i < numChannels; i++) { if (weights[i] <= 0) { scaledWeights[i] = meanWeight; } else { - scaledWeights[i] = (int) Math.round(weights[i] * scalingFactor); + scaledWeights[i] = (short) Math.round(weights[i] * scalingFactor); } } @@ -414,7 +420,7 @@ int pick() { long sequence = this.nextSequence(); int backendIndex = (int) (sequence % this.sizeDivisor); long generation = sequence / this.sizeDivisor; - long weight = this.scaledWeights[backendIndex]; + int weight = Short.toUnsignedInt(this.scaledWeights[backendIndex]); long offset = (long) K_MAX_WEIGHT / 2 * backendIndex; if ((weight * generation + offset) % K_MAX_WEIGHT < K_MAX_WEIGHT - weight) { continue; From 40729079f3b0ef4fd3a09458bf6882d5a9dc40de Mon Sep 17 00:00:00 2001 From: tonyjongyoonan Date: Thu, 29 Jun 2023 16:49:03 -0700 Subject: [PATCH 40/42] fixed message and short logic --- .../xds/WeightedRoundRobinLoadBalancer.java | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index a5308015a49..76afd697af8 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -332,10 +332,6 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { * The Static Stride Scheduler is an implementation of an earliest deadline first (EDF) scheduler * in which each object is chosen periodically with frequency proportional to its weight. *

      - * Specifically, each backend is given a deadline equal to the multiplicative inverse of - * its weight. The place of each backend in its deadline is tracked, and each call to - * pick a backend returns the backend index with the least remaining time in its deadline. - *

      * The way in which this is implemented is through a static stride scheduler. * The Static Stride Scheduler works by iterating through the list of subchannel weights * and using modular arithmetic to proportionally distribute picks, favoring entries @@ -375,8 +371,7 @@ static final class StaticStrideScheduler { double scalingFactor = K_MAX_WEIGHT / maxWeight; if (numWeightedChannels > 0) { - int value = (int) Math.round(scalingFactor * sumWeight / numWeightedChannels); - meanWeight = (short) (value > 0x7FFF ? value - 0x10000 : value); + meanWeight = (short) Math.round(scalingFactor * sumWeight / numWeightedChannels); } else { meanWeight = 1; } @@ -409,11 +404,32 @@ long getSequence() { /* * Selects index of next backend server. *

      - * A 2D array is compactly represented where the row represents the generation and the column - * represents the backend index. The value of an element is a boolean value which indicates - * whether or not a backend should be picked now. An atomically incremented counter keeps track - * of our backend and generation through modular arithmetic within the pick() method. - * An offset is also included to minimize consecutive non-picks of a backend. + * A 2D array is compactly represented as a function of W(backend), where the row + * represents the generation and the column represents the backend index: + * X(backend,generation) | generation ∈ [0,kMaxWeight). + * Each element in the conceptual array is a boolean indicating whether the backend at + * this index should be picked now. If false, the counter is incremented again, + * and the new element is checked. An atomically incremented counter keeps track of our + * backend and generation through modular arithmetic within the pick() method. + *

      + * Modular arithmetic allows us to evenly distribute picks and skips between + * generations based on W(backend). + * X(backend,generation) = (W(backend) * generation) % kMaxWeight >= kMaxWeight - W(backend) + * If we have the same three backends with weights: + * W(backend) = {2,3,6} scaled to max(W(backend)) = 6, then X(backend,generation) is: + *

      + * B0 B1 B2 + * T T T + * F F T + * F T T + * T F T + * F T T + * F F T + * The sequence of picked backend indices is given by + * walking across and down: {0,1,2,2,1,2,0,2,1,2,2}. + *

      + * To reduce the variance and spread the wasted work among different picks, + * an offset that varies per backend index is also included to the calculation. */ int pick() { while (true) { From f749e52b46659488ab8721128411ba85fbd168e5 Mon Sep 17 00:00:00 2001 From: tonyjongyoonan Date: Sat, 1 Jul 2023 00:08:54 -0700 Subject: [PATCH 41/42] removed obselete comment --- .../java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index 48bf756c682..58a19af96ad 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -1115,7 +1115,6 @@ public FakeRandom(int nextInt) { @Override public int nextInt() { // return constant value to disable init deadline randomization in the scheduler - // sequence will be initialized to 0 return nextInt; } } From be21c23c12a91d09adcc13b35ed2027a9554fbc9 Mon Sep 17 00:00:00 2001 From: tonyjongyoonan Date: Wed, 5 Jul 2023 10:25:51 -0700 Subject: [PATCH 42/42] changed annotations --- .../main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 76afd697af8..d5d8c4d9e45 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -330,7 +330,7 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { /* * The Static Stride Scheduler is an implementation of an earliest deadline first (EDF) scheduler - * in which each object is chosen periodically with frequency proportional to its weight. + * in which each object's deadline is the multiplicative inverse of the object's weight. *

      * The way in which this is implemented is through a static stride scheduler. * The Static Stride Scheduler works by iterating through the list of subchannel weights @@ -397,6 +397,7 @@ private long nextSequence() { return Integer.toUnsignedLong(sequence.getAndIncrement()); } + @VisibleForTesting long getSequence() { return Integer.toUnsignedLong(sequence.get()); }