woody_erlang/README.md
Andrew Mayorov d106ef66bd
Use new performant thrift codec infrastructure (#145)
* Implement woody_client_thrift_v2 over new thrift codec facility, make it default
* Move tracing facilities into woody_trace_h
* Implement woody_server_thrift_v2 over thrift codec facility, make it default
* Bump to rbkmoney/genlib@54920e76
* Enforce tuple-based args representation
* Enable cross tests between impls
* Switch to master rbkmoney/thrift_erlang@4eda678c
2020-08-18 13:01:47 +03:00

179 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Woody [![Build Status](http://ci.rbkmoney.com/buildStatus/icon?job=rbkmoney_private/woody_erlang/master)](http://ci.rbkmoney.com/job/rbkmoney_private/view/Erlang/job/woody_erlang/job/master/)
======
Erlang реализация [Библиотеки RPC вызовов для общения между микросервисами](http://coredocs.rbkmoney.com/design/ms/platform/rpc-lib/)
версия требований: __ac4d40cc22d649d03369fcd52fb1230e51cdf52e__
## API
### Сервер
Получить _child_spec_ RPC сервера:
```erlang
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)](http://erlang.org/doc/man/erlang.html#process_flag-2)) и на максимальный размер памяти vm (см. [erlang:memory(total)](http://erlang.org/doc/man/erlang.html#memory-1)), при достижении которого 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 приложения. Например:
```erlang
6> {ok, _} = supervisor:start_child(MySup, ServerSpec).
```
### Клиент
Сделать синхронный RPC вызов:
```erlang
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_ файле сервиса (т.е. _Бизнес ошибку_ в [терминологии](http://coredocs.rbkmoney.com/design/ms/platform/overview/#_7) макросервис платформы), `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}]`.
```erlang
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 ](https://tools.ietf.org/html/rfc7230#section-3.2.6). Размер ключа записи метаданных не должен превышать _53 байта_ (см. остальные требования к метаданным в [описании библиотеки](http://coredocs.rbkmoney.com/design/ms/platform/rpc-lib/#rpc_2)).
```erlang
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](http://coredocs.rbkmoney.com/design/ms/platform/rpc-lib/#deadline) на исполнение запроса. Значение _deadline_ вложенных запросов можно менять произвольным образом. Также таймауты на запрос, [вычисляемые по deadline](src/woody_client_thrift_http_transport.erl), можно явно переопределить из приложения через _transport_opts_ в `woody_client:options()`. Модуль [woody_deadline](src/woody_deadline.erl) содержит API для работы с deadline.
```erlang
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
```erlang
-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](src/woody_event_handler_default.erl).
### Tracing
Можно динамически включать и выключать трассировку http запросов и ответов.
На сервере:
```erlang
application:set_env(woody, trace_http_server, true).
application:unset_env(woody, trace_http_server).
```