2016년 12월 13일 화요일

GenServer.call은 응답 메시지는 어떻게 구분할까.

Erlang/Elixir 를 처음 공부할때 프로세스 통신에서 조금 신기했습니다. 다른 언어들은 동기 통신을 설명하고 비동기 통신을 설명하는데 Erlang/Elixir 는 비동기 통신부터 설명하고 동기 통신에 대해선 GenServer의 call 를 제외하곤 잘 다루지 않고 GenServer.call 또한 내부적으로 비동기 호출을 이용해서 구현되어 있다고 합니다.

그렇다면 GenSever의 call은 어떻게 구현되어 있을까요. 그리고 더 나아가서 프로세스들이 주고받는 메시지는 어떻게 구분할까요.

시나리오

"프로세스들이 주고받는 메시지는 어떻게 구분할까" 를 살펴보기 위해 시나리오를 그려봤습니다.
만약 한 액터가 다른 액터에게 동기 메시지를 보내고 응답 메시지를 기다리는 도중에 다른 액터에게서 새로운 요청 메시지를 받았다면 그 액터는 이 메시지가 응답 메시지인지 요청 메시지인지 어떻게 구분할까.
의 상황을 말하고 싶었는데 제가 글솜씨가 떨어져서 무리가 있네요.


1) 독립된 프로세스인 AcotrA, AcotrB, AcotrC 를 만듭니다.
2) ActorB 는 ActorC로 call함수로 동기로 메시지를 보냅니다. (call)
3) 보낸 메시지는 ActorC의 mail box에 쌓입니다.
1) ActorB가 ActorC를 GenServer.call를 이용하여 동기 호출
4) ActorC는 mail box에서 메시지를 뽑아 처리를 합니다.
5) ActorB는 ActorC의 응답을 기다립니다.
2) ActorC는 ActorB로 부터 받은 메시지를 처리중, ActorB는 block 상태

6) ActorB가 ActorC의 메시지를 기다리는 동안 ActorA가 ActorB에게 메시지를 보냅니다.
7) ActorB는 ActorA로 부터 온 메시지는 무시하고 ActorC에게 응답이 올때까지 대기합니다.
3) ActorC에 의해 block된 ActorB로 ActorA가 메시지를 보냄
8) ActorC가 ActorB에게 응답 메시지를 보냅니다.
9) ActorB는 응답 메시지를 받아 대기를 풀고 응답에 메시지에 대한 처리를 합니다.
4) ActorC가 처리를 끝내고 응답을 줌, ActorB가 응답을 받고 후처리를 함





코드

그럼 이제 코드를 보면서 GenServer가 어떻게 비동기 호출을 이용해서 동기 호출 call을 구현했고, 어떻게 메시지들을 구분하는지 알아보겠습니다.


%% -------------------------------------------
%% gen_server.erl
%% -------------------------------------------

call(Name, Request) ->
    case catch gen:call(Name, '$gen_call', Request) of
 {ok,Res} ->
     Res;
 {'EXIT',Reason} ->
     exit({Reason, {?MODULE, call, [Name, Request]}})
    end.


일단 GenServer모듈의 gen_server.erl 소스를 보면 굉장이 간단하게 작성되어 있습니다. 모듈화 자체가 잘되어 있어서 gen_server.erl내에서 구현된것보다 외부 모듈을 사용하는것이 많습니다.
 


case catch gen:call(Name, '$gen_call', Request) of

우리가 보고싶은 call 함수도 직접적인 구현은 gen 모듈에 구현되어있고 gen_server.erl에서는 호출만 하고 있습니다.


%% -------------------------------------------
%% gen.erl
%% -------------------------------------------

do_call(Process, Label, Request, Timeout) ->
    try erlang:monitor(process, Process) of
 Mref ->
     catch erlang:send(Process, {Label, {self(), Mref}, Request},
    [noconnect]),
     receive
  {Mref, Reply} ->
      erlang:demonitor(Mref, [flush]),
      {ok, Reply};
  {'DOWN', Mref, _, _, noconnection} ->
      Node = get_node(Process),
      exit({nodedown, Node});
  {'DOWN', Mref, _, _, Reason} ->
      exit(Reason)
     after Timeout ->
      erlang:demonitor(Mref, [flush]),
      exit(timeout)
     end
    catch
 error:_ ->
     Node = get_node(Process),
     monitor_node(Node, true),
     receive
  {nodedown, Node} -> 
      monitor_node(Node, false),
      exit({nodedown, Node})
     after 0 -> 
      Tag = make_ref(),
      Process ! {Label, {self(), Tag}, Request},
      wait_resp(Node, Tag, Timeout)
     end
    end.



우리가 궁금해하는 동작은 gen.erl 에 do_call 함수안에 있습니다.
뭔가 좀 긴듯한 느낌이 들지만 애러 핸들링 하는 부분을 빼고나면 중요한 로직은 세부분만 남습니다.

첫번째 코드는 erlang:monitor를 이용해서 메시지를 보낼 프로세스의 모니터를 생성하는 부분입니다.
   try erlang:monitor(process, Process) of

모니터는 call를 호출할때마다 새로 생성하고 이때 리턴받은 레퍼런스를 메시지를 구분하는 아이디 용도로 요청을 보낼때 그리고 응답을 받았을때 사용합니다.

두번째는 erlang:send를 이용해서 메시지를 보내는 코드입니다. 여기서 눈여겨볼 부분은 메시지 안에 메시지를 보내는 나의 pid(self() 함수)와 첫번째 코드에서 생성한  Mref(레퍼런스)를 메시지와 합쳐서 보내는 부분입니다.

   catch erlang:send(Process, {Label, {self(), Mref}, Request},

이렇게 {self(), Mref} 를 메시지와 같이 보내서 요청받은 프로세스가 완료된 응답 메시지를 누구에게 다시 응답줄지를 결정하게 됩니다. 요청받은 프로세스는 요청을 다 완료한 후 self() 로 넘겨준 pid 에게 {Mref, Reply} 라는 메시지를 응답으로 보내게 됩니다.

그리고 그 응답 메시지를 받는 부분이 바로 세번째 코드인 receive 입니다.

   receive
     {Mref, Reply} ->
       erlang:demonitor(Mref, [flush]),
       {ok, Reply};

receive 는 원하는 패턴이 올때까지 대기를 하면서 기다립니다. 그리고 이 패턴은 내가 요청을 보낼때 보냈던 메시지 아이디 대용으로 사용한 레퍼런스 Mref를 포함한 응답 메시지 입니다. 


그리고 정상적으로 응답이 왔다면 레퍼런스를 이용해서 해당 프로세스에대한 모니터를 해제하고 응답 메시지를 리턴합니다.

receive의 erlang 도큐먼트를 보면 다음과 같은 설명이 있습니다.
Receive

receive
    Pattern1 [when GuardSeq1] ->
        Body1;
    ...;
    PatternN [when GuardSeqN] ->
        BodyN
end 
Receives messages sent to the process using the send operator (!). The patterns Pattern are sequentially matched against the first message in time order in the mailbox, then the second, and so on. If a match succeeds and the optional guard sequence GuardSeq is true, the corresponding Body is evaluated. The matching message is consumed, that is, removed from the mailbox, while any other messages in the mailbox remain unchanged.

The return value of Body is the return value of the receive expression.

receive never fails. The execution is suspended, possibly indefinitely, until a message arrives that matches one of the patterns and with a true guard sequence.

위 설명에서 제일 중요한 부분은 다른 메시지는 변경하지 않고 원하는 패턴의 메시지가 왔을때만 사용한다는 부분입니다. 바로 이런 특성 덕분에 프로세스는 내가 받은 요청 메시지들과 응답으로 받은 응답 메시지를 구분할 수 있습니다.


결론

Erlang/Elixir를 처음 공부할때 쉽게 오해할 수 있는 부분이 메일 박스가 단순한 메시지 큐이고 receive는 순서대로 메시지를 빼온다는것 입니다. (책을 꼼꼼히 읽는 분들은 아니겠지만..) 하지만 실제로는 그렇게 작동하지 않죠 메일 박스는 큐처럼 쌓이긴 하지만 앞에서부터 소비되지 않고 receive는 패턴매칭을 이용해서 원하는 메시지만 메일 박스에서 소비됩니다. 이런 작은 특성들이 복잡하게 보일수도 있었던 기능들을 간단하게 구현 가능하게 했습니다. 그러면서 도큐먼트를 자세히 읽어야 겠다는 반성을 하게 되네요..