無題の備忘録

IT技術について調べたことや学んだこと、試したこと記録するブログです。Erlang、ネットワーク、 セキュリティ、Linux関係のものが多いです。

PostgreSQLの認証とアクセス権限の制御

PostgreSQLSQL 文を実行する際に、理解が必要な主な各種の認証とアクセス権限の制御方法について学びます。

ここで、OS は Ubuntu 20.04 LTS、PostgreSQL は 11 を想定しています。

PostgreSQL の認証やアクセス権限の制御には下記のようないくつかの層があります。

  • (1) OSによる制御
    • まずはOSによるアクセス制御があります。例えば、Linux なら iptables などで外部からのアクセスを許可が必要です。デフォルトでは TCP 5432 ポートを使うので、このポートへのアクセスが許可されている必要があります。
  • (2) PostgreSQL サーバによる制御
    • PostgreSQLサーバの設定によって、外部からのアクセスを許可するかを設定します。 postgresql.conf で設定します。
  • (3) ロールの認証
    • 接続してきたロールがアクセス可能かどうかを認証します。pg_hba.conf で設定します。
  • (4) アクセス権限
    • 接続してきたロールがデータベースやテーブルなどに対してアクセスする権限を持っているかで制御します。GRANT 文や REVOKE 文で設定します。

ここでは PostgreSQL について学びたいので、(2)〜(4)を学びたいと思います。

(2) PostgreSQL サーバによる制御 (postgresql.conf)

postgresql.conf ファイルで設定します。

例えば、Ubuntu 20.04 LTS だと下記のパスにファイルがあります。 /etc/postgresql/11/main/postgresql.conf

設定パラメータは、listen_addresses でデフォルトだとコメントアウトされています。

このパラメータには、アクセスを許可するIPアドレスを指定します。

設定例

リモートからのアクセスを許可しない

listen_addresses = 'localhost' と指定します。PostgreSQL が起動しているホストからのアクセスのみを許可します。リモートからのアクセスを許可しません。

特定のIPアドレスからのアクセスを許可する

listen_addresses = '192.168.0.100' と指定します。192.168.0.100 からのアクセスのみを許可します。その他のアクセスを許可しません。

すべてのアクセスを許可する

listen_addresses = '*' と指定します。すべてのネットワークからのアクセスを許可します。

(3) ロールの認証 (pg_hba.conf)

pg_hba.conf ファイルで設定します。ここで hbaとは、host-based authentication (ホストベース認証)の略です。

どのような接続方式で、どのデータベースに誰が接続できるかを制御します。

pg_hba.conf ファイルの書式は、1行ごとに記述するものです。1行にはいくつかのフィールドがあり、それらはスペースかタブで区切られます。

具体的には下記の7つの書式のどれかの形式をとります。

local      database  user  auth-method  [auth-options]
host       database  user  address  auth-method  [auth-options]
hostssl    database  user  address  auth-method  [auth-options]
hostnossl  database  user  address  auth-method  [auth-options]
host       database  user  IP-address  IP-mask  auth-method  [auth-options]
hostssl    database  user  IP-address  IP-mask  auth-method  [auth-options]
hostnossl  database  user  IP-address  IP-mask  auth-method  [auth-options]

各フィールドの意味

接続方式

接続データベース(database)

対象となるデータベース名を指定します。複数指定する場合は、カンマで区切ります。

その他、データベース名を指定する方法以外に、下記のような指定ができます。

  • all
    • すべてのデータベースを指定します。ユーザによって特に制限しない場合に使います。
  • sameuser
    • アクセスするデータベースと同名のユーザの接続に対応します。
  • samerole
    • データベースと同じ名前のロールのメンバでなければならないことを指定します。

その他、replication があるが直近で使いそうにないので省略します。

データベース名そのものではなくて、別のファイルにデータベース名のリストを記述した場合は、ファイル名の前に@を付けて、そのファイルを参照するように指定する方法もあるようです。

接続ユーザ(user)

データベースユーザを指定します。このユーザですが、どうも PostgreSQLではロールと、グループと同じ意味のようです。

CREATE ROLEは、PostgreSQLデータベースクラスタに新しいロールを加えます。 ロールとは、データベースオブジェクトを所有することができ、データベース権限を持つことができる実体のことです。 ロールは、使用状況に応じて「ユーザ」、「グループ」、もしくは、その両方であるとみなすことができます。

CREATE ROLE より

  • all
    • すべてのユーザを指定します
  • USER_NAME
    • 完全一致するデータベースユーザ名を指定します。
  • +USER_NAME
    • 先頭に + をつけると、そのロールのメンバと一致した場合に対象になります。
    • 例えば、roleA は roleB のメンバーだとすると、+roleB と指定すると、roleB だけでなく、roleA にも接続を許可することになります。

これ以外にもユーザ名のリストを含む別のファイルを作成して、ファイル名の前に@をつけて指定する方法もあるようです。

接続元のIPアドレス ( IP-address IP-mask)

データベースに接続するクライアントのアドレスを指定します。

アドレスの記述方法は、ホスト名、IPアドレスIPアドレスの範囲を指定することができます。その他、いくつか特殊な指定方法もあります。

  • 単一のホスト例
    • 172.20.143.89/32 (単一ホストを指定するには、IPv4では32、IPv6では128というマスク長を使用する)
  • IPアドレスの範囲の例(ネットワーク)
    • 172.20.143.0/24
  • 全てのIPv4アドレス
    • 0.0.0.0/0
  • 全てのIPv6アドレス
    • ::0/0
  • どのIPアドレスにも一致する
    • all
  • サーバ自身のIPアドレスのいずれかにも一致する
    • samehost
  • サーバが直接接続されているサブネット内のアドレスのいずれかにも一致する
    • samenet

認証方式 (auth-method)

クライアントから PostgreSQL サーバに接続するときの認証方式を指定します。

主に下記のような認証方式があります。

  • trust
    • 接続を無条件で許可する
  • reject
    • 接続を無条件で拒否する
    • 特定のホストをあるグループから除外するために使う
  • scram-sha-256
    • SCRAM-SHA-256 暗号化によるパスワード認証を行う
  • md5
    • md5 暗号化によるパスワード認証を行う
  • password
    • 平文でパスワード認証を行う
  • gss
    • GSSAPIによる認証を行う
  • sspi
    • SSPIによる認証を行う
    • Windowsでのみ使用可能
  • ident
    • クライアントのOSのユーザ名を idnet サーバから取得してデータベース接続ユーザ名として使用する
  • peer
    • OSのユーザ名をカーネルから取得してデータベースの接続ユーザ名として使用する
    • ローカル接続でのみ使用可能
  • ldap
    • LDAPサーバを使用して認証を行う
  • radius
    • RADIUSサーバを使用して認証を行う
  • cert
    • SSLクライアント証明書を使用して認証を行う。
  • pam
    • PAMを使用して認証を行う

認証オプション (auth-options)

auth-methodフィールドの後ろに、 認証方式のオプションを指定することがあります。 今回知りたいことではなかったので、ここでは、省略します。

(4) アクセス権限

力尽きたので、(4) アクセス権限 についは、また次回学びたいと思います。

参考URL :

https://www.postgresql.jp/docs/11/auth-pg-hba-conf.html

Nginx のリバースプロキシでポート変換する

数カ月前に ASUS X200LA (11.6インチ、Core i3 4010U/1.7GHz/2コア メモリ4GB)を人から譲ってもらったのだが、Ubuntu 20.04 LTS をインストールした後、特に使い道がなかった。

が、ふと思い立って gitlab をインストールして運用することにした。リモートワークでの作業メモなどを gitlab の issue や wiki に書いて記録を残したり、自分用のコードを保存したりしてる。そうこうしていると、ちょっとした他のサービスも運用したくなってきていくつかのサービスを ASUS X200LA にホストしている。

使っている手元のPCの /etc/hosts/ASUS X200LA のホスト名とIPアドレスを追記して使っていたが、各サービスの識別にはポート番号を直打ちしていた。

そうこうしていると、これらのサービスにブラウザがからアクセスするときに、ポート番号の入力が面倒になってきたので、それぞれのサービスに適当な名前をつけて、ポート番号を入力しなくても良いようにしたいと思うようになってきた。

そこで、Nginx のリバースプロキシを使うことにした。

ASUS X200LA に Nginx をインストールして、ホスト名によって、中継するポート番号を切り替える。

Nginx のインストールは下記のコマンドで行い、

sudo apt install nginx

リバースプロキシ用の設定ファイルを conf.d ディレクトリに新し作成する。例えば、以下のような感じだ。

$ cat /etc/nginx/conf.d/proxy.conf 
server {
    listen 80;
    server_name  a.service.example;
    location / {
        proxy_pass http://127.0.0.1:8001;
    }
}

server {
    listen 80;
    server_name  b.service.example;
    location / {
        proxy_pass http://127.0.0.1:8002;
    }
}

server {
    listen 80;
    server_name  c.service.example;
    location / {
        proxy_pass http://127.0.0.1:8003;
    }
}

Nginx を再起動する。

sudo service nginx restart

そしたら、手元のPCの /etc/hosts に 下記のような感じで追加しておけば、

192.168.1.101 a.service.example
192.168.1.101 b.service.example
192.168.1.101 c.service.example

ブラウザから a.service.example にアクセスすれば、192.168.1.101:8001 にアクセスできる。

Ubuntu 18.04 LTS で Logicool マーブルマウス TM-150r をスクロールできるようにする ver.2

以前、下記の記事を書いたが、最近つらくなってきた。何がつらいかというと、スクロールだ。

stacktrace.hatenablog.jp

原因は、小さい右のボタンを押し続けていないとスクロールができないのだが、このボタンを押し続けるのに結構力が必要で、指が痛くなってきた。

そこで、見直した結果がこちらの設定である。

Section "InputClass"
        Identifier      "Marble Mouse"
        MatchProduct    "Logitech USB Trackball"
        Driver          "libinput"
        Option          "ScrollMethod" "button"
        Option          "ScrollButton" "3"
EndSection

以前よりシンプルになった。それで、肝心のスクロールするためのボタンは「右の大きいボタン」にした。大きいボタンなら押しやすい。

右クリックは一回クリックすれば効くし、スクロールをしたくなれば右の大きいボタンをクリックしたまま、トラックボールを操作すれば良い。

スクロールするためのボタンが大きくなって押しやすくなったので、なんなら、この設定だけで、左側にトラックボールを置いて左手で操作しても良いし、左手が疲れてきたら、そのまま右側にトラックボールを置いて右手で操作しても良くなって、さらに使いやすくなった。

満足である。

Erlang の gen_server モジュールを学ぶ

以前の記事で (2年も前くらいの記事...)、gen_server ビヘイビアの概要を学びました。

今回は、gen_server モジュールに用意されている各関数と gen_server モジュールのコールバック関数を学びたいと思います。というか書きかけだったのに気がついて、今書き上げました。

gen_server はいくつかの関数をコールバック関数として export されていることを期待しています。

gen_server ビヘイビアの関数とコールバック関数の関係は下記のようになります。

例えば、gen_server:start_link 関数が呼ばれると、コールバックモジュール Moduleinit/1 関数が呼ばれるというふうに対応しています。

gen_server module            Callback module
-----------------            ---------------
gen_server:start
gen_server:start_link -----> Module:init/1

gen_server:stop       -----> Module:terminate/2

gen_server:call
gen_server:multi_call -----> Module:handle_call/3

gen_server:cast
gen_server:abcast     -----> Module:handle_cast/2

-                     -----> Module:handle_info/2

-                     -----> Module:handle_continue/2

-                     -----> Module:terminate/2

-                     -----> Module:code_change/3

コールバック関数が失敗するか、不正な値を返すと、gen_server プロセスは終了します。

gen_server はプロセスは sys モジュール に記述されているメッセージを扱うことができます。 sys モジュールは gen_server をデバッグするために使用することができます。

gen_server プロセスは、終了シグナル ( exit signals ) を自動的に捕捉 ( trap ) しないことに注意してください。コールバック関数の init 関数で明示的に process_flag(trap_exit, true) をする必要があります。

特に指定がない限り、指定された gen_server プロセスが存在しない場合、または不正な引数が指定されている場合、このモジュールの関数はすべて失敗します。

コールバック関数が timeout ではなくhibernate を指定している場合、gen_serverプロセスは休止状態になります( erlang:hibernate/3 を参照)。

これは、サーバーが長時間アイドル状態になることが予想される場合に役立ちます。 ただし、ハイバネーションは少なくとも、ハイバネートするときとアイドル状態から起きるときの 2回のガーベッジコレクションが行われます。また、サーバーが忙しい場合、各関数の呼び出しの間にハイバネーションを実行しない方が良いことに注意してください。

gen_server プロセスが初期化直後にアクションを実行する必要がある場合、またはコールバックの実行を複数のステップに分割する必要がある場合、タイムアウトまたはハイバネーションの値の代わりに {continue, Continue} を返すことができます。この値を返した直後に handle_continue/2 コールバック関数を呼び出します。

gen_server の関数

abcast/2, abcast/3 関数

abcast(Name, Request) -> abcast
abcast(Nodes, Name, Request) -> abcast

Types

Nodes = [Node]
 Node = atom()
Name = atom()
Request = term()

この関数は、指定した Nodes のローカルに Name として登録された gen_server プロセスへ非同期リクエストを送信します。 この関数はすぐに処理が戻り、存在しないノード、または Name という名前の gen_server が存在しない場合、無視します。

gen_server プロセスは、Module:handle_cast/2 を呼び出してリクエストを処理します。

call/2, call/3 関数

call(ServerRef, Request) -> Reply
call(ServerRef, Request, Timeout) -> Reply

Types

ServerRef = Name | {Name,Node} | {global,GlobalName}
  | {via,Module,ViaName} | pid()
 Node = atom()
 GlobalName = ViaName = term()
Request = term()
Timeout = int()>0 | infinity
Reply = term()

この関数は、gen_server プロセスの ServerRef へ同期呼び出しをします。リクエストを送信し、応答が到着するかタイムアウトが発生するまで待ちます。

gen_serverプロセスは、Module:handle_call/3 を呼び出してリクエストを処理します。

ServerRef は次のいずれかです。

  • pid() 、 gen_server プロセスのIDそのもの
  • Name、gen_server プロセスがローカルに登録されている場合
  • {Name,Node}、gen_server プロセスが別のノード ( Node ) のローカルに登録されている場合
  • {global,GlobalName}、gen_server プロセスがグローバルに登録されている場合
  • {via,Module,ViaName}、 gen_server プロセスが代替プロセスレジストリを介して登録されている場合

Request は任意の term()Module:handle_call/3 へ渡される引数の1つです。

Timeout は応答を待機する時間(ミリ秒)、または無期限に待機することを意味するアトム infinity です。デフォルトは 5000 です。 指定された時間内に応答が受信されない場合、関数呼び出しは失敗します。

呼び出し元がタイムアウトによる失敗を catch して実行を継続している場合、サーバーは少し遅れて応答すると、呼び出し元のメッセージキューにサーバーから応答が届きます。この場合、呼び出し元は、このようなガベージメッセージを破棄する必要があります。

Reply は、Module:handle_call/3 の返り値で定義されます。

呼び出しは、タイムアウトや呼び出された gen_server プロセスが呼び出し前や呼び出し中に死ぬなど、様々な理由で失敗する可能性があります。

cast/2 関数

cast(ServerRef, Request) -> ok

Types

ServerRef = Name | {Name,Node} | {global,GlobalName}
  | {via,Module,ViaName} | pid()
 Node = atom()
 GlobalName = ViaName = term()
Request = term()

非同期リクエストを gen_server プロセスの ServerRef に送信し、すぐに ok を返します。宛先ノードまたは gen_server プロセスが存在しない場合は無視します。

gen_server プロセスは、 Module:handle_cast/2 を呼び出してリクエストを処理します。

RequestModule:handle_cast/2 への引数の1つとして渡される term() です。

enter_loop/3, enter_loop/4, enter_loop/5 関数

enter_loop(Module, Options, State)
enter_loop(Module, Options, State, ServerName)
enter_loop(Module, Options, State, Timeout)
enter_loop(Module, Options, State, ServerName, Timeout)

Types

Module = atom()
Options = [Option]
 Option = {debug,Dbgs} | {hibernate_after,HibernateAfterTimeout}
  Dbgs = [Dbg]
   Dbg = trace | log | statistics
    | {log_to_file,FileName} | {install,{Func,FuncState}}
State = term()
ServerName = {local,Name} | {global,GlobalName}
  | {via,Module,ViaName}
 Name = atom()
 GlobalName = ViaName = term()
Timeout = int() | infinity

既存のプロセスを gen_server プロセスにします。戻りません。代わりに、呼び出しプロセスは gen_server プロセスの受信ループに入り、gen_server プロセスになります。

プロセスは、proc_libstart 関数の1つを使用して開始されている必要があります。

ユーザーは、プロセスの名前の登録など、プロセスの初期化に責任があります。 この関数は、gen_server ビヘイビアが提供するよりも複雑な初期化手順が必要な場合に役立ちます。

ModuleOptions および ServerNameは、start/3,4 または start_link/3,4 を呼び出すときのものと同じです。ただし、ServerNameが指定されている場合、この関数が呼び出される前にプロセスがそれに応じて登録されている必要があります。

StateTimeoutは、Module:init/1 の返り値と同じ意味を持ちます。コールバックモジュール Module は、init/1 関数をエクスポートする必要はありません。

呼び出しプロセスが proc_libstart 関数によって開始されなかった場合、または ServerName として登録されていない場合、関数は失敗します。

multi_call/2, multi_call/3, multi_call/4 関数

multi_call(Name, Request) -> Result
multi_call(Nodes, Name, Request) -> Result
multi_call(Nodes, Name, Request, Timeout) -> Result

Types

Nodes = [Node]
 Node = atom()
Name = atom()
Request = term()
Timeout = int()>=0 | infinity
Result = {Replies,BadNodes}
 Replies = [{Node,Reply}]
  Reply = term()
BadNodes = [Node]

最初にすべてのノードにリクエストを送信してから応答を待機することにより、指定されたノードで Name としてローカルに登録されたすべての gen_server プロセスに対して同期呼び出しを行います。

gen_serverプロセスは、Module:handle_call/3 を呼び出してリクエストを処理します。

この関数はタプル {Replies, BadNodes} を返します。Replies{Node, Reply} のリストで、BadNodes は存在しなかったノード、または Name として登録された gen_server が存在しないか、gen_server から応答しなかったノードのリストです。

Nodes は、リクエストの送信先となるノード名のリストです。デフォルト値は、既知のすべてのノードのリスト [node() | nodes()] です。

Name は、各ノードのローカルに登録された gen_server プロセスの名前です。

Request は、Module:handle_call/3 への引数の1つとして渡される term() です。

Timeout は応答を待機する時間(ミリ秒)、または無期限に待機することを意味するアトム infinity です。デフォルトは infinity です。 指定された時間内にノードから応答が受信されない場合、ノードは BadNodes に追加されます。

ノード Node の gen_server プロセスから応答 Reply を受信すると、{Node, Reply}Replies に追加されます。Reply は、Module:handle_call/3 の返り値で定義されます。

例えば、(C または Java ノードなど)ノードの1つがモニターを処理できず、要求の送信時に直ぐに gen_server プロセスが開始されないが、2秒以内には開始されるような場合、この関数はタイムアウトになるまで待機します。

タイムアウト後の遅い応答が呼び出し元のメッセージキューを汚染するのを防ぐために、仲介者プロセスを使用して呼び出しを行います。終了したプロセスに到着した後の遅い回答は破棄されます。

reply/2 関数

reply(Client, Reply) -> Result

Types

Client - see below
Reply = term()
Result = term()

gen_server プロセスは、Module:handle_call/3 の返り値で返信を定義できない場合に、この関数を使用して call/2,3 または multi_call/2.3.4 を呼び出したクライアントへ明示的に返信することができます。

Client は、コールバック関数に与えられる From 引数でなければなりません。Replyは、call/2,3 または multi_call/2,3,4 の返り値としてクライアントに返される任意の term() です。

start/3, start/4 関数

start(Module, Args, Options) -> Result
start(ServerName, Module, Args, Options) -> Result

Types

ServerName = {local,Name} | {global,GlobalName}
  | {via,Module,ViaName}
 Name = atom()
 GlobalName = ViaName = term()
Module = atom()
Args = term()
Options = [Option]
 Option = {debug,Dbgs} | {timeout,Time} | {hibernate_after,HibernateAfterTimeout} | {spawn_opt,SOpts}
  Dbgs = [Dbg]
   Dbg = trace | log | statistics | {log_to_file,FileName} | {install,{Func,FuncState}}
  SOpts = [term()]
Result = {ok,Pid} | ignore | {error,Error}
 Pid = pid()
 Error = {already_started,Pid} | term()

スタンドアロンの gen_serve rプロセス、つまり、監視ツリーの一部ではないため、スーパーバイザーを持たない gen_server プロセスを作成します。 引数と返り値の説明については、start_link/3,4 を参照してください。

start_link/3, 4 関数

start_link(Module, Args, Options) -> Result
start_link(ServerName, Module, Args, Options) -> Result

Types

ServerName = {local,Name} | {global,GlobalName}
  | {via,Module,ViaName}
 Name = atom()
 GlobalName = ViaName = term()
Module = atom()
Args = term()
Options = [Option]
 Option = {debug,Dbgs} | {timeout,Time} | {hibernate_after,HibernateAfterTimeout} | {spawn_opt,SOpts}
  Dbgs = [Dbg]
   Dbg = trace | log | statistics | {log_to_file,FileName} | {install,{Func,FuncState}}
  SOpts = [term()]
Result = {ok,Pid} | ignore | {error,Error}
 Pid = pid()
 Error = {already_started,Pid} | term()

監視ツリーの一部として gen_server プロセスを作成します。この関数は、スーパーバイザーによって直接または間接的に呼び出されます。gen_server プロセスがスーパーバイザーにリンクされることを保証します。

gen_server プロセスは Module:init/1 を呼び出して初期化します。同期された起動手順を確実にするために、start_link/3,4Module:init/1 から処理が戻るまで、この関数も処理が戻りません。

  • ServerName = {local,Name} の場合、gen_server プロセスは register/2 を使用して名前 Name としてローカルに登録されます。
  • ServerName = {global,GlobalName}の場合、gen_server プロセスID は global:register_name/2 を使用して名前 GlobalName としてグローバルに登録されます。
  • ServerName = {via, Module, ViaName} の場合、gen_server プロセスは Module で表されるレジストリに登録します。その Module ではコールバック関数として、global モジュールの同じ名前の関数に対応するように動作する関数 register_name/2unregister_name/1whereis_name/1send/2 をエクスポートします。つまり、これは {via, global, GlobalName} も有効な参照になることを意味しています。

Module はコールバックモジュールの名前です。

ArgsModule:init/1 への引数として渡される任意の term() です。

  • オプション {timeout, Time} が存在する場合、gen_server プロセスは Time ミリ秒を初期化に費やすことが許可されるか、初期化が終わらない場合 gen_server プロセスは終了され start_link 関数は {error, timeout} を返します。
  • オプション {hibernate_after, HibernateAfterTimeout} が存在する場合、gen_server プロセスはメッセージをHibernateAfterTimeout ミリ秒待機し、メッセージが受信されない場合、proc_lib:hibernate/3 を呼び出してプロセスは自動的に休止状態に入ります。
  • オプション {debug, Dbgs} が存在する場合、対応する sys 関数が Dbgs の各アイテムに対して呼び出されます。sys モジュールを参照してください。
  • オプション {spawn_opt, SOpts} が存在する場合、SOpts はオプションリストとして spawn_opt BIFに渡されます。これは、gen_server プロセスの生成に使用されます。 spawn_opt/2を参照してください。

gen_server プロセスが正常に作成および初期化されると、関数は {ok, Pid} を返します。Pid は gen_server プロセスの pid です。

指定された ServerName のプロセスが既に存在する場合、関数は {error, {already_started, Pid}} を返します。Pid はそのプロセスの pid です。

Module.:init /1Reason で失敗した場合、関数は {error, Reason} を返します。 Module:init /1{stop, Reason} または ignore を返す場合、プロセスは終了し、関数はそれぞれ {error, Reason} または ignore を返します。

stop/1, stop/3 関数

stop(ServerRef) -> ok
stop(ServerRef, Reason, Timeout) -> ok

Types

ServerRef = Name | {Name,Node} | {global,GlobalName}
  | {via,Module,ViaName} | pid()
 Node = atom()
 GlobalName = ViaName = term()
Reason = term()
Timeout = int()>0 | infinity

指定された Reason で終了するように gen_server に命令し、終了するのを待ちます。 gen_server プロセスは、終了する前に Module:terminate/2 を呼び出します。

サーバーが予期した理由で終了した場合、関数は ok を返します。normalshutdown または {shutdown,Term} 以外の理由により終了した場合、logger を使用してエラーレポートが発行されます。デフォルトの Reasonnormal です。

Timeout はゼロより大きい整数で、サーバーの終了を待機する時間(ミリ秒)、または無期限に待機することを意味するアトム infinity を指定します。デフォルトは infinity です。

指定した時間内に gen_server が終了しない場合は、timeout 例外が発生します。

プロセスが存在しない場合、noproc 例外が発生します。

コールバック関数

下記の関数は、gen_server コールバックモジュールからエクスポートされる関数です。

Module:code_change/3 関数

Module:code_change(OldVsn, State, Extra) -> {ok, NewState} | {error, Reason}

Types

OldVsn = Vsn | {down, Vsn}
  Vsn = term()
State = NewState = term()
Extra = term()
Reason = term()

このコールバックはオプションであるため、コールバックモジュールでエクスポートする必要はありません。

code_change/3 が実装されていないときに appup ファイルで Change = {advanced, Extra} が指定されたリリースアップグレード/ダウングレードが行われると、undef 終了理由でプロセスがクラッシュします。

この関数は、リリースのアップグレード/ダウングレード中に内部状態を更新する場合、つまり、{update, Module, Change, ...} 命令の場合に、gen_serverプロセスによって呼び出されます。 ここで、Change = {advanced, Extra} が appupファイルで指定されます。 詳細については、OTP Design Principles の Release Handling Instructions を参照してください。

アップグレードの場合、OldVsnVsn です。ダウングレードの場合、OldVsn{down, Vsn} です。 Vsn は、コールバックモジュール Module の古いバージョンの vsn 属性によって定義されます。 そのような属性が定義されていない場合、バージョンは Beam ファイルのチェックサムです。

State は gen_sever プロセスの内部状態です。

Extra はアップデート命令の {advanced, Extra} 部分から「そのまま」渡されます。

成功した場合、関数は更新された内部状態を返す必要があります。

関数が {error, Reason} を返す場合、進行中のアップグレードは失敗し、古いリリースにロールバックします。

Module:format_status/2 関数

Module:format_status(Opt, [PDict, State]) -> Status

Types

Opt = normal | terminate
PDict = [{Key, Value}]
State = term()
Status = term()

このコールバックはオプションであるため、コールバックモジュールでエクスポートする必要はありません。

デフォルトで gen_server モジュールは、コールバックモジュールの状態を返すように実装されています。

この関数は、下記の状況で gen_server プロセスによって呼び出されます。 * sys:get_status/1,2 の1つが呼び出され、gen_server ステータスを取得します。 Opt は、normalatom が設定されます。 * gen_server プロセスは異常終了し、エラーを記録します。Optterminateatom が設定されます。

この関数は、これらのケースの gen_server ステータスの形と見た目を変更するのに役立ちます。

sys:get_status/1,2 の返り値とそのステータスが終了エラーログにどのように表示されるかを変更するコールバックモジュールは、gen_serverプロセスの現在のステータスを説明する term を返す format_status/2インスタンスをエクスポートします。

PDict は gen_server プロセスのプロセス辞書の現在の値です。

State は gen_server プロセスの内部状態です。

この関数は現在の状態の詳細と gen_server プロセスの状態を変更する term である Status を返します。 Status が取ることのできる形式には制限はありませんが、sys:get_status/1,2 の場合(Optnormal の場合)、Status の推奨形式は [{data, [{"State", Term}]}] です。ここで、Term は gen_server の状態に関連する詳細を提供します。 この推奨事項に従うことは必須ではありませんが、これは sys:gen_status/1,2 の返り値の残りの部分とコールバックモジュールのステータスを一致させます。

この関数の1つの用途は、状態の代替表現をコンパクトにし、大きな状態の term をログファイルに書き出すことを避けます。

Module:handle_call/3 関数

Module:handle_call(Request, From, State) -> Result

Types

Request = term()
From = {pid(),Tag}
State = term()
Result = {reply,Reply,NewState} | {reply,Reply,NewState,Timeout}
  | {reply,Reply,NewState,hibernate}
  | {reply,Reply,NewState,{continue,Continue}}
  | {noreply,NewState} | {noreply,NewState,Timeout}
  | {noreply,NewState,hibernate}
  | {noreply,NewState,{continue,Continue}}
  | {stop,Reason,Reply,NewState} | {stop,Reason,NewState}
 Reply = term()
 NewState = term()
 Timeout = int()>=0 | infinity
 Continue = term()
 Reason = term()

gen_server プロセスが call/2,3 または multi_call/2,3,4 を使用して送信されたリクエストを受信するたびに、リクエストを処理するためにこの関数が呼び出されます。

Request は、call または multi_call に与えられる引数 Request です。

From はタプル {Pid, Tag} です。Pid はクライアントの pid、Tag は一意のタグです。

State は gen_sever プロセスの内部状態です。

  • {reply,Reply,NewState}{reply,Reply,NewState,Timeout} または {reply,Reply,NewState,hibernate} を返した場合、 Replycall/2,3 の返り値として From に返されるか、multi_call/2,3,4 の返り値に含まれます。gen_server プロセスは、新しい内部状態 NewState で実行を続けます。Timeouthibernate は、Module:init/1 を参照してください。
  • {noreply,NewState} または {noreply,NewState,Timeout} または {noreply,NewState,hibernate} を返した場合、gen_server プロセスは NewState で実行を続けます。From へは返信されません。From への返信は、reply/2 を使用して明示的に指定する必要があります。
  • {stop,Reason,Reply,NewState} を返した場合、ReplyFrom へ返されます。
  • {stop,Reason,NewState} を返した場合、From へは返信されません。From への返信は、reply/2 を使用して明示的に指定する必要があります。gen_server プロセスは Module:terminate(Reason,NewState) を呼び出し、終了します。

Module:handle_cast/2 関数

Module:handle_cast(Request, State) -> Result

Types

Request = term()
State = term()
Result = {noreply,NewState} | {noreply,NewState,Timeout}
  | {noreply,NewState,hibernate}
  | {noreply,NewState,{continue,Continue}}
  | {stop,Reason,NewState}
 NewState = term()
 Timeout = int()>=0 | infinity
 Continue = term()
 Reason = term()

gen_server プロセスが cast/2 または abcast/2,3 を使用して送信されたリクエストを受信するたびに、この関数はリクエストを処理するために呼び出されます。

引数と返り値の説明については、Module:handle_call/3 を参照してください。

Module:handle_continue/2 関数

Module:handle_continue(Continue, State) -> Result

Types

Continue = term()
State = term()
Result = {noreply,NewState} | {noreply,NewState,Timeout}
  | {noreply,NewState,hibernate}
  | {noreply,NewState,{continue,Continue}}
  | {stop,Reason,NewState}
 NewState = term()
 Timeout = int()>=0 | infinity
 Continue = term()
 Reason = normal | term()

このコールバックはオプションであるため、コールバックモジュールは、別のコールバックから {continue, Continue} を返す場合にのみエクスポートする必要があります。もし continue が使用され、コールバックが実装されていない場合、プロセスは undef エラーで終了します。

この関数は、前のコールバック関数が {continue, Continue} を返すたびに gen_server プロセスによって呼び出されます。

handle_continue/2 は、前のコールバック関数の直後に呼び出されるため、初期化後に作業を実行したり、コールバック内の作業を複数のステップに分割したり、途中でプロセス状態を更新したりするのに役立ちます。

他の引数と返り値の説明については、Module:handle_call/3 を参照してください。

Module:handle_info/2 関数

Module:handle_info(Info, State) -> Result

Types

Info = timeout | term()
State = term()
Result = {noreply,NewState} | {noreply,NewState,Timeout}
  | {noreply,NewState,hibernate}
  | {noreply,NewState,{continue,Continue}}
  | {stop,Reason,NewState}
 NewState = term()
 Timeout = int()>=0 | infinity
 Reason = normal | term()

このコールバックはオプションであるため、コールバックモジュールでエクスポートする必要はありません。 gen_server モジュールは、この関数のデフォルトの実装を提供します。デフォルト実装は、予期しない Info メッセージについてログに記録し、ドロップして {noreply, State} を返します。

この関数は、タイムアウトが発生したとき、または同期または非同期リクエスト(またはシステムメッセージ)以外のメッセージを受信したときに、gen_server プロセスによって呼び出されます。

Infoは、タイムアウトが発生した場合を表すアトムの timeout 、または受信したメッセージのいずれかです。

Module:init/1 関数

Module:init(Args) -> Result

Types

Args = term()
Result = {ok,State} | {ok,State,Timeout} | {ok,State,hibernate}
 | {ok,State,{continue,Continue}} | {stop,Reason} | ignore
 State = term()
 Timeout = int()>=0 | infinity
 Reason = term()

gen_server プロセスが start/3,4 または start_link/3,4 を使用して開始されるたびに、この関数は新しい gen_server プロセスによって呼び出されて初期化を行います。

Args は start 関数の引数の Args です。

初期化が成功した場合、この関数は {ok, State}{ok, State, Timeout}、または {ok, State, hibernate} を返します。State はgen_server プロセスの内部状態です。

整数のタイムアウト値を指定すると、Timeout ミリ秒以内に要求またはメッセージが受信されない限り、タイムアウトが発生します。タイムアウトが発生すると、Module:handle_info/2 コールバック関数にアトムの timeout が渡されます。 無期限に待機させたい場合は、タイムアウト値にはアトムの infinity を使用することができます。infinity はデフォルト値です。

タイムアウト値の代わりに hibernate が指定されている場合、プロセスは次のメッセージの到着を待機するときに (proc_lib:hibernate/3 を呼び出すことで) 休止状態に入ります。

初期化に失敗した場合、この関数は {stop, Reason} を返します。Reason は任意の term か、ignore です。

Module:terminate/2 関数

Module:terminate(Reason, State)

Types

Reason = normal | shutdown | {shutdown,term()} | term()
State = term()

このコールバックはオプションであるため、コールバックモジュールでエクスポートする必要はありません。 gen_server モジュールは、クリーンアップなしでデフォルトの実装を提供します。

この関数は、 gen_server プロセスが終了しようとしているときに呼び出されます。Module:init/1関数の反対に必要な後始末を行います。この関数から処理が戻ると gen_server プロセスは Reason で終了します。返り値は無視されます。

Reason は停止理由を示す term であり、State は gen_server プロセスの内部状態です。

Reason は gen_server プロセスが終了する理由に依存します。別のコールバック関数が停止タプル {stop, ..} を返したためである場合、Reason はそのタプルで指定された値を持ちます。失敗が原因である場合は、Reason はエラーの理由です。

gen_server プロセスが監視ツリーの一部であり、スーパーバイザーによって終了するように命じられている場合、次の条件がアタハマる場合、この関数は Reason = shutdown で呼び出されます。 * gen_server プロセスは、終了シグナルをトラップするように設定されている * スーパーバイザーの子仕様で定義されているシャットダウン戦略は、brutal_kill ではなく整数のタイムアウト値である

gen_server プロセスが監視ツリーの一部ではない場合でも、親から「EXIT」メッセージを受信すると、この関数が呼び出されます。Reason は、「EXIT」メッセージの場合と同じです。

それ以外の場合、gen_server プロセスはすぐに終了します。

normal, shutdown または {shutdown, Term} 以外の理由の場合、gen_server プロセスはエラーが原因で終了すると想定されているので、logger を使用してエラーレポートが発行されることに注意してください。

Erlang の監視ツリーで gen_server を停止する

Erlang -- gen_server Behaviour にある監視ツリーに含まれている gen_server の停止を試してみたいと思います。

スーパーバイザーとそれによって監視されるワーカーのサンプルコードは、前回の記事( Erlang の supervisor ビヘイビアを学ぶ - 無題の備忘録 )の sample_sup (supervisor ) モジュールと value_server ( gen_server ) モジュールを使います。

ワーカーのサンプルコードとして value_server を使いますが、下記のように2点変更します。

  • まず、init関数に process_flag(trap_exit, true),を追加します。これは、スーパーバイザーからの停止命令( shutdown メッセージ) を補足できるようにするためです。
  • 次に、terminate/2 関数を追加します。この関数にクリーンアップ処理を実装します。この例では、io:format で単に文字列をErlangシェルに表示します。

上記の2点を変更すると、value_server モジュールは下記のようなサンプルコードになります。

-module(value_server).
-behaviour(gen_server).

-export([start_link/1, value/1, pid/1]).
-export([init/1, handle_call/3, handle_cast/2, terminate/2]).

start_link({Name, Value}) ->
    gen_server:start_link({local, Name}, ?MODULE, Value, []).

value(Name) ->
    gen_server:call(Name, value).
pid(Name) ->
    gen_server:call(Name, pid).

init(Value) ->
    process_flag(trap_exit, true),
    {ok, Value}.

handle_call(value, _From, Value) ->
    {reply, Value, Value};
handle_call(pid, _From, Value) ->
    {reply, self(), Value}.
handle_cast(_Request, Value) ->
    {noreply, Value}.

terminate(Reason, Value) ->
    io:format("~p terminate reason:~p with value:~p.~n", [self(), Reason, Value]),
    ok.

下記は使用例です。

> c(sample_sup).
{ok,sample_sup}
> c(value_server).
{ok,value_server}
> {ok, Sup} = sample_sup:start_link().
{ok,<0.91.0>}
> value_server:value(name_a).
value_a
> PidA = value_server:pid(name_a).
<0.92.0>
> is_process_alive(PidA).
true
> supervisor:terminate_child(Sup, name_a).
<0.92.0> terminate reason:shutdown with value:value_a.
ok
> is_process_alive(PidA).
false

sample_sup:start_link() によってスーパーバイザーを起動します。すると、name_a という名前の子プロセスが起動されます。

value_server:value(name_a) は、name_a に保持している値を問い合わせるリクエストを送信しています。

value_server:pid(name_a) によって、ワーカーの Pid を確認しています。

下記の関数によって、name_a の子プロセスを終了しています。すると、value_server の terminate/2 関数に記述した処理が実行されていることがわかります。

> supervisor:terminate_child(Sup, name_a).
<0.92.0> terminate reason:shutdown with value:value_a.

最後は、下記の関数で name_a という名前の子プロセスが生きているか調べて、終了されていることを確認しました。

> is_process_alive(PidA).
false

Erlang の supervisor ビヘイビアを学ぶ

Erlang/OTP の supervisor ビヘイビアについて学びたいと思います。

スーパーバイザーの原則

スーパーバイザーは、子プロセスの起動、停止、子プロセスの監視に対して責任を持ちます。スーパーバイザーの基本的な考え方は、必要に応じて子プロセスを再起動することで、子プロセスを起動した状態に保つことです。

開始および監視する子プロセスは、子プロセスの仕様 (Child Specification) のリストで設定します。子プロセスは、このリストで指定された順序で開始され、逆の順序で終了します。

サンプルコード

gen_server を起動するスーパーバイザーのコールバックモジュールのサンプルコードです。

-module(sample_sup).
-behaviour(supervisor).

-export([start_link/0]).
-export([init/1]).

start_link() ->
    supervisor:start_link(sample_sup, []).

init(_Args) ->
    SupFlags = #{strategy => one_for_one, intensity => 1, period => 5},
    ChildSpecs = [
        #{id => name_a,
            start => {value_server, start_link, [{name_a, value_a}]},
            restart => permanent,
            shutdown => 3000,
            type => worker,
            modules => [name_server]},
        #{id => name_b,
            start => {value_server, start_link, [{name_b, value_b}]},
            restart => permanent,
            shutdown => 3000,
            type => worker,
            modules => [name_server]}],
    {ok, {SupFlags, ChildSpecs}}.

init/1 の返り値の SupFlags 変数は、スーパーバイザーのフラグを表します。

また、init/1 の返り値の ChildSpecs 変数は、子プロセスの仕様のリストです。

ここでの子プロセスの仕様は、 gen_server で実装した value_server というモジュールのワーカーを起動します。

value_server のコードは下記に示します。

-module(value_server).
-behaviour(gen_server).

-export([start_link/1, value/1, pid/1]).
-export([init/1, handle_call/3, handle_cast/2]).

start_link({Name, Value}) ->
    gen_server:start_link({local, Name}, ?MODULE, Value, []).

value(Name) ->
    gen_server:call(Name, value).
pid(Name) ->
    gen_server:call(Name, pid).

init(Value) ->
    {ok, Value}.

handle_call(value, _From, Value) ->
    {reply, Value, Value};
handle_call(pid, _From, Value) ->
    {reply, self(), Value}.
handle_cast(_Request, Value) ->
    {noreply, Value}.

value_server にリクエストを送信すると、 value_server が保持している値をレスポンスとして返すだけのサーバーです。

value_server は起動時に value_server:start_link({Name, Value}) で、 gen_server の名前 Name とその gen_server のプロセスが保持する値 Value を受け取ります。

起動時に指定した gen_server の名前 Name を引数に、インターフェース関数 value/1 を呼び出すとその名前の gen_server が保持する値 Value を返します。

プロセスを識別するために、gen_server の名前 Name を引数に、インターフェース関数 pid/1 を呼び出すとその gen_server のプロセスIDを返す関数も用意します。

使用例

上記のサンプルコードの sample_sup.erl と value_server.erl の使用例を下記に示します。

> c(sample_sup).
{ok,sample_sup}
> c(value_server).
{ok,value_server}
> sample_sup:start_link().
{ok,<0.91.0>}
> value_server:value(name_a).
value_a
> value_server:value(name_b).
value_b
> PidA = value_server:pid(name_a).
<0.92.0>
> PidB = value_server:pid(name_b).
<0.93.0>
> exit(PidA, kill_pid_a).
=SUPERVISOR REPORT==== 7-Nov-2019::13:13:24.047059 ===
    supervisor: {<0.91.0>,sample_sup}
    errorContext: child_terminated
    reason: kill_pid_a
    offender: [{pid,<0.92.0>},
               {id,name_a},
               {mfargs,{value_server,start_link,[{name_a,value_a}]}},
               {restart_type,permanent},
               {shutdown,3000},
               {child_type,worker}]
true
> value_server:value(name_a).
value_a
> value_server:value(name_b).
value_b
> value_server:pid(name_a).  
<0.99.0>
> value_server:pid(name_b).
<0.93.0>

このスーパーバイザー (sample_sup) は 2つの gen_server を子プロセスを持ちます。

使用例では、スーパーバイザーを起動すると2つの gen_server が起動するので、インタフェース関数 value/1pid/1 で値を返せるかどうか、その gen_server のプロセスIDがいくつかを確認しています。

その後、exit/2関数で name_a という名前を持つ gen_server を殺します。

殺しますが、スーパーバイザーの監視により即座に新しいプロセスで name_a を持つ gen_server が再起動されます。

name_a を殺した後、value/1関数で name_a という gen_server にリクエストを送信してレスポンスが返ってくるか確認しています。

また、name_a の gen_server のプロセスIDを確認し、以前のプロセスIDとは異なっていること(新しいプロセスであること)を確認しています。

スーパーバイザーのフラグ

スーパーバイザーのフラグのためのタイプの定義は下記のものです。

sup_flags() = #{strategy => strategy(),         % optional
                intensity => non_neg_integer(), % optional
                period => pos_integer()}        % optional
    strategy() = one_for_all
               | one_for_one
               | rest_for_one
               | simple_one_for_one

strategy は再起動戦略を指定します。

intensityperiod は最大再起動強度を指定します。

再起動戦略

再起動戦略は、コールバック関数 init によって返されるスーパーバイザーのフラグの strategy キーによって指定されます。

SupFlags = #{strategy => Strategy, ...}

スーパーバイザーのフラグはマップ形式です。このマップで、strategy はオプションです。指定されていない場合は、one_for_one がデフォルトになります。

one_for_one 戦略

子プロセスが終了すると、そのプロセスのみが再起動されます。

f:id:storkibis:20191104151842p:plain
one_for_one 戦略

1対1 の監視です。もし、子プロセスが死んだら、その子プロセスだけ再起動します。

one_for_all 戦略

子プロセスが終了すると、他のすべての子プロセスも終了し、最初に終了した子プロセスを含むすべての子プロセスが再起動されます。

f:id:storkibis:20191104151923p:plain
one_for_all 戦略

rest_for_one 戦略

子プロセスが終了すると、残りの子プロセスは終了します。 その後、終了した子プロセスと残りの子プロセスが再起動されます。ここで、残りのプロセスとは、プロセスの起動順で、終了したプロセスよりも後だった子プロセスのことです。別の言い方をすると、ChildSpecs数の子プロセスの仕様のリストで、終了したプロセスの Spec の記述よりもリストの後ろだった子プロセスのことです。

simple_one_for_one 戦略

この戦略のスーパーバイザーは、単純化された one_for_one スーパーバイザーであり、子プロセスの仕様を1つだけ持ちます。この戦略では、子プロセスを動的にスーパーバイザーに追加します。

simple_one_for_one スーパーバイザーは多くの子プロセスを持つことができるため、それらをすべて非同期的にシャットダウンします。つまり、子プロセスはクリーンアップを並行して実行するため、停止する順序は定義されていません。

最大再起動強度

スーパーバイザーには、特定の時間間隔で発生する可能性がある再起動の回数を制限するためのメカニズムが組み込まれています。 これは、コールバック関数initによって返されるスーパーバイザーのフラグマップの2つのキーの intensityperiod によって指定されます。

SupFlags = #{intensity => MaxR, period => MaxT, ...}

直近の MaxT 秒間に MaxR を超える数の再起動が発生した場合、スーパーバイザーはすべての子プロセスを終了し、スーパーバイザー自身を終了します。 その場合、スーパーバイザー自体の終了理由は shutdown にされます。

スーパーバイザーが終了すると、次に上位のスーパーバイザーが同様のアクションを実行します。 最大再起動強度によって、終了したスーパーバイザーを再起動するか、上位のスーパーバイザー自身を終了します。

この再起動メカニズムの目的は、同じ理由でプロセスが繰り返し停止と再起動するだけの状況に陥るのを防ぐことです。

スーパーバイザーのフラグであるマップのキーの intensityperiod はオプションです。 指定されていない場合、デフォルトでそれぞれ 1 と 5 が設定されます。つまり、直近の 5 秒間に 1 回の再起動が発生した場合、そのスーパーバイザーは子プロセスを終了して、自分自身も終了します。

intensityperiod の調整

デフォルト値は、5 秒ごとに 1 回の再起動です。 これは、深い監視階層がある場合でも、ほとんどのシステムで安全になるように選択されましたが、特定のユースケースに合わせて設定を調整する必要があるでしょう。

まず、 intensity によって、許容される再起動の大規模なバーストが決まります。 たとえば、再起動に成功した場合は、同じ秒以内であっても、最大 5 回や 10 回の試行のバーストを受け入れることができます。

次に、クラッシュが引き続き発生するが、スーパーバイザーがあきらめるほど頻繁ではない場合、持続的な故障率を考慮する必要があります。 intensity を10に設定し、period を 1 に設定すると、スーパーバイザーは子プロセスが 1 秒間に最大 10 回再起動し続けることを許可し、誰かが手動で介入するまでログをクラッシュレポートで満たします。

したがって、スーパーバイザーがその期間に対して再起動する回数の割合で継続して、再起動することを受け入れることができるように、period を十分に長く設定する必要があります。 たとえば、intensity の値を 5 、period を 30 秒に設定した場合、6 秒ごとに最大で 1 回の再起動が可能になります。つまり、ログがすぐにいっぱいにならず、 また、障害を観察して修正を適用する機会があります。

これらの選択は、問題のあるドメインに大きく依存します。 組み込みシステムなど、リアルタイムの監視と問題を迅速に修正する機能がない場合は、スーパーバイザーがあきらめて次のレベルにエスカレートして エラーは自動的にクリアする前に、1 分あたり最大 1 回の再起動を受け入れることができます。 一方、高い失敗率でも試行し続けることがより重要な場合は、1 秒あたり 1〜2 回の再起動という試行率が必要になる場合があります。

よくある間違い

  • バーストレートを考慮することを忘れないでください。intensity を 1 に設定し、period を 6 に設定すると、5/30 や 10/60 と同じ失敗率が得られますが、すぐに 2 回目の再起動試行が行われるわけではありません。これはおそらくあなたが望んでいたものではありません。言い換えると、6 秒間に 1回の再起動を許すのと、30秒間に 5 回の再起動を許すということは違うということです。後者では最初の 6 秒間に 5 回再起動しても、その後の24秒間に再起動が発生しなければ、許容されます。

  • バーストを許容する場合は、期間を非常に高い値に設定しないでください。 intensity を 5 に設定し、period を 3600(1時間)に設定すると、スーパーバイザーは 5 回の再起動の短いバーストを許可しますが、ほぼ1時間後に別の単一の再起動が発生すると放棄します。 おそらく、これらのクラッシュを個別のインシデントと見なしたいので、period を 5 分または 10 分に設定する方が合理的です。

  • アプリケーションに複数レベルの監視ツリーの階層がある場合、すべてのレベルで同じ再起動強度の値を単純に設定しないでください。 トップレベルのスーパーバイザーがアプリケーションを放棄して終了する場合の再起動の合計回数は、障害が発生した子プロセスより上のすべてのスーパーバイザーの再起動強度の値の積になることに注意してください。

たとえば、トップレベルで 10 回の再起動が許可され、次のレベルでも 10 回の再起動が許可されている場合、そのレベル以下でクラッシュした子プロセスは 100 回再起動されます。 おそらく、この再起動回数は過剰です。このような場合、トップレベルのスーパーバイザーに対して例えば最大 3 回の再起動を許可するように抑えた方が適切な場合があります。

子プロセスの仕様

子プロセスの仕様の型定義は次のとおりです。

child_spec() = #{id => child_id(),       % mandatory
                 start => mfargs(),      % mandatory
                 restart => restart(),   % optional
                 shutdown => shutdown(), % optional
                 type => worker(),       % optional
                 modules => modules()}   % optional
    child_id() = term()
    mfargs() = {M :: module(), F :: atom(), A :: [term()]}
    modules() = [module()] | dynamic
    restart() = permanent | transient | temporary
    shutdown() = brutal_kill | timeout()
    worker() = worker | supervisor
  • id は、スーパーバイザーによって内部で子プロセスの仕様を識別するために使用されます。 idキーは必須です。 この識別子は時々 "name" と呼ばれていることに注意してください。 可能な限り、"identifier" または "id" という用語が使用されるようになりましたが、後方互換性を維持するために、エラーメッセージなどで "name" の出現箇所を見つけることができます。
  • start は、子プロセスを開始するために使用される関数呼び出しを定義します。start キーは必須です。これは apply(M, F, A) として使用される module-function-arguments タプルです。Mはモジュール名、Fは関数名、Aは引数のことです。 次のいずれかの呼び出しになります(またはその結果になります)。
    • supervisor:start_link
    • gen_server:start_link
    • gen_statem:start_link
    • gen_event:start_link
  • restart は、終了した子プロセスをいつ再起動するかを定義します。restart キーはオプションです。指定しない場合、デフォルト値の permanent が使用されます。
    • permanent が設定された子プロセスは常に再起動されます。
    • temporary が設定された子プロセスは再起動されません(スーパーバイザーの再起動戦略が rest_for_one または one_for_all であり、兄弟プロセスが死亡したために一時プロセスが終了した場合でも、再起動されない)
    • transient が設定された子プロセスは、異常終了した場合のみ再起動します。つまり、終了理由が normalshutdown または {shutdown, Term} 以外の場合は再起動されます。
  • shutdown は子プロセスの終了方法を定義します。shutdownキーはオプションです。shutdown が指定されてない場合、子のタイプがワーカーの場合、デフォルト値は 5000 が使用されます。 子のタイプがスーパーバイザーの場合、デフォルト値は infinity が使用されます。
    • brutal_kill は、子プロセスが exit(Child, kill) を使用して無条件に終了されることを意味します。
    • 整数のタイムアウト値は、スーパーバイザーが exit(Child, shutdown) を呼び出して子プロセスに終了するように指示し、終了信号が返されるのを待つことを意味します。 指定された時間内に終了シグナルが受信されない場合、子プロセスは exit(Child, kill) を使用して無条件に終了されます。
    • 子プロセスが別のスーパーバイザーである場合、サブツリーにシャットダウンするのに十分な時間を与えるために、その子プロセスの shutdowninfinity を設定する必要があります。 子プロセスがワーカーの場合も、infinity を設定することができます。 以下の警告を参照してください。
      • 警告1 スーパーバイザーの子プロセスのシャットダウン時間に infinity 以外に設定すると、その子プロセスが自身の子プロセス(孫プロセス)たちとリンクを解除しようとするが、子プロセスが殺される前に孫プロセスの終了処理を失敗する競合を発生させる可能性があります。
      • 警告2 子プロセスがワーカーの場合、シャットダウン時間を infinity に設定するときは注意してください。 この状況では、監視ツリーの終了は子プロセスに依存するためです。 安全な方法で実装する必要があり、そのクリーンアップの処理は常に呼び出し元に返ってくる必要があります。
  • typeは、子プロセスがスーパーバイザー (supervisor) かワーカー(worker)かを指定します。type キーはオプションです。 指定されていない場合、デフォルト値 worker が使用されます。
  • modules は、子プロセスが supervisor, gen_server, gen_statem であるときは一つの要素を持つリスト[Module]になります。Module はコールバックモジュールの名前です。子プロセスが gen_event である場合、modules の値は dynamic でなければなりません。
    この情報はアップグレードとダウングレード中にリリースハンドラーによって使われます。リリースハンドリングを参照してください。
    modules キーはオプションです。指定されない場合、start キーに指定される{M, F, A}M の値が使われます。

スーパーバイザーの起動

最初に示したサンプルコードのスーパーバイザーの例では、sample_sup:start_link() を呼び出すことによって、スーパーバイザーが起動されます。

start_link() ->
    supervisor:start_link(sample_sup, []).

sample_sup:start_link は、supervisor:start_link/2 関数を呼び出します。この関数は、新しいプロセスであるスーパーバイザーを生成してリンクします。

  • 最初の引数 sample_sup は、コールバックモジュールの名前、つまり init コールバック関数が配置されているモジュールです。
  • 2番目の引数 [] は、コールバック関数 init にそのまま渡される term() です。最初に示したスーパーバイザーのサンプルコードでは、init 関数は入力データを必要とせず、引数を無視しています。

このケースでは、スーパーバイザーは名前を登録されていません。名前の代わりに pid を使用する必要があります。スーパーバイザーの名前を指定するには、supervisor:start_link({local, Name}, Module, Args) もしくは supervisor:start_link({global, Name}, Module, Args) を呼び出します。

新しいスーパーバイザープロセスは、コールバック関数 sample_sup:init([]) を呼び出します。この init 関数は {ok, {SupFlags, ChildSpecs}} を返さなければなりません。

init(_Args) ->
    SupFlags = #{strategy => one_for_one, intensity => 1, period => 5},
    ChildSpecs = [
                    #{id => name_a,
                        start => {value_server, start_link, [{name_a, value_a}]},
                        restart => permanent,
                        shutdown => 3000,
                        type => worker,
                        modules => [name_server]},
                    #{id => name_b,
                        start => {value_server, start_link, [{name_b, value_b}]},
                        restart => permanent,
                        shutdown => 3000,
                        type => worker,
                        modules => [name_server]}
                ],
    {ok, {SupFlags, ChildSpecs}}.

その後、スーパーバイザーは、指定された子プロセスの仕様に従ってすべての子プロセスを開始します。上の例では、2つの子プロセス name_aname_b が開始されます。   supervisor:start_link 関数は同期です。すべての子プロセスが開始されるまで処理は戻りません。

子プロセスの追加

静的な監視ツリーに加えて、次の呼び出しで既存のスーパーバイザーに動的に子プロセスを追加できます。

supervisor:start_child(Sup, ChildSpec)

Sup はスーパーバイザーの pid か名前です。ChildSpec は子プロセスの仕様 child_spec() を渡します。

start_child/2 を使用して追加された子プロセスは、他の子プロセスと同じように動作しますが、重要な例外があります。スーパーバイザーが死んで再起動された場合、スーパーバイザーに動的に追加されたすべての子プロセスは失われます。

子プロセスの停止

子プロセスは、静的または動的にかかわらず、シャットダウン仕様に従って停止できます。

supervisor:terminate_child(Sup, Id)

停止した子プロセスの仕様は、次の呼び出しで削除されます。

supervisor:delete_child(Sup, Id)

ここで、Sup はスーパーバイザーの pid または名前です。Id は、子プロセスの仕様の id キーに関連付けられた値です。

動的に追加された子プロセスと同様に、スーパーバイザー自体が再起動すると、静的な子プロセスを削除した効果は失われます。

Erlang のスタンドアローンの gen_server を停止する

Erlang -- gen_server Behaviour にあるスタンドアローンでの gen_server の停止を試してみたいと思います。

gen_server のサンプルコードとして sample_queue.erl というファイル名で下記のコードを作成します。 このコードは、gen_server のビヘイビアを使って、キューの機能を提供します。

in/1関数で gen_server にアイテムを登録して、out/0関数で gen_server からアイテムを取得します。stop/0関数で gen_server を停止します。

-module(sample_queue).
-behaviour(gen_server).

-export([start_link/0, in/1, out/0, stop/0]).
-export([init/1, handle_call/3, handle_cast/2, terminate/2]).

start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

in(Item) ->
    gen_server:call(?MODULE, {in, Item}).

out() ->
    gen_server:call(?MODULE, out).

stop() ->
    gen_server:cast(?MODULE, stop).


init(_Args) ->
    {ok, queue:new()}.

handle_call({in, Item}, _From, Queue) ->
    NewQueue = queue:in(Item, Queue),
    {reply, ok, NewQueue};

handle_call(out, _From, Queue) ->
    case queue:out(Queue) of
        {{value, Item}, NewQueue} -> {reply, Item, NewQueue};
        {empty, NewQueue} -> {reply, empty, NewQueue}
    end.

handle_cast(stop, Queue) ->
    {stop, normal, Queue}.

terminate(normal, _State) ->
    timer:sleep(1000),
    io:format("clean up done.~n"),
    ok.

下記は使用例です。下記では、sample_queue モジュールの gen_server を起動し、その後、sample_queue モジュールのAPIを使って、a, b, c というアイテムを登録し、その後はキューのアイテムを空になるまで取り出しています。最後に、gen_server を停止します。

> c(sample_queue).
{ok,sample_queue}
> sample_queue:start_link().
{ok,<0.86.0>}
> sample_queue:in(a).
ok
> sample_queue:in(b).
ok
> sample_queue:in(c). 
ok
> sample_queue:out().
a
> sample_queue:out().
b
> sample_queue:out().
c
> sample_queue:out().
empty
> sample_queue:stop().
ok
clean up done.

stop/0関数は gen_server:cast関数を呼び出しており、非同期リクエストです。

stop() ->
    gen_server:cast(?MODULE, stop).

そのため、sample_queue:stop() を呼び出すとすぐに ok が返ってきますが、終了処理は続いています。Erlang シェルで okの後に clean up done. が表示されているのはそのためです。

> sample_queue:stop().
ok
clean up done.

話は前後しますが、gen_server:cast(?MODULE, stop). の非同期リクエストは、下記のコールバック関数を呼び出し、

handle_cast(stop, Queue) ->
    {stop, normal, Queue}.

この関数の返り値を {stop, normal, Queue} とすることで、下記のように terminate(Reason, State) の Reason を normal とした関数にマッチします。後始末が必要な gen_server であればこの teminate/2 関数に後始末の処理を記述します。

terminate(normal, _State) ->
    timer:sleep(1000),
    io:format("clean up done.~n"),
    ok.