Skip to content

Commit e0c3852

Browse files
committed
[grid] Make HostSelector an interface
1 parent d6c10c6 commit e0c3852

4 files changed

Lines changed: 170 additions & 139 deletions

File tree

java/server/src/org/openqa/selenium/grid/distributor/local/LocalDistributor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.openqa.selenium.grid.data.NodeStatus;
3535
import org.openqa.selenium.grid.distributor.Distributor;
3636
import org.openqa.selenium.grid.distributor.model.Host;
37+
import org.openqa.selenium.grid.distributor.selector.DefaultHostSelector;
3738
import org.openqa.selenium.grid.distributor.selector.HostSelector;
3839
import org.openqa.selenium.grid.log.LoggingOptions;
3940
import org.openqa.selenium.grid.node.Node;
@@ -179,7 +180,7 @@ public CreateSessionResponse newSession(HttpRequest request)
179180
Lock writeLock = this.lock.writeLock();
180181
writeLock.lock();
181182
try {
182-
HostSelector hostSelector = new HostSelector();
183+
HostSelector hostSelector = new DefaultHostSelector();
183184
// Find a host that supports the capabilities present in the new session
184185
Optional<Host> selectedHost = hostSelector.selectHost(firstRequest.getCapabilities(), this.hosts);
185186
// Reserve some space for this session
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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 com.google.common.annotations.VisibleForTesting;
21+
import org.openqa.selenium.Capabilities;
22+
import org.openqa.selenium.grid.distributor.model.Host;
23+
24+
import java.util.Comparator;
25+
import java.util.HashMap;
26+
import java.util.HashSet;
27+
import java.util.List;
28+
import java.util.Map;
29+
import java.util.Optional;
30+
import java.util.Set;
31+
import java.util.logging.Logger;
32+
import java.util.stream.Collectors;
33+
import java.util.stream.Stream;
34+
35+
import static org.openqa.selenium.grid.distributor.model.Host.Status.UP;
36+
37+
public class DefaultHostSelector implements HostSelector {
38+
39+
private static final Logger LOG = Logger.getLogger("Selenium Host Selector");
40+
41+
@Override
42+
public Optional<Host> selectHost(Capabilities capabilities, Set<Host> hosts) {
43+
Optional<Host> selected;
44+
Stream<Host> firstRound = hosts.stream()
45+
.filter(host -> host.getHostStatus() == UP)
46+
// Find a host that supports this kind of thing
47+
.filter(host -> host.hasCapacity(capabilities));
48+
49+
//of the hosts that survived the first round, separate into buckets and prioritize by browser "rarity"
50+
Stream<Host> prioritizedHosts = getPrioritizedHostStream(firstRound, capabilities);
51+
52+
//Take the further-filtered Stream and prioritize by load, then by session age
53+
selected = prioritizedHosts
54+
.min(
55+
// Now sort by node which has the lowest load (natural ordering)
56+
Comparator.comparingDouble(Host::getLoad)
57+
// Then last session created (oldest first), so natural ordering again
58+
.thenComparingLong(Host::getLastSessionCreated)
59+
// And use the host id as a tie-breaker.
60+
.thenComparing(Host::getId));
61+
return selected;
62+
}
63+
64+
/**
65+
* Takes a Stream of Hosts, along with the Capabilities of the current request, and prioritizes the
66+
* request by removing Hosts that offer Capabilities that are more rare. e.g. if there are only a
67+
* couple Edge nodes, but a lot of Chrome nodes, the Edge nodes should be removed from
68+
* consideration when Chrome is requested. This does not currently take the amount of load on the
69+
* server into consideration--it only checks for availability, not how much availability
70+
* @param hostStream Stream of hosts attached to the Distributor (assume it's filtered for only those that offer these Capabilities)
71+
* @param capabilities Passing in the whole Capabilities object will allow us to prioritize more than just browser
72+
* @return Stream of distinct Hosts with the more rare Capabilities removed
73+
*/
74+
@VisibleForTesting
75+
Stream<Host> getPrioritizedHostStream(Stream<Host> hostStream, Capabilities capabilities) {
76+
//TODO for the moment, we're not going to operate on the Stream that was passed in--we need to
77+
// alter and futz with the contents, so the stream isn't the right place to operate. This
78+
// will likely be optimized back into the algo, but not yet
79+
Set<Host> filteredHostSet = hostStream.collect(Collectors.toSet());
80+
81+
//A "bucket" is a list of hosts that can use a particular browser. The "edge" bucket is the
82+
// complete list of Hosts that support "edge". By separating Hosts into buckets, we will
83+
// know which browsers have fewer nodes available for the browsers we're not interested in, and
84+
// can prioritize based on the browsers that have more availability
85+
Map<String, Set<Host>> hostBuckets = sortHostsToBucketsByBrowser(filteredHostSet);
86+
87+
//First, check to see if all buckets are the same size. If they are, just send back the full list of hosts
88+
// (i.e. the hosts are all "balanced" with regard to browser priority)
89+
if (allBucketsSameSize(hostBuckets)) {
90+
return hostBuckets.values().stream().distinct().flatMap(Set::stream);
91+
}
92+
93+
//Then, starting with the smallest bucket that isn't the current browser being prioritized,
94+
// remove all hosts in that bucket from consideration, then rebuild the buckets. Then do the
95+
// "same size" check again, and keep doing this until either a) there is only one bucket, or b)
96+
// all buckets are the same size
97+
98+
//Note: there should never be a case where a bucket will have *more* nodes available for the
99+
// given browser than the one being requested. The first filter in this check looks for
100+
// "equal", not "equal-or-greater" as a result of this assumption
101+
102+
//There might be unforeseen cases that challenge this assumption. TODO Create unit tests to prove it
103+
104+
//TODO a List of Map.Entry is silly. whatever this structure needs to be needs to be returned by
105+
// the sortHostsToBucketsByBrowser method in a way that we don't have to sort it separately like this
106+
final List<Map.Entry<String, Set<Host>>> sorted = hostBuckets.entrySet().stream().sorted(
107+
Comparator.comparingInt(v -> v.getValue().size())
108+
).collect(Collectors.toList());
109+
110+
// Until the buckets are the same size, keep removing hosts that have more "rare" browser capabilities
111+
Map<String, Set<Host>> newHostBuckets;
112+
for (Map.Entry<String, Set<Host>> entry : sorted) {
113+
//Don't examine the bucket containing the browser in question--we're prioritizing the other browsers
114+
//TODO This shouldn't be necessary, because if the list is sorted by size, this won't be possible until
115+
// they're all the same size. Create a unit test to prove it
116+
if (entry.getKey().equals(capabilities.getBrowserName())) {
117+
continue;
118+
}
119+
120+
//Remove all hosts from this bucket from the full set of eligible hosts
121+
final Set<Host> filteredHosts = filteredHostSet.stream().filter(host -> !entry.getValue().contains(host)).collect(Collectors.toSet());
122+
123+
//Rebuild the buckets by browser
124+
newHostBuckets = sortHostsToBucketsByBrowser(filteredHosts);
125+
126+
//Check the bucket sizes--if they're the same, then we're done
127+
if (allBucketsSameSize(newHostBuckets)) {
128+
LOG.fine("Hosts have been balanced according to browser priority");
129+
return newHostBuckets.values().stream().distinct().flatMap(Set::stream);
130+
}
131+
}
132+
133+
return hostBuckets.values().stream().distinct().flatMap(Set::stream);
134+
}
135+
136+
Map<String, Set<Host>> sortHostsToBucketsByBrowser(Set<Host> hostSet) {
137+
//Make a hash of browserType -> list of hosts that support it
138+
Map<String, Set<Host>> hostBuckets = new HashMap<>();
139+
hostSet.forEach(host -> host.asSummary().getStereotypes().forEach((k, v) -> {
140+
if (!hostBuckets.containsKey(k.getBrowserName())) {
141+
Set<Host> newSet = new HashSet<>();
142+
newSet.add(host);
143+
hostBuckets.put(k.getBrowserName(), newSet);
144+
}
145+
hostBuckets.get(k.getBrowserName()).add(host);
146+
}));
147+
return hostBuckets;
148+
}
149+
150+
@VisibleForTesting
151+
boolean allBucketsSameSize(Map<String, Set<Host>> hostBuckets) {
152+
Set<Integer> intSet = new HashSet<>();
153+
hostBuckets.values().forEach(bucket -> intSet.add(bucket.size()));
154+
return intSet.size() == 1;
155+
}
156+
}

java/server/src/org/openqa/selenium/grid/distributor/selector/HostSelector.java

Lines changed: 6 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -17,142 +17,16 @@
1717

1818
package org.openqa.selenium.grid.distributor.selector;
1919

20-
import com.google.common.annotations.VisibleForTesting;
2120
import org.openqa.selenium.Capabilities;
2221
import org.openqa.selenium.grid.distributor.model.Host;
2322

24-
import java.util.Comparator;
25-
import java.util.HashMap;
26-
import java.util.HashSet;
27-
import java.util.List;
28-
import java.util.Map;
2923
import java.util.Optional;
3024
import java.util.Set;
31-
import java.util.logging.Logger;
32-
import java.util.stream.Collectors;
33-
import java.util.stream.Stream;
3425

35-
import static org.openqa.selenium.grid.distributor.model.Host.Status.UP;
36-
37-
public class HostSelector {
38-
39-
private static final Logger LOG = Logger.getLogger("Selenium Host Selector");
40-
41-
public HostSelector() {
42-
}
43-
44-
public Optional<Host> selectHost(Capabilities capabilities, Set<Host> hosts) {
45-
Optional<Host> selected;
46-
Stream<Host> firstRound = hosts.stream()
47-
.filter(host -> host.getHostStatus() == UP)
48-
// Find a host that supports this kind of thing
49-
.filter(host -> host.hasCapacity(capabilities));
50-
51-
//of the hosts that survived the first round, separate into buckets and prioritize by browser "rarity"
52-
Stream<Host> prioritizedHosts = getPrioritizedHostStream(firstRound, capabilities);
53-
54-
//Take the further-filtered Stream and prioritize by load, then by session age
55-
selected = prioritizedHosts
56-
.min(
57-
// Now sort by node which has the lowest load (natural ordering)
58-
Comparator.comparingDouble(Host::getLoad)
59-
// Then last session created (oldest first), so natural ordering again
60-
.thenComparingLong(Host::getLastSessionCreated)
61-
// And use the host id as a tie-breaker.
62-
.thenComparing(Host::getId));
63-
return selected;
64-
}
65-
66-
/**
67-
* Takes a Stream of Hosts, along with the Capabilities of the current request, and prioritizes the
68-
* request by removing Hosts that offer Capabilities that are more rare. e.g. if there are only a
69-
* couple Edge nodes, but a lot of Chrome nodes, the Edge nodes should be removed from
70-
* consideration when Chrome is requested. This does not currently take the amount of load on the
71-
* server into consideration--it only checks for availability, not how much availability
72-
* @param hostStream Stream of hosts attached to the Distributor (assume it's filtered for only those that offer these Capabilities)
73-
* @param capabilities Passing in the whole Capabilities object will allow us to prioritize more than just browser
74-
* @return Stream of distinct Hosts with the more rare Capabilities removed
75-
*/
76-
@VisibleForTesting
77-
Stream<Host> getPrioritizedHostStream(Stream<Host> hostStream, Capabilities capabilities) {
78-
//TODO for the moment, we're not going to operate on the Stream that was passed in--we need to
79-
// alter and futz with the contents, so the stream isn't the right place to operate. This
80-
// will likely be optimized back into the algo, but not yet
81-
Set<Host> filteredHostSet = hostStream.collect(Collectors.toSet());
82-
83-
//A "bucket" is a list of hosts that can use a particular browser. The "edge" bucket is the
84-
// complete list of Hosts that support "edge". By separating Hosts into buckets, we will
85-
// know which browsers have fewer nodes available for the browsers we're not interested in, and
86-
// can prioritize based on the browsers that have more availability
87-
Map<String, Set<Host>> hostBuckets = sortHostsToBucketsByBrowser(filteredHostSet);
88-
89-
//First, check to see if all buckets are the same size. If they are, just send back the full list of hosts
90-
// (i.e. the hosts are all "balanced" with regard to browser priority)
91-
if (allBucketsSameSize(hostBuckets)) {
92-
return hostBuckets.values().stream().distinct().flatMap(Set::stream);
93-
}
94-
95-
//Then, starting with the smallest bucket that isn't the current browser being prioritized,
96-
// remove all hosts in that bucket from consideration, then rebuild the buckets. Then do the
97-
// "same size" check again, and keep doing this until either a) there is only one bucket, or b)
98-
// all buckets are the same size
99-
100-
//Note: there should never be a case where a bucket will have *more* nodes available for the
101-
// given browser than the one being requested. The first filter in this check looks for
102-
// "equal", not "equal-or-greater" as a result of this assumption
103-
104-
//There might be unforeseen cases that challenge this assumption. TODO Create unit tests to prove it
105-
106-
//TODO a List of Map.Entry is silly. whatever this structure needs to be needs to be returned by
107-
// the sortHostsToBucketsByBrowser method in a way that we don't have to sort it separately like this
108-
final List<Map.Entry<String, Set<Host>>> sorted = hostBuckets.entrySet().stream().sorted(
109-
Comparator.comparingInt(v -> v.getValue().size())
110-
).collect(Collectors.toList());
111-
112-
// Until the buckets are the same size, keep removing hosts that have more "rare" browser capabilities
113-
Map<String, Set<Host>> newHostBuckets;
114-
for (Map.Entry<String, Set<Host>> entry : sorted) {
115-
//Don't examine the bucket containing the browser in question--we're prioritizing the other browsers
116-
//TODO This shouldn't be necessary, because if the list is sorted by size, this won't be possible until
117-
// they're all the same size. Create a unit test to prove it
118-
if (entry.getKey().equals(capabilities.getBrowserName())) {
119-
continue;
120-
}
121-
122-
//Remove all hosts from this bucket from the full set of eligible hosts
123-
final Set<Host> filteredHosts = filteredHostSet.stream().filter(host -> !entry.getValue().contains(host)).collect(Collectors.toSet());
124-
125-
//Rebuild the buckets by browser
126-
newHostBuckets = sortHostsToBucketsByBrowser(filteredHosts);
127-
128-
//Check the bucket sizes--if they're the same, then we're done
129-
if (allBucketsSameSize(newHostBuckets)) {
130-
LOG.fine("Hosts have been balanced according to browser priority");
131-
return newHostBuckets.values().stream().distinct().flatMap(Set::stream);
132-
}
133-
}
134-
135-
return hostBuckets.values().stream().distinct().flatMap(Set::stream);
136-
}
137-
138-
Map<String, Set<Host>> sortHostsToBucketsByBrowser(Set<Host> hostSet) {
139-
//Make a hash of browserType -> list of hosts that support it
140-
Map<String, Set<Host>> hostBuckets = new HashMap<>();
141-
hostSet.forEach(host -> host.asSummary().getStereotypes().forEach((k, v) -> {
142-
if (!hostBuckets.containsKey(k.getBrowserName())) {
143-
Set<Host> newSet = new HashSet<>();
144-
newSet.add(host);
145-
hostBuckets.put(k.getBrowserName(), newSet);
146-
}
147-
hostBuckets.get(k.getBrowserName()).add(host);
148-
}));
149-
return hostBuckets;
150-
}
151-
152-
@VisibleForTesting
153-
boolean allBucketsSameSize(Map<String, Set<Host>> hostBuckets) {
154-
Set<Integer> intSet = new HashSet<>();
155-
hostBuckets.values().forEach(bucket -> intSet.add(bucket.size()));
156-
return intSet.size() == 1;
157-
}
26+
/** Used to determine which {@link org.openqa.selenium.grid.node.Node} to
27+
* send a particular New Session request to.
28+
*/
29+
@FunctionalInterface
30+
public interface HostSelector {
31+
Optional<Host> selectHost(Capabilities capabilities, Set<Host> hosts);
15832
}

java/server/test/org/openqa/selenium/grid/distributor/selector/HostSelectorTest.java renamed to java/server/test/org/openqa/selenium/grid/distributor/selector/DefaultHostSelectorTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
import java.util.stream.IntStream;
5151
import java.util.stream.Stream;
5252

53-
public class HostSelectorTest {
53+
public class DefaultHostSelectorTest {
5454

5555
private Tracer tracer;
5656
private EventBus bus;
@@ -76,7 +76,7 @@ public void testGetPrioritizedHostBuckets() {
7676
hosts.add(createHost("chrome", "firefox"))
7777
);
7878

79-
HostSelector selector = new HostSelector();
79+
DefaultHostSelector selector = new DefaultHostSelector();
8080

8181
//When you prioritize for Edge, you should only have 1 possibility
8282
Stream<Host>
@@ -101,23 +101,23 @@ public void testGetPrioritizedHostBuckets() {
101101
public void testAllBucketsSameSize() {
102102
Map<String, Set<Host>> hostBuckets = buildBuckets(5, 5, 5, 5, 5, 5, 5, 5, 5, 5);
103103

104-
HostSelector selector = new HostSelector();
104+
DefaultHostSelector selector = new DefaultHostSelector();
105105
assertThat(selector.allBucketsSameSize(hostBuckets)).isTrue();
106106
}
107107

108108
@Test
109109
public void testAllBucketsNotSameSize() {
110110
Map<String, Set<Host>> hostBuckets = buildBuckets(3, 5, 8 );
111111

112-
HostSelector selector = new HostSelector();
112+
DefaultHostSelector selector = new DefaultHostSelector();
113113
assertThat(selector.allBucketsSameSize(hostBuckets)).isFalse();
114114
}
115115

116116
@Test
117117
public void testOneBucketStillConsideredSameSize() {
118118
Map<String, Set<Host>> hostBuckets = buildBuckets(3 );
119119

120-
HostSelector selector = new HostSelector();
120+
DefaultHostSelector selector = new DefaultHostSelector();
121121
assertThat(selector.allBucketsSameSize(hostBuckets)).isTrue();
122122
}
123123

@@ -126,7 +126,7 @@ public void testAllBucketsNotSameSizeProveNotUsingAverage() {
126126
//Make sure the numbers don't just average out to the same size
127127
Map<String, Set<Host>> hostBuckets = buildBuckets(4, 5, 6 );
128128

129-
HostSelector selector = new HostSelector();
129+
DefaultHostSelector selector = new DefaultHostSelector();
130130
assertThat(selector.allBucketsSameSize(hostBuckets)).isFalse();
131131
}
132132

0 commit comments

Comments
 (0)