Giter Club home page Giter Club logo

woody_erlang's Introduction

Woody Build Status

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.

Tracing

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

На сервере:

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

woody_erlang's People

Contributors

arentrue avatar ciiol avatar kpy3 avatar keynslug avatar yozhig avatar kehitt avatar petrkozorezov avatar antonlva avatar galaxie avatar jrogov avatar 4tytwo avatar dinama avatar ndiezel0 avatar

Stargazers

George Belyakov avatar Conrad Steenberg avatar Roman Pushkov avatar  avatar

Watchers

James Cloos avatar  avatar struga avatar  avatar  avatar CI bot avatar

Forkers

aenglisc

woody_erlang's Issues

UserInfo в контексте

Хочется прокидывать auth token или user info в контексте по всей цепочке, чтобы бизнес логика могла на это ориентироваться.

Пустая метадата приходит в виде атома `undefined`

Версия: 249fa01d1385babf7da96aeb82d9ed006d55465d.

В спеке заявлено наличие поля с типом в виде мапы:

-type meta_invoke_service_handler() :: #{
    service  => woody:service_name(),
    function => woody:func(),
    args     => woody:args(),
    metadata => woody_context:meta()
}.

Но в реальных событиях приходит атом undefined. Я ожидал, что там будет пустая мапа.

Рассмотреть изменение работы с root запросами в woody клиенте

Согласно текущим требованиям к RPC lib на стороне клиента root запрос - всегда единственный и создаётся именно woody клиентом.

Более гибкий подход: в woody клиенте одинаково работать со всеми запросами, как с запросами относящимися к дочерней ноде dapper дерева, и при этом в api предоставить функцию/метод для генерации валидного контекста root запроса, который затем и передавать в api call woody клиента. Это позволит строить dapper дерево, включающее все запросы в рамках бизнес процесса, даже если он генерируют несколько root запросов в текущем определении такого запроса в woody.

Такое изменение потребует корректировку требований RPC lib и, вероятно, обновление всех остальных реализаций woody библиотеки.

handler_opts() — это list()

Сейчас в поведении woody_server_thrift_handler в handle_function() handler_opts() — это list(), а должен быть просто term().

Некорректное форматирование бизнес-исключений в текст

Судя по https://github.com/rbkmoney/woody_erlang/blob/master/src/woody_event_handler.erl#L190 не умеет форматировать бизнес-исключения, из-за чего мы наблюдаем следующие сообщения в логах:

13:58:30.834 [warning] [6o4igxur3 675339606916333568 675339612524118016]unknown woody event type 'service result' with meta #{result => {payproc_ClaimNotFound},status => exception}

Предложения по улучшению интроспекции

Ошибка

Когда на проводе мы получаем такое вот добро:

HTTP/1.1 500 Internal Server Error
server: Cowboy
date: Fri, 26 Aug 2016 11:51:56 GMT
content-length: 103
x-rbk-parent-id: 6k37eS4HH
x-rbk-span-id: CIwIGXFAAAA=:1
x-rbk-trace-id: 6k37eS4HH
x-rbk-rpc-error-thrift: application exception unknown
content-type: application/x-thrift

........Start..........GAn error occurred: {exception_not_declared_as_thrown,{'MachineFailed'}}........

В ивенте светится только следующая информация:

#{result => {transport_error,server_error},status => error}

HTTPS support

Поддержка https (как будущее требование к RPC библиотеке).

Dynamic support for thrift protocol

Реализовать поддержку различных thrift протоколов в зависимости от http хэдера content-type.

Некорректная обработка падения с exit

Код упал с noproc, в логах

18:58:58.907 [debug]  internal error: #{error => <<"http handler terminated abnormally">>,reason => {case_clause,{exit,{noproc,{gen_server,call,[{via,gproc,{n,l,{mg_storage_test_server,<<"incorrect_event_sink_id">>}}},{get_machine,<<"incorrect_event_sink_id">>}]}}}}}

Доп копания показали, что case_clause во внутренностях вуди

{[{reason,
      {case_clause,
          {exit,
              {noproc,
                  {gen_server,call,
                      [{via,gproc,
                           {n,l,
                              {mg_storage_test_server,
                                  <<"incorrect_event_sink_id">>}}},
                       {get_machine,<<"incorrect_event_sink_id">>}]}}}}},
  {mfa,{woody_server_thrift_http_handler,handle,2}},
  {stacktrace,
      [{woody_server_thrift_handler,handle_function_catch,6,
           [{file,
                "/Users/petrkozorezov/Desktop/workspace/rbk_money/petrkozorezov/machinegun/_build/default/lib/woody/src/woody_server_thrift_handler.erl"},
            {line,198}]},
       {woody_server_thrift_handler,process,1,
           [{file,
                "/Users/petrkozorezov/Desktop/workspace/rbk_money/petrkozorezov/machinegun/_build/default/lib/woody/src/woody_server_thrift_handler.erl"},
            {line,96}]},
       {woody_server_thrift_handler,start,5,
           [{file,
                "/Users/petrkozorezov/Desktop/workspace/rbk_money/petrkozorezov/machinegun/_build/default/lib/woody/src/woody_server_thrift_handler.erl"},
            {line,69}]},
       {woody_server_thrift_http_handler,do_handle,5,
           [{file,
                "/Users/petrkozorezov/Desktop/workspace/rbk_money/petrkozorezov/machinegun/_build/default/lib/woody/src/woody_server_thrift_http_handler.erl"},
            {line,317}]},
       {cowboy_handler,handler_handle,4,
           [{file,
                "/Users/petrkozorezov/Desktop/workspace/rbk_money/petrkozorezov/machinegun/_build/default/lib/cowboy/src/cowboy_handler.erl"},
            {line,111}]},
       {cowboy_protocol,execute,4,
           [{file,
                "/Users/petrkozorezov/Desktop/workspace/rbk_money/petrkozorezov/machinegun/_build/default/lib/cowboy/src/cowboy_protocol.erl"},
            {line,442}]}]}

Да, и не хватает стектрейса ;)

Bug in woody_server_thrift_handler:process/1

Функция woody_server_thrift_handler:process/1 не возвращает протокол, в случае ошибки чтения thrift_protocol:read(Protocol, message_begin).

Надо:

    case MessageBegin of
        ...
        {error, Reason} ->
            {handle_protocol_error(State1, Reason), State1#state.protocol}
    end.

Параметризация event handler'а

Не хватает возможности прокинуть параметры в event handler. Например, чтобы использовать один и тот же модуль для клиента и для сервера, при этом понимая, кто, что пишет.

Рефакторинг event_handler

Порефакторить event_handler behavior для более удобной работы с логированием в его реализации. Например, в woody_server_thrift_handler пользовательский handle_function теперь всегда возвращает WoodyContext и он тоже попадает в result поле метадаты вместе с thrift ответом, засоряя лог события.

Связана с Issue 12.

Поправить спеки для woody_server:child_spec

Сделать более понятный handler спек для woody_server:thrift_handler:

{
    ServiceSpec :: {
        Path,
        Service :: {ThriftHandler :: module(), ServiceThriftName :: atom()}
    },
    HandlerImpl :: module(),
    HandlerOpts :: term()
}

Возможно (в контексте Issue 12), даже, имеет смысл объединить последние 2 параметра в:

HandlerModOpts :: {HandlerImpl :: module(), HandlerOpts :: term()}

Всё в типы завернуть, разумеется.

Избавиться от флага #root_rpc в woody_context:ctx()

Убрать поддержку создания именно коренного запроса из woody_client, дабы унифицировать логику работы с контекстом на клиенте и сервере и избавиться от флага #root_rpc в woody_context:ctx().

Добавлять метаданные общие для всего запроса во все woody event'ы

Выделить среди множества полей woody meta к разным событиям, те которые относятся ко всей обработке rpс запроса (например, service, function, type) и добавлять их в метаданные во все woody event'ы (для этого можно сохранять их в woody context).

Взыв мозга от падения в format_return

В woody_client_thrift функция format_return делает немного не то, что от неё ожидается из её названия. Функции форматирования обычно переводят один формат данных в другой, и когда они падают это значит, что что-то не так с данными. Тут же функция по смыслу обрабатывает результат (handle_result), т.к. в результат не только переформатируется, но и некоторых случаях кидается исключение. И когда видишь стек трейс в котором всё заканчивается на format_return, первая мысль, что что-то взывается внутри.

Фильтрация деталей ошибок в woody_client_thrift_http_transport при возврате клиенту

В woody_client_thrift_http_transport во многих случаях детали ошибки, полученной в процессе вызова сервера, приводятся только в woody event'е, но не возвращаются клиенту.

Например:

handle_result({error, Reason}, Context) ->
     _ = log_event(?EV_CLIENT_RECEIVE, Context, #{status => error, reason => woody_error:format_details(Reason)}),
     {error, {system, {internal, result_unexpected, <<"http request send error">>}}}

Это сделано намеренно, так как не предполагается, что клиентский код будет завязывать бизнес логику на поле error details. Полученные детали ошибки при этом доступны для интроспекции через woody event.

Тем не менее, поступило предложение (@keynslug), все-таки передавать детали ошибки клиенту.

Обдумать и обсудить.

Добавить возможность long poling'а ивентов

Для случаев, когда клиент ожидает какого-то события от сервера добавить инструментами woody подписаться на такое событие. Реализовать это можно, например, с помощью long pooling'а event'ов.

Добавить транзиентные ошибки

Сейчас очень не хватает транзиентных ошибок (в http это 503).
Так же было бы полезно иметь bad gateway и gateway timeout.
Кроме того были мысли в cds заменить исключение KeyringLocked на 503 c коментарием (нужно придумать как коментарий к ошибкам сделать).

Дефолтный форматтер ругается на штатное событие

В результате в логах появляется вот такое:

16:32:54.541 [warning] [6oguXdgA3 684438162675597312 684438162780454912]unknown woody event type 'service result' with meta #{result => {payproc_NoLastEvent},status => exception}

Означенное событие эмиттится в woody_client_thrift:93 после получения клиентом бизнес-исключения.

Версия: b5ae9ae3abc8da470f68d2e2ca15ccc606801225.

Лишний тапл в результате woody:call

Сейчас woody:call в ответ возвращает {{ok, V},Context}, хотя интерфейс кидающий и атом ok там не нужен. Поэтому предлагается сделать просто {V,Context} если есть значение в ответ, и {ok,Context} если значения нет.

Поправить тип `woody_error:details()` и передачу stacktrace неожиданных системных ошибок сервера клиенту

  1. Тип woody_error:details() можно сделать более гибким (например, вообще, term()) и форматированием его в binary() заниматься на сервере непосредственно при построении http ответа.
  2. Сейчас (commit 992c279466e7eb1c24a9d9c6e7e8d66c597bc7e1) stacktrace неожиданной ошибки сервера (result_unexpected) прокидывается клиенту в http заголовке woody.error.reason. Корректно передавать такой stacktrace в body, а не в http заголовке. Надо задокументировать это, как общее требование к библиотеке и там же описать схему миграции (аналогично схеме миграции именования woody хэдеров). Затем поправить реализацию в woody_erlang.

Debugging improvements

  • When debug is enabled (option enable_debug=true) print the request body as well (or make the option more granular, so one can choose to print only headers or headers+body).
  • Make it possible to enable debug in runtime (not only on woody server startup)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.