feat(loadtest): IFT Tsung smoke/mix + verification_token на стендах. Refs EventHub/EventHubBack#30
This commit is contained in:
@@ -78,14 +78,23 @@ register(Req) ->
|
|||||||
% Создаём верификационный токен и отправляем письмо
|
% Создаём верификационный токен и отправляем письмо
|
||||||
{ok, VerificationToken, _Expires} = core_verification:create_token(User#user.id),
|
{ok, VerificationToken, _Expires} = core_verification:create_token(User#user.id),
|
||||||
logic_email:send_verification_email(Email, VerificationToken),
|
logic_email:send_verification_email(Email, VerificationToken),
|
||||||
Response = #{
|
BaseUser = #{
|
||||||
user => #{
|
id => User#user.id,
|
||||||
id => User#user.id,
|
email => User#user.email,
|
||||||
email => User#user.email,
|
role => atom_to_binary(User#user.role, utf8),
|
||||||
role => atom_to_binary(User#user.role, utf8),
|
status => atom_to_binary(User#user.status, utf8) % <<"pending">>
|
||||||
status => atom_to_binary(User#user.status, utf8) % <<"pending">>
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
%% На стендах (не prod) отдаём verification_token для loadtest/E2E
|
||||||
|
%% без доступа к почте (IFT EVENTHUB_ENV=stage).
|
||||||
|
Response = case os:getenv("EVENTHUB_ENV") of
|
||||||
|
"prod" ->
|
||||||
|
#{user => BaseUser};
|
||||||
|
_ ->
|
||||||
|
#{
|
||||||
|
user => BaseUser,
|
||||||
|
verification_token => VerificationToken
|
||||||
|
}
|
||||||
|
end,
|
||||||
handler_utils:send_json(Req1, 201, Response);
|
handler_utils:send_json(Req1, 201, Response);
|
||||||
{error, email_exists} ->
|
{error, email_exists} ->
|
||||||
handler_utils:send_error(Req1, 409, <<"Email already exists">>);
|
handler_utils:send_error(Req1, 409, <<"Email already exists">>);
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Tsung — нагрузка EventHub
|
||||||
|
|
||||||
|
## Профили
|
||||||
|
|
||||||
|
| Файл | Назначение |
|
||||||
|
|------|------------|
|
||||||
|
| `eventhub_ift_smoke.xml` | Короткий smoke на IFT после prepare |
|
||||||
|
| `eventhub_ift.xml` | Mix: `auth_browse` 60% + `create_flow` 40%, фазы warmup/ramp/peak |
|
||||||
|
| `eventhub_tsung.xml` | Legacy localhost (thinktime там в секундах — не использовать для IFT) |
|
||||||
|
| `eventhub_http.xml` | Legacy короткий localhost |
|
||||||
|
|
||||||
|
Цель IFT: **HTTPS** `api.ift.eventhub.local:443` (Traefik).
|
||||||
|
|
||||||
|
Auth path: `POST /v1/register` → `POST /v1/verify` → `POST /v1/login`.
|
||||||
|
На стендах (`EVENTHUB_ENV` ≠ `prod`) register отдаёт `verification_token` (без почты).
|
||||||
|
|
||||||
|
## Перед прогоном (хост IFT)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1 нода (default)
|
||||||
|
EVENTHUB_REPLICAS=1 bash /opt/eventhub-ift/devops/scripts/ift-loadtest-prepare.sh
|
||||||
|
|
||||||
|
# или N нод
|
||||||
|
EVENTHUB_REPLICAS=2 bash /opt/eventhub-ift/devops/scripts/ift-loadtest-prepare.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Оставляет `traefik` + `eventhub×N`, гасит admin-ui / bot / observability / fallback.
|
||||||
|
|
||||||
|
## Дех-машина (WSL)
|
||||||
|
|
||||||
|
1. DNS: `api.ift.eventhub.local` → IP IFT (тот же, что в OpenWRT).
|
||||||
|
2. Установка: `sudo apt install tsung` (или [документация Tsung](http://tsung.erlang-projects.org/)).
|
||||||
|
3. TLS: самоподписанный сертификат стенда — импортировать CA из `EventHubDevOps/ift/traefik/certs` в trust store WSL, либо временно положить CA в `/usr/local/share/ca-certificates/` и `update-ca-certificates`. Проверка: `curl -sf https://api.ift.eventhub.local/health`.
|
||||||
|
4. Запуск из корня `EventHubBack`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# smoke (~1 мин)
|
||||||
|
tsung -f test/tsung/eventhub_ift_smoke.xml start
|
||||||
|
|
||||||
|
# полный прогон (~6 мин фаз)
|
||||||
|
tsung -f test/tsung/eventhub_ift.xml start
|
||||||
|
```
|
||||||
|
|
||||||
|
Отчёт: `~/.tsung/log/<timestamp>/` → `tsung_stats.pl` / HTML.
|
||||||
|
|
||||||
|
## После прогона
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /opt/eventhub-ift/devops/scripts/ift-loadtest-restore.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Настройка интенсивности
|
||||||
|
|
||||||
|
В `eventhub_ift.xml` блок `<load>`: `interarrival` (сек между стартами пользователей) и `duration` фаз.
|
||||||
|
`thinktime` — **секунды** (не мс).
|
||||||
|
|
||||||
|
## Что сознательно убрано из hot path
|
||||||
|
|
||||||
|
- dummy `/v1/refresh`
|
||||||
|
- reports / tickets (шум модерации)
|
||||||
|
- завышенные thinktime (старый `eventhub_tsung.xml`)
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE tsung SYSTEM "/usr/share/tsung/tsung-1.0.dtd">
|
||||||
|
<!--
|
||||||
|
IFT load mix: auth_browse 60% + create_flow 40%.
|
||||||
|
Auth: register → verify (verification_token на стендах) → login.
|
||||||
|
Перед прогоном: EVENTHUB_REPLICAS=1 bash …/ift-loadtest-prepare.sh
|
||||||
|
thinktime — секунды.
|
||||||
|
-->
|
||||||
|
<tsung loglevel="notice" version="1.0">
|
||||||
|
<clients>
|
||||||
|
<client host="localhost" use_controller_vm="true" maxusers="2000"/>
|
||||||
|
</clients>
|
||||||
|
|
||||||
|
<servers>
|
||||||
|
<server host="api.ift.eventhub.local" port="443" type="ssl"/>
|
||||||
|
</servers>
|
||||||
|
|
||||||
|
<load>
|
||||||
|
<arrivalphase phase="1" duration="1" unit="minute">
|
||||||
|
<users interarrival="1.0" unit="second"/>
|
||||||
|
</arrivalphase>
|
||||||
|
<arrivalphase phase="2" duration="2" unit="minute">
|
||||||
|
<users interarrival="0.4" unit="second"/>
|
||||||
|
</arrivalphase>
|
||||||
|
<arrivalphase phase="3" duration="3" unit="minute">
|
||||||
|
<users interarrival="0.15" unit="second"/>
|
||||||
|
</arrivalphase>
|
||||||
|
</load>
|
||||||
|
|
||||||
|
<options>
|
||||||
|
<option name="ssl_reuse" value="true"/>
|
||||||
|
</options>
|
||||||
|
|
||||||
|
<sessions>
|
||||||
|
<session name="auth_browse" probability="60" type="ts_http">
|
||||||
|
<setdynvars sourcetype="random_number" start="100000" end="9999999">
|
||||||
|
<var name="rand_id"/>
|
||||||
|
</setdynvars>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<dyn_variable name="vtoken" re=""verification_token"\s*:\s*"([^"]+)"/>
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/register" method="POST" content_type="application/json"
|
||||||
|
contents='{"email":"lt_b_%%_rand_id%%@loadtest.local","password":"testpassword123"}'/>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1" max="2" random="true"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/verify" method="POST" content_type="application/json"
|
||||||
|
contents='{"token":"%%_vtoken%%"}'/>
|
||||||
|
</request>
|
||||||
|
<thinktime value="1"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<dyn_variable name="token" re=""token"\s*:\s*"([^"]+)"/>
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/login" method="POST" content_type="application/json"
|
||||||
|
contents='{"email":"lt_b_%%_rand_id%%@loadtest.local","password":"testpassword123"}'/>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1" max="2" random="true"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/user/me" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1" max="2" random="true"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/calendars" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1" max="2" random="true"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<match do="log" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/search?q=lt" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1" max="2" random="true"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<match do="log" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/user/bookings" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
</session>
|
||||||
|
|
||||||
|
<session name="create_flow" probability="40" type="ts_http">
|
||||||
|
<setdynvars sourcetype="random_number" start="100000" end="9999999">
|
||||||
|
<var name="rand_id"/>
|
||||||
|
</setdynvars>
|
||||||
|
<setdynvars sourcetype="random_number" start="10" end="28">
|
||||||
|
<var name="day"/>
|
||||||
|
</setdynvars>
|
||||||
|
<setdynvars sourcetype="random_number" start="10" end="59">
|
||||||
|
<var name="sec"/>
|
||||||
|
</setdynvars>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<dyn_variable name="vtoken" re=""verification_token"\s*:\s*"([^"]+)"/>
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/register" method="POST" content_type="application/json"
|
||||||
|
contents='{"email":"lt_c_%%_rand_id%%@loadtest.local","password":"testpassword123"}'/>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1" max="2" random="true"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/verify" method="POST" content_type="application/json"
|
||||||
|
contents='{"token":"%%_vtoken%%"}'/>
|
||||||
|
</request>
|
||||||
|
<thinktime value="1"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<dyn_variable name="token" re=""token"\s*:\s*"([^"]+)"/>
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/login" method="POST" content_type="application/json"
|
||||||
|
contents='{"email":"lt_c_%%_rand_id%%@loadtest.local","password":"testpassword123"}'/>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1" max="2" random="true"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<dyn_variable name="calendar_id" re=""id"\s*:\s*"([^"]+)"/>
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/calendars" method="POST" content_type="application/json"
|
||||||
|
contents='{"title":"LT Cal %%_rand_id%%","confirmation":"auto","description":"loadtest"}'>
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1" max="2" random="true"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<match do="log" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/calendars/%%_calendar_id%%" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime value="1"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<dyn_variable name="event_id" re=""id"\s*:\s*"([^"]+)"/>
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/calendars/%%_calendar_id%%/events" method="POST" content_type="application/json"
|
||||||
|
contents='{"title":"LT Ev %%_rand_id%%","start_time":"2031-07-%%_day%%T12:00:%%_sec%%Z","duration":45,"capacity":30}'>
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1" max="2" random="true"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/events/%%_event_id%%" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime value="1"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<match do="log" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/calendars" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
</session>
|
||||||
|
</sessions>
|
||||||
|
</tsung>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE tsung SYSTEM "/usr/share/tsung/tsung-1.0.dtd">
|
||||||
|
<!--
|
||||||
|
IFT smoke: register → verify → login → calendar/event.
|
||||||
|
Требует EVENTHUB_ENV≠prod (в ответе register есть verification_token).
|
||||||
|
Запуск: tsung -f test/tsung/eventhub_ift_smoke.xml start
|
||||||
|
-->
|
||||||
|
<tsung loglevel="notice" version="1.0">
|
||||||
|
<clients>
|
||||||
|
<client host="localhost" use_controller_vm="true" maxusers="50"/>
|
||||||
|
</clients>
|
||||||
|
|
||||||
|
<servers>
|
||||||
|
<server host="api.ift.eventhub.local" port="443" type="ssl"/>
|
||||||
|
</servers>
|
||||||
|
|
||||||
|
<load>
|
||||||
|
<arrivalphase phase="1" duration="1" unit="minute">
|
||||||
|
<users interarrival="2" unit="second"/>
|
||||||
|
</arrivalphase>
|
||||||
|
</load>
|
||||||
|
|
||||||
|
<options>
|
||||||
|
<option name="ssl_reuse" value="true"/>
|
||||||
|
</options>
|
||||||
|
|
||||||
|
<sessions>
|
||||||
|
<session name="ift_smoke" probability="100" type="ts_http">
|
||||||
|
<setdynvars sourcetype="random_number" start="100000" end="9999999">
|
||||||
|
<var name="rand_id"/>
|
||||||
|
</setdynvars>
|
||||||
|
<setdynvars sourcetype="random_number" start="10" end="59">
|
||||||
|
<var name="sec"/>
|
||||||
|
</setdynvars>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<dyn_variable name="vtoken" re=""verification_token"\s*:\s*"([^"]+)"/>
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/register" method="POST" content_type="application/json"
|
||||||
|
contents='{"email":"lt_smoke_%%_rand_id%%@loadtest.local","password":"testpassword123"}'/>
|
||||||
|
</request>
|
||||||
|
<thinktime value="1"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/verify" method="POST" content_type="application/json"
|
||||||
|
contents='{"token":"%%_vtoken%%"}'/>
|
||||||
|
</request>
|
||||||
|
<thinktime value="1"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<dyn_variable name="token" re=""token"\s*:\s*"([^"]+)"/>
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/login" method="POST" content_type="application/json"
|
||||||
|
contents='{"email":"lt_smoke_%%_rand_id%%@loadtest.local","password":"testpassword123"}'/>
|
||||||
|
</request>
|
||||||
|
<thinktime value="1"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<dyn_variable name="calendar_id" re=""id"\s*:\s*"([^"]+)"/>
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/calendars" method="POST" content_type="application/json"
|
||||||
|
contents='{"title":"Smoke Cal %%_rand_id%%","confirmation":"auto"}'>
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime value="1"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<dyn_variable name="event_id" re=""id"\s*:\s*"([^"]+)"/>
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/calendars/%%_calendar_id%%/events" method="POST" content_type="application/json"
|
||||||
|
contents='{"title":"Smoke Ev %%_rand_id%%","start_time":"2030-06-15T10:00:%%_sec%%Z","duration":60,"capacity":20}'>
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime value="1"/>
|
||||||
|
|
||||||
|
<request subst="true">
|
||||||
|
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
|
||||||
|
<http url="/v1/events/%%_event_id%%" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
</session>
|
||||||
|
</sessions>
|
||||||
|
</tsung>
|
||||||
Reference in New Issue
Block a user