В нашей небольшой, но очень динамично развивающейся компании каждый день тестируется больше сотни задач. Все они проверяются как в тестовом окружении, так и в окружениях, более приближенных к реальному. Подавляющее большинство задач, связанных с web, проверяется автотестами, которых у нас много.
Примерно полгода назад тестов и задач стало столько, что наша маленькая ферма с Selenium в час пик стала буквально «захлебываться» от запросов на новую сессию Firefox или Chrome. Выглядело это примерно так: на Selenium grid образуется очередь из сессий, которые ждут свободный браузер. Пользователи продолжают запускать автотесты, и эта очередь продолжает расти, но браузеры заняты старыми задачами и сессии «отваливаются» с таймаутом.
На тот момент максимальное количество нод, разделенных между Firefox, Chrome, Internet Explorer и PhantomJS, было около 200. Один из вариантов решения проблемы, который мне пришел в голову — это отслеживать количество свободных нод перед запуском теста и «придерживать» тесты в методе setup(), пока свободных нод недостаточно.
В описаниях изменений Selenium в свое время проскакивал функционал получения информации от grid с помощью HTTP-запросов. Доступные команды можно посмотреть прямо в коде сервлета HubStatusServlet.java. Их всего три: configuration (конфигурация), slotCounts (количество слотов) и newSessionRequestCount (количество сессий в очереди на получение браузера).
Формат запроса достаточно хитрый: это GET request, но с телом. Для экспериментов воспользуемся cURL и проверим, что возвращают эти команды:
$ curl -XGET http://selenium1:5555/grid/api/hub -d '{"configuration":[]}'
{
'success': true,
'port': '5555',
'hubConfig': '/usr/local/selenium-rc/grid.json',
'host': 'selenium1.d3',
'servlets': 'org.openqa.grid.web.servlet.HubStatusServlet',
'cleanUpCycle': 5000,
'browserTimeout': 120000,
'newSessionWaitTimeout': 30000,
'capabilityMatcher': 'org.openqa.grid.internal.utils.DefaultCapabilityMatcher',
'prioritizer': null,
'throwOnCapabilityNotPresent': true,
'nodePolling': 5000,
'maxSession': 5,
'role': 'hub',
'jettyMaxThreads': - 1,
'timeout': 90000
}
$ curl -XGET http://selenium1:5555/grid/api/hub -d '{"configuration":["slotCounts"]}'
{
'success': true,
'slotCounts': {
'free': 50,
'total': 196
}
}
curl -XGET http://selenium1:5555/grid/api/hub -d '{"configuration":["newSessionRequestCount"]}'
{
'success': true,
'newSessionRequestCount': 3
}
У нас все тесты для Selenium написаны на PHP, в нем подобный запрос будет выглядеть так:
<?php
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, 'http://selenium1:5555/grid/api/hub');
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($curl, CURLOPT_POSTFIELDS, '{"configuration":["slotCounts"]}');
curl_exec($curl);
В принципе, запрашивая в setUp()-методе тестов общее количество слотов и количество ждущих сессий, можно начинать ждать. Но это не очень удобно в том случае, если у вас неравномерно выделены ресурсы на разные браузеры. Например, у нас количество нод для Firefox примерно на треть больше, чем Google Chrome. А Internet Explorer и MS Edge занимают всего около 10 нод (и то они могут делиться по версиям). Получается, что свободных нод именно для Chrome может уже и не быть, хотя Selenium Grid говорит, что свободные ноды еще есть.
Поэтому пришлось дописать функционал сервлета, чтобы понять, сколько же и каких браузеров нам доступно. Сам патч не очень большой, вот его код:
diff --git a/java/server/src/org/openqa/grid/web/servlet/HubStatusServlet.java b/java/server/src/org/openqa/grid/web/servlet/HubStatusServlet.java
index 8b9c578..550c5db 100644
--- a/java/server/src/org/openqa/grid/web/servlet/HubStatusServlet.java
+++ b/java/server/src/org/openqa/grid/web/servlet/HubStatusServlet.java
@@ -29,10 +29,12 @@
import org.openqa.grid.internal.Registry;
import org.openqa.grid.internal.RemoteProxy;
import org.openqa.grid.internal.TestSlot;
+import org.openqa.selenium.remote.CapabilityType;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@@ -128,6 +130,11 @@ private JsonObject getResponse(HttpServletRequest request) throws IOException {
paramsToReturn.remove("slotCounts");
}
+ if (paramsToReturn.contains("browserSlotsCount")) {
+ res.add("browserSlotsCount", getBrowserSlotsCount());
+ paramsToReturn.remove("browserSlotsCount");
+ }
+
for (String key : paramsToReturn) {
Object value = allParams.get(key);
if (value == null) {
@@ -169,6 +176,53 @@ private JsonObject getSlotCounts() {
return result;
}
+ private JsonObject getBrowserSlotsCount() {
+ int freeSlots = 0;
+ int totalSlots = 0;
+
+ Map<String, Integer> freeBrowserSlots = new HashMap<>();
+ Map<String, Integer> totalBrowserSlots = new HashMap<>();
+
+ for (RemoteProxy proxy : getRegistry().getAllProxies()) {
+ for (TestSlot slot : proxy.getTestSlots()) {
+ String
+ slot_browser_name =
+ slot.getCapabilities().get(CapabilityType.BROWSER_NAME).toString().toUpperCase();
+ if (slot.getSession() == null) {
+ if (freeBrowserSlots.containsKey(slot_browser_name)) {
+ freeBrowserSlots.put(slot_browser_name, freeBrowserSlots.get(slot_browser_name) + 1);
+ } else {
+ freeBrowserSlots.put(slot_browser_name, 1);
+ }
+ freeSlots += 1;
+ }
+ if (totalBrowserSlots.containsKey(slot_browser_name)) {
+ totalBrowserSlots.put(slot_browser_name, totalBrowserSlots.get(slot_browser_name) + 1);
+ } else {
+ totalBrowserSlots.put(slot_browser_name, 1);
+ }
+ totalSlots += 1;
+ }
+ }
+
+ JsonObject result = new JsonObject();
+
+ for (String str : totalBrowserSlots.keySet()) {
+ JsonObject browser = new JsonObject();
+ browser.addProperty("total", totalBrowserSlots.get(str));
+ if (freeBrowserSlots.containsKey(str)) {
+ browser.addProperty("free", freeBrowserSlots.get(str));
+ } else {
+ browser.addProperty("free", 0);
+ }
+ result.add(str, browser);
+ }
+
+ result.addProperty("total", totalSlots);
+ result.addProperty("total_free", freeSlots);
+ return result;
+ }
+
private JsonObject getRequestJSON(HttpServletRequest request) throws IOException {
JsonObject requestJSON = null;
BufferedReader rd = new BufferedReader(new InputStreamReader(request.getInputStream()));
До сих пор немного стыдно, что я его не оформил по всем правилам (с тестами и т.п.) и не отправил в SeleniumHQ. Обещаю, что сделаю это в ближайшее время, если читатели найдут функционал полезным :)
Накладываем патч на локальную копию исходников Selenium, собираем собственную сборку Selenium-grid (тут есть подробная инструкция по сборке). Если нет желания возиться со сборкой, можете попробовать уже собранное мной: https://github.com/leipreachan/misc_scripts/tree/master/blob/selenium
Теперь перезапускаем selenium-grid и смотрим, какие значения он возвращает:
curl -XGET http://selenium1:5555/grid/api/hub -d '{"configuration":["browserSlotsCount"]}'
и результат:
{
'success': true,
'browserSlotsCount': {
'IEXPLORER': {
'total': 4,
'free': 3
},
'FIREFOX': {
'total': 95,
'free': 50
},
'MICROSOFTEDGE': {
'total': 1,
'free': 1
},
'PHANTOMJS': {
'total': 20,
'free': 20
},
'CHROME': {
'total': 76,
'free': 75
},
'total': 196,
'total_free': 149
}
}
Итак, теперь мы знаем, какие свободные браузеры и в каком количестве у нас представлены в Selenium Grid. Осталось немного поправить метод setup() (или аналогичный):
- реализовать проверку на количество свободных нод;
- в этой проверке добавить небольшой период ожидания (например, две минуты) перед тем, как тест упадёт с таймаутом;
- не забыть, что не надо запрашивать эти параметры каждую секунду :)
Лично для нас это стало выглядеть так, что selenium-тесты в час пик идут немного медленнее, зато гораздо, гораздо стабильнее. Учитывая, что у нас несколько сотен тестов запускаются автоматически, это существенно упростило жизнь всем, кто с связан с тестированием.
Артём Солдаткин
Lead QA Engineer, Badoo
Автор: Badoo