В нашей небольшой, но очень динамично развивающейся компании каждый день тестируется больше сотни задач. Все они проверяются как в тестовом окружении, так и в окружениях, более приближенных к реальному. Подавляющее большинство задач, связанных с 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

Поделиться с друзьями
-->

Комментарии (9)


  1. olegchir
    10.08.2016 18:05
    +5

    Эээ а где ссылка на пул-риквест в гитхаб Селениума?


    1. bbidox
      10.08.2016 18:39
      +4

      В ближайшие дни напишу тесты и отправлю :)


  1. Kedanachi
    10.08.2016 18:27
    +1

    Зачем было патчить грид, если селениум позволяет подключать свои сервлеты?
    http://www.seleniumhq.org/docs/07_selenium_grid.jsp#customizing-the-grid


    1. bbidox
      10.08.2016 18:35
      +2

      Да как вам сказать… Запатчил за пять минут, собралось, пока чай пил — так оно и работает до сих пор :)
      А патч, строго говоря, для сервлета org.openqa.grid.web.servlet.HubStatusServlet


  1. spmbt
    11.08.2016 01:26
    -2

    Альбатросы не удержатся на канатах — тело тяжёлое, а лапы слабые. Сравните с воробьиными или куриными. Для сочетания «ветки и перепонки», вам, скорее, подошли бы гоголи. Да и то, им это нужно, чтобы гнездоваться в дуплах, а не высоко на ветке сидеть. Вот максимум:

    гоголь на бревне


    1. bbidox
      11.08.2016 11:13
      +3

      По-моему, это были чайки, которые кричали «моё» — mine!


  1. vaniaPooh
    11.08.2016 13:31

    http://github.com/seleniumkit/gridrouter


    1. bbidox
      11.08.2016 15:24

      Да, про гридроутер я в курсе. Но для того, чтобы им пользоваться круто, свободных нод для любого браузера всегда должно быть много. А у нас это не так (пока). Поэтому мы гонимся за тем, чтобы выжать максимум из того, что есть :)


    1. banuchka
      11.08.2016 16:08

      vaniaPooh отличная штука, спасибо! Есть опыт применения/использования.