Go to file
2024-08-13 15:07:29 +00:00
.github chore(deps): update valitydev/erlang-workflows action to v1.0.15 2024-08-13 15:07:29 +00:00
src Fixes 'undefined' event handler options for severity mapping (#37) 2024-06-03 16:53:07 +03:00
test TD-788: Adds prometheus support for hackney per host metrics (#32) 2024-01-12 12:55:44 +03:00
.env Sync w/ valitydev/erlang-templates (#20) 2022-10-03 14:27:41 +03:00
.gitignore Sync w/ valitydev/erlang-templates (#20) 2022-10-03 14:27:41 +03:00
Dockerfile.dev Sync w/ valitydev/erlang-templates (#20) 2022-10-03 14:27:41 +03:00
elvis.config Sync w/ valitydev/erlang-templates (#20) 2022-10-03 14:27:41 +03:00
LICENSE 🔄 Synced file(s) with valitydev/configurations (#5) 2022-11-24 04:03:56 +07:00
Makefile Sync w/ valitydev/erlang-templates (#20) 2022-10-03 14:27:41 +03:00
README.md TD-788: Adds metrics collectors for hackney and ranch stats (#30) 2023-12-19 15:17:08 +03:00
rebar.config TD-788: Adds metrics collectors for hackney and ranch stats (#30) 2023-12-19 15:17:08 +03:00
rebar.lock Fixes 'undefined' event handler options for severity mapping (#37) 2024-06-03 16:53:07 +03:00
renovate.json chore(deps): add renovate.json (#2) 2022-02-22 00:53:18 +03:00

Woody

Erlang реализация Библиотеки RPC вызовов для общения между микросервисами

версия требований: ac4d40cc22d649d03369fcd52fb1230e51cdf52e

API

Сервер

Получить child_spec RPC сервера:

1> EventHandler = my_event_handler.  %% | {my_event_handler, MyEventHandlerOpts :: term()}. woody_event_handler behaviour
2> Service = {
2>     my_money_thrift, %% имя модуля, сгенерированного из money.thrift файла
2>     money %% имя thrift сервиса, заданное в money.thift
2> }.
3> ThriftHandler = my_money_thrift_service_handler.  %% | {my_money_thrift_service_handler, MyHandlerOpts :: term()}. woody_server_thrift_handler behaviour
4> Handlers = [{"/v1/thrift_money_service",{Service, ThriftHandler}}].
5> ServerSpec = woody_server:child_spec(money_service_sup, #{
5>     handlers => Handlers,
5>     event_handler => EventHandler,
5>     ip => {127,0,0,1},
5>     port => 8022
5>     %% optional:
5>     %% transport_opts => woody_server_thrift_http_handler:transport_opts()
5>     %% protocol_opts  => cowboy_protocol:opts()
5>     %% handler_limits => woody_server_thrift_http_handler:handler_limits()
5>     %% shutdown_timeout => timeout()
5> }).

С помощью опциональных полей можно:

  • transport_opts - задать дополнительные опции для обработчика входящих соединений
  • protocol_opts - задать дополнительные опции для обработчика http протокола сервера cowboy
  • handler_limits - поставить лимиты на heap size процесса хэндлера (beam убьет хэндлер при превышении лимита - см. erlang:process_flag(max_heap_size, MaxHeapSize)) и на максимальный размер памяти vm (см. erlang:memory(total)), при достижении которого woody server начнет отбрасывать входящие rpc вызовы с системной ошибкой internal resourse unavailable.
  • shutdown_timeout - задать время ожидания завершения всех текущих соединений при получении сервером сигнала shutdown. При выборе значения данного параметра учитывайте опции request_timeout и max_keepalive в protocol_opts. Безопасным будет являться значение request_timeout, плюс теоретическое максимальное время обслуживания операции на сервере. При этом подразумевается, что отсутствие возможности обращения к удерживаемым в это время открытыми сокетам будет обеспечено внешними средствами. В том случае, если max_keepalive =:= 1, значением request_timeout в расчете и вышеупомянутыми средствами возможно пренебречь.

Теперь можно поднять RPC сервер в рамках supervision tree приложения. Например:

6> {ok, _} = supervisor:start_child(MySup, ServerSpec).

Клиент

Сделать синхронный RPC вызов:

7> Url = <<"localhost:8022/v1/thrift_money_service">>.
8> Function = give_me_money.  %% thrift метод
9> Args = {100, <<"rub">>}.
10> Request = {Service, Function, Args}.
11> ClientEventHandler = {my_event_handler, MyCustomOptions}.
12> Context1 = woody_context:new(<<"myUniqRequestID1">>).
13> Opts = #{url => Url, event_handler => ClientEventHandler}.
14> {ok, Result1} = woody_client:call(Request, Opts, Context1).

В случае вызова thrift oneway функции (thrift реализация cast) woody_client:call/3 вернет {ok, ok}.

Если сервер бросает Exception, описанный в .thrift файле сервиса (т.е. Бизнес ошибку в терминологии макросервис платформы), woody_client:call/3 вернет это исключение в виде: {exception, Exception}.

В случае получения Системной ошибки клиент выбрасывает erlang:error типа {woody_error, woody_error:system_error()}.

woody_context:new/0 - можно использовать для создания контекста корневого запроса с автоматически сгенерированным уникальным RPC ID.

Можно создать пул соединений для thrift клиента (например, для установления keep alive соединений с сервером). Для этого надо использовать woody_client:child_spec/2. Для работы с определенным пулом в Options есть поле transport_opts => [{pool, pool_name}, {timeout, 150000}, {max_connections, 100}].

15> Opts1 = Opts#{transport_opts => [{pool, my_client_pool}]}.
16> supervisor:start_child(Sup, woody_client:child_spec(Opts1)).
17> Context2 = woody_context:new(<<"myUniqRequestID2">>).
18> {ok, Result2} = woody_client:call(Request, Opts1, Context2).

Context позволяет аннотировать RPC запросы дополнительными мета данными в виде key-value. Context передается только в запросах и изменение мета данных возможно только в режиме append-only (т.е. на попытку переопределить уже существующую запись в context meta, библиотека вернет ошибку). Поскольку на транспортном уровне контекст передается в виде custom HTTP заголовков, синтаксис метаданных key-value должен следовать ограничениям RFC7230 . Размер ключа записи метаданных не должен превышать 53 байта (см. остальные требования к метаданным в описании библиотеки).

19> Meta1 = #{<<"client1-name">> => <<"Vasya">>}.
20> Context3 = woody_context:new(<<"myUniqRequestID3">>, Meta1).
21> Meta1 = woody_context:get_meta(Context3).
22> Meta2 = #{<<"client2-name">> => <<"Masha">>}.
23> Context4 = woody_context:add_meta(Context4, Meta2).
24> <<"Masha">> = woody_context:get_meta(<<"client2-name">>, Context4).
25> FullMeta = maps:merge(Meta1, Meta2).
26> FullMeta = woody_context:get_meta(Context4).

Context также позволяет задать deadline на исполнение запроса. Значение deadline вложенных запросов можно менять произвольным образом. Также таймауты на запрос, вычисляемые по deadline, можно явно переопределить из приложения через transport_opts в woody_client:options(). Модуль woody_deadline содержит API для работы с deadline.

27> Deadline = {{{2017, 12, 31}, {23, 59, 59}}, 350}.
28> Context5 = woody_context:set_deadline(Deadline, Context4).
29> Context6 = woody_context:new(<<"myUniqRequestID6">>, undefined, Deadline).
30> Deadline = woody_context:get_deadline(Context5).
31> Deadline = woody_context:get_deadline(Context6).
32> true     = woody_deadline:is_reached(Deadline).

Кеширующий клиент

Для кеширования на стороне клиента можно иcпользовать обертку woody_caching_client. Она содержит в себе обычный woody_client, но кеширует результаты вызовов.

Дополнительно, woody_caching_client способен объединять одинаковые выполняющиеся параллельно запросы. Для включения этой функции необходимо указать в опциях joint_control => joint.

Перед использованием необходимо запустить служебные процессы, см woody_caching_client:child_spec/2.

Woody Server Thrift Handler

-module(my_money_thrift_service_handler).
-behaviour(woody_server_thrift_handler).

%% Auto-generated Thrift types from money.thrift
-include("my_money_thrift.hrl").

-export([handle_function/4]).

-spec handle_function(woody:func(), woody:args(), woody_context:ctx(), woody:options()) ->
    {ok, woody:result()} | no_return().
handle_function(give_me_money, Sum = {Amount, Currency}, Context, _MyOpts) ->

    %% RpcId можно получить из Context, полученного handle_function,
    %% для использования при логировании.
    RpcId = woody_context:get_rpc_id(Context),

    case check_loan_limits(Sum, Context, 5) of
        {ok, ok} ->

            %% Логи рекомендуется тэгировать RpcId.
            lager:info("[~p] giving away ~p ~p", [RpcId, Amount, Currency]),
            RequestMoney = {my_wallet_service, get_money, Sum},

            %% Используется значение Context, полученное из родительского вызова
            Opts = #{url => wallet, event_handler => woody_event_handler_default},
            Meta = #{<<"approved">> => <<"true">>},
            woody_client:call(RequestMoney, Opts, woody_context:add_meta(Context, Meta));
        {ok, not_approved} ->
            lager:info("[~p] ~p ~p is too much", [RpcId, Amount, Currency]),

            %% Thrift исключения выбрасываются через woody_error:raise/2 с тэгом business.
            woody_error:raise(business, #take_it_easy{})
    end.

check_loan_limits(_Limits, _Context, 0) ->

    %% Системные ошибки выбрасываются с помощью woody_error:raise/2 с тэгом system.
    woody_error:raise(system, {external, result_unknown, <<"limit checking service">>});
check_loan_limits(Limits, Context, N) ->
    Wallet = <<"localhost:8022/v1/thrift_wallet_service">>,
    RequestLimits = {my_wallet_service, check_limits, Limits},

    %% Используется Context, полученный handle_function.
    %% woody_context:new() вызывать не надо.
    Opts = #{url => Wallet, event_handler => woody_event_handler_default},
    try woody_client:call(RequestLimits, Opts, Context) of
        {ok, ok} -> {ok, approved};
        {exception, #over_limits{}} -> {ok, not_approved}
    catch
        %% Transient error
        error:{woody_error, {external, result_unknown, Details}} ->
            lager:info("Received transient error ~p", [Details]),
            check_loan_limits(Limits, Context, N - 1)
    end.

Woody Event Handler

Интерфейс для получения и логирования событий RPC библиотеки. Также содержит вспомогательные функции для удобного форматирования событий. Пример реализации event handler'а - woody_event_handler_default.erl.

Через опции обработчика можно сообщить параметры соответствия событий RPC для уровня логирования:

woody_event_handler_default:handle_event(Event, RpcId, Meta, #{
    formatter_opts => ...,
    events_severity => #{
        ['call service'] => debug,
        ...
    }
}).

Где эти параметры имеют значения по умолчанию в следующем виде:

#{
    events_severity => #{
        %% Пограничные события работы клиента
        ['client begin'] => debug,
        ['client end'] => debug,

        %% Начало вызова сервиса, перед формированием запроса
        ['call service'] => info,
    
        %% Результат вызова сервиса на клиенте
        ['service result'] => info,
        ['service result', error] => error,
        %% Событие состоявшегося вызова с возвращённой ошибкой в качестве 
        %% результата
        ['service result', warning] => warning,

        %% Клиентские события, включая обнаружения хоста
        ['client send'] => debug,
        ['client resolve begin'] => debug,
        ['client resolve result'] => debug,
        ['client receive'] => debug,
        ['client receive', error] => warning,

        %% Непосредственные события обслуживания запроса сервером
        ['server receive'] => debug,
        ['server send'] => debug,
        ['server send', error] => warning,

        %% Начало обслуживания вызова функции сервиса
        ['invoke service handler'] => info,

        %% Завершение обслуживание вызова с разным итогом
        ['service handler result'] => info,
        ['service handler result', error, business] => info,
        ['service handler result', error, system] => error,
        %% Обслуживание вызова завершилось поправимой ошибкой;
        %% по крайней мере она не в рамках бизнес-логики но и не системное 
        %% исключение
        ['service handler result', warning] => warning,
        
        %% События кеширующей обертки клиента
        ['client cache begin'] => debug,
        ['client cache end'] => debug,
        ['client cache hit'] => info,
        ['client cache miss'] => debug,
        ['client cache update'] => debug,
        ['client cache result'] => debug,
        
        %% Внутренние ошибки с разным контекстом/происхождением
        ['internal error', system] => error,
        ['internal error', business] => warning,
        %% Событие трассировки на уровне woody, см. далее
        
        ['trace event'] => debug
    }
}.

Tracing

Можно динамически включать и выключать трассировку http запросов и ответов.

На сервере:

application:set_env(woody, trace_http_server, true).
application:unset_env(woody, trace_http_server).

Prometheus metrics

Чтобы осуществлять экспорт метрик следует добавить соответствующий хэндлер для cowboy-сервера.

{deps, [
    ...
    {prometheus_cowboy, "0.1.8"}
]}

Для сбора серверных метрик необходимо на старте приложения объявить их

ok = woody_ranch_prometheus_collector:setup()

Если дополнительно интересуют все метрики ковбоя то можно добавить реализацию обсервера из библиотеки.

Для сбора клиентских метрик необходимо на старте приложения объявить их

ok = woody_hackney_prometheus_collector:setup()

Это будет публиковать целочисленные значения в шкале 'woody_hackney_pool_usage' с метками pool в качестве названия пула и status в качестве параметра соответствующего значения:

  • in_use_count -- используемые соединения в пуле;
  • free_count -- свободное количество в пуле;
  • queue_count -- очередь за свободными соединенеиями

TODO Возможно стоит рассмотреть публикацию метрик по количеству исполняемых запросов в общем, с разбивкой по хосту и количества новых и переиспользуемых соедининий в каждом из пулов. Хакни это предоставляет.