Повышение параллелизма UnitTest’ов utPLSQL в Oracle

от автора

Могут ли девять женщин родить ребёнка за один месяц…?

Старинная индейская мудрость.

Быстрое развитие проекта несет в себе множество сложностей — большая вероятность сломать старый функционал или привнести новые баги. Одним из способов поддержания качества кода в хорошем состоянии, является наличие UnitTest’ов для существующего кода и обязательность создания Unit тестов для нового функционала.

Чем больше покрытие кода Unit тестами, тем выше качество. Но следствием увеличения покрытия кода Unit тестами, является увеличение времени работы самих Unit тестов, что негативно сказывается на скорости рабочего процесса.

В статье моего коллеги — https://habr.com/ru/companies/sportmaster_lab/articles/718472 описан механизм запуска Oracle UnitTest’ов с использованием библиотеки utPLSQL, в параллельном режиме. Попробуем достигнуть максимума – скомбинируем UnitTest’ы таким образом, чтобы достигнуть наибольшего быстродействия.

Библиотека utPLSQL объединяет пакеты, содержавшие Unit тесты, по логическим группам — suit’ам в терминах utPLSQL. Для этого в коде PL/SQL используется аннотация следующего вида:

—%suite(The name of my test suite)

https://www.utplsql.org/utPLSQL/latest/userguide/annotations.html

При первой реализации, параллельное выполнение Unit тестов было разбито именно по логическим группам — suit’ам. Данное разбиение может быть не оптимально по времени выполнения. Рассмотрим способы оптимизации, исходя их того, что мы не можем менять пакеты PL\SQL Oracle, в которых содержаться Unit тесты, но можем менять группы пакетов, запускаемых параллельно.

Очевидно, что наибольшее быстродействие достигается, когда все пакеты выполняются параллельно. Но, это не это не самое оптимальное решение, поскольку пакеты, выполняющиеся быстро, можно и нужно объединять вместе, для уменьшения степени параллелизма и уменьшения нагрузки на систему.

Сформируем алгоритм разбиения пакетов:

  • на основе статистики, полученной при выполнении Unit тестов ранее, выбираем самый долго исполняющийся пакет – он выступает якорем, определяющим верхнюю границу времени исполнения, групп пакетов, которые должны быть объединены вместе. То есть первая группа – это пакет(пакеты) с максимальным временем исполнения;

  • объединяем остальные пакеты в группы, таким образом, чтобы суммарное время выполнения было меньше максимального времени исполнения.

Такой алгоритм позволяет уменьшить количество параллельно работающих групп и достичь общего времени исполнения всех UnitTest’ов, равного времени исполнения максимального пакета.

В первой реализации для разбиения использовался запрос из табличной функции utPLSQL ut_runner.get_suites_info:

http://www.utplsql.org/utPLSQL/v3.1.3/userguide/querying_suites.html?trk=article-ssr-frontend-pulse_little-text-block

Представление, получающее разбиение Unit тестов по наборам пакетов:

/***************************************************************/ /*         Справочник разделений Unit тестов по наборам         */ create or replace force view v_utp_suit_packages as    with tests as (     select          t.*,         replace(regexp_substr(t.path, '\S*\.'), '.', '') as suite     from          table(utp.ut_runner.get_suites_info()) t ) select     row_number() over(order by t.suite)                             as pie,     t.suite,     listagg(object_name, ', ') within group (order by object_name)  as packages,     count(1) over ()                                                as total from     tests t where     item_type = 'UT_SUITE' group by     t.suite; comment on table v_utp_suit_packages            is 'Справочник разделений Unit тестов по наборам'; comment on column v_utp_suit_packages.pie       is 'Номер теста'; comment on column v_utp_suit_packages.suite     is 'Название suitа'; comment on column v_utp_suit_packages.packages  is 'Список пакетов suitа'; comment on column v_utp_suit_packages.total     is 'Общее кол-во групп тестов разбитых по suit';            

Напишем конвейерную табличную функцию, которая при отсутствии статистики, или при переданном флаге, что не нужно использовать статистику, будет возвращать данные из вышеуказанного представления v_utp_suit_packages. В противном случае процедура возвращает динамически сформированные группы пакетов на основании статистики.

Прототип функции выглядит следующим образом:

type t_utp_suit_packages_tbl is table of v_utp_suit_packages%rowtype;  /*********************************************************/ /*             Получить разбиение UTP пакетов            */ function get_utp_packages (     p_use_statistic     in number default 0,     p_statistic_uuid    in raw default null ) return t_utp_suit_packages_tbl pipelined is     v_count                     number;     ... begin     select         count(*),     into         v_count,     from         -- Таблица с результатами тестов         utp_tests t     where         t.uuid = nvl(p_statistic_uuid, uuid);     if p_use_statistic = 0 or v_count = 0 then         -- Используем разбиение UTP тестов по наборам (suit)         for v_rec in         (             select                 t.pie,                 t.suite,                 t.packages,                 t.total             from                 v_utp_suit_packages t         )         loop             pipe row (v_rec);         end loop;     else         -- Используем данные статистики         ...     end if; end get_utp_packages;

Создание табличной функции позволяет нам бесшовно модифицировать наш пакет, который обеспечивает параллельный запуск наших UnitTest’ов.

Вернемся назад, и коротко опишем реализацию пакета tst_utils:

create or replace package tst_utils is  /***************************************************************/ /* Выполнение UTP тестов в параллельном режиме */ procedure execute_parallel_utp;  /***************************************************************/ /* Выполнение группы UTP тестов разбитого по набору p_start_id */ procedure execute_utp (     p_start_id          in number,     p_end_id            in number,     p_uuid              in raw );  /******************************************************/ /* Функция выводит результаты тестов из utp_tests */ function get_test_result_clob (     p_uuid  utp_tests.uuid%type ) return clob;  end tst_utils; / create or replace package body tst_utils is  /***************************************************************/ /* Выполнение UTP тестов в параллельном режиме */ procedure execute_parallel_utp is     v_uuid              raw(16);     v_task_name         varchar(4000) := 'utp_parallel_run';     v_chunk_sql         varchar(4000) := q'[         select             t.pie,             t.total         from             v_utp_suit_packages t ]';   v_sql                 varchar(4000) := q'[         begin             tst_utils.execute_utp             (                 p_start_id          => :start_id ,                  p_end_id            => :end_id,                 p_uuid              => '$uuid',                 p_statistic_uuid    => $statistic_uuid             );         end; ]';     -- Подготовка данных     v_uuid := sys_guid();     v_SQL := replace(v_sql, '$uuid', v_uuid);     v_task_name := v_task_name || v_uuid;     select         total     into         v_parallel_count     from         v_utp_suit_packages     fetch first 1 row only;     -- Выполнение UTP тестов в параллель     service_utils.run_parallel_sql     (         p_task_name      => v_task_name,         p_chunk_sql      => v_chunk_sql,         p_task_sql       => v_sql,         p_parallel_level => v_parallel_count     );     -- Вывод результаты работы в dbms_output     print_clob_to_output     (         p_clob =>             get_test_result_clob             (                 p_uuid => v_uuid             )     ); end execute_parallel_utp;  /***************************************************************/ /* Выполнение группы Unit тестов разбитого по набору p_start_id */ procedure execute_utp (     p_start_id      in number,     p_end_id        in number,     p_uuid          in raw ) is     v_packages  varchar(4000);     v_start     timestamp(3);     v_buffer    DBMS_OUTPUT.chararr;     v_num_lines PLS_INTEGER;     v_result    clob;     v_error     number; begin     v_start := systimestamp;     select         packages     into         v_packages     from         v_utp_suit_packages     where         pie = p_start_id          and p_end_id is not null;     -- Выполняем Unit тесты     utp.ut.run     (          a_paths => utp.ut_varchar2_list(v_packages),          a_reporter => utp.ut_junit_reporter()      );     -- Собираем результат из dbms_output     v_num_lines := 4000;     dbms_output.get_lines(v_buffer, v_num_lines);     for i in 1..v_buffer.count loop         v_result := v_result || v_buffer(i);     end loop;     -- Сохраняем результат в итоговую таблицу     insert into          utp_tests ( uuid, pie, start_ts, end_ts, error_code, result_data )     values         ( p_uuid, p_start_id, v_start, systimestamp, 0, v_result);     commit; exception     when others then          v_result := sqlerrm;         v_error := sqlcode;         insert into               utp_tests ( uuid, pie, start_ts, end_ts, error_code, result_data )         values             ( p_uuid, p_start_id, v_start, systimestamp, v_error, v_result );         commit; end execute_utp;  end tst_utils; /

Часть процедур сознательно опущена, поскольку они не нужны для описания идеи подхода.

Процедура execute_parallel_utp:

  • на основании представления v_utp_suit_packages разбивает пакеты Oracle на группы;

  • используя механизм Oracle dbms_parallel_execute (вызов dbms_parallel_execute скрыт в service_utils.run_parallel_sql) создает параллельные задания и выполняет их в процедурах execute_utp;

  • ожидает выполнения заданий;

  • выводит результат работы UnitTest’ов в буфер dbms_output.

Главное, что можно видеть из выше указанного фрагмента, что в процедурах execute_parallel_utp и execute_utp используется представление v_utp_suit_packages для выбора, какие UnitTest’ы должны быть обработаны.

Если мы заменим представление v_utp_suit_packages, на результат конвейерной табличной функции get_utp_packages (select * from table(tst_utils.get_utp_packages)) – то внешние системы, использующие вызовы UnitTest’ов не потребуют изменений. Не нужно изменять конвейер CI/CD – все изменения и вся магия остается внутри пакета tst_utils.

Заголовок процедуры execute_parallel_utp изменится следующим образом:

procedure execute_parallel_utp (     -- Флаг, используемый для определения нужно или нет использовать статистику      p_use_statistic  in  number   default 1 ) is     v_uuid              raw(16);     v_task_name         varchar(4000) := 'utp_parallel_run';     v_chunk_sql         varchar(4000) := q'[         select             t.pie,             t.total         from             table(tst_utils.get_utp_packages($use_statistic)) t ]';   v_sql                 varchar(4000) := q'[         begin             tst_utils.execute_utp             (                 p_start_id          => :start_id ,                  p_end_id            => :end_id,                 p_uuid              => '$uuid',                 p_statistic_uuid    => $statistic_uuid             );         end; ]'; ...........

Процедура execute_utp примет следующий вид:

procedure execute_utp (     p_start_id          in number,     p_end_id            in number,     p_uuid              in raw,     p_statistic_uuid    in raw default null ) is     v_packages      varchar(4000);     v_suite         varchar(4000);     v_start         timestamp(3); ................. begin     v_start := systimestamp;     if p_statistic_uuid is null then         select             t.packages,             t.suite         into             v_packages,             v_suite         from             v_utp_suit_packages t         where             t.pie = p_start_id             and p_end_id is not null;     else         select             t.packages,             t.suite         into             v_packages,             v_suite         from             table(tst_utils.get_utp_packages(1, p_statistic_uuid)) t         where             t.pie = p_start_id             and p_end_id is not null;     end if; .................

Результат оптимизации:

Соглашусь с теми, кто скажет, что разбиение UntiTest’ов по suit’ам – это не оптимальное решение. Но даже такое решение, позволившие распараллелить выполнение UntiTest’ов, на момент внедрения, позволило укорить работу в три раза! На текущий день количество UntiTest’ов продолжает расти. Количество suit’ов выросло с 6 до 18.

Поскольку разбиение на suit’ы – это очень индивидуальное разбиение, зависящее от команды, бизнес направлений в проекте и т.п., поэтому мои цифры по оптимизации могут отличаться от Ваших.  Однозначно, решение отказаться от разбиения по suit’ам, и использовать статистику, ведет к уменьшению времени исполнения всех UntiTest’ов.

В моем случае время выполнения UntiTest’ов сократилось примерно на 30%. Такое небольшое ускорение, вызвано тем, что периодически проводится ревизия и ручное разбиение suit’ов, время исполнения, которых существенно отличается от других.

Мне кажется, что это очень неплохой результат, так как данная оптимизация позволяет исключить ручное вмешательство разработчиков. Считаю необходимым расширять свои навыки, уходить от шаблонных решений при использовании PL\SQL.

P.S. Замечания и предложения только приветствуются.

P.S.S. Старался использовать в статье, как можно меньше больших кусков кода, но избежать этого не удалось. Если будет запрос, постараюсь выложить весь код на GitHub.


ссылка на оригинал статьи https://habr.com/ru/articles/839316/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *