DiedUnknownId - distributed-processのデバッグ

parallel and concurrent haskellの14章を読んでいてtyped channelを用いた実装をやってみた際、なかなかデバッグできず苦労したのでメモ。

まず私の環境では以下のようにmoduleを分けて実装していた。
Channel.hs

module Channel where

data Message = Ping (SendPort ProcessId) deriving (Typeable, Generic)               
                                                                                    
instance Binary Message                                                             
                                                                                    
pingServerChannel :: Process ()                                                     
pingServerChannel = do                                                              
  Ping chan <- expect                                                               
  say $ printf "ping received from %s" (show chan)                                  
  mypid <- getSelfPid                                                               
  sendChan chan mypid                                                               
                                                                                    
$(remotable ['pingServerChannel])                                           
                                                                                    
master :: [NodeId] -> Process ()                                                    
master peers = do                                                                   
  ps <- forM peers $ \nid -> do                                                     
    say $ printf "spawning on %s" (show nid)                                        
    spawn nid $(mkStaticClosure 'pingServerChannel)                         
                                                                                    
  ports <- forM ps $ \pid -> do                                                     
    say $ printf "pinging %s" (show pid)                                            
    (sp, rp) <- newChan                                                             
    withMonitor pid $ do                                                            
      send pid $ Ping sp                                                            
      receiveWait                                                                   
        [ match $ \(ProcessMonitorNotification _ deadpid reason) -> do              
            say $ printf "process %s died: %s" (show deadpid) (show reason)         
            terminate                                                               
        ]                                                                           
    return rp                                                                       
                                                                                    
  forM_ ports $ \port -> do                                                         
    _ <- receiveChan port                                                           
    return ()                                                                       
                                                                                    
  say "All pongs received"                                                          
  terminate                                                                         

Main.hs

import Channel

main :: IO ()                                                                 
main = do                                                                     
  (command:args) <- getArgs                                                   
  case command of                                                             
    "simple" -> withArgs args $ distribMain (\_ -> master) Main.__remoteTable 
    "channel" -> withArgs args $ distribMain Channel.master Main.__remoteTable
    ...

14章の例をそれぞれ実装するためのcabalファイルで一つのExecutableとしてdistributedというバイナリを作成するようにしていた。そのためMain.hsでは第一引数でどの例を実行するのか指定できるようにし、それぞれの実装は別のモジュールとして分けていた。

わかりづらいと思うのでコードの全体についてはこちらを。
https://github.com/y-kamiya/parallel-concurrent-haskell/tree/master/src/Distributed

slaveノードを一つ作り、上記のコードを実行すると以下のような結果になる。

$ ./dist/build/distributed/distributed channel slave 44445 &
$ ./dist/build/distributed/distributed channel                                                                                                                                             
Sun Mar 22 03:13:44 UTC 2015 pid://localhost:44444:0:10: pinging pid://localhost:44445:0:18
Sun Mar 22 03:13:44 UTC 2015 pid://localhost:44444:0:10: process pid://localhost:44445:0:18 died: DiedUnknownId
distributed: ProcessTerminationException

withMonitorによる監視の時点でProcessMonitorNotificationとしてDiedUnknownIdが返って来ていると考えられる。

ただ、これだけだと悪い部分の原因を突き止めることができない。hackageやソースを見るとDiedUnknownIdはこれを表しているらしい。

DiedUnknownId   Invalid (process/node/channel) identifier

わからん。

のでググっていろいろ調べてみるとこんなJIRAのチケットが上がっていた。
https://cloud-haskell.atlassian.net/browse/DP-96

こんな感じで環境変数を定義して実行するとdebug traceが取れるらしいのでやってみた。

$ DISTRIBUTED_PROCESS_TRACE_FLAGS=d DISTRIBUTED_PROCESS_TRACE_CONSOLE=yes ./dist/build/distributed/distributed channel

実行結果。

$ DISTRIBUTED_PROCESS_TRACE_FLAGS=d DISTRIBUTED_PROCESS_TRACE_CONSOLE=yes ./dist/build/distributed/distributed channel 
Sun Mar 22 03:46:18 UTC 2015 [trace] MxProcessDied pid://localhost:44444:0:7 DiedNormal
Sun Mar 22 03:46:18 UTC 2015 [trace] MxProcessDied pid://localhost:44444:0:9 DiedNormal
Sun Mar 22 03:46:19 UTC 2015 pid://localhost:44444:0:10: pinging pid://localhost:45000:0:13
Sun Mar 22 03:46:19 UTC 2015 pid://localhost:44444:0:10: process pid://localhost:45000:0:13 died: DiedUnknownId
distributed: ProcessTerminationException

[trace]という行が追加された。ただ、masterプロセス側のものだけのよう。DiedNormalなので何かの処理が正常終了したらしい。

JIRAのチケットをもう少し読んでみるとリモートのプロセスのデバッグメッセージを取得するにはそれ用のコードを追加する必要があるとわかった。

Note that todo this for a remote node (i.e., writing out trace output for a node residing on another host or os process), you'd need to use the enableTraceRemote primitive and it's associated Apis.

enableTraceRemoteという関数を使うらしい。ということでそれがあるモジュールを調べてみる。
Control/Distributed/Process/Management/Internal/Trace/Remote.hs

-- | Remote Table.                                                               
remoteTable :: RemoteTable -> RemoteTable                                        
remoteTable = registerStatic "$enableTraceRemote" (toDynamic enableTraceRemote)  
                                                                                 
enableTraceRemote :: ProcessId -> Process ()                                     
enableTraceRemote pid =                                                          
  getSelfPid >>= enableTrace >> relay pid                                        
                                                                                 
-- | Starts a /trace relay/ process on the remote node, which forwards all trace 
-- events to the registered tracer on /this/ (the calling process') node.        
startTraceRelay :: NodeId -> Process ProcessId                                   
startTraceRelay nodeId = do                                                      
  withRegisteredTracer $ \pid ->                                                 
    spawn nodeId $ cpEnableTraceRemote pid                                       
                                                                                 
-- | Set the given flags for a remote node (asynchronous).                       
setTraceFlagsRemote :: TraceFlags -> NodeId -> Process ()                        
setTraceFlagsRemote flags node = do                                              
  nsendRemote node                                                               
              "trace.controller"                                                 
              ((Nothing :: Maybe (SendPort TraceOk)), flags)                     
||<                                                                                 
丁寧にコメントが付けられているおかげでなんとなくわかった。setTraceFlagsRemoteで特定のノードのどんな種類のデバッグメッセージを追うか指定し、startTraceRelayでそれをmaster側にメッセージとして送るためのプロセスを起動するという感じ。enableTraceRemoteでtrace用の関数がリモート側で使えるようになるということだろうか。

これをもとに実装してみたのがこちら。(変更したところのみ)
Channel.hs
>||
import qualified Control.Distributed.Process.Management.Internal.Trace.Remote as TR
import qualified Control.Distributed.Process.Management.Internal.Trace.Types as TT 

master :: [NodeId] -> Process ()                                           
master peers = do                                                          
  let flags = TT.defaultTraceFlags { TT.traceDied = Just TT.TraceAll }  
  ps <- forM peers $ \nid -> do                                            
    say $ printf "spawning on %s" (show nid)                            
    TR.setTraceFlagsRemote flags nid                                    
    _ <- TR.startTraceRelay nid                                         
    spawn nid $(mkStaticClosure 'pingServerChannel)                

Main.hs

import qualified Control.Distributed.Process.Management.Internal.Trace.Remote as TR

main :: IO ()                                                                 
main = do                                                                     
  (command:args) <- getArgs                                                   
  case command of                                                             
    "simple" -> withArgs args $ distribMain (\_ -> master) Main.__remoteTable 
    "channel" -> withArgs args $ distribMain Channel.master TR.remoteTable 
    ...

これを実行した結果がこちら。

$ DISTRIBUTED_PROCESS_TRACE_FLAGS=d DISTRIBUTED_PROCESS_TRACE_CONSOLE=yes ./dist/build/distributed/distributed channel
Sun Mar 22 04:06:20 UTC 2015 [trace] MxProcessDied pid://localhost:44444:0:7 DiedNormal
Sun Mar 22 04:06:20 UTC 2015 [trace] MxProcessDied pid://localhost:44444:0:9 DiedNormal
Sun Mar 22 04:06:21 UTC 2015 pid://localhost:44444:0:10: spawning on nid://localhost:45000:0
Sun Mar 22 04:06:21 UTC 2015 [trace] MxProcessDied pid://localhost:44444:0:4 DiedDisconnect
Sun Mar 22 04:06:21 UTC 2015 [trace] MxProcessDied pid://localhost:44444:0:4 DiedDisconnect
Sun Mar 22 04:06:21 UTC 2015 [trace] MxProcessDied pid://localhost:44444:0:4 DiedDisconnect
Sun Mar 22 04:06:21 UTC 2015 [trace] MxProcessDied pid://localhost:44444:0:4 DiedDisconnect
Sun Mar 22 04:06:21 UTC 2015 [trace] MxProcessDied pid://localhost:44444:0:4 DiedDisconnect
Sun Mar 22 04:06:21 UTC 2015 pid://localhost:44444:0:10: pinging pid://localhost:45000:0:15
Sun Mar 22 04:06:21 UTC 2015 [trace] MxProcessDied pid://localhost:45000:0:15 (DiedException "user error (Error: Could not resolve closure: Invalid static label 'Channel.pingServerChannel')")
Sun Mar 22 04:06:21 UTC 2015 [trace]  [network] invalid request: 12884902916
Sun Mar 22 04:06:21 UTC 2015 [trace]  [network] invalid request: 12884902916
Sun Mar 22 04:06:21 UTC 2015 [trace]  [network] invalid request: 12884902916
Sun Mar 22 04:06:21 UTC 2015 pid://localhost:44444:0:10: process pid://localhost:45000:0:15 died: DiedUnknownId
distributed: ProcessTerminationException

DiedExceptionの内容が取得できた。Channel.pingServerChannelというのがremoteTableに登録されてないということのよう。登録する部分を$(remoteTable ['Channel.pingServerChannel])という形にしてみたがそれでもうまくいかなかった。RemoteTableの仕組みについて調べてみる必要がありそうだが、それはまた別の機会に。

今回はモジュール分けせずに一つのファイル内に実装することで上記の問題は解決した。

デバッグメッセージとして追えるものにはいくつか種類があるようで、今回使ったのはtraceDie。以下のモジュールに定義されているのでプロセスが死んだとき以外でデバッグメッセージを取得した場合はそのフラグを指定することで取ることができる。
Control.Distributed.Process.Management.Internal.Trace.Types

また、これをやっている最中におかしいなと感じた点が、コード修正してビルドしなおしてもRemoteTableが更新されていないかのような挙動をした場合があったこと。一度登録したRemoteTableの値はどこかに保存されて使いまわされている?もしそうならかなり不便な気がするので何か思い違いをしている所があるのかもしれない。。