PostgreSQLの認証とアクセス権限の制御
PostgreSQL で SQL 文を実行する際に、理解が必要な主な各種の認証とアクセス権限の制御方法について学びます。
ここで、OS は Ubuntu 20.04 LTS、PostgreSQL は 11 を想定しています。
PostgreSQL の認証やアクセス権限の制御には下記のようないくつかの層があります。
- (1) OSによる制御
- (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]
各フィールドの意味
接続方式
- local
- ローカルホストからの接続に対する制御
- host
- リモートホストからの接続に対する制御
- hostssl
- hostnossl
接続データベース(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アドレスの範囲を指定することができます。その他、いくつか特殊な指定方法もあります。
- 単一のホスト例
- 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 :
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
以前、下記の記事を書いたが、最近つらくなってきた。何がつらいかというと、スクロールだ。
原因は、小さい右のボタンを押し続けていないとスクロールができないのだが、このボタンを押し続けるのに結構力が必要で、指が痛くなってきた。
そこで、見直した結果がこちらの設定である。
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
関数が呼ばれると、コールバックモジュール Module
の init/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
を呼び出してリクエストを処理します。
Request
は Module: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_lib の start
関数の1つを使用して開始されている必要があります。
ユーザーは、プロセスの名前の登録など、プロセスの初期化に責任があります。 この関数は、gen_server ビヘイビアが提供するよりも複雑な初期化手順が必要な場合に役立ちます。
Module
、 Options
および ServerName
は、start/3,4
または start_link/3,4
を呼び出すときのものと同じです。ただし、ServerNameが指定されている場合、この関数が呼び出される前にプロセスがそれに応じて登録されている必要があります。
State
と Timeout
は、Module:init/1
の返り値と同じ意味を持ちます。コールバックモジュール Module
は、init/1
関数をエクスポートする必要はありません。
呼び出しプロセスが proc_lib
の start
関数によって開始されなかった場合、または 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,4
は Module: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/2
、unregister_name/1
、whereis_name/1
、send/2
をエクスポートします。つまり、これは{via, global, GlobalName}
も有効な参照になることを意味しています。
Module
はコールバックモジュールの名前です。
Args
は Module: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 /1
が Reason
で失敗した場合、関数は {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
を返します。normal
、 shutdown
または {shutdown,Term}
以外の理由により終了した場合、logger を使用してエラーレポートが発行されます。デフォルトの Reason
は normal
です。
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 を参照してください。
アップグレードの場合、OldVsn
は Vsn
です。ダウングレードの場合、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
は、normal
の atom が設定されます。
* gen_server プロセスは異常終了し、エラーを記録します。Opt
は terminate
の atom が設定されます。
この関数は、これらのケースの 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
の場合(Opt
が normal
の場合)、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}
を返した場合、Reply
はcall/2,3
の返り値としてFrom
に返されるか、multi_call/2,3,4
の返り値に含まれます。gen_server プロセスは、新しい内部状態NewState
で実行を続けます。Timeout
とhibernate
は、Module:init/1
を参照してください。{noreply,NewState}
または{noreply,NewState,Timeout}
または{noreply,NewState,hibernate}
を返した場合、gen_server プロセスはNewState
で実行を続けます。From
へは返信されません。From
への返信は、reply/2
を使用して明示的に指定する必要があります。{stop,Reason,Reply,NewState}
を返した場合、Reply
はFrom
へ返されます。{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/1
と pid/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
は再起動戦略を指定します。
intensity
と period
は最大再起動強度を指定します。
再起動戦略
再起動戦略は、コールバック関数 init
によって返されるスーパーバイザーのフラグの strategy
キーによって指定されます。
SupFlags = #{strategy => Strategy, ...}
スーパーバイザーのフラグはマップ形式です。このマップで、strategy
はオプションです。指定されていない場合は、one_for_one
がデフォルトになります。
one_for_one 戦略
子プロセスが終了すると、そのプロセスのみが再起動されます。
1対1 の監視です。もし、子プロセスが死んだら、その子プロセスだけ再起動します。
one_for_all 戦略
子プロセスが終了すると、他のすべての子プロセスも終了し、最初に終了した子プロセスを含むすべての子プロセスが再起動されます。
rest_for_one 戦略
子プロセスが終了すると、残りの子プロセスは終了します。 その後、終了した子プロセスと残りの子プロセスが再起動されます。ここで、残りのプロセスとは、プロセスの起動順で、終了したプロセスよりも後だった子プロセスのことです。別の言い方をすると、ChildSpecs
変数の子プロセスの仕様のリストで、終了したプロセスの Spec の記述よりもリストの後ろだった子プロセスのことです。
simple_one_for_one 戦略
この戦略のスーパーバイザーは、単純化された one_for_one スーパーバイザーであり、子プロセスの仕様を1つだけ持ちます。この戦略では、子プロセスを動的にスーパーバイザーに追加します。
simple_one_for_one スーパーバイザーは多くの子プロセスを持つことができるため、それらをすべて非同期的にシャットダウンします。つまり、子プロセスはクリーンアップを並行して実行するため、停止する順序は定義されていません。
最大再起動強度
スーパーバイザーには、特定の時間間隔で発生する可能性がある再起動の回数を制限するためのメカニズムが組み込まれています。 これは、コールバック関数init
によって返されるスーパーバイザーのフラグマップの2つのキーの intensity
と period
によって指定されます。
SupFlags = #{intensity => MaxR, period => MaxT, ...}
直近の MaxT
秒間に MaxR
を超える数の再起動が発生した場合、スーパーバイザーはすべての子プロセスを終了し、スーパーバイザー自身を終了します。 その場合、スーパーバイザー自体の終了理由は shutdown
にされます。
スーパーバイザーが終了すると、次に上位のスーパーバイザーが同様のアクションを実行します。 最大再起動強度によって、終了したスーパーバイザーを再起動するか、上位のスーパーバイザー自身を終了します。
この再起動メカニズムの目的は、同じ理由でプロセスが繰り返し停止と再起動するだけの状況に陥るのを防ぐことです。
スーパーバイザーのフラグであるマップのキーの intensity
と period
はオプションです。 指定されていない場合、デフォルトでそれぞれ 1 と 5 が設定されます。つまり、直近の 5 秒間に 1 回の再起動が発生した場合、そのスーパーバイザーは子プロセスを終了して、自分自身も終了します。
intensity
と period
の調整
デフォルト値は、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
が設定された子プロセスは、異常終了した場合のみ再起動します。つまり、終了理由がnormal
、shutdown
または{shutdown, Term}
以外の場合は再起動されます。
shutdown
は子プロセスの終了方法を定義します。shutdown
キーはオプションです。shutdown
が指定されてない場合、子のタイプがワーカーの場合、デフォルト値は 5000 が使用されます。 子のタイプがスーパーバイザーの場合、デフォルト値はinfinity
が使用されます。brutal_kill
は、子プロセスがexit(Child, kill)
を使用して無条件に終了されることを意味します。- 整数のタイムアウト値は、スーパーバイザーが
exit(Child, shutdown)
を呼び出して子プロセスに終了するように指示し、終了信号が返されるのを待つことを意味します。 指定された時間内に終了シグナルが受信されない場合、子プロセスはexit(Child, kill)
を使用して無条件に終了されます。 - 子プロセスが別のスーパーバイザーである場合、サブツリーにシャットダウンするのに十分な時間を与えるために、その子プロセスの
shutdown
にinfinity
を設定する必要があります。 子プロセスがワーカーの場合も、infinity
を設定することができます。 以下の警告を参照してください。- 警告1 スーパーバイザーの子プロセスのシャットダウン時間に
infinity
以外に設定すると、その子プロセスが自身の子プロセス(孫プロセス)たちとリンクを解除しようとするが、子プロセスが殺される前に孫プロセスの終了処理を失敗する競合を発生させる可能性があります。 - 警告2 子プロセスがワーカーの場合、シャットダウン時間を
infinity
に設定するときは注意してください。 この状況では、監視ツリーの終了は子プロセスに依存するためです。 安全な方法で実装する必要があり、そのクリーンアップの処理は常に呼び出し元に返ってくる必要があります。
- 警告1 スーパーバイザーの子プロセスのシャットダウン時間に
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_a
と name_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.