2424import org .openqa .selenium .grid .data .SlotId ;
2525
2626import java .util .Comparator ;
27- import java .util .HashMap ;
28- import java .util .HashSet ;
29- import java .util .LinkedHashSet ;
30- import java .util .List ;
31- import java .util .Map ;
32- import java .util .Optional ;
3327import java .util .Set ;
34- import java .util .logging .Logger ;
35- import java .util .stream .Collectors ;
36- import java .util .stream .Stream ;
3728
3829import static com .google .common .collect .ImmutableSet .toImmutableSet ;
3930
4031public class DefaultSlotSelector implements SlotSelector {
4132
42- private static final Logger LOG = Logger .getLogger (DefaultSlotSelector .class .getName ());
43-
4433 @ Override
4534 public Set <SlotId > selectSlot (Capabilities capabilities , Set <NodeStatus > nodes ) {
46- Stream <NodeStatus > firstRound = nodes .stream ()
47- // Find a node that supports this kind of thing
48- .filter (node -> node .hasCapacity (capabilities ));
49-
50- // of the nodes that survived the first round, separate into buckets and prioritize by browser "rarity"
51- Stream <NodeStatus > prioritizedNodes = getPrioritizedNodeStream (firstRound , capabilities );
52-
53- //Take the further-filtered Stream and prioritize by load, then by session age
54-
55- return prioritizedNodes
35+ // First, filter the Nodes that support the required capabilities. Then, the filtered Nodes
36+ // get ordered in ascendant order by the number of browsers they support.
37+ // With this, Nodes with diverse configurations (supporting many browsers, e.g. Chrome,
38+ // Firefox, Safari) are placed at the bottom so they have more availability when a session
39+ // requests a browser supported only by a few Nodes (e.g. Safari only supported on macOS
40+ // Nodes).
41+ // After that, Nodes are ordered by their load, last session creation, and their id.
42+ return nodes .stream ()
43+ .filter (node -> node .hasCapacity (capabilities ))
5644 .sorted (
45+ Comparator .comparingLong (this ::getNumberOfSupportedBrowsers )
5746 // Now sort by node which has the lowest load (natural ordering)
58- Comparator . comparingDouble (NodeStatus ::getLoad )
47+ . thenComparingDouble (NodeStatus ::getLoad )
5948 // Then last session created (oldest first), so natural ordering again
6049 .thenComparingLong (NodeStatus ::getLastSessionCreated )
6150 // And use the node id as a tie-breaker.
@@ -67,99 +56,13 @@ public Set<SlotId> selectSlot(Capabilities capabilities, Set<NodeStatus> nodes)
6756 .collect (toImmutableSet ());
6857 }
6958
70- /**
71- * Takes a Stream of NodeStatus, along with the Capabilities of the current request, and prioritizes the
72- * request by removing NodeStatus that offer Capabilities that are more rare. e.g. if there are only a
73- * couple Edge nodes, but a lot of Chrome nodes, the Edge nodes should be removed from
74- * consideration when Chrome is requested. This does not currently take the amount of load on the
75- * server into consideration--it only checks for availability, not how much availability
76- *
77- * @param nodes Stream of nodestatus attached to the Distributor (assume it's filtered for only those that offer these Capabilities)
78- * @param capabilities Passing in the whole Capabilities object will allow us to prioritize more than just browser
79- * @return Stream of distinct NodeStatus with the more rare Capabilities removed
80- */
8159 @ VisibleForTesting
82- Stream <NodeStatus > getPrioritizedNodeStream (Stream <NodeStatus > nodes , Capabilities capabilities ) {
83- //TODO for the moment, we're not going to operate on the Stream that was passed in--we need to
84- // alter and futz with the contents, so the stream isn't the right place to operate. This
85- // will likely be optimized back into the algo, but not yet
86- Set <NodeStatus > filteredNodeSet = nodes .collect (Collectors .toSet ());
87-
88- //A "bucket" is a list of nodes that can use a particular browser. The "edge" bucket is the
89- // complete list of nodes that support "edge". By separating nodes into buckets, we will
90- // know which browsers have fewer nodes available for the browsers we're not interested in, and
91- // can prioritize based on the browsers that have more availability
92- Map <String , Set <NodeStatus >> nodeBuckets = sortNodesToBucketsByBrowser (filteredNodeSet );
93-
94- //First, check to see if all buckets are the same size. If they are, just send back the full list of nodes
95- // (i.e. the nodes are all "balanced" with regard to browser priority)
96- if (allBucketsSameSize (nodeBuckets )) {
97- return nodeBuckets .values ().stream ().distinct ().flatMap (Set ::stream );
98- }
99-
100- //Then, starting with the smallest bucket that isn't the current browser being prioritized,
101- // remove all nodes in that bucket from consideration, then rebuild the buckets. Then do the
102- // "same size" check again, and keep doing this until either a) there is only one bucket, or b)
103- // all buckets are the same size
104-
105- //Note: there should never be a case where a bucket will have *more* nodes available for the
106- // given browser than the one being requested. The first filter in this check looks for
107- // "equal", not "equal-or-greater" as a result of this assumption
108-
109- //There might be unforeseen cases that challenge this assumption. TODO Create unit tests to prove it
110-
111- //TODO a List of Map.Entry is silly. whatever this structure needs to be needs to be returned by
112- // the sortHostsToBucketsByBrowser method in a way that we don't have to sort it separately like this
113- final List <Map .Entry <String , Set <NodeStatus >>> sorted = nodeBuckets .entrySet ().stream ().sorted (
114- Comparator .comparingInt (v -> v .getValue ().size ())
115- ).collect (Collectors .toList ());
116-
117- // Until the buckets are the same size, keep removing nodes that have more "rare" browser capabilities
118- Map <String , Set <NodeStatus >> newNodeBuckets ;
119- for (Map .Entry <String , Set <NodeStatus >> entry : sorted ) {
120- //Don't examine the bucket containing the browser in question--we're prioritizing the other browsers
121- //TODO This shouldn't be necessary, because if the list is sorted by size, this won't be possible until
122- // they're all the same size. Create a unit test to prove it
123- if (entry .getKey ().equals (capabilities .getBrowserName ())) {
124- continue ;
125- }
126-
127- //Remove all nodes from this bucket from the full set of eligible nodes
128- final Set <NodeStatus > filteredNodes = filteredNodeSet .stream ()
129- .filter (node -> !entry .getValue ().contains (node ))
130- .collect (Collectors .toSet ());
131-
132- //Rebuild the buckets by browser
133- newNodeBuckets = sortNodesToBucketsByBrowser (filteredNodes );
134-
135- //Check the bucket sizes--if they're the same, then we're done
136- if (allBucketsSameSize (newNodeBuckets )) {
137- LOG .fine ("Nodes have been balanced according to browser priority" );
138- return newNodeBuckets .values ().stream ().distinct ().flatMap (Set ::stream );
139- }
140- }
141-
142- return nodeBuckets .values ().stream ().distinct ().flatMap (Set ::stream );
143- }
144-
145- Map <String , Set <NodeStatus >> sortNodesToBucketsByBrowser (Set <NodeStatus > nodes ) {
146- //Make a hash of browserType -> list of nodes that support it
147- Map <String , Set <NodeStatus >> buckets = new HashMap <>();
148-
149- for (NodeStatus node : nodes ) {
150- for (Slot slot : node .getSlots ()) {
151- String name = Optional .ofNullable (slot .getStereotype ().getBrowserName ()).orElse ("" );
152- buckets .computeIfAbsent (name , n -> new LinkedHashSet <>()).add (node );
153- }
154- }
155-
156- return buckets ;
60+ long getNumberOfSupportedBrowsers (NodeStatus nodeStatus ) {
61+ return nodeStatus .getSlots ()
62+ .stream ()
63+ .map (slot -> slot .getStereotype ().getBrowserName ().toLowerCase ())
64+ .distinct ()
65+ .count ();
15766 }
15867
159- @ VisibleForTesting
160- boolean allBucketsSameSize (Map <String , Set <NodeStatus >> buckets ) {
161- Set <Integer > intSet = new HashSet <>();
162- buckets .values ().forEach (bucket -> intSet .add (bucket .size ()));
163- return intSet .size () == 1 ;
164- }
16568}
0 commit comments