Skip to content

Commit 71a813e

Browse files
committed
[grid] Add GreedySlotSelector as a built-in slot-matcher option
Signed-off-by: Viet Nguyen Duc <[email protected]>
1 parent 1fe6d27 commit 71a813e

File tree

2 files changed

+528
-0
lines changed

2 files changed

+528
-0
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.openqa.selenium.grid.distributor.selector;
19+
20+
import static com.google.common.collect.ImmutableSet.toImmutableSet;
21+
import static org.openqa.selenium.grid.data.Availability.UP;
22+
23+
import com.google.common.annotations.VisibleForTesting;
24+
import java.util.Comparator;
25+
import java.util.Locale;
26+
import java.util.Set;
27+
import org.openqa.selenium.Capabilities;
28+
import org.openqa.selenium.grid.config.Config;
29+
import org.openqa.selenium.grid.data.NodeStatus;
30+
import org.openqa.selenium.grid.data.SemanticVersionComparator;
31+
import org.openqa.selenium.grid.data.Slot;
32+
import org.openqa.selenium.grid.data.SlotId;
33+
import org.openqa.selenium.grid.data.SlotMatcher;
34+
35+
/**
36+
* A greedy slot selector that aims to maximize node utilization by minimizing the number of
37+
* partially filled nodes. The algorithm works as follows: 1. For each node, calculate its
38+
* utilization ratio (used slots / total slots) 2. Sort nodes by utilization ratio in descending
39+
* order 3. For nodes with same utilization, prefer those with fewer total slots 4. For nodes with
40+
* same utilization and total slots, prefer those with lower load This approach helps to: - Fill up
41+
* nodes that are already partially utilized - Minimize the number of nodes that are partially
42+
* filled - Distribute load evenly across nodes
43+
*/
44+
public class GreedySlotSelector implements SlotSelector {
45+
46+
public static SlotSelector create(Config config) {
47+
return new GreedySlotSelector();
48+
}
49+
50+
@Override
51+
public Set<SlotId> selectSlot(
52+
Capabilities capabilities, Set<NodeStatus> nodes, SlotMatcher slotMatcher) {
53+
return nodes.stream()
54+
.filter(node -> node.hasCapacity(capabilities, slotMatcher) && node.getAvailability() == UP)
55+
.sorted(
56+
// First and foremost, sort by utilization ratio (descending)
57+
// This ensures we ALWAYS try to fill nodes that are already partially utilized first
58+
Comparator.comparingDouble(this::getNodeUtilization)
59+
.reversed()
60+
// Then sort by total number of slots (ascending)
61+
// Among nodes with same utilization, prefer those with fewer total slots
62+
.thenComparingLong(node -> node.getSlots().size())
63+
// Then sort by node which has the lowest load (natural ordering)
64+
.thenComparingDouble(NodeStatus::getLoad)
65+
// Then last session created (oldest first)
66+
.thenComparingLong(NodeStatus::getLastSessionCreated)
67+
// Then sort by stereotype browserVersion (descending order)
68+
.thenComparing(
69+
Comparator.comparing(
70+
NodeStatus::getBrowserVersion, new SemanticVersionComparator().reversed()))
71+
// And use the node id as a tie-breaker
72+
.thenComparing(NodeStatus::getNodeId))
73+
.flatMap(
74+
node ->
75+
node.getSlots().stream()
76+
.filter(slot -> slot.getSession() == null)
77+
.filter(slot -> slot.isSupporting(capabilities, slotMatcher))
78+
.map(Slot::getId))
79+
.collect(toImmutableSet());
80+
}
81+
82+
@VisibleForTesting
83+
double getNodeUtilization(NodeStatus node) {
84+
long totalSlots = node.getSlots().size();
85+
if (totalSlots == 0) {
86+
return 0.0;
87+
}
88+
long usedSlots = node.getSlots().stream().filter(slot -> slot.getSession() != null).count();
89+
return (double) usedSlots / totalSlots;
90+
}
91+
92+
@VisibleForTesting
93+
long getNumberOfSupportedBrowsers(NodeStatus nodeStatus) {
94+
return nodeStatus.getSlots().stream()
95+
.map(slot -> slot.getStereotype().getBrowserName().toLowerCase(Locale.ENGLISH))
96+
.distinct()
97+
.count();
98+
}
99+
}

0 commit comments

Comments
 (0)