1.11.2014

Scala: Parallel SBT Test with Docker Containers

Scala: Docker を使って sbt test を並列化してみた

 

Docker + CI が巷で流行しているそうなので便乗。

 

動機

ここに、残念なテストがある。

ローカル上の Redis サーバに接続し、100件のレコードを投入した後で 1件削除。
そのときにレコード数が 99件かどうかを検証するだけの簡単なプログラムだ。

これが全部で 4セットほどある。

このテストは明らかに

  • 無駄な sleep が入っていて実行時間が長い (1クラスごとに10秒待たされる)
  • テストを並列で実行するとカウントがずれてテストが通らない場合がある
  • テストを流す前に既に DB に他のデータが入っていたら、カウントがずれてテストが通らない

というような問題を抱えている。

 

単に実行すると 48秒かかる
$ sbt test
(略)
[info] Passed: Total 4, Failed 0, Errors 0, Passed 4
[success] Total time: 48 s, completed Jan 11, 2014 1:35:59 AM

 

parallelExecution を有効にするとテストが通らない
$ sbt 'set parallelExecution in Test := true' test
(略)
[info] test1 should
[error] x insert 100 records and delete 1 record
[error]    'Some(96)' is not Some with value '99' (RedisConnectionSpec.scala:27)
(略)
[error] Failed: Total 4, Failed 4, Errors 0, Passed 0
[error] Failed tests:
[error]         com.github.mogproject.dockersbttest.example.RedisConnectionSpec1
[error]         com.github.mogproject.dockersbttest.example.RedisConnectionSpec2
[error]         com.github.mogproject.dockersbttest.example.RedisConnectionSpec3
[error]         com.github.mogproject.dockersbttest.example.RedisConnectionSpec4
[error] (test:test) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 14 s, completed Jan 11, 2014 1:37:52 AM

 

テストデータ以外が含まれていると、やはりテストが通らない
$ redis-cli set 'foo' 'bar'
OK
$ sbt test
(略)
[info] test1 should
[error] x insert 100 records and delete 1 record
[error]    'Some(100)' is not Some with value '99' (RedisConnectionSpec.scala:27)
(略)
[error] Failed: Total 4, Failed 4, Errors 0, Passed 0
[error] Failed tests:
[error]         com.github.mogproject.dockersbttest.example.RedisConnectionSpec4
[error]         com.github.mogproject.dockersbttest.example.RedisConnectionSpec3
[error]         com.github.mogproject.dockersbttest.example.RedisConnectionSpec2
[error]         com.github.mogproject.dockersbttest.example.RedisConnectionSpec1
[error] (test:test) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 46 s, completed Jan 11, 2014 1:39:52 AM
$ redis-cli del 'foo'
(integer) 1

 

アイデア

当然ながら、不要な待ち時間を減らしたり、モックを作ったりして状態への依存を極力少なくした
正しいテストを書くのが最もよいアプローチだと思う。

しかし現実にはそれが難しかったり、対応に時間がかかってしまう場合もある。

そこで、Docker を使ってテストクラスの単位で独立したコンテナを立ち上げ
その中で個別のテスト(sbt 'test-only クラス名')を実行したいと考えた。

処理の流れは以下のとおり。

  1. [事前準備] あらかじめ、テストで使う Docker イメージを作成しておく。(以下、ベースイメージとする)
  2. [前処理] ベースイメージに対して、2種類の変更を行い新規イメージを作成。
    そのイメージ(以下、テストイメージとする) に対して新しいタグを付ける。
    • 事前コマンド
      テストの度に実行するコマンドがあれば、それを登録。
      モジュールの最新化 (git pull) などを想定。
    • コンパイル
      テストを流すたびにコンパイルが走ると遅くなるので、事前に sbt compile test:compile を実行
  3. [テスト一覧取得] テストイメージを使って、テスト対象クラス名の一覧を取得する。
    これは sbt 'show test:defined-test-names' コマンドで確認できた。
     
  4. [テスト並列実行] 手順 3 で取得したクラス名に対して、並列で以下の処理を実行。
    • テストイメージを使って新規コンテナを起動
    • コンテナの中で sbt 'test-only クラス名' を実行 (実行中の標準出力はバッファリング)
    • テストが完了したら、コンテナは停止する
    • コマンドのリターンコードでテスト成功/失敗を判断
    • バッファリングしていた標準出力を全て出力
    • コンテナを削除 

    並列処理の多重度は、Docker ホストマシンの CPU数と同じにする。

  5. [結果表示] 全てのテストが完了したら、結果一覧を表示。
  6. [後処理] テストイメージを削除。

 

動作イメージ

Docker sbt test

 

作ってみた

上記の 2. 〜 6. の処理を行うスクリプトを Python で書いてみた。

docker_sbt_test.py というスクリプトに対して、ベースイメージのリポジトリ名(+タグ名)、テストイメージ内部にある sbt プロジェクトのディレクトリ、ベースイメージに対して行う事前実行コマンドを指定して実行する。

docker_sbt_test.py REPOSITORY[:TAG] [options]

Options:
  -h, --help            show this help message and exit
  -d DIR, --dir=DIR     path to the sbt project directory in the container
  -s SETUP, --setup=SETUP
                        commands to be executed in the shell before testing

より詳しい実行例はこちらを参照。

 

実行結果

手元 (4CPU, 4GB MEM の VirtualBox環境) で実行してみた結果はこちら。

それぞれのテストが並列で実行され、全て成功。sbt test-only は14〜15秒ほどで完了した。

ログ中の starting sbt と finished sbt の間で 30秒ほどかかっているように見えるが、
これは docker コンテナの起動/終了処理と sbt 自体の起動処理にかかる時間が含まれるためと考えられる。
(ちなみに、単体実行時でも sbt の起動には 5秒程度かかっていた。)

VirtualBox 環境でなければもっと速いかもしれない。

  • 抜粋
    $ ./docker_sbt_test.py local/sbt-test --dir /workspace/docker-sbt-test/example --setup " \
    rm -fr /workspace/docker-sbt-test && \
    git clone --depth 1 https://github.com/mogproject/docker-sbt-test.git /workspace/docker-sbt-test \
    "
    2014-01-11 07:24:53,272 [INFO] Creating test image (local/sbt-test:test-1389425093.27) ...
    (略)
    2014-01-11 07:25:13,073 [INFO] Compiling...
    (略)
    2014-01-11 07:25:41,707 [INFO] Getting test class names...
    2014-01-11 07:25:48,859 [INFO] Starting sbt 'test-only com.github.mogproject.dockersbttest.example.RedisConnectionSpec1' ...
    2014-01-11 07:25:48,859 [INFO] Starting sbt 'test-only com.github.mogproject.dockersbttest.example.RedisConnectionSpec2' ...
    2014-01-11 07:25:48,859 [INFO] Starting sbt 'test-only com.github.mogproject.dockersbttest.example.RedisConnectionSpec3' ...
    2014-01-11 07:25:48,859 [INFO] Starting sbt 'test-only com.github.mogproject.dockersbttest.example.RedisConnectionSpec4' ...
    2014-01-11 07:26:13,257 [INFO] Finished sbt 'test-only com.github.mogproject.dockersbttest.example.RedisConnectionSpec3' ...
    Starting redis-server: [  OK  ]
    (略)
    [info] RedisConnectionSpec3
    [info]
    [info] test3 should
    [info] + insert 100 records and delete 1 record
    [info]
    [info]
    [info] Total for specification RedisConnectionSpec3
    [info] Finished in 39 ms
    [info] 1 example, 0 failure, 0 error
    [info]
    [info] Passed: Total 1, Failed 0, Errors 0, Passed 1
    [success] Total time: 15 s, completed Jan 11, 2014 2:26:13 AM
    
    2014-01-11 07:26:13,600 [INFO] Finished sbt 'test-only com.github.mogproject.dockersbttest.example.RedisConnectionSpec4' ...
    (略)
    [info] Passed: Total 1, Failed 0, Errors 0, Passed 1
    [success] Total time: 14 s, completed Jan 11, 2014 2:26:13 AM
    
    2014-01-11 07:26:13,911 [INFO] Finished sbt 'test-only com.github.mogproject.dockersbttest.example.RedisConnectionSpec2' ...
    (略)
    [info] Passed: Total 1, Failed 0, Errors 0, Passed 1
    [success] Total time: 14 s, completed Jan 11, 2014 2:26:13 AM
    
    2014-01-11 07:26:14,035 [INFO] Finished sbt 'test-only com.github.mogproject.dockersbttest.example.RedisConnectionSpec1' ...
    (略)
    [info] Passed: Total 1, Failed 0, Errors 0, Passed 1
    [success] Total time: 14 s, completed Jan 11, 2014 2:26:13 AM
    
    2014-01-11 07:26:14,080 [INFO] *** SUMMARY ***
    2014-01-11 07:26:14,081 [INFO] com.github.mogproject.dockersbttest.example.RedisConnectionSpec1 -> OK
    2014-01-11 07:26:14,081 [INFO] com.github.mogproject.dockersbttest.example.RedisConnectionSpec2 -> OK
    2014-01-11 07:26:14,081 [INFO] com.github.mogproject.dockersbttest.example.RedisConnectionSpec3 -> OK
    2014-01-11 07:26:14,081 [INFO] com.github.mogproject.dockersbttest.example.RedisConnectionSpec4 -> OK
    2014-01-11 07:26:14,081 [INFO] ***************
    2014-01-11 07:26:14,081 [INFO] Removing test image (local/sbt-test:test-1389425093.27) ...
    (略)
    
  • 全文 -> docker-sbt-test-example-result.txt

 

注意事項

こちらに dstat の結果を載せたが、複数のコンテナを同時に起動すると
思ったよりメモリリソースの消費が激しいようだ。

スワップが起こるような状況であれば、もちろん並列化しない時よりもずっと遅くなる。

 

まとめ

  • sbt test を並列化する最善のアプローチは parallelExecution を有効にしても通るテストを書くこと。

    ただ、同時に実行できない多数のテストが存在する場合でも、Docker + 並列化スクリプトで
    高速化できる可能性がある。
     
  • テストが外部環境の状態に依存する場合も、Docker を使えばクリーンな環境を保証できる。

 

 

 

References

1.08.2014

Running shogi-server in a Docker Container

shogi-server を Docker コンテナで動かす

 

コンピュータ将棋協会規定のTCP/IPプロトコルに従った将棋のネット対局サーバ shogi-server
Docker コンテナ上で動かしてみたのでメモ。

事前準備

  • Docker ホスト環境の構築
  • 適当なプロトコル対応クライアントソフトの入手
    • こちら から適当なものをインストール。
      Mac 環境であれば SFICP (Shogi Framework Implements CSA Protocol) がおすすめ。
    • telnet コマンドでも一通りの動作確認は可能。

 

遊び方

手順は全て Docker リポジトリに書いておいた。

基本的には、Docker ホスト上で以下のコマンドを叩けばコンテナが起動する。

$ docker pull mogproject/shogi-server
$ docker run -d -p 4081:4081 mogproject/shogi-server

forwarding するポートを指定して (-p 4081:4081)、 Docker ホストの 4081 番ポートがそのまま
Docker コンテナの 4081 番ポートに繋がるようにしているが、ここはお好みで。

その後は将棋ソフト(クライアント)から Docker ホストに接続するか、telnet で 4081 番ポートに接続すれば
shogi-server が応答を返すはずだ。

 

Dockerfile

# Dockerfile for shogi-server
#
#

# use the centos image provided by dotCloud
FROM centos:6.4

# install packages
RUN yum install -y ruby
RUN yum install -y git

# download shogi-server module
RUN git clone git://git.sourceforge.jp/gitroot/shogi-server/shogi-server.git /opt/shogi-server

# create shogi user and change owner of the directory.
RUN useradd shogi
RUN chown -R shogi /opt/shogi-server

# launch shogi-server when launching the container
ENTRYPOINT ["ruby", "/opt/shogi-server/shogi-server", "event1", "4081"]

# run shogi-server as shogi user
USER shogi

# expose shogi-server port
EXPOSE 4081

ruby のバージョン指定もしていないし、shogi-server のログ出力も十分でない、超手抜き実装。

ベースは dotCloud 公式の CentOS 6.4 イメージ。

モジュールは /opt/shogi-server 配下に置かれ、shogi という OS ユーザで、event1 というイベント名でプロセスが立ち上がる。

 

自分でビルドを行う場合は Dockerfile のあるディレクトリに移動し、

$ docker build -t ユーザ名/イメージ名 .
$ docker run -d -p 4081:4081 ユーザ名/イメージ名

と実行すればよい。

 

起動中の shogi-server の出力を見たい場合は、Docker ホストサーバ上で

$ docker ps

を叩いてコンテナID (CONTAINER ID/CID) を確認し、

$ docker logs コンテナID

で OK。

 

 

 

Related Posts

1.06.2014

Vagrant: Creating CentOS 6.5 Docker Host

Vagrant: CentOS 6.5 Docker 母艦の建造

 

Vagrant 1.4 で Docker へのプロビジョニング対応 (Vagrant 1.4 - Vagrant) が行われたことで
ローカルマシン(VirtualBox)上での Docker ホストOS の構築はより一層簡単になった。

尚、Ubuntu ホストであればこちらの手順通り、公式リポジトリをそのまま使えばよい。

 

事前準備

 

環境
  • ローカルマシン: Mac OS X 10.9.1
    • Vagrant: 1.4.2 (確認は「vagrant -v」)
    • VirtualBox: 4.3.6
  • Docker ホスト (母艦): CentOS 6.5
    • Docker: 0.7.2 (確認は「docker -v」)
  • Docker コンテナ: CentOS 6.4

 

母艦の立ち上げ

基本的には適当なディレクトリに Vagrantfile を作成して、「vagrant up」するだけ。

Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "opscode-centos65"
  config.vm.box_url = "http://opscode-vm-bento.s3.amazonaws.com/vagrant/virtualbox/opscode_centos-6.5_chef-provisionerless.box"

  config.vm.provider :virtualbox do |vb|
    vb.name = "docker1"
    vb.customize ["modifyvm", :id, "--memory", 1024]
  end

  config.vm.network :private_network, ip: "192.168.33.10"

  config.vm.provision :docker do |d|
    d.pull_images "centos:6.4"
  end
end
  • 8行目: OpsCode 提供の CentOS 6.5 box をダウンロードして利用
  • 11-12行目: VirtualBox の VM に docker1 という名前を付け、メモリを少し増やす
  • 15行目: Docker コンテナへ直接アクセスしたいので、静的アドレスを割り振った方が便利
    (ネットワークアドレスは VirtualBox の仮想ネットワークアダプタの設定に依存。
    デフォルトでは host-only ネットワークが 192.168.33.0/24 として構築されている)
  • 17-19行目: Docker のプロビジョニング設定。ここでは centos:6.4 のイメージを pull している。
    これを記述することで、Docker 自体のインストール、サービスへの登録が自動的に行われる。

 

仮想マシンの作成

単に「vagrant up」でもよいが、ゴミの消去も含めたシェルスクリプトを書いて実行した。

#!/bin/bash


### Clean.
vagrant destroy --force

### Create VirtualBox image and boot.
vagrant up

### Print status.
vagrant status

実行結果は以下のとおり。プロビジョニングの工程で Docker のインストールが行われている。

[default] VM not created. Moving on...
Bringing machine 'default' up with 'virtualbox' provider...
[default] Importing base box 'opscode-centos65'...
[default] Matching MAC address for NAT networking...
[default] Setting the name of the VM...
[default] Clearing any previously set forwarded ports...
[default] Clearing any previously set network interfaces...
[default] Preparing network interfaces based on configuration...
[default] Forwarding ports...
[default] -- 22 => 2222 (adapter 1)
[default] Running 'pre-boot' VM customizations...
[default] Booting VM...
[default] Waiting for machine to boot. This may take a few minutes...
[default] Machine booted and ready!
[default] Configuring and enabling network interfaces...
[default] Mounting shared folders...
[default] -- /vagrant
[default] Running provisioner: docker...
[default] Installing Docker (latest) onto machine...
[default] Pulling Docker images...
[default] -- Image: centos:6.4
Current machine states:

default                   running (virtualbox)

The VM is running. To stop this VM, you can run `vagrant halt` to
shut it down forcefully, or you can run `vagrant suspend` to simply
suspend the virtual machine. In either case, to restart it again,
simply run `vagrant up`.

ちなみに、vagrant-vbguest プラグインがインストールされている場合には以下のようなビルドエラーが発生した。

Building the OpenGL support module[FAILED]
(Look at /var/log/vboxadd-install.log to find out what went wrong)
Doing non-kernel setup of the Guest Additions[  OK  ]
Installing the Window System drivers[FAILED]
(Could not find the X.Org or XFree86 Window System.)
An error occurred during installation of VirtualBox Guest Additions 4.3.6. Some functionality may not work as intended.
In most cases it is OK that the "Window System drivers" installation failed.

X Window はインストールされていないので無視してよさそうだが、OpenGL のほうは少し気になる。

 

母艦への接続

「vagrant ssh」で母艦に接続した後、docker コンテナを立ち上げて
/etc/redhat-release ファイルの中身を参照してみた。

$ vagrant ssh
Last login: Thu Dec  5 05:57:05 2013 from 10.0.2.2
[vagrant@localhost ~]$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
centos              6.4                 539c0211cd76        9 months ago        300.6 MB
[vagrant@localhost ~]$ sudo docker run centos:6.4 /bin/cat /etc/redhat-release
CentOS release 6.4 (Final)

Cent OS 6.4 のコンテナが起動したことが確認できた。

 

 

 

References

 

Related Posts

1.05.2014

Getting Started with Erlang pt.6

Erlang をはじめよう その6

 

前回 - mog project: Getting Started with Erlang pt.5 の続き

CHAPTER 11: Getting Started with OTP

いよいよ OTP の章に入る。

OTPとは Open Telecom Platform の略。
分散並列処理のためのライブラリ群と耐障害性の高いアプリケーションサーバ機能を持った
フレームワークの名称である。

Open Telecom Platform - Wikipedia, the free encyclopedia

この OTP の機能を活用することこそ、Erlang を使う最大の目的である。

 

モジュールをサービスとして実行する

-define はマクロの定義、?MODULE は組み込みマクロ。

-module(shop).
-behaviour(gen_server).
-export([start_link/0]). % convenience call for startup
-export([init/1,
         handle_call/3,
         handle_cast/2,
         handle_info/2,
         terminate/2,
         code_change/3]). % gen_server calls
-define(SERVER, ?MODULE). % macro that just defines this module as server
-record(state, {count}). % simple counter state

%%% convenience method for startup
start_link() ->
        gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%%% gen_server callbacks
init([]) ->
        {ok, #state{count=0}}.

handle_call(_Request, _From, State) ->
        Distance = _Request,
        Reply = {ok, buy(Distance)},
        NewState=#state{ count = State#state.count+1 },
        {reply, Reply, NewState}.

handle_cast(_Msg, State) ->
        io:format("So far, calculated ~w prices.~n", [State#state.count]),
        {noreply, State}.

handle_info(_Info, State) ->
        {noreply, State}.

terminate(_Reason, _State) ->
        ok.

code_change(_OldVsn, State, _Extra) ->
        {ok, State}.

%%% Internal functions

buy(Number) -> 100 * Number.

実行

> c(shop)
> shop:start_link().
> gen_server:call(shop, 3).
> gen_server:call(shop, 5).
> gen_server:cast(shop, {}).
> gen_server:call(shop, 7).
> gen_server:cast(shop, {}).

サーバ実行中にモジュールを書き換えることもできる。
例えば、shop.erl の最終行を以下のように書き換える。

buy(Number) -> 105 * Number.

先ほどのコンソールで引き続き。

> c(shop).
> gen_server:call(shop, 7).
> gen_server:cast(shop, {}).

ただし、処理中にエラーが発生するとサーバは停止してしまう。

> gen_server:call(shop, apple).
> gen_server:call(shop, 7).

 

スーパーバイザーの実行
-module(shop_sup).
-behaviour(supervisor).
-export([start_link/0]). % convenience call for startup
-export([init/1]). % supervisor calls
-define(SERVER, ?MODULE). % macro that just defines this module as server


%%% convenience method for startup
start_link() ->
        supervisor:start_link({local, ?SERVER}, ?MODULE, []).

%%% supervisor callback
init([]) ->
        RestartStrategy = one_for_one,
        MaxRestarts = 1, % one restart every
        MaxSecondsBetweenRestarts = 5, % five seconds

        SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts},

        Restart = permanent, % or temporary, or transient
        Shutdown = 2000, % milliseconds, could be infinity or brutal_kill
        Type = worker, % could also be supervisor

        Shop = {shop, {shop, start_link, []},
                          Restart, Shutdown, Type, [shop]},

        {ok, {SupFlags, [Shop]}}.


%%% Internal functions (none here)

実行

> c(shop_sup).
> {ok, Pid} = shop_sup:start_link().
> unlink(Pid).
> gen_server:call(shop, 3).
> whereis(shop).
> gen_server:call(shop, apple).    % error
> whereis(shop).
> gen_server:call(shop, 5).

start_link を行うとシェル自身がスーパーバイザーになってしまうので、
サーバを起動し続けるためには unlink を行う必要がある。

別の方法としては、gen_server:call の呼び出しを catch で囲むアプローチもある。

 

アプリケーションとしてパッケージングする

アプリケーション リソースファイル

{application,shop,
[{description,"Shopping some fruits"},
{vsn,"0.0.1"},
{modules,[shop, shop_sup]},
{applications,[kernel,stdlib]},
{mod,{shop_app,[]}}]}.

アプリケーション モジュール

-module(shop_app).
-behaviour(application).
-export([start/2, stop/1]).

start(_Type, _StartArgs) ->
  shop_sup:start_link().

stop(_State) ->
  ok.

実行

> c(shop_app).
> application:load(shop).
> application:loaded_applications().
> application:start(shop, 3).
> application:start(shop).
> gen_server:call(shop, 3).
> whereis(shop).
> gen_server:call(shop, apple).
> whereis(shop).
> gen_server:call(shop, 5).

カレントディレクトリ以外の場所にリソースがある場合には
code:add_path("path/to/the/directory").
が必要。

 

CHAPTER 12: Next Steps Through Erlang

最終章。次のステップは?

 

 

 

References

 

Related Posts

1.04.2014

Getting Started with Erlang pt.5

Erlang をはじめよう その5

 

前回 - mog project: Getting Started with Erlang pt.4 の続き

CHAPTER 9: Exceptions, Errors, and Debugging

 

try .. catch の基本形
> try math:sqrt(-1) of
>   Result -> Result
> catch
>   error: Error -> {error, Error}
> end.
> try math:sqrt(2)
> catch
>   error: Error -> {error, Error}
> end.  % ok
> try math:sqrt(-2)
> catch
>   error: Error -> {error, Error}
> end.
> try
>   X = -2,
>   math:sqrt(X)
> of
>   Result -> Result
> catch
>   error: Error -> {error, Error}
> end.

 

after 節の指定
> F = fun(X) -> try math:sqrt(X)
> catch
>   error: Error -> {error, Error}
> after
>   io:format("AFTER CODE~n")
> end
> end.
> F(2).
> F(-2).

 

例外の送出

エラーと例外を区別することができる。

> throw(my_exception).
> try throw(my_exception)
> catch
>   error: Error -> {found, Error};
>   throw: Exception -> {caught, Exception}
> end.

 

メッセージのロギング
> error_logger:info_msg("information~n").
> error_logger:warning_msg("warning~n").
> error_logger:error_msg("error~n").
> error_logger:info_msg("~p~n", []).
> error_logger:info_report("~p~n", []).    % フォーマットエラーの場合の挙動が異なる

 

ログファイルへの書き込み

カレントディレクトリ配下に test.log というファイルを作成。

> error_logger:logfile({open, "test.log"}).
> error_logger:info_msg("information").
> error_logger:logfile(close).

 

GUI でのデバッグ

debug_info オプションを付けてコンパイルする必要がある。

> c(shop, [debug_info]).
> debugger:start().
  • [GUI操作] Module -> Interrupt... -> デバッグ対象のモジュールを Choose
> Pid1 = spawn(async_shop,async_shop,[]).
  • [GUI操作] Break -> Line Break でブレイクポイントを設置。
> Pid1 ! {apple, 3}.

ステップ実行や、変数の値を確認できた。

 

コンソールでのデバッグ
  • 送受信メッセージのトレース
    > dbg:tracer().
    > Pid1 = spawn(async_shop,async_shop,[]).
    > dbg:p(Pid1,m).
    > Pid1 ! {apple, 3}.
  • 関数呼び出しのトレース
    -module(fact).
    -export([factorial/1]).
    
    factorial(N) -> factorial(1, N, 1).
    
    factorial(Current, N, Result) when Current =< N -> factorial(Current + 1, N, Result * Current);
    factorial(Current, N, Result) -> Result.
    > c(fact).
    > dbg:tracer().
    > dbg:p(all, c).
    > dbg:tpl(fact, factorial, []).
    > fact:factorial(4).

 

CHAPTER10: Storing Structured Data

 

レコード

レコードとは、固定長のデータ構造であり、名前でアクセスすることができるフィールドからなる。
Cの構造体のようなもの。

Erlang におけるレコードはコンパイル時の機能であって、VMに固有の型があるわけではない。

  • レコードの基本操作

    複数のモジュールで共有できるように、レコード定義は個別のファイル(拡張子hrl)に記述するのがよいらしい。

    -record(person, {name, age=20, phone}).
    
    > rr("records.hrl").
    > Person1=#person{}.
    > Person2=#person{name="Alice", age=16}.
    > Person3=#person{name="Bob", phone="123-4567", age=35}.
    > #person{phone=P, name=N} = Person3.
    > {P, N}.
    > Person3 = Person3#person{name="Charlie"}.    % error
    > Person4 = Person3#person{name="Charlie"}.
    > rf().
    > #person{}.
    > Person1.
  • モジュール内でレコードを利用する
    -module(person).
    -export([rename/2]).
    -include("records.hrl").
    
    rename(#person{name=Name} = Person, NewName) ->
      io:format("Changed name: ~s -> ~s~n", [Name, NewName]),
      Person#person{name=NewName}.
    > c(person).
    > rr("records.hrl").
    > P = #person{name="Alice", age=16, phone="123-4567"}.
    > person:rename(P, "Bob").

 

Erlang Term Storage (ETS)

Erlang 付属の KVS (key/value store)。

  • テーブルの作成
    -module(users).
    -export([setup/0]).
    -include("records.hrl").
    
    setup() ->
      Table = ets:new(users, [named_table, {keypos, #person.name}]),
      ets:insert(users, #person{ name="Alice", age=16, phone="123-4567"}),
      ets:insert(users, #person{ name="Bob", age=35, phone="000-0000"}),
      ets:insert(users, #person{ name="Charlie", age=66, phone="111-1111"}),
      ets:info(Table).
    > c(users).
    > users:setup().    % size を確認
    > rr("records.hrl").
    > ets:tab2list(users).
    > tv:start().    % GUI が起動
    > ets:i().

    このような GUI でレコードの内容を確認できる。

    TV ETS users Node nonode nohost
  • レコードの読み込みと更新
    > users:setup().
    > rr("records.hrl").
    > ets:lookup(users, "Alice").
    > ets:lookup(users, "Carol").
    > Alice = hd(ets:lookup(users, "Alice")).
    > ets:insert(users, Alice#person{age=17}).
    > ets:lookup(users, "Alice").

 

Mnesia

分散、並列機能を完全に有した Erlang 付属のDBMS。発音は「エムニージア」でよさそうだ。

  • スキーマ、テーブルの作成
    -module(users).
    -export([setup/0]).
    -include("records.hrl").
    
    setup() ->
      mnesia:create_schema([node()]),
      mnesia:start(),
      mnesia:create_table(person, [{attributes, record_info(fields, person)}]),
    
      F = fun() ->
        mnesia:write(#person{ name="Alice", age=16, phone="123-4567"}),
        mnesia:write(#person{ name="Bob", age=35, phone="000-0000"}),
        mnesia:write(#person{ name="Charlie", age=66, phone="111-1111"})
      end,
    
      mnesia:transaction(F).
    > c(users).
    > rr("records.hrl").
    > users:setup().
    > mnesia:table_info(person, all).
    > tv:start().
  • クエリの実行
    > mnesia:transaction(fun() -> mnesia:read(person, "Alice") end).
    > mnesia:transaction(fun() -> qlc:e(qlc:q( [X || X <- mnesia:table(person)])) end).
    > mnesia:transaction(fun() -> qlc:e(qlc:q(
        [X || X <- mnesia:table(person), X#person.age < 40]
      )) end).
    > mnesia:transaction(fun() -> qlc:e(qlc:q(
        [ {X#person.name, X#person.age} ||
          X <- mnesia:table(person), X#person.age < 40]
      )) end).
    

 

 

 

References

 

Related Posts

1.03.2014

Getting Started with Erlang pt.4

Erlang をはじめよう その4

 

前回 - mog project: Getting Started with Erlang pt.3 の続き

CHAPTER 8: Playing with Processes

プロセスこそが Erlang のキー・コンセプトである。

プロセスIDの確認とメッセージの送受信
> self().
> self() ! test1.
> Pid = self().
> Pid ! test2.
> flush().
> flush().
> self() ! test1.
> receive X -> X end.
> self() ! 23.
> receive Y -> 2 * Y end.

 

モジュールからプロセスを生成する

このモジュールの場合、一度メッセージを受け取ったらプロセスは即座に終了する。

-module(bounce).
-export([report/0]).

report() ->
  receive
    X -> io:format("Received ~p~n", [X])
  end.

> c(bounce).
> Pid = spawn(bounce, report, []).
> Pid ! 23.
> Pid ! 45.

 

再帰を利用すれば、永久的にメッセージを受信できるようになる。

-module(bounce).
-export([report/0]).

report() ->
  receive
    X -> io:format("Received ~p~n", [X]),
    report()
  end.
> c(bounce).
> Pid = spawn(bounce, report, []).
> Pid ! 23.
> Pid ! message.

 

再帰のパラメータを変えることで、シンプルなカウンターを作れる。

-module(bounce).
-export([report/1]).

report(Count) ->
  receive
    X -> io:format("Received #~p: ~p~n", [Count, X]),
    report(Count + 1)
  end.
> c(bounce).
> Pid = spawn(bounce, report, [1]).
> Pid ! test.
> Pid ! 123.
> Pid ! message.

 

以下のように、receive の戻り値を利用してもよい。

-module(bounce).
-export([report/1]).

report(Count) ->
  NewCount = receive
    X -> io:format("Received #~p: ~p~n", [Count, X]),
    Count + 1
  end,
  report(NewCount).

 

プロセスの登録

引き続き同じ bounce.erl を使用。

> Pid1 = spawn(bounce, report, [1]).
> register(bounce, Pid1).
> regs().
> bounce ! hello.
> bounce ! 123.
> bounce2 ! test.    % error
> GetBounce = whereis(bounce).
> unregister(bounce).
> regs().
> whereis(bounce).
> GetBounce ! "Still there?".

 

プロセスが異常終了するとき
-module(fragile).
-export([report/0]).

report() ->
  receive
    X -> io:format("Divided to ~p~n", [X/2]),
    report()
  end.
> c(fragile).
> Pid = spawn(fragile, report, []).
> Pid ! 38.
> Pid ! 0.
> Pid ! one.
> Pid ! 10.

既に終了してしまったプロセスに対してメッセージを送っても、何も起こらない。

 

メッセージのコールバック
-module(shop).
-export([buy/0]).

buy() ->
  receive
    {From, Item, Number} ->
      From ! {Item, Number, price(Item, Number)},
      buy()
  end.

price(apple, Num)  when Num >= 0 -> 100 * Num;
price(banana, Num) when Num >= 0 -> 200 * Num;
price(_, Num)      when Num >= 0 -> 500 * Num.
price(_, _)                      -> 0.
> c(shop).
> P = spawn(shop, buy, []).
> P ! {self(), apple, 10}.
> P ! {self(), banana, 20}.
> flush().

 

関数の中でプロセスを生成する
-module(async_shop).
-export([async_shop/0]).

async_shop() ->
  Shop = spawn(shop, buy, []),
  buy(Shop).

buy(Shop) ->
  receive
    {Item, Number} ->
      Shop ! {self(), Item, Number},
      buy(Shop);
    {Item, Number, Price} ->
      io:format("You bought ~p ~p(s) for ~p Yen.~n", [Number, Item, Price]),
      buy(Shop)
  end.
> c(async_shop).
> P = spawn(async_shop, async_shop, []).
> P ! {apple, 10}.
> P ! {banana, 20}.

 

プロセスの状態を確認する
> pman:start().

GUI が起動する。特定のプロセスをダブルクリックすると、ヒープサイズなどの詳細な情報を得られる。
そのプロセスでメッセージの送受信が行われれば、その内容も表示される。

Pman Process 0 41 0 on nonode nohost

 

プロセスどうしのリンク
  • リンクしない場合
    > pman:start().
    > P = spawn(async_shop, async_shop, []).
    > P ! {apple, one}.

    子プロセス(shop)がエラーで終了した後も、親プロセス(async_shop)が残り続ける。

  • リンクした場合
    -module(async_shop).
    -export([async_shop/0]).
    
    async_shop() ->
      Shop = spawn_link(shop, buy, []),
      buy(Shop).
    
    buy(Shop) ->
      receive
        {Item, Number} ->
          Shop ! {self(), Item, Number},
          buy(Shop);
        {Item, Number, Price} ->
          io:format("You bought ~p ~p(s) for ~p Yen.~n", [Number, Item, Price]),
          buy(Shop)
      end.
    > c(async_shop).
    > pman:start().
    > P = spawn(async_shop, async_shop, []).
    > P ! {apple, one}.

    子プロセス(shop)がエラーで終了したら、親プロセス(async_shop)も終了する。 (リンクは常に双方向)

 

エラートラップと新しいプロセスの生成
-module(async_shop).
-export([async_shop/0]).

async_shop() ->
  process_flag(trap_exit, true),
  Shop = spawn_link(shop, buy, []),
  buy(Shop).

buy(Shop) ->
  receive
    {Item, Number} ->
      Shop ! {self(), Item, Number},
      buy(Shop);
    {'EXIT', Pid, Reason} ->
      io:format("FAILURE: ~p died because of ~p.~n", [Pid, Reason]),
      NewShop = spawn_link(shop, buy, []),
      buy(NewShop);
    {Item, Number, Price} ->
      io:format("You bought ~p ~p(s) for ~p Yen.~n", [Number, Item, Price]),
      buy(Shop)
  end.
> c(async_shop).
> pman:start().
> P = spawn(async_shop, async_shop, []).
> P ! {apple, one}.
> P ! {apple, 10}.
> P ! {banana, two}.

エラーをトラップしてメッセージが出力された後、即座に新しいプロセスが立ち上がる。
GUI では、エラーが発生するたびに shop:buy/0 の実行プロセスIDが新しくなっていくのを確認できる。

 

 

 

References

 

Related Posts

Getting Started with Erlang pt.3

Erlang をはじめよう その3

 

前回 - mog project: Getting Started with Erlang pt.2 の続き

CHAPTER 5: Communicating with Humans

 

文字列リテラル

Unicode も標準で扱うことができる。

> io:format("h\"e\'l'l\\o\s\127\x4f\trld\n").
> io:format("~p ~p ~p ~w ~s ~c ~tc ~i ~n", [100, true, "yes", "yes", "yes", 88, 16#3042, 200]).
> $1.
> $A.
> $ .
> $あ.
文字列の操作

各種標準関数など。

> "erl" ++ "ang" == "erlang"
> string:concat("erl", "ang") =:= "erlang".
> hd("hello").
> hd('hello').    % error (this is an atom)
> length("hello").
> lists:nth(2, "hello").
> io:format("~c~n", [lists:nth(2, "hello")]).
> string:chr("hello", $l).
> string:str("hello", "lo").
> string:substr("hello", 3).
> string:substr("hello", 3, 2).
> string:sub_string("hello", 3, 4).
> string:tokens("this is a token.", " .").
> string:join(["one", "two", "three"], ",").
> string:words("this is a word.").
> string:chars($*, 10).
> string:copies("* ", 10).
> string:strip(" x  \n").
> string:strip(" x  ").
> string:left("x", 10).
> string:right("x", 10).
> string:centre("x", 10).
> lists:reverse("erlang").
> string:to_float("1.1 2 3").
> string:to_float("1 2 3").
> string:to_integer("1 2 3").
> string:to_integer("a1 2 3").
> string:to_lower("Hello World!").
> string:to_upper("Hello World!").
> integer_to_list(123).
> float_to_list(123.45).
> erlang:fun_to_list(fun(X) -> X * 2 end).
> list_to_atom("Hello").
ユーザ入力の読み取り
> io:read(">>> ").
>>> [1,2,3].
> io:read(">>> ").
>>> True.    % error
> io:get_chars(">>> ").
>>> 1
> io:get_line(">>> ").
>>> 1 2 3

 

CHAPTER 6: Lists

 

基本的な操作
> [1,X,4,Y] = [1,2,4,8].
> {X, Y}.
> lists:flatten([1,[2,4,8],16]).
> [1,2,4] ++ [8,16].
> lists:append([1,2,4], [8,16]).
> lists:append([[1,2,4], [8,16], [32,64]]).
> lists:seq(1, 10).
> lists:seq($A, $Z).
head と tail の操作
> [H1 | T1] = [1, 2, 4].
> {H1, T1}.
> [H2 | T2] = [1].
> {H2, T2}.
> [H3 | T3] = [].
> F = (fun ([], _) -> 1; ([Head|Tail], Fun) -> Head * Fun(Tail, Fun) end).
> F([1,2,4,16], F).
> [1|[2,3]].
> [1,2|[3]].
> [1,2|3].
> [[1,2]|[3]].
zip と key-value 操作
> T = lists:zip([1,2,3,4,5], [a,b,c,d,e]).
> lists:unzip(T).
> lists:keystore(7,1,[{1,tiger}, {3,bear}],{7,panther}).
> lists:keyreplace(7,1,[{1,tiger}, {3,bear}],{7,panther}).
> lists:keyfind(3,1,[{1,tiger}, {3,bear}, {7,panther}]).
> lists:keyfind(4,1,[{1,tiger}, {3,bear}, {7,panther}]).

 

CHAPTER 7:
Higher-Order Functions and List Comprehensions

 

単純な高階関数
> Tripler = fun (Value, Function) -> 3 * Function(Value) end.
> Tripler(6, fun(X)->20*X end).
> X=20.
> F=fun(Value)->X * Value end.
> f(X).
> X.    % 'X' is unbound
> Tripler(6, F).
> Tripler(math:pi(), fun math:cos/1).
高階関数を使ってリストを操作する
> Print = fun(Value) -> io:format("  ~p~n", [Value]) end.
> List = [1,1,2,3,5,8,13].
> lists:foreach(Print, List).
> lists:map(fun(Value) -> Value * Value end, List).
> [Value * Value || Value  lists:filter(fun(Value) -> (Value >= 3) and (Value rem 2 == 1) end, List).
> lists:filter(fun(Value) -> (Value >= 3) and (Value rem 2 == 1) end, List).
> [Value || Value = 3, Value rem 2 == 1].
> [Value || Value  lists:all(fun(Value) -> Value > 0 end, List).
> lists:any(fun(Value) -> Value < 0 end, List).
> lists:partition(fun(Value) -> (Value >= 3) and (Value rem 2 == 1) end, List).
> lists:dropwhile(fun(Value) -> Value =< 3 end, List).
> lists:takewhile(fun(Value) -> Value =< 3 end, List).
> lists:foldl(fun(Value, Accumulator) -> Value - Accumulator end, 0, [1,2,3,4]).
> 4-(3-(2-(1-0))).
> lists:foldr(fun(Value, Accumulator) -> Value - Accumulator end, 0, [1,2,3,4]).
> 1-(2-(3-(4-0))).

 

 

 

References

 

Related Posts