company_banner

Маленький, но очень полезный патч в Selenium

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

    • +40
    • 15,1k
    • 9
    Badoo
    399,00
    Big Dating
    Поделиться публикацией

    Похожие публикации

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

      +5
      Эээ а где ссылка на пул-риквест в гитхаб Селениума?
        +4
        В ближайшие дни напишу тесты и отправлю :)
        +1
        Зачем было патчить грид, если селениум позволяет подключать свои сервлеты?
        http://www.seleniumhq.org/docs/07_selenium_grid.jsp#customizing-the-grid
          +2
          Да как вам сказать… Запатчил за пять минут, собралось, пока чай пил — так оно и работает до сих пор :)
          А патч, строго говоря, для сервлета org.openqa.grid.web.servlet.HubStatusServlet
          –2
          Альбатросы не удержатся на канатах — тело тяжёлое, а лапы слабые. Сравните с воробьиными или куриными. Для сочетания «ветки и перепонки», вам, скорее, подошли бы гоголи. Да и то, им это нужно, чтобы гнездоваться в дуплах, а не высоко на ветке сидеть. Вот максимум:
          гоголь на бревне
            +3
            По-моему, это были чайки, которые кричали «моё» — mine!
            0
            http://github.com/seleniumkit/gridrouter
              0
              Да, про гридроутер я в курсе. Но для того, чтобы им пользоваться круто, свободных нод для любого браузера всегда должно быть много. А у нас это не так (пока). Поэтому мы гонимся за тем, чтобы выжать максимум из того, что есть :)
                0
                vaniaPooh отличная штука, спасибо! Есть опыт применения/использования.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое