Quantcast
Browsing Latest Articles All 25 Live
Mark channel Not-Safe-For-Work? (0 votes)
Are you the publisher? or about this channel.
No ratings yet.
Articles:

ZEAM開発ログ2018年総集編その1: Elixir 研究構想についてふりかえる(前編)

(この記事は「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」の1日目です)

ZACKY こと山崎進です。

2018年ももう12月です。ついにアドベントカレンダーの季節がやってまいりました。「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」のトップバッターということで,Elixir研究構想について,ふりかえりたいと思います。

なお,「技術的ポエム Advent Calendar 2018」にて「ZEAM開発ログ2018ふりかえり第1巻(黎明編): 2017年秋の出会いから2018年2月にElixirを始めるに至った経緯について」というタイトルで,fukuoka.ex 代表の @piacere_ex さんとの出会いからElixir研究に突入するまでの経緯について詳細に解説しています。もしよろしければ合わせてご覧になっていただければ幸いです。

Elixir 研究構想全体像

私が2018年4月の時点で思い描いた Elixir 研究構想の全体像を下図に示します。

Elixir-proposal-2018.png

この研究構想の中で,2018年中に着手した研究テーマは次の通りです。

  • Hastega(ヘイスガ): 並列コンピューティングドライバのうち,マルチコアCPUとGPUに該当
  • micro Elixir / ZEAM: コード生成/実行基盤に該当
  • データ分析基盤のAI/ML/各種数学については,Hastegaと統合してFAISにて研究申請し採択。目下,研究開発中。
  • Sabotendar(サボテンダー): 並行プログラミング機構に該当

以下,2018年中に得られた各プロジェクトの成果をふりかえります。前編の今回は,Hastega と micro Elixir / ZEAM についてふりかえります。

Hastega: SIMD マルチコアCPU / GPU 駆動処理系

Hastega(ヘイスガ)は当初 Elixir から GPU を駆動して超並列処理をするライブラリ/処理系として研究がスタートしました。Hastega の名称はファイナルファンタジーに登場する最強のスピードアップ呪文に由来します。ちなみに Elixir や Phoenix もファイナルファンタジー由来の名称です。この研究プロジェクトが目標とするマルチコア CPU / GPU をフル活用して高速化する技術として Hastega は最もふさわしい名称ではないでしょうか。

Hastega は,次のような Elixir の MapReduce スタイルのコード

1..1_000_000
|> Enum.map(foo)
|> Enum.map(bar)

は,次のような関数 f

def f(a) do
  a |> foo |> bar
end

を処理するコードを 1,000,000 並列に実行するのと等価であることから着想しています。

整数演算ベンチマークについて,Elixir から,SIMD 命令を用いたマルチコアCPU駆動のネイティブコードおよび OpenCL による GPU 駆動のネイティブコードを呼び出す Hastega プロトタイプを開発しました。8月にプログラミング研究会とSWESTにて発表しました。当時得られた結果では Elixir からの速度向上は約4〜8倍,Pythonからの速度向上は3倍以上となりました。発表資料(論文,プレゼンテーション,ポスター)を下記に示します。

Hastega: Elixirプログラミングにおける超並列化を実現するためのGPGPU活用手法

Hastega: Elixirプログラミングにおける超並列化を実現するためのGPGPU活用手法

Hastega: Elixirプログラミングにおける超並列化を実現するためのGPGPU活用手法

その後,複数の研究助成を受けて数々のマシンでテストする機会が得られたり,研究室学生が研究に合流してくれたりして,研究が進みました。

GPUでの並列処理そのものは非常に高速であるものの,CPUとGPUの間のプログラムコードやデータの転送がボトルネックになることが明らかになりました。したがって,GPUの高速性を生かすには,少ないデータ転送で計算負荷の高い処理を選ぶことと,転送のスケジューリングを最適化することが求められます。

マルチコアCPUによる並列処理も,並列化するまでのデータを分配する部分や,並列処理した後の結果を集計する部分に同期処理が必要で,これらの部分にかかる時間を短縮しないと並列化の効果が出ません。このことはアムダール(Amdahl)の法則として知られています。数々の実験をしたことで,CPUバウンドな処理の場合,処理するデータ量が相当多くないとマルチコアCPUで並列化するのがペイしないことが明らかになりました。一方,SIMD命令を用いた並列処理については,このようなペナルティが少なくなる見込みであることも明らかになりました。I/Oバウンドな処理の場合については,おそらくマルチコアCPUによる並列化の前に,並行プログラミング機構と非同期I/Oの整備に取り組んだ方が効果が高いと考えられます。

以上の知見から,現在研究開発を進めている Elixir コードから並列処理するネイティブコードを生成する Hastega / micro Elixir / ZEAM 処理系では,SIMD命令による並列化に優先して取り組むのが最善だろうと考えています。

micro Elixir / ZEAM:

ZEAM(ジーム) は ZACKY's Elixir Abstract Machine の略です。Erlang VM の BEAM (Bogdan/Björn's Erlang Abstract Machine)に対応するような形で命名しました。ZEAM という名称の初出は2018年2月の「fukuoka.ex #5」です。

fukuoka.ex ZEAM開発ログ 第1回: BEAMバイトコード・インサイド〜30年の歴史を誇るBEAMを超えるには

ZEAM はその名の通り,Erlang VM に代わる Elixir ネイティブな処理系として構想されました。当初構想では BEAM バイトコードと互換性を持たせるつもりでいたのですが,BEAMバイトコードの解析に難儀したことと,その後の議論で,バイトコードレベルの互換性は不要で,Elixir のソースコードレベルの互換性があれば良いという結論に至り,当初構想から大きく方向転換することとなりました。

現在の構想では,Elixir のサブセットとなるプログラミング言語を策定し,その言語をコンパイル・実行する処理系として研究開発を始動しています。このサブセット言語を micro Elixir と呼んでいます。

micro Elixir / ZEAM 構想の初出はfukuoka.ex#13:夏のfukuoka.ex祭=技術のパラダイムシフトおよびSWEST20です。下記のプレゼンテーションの後半で示されるように,かなり野心的な構想になっています。

耐障害性が高くマルチコア性能を最大限発揮できるElixir(エリクサー)を学んでみよう

micro Elixir の全ての仕様はまだ確定していませんが,まずは Elixir のデータ処理の部分を抜き出して Hastega のコードを生成するという部分に集中することにしました。また,当面は NIFコードを生成することとし,Elixir / Erlang VM から呼び出すようにすることにしました。これを Hastega / micro Elixir / ZEAM と呼んでいます。このようにデザインすることで,すぐに既存のElixirのコードに組込むことが可能になります。

実装には Elixir マクロと LLVM を用いました。この辺りについては,明日2018年12月2日に公開予定の「言語実装 Advent Calendar 2018」2日目の記事「ZEAM開発ログ: Elixir マクロ + LLVM で超並列プログラミング処理系を研究開発中」に詳しく書きますので,お楽しみに。

前述の通り,Hastegaの研究によって得られた知見をもとに,基本的な算術演算をコンパイルできるようにした後は,リストと Enum.map による次のような計算を SIMD 命令による並列化をしたループとしてコード生成することに集中する方針を立てました。

1..1_000_000
|> Enum.map(foo)
|> Enum.map(bar)

おわりに

当初別個に始まった Hastega と micro Elixir / ZEAM ですが,現在は合流して Hastega / micro Elixir / ZEAM として リストと Enum.map を用いた Elixir コードから SIMD 命令による並列化をしたループのネイティブコードを生成する処理系を研究開発しています。この詳細については,明日2018年12月2日に公開予定の「言語実装 Advent Calendar 2018」2日目の記事「ZEAM開発ログ: Elixir マクロ + LLVM で超並列プログラミング処理系を研究開発中」に書きます。お楽しみに!

2018年中に着手した残りの研究構想は次のように紹介する予定です。お楽しみに!

もしよかったら他の「ZEAM開発ログ」もお読みください。

明日の「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」2日目の記事は, @koyo-miyamura さんの「【Phoenix】パスワード認証&リレーションあり「ブログチュートリアル」」です。こちらもお楽しみに!

【Phoenix1.4】(前編)パスワード認証&リレーションあり「ブログチュートリアル」

(この記事は、fukuoka.ex Elixir/Phoenix Advent Calendar 2018の2日目です)
昨日はzacky1972さんの「ZEAM開発ログ2018年総集編その1: Elixir 研究構想についてふりかえる(前編)」です!こちらもぜひぜひ!

学生エンジニアKoyoです!
紅葉の季節ですね~

今までCakePHP, RailsからGoやVueまで色んな言語(フレームワーク)を触ってきました
フルスタックのフレームワークには大体「ブログチュートリアル」がありますよね

ElixirのフレームワークであるPhoenixは、公式ドキュメントにはいくつかチュートリアルはあるのですが、日本語の情報が少ないです><
(結構苦労して笑)先日パスワード認証&リレーションありの「ブログチュートリアル」を自分でやってみたので、その過程をシェアしたいと思います

|> 後編はこちら
【Phoenix1.4】(後編)パスワード認証&リレーションあり「ブログチュートリアル」

|> 対象読者

|> Elixir, Phoenixをインストールして少し動かしてみたけど、もうすこしステップアップしたい方

特にfukuoka.ex代表のpiacereさんのコラムは入門に最適なので、まだやってない方は是非全6回やってみてください!

|> Excelから関数型言語マスター1回目:行の「並べ替え」と「絞り込み」

また、今回はつい先日リリースされたPhoenix 1.4でやってみようかなと思うので、以下のコラムも見てみるといいと思います!(1.3の場合はRoutes.user_path(...)という記述のRoutes.をすべて除けば動作すると思われます)

|> Phoenix 1.4正式版① インストール編

|> ブログチュートリアル

|> 筆者の実行環境

  • Windows10
  • WSL(Windows Subsystem for Linux)
  • Erlang/OTP 21
  • Elixir 1.7.2 (compiled with Erlang/OTP 20)
  • Phoenix1.4

動作未確認ですが他の環境でも(Macとか)でもおそらく大丈夫だと思います!

|> プロジェクトを作成

mix phx.new ex_blog でex_blogという名前でプロジェクトを作成しましょう
※今回はwebpack使わないからといって--no-webpackつけたりすると、deleteメソッドが動かない罠にハマるので気を付けましょう(筆者はハマった笑

以下のプロンプトが出たらYで依存関係をインストールしましょう
image.png

以下の指示に従ってDBを生成しましょう

cd ex_blog
mix ecto.create

image.png

iex -S mix phx.serverとして以下が表示されればOKです!
image.png

ここで上手くいかない場合はPhoenixのインストールやDBの設定が上手くいっていない場合が考えられるので、以下を参考に見直してみましょう
Excelから関数型言語マスター3回目:WebにDBデータ表示【PostgreSQL or MySQL編】

※ちなみに筆者はPostgreSQL起動し忘れていたのでsudo service postgresql startしました笑

|> Userリソースの追加

Accountsという名前でコンテキストを生成し、その中にUserリソースを追加します
Userはname, email, passwordカラムを持ち、emailは一意である(ユニーク制約)としましょう
mix phx.gen.html Accounts User users name:string email:string:unique password:string

ちなみにRails触っていた人はコンテキストの概念がよく分からないと思いますが、モデルのロジック相当と捉えるとしっくりくるかと思います(合ってるか分かりませんが筆者はそう解釈しています)
似たようなロジックは同じコンテキスト内にまとめていくとよいでしょう

以下のようにファイルがたくさんできます
image.png

指示に従ってrouter.exresources "/users", UserControllerを追加

router.ex
defmodule ExBlogWeb.Router do
  use ExBlogWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", ExBlogWeb do
    pipe_through :browser

    get "/", PageController, :index
    resources "/users", UserController # ここ追加!
  end

  # Other scopes may use custom stacks.
  # scope "/api", ExBlogWeb do
  #   pipe_through :api
  # end
end

これで以下のようにRESTfulなルーティングが追加されますbb
image.png

|> Articleリソースの追加

title, content属性を持つArticleリソースを作成しましょう
リレーションはこんな感じ

  • User has many Articles
  • Article belongs to user

以下のように書くとuser_idを外部キーとしてBlogコンテキストのArticleリソースを作成できます!
mix phx.gen.html Blog Article articles user_id:references:users title:string content:string

今回Articleリソースは、userリソースが属するAccountコンテキストと同じグループではないと考えられるので、新しくBlogコンテキストを作ってその中にいれてみました
例えばArticleに対するコメント機能などを作成する場合は、commentリソースはBlogコンテキストに属すると思います

resources "/articles", ArticleControllerをroute.exに追加しましょう
その後mix ecto.migrateしてみましょう
※エラーになる場合はmix ecto.resetしてみましょう

|> DB周りの設定

|> Migrationファイルをいじる

priv/repo 以下にタイムスタンプつきのcreate_users.exsファイルができていると思います

20181130....._create_users.exs
defmodule ExBlog.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :name, :string
      add :email, :string
      add :password, :string

      timestamps()
    end

    create unique_index(:users, [:email])
  end
end

DBレベルでnullを許容したくないので以下のように書き換えます

defmodule ExBlog.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :name, :string, null: false # ここ追加
      add :email, :string, null: false # ここ追加
      add :password, :string, null: false # ここ追加

      timestamps()
    end

    create unique_index(:users, [:email])
  end
end

同様にarticleの方も

20181130..._create_articles.exs
defmodule ExBlog.Repo.Migrations.CreateArticles do
  use Ecto.Migration

  def change do
    create table(:articles) do
      add :title, :string
      add :content, :string
      add :user_id, references(:users, on_delete: :nothing)

      timestamps()
    end

    create index(:articles, [:user_id])
  end
end

userリソースが削除されたら関連するArticleも削除してほしいので、以下のように書き換えます

defmodule ExBlog.Repo.Migrations.CreateArticles do
  use Ecto.Migration

   def change do
    create table(:articles) do
      add :title, :string, null: false ## ここ変更
      add :content, :string
      add :user_id, references(:users, on_delete: :delete_all), null: false ## ここ変更

      timestamps()
    end

    create index(:articles, [:user_id])
  end
end

このあたりの設定は、Phoenixが依存しているEctoSQLに詳しく載っているので、カスタマイズしてみたい方は試してみてください!

最後にmix ecto.resetしてmigrationを再設定しましょう

|> Schemaファイルをいじる

自動生成されたSchemaファイルは以下のようになっていると思います

ex_blog/lib/ex_blog/accounts/user.ex
defmodule ExBlog.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset


  schema "users" do
    field :email, :string
    field :name, :string
    field :password, :string

    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :password])
    |> validate_required([:name, :email, :password])
    |> unique_constraint(:email)
  end
end

Ecto(PhoexnixのORM, Railsで言えばActionRecord)でリレーションたどれるように設定していきます

defmodule ExBlog.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias ExBlog.Blog.Article # ここ追加

  schema "users" do
    field :email, :string
    field :name, :string
    field :password, :string

    has_many :articles, Article # ここ追加
    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :password])
    |> validate_required([:name, :email, :password])
    |> unique_constraint(:email)
  end
end

Articleスキーマも同様に

ex_blog/lib/ex_blog/blog/article.ex
defmodule ExBlog.Blog.Article do
  use Ecto.Schema
  import Ecto.Changeset


  schema "articles" do
    field :content, :string
    field :title, :string
    field :user_id, :id

    timestamps()
  end

  @doc false
  def changeset(article, attrs) do
    article
    |> cast(attrs, [:title, :content])
    |> validate_required([:title, :content])
  end
end

以下のように変更します

defmodule ExBlog.Blog.Article do
  use Ecto.Schema
  import Ecto.Changeset
  alias ExBlog.Accounts.User # ここ追加

  schema "articles" do
    field :content, :string
    field :title, :string

    belongs_to :user, User # ここ追加
    timestamps()
  end

  @doc false
  def changeset(article, attrs) do
    article
    |> cast(attrs, [:title, :content])
    |> validate_required([:title, :content])
  end
end

|> ここまでで画面の確認

さて、ここまででリレーションの土台ができました
これでArticleを投稿したときにログインしたユーザのuser_idをカラムに追加するように設定したりできます

iex -S mix phx.serverしてlocalhost:4000にアクセスしてみましょう
localhost:4000/users
image.png

localhost:4000/users/new
image.png

ここでパスワード入力する際にパスワードがガッツリ表示されています
また、indexやshowにパスワードが表示されるようになっています
これはあまり好ましくないので以下を参考に修正します

Phoenixのphx.gen.htmlでpasswordフィールドを生成するときは忘れずにtemplateを変更しよう!

lib/ex_blog_web/templates/user/index.html.eex
<h1>Listing Users</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Email</th>
-      <th>Password</th>

      <th></th>
    </tr>
  </thead>
  <tbody>
<%= for user <- @users do %>
    <tr>
      <td><%= user.name %></td>
      <td><%= user.email %></td>
-      <td><%= user.password %></td>

      <td>
        <%= link "Show", to: Routes.user_path(@conn, :show, user) %>
        <%= link "Edit", to: Routes.user_path(@conn, :edit, user) %>
        <%= link "Delete", to: Routes.user_path(@conn, :delete, user), method: :delete, data: [confirm: "Are you sure?"] %>
      </td>
    </tr>
<% end %>
  </tbody>
</table>

<span><%= link "New User", to: Routes.user_path(@conn, :new) %></span>

lib/ex_blog_web/templates/user/show.html.eex
<h1>Show User</h1>

<ul>

  <li>
    <strong>Name:</strong>
    <%= @user.name %>
  </li>

  <li>
    <strong>Email:</strong>
    <%= @user.email %>
  </li>

-  <li>
-    <strong>Password:</strong>
-    <%= @user.password %>
-  </li>

</ul>

<span><%= link "Edit", to: Routes.user_path(@conn, :edit, @user) %></span>
<span><%= link "Back", to: Routes.user_path(@conn, :index) %></span>

lib/ex_blog_web/templates/user/form.html.eex
<%= form_for @changeset, @action, fn f -> %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <%= label f, :name %>
  <%= text_input f, :name %>
  <%= error_tag f, :name %>

  <%= label f, :email %>
  <%= text_input f, :email %>
  <%= error_tag f, :email %>

  <%= label f, :password %>
-  <%= text_input f, :password %>
+  <%= password_input f, :password %>
  <%= error_tag f, :password %>

  <div>
    <%= submit "Save" %>
  </div>
<% end %>

これで以下のように安全にパスワード入力ができます!
image.png

次は実際にリレーションを反映するためにログイン機能を実装しましょう

|> Guardianでパスワード認証の実装

パスワード認証などを行うにはセッションを扱う必要があります
以下の分かりやすい記事のようにPhoenixのSession機能を使って実装する方法もありますが今回はGuardianというライブラリを使ってみます

Guardianについては公式ドキュメントのほかに以下が参考になりました

|> Guardianと必要なライブラリの導入

以下をmix.exsに書いて、mix deps.getします

mix.exs
      {:guardian, "~> 1.1"},
      {:comeonin, "~> 4.1"},
      {:bcrypt_elixir, "~> 1.1"}

次にmix guardian.gen.secretを実行して出てきた文字列を使って以下のように設定します

config/config.exs
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.

# General application configuration
use Mix.Config

config :ex_blog,
  ecto_repos: [ExBlog.Repo]

# Configures the endpoint
config :ex_blog, ExBlogWeb.Endpoint,
...

+ config :ex_blog, ExBlog.Accounts.Guardian,
+   issuer: "ex_blog",
+   secret_key: # mix guardian.gen.secret の結果を貼り付け

# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.eznv()}.exs"

|>ちょっと余談

ちなみに学習用は構いませんが本番環境でsecret_keyを直書きするのは良くないので、.bashrcexport EX_BLOG_GUARDIAN_KEY=.....などで環境変数をセットしておいて、以下のように書く方法もあります

secret_key: "${EX_BLOG_GUARDIAN_KEY}"

このときSystem.get_envを使う方法と${}を使う方法2通りあるんですが、System.get_envだと(後述するGigalixirなどで必要になる)Distilleryなどでコンパイルするときに、コンパイル時の実行時の環境変数で固定されてしまうので後から環境変数を注入できず不便です
${}だと現在の環境変数読んでくれるみたいなのでこっちがいいかもです

https://twitter.com/KoyoMiyamura/status/1056952578745892864
https://twitter.com/KoyoMiyamura/status/1056948597957156864

|> Guardianを使ったログイン機能の実装

|> ルーティング

まず最終的なルーティングを以下のようにします
2つのパイプライン:authと:ensure_authを定義します
:authはセッションを取得したりする機能を使うために必要で、これはログインしているユーザを判別するために必要なので全ページに適用します
:ensure_authはログイン済みかどうかを判定してくれて、userのcreate/new(ユーザ登録)以外のuserリソースとarticleリソースはログイン済みのみ閲覧可能にしたいので以下のように適用させます
またログイン/ログアウト用にSessionコントローラを新しく定義します

lib/ex_blog_web/router.ex
defmodule ExBlogWeb.Router do
  use ExBlogWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

+ pipeline :auth do
+   plug ExBlog.Accounts.Pipeline
+ end
+ pipeline :ensure_auth do
+   plug Guardian.Plug.EnsureAuthenticated
+ end

  scope "/", ExBlogWeb do
-    pipe_through :browser
+    pipe_through [:browser, :auth]

+     get "/", PageController, :index
+     get "/signin", UserController, :new
+     post "/signin", UserController, :create
+     get "/login", SessionController, :new
+     post "/login", SessionController, :create
+     delete "/logout", SessionController, :delete
+   end
-    resources "/users", UserController
+   scope "/", ExBlogWeb do
+     pipe_through [:browser, :auth, :ensure_auth]
+     resources "/users", UserController, except: [:new, :create]
    resources "/articles", ArticleController
  end

  # Other scopes may use custom stacks.
  # scope "/api", ExBlogWeb do
  #   pipe_through :api
  # end
end

|> パイプラインの実装

先ほど紹介した参考記事やGuardianの公式を参考に実装していきます

lib/ex_blog/accounts/guardian.ex
defmodule ExBlog.Accounts.Guardian do
  use Guardian, otp_app: :ex_blog
  alias ExBlog.Accounts
  def subject_for_token(user, _claims) do
    {:ok, to_string(user.id)}
  end
  def resource_from_claims(claims) do
    user = claims["sub"]
    |> Accounts.get_user!
    {:ok, user}
    # If something goes wrong here return {:error, reason}
  end
end
lib/ex_blog/accounts/pipeline.ex
defmodule ExBlog.Accounts.Pipeline do
  use Guardian.Plug.Pipeline,
    otp_app: :ex_blog,
    error_handler: ExBlog.Accounts.ErrorHandler,
    module: ExBlog.Accounts.Guardian
  # If there is a session token, validate it
  plug Guardian.Plug.VerifySession, claims: %{"typ" => "access"}
  # If there is an authorization header, validate it
  plug Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"}
  # Load the user if either of the verifications worked
  plug Guardian.Plug.LoadResource, allow_blank: true
end
lib/ex_blog/accounts/error_handler.ex
defmodule ExBlog.Accounts.ErrorHandler do
  import Plug.Conn
  def auth_error(conn, {type, _reason}, _opts) do
    body = to_string(type)
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(401, body)
  end
end

|> Accountsリソースにログイン機能の実装

コントローラで使いたいので、コンテキストにログイン機能を追加します
authenticate_userはログインemailとパスワードを受け取ってログイン可能かを判定します

check_passwordの定義がすごくElixirらしいですね!
第一引数がnilの場合とそうでない場合で処理を分けるためにパターンマッチを使って分けています
これにより一つの関数は小さく保ちつつ複雑な処理を記述することを可能にします

lib/ex_blog/accounts/accounts.ex
  alias ExBlog.Accounts.User
+  alias Comeonin.Bcrypt
...

+  def authenticate_user(email, plain_text_password) do
+    query = from u in User, where: u.email == ^email
+    Repo.one(query)
+    |> check_password(plain_text_password)
+  end
+  defp check_password(nil, _), do: {:error, "Incorrect username or password"}
+  defp check_password(user, plain_text_password) do
+    case Bcrypt.checkpw(plain_text_password, user.password) do
+      true -> {:ok, user}
+      false -> {:error, "Incorrect username or password"}
+    end
+  end
+ end   
end

|> userスキーマに保存時にパスワードのハッシュ化と簡単なバリデーションの実装

userスキーマにパスワード保存時のハッシュ化処理を追加します
同名関数を定義して、パターンマッチするかどうかで処理を分岐させるやりかたはいかにもElixirらしいスタイルですね!

lib/ex_blog/accounts/user.ex
defmodule ExBlog.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias ExBlog.Blog.Article
+  alias Comeonin.Bcrypt

  schema "users" do
    field :email, :string
    field :name, :string
    field :password, :string

    has_many :articles, Article # ここ追加
    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :password])
    |> validate_required([:name, :email, :password])
    |> unique_constraint(:email)
+    |> validate_format(:email, ~r/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
+    |> validate_length(:password, min: 5)
+    |> put_pass_hash()
  end
+
+  defp put_pass_hash(%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset) do
+    change(changeset, password: Bcrypt.hashpwsalt(password))
+  end
+  defp put_pass_hash(changeset), do: changeset
end

layoutのViewファイルを変更して、eex中でcurrent_userという関数で現在ログイン中のユーザを取得できるようにします

lib/ex_blog_web/views/layout_view.ex
defmodule ExBlogWeb.LayoutView do
  use ExBlogWeb, :view
+  alias ExBlog.Accounts.Guardian
+  def current_user(conn) do
+    Guardian.Plug.current_resource(conn)
+  end
end

|> 細かい部分の変更

user登録完了後にuser/showではなくpage/indexに飛ぶようにします(好みなので必須では無いです)

lib/ex_blog_web/controllers/user_controller.ex
  def create(conn, %{"user" => user_params}) do
    case Accounts.create_user(user_params) do
-      {:ok, user} ->
+      {:ok, _user} ->
        conn
        |> put_flash(:info, "User created successfully.")
-        |> redirect(to: Routes.user_path(conn, :show, user))
+        |> redirect(to: Routes.page_path(conn, :index))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

|> sessionコントローラ/ビューの実装

lib/ex_blog_web/controllers/session_controller.ex
defmodule ExBlogWeb.SessionController do
  use ExBlogWeb, :controller
  alias ExBlog.Accounts
  alias ExBlog.Accounts.User
  alias ExBlog.Accounts.Guardian
   def new(conn, _params) do
    changeset = Accounts.change_user(%User{})
    render(conn, "new.html", changeset: changeset)
  end
   def create(conn, %{"user" => %{"email" => email, "password" => password}}) do
    Accounts.authenticate_user(email, password)
    |> login_reply(conn)
  end
   defp login_reply({:error, error}, conn) do
    conn
    |> put_flash(:error, error)
    |> redirect(to: Routes.session_path(conn, :new))
  end
  defp login_reply({:ok, user}, conn) do
    conn
    |> put_flash(:info, "Welcome back!")
    |> Guardian.Plug.sign_in(user)
    |> redirect(to: "/")
  end
   def delete(conn, _) do
    conn
    |> Guardian.Plug.sign_out()
    |> put_flash(:info, "Logout successfully.")
    |> redirect(to: Routes.page_path(conn, :index))
  end
end

Contorllerには必ず対応するViewファイルが必要なので作成しましょう(ないとエラーになる)

Railsやったことある人はViewってerbテンプレートの置き場所だと感じるのですが、Phoenixの場合はコントローラから渡ってきた変数をビュー用に整形したり、表示用のhelper関数を定義する場所として使われるようです
Railsでいうhelperの役割と、コントローラ/テンプレート間の仲立ちをする層というイメージみたいです

ちなみにJSON APIを作る場合はここでレスポンスデータの定義をします

lib/ex_blog_web/views/session_view.ex
defmodule ExBlogWeb.SessionView do
  use ExBlogWeb, :view
end

ログイン画面の実装

lib/ex_blog_web/templates/session/new.html.eex
<h2>Login Page</h2>
<%= form_for @changeset, Routes.session_path(@conn, :create), fn f -> %>
   <div class="form-group">
    <%= label f, :email, class: "control-label" %>
    <%= text_input f, :email, class: "form-control" %>
    <%= error_tag f, :email %>
  </div>
  <div class="form-group">
    <%= label f, :password, class: "control-label" %>
    <%= password_input f, :password, class: "form-control" %>
    <%= error_tag f, :password %>
  </div>
  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
<% end %>

ヘッダー部分にLogoutリンクを追加し、userがログインしている場合のみ表示するようにしてみましょう

lib/ex_blog_web/templates/layout/app.html.eex
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>ExBlog · Phoenix Framework</title>
    <link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
  </head>
  <body>
    <header>
      <section class="container">
        <nav role="navigation">
          <ul>
-            <li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
+            <%= if current_user(@conn) do %>
+              <li><%= link "Logout", to: Routes.session_path(@conn, :delete), method: :delete %></li>
+            <% end %>
          </ul>
        </nav>
        <a href="http://phoenixframework.org/" class="phx-logo">
          <img src="<%= Routes.static_path(@conn, "/images/phoenix.png") %>" alt="Phoenix Framework Logo"/>
        </a>
      </section>
    </header>
    <main role="main" class="container">
      <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
      <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
      <%= render @view_module, @view_template, assigns %>
    </main>
    <script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>

|> ここまでで画面表示

さて、iex -S mix phx.serverしてlocalhost:4000を開いてみましょう
ログインしていない場合に、ログインが必要なリソースを表示させると以下のようにエラーページが表示されます
image.png

localhost:4000/signinにアクセスし、適当なユーザを作成してみましょう
image.png

image.png

今回はユーザ登録完了後にログインを自動で行う実装ではないので、ログインを行う必要があります
(UX的にはユーザ登録後にログインした方がよさげ)

image.png

ログインするとヘッダー部分にLogoutのリンクができていますね!
image.png

localhost:4000/usersにアクセスするとログイン前は見ることが出来なかったページが見れるようになっています
image.png

localhost:4000/articles
image.png

ヘッダのLogoutリンクをクリックすると
image.png

フラッシュが表示されます
もう一度localhost:4000/userslocalhost:4000/articlesにアクセスしてみましょう
image.png

ちゃんとアクセスできなくなっています!すごい!笑

|> ※続きはElixir Advent Calendarで!

若干睡眠不足で執筆していたので、今見返したらめちゃ長いですねこの記事・・・!
後半は分割・修正してElixir Advent Calandar の方にしようと思います

|> 後編はこちら
【Phoenix1.4】(後編)パスワード認証&リレーションあり「ブログチュートリアル」

|> 次回

次回はsym_numさんのはじめての並行処理 -Queensパズルを題材にして-です!こちらも是非どうぞ~

はじめての並行処理 -8Queensパズルを題材にして-

自己紹介

長らくLispとPrologを好んで使ってきました。この度、Elixirを使ってみてその良さに惚れ込みました。ElixirはS式ではないLispのようです。Ruby風のシンタックスのため、すぐに基本文法に慣れることができました。Elixirの特長である並行処理に挑戦してみました。

題材

Elixirに習熟するために素因数分解や8Queensパズルを書いて基本的な書き方を学んでいました。ある程度、慣れてきましたので、いよいよElixirの魅力である並行処理にとりかかることにしました。題材には練習で書いた8Queensパズルを使うことにしました。マルチCPUの並列を活かして逐次処理より高速にできないか?という課題に取り組みました。

Queens問題

図のようにチェス盤上でクイーンが互いに利き筋にならないような配置を求める問題です。リストを使って位置を表すようにします。図の配置であればリストは次のようになります。

リスト [2,4,6,8,3,1,7,5]

(出典 Wikipedia)
2018-11-27_11-08-06.png

素朴な方法

当初、Elixirらしいパイプ演算子を用いる方法で逐次的に計算するものを作りました。順列のリストを生成、それを利き筋でないかどうかを確認するsafe/1という関数を作り、フィルターで解を絞り込むという方法です。ここに投稿があります。投稿記事

並行処理のアイディア

上記の素朴な方法をベースにしてElixirの軽量プロセスを活かす方法を考えました。順列を全部生成してからではなく、1つ生成する都度、利き筋でないかを確認するプロセスを起動して渡すというアイディアです。書籍「プログラミングElixir」ではプロセスを1万個程度、生成することはElixirにとっては容易なことであるという記述があったことを思い出しました。上記の素朴に1つ1つの順列に対してプロセスを起動しても動くのではないかと期待しました。

コードは下記の通りです。

defmodule W do
  def judge do
    receive do
      {_,msg} ->
        if safe(msg) do
          :io.write(msg)
        end
    end
  end

  def safe([]) do true end
  def safe([l|ls]) do
    if safe1(ls,l,1) do
      safe(ls)
    else
      false
    end
  end

  def safe1([],_,_) do true end
  def safe1([l|ls],a,n) do
    if a+n == l || a-n == l do
      false
    else
      safe1(ls,a,n+1)
    end
  end
end


defmodule M do
    def queens(n) do
    Enum.to_list(1..n)
    |> perm
  end

  def perm (ls) do
    perm1(ls,[])
  end

  def perm1([],a) do
    ls = Enum.reverse(a)
    pid = spawn(W, :judge, [])
    send pid, {self(),ls}
  end
  def perm1(ls,a) do
    Enum.each(ls,fn(n) ->
                  perm1(ls--[n],[n|a]) end)
  end
end

結果及び考察

驚きました。8Queensを難なく計算します。8!=40,320のプロセスが起動しているはずです。Elixirは涼しい顔をして計算を終えます。それならばと9Queensではどうか?10Queensではどうか?とやってみました。ちゃんと動作します。たいへん、驚きました。タスクマネージャーでCPUの稼働率を表示しながら10Queensを実行してみるとCPU稼働率が100%に達します。これはすごい!

しかし、冷静になって計算時間を計測してみると並行処理の方が遅いことがわかりました。解を表示させるとターミナルへの表示速度が影響してしまうため、表示なしで計測してみました。10Queensでは並行処理で8.797秒。逐次処理で3.797秒でした。いくら軽量プロセスとはいえ起動のためのタイムロスが大きいのだろうと思います。それにしてもまずは並行処理が動きました。

改良

上記の結果を踏まえて改良を考えました。

プロセス数

N-queensのN個のプロセスを生成することにしました。8Queensなら8つに分割して8つのプロセスに渡します。順列を生成するときに例えば1..8の順列を生成する場合であれば1を抜き取ります。2..8の順列の先頭に1を加えます。同様に2を抜き取り、1,3..8の順列を生成し、先頭に2を加えます。このようにして生成区間をN個に分割します。

枝刈り

順列を全部生成してからセーフかどうかを判定するのは非効率です。[1,2]まで生成した時点でもう以後はセーフにはなり得ないことが判明しています。生成する時に同時にセーフかどうかを判定しながら生成すると桁違いに速度が向上します。これは有名な教科書のSICPでも書かれていたのですが、無視していました。並列処理をしてもせいぜいコア数分程度しか性能が向上しないのに対し、アルゴリズムの工夫は時に桁違いな性能向上になります。あわせて改良しました。逐次処理版の9Queensは16ミリ秒に改良できました。

リスト化

並行処理で生成した解が正しいかどうかを検証するのに既に知られている解の数との比較、確認することにしました。そうすると結果を表示するのではなく、リストにしておいてlength/1でリストの要素数を確認できた方が便利です。リストを返すように改良しました。

改良後のコード

上記の改良を織り込んだコードは下記の通りです。

M.pqueens/1 並行処理のN-Queens
M.queens/1 逐次処理のN-Queens (比較用)
pqueens1/2 pqueens/1の下請け関数 順列生成をn個に分割してn個のプロセスに割り振る。
pqueens2/2 n個のプロセスから解を受け取りリストにまとめる。
W.part/0 pqueens1/2から生成されるプロセス。N-QueensのNとその部分xを受け取り、part_perm_list/2に渡す。
part_parm_list/2 n*nの盤面でのx列でのQueensの部分解を得る。

defmodule M do
  def pqueens(n) do
    pqueens1(n,1)
    pqueens2(n,[])
  end

  def queens(n) do
    ls = Enum.to_list(1..n)
    W.perm_list(ls)
  end

  def pqueens1(n,x) do
    if x>n do
      true
    else
      pid = spawn(W,:part,[])
      send pid, {self(),{n,x}}
      pqueens1(n,x+1)
    end
  end

  def pqueens2(0,ans) do ans end
  def pqueens2(n,ans) do
    receive do
      {:answer, msg } ->
        pqueens2(n-1,msg++ans)
    end
  end
end

defmodule W do

  def part do
    receive do
      {sender,{n,x}} -> send sender,{:answer, W.part_perm_list(n,x) }
    end
  end

  def part_perm_list(n,x) do
    ls = Enum.to_list(1..n) -- [x]
    perm_list1(ls,[x],[])
  end

  def perm_list(ls) do
    perm_list1(ls,[],[])
  end

  def perm_list1([],a,b) do
    if safe(a) && a != [] do
      [Enum.reverse(a)|b]
    else
      b
    end
  end

  def perm_list1(ls,a,b) do
    if safe(a) do
      List.foldr(ls,
                b,
                fn(x,y) -> perm_list1(ls--[x],[x|a],y) end)
    else
      List.foldr(ls,
                b,
                fn(x,y) -> perm_list1([],[],y) end)
    end
  end

  def safe([]) do true end
  def safe([l|ls]) do
    if safe1(ls,l,1) do
      safe(ls)
    else
      false
    end
  end

  def safe1([],_,_) do true end
  def safe1([l|ls],a,n) do
    if a+n == l || a-n == l do
      false
    else
      safe1(ls,a,n+1)
    end
  end
end


結果

逐次処理のものと並行処理のものと2つを用意しました。1Queensから15Queensまでにつき解の個数が既に知られている個数と一致することを確認しました。

9queensで逐次処理ですと16ミリ秒です。驚いたことに並行処理をする方は計測不能、0秒となっています。10Queensでようやく数値が計測できました。結果は下記の通りです。計測にあたっては自作したtimeマクロを利用しました。(使用マシンのCPU インテル core i5 3.20GHz)

#並行処理版
iex(11)> T.time(M.pqueens(9))
"time: 0 micro second"
"-------------"
[
  [3, 1, 4, 7, 9, 2, 5, 8, 6],...]

iex(7)> T.time(M.pqueens(10))
"time: 16000 micro second"
"-------------"
[
  [8, 1, 3, 6, 9, 7, 10, 4, 2, 5],...]

#逐次処理版
iex(9)> T.time(M.queens(9))
"time: 16000 micro second"
"-------------"
[
  [1, 3, 6, 8, 2, 4, 9, 7, 5],...]



iex(8)> T.time(M.queens(10))
"time: 63000 micro second"
"-------------"
[
  [1, 3, 6, 8, 10, 5, 9, 2, 4, 7],...]


並行処理をした方がおよそ4倍高速になっていました。core i5 なので台数効果がそのくらいなのだと思います。

並行処理版では15Queensくらいまでなら実用時間内に計算が終わります。15Queensで数分かかりました。

終わりに

いよいよ並列の時代が到来したのだと実感しました。Elixirの土台となっているErlangはアクターモデル、軽量プロセスを採用しています。現在はインテル core i5 i7 が通常ですが、将来 i100、i1000 といったマルチコアCPUが出現する可能性もあります。Elixirはこうした技術の恩恵を余すところなく享受できます。ますます複雑化し、高速化が要求されるWEBプログラムに応えられる最有力候補はElixirであろうと思います。

参考文献、資料

「プログラミングElixir」 Dave Thomas 著 笹田耕一、鳥井雪 共訳

timeマクロ
https://qiita.com/sym_num/items/4fc0dcfd101d0ae61987

Phoenix1.4 でのvue環境構築メモ(Part1: 単一ファイルコンポーネントを使えるようにするまで)

(この記事は「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」の4日目です)

こんにちは、@koga1020です。 早いもので、Advent Calendarのシーズンですね。全然冬らしくない暑さの福岡よりお届けしております(笑)

本題

以前、Phoenix + Vue.js入門 を投稿したのですが、こちらの記事ではVue.jsの部分はサクッとCDNから利用する方法で書いていました。

本記事では、Phoenix1.4に標準で入っているWebpack環境を利用してVueの単一ファイルコンポーネント(.vueファイル) のビルド環境を整える手順をまとめたいと思います。

環境

  • Elixir 1.7.3
  • Phoenix 1.4.0

前提

  • Phoenix1.4の環境が作れている
  • 上記環境で、npm or yarn を叩ける状態

まで進めている方を想定しています。

以下に、検証用に作ったdocker-composeを置いておきます。「これから環境作ってみる」という方は使えるかもしれません。

https://github.com/koga1020/phoenix1.4-vue-template

手順

パッケージのインストール

vue, vue-loader, vue-template-compilerを入れます

cd assets
yarn add vue
yarn add -D vue-loader vue-template-compiler

webpack.config.jsの修正

const path = require('path');
const glob = require('glob');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin'); // 追加

module.exports = (env, options) => ({
  optimization: {
    minimizer: [
      new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: false }),
      new OptimizeCSSAssetsPlugin({})
    ]
  },
  entry: './js/app.js',
  output: {
    filename: 'app.js',
    path: path.resolve(__dirname, '../priv/static/js')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader'
        }
      },
      // 追加
      {
        test: /\.vue$/,
        use: {
          loader: 'vue-loader'
        }
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      }
    ]
  },
  // 追加
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      vue$: 'vue/dist/vue.esm.js',
    }
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: '../css/app.css' }),
    new CopyWebpackPlugin([{ from: 'static/', to: '../' }]), // ← 末尾のカンマをお忘れなく!
    new VueLoaderPlugin() // 追加
  ]
});

js/app.jsの修正

// We need to import the CSS so that webpack will load it.
// The MiniCssExtractPlugin is used to separate it out into
// its own CSS file.
import css from "../css/app.css"

// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured
// in "webpack.config.js".
//
// Import dependencies
//
import "phoenix_html"

// Import local files
//
// Local files can be imported directly using relative paths, for example:
// import socket from "./socket"

// 以下を追加
import Vue from 'vue';
import App from './App.vue';

new Vue(App).$mount('#app');

App.vueの作成

js/App.vueを作成後、以下を記述します

<template>
<div>タイトル: {{ title }}</div>
</template>

<script>
export default {
  name: 'app',
  data () {
    return {
      title: 'This is App.vue!!'
    }
  }
}
</script>

app.html.eexの修正

vueコンポーネントをマウントする、id="app"の要素を追加します。mainタグの中をごそっと変えてみましょう

    <main role="main" class="container">
      <div id="app"></div>
    </main>

ビルド

assetsフォルダ内で、以下を叩きましょう。

yarn run watch

ビルド対象ファイルの変更を検出して自動でビルドが走るようになります。これでApp.vueに書いた内容もビルドされています。

Phoenixを起動

yarn run watchがフォアグラウンドで動いているので、別シェルでPhoenixを起動してください。

mix phx.server

ウェルカム画面を確認

App.vueに書いた内容が表示されました!

screencapture-localhost-4001-2018-12-02-17_01_47.png

まとめ

Phoenix1.4からデフォルトで準備されているwebpackを用いて、vueの単一ファイルコンポーネントを利用できる状態になりました。あとはvue-routerやaxiosあたりを追加していけば良さそうです。

明日の「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」5日目の記事は, @takasehideki さんの「ElixirでIoT#2.3:ラズパイの温湿度と超音波センサ値をPhoenixでサクッと?リアルタイム表示」です。お楽しみに!

ElixirでIoT#2.3:ラズパイの温湿度と超音波センサ値をPhoenixでサクッと?リアルタイム表示

(この記事は「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」の5日目です)
昨日の記事は @koga1020 さんの「Phoenix1.4 でのvue環境構築メモ(Part1: 単一ファイルコンポーネントを使えるようにするまで)」でした.

はじめに

こんにちは
fukuoka.exではIoT芸人をやっております.
福岡コミュニティに参加していながら実は京都在住なので,先日立ち上がったkyoto.exもばんばん盛り上げていきたいところです!!

さてこの記事では,もう半年も前の話しになってしまっていますが,「fukuoka.ex#11:DB/データサイエンスにコネクトするElixir」にて披露したIoT芸のネタについて,Advent Calendarらしく2018年の総決算!として今さらながら紹介しようかと思います.
# 連載記事が滞ってしまっていてすみません^^;

fukuoka.ex#11の発表スライドとGitHubリポジトリは下記のとおりです.

ラズパイやIoTなお話しはこれまでの連載記事も合わせてご笑覧ください.

こんなの作りました

ラズパイとElixirを使ってリアルタイムな環境センシングをサクッとやってみました!

登壇直前までデモが現地でまともに動かなくって,よっしゃギリで動いた!とりまバックアップで撮っておくぞ!!ってな焦りが手ブレから伝われば幸いです^^;
静止画の概念図だとこんな感じです.

pic1.jpg

コレを作るまでに様々な要素技術を駆使して多くのTIPSを得られました.
ですが細かいトコロまで解説しているとキリがない,,,(そしてまとまりがつかない!)ので,この記事では各TIPSの紹介は概要レベルに留めます.

用意したモノ

まずは使ったモノを紹介します.

ハードウェア

  • ボード:Raspberry Pi 3 Model B
    • 使いやすさが自慢のIoT時代の風雲児です.とりあえず買ってみて動かしてみた方も多いかと思います.
    • 低価格なのに64-bit CPUが4-coreも載っていたり無線LANが標準搭載だったりで,下手なラップトップより性能が高くなるなんてこともあります.
  • Groveシールド:GrovePi+
    • Groveとは,各種センサなどのIoTデバイスを画一パッケージ化したモジュール群です.
    • 入出力ピンが4ピンのGroveコネクタで統一されており,簡単に付け替えできるのが特徴です.
    • このシールドはラズパイに適合していて,様々なIoTシステムの開発をラピッドに試すことができます:bullettrain_side::bullettrain_side::bullettrain_side:
  • Grove:今回は下記のモジュールを使いました.

今回はラズパイ上でのホスト開発です.
キーボードやディスプレイはよしなにご用意ください.SSHログインできるなら不要です.

ソフトウェア

  • カーネル:Raspbian Stretch with Desktop 4.9
  • Elixirバージョン:Elixir 1.6.5 (compiled with OTP 20)
    • Erlang/OTP 20 [erts-9.3] [source] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
  • GrovePi+ライブラリ:こちらを参考にしてください.インストール不要だったかも.
  • グラフ表示:Chart.js
    • Phoenixページでのhtmlグラフ表示に使用しました.簡単にカスタマイズできて便利です.バージョンは2.1.4を使いました.
    • Phoenixのhtmlページを整形している lib/home_weather_phx_web/templetes/page/index.html.eex にて使用しています.

なんかいろいろバージョン古くね!?ってのは,半年前ですしね,,,
それぞれ新しいものでも動くとは思います(誰か試して,,,

Elixir用のGroveライブラリは,下記を使用しました.

GrovePi+とGrovePi Zeroに対応しています.Groveモジュールの対応状況はHexDocsをご参照ください.また,このライブラリでは,Erlang VMのNIF経由でラズパイのGPIO,I2C,SPIの各種デバドラにアクセスできるelixir_aleが使われています.

動かしカタ

中身の説明に入る前に,デモの使い方を紹介します.

まずはハードウェアの準備としてラズパイのセットアップです.
GrovePi+をラズパイ3Bに刺して,下記の通りGroveモジュールを接続します.

Groveモジュール GrovePi+接続先
温湿度センサ D7
超音波センサ D4
LCD I2C-1

次はソフトウェア側の操作です.
まぁひとまずcloneします.

$ git clone https://github.com/takasehideki/fukuokaex11

今回のアプリはhome_weather_phxです.

$ cd home_weather_phx/

他にも開発したものはREADME.mdをご参照ください.いろいろ悪戦苦闘の跡が感じ取れるかと.

本日の日時をlib/home_weather_phx_web/templates/page/index.html.eexの153行目のtodayに定義してください.(自動化したかったんですけどねぇ^^;

lib/home_weather_phx_web/templates/page/index.html.eex
window.onload = function () {
  var csvData = csvToArray("dhtdata.csv");
  var today = "2018-12-04 ";
  ...
};

あとはこのディレクトリにて,Phoenixサーバとアプリを起動するだけです.

$ MIX_ENV=dev mix phx.server

ブラウザからhttp://<IP>:4000/にアクセスすれば,温湿度・超音波距離の取得値がグラフ表示されます.しかもリアルタイムに表示更新します!
ラズパイ内からlocalhostでも,同ネットワーク内のPCやスマホからIP直打ちでも閲覧可能です.

pic2.jpg

仕組みのナカミ

少しだけナカミを紹介します.
詳細は(また気が向いたら)連載の別記事にしたいと思います(たぶん).

mix.exs

まずはmix.exsの中身です.

mix.exs
defmodule HomeWeatherPhx.Mixfile do
  use Mix.Project

  def project do
    [
      app: :home_weather_phx,
      version: "0.0.1",
      elixir: "~> 1.4",
      elixirc_paths: elixirc_paths(Mix.env),
      compilers: [:phoenix, :gettext] ++ Mix.compilers,
      build_embedded: Mix.env() == :prod,
      start_permanent: Mix.env == :prod,
      deps: deps()
    ]
  end

  # Configuration for the OTP application.
  #
  # Type `mix help compile.app` for more information.
  def application do
    [
      mod: {HomeWeatherPhx.Application, []},
      extra_applications: [:logger, :runtime_tools, :timex]
    ]
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_),     do: ["lib"]

  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [
      {:phoenix, "~> 1.3.2"},
      {:phoenix_pubsub, "~> 1.0"},
      {:phoenix_html, "~> 2.10"},
      {:phoenix_live_reload, "~> 1.0", only: :dev},
      {:gettext, "~> 0.11"},
      {:cowboy, "~> 1.0"},
      {:timex, "~> 3.1"},
      {:grovepi, github: "adkron/grovepi", branch: "master"}
    ]
  end
end

timexは現在時刻の取得に,phoenix_live_reloadはブラウザのリアルタイム表示に使用しています.
今回はpriv/static/dhtdata.csvに取得データを書き込んでいくこととして,このCSVファイルが更新されたらPhoenixも更新されるようにしました.dev.exsのlive_reload対象にcsvを追加しています.

config/dev.exs
# Watch static and templates for browser reloading.
config :home_weather_phx, HomeWeatherPhxWeb.Endpoint,
  live_reload: [
    patterns: [
      ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg|csv)$},
      ~r{priv/gettext/.*(po)$},
      ~r{lib/home_weather_phx_web/views/.*(ex)$},
      ~r{lib/home_weather_phx_web/templates/.*(eex)$}
    ]
  ]

GrovePiライブラリがなかなか手癖がありまして,ReleaseされているHexパッケージでは不具合がありました.このため,GitHubからmasterブランチを直接指定するようにしました.

      {:grovepi, github: "adkron/grovepi", branch: "master"}

実はdepsはこんな書き方もできるのです.

home_weather_phx.ex

アプリ本体の記述です.

lib/home_weather_phx.ex
defmodule HomeWeatherPhx do
  @moduledoc false
  use GenServer
  use Timex
  require Logger

  defstruct [:dht]

  alias GrovePi.{RGBLCD, DHT}

  @us_pin 4 # Use port 4 for Ultrasonic

  def start_link(pin) do
    GenServer.start_link(__MODULE__, pin)
  end

  def init(dht_pin) do
    state = %HomeWeatherPhx{dht: dht_pin}

    {:ok, _pid} = GrovePi.Ultrasonic.start_link(@us_pin)

    RGBLCD.initialize()
    RGBLCD.set_text("Ready!")

    # Create CSV file
    File.write "priv/static/dhtdata.csv", ""

    DHT.subscribe(dht_pin, :changed)
    {:ok, state}
  end

  def handle_info({_pin, :changed, %{temp: temp, humidity: humidity}}, state) do
    # Measure distance
    distance = GrovePi.Ultrasonic.read_distance(@us_pin)
    distance = if distance >= 200, do: 0, else: distance

    # Get date
    date = Timex.now("Asia/Tokyo")
      |> Timex.format!( "%Y-%m-%d %H:%M:%S", :strftime )

    temp = format_temp(temp)
    humidity = format_humidity(humidity)
    distance = format_distance(distance)

    # Write data to CSV
    File.write "priv/static/dhtdata.csv", "#{date},#{temp},#{humidity},#{distance}\n", [:append]

    flash_rgb()

    RGBLCD.set_text(temp)
    RGBLCD.set_cursor(1, 0)
    RGBLCD.write_text(humidity)
    Logger.info temp <> " " <> humidity <> "" <> distance

    {:noreply, state}
  end

  def handle_info(_message, state) do
    {:noreply, state}
  end

  defp flash_rgb() do
    RGBLCD.set_rgb(255, 0, 0)
    Process.sleep(1000)
    RGBLCD.set_color_white()
  end

  defp format_temp(temp) do
    "Temp: #{Float.to_string(temp)} C"
  end

  defp format_humidity(humidity) do
    "Humidity: #{Float.to_string(humidity)}%"
  end

  defp format_distance(distance) do
    "distance: #{distance}cm"
  end
end

温湿度センサGrovePi.DHTのライブラリはGenServerSupervisorで動作する設計になっています.
handle_info()の第2引数が:changedしか選べないのがちょっと厄介.要するに温湿度センサの値が変更したときしか動作しません.
同じくhandle_info()内では,超音波センサの値を取得したのちに,出力用にデータをフォーマットしてCSVファイルpriv/static/dhtdata.csvに追加書き込みします.同時にLCDにもデータを表示させています.

application.exの記述はこんな感じです.

lib/home_weather_phx/application.ex
defmodule HomeWeatherPhx.Application do
  use Application

  # RGB LCD Screen should use the IC2-1 port
  @dht_pin 7 # Use port 7 for the DHT
  @dht_poll_interval 10_000 # poll every 10 second

  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    # Define workers and child supervisors to be supervised
    children = [
      # Start the endpoint when the application starts
      supervisor(HomeWeatherPhxWeb.Endpoint, []),
      # Start your own worker by calling: HomeWeatherPhx.Worker.start_link(arg1, arg2, arg3)
      # worker(HomeWeatherPhx.Worker, [arg1, arg2, arg3]),

      # Start the GrovePi sensor we want
      worker(GrovePi.DHT, [@dht_pin, [poll_interval: @dht_poll_interval]]),

      # Start the main app
      worker(HomeWeatherPhx, [@dht_pin]),
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: HomeWeatherPhx.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # Tell Phoenix to update the endpoint configuration
  # whenever the application is updated.
  def config_change(changed, _new, removed) do
    HomeWeatherPhxWeb.Endpoint.config_change(changed, removed)
    :ok
  end
end

GrovePi.DHTのworkerを定義しています.

おわりに

2018年はElixirに出会って,そして@piacere_exさんを始めとしたfukuoka.exな多くの仲間に出会えた素晴らしい年でした.このデモアプリも皆さんのお力添えが無ければ完成することはできませんでした.
そしてそして,このような出会いのきっかけを作っていただいた@zacky1972先生には特に感謝感激雨霰でございます!!
(この辺りだけでけっこうなボリュームになりそなので,また技術ポエムにまとめますかね^^;

とはいえ2018年に挙げたネタは,所詮は他人のフンドシを借りていたに過ぎません.研究屋さんとしては新しい技術や概念をドンドン打ち出していくつもりです.脳内には構想や計画がもうたっぷりとあるのであります><;
2019年もElixirでIoT芸に磨きを掛けて,研究を加速させていきまっす!!

明日の「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」の担当は @kobatako さんの
ソフトウェアルータでカオスエンジニアリング入門」です!

ソフトウェアルータでカオスエンジニアリング入門

(この記事は「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」の6日目です)
昨日の記事は @takasehideki さんの「ElixirでIoT#2.3:ラズパイの温湿度と超音波センサ値をPhoenixでサクッと?リアルタイム表示」でした.

こんにちは、@kobatakoです。
アドベントカレンダーも始まり、クリスマスのカウントダウンが始まるのと今年も後少ししかないって思ってしまいますねぇ
寒さもましてきましたが気合で乗り切りたいです(笑)

内容

カオスエンジニアリングっという言葉はご存知でしょうか?
カオスエンジニアリングについては下記のQiitaの記事にまとめてあるので詳しく知りたい方は見てもらったほうが早いのですが、

https://qiita.com/shotat/items/f066d296bb1becb96e3f

ざっくりまとめると「本番環境が障害に耐えれるかどうかを実験する」っということです。

NetflixなんかではChaos Monkeyっというツールをもちいて人工的にシステム障害を引き起こします。NetflixではAWSを使っていますので、ランダムにEC2インスタンスを落としてサービスが冗長化されており、耐障害性を持っているかどうか、検証を行います。ほかにも米国の金融機関も使ったりとしているようで、耐障害対策として広がってきています。

なのでそんなカオスエンジニアリングまがいなことをしたいと思い、自作のソフトウェアルータでカオスエンジニアリング入門をしてみようと思いました。

実装したもの

まずは実装したものについて、ソフトウェアルータなので範囲は基本的にL2,L3の部分となりますが、もう少し上の層まで対応したいと思いL4まで含めて実装をしていきました。
下記が実装したものです。

  • 遅延
  • パケットロス
  • 二重送信
  • TCPのack

それぞれの実装はシンプルで遅延なんかは単純にsleep処理を入れるだけと、単純な実装で障害まがいなことが(遅延は障害ではないが)できる感じです。
では、それぞれがどのように実装しているのか見ていきたいと思います。

障害の追加

まずはどれを行いたいのかを宣言していきます。

  chaos :default do
    delay(
      source_ip: {192, 168, 20, 0},
      source_netmask: {255, 255, 255, 0},
      protocol: :tcp,
      milisec: 100,
      rate: 0
    )
    loss(
      source_ip: {192, 168, 20, 0},
      source_netmask: {255, 255, 255, 0},
      protocol: :tcp,
      rate: 0
    )
    duplicate(
      source_ip: {192, 168, 20, 0},
      source_netmask: {255, 255, 255, 0},
      protocol: :tcp,
      rate: 0
    )
    tcp_ack(
      source_ip: {192, 168, 20, 0},
      source_netmask: {255, 255, 255, 0},
      protocol: :tcp,
      rate: 0
    )
  end

chaosのブロックの中に実行したい処理を記載していきます。:defalutは識別子で識別子をもとにどの障害を起こしていくかを決定します。
上から順に見ていきますと、
delayがパケットの遅延、lossがパケットロス、duplicateが二重送信、tcp_ackがTCPでのACkフラグが立っているもののパケットをロスさせる処理になります。

定義

それぞれの実装を見ていきたいと思います。まずはchaosになりますが、これはマクロで実装して言います。

defmacro chaos(identifier \\ :global, attrs \\ [], do: context) do
  do_chaos(identifier, attrs, context)
end

defp do_chaos(identifier, attrs, context) do
  quote do
    Module.register_attribute(__MODULE__, :change_chaos, accumulate: true)
    Module.register_attribute(__MODULE__, :change_chaos_record, accumulate: true)
    Module.register_attribute(__MODULE__, :chaos_pipeline, accumulate: true)

    identifier = unquote(identifier)
    attrs = unquote(attrs)

    Module.put_attribute(__MODULE__, :change_chaos, {:identifier, identifier})

    try do
      unquote(context)
    after
      :ok
    end

    loaded = Eshe.Chaos.__load__(__MODULE__, @change_chaos_record)
    Module.put_attribute(__MODULE__, :chaos_pipeline, loaded)

    def chaos_pipeline, do: @chaos_pipeline
  end
end

attrsには将来的に設定を渡せるよう引数を持たせてます。マクロとのchaosが呼ばれた後にすぐにdo_chaosを実行します。
Module.register_attributeで属性を追加していきますが、主にこの属性値に値を入れながら一連の障害を作っていきます。
まず、change_chaosには最終的に実行する処理が識別子とともに格納されます。
次にchange_chaos_recordですが、一連の処理が順番に格納されていきます。なのでここで行くとdelay -> loss -> dupllicate -> tcp_ackの順番に登録されていき、最終的にchange_chaosに登録されます。
最後のchaos_pipelineに複数の一連処理が入っています。
属性値に値を入れてることでこのようなマクロを組むときに一連の処理やデータ情報を保存しておくのに使うことができます。

    try do
      unquote(context)
    after
      :ok
    end

そして上記の処理でchaosのコードブロック内の処理を行っていきます。基本的にそれぞれ同じことをしているのでdelayの処理のみ見ていきたいとお思います。

  defmacro delay(c) do
    quote do
      c = unquote(c)
      Eshe.Chaos.__delay__(__MODULE__, Map.new(c))
    end
  end

  def __delay__(module, c) do
    record = Map.merge(@default_delay_record, c)
    Module.put_attribute(module, :change_chaos_record, {:delay, record})
  end

delayもマクロで実装しており、呼ばれたときにデフォルトの設定情報とマージし、change_chaos_recordに追加します。
今の所どの処理も同じようにchange_chaos_recordに追加していくのですが、タプルでどの処理の設定なのかわかるようにのみしています。

Module.put_attribute(module, :change_chaos_record, {:delay, record})

ここで登録する情報としては先程のdelayの引数である、送信元、送信先のIPとポート番号、その他設定情報を保存します。送信元、送信先のIPとポート番号の制限はFirewallと同様、どのパケットに対して障害を起こすのか、定義していきます。

すべての処理の追加が終わったら最終的にchaos_pipelineに追加し、一つのchaosの処理をまとめることができました。

実装

ここから、実際に定義した障害の実装をしていきたいと思います。

def chaos_pipeline([], data, option) do
  {:ok, data, option}
end

def chaos_pipeline([head | tail], data, option) do
  case chaos_type_pipeline(head, data, option) do
    {:ok, data, option} ->
      chaos_pipeline(tail, data, option)

    error ->
      error
  end
end

まずはそれぞれの処理を再帰で実行していきます。chaos_type_pipelineで処理の内容ごとにパターンマッチで分岐を行っていきます。

delay

def chaos_type_pipeline({:delay, record}, data, option) do
  if Eshe.Firewall.match(record, data) do
    rate = record[:rate]
    ran = trunc(:rand.uniform() * 100)

    if rate >= ran do
      time = record[:milisec]
      :timer.sleep(time)
    end

    {:ok, data, option}
  else
    {:ok, data, option}
  end
end

Eshe.Firewall.match(record, data)っとうのでdelayで定義した送信元、送信先のIPとポートにマッチするかを判定します。一番最初にdelayで定義したもので行くと送信元が192.168.20.0/24のネットワークからの通信に対して遅延処理を行っていきます。そしてrateが起こる頻度となりmilisecが実際にどれぐらい遅延するかを決定します。
:rand.uniform()によりランダムな値を取得し、更に送信元が192.168.20.0/24のネットワークからのリクエストだった場合は遅延処理を行います。

loss

次にパケットロスの実装を見ていきたいと思います

def chaos_type_pipeline({:loss, record}, data, option) do
  if Eshe.Firewall.match(record, data) do
    rate = record[:rate]
    ran = trunc(:rand.uniform() * 100)
    if rate >= ran do
      {:error, {{:message, :chaos_type_loss}, {:record, record}, {:data, data}}}
    else
      {:ok, data, option}
    end
  else
    {:ok, data, option}
  end
end

先程のdelayとはほとんど同じですが、条件に当てはまったものはパケットを止めてしまうよ、errorを返ります。

duplicate

次に二重送信を行っていきます。

def chaos_type_pipeline({:duplicate, record}, data, option) do
  if Eshe.Firewall.match(record, data) do
    rate = record[:rate]
    ran = trunc(:rand.uniform() * 100)
    if rate >= ran do
      {:ok, data, Map.merge(%{duplicate: true}, option)}
    else
      {:ok, data, option}
    end
  else
    {:ok, data, option}
  end
end

こちらは二重送信のフラグをつけて、実際の送信処理を二回呼ぶようにしています。

tcp_ack

最後にACkフラグのパケットの停止処理です。

def chaos_type_pipeline({:tcp_ack, record}, data, option) do
  if Eshe.Firewall.match(record, data) do
    rate = record[:rate]
    ran = trunc(:rand.uniform() * 100)
    if rate >= ran and has_tcp_ack(data) == true do
      {:error, {{:message, :chaos_type_tcp_ack}, {:record, record}, {:data, data}}}
    else
      {:ok, data, option}
    end
  else
    {:ok, data, option}
  end
end

def has_tcp_ack(<<_ :: size(72), 6, _ :: size(80), tcp :: size(106), _:: size(1), 1 :: size(1), _ :: binary>>) do
  false
end
def has_tcp_ack(_) do
  true
end

ここではhas_tcp_ackっという関数でパケット内にACKフラグの立っているパケットかどうか判定します。こういうパケットでのフラグのパターンマッチはElixirの得意なところですね。

まとめ

とりあえず入門として、まずはカオスエンジニアリングするための土台を作っていきました。プログラム自体もとりあえず動くものとして書いていきました。これを作ってる中で、どういった障害が起こり得るのか、どのタイミングで起こるものなんかなどを考えたりもしたのでどういったものがあるか知ることもでき新たな学びにもなりました。

これからはもう少し実践としてつけるよう障害の種類とタイミングなどを細かく調整できるようにしていきたいと思います。

Authorizationヘッダーで認証が必要なPhoenixのRequestSpecについて

(この記事は「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」の7日目です)

昨日の記事は,@kobatakoさんの「ソフトウェアルータでカオスエンジニアリング入門」でした。

はじめに

こんにちは。 @zuminです。
アドベントカレンダーと聞くと,クリスマスが近づいてきて今年ももう終わりかーという気持ちになりますね。
今年1年も一瞬で過ぎ去っていった感じがします。

今回は,PhoenixでRESTful APIを作成した場合などにRequestSpecを書く際,AuthorizationヘッダーにJWTなどを含む必要がある場合について書こうと思います。

動作環境

ExUnitの例はたくさん出てくると思うので,今回はESpecを例に進めていきます。
ただし,ExUnitでも Phoenix.ConnTest まわりは変わらないと思うので, build_conn() は同様に利用することができると思います。

  • Elixir 1.7.4
  • Phoenix 1.3.4
  • espec 1.6.3
  • espec_phoenix 0.6.10

内容

以下のような,curlでユーザーデータのリストがとれるようなWeb APIのRequestSpecをESpecで書き, JWT認証を行う必要がある場合を想定しています。

curl http://localhost:4000/api/v1/users \
  -H "accept: application/json" \
  -H "Authorization:Bearer TOKEN"

JWT認証の実装に関しては今回は触れませんが, @yujikawaさんのPhoenix/ElixirでAPIのログイン機能を作成する(guardian・guardianDB)
が参考になると思います。

テストコード例

describe "GET /api/v1/users" do
  context "is successful" do
    let :token do, "TOKEN"
    let :conn do
      build_conn()
      |> put_req_header("authorization", "Bearer #{token()}")
    end

    it do
      response = get(conn(), "/api/v1/users")
      expect(response.status)
      |> to(eq 200)
    end
  end
end

Phoenix.ConnTestbuild_conn() でテスト用の Plug.Conn のRequest Fieldsを生成することができます。
この時に,build_conn() |> put_req_header("authorization", "Bearer TOKEN") (TOKENには任意の文字列が入ります) とすることで,Authorizationヘッダーを含んだ Plug.Conn を作ることができます。

まとめ

Phoenixでテストを書くとき, Plug.ConnPhoenix.ConnTest のヘルパーによって楽にAuthorizationヘッダーなどを含めることができます。

「認証」が絡むテストだ・・・。と身構えてしまうかもしれませんが,意外と簡単にテストを書くことができましたね。
それでは良いBDDライフを!

明日は,「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」の担当は @mocamocalandさんです。 お楽しみに!

PhoenixをGCPにデプロイする方法

ようやくQiitaに初投稿です!

今回はElixirのフレームワークであるPhoenixで
GCP環境にデプロイする手順を解説します。
アプリは参照先に記載されてるnano_plannnerを使用します。

環境は以下の通り

  • Ubuntu 18.04 Server LTS
  • リージョン: asia-northeast1(東京)
  • ゾーン: asia-northeast1-b
  • n1-standard-1(vCPU x 1、メモリ 3.75 GB)
  • HTTP、HTTPSトラフィックを許可する。

今回はお試しで作るだけなので、インスタンスは1個のみで行います。

以下のユーザー名を追加します。
  • mocamocaland
  • phoenix
ユーザーの追加方法

作成したインスタンスにsshでアクセスしてから以下の手順でユーザーを追加していきます。

$ sudo adduser mocamocaland

そうすると以下の内容が出て来ます。

Adding user `mocamocaland' ...
Adding new group `mocamocaland' (1003) ...
Adding new user `mocamocaland' (1002) with group `mocamocaland' ...
Creating home directory `/home/mocamocaland' ...
Copying files from `/etc/skel' ...
Enter new UNIX password: mocamocaland # 今回はお試しで作っているため
Retype new UNIX password: mocamocaland
passwd: password updated successfully
Changing the user information for mocamocaland
Enter the new value, or press ENTER for the default
# 以下5項目は何も入れずにenterを入力
        Full Name []: 
        Room Number []: 
        Work Phone []: 
        Home Phone []: 
        Other []: 
Is the information correct? [Y/n] # Yを入力してenterを入力
$ gpasswd -a mocamocaland sudo

上記を作業をphoenixユーザーでも行なって下さい。

まずmocamocalandユーザーでログインします。

$ sudo login mocamocaland

Erlang と Elixir のインストール

以下の順番で環境構築とインストールをします。
途中聞かれるようなことがあればYを入力して進めて下さい。

$ wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb
$ sudo dpkg -i erlang-solutions_1.0_all.deb
$ sudo apt-get update
$ sudo apt-get -y install esl-erlang
$ sudo apt-get -y install elixir

Elixirが入ってることを確認します。

$ elixir --version
Erlang/OTP 21 [erts-10.1.3] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:1] [hipe]
Elixir 1.7.4 (compiled with Erlang/OTP 20)

Node.jsのインストール

$ sudo apt install nodejs npm

PostgreSQLのインストールとセットアップ

$ sudo apt-get -y install postgresql
$ sudo -u postgres createuser -d phoenix -P

パスワード入力を求められたらphoenixで入力します。
更に以下を実行します。

$ sudo -u postgres createdb --owner phoenix nano_planner_prod

Nginxのインストールと設定

Nginxをインストールをします。

$ sudo apt-get -y install nginx

インストールが終わりましたら
/etc/nginx/sites-available ディレクトリに移動します。

$ cd ~/etc/nginx/sites-available

移動したら新規ファイル でnano_planner を作成します。
ファイルには以下の内容を記載します。
GCPの外部IPは作成したGCPのインスタンスで自動で生成されてる外部IPを入力して下さい。

upstream phoenix {
  server 127.0.0.1:4000;
}

server {
  listen 80;
  server_name `GCPの外部IP`;

  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header Host $http_host;
  proxy_set_header X-Cluster-Client-Ip $remote_addr;

  location / {
    proxy_pass  http://phoenix;
    allow all;
  }
}

最後に以下を実行します。

$ cd /etc/nginx/sites-enabled
$ sudo ln -s ../sites-available/nano_planner .
$ sudo systemctl reload nginx

phoenixユーザーに変更

$ sudo login
sampler login: phoenix
Password: phoenix

ソースコードの取得、npmのインストールとrun deployを実行

$ git clone -b master4-deploy https://github.com/oiax/nano_planner.git
$ cd nano_planner
$ cp config/skel/prod.secret.exs config/
$ cd assets
$ npm install
$ npm run deploy
$ cd ..

tarballの作成します。

$ mix deps.get --only prod
$ MIX_ENV=prod mix compile
$ MIX_ENV=prod mix phx.digest
$ MIX_ENV=prod mix release

上記でprivate/staticのディレクトリがなくて以下のエラーが出る場合は
mkdir priv/staticで作成して下さい。

# エラー内容
$ MIX_ENV=prod mix phx.digest
The input path "priv/static" does not exist

リリースの配備をします。

$ mkdir -p ~/app/releases/0.1.0
$ cd _build/prod/rel/nano_planner/releases/0.1.0
$ cp nano_planner.tar.gz ~/app/releases/0.1.0
$ cd ~/app/releases/0.1.0
$ tar -xf nano_planner.tar.gz -C ~/app

データベースを初期化

$ cd ~/nano_planner
$ MIX_ENV=prod mix ecto.drop # 中身を一旦削除します。
$ MIX_ENV=prod mix ecto.create
$ MIX_ENV=prod mix ecto.migrate
$ MIX_ENV=prod mix run priv/repo/seeds.exs

アプリを起動します。

$ cd ~/app
$ bin/nano_planner start

最後に4000番ポートでプロセスの存在を確認します。

$ lsof -i:4000

以下の結果が出ればGCPの外部IPより表示を確認できます。
http://'外部IP'で表示確認ができます。

COMMAND   PID    USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
beam.smp 2080 phoenix   15u  IPv6  25000      0t0  TCP *:4000 (LISTEN)

参考先URL
Elixir/Phoenix: Distillery を利用した tarball 作成と配備

Ubuntu 端末 その18 - 他のユーザーでログインする : login、logoutコマンド

ubuntu ユーザを追加して sudo 権限をつける

RustElixirで線形回帰を高速化した話

(この記事は「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」の9日目です)
昨日の記事は @mocamocaland さんの「PhoenixをGCPにデプロイする方法」でした.


どうもfukuoka.exキャストの@hisawayです.

ElixirからRustler(NIF)経由でRustのコードを呼び出したら,どれだけコードが速くなるか,線形回帰で検証しました. @zacky1972 さんの「ZEAM開発ログ」,特にHastegaシリーズの先行探検隊な位置付けです. HastegaというのはElixirからSIMD:マルチコアCPU/GPU 並列処理を行う機能の総称です.

より詳しいHastegaについてはZEAM開発ログ: Elixir マクロ + LLVM で超並列プログラミング処理系を研究開発中をご覧ください.

(タグがRustなのがきになってしょうがないです.タイトル以外はRustに統一してます)

概要

Elixirの速さ

ElixirはErlangVM(BEAM)上で動く仕様であるため,ネイティブコードではなく,バイトコードを出力します.
Python, Rubyと比べると速いんですが, 比較対象にCやC++を挙げてしまうと遅いです.

HiPEというオプションを有効にすることでネイティブコードでコンパイルすることができますが,速度を重視する際には,やはり元からスピードのある言語を使用する必要があります.
(HiPEはErlangのコンパイルオプションです.)
|> @sileHiPE(High Performance Erlang)について

そこでRustの力を借ります.ElixirからRustを使おうって記事は先人の方が投稿しております.
|> @tatsuya6502 さん 「ElixirからRustの関数をつかう → はやい
|> @twinbee さん 「Elixirから簡単にRustを呼び出せるRustler #1 準備編

用語

NIF

Native Implemented Functionsの略で,Elixirの下で動いているErlangVM(BEAM)からネイティブコードを使う仕組みのことです.

Rustler

NIF経由でRustの関数を呼びだすElixirのモジュールです.Rustで記述した関数とElixirの関数を対応づけて記述することで楽にコーディングすることができます.

Rustler_export_nifs! {
  "Elixir.LinearRegressorNif", // Elixirのモジュール名を指定
  [
    //("Elixir's func-name, number of arguments, Rust's func)
    ("_fit", 5, nif_fit), 
  ],
  None
}

SIMD

Single Instruction Multiple Dataの略です.たくさんのデータに対して,1つだけ処理を与えるような命令をいいます.「このベクトルの全要素を2倍してよ」みたいな感じです.
RustはデフォルトでSIMD命令にコンパイルしてくれます.

題材について

高速化する対象が線形回帰なのは,Elixirで機械学習しようという話になったとき,古典的な機械学習で取り組みやすいとして, @piacere_ex さんがElixirで書かれた線形回帰のコードを提供してくださったことが理由です.

高速化の方針

以下の2つの方法で高速化を行いました.アルゴリズムの最適化はしません.
1. Rustler経由でネイティブコードの呼び出し及び,NIFの非同期呼び出し
2. Rust の Rayonを使用したCPUマルチコア並列実行

Rayonは連続して計算をするコードを簡単に並列計算仕様にできるデータ処理ライブラリです.
|> Crate Rayon

備考

今回は線形回帰を高速化させるのが目的ではなく,Elixirのネイティブコード化による速度向上を評価することが目的です.
もし劇的な高速化を図る際は,アルゴリズムから組み直してくださいね.

動作環境

PC

  • MacBook Air(Early 2015)
    • Processor: 1.6 GHz Intel Core i5
    • Memory: 8 GB 1600 MHz DDR3
    • Graphics: Intel HD Graphics 6000 1536MB
  • iMac Pro(2017)
    • Processor: 2.3 GHz Intel Xeon W (プロセッサ数 1,物理コア 18,論理コア 36)
    • Memory: 32 GB 2666 MHz DDR4
    • Graphics: Radeon Pro Vega 64 16368MB
  • Mac Pro (Mid 2010)

    • Processor:
    • 2 x 3.46 GHz 6-Core Intel Xeon (プロセッサ数 2,物理コア数 6 x 2,論理コア数 12 x 2)
    • Memory: 16 GB 1066 MHz DDR3
    • Graphics: ATI Radeon HD 5770 1024MB
    • OS : Sierra 10.12.6
  • GPGPUサーバー ユニットコム UCGPU-E2630V4- 32GB

    • Processor: 2.20GHz Intel Xeon E5-2630 v4 (プロセッサ数 2 物理コア数 10コア x 2,論理コア数 20コア x 2)
    • Memory: 32 GB 2400 MHz DDR4
    • Graphics: NVIDIA GeForce GTX 1080 Ti
    • OS : Linux Ubuntu 16.04
  • 言語

    • Elixir 1.7.3(Mac Proは 1.7.4でした)
    • Erlang/OTP 21 [erts-10.1] [source] [64-bit] [smp:40:40] [ds:40:40:10] [async-threads:1] [hipe] [sharing-preserving]
    • rustc 1.28.0
    • Rustler 0.18.0

性能評価

  1. inline: Elixirのみ,一部機能をインライン展開して最適化したコード
  2. Rust: Rustlerを使って,ElixirからRustのコードを呼び出し
  3. Rayon: Rustのコードに加えて一部を並列処理

inlineを基準として倍率を測定しています.

MacBook Air(Early 2015)

inline Rust Rayon
実行時間[s] 7.240421 4.860835 7.394763
倍率 (1.00) 1.48 0.97

Mac Pro(Mid 2010)

inline Rust Rayon
実行時間[s] 7.066738 3.64614 8.535108
倍率 (1.00) 1.93 0.82

iMac Pro(2017)

inline Rust Rayon
実行時間[s] 4.159493 3.01175 7.962244
倍率 (1.00) 1.38 0.52

GPGPUサーバー

inline Rust Rayon
実行時間[s] 6.643913 3.113606 6.690754
倍率 (1.00) 2.13 0.99

評価総括

  • 純粋にネイティブコードに落とすと,1.38 ~ 2.13 倍ほど速くなりました.
  • 並列処理にすると,0.52 ~ 0.99倍と遅くなりました
  • iMac Proだけinlineが異常に速い(原因は未調査)

実装

線形回帰のメインであるfittingを行う関数です.
大まかな流れは,

  1. NIF経由で起動されたスレッドから別スレッドを立てる準備をする
  2. 元のスレッドは制約があるのでさっさと終了する
  3. 別スレッドの方でメインの計算をする

といった感じです.

すぐかはわかりませんが,後ほどリポジトリを公開する予定です!

native/linear_regressor_nif/src/lib.rs#L95-L145
fn nif_fit<'a>(env: Env<'a>, args: &[Term<'a>])-> NifResult<Term<'a>> {
  let pid = env.pid();
  let mut my_env = OwnedEnv::new();

  let saved_list = my_env.run(|env| -> NifResult<SavedTerm> {
    let _x = args[0].in_env(env);
    let _y = args[1].in_env(env);
    let theta = args[2].in_env(env);
    let alpha = args[3].in_env(env);
    let iterations = args[4].in_env(env);
    Ok(my_env.save(make_tuple(env, &[_x, _y, theta, alpha, iterations])))
  })?;

  std::thread::spawn(move ||  {
    my_env.send_and_clear(&pid, |env| {
      let result: NifResult<Term> = (|| {
        let tuple = saved_list
        .load(env).decode::<(
          Term, 
          Term,
          Term,
          Num,
          i64)>()?; 

        let x: Vec<Vec<Num>> = try!(tuple.0.decode());
        let y: Vec<Vec<Num>> = try!(tuple.1.decode());
        let theta: Vec<Vec<Num>> = try!(tuple.2.decode());
        let alpha: Num = tuple.3;
        let iterations: i64 = tuple.4;

        let tx = transpose(&x);
        let m = y.len() as Num;
        let (row, col) = (theta.len(), theta[0].len());
        let tmp = alpha/m;
        let a :Vec<Vec<Num>> = vec![vec![tmp; col]; row]; 

        let ans = (0..iterations)
          .fold( theta, |theta, _iteration|{
           sub2d(&theta, &emult2d(&mult( &tx, &sub2d( &mult( &x, &theta ), &y ) ), &a))
          });

        Ok(ans.encode(env))
      })();
      match result {
          Err(_err) => env.error_tuple("test failed".encode(env)),
          Ok(term) => term
      }  
    });
  });
  Ok(atoms::ok().to_term(env))
}

解説

ElixirとRustの通信

ElixirからRustに引数を渡すのは1手間かかります.Elixirから渡されるデータはErlangVMが読むためのバイトコードなので,コンパイル時に型推論ができません.ちゃんと指定してあげましょう.

|>「Elixir + Rustlerでベクトル演算を高速化しよう〜Rustler初級者編 1 〜

またElixirで引数を渡す際には,あらかじめリストの中身をすべて浮動小数点型にしましょう.

# Rust側の関数と対応づけ
def _fit(_x, _y, _theta, _alpha, _iteration), do: exit(:nif_not_loaded)

def to_float(num) when is_integer(num), do: num /1
def to_float(r) when is_list(r) do
    r
    |>Enum.map( &to_float(&1) )
end

def fit( x, y, theta, alpha, iterations ) do
   _fit(
      x |> to_float, 
      y |> to_float, 
      theta |> to_float, 
      alpha |> to_float,
      iterations)
    receive do
      l -> l
    end
  end

Rustの方から結果を受け取るにはreceiveを使います.

NIFの制約

NIFはElixirとの通信,内部でのデータ処理を1ms以下に抑えないと,パフォーマンスが大幅に低下します.
そのため,Rust側でスレッドを立てて,そこから計算結果を受信する構造になっています.

参照や所有権について

Rustではデータにつき,1つの変数が所有権をもっています.そのため,基本的に読み取り専用の参照で関数に渡すと速くなりますし,その後のコードが書きやすいです.

もし参照を渡さない場合は,関数に設定した引数に元の変数の所有権が移ってしまうためにその関数が処理を終えた後,再利用できなくなってしまいます.以前はこの対処に事前にclone()して関数に渡すという方法をとっていました.
今思えば,プログラマーに参照渡しを強制させられる仕様になっているように思えますね.

並列処理部

下記コードがメインの処理部分です.

native/linear_regressor_nif/src/lib.rs#L131-L134
let ans = (0..iterations)
  .fold( theta, |theta, _iteration|{
    sub2d(&theta, &emult2d(&mult( &tx, &sub2d( &mult( &x, &theta ), &y ) ), &a))
  });

マルチコアで並列実行する際にはiterpar_iterに変更します.また,一番外側の処理を並列化すると効果が高いです.今回の場合だと,行列の各要素で減算を行うsub2d関数に変更を加えます.

変更前

native/linear_regressor_nif/src/lib.rs#L52-L56
pub fn sub2d(x: &Vec<Vec<Num>>, y: &Vec<Vec<Num>>) -> Vec<Vec<Num>>{
  x.iter().zip(y.iter())
  .map(|t| sub(&t.0.to_vec(), &t.1.to_vec()))
  .collect()
}

変更後

native/linear_regressor_nif/src/lib.rs#L52-L56
pub fn sub2d(x: &Vec<Vec<Num>>, y: &Vec<Vec<Num>>) -> Vec<Vec<Num>>{
  x.par_iter().zip(y.par_iter())
  .map(|t| sub(&t.0.to_vec(), &t.1.to_vec()))
  .collect()
}

Rayonはiter->par_iterと変えるだけで並列処理ができる便利なライブラリですが,zip()と併用するとめっちゃ遅いみたいなことをどこかで見た覚えがあるので,近いうちに調査しようと思ってます.

今後への繋ぎ

本稿では,Elixirのネイティブコード化,CPUマルチコアを生かした並列処理をコードに組み込んだ場合のパフォーマンスを測定しました.

上記の変更だけでは,パフォーマンスは劇的には伸びませんでしたね.その要因の1つに処理自体がそんなに重くないというのがありますが,手動で最適化・高速化しようとしてみると,とても手間がかかる上に結果がよろしくないというあまりおもんない結果でした.

そのため,Hastegaを有効に使うためには普通に動かす場合,CPUマルチコア,GPUマルチコアのどれが速くなるのか判断する機能が必要になりますね.並列処理させる方法としては,Elixirのプロセスもあげられるのでプロセス間の通信方法を改良する(Flowの改良?)とかもするかもしれません.

研究計画

今後の取り組みとして,

  1. 大きめの線形的な擬似モデルを生成する機能を作る(現実的なモデルとして存在する規模かは一旦置いとく)
  2. SIMD & CPUマルチコアにおける性能評価を再度行う.

  3. RustのOpenCLラッパーであるCrate oclを使ったGPGPUのコードを評価を行います.

出来上がり次第記事にします.

最後に

研究の全体構想について知りたい方はこちらを合わせてお読みください.

ZEAM開発ログ2018年総集編その1: Elixir 研究構想についてふりかえる(前編)

2018年12月15日に公開予定:
fukuoka.ex Elixir/Phoenix Advent Calendar 2018」15日目
「ZEAM開発ログ2018年総集編その2: Elixir 研究構想についてふりかえる(後編)」

追記

リポジトリを公開しました
https://github.com/zeam-vm/linear_regressor_cli

今週中(~2018/12/16まで)にコメントとご指摘いただいた内容と上記で示した研究計画を実施,それとドキュメントを整備する予定です.


明日の「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」10日目の記事は, @curry_on_a_rice さんの「Elixirでニューラルネットを実装しようとした話」」です。こちらもお楽しみに!

Elixirでニューラルネットワークを実装しようとした話

External article

grpc-elixirでGoと通信してみる #1

(この記事は、「fukuoka.ex Elixir/Phoenix Advent Calendar Advent Calendar 2018」の11日目です)

昨日は @curry_on_a_rice さんのElixirでニューラルネットを実装しようとした話でした。

grpc-elixirについて

gRPCはマイクロサービス用の通信プロトコルとして脚光を浴びていますが、今回はそのElixir実装である、gRPC Elixirをご紹介します。C++, Node.js, Python, Ruby, Objective-C, PHP, C#, Java, Goと、様々な言語サポートがある一方で、Elixirは蚊帳の外だったのですが、Tony Han氏によりgRPCのElixir実装が進んでいます。

image.png

gRPCに関してはマイクロサービスバックエンドAPIのためのRESTとgRPCをご覧ください。

2018年12月現在はα版ですが、( ⇒ 12/11α版表記取れました ! )
他言語との連携を確認する分には問題ないので、今回ご紹介することにしました。ロードマップを見る限りは、圧縮・ロギング・外部エンコーディング以外の実装は済んでます。

gRPC公式には単純な接続を確認するHello Worldと、双方向ストリーミングを行うRoute Guideというサンプルがあります。elixir-grpcでは両方とも実装されているので、2つのサンプルを使った通信をPlay with Dockerを使用して行います。

いわゆる「体裁」面はほぼ実装済みで、β版への移行は内部で使用しているgunなどのhttp2の関連ライブラリの安定待ちではないかと、私は推測しています。
本コラム投稿直前に正式版となりました!

以降の手順で必用とされるスキル

vimでファイルの編集ができること
Dockerコンテナで中と外の居場所の区別がつくこと

一般的な実装手順

今回は単項目の呼び出しに限定して、解説します。
導入手順に関しては、grpc-elixirのリポジトリに丁寧に書いてあるので、ここではその手順を軽く辿るのみとします。

0. プロジェクトへのインストール

mix.exsにパッケージを追加します

mix.exs
def deps do
  [{:grpc, github: "tony612/grpc-elixir"}]
end
mix deps.get

1. protocol buffer elixirプラグインのインストール

gRPCでは、.proto拡張子が付くIDL(インターフェス定義言語)を定義します。protocツールを使って、各言語用のスキーマーや構造体を作成します。各言語用のprotocプラグインがあるのですが、なんと、elixirプラグインであるelixir-protobufもtony氏が作られたものです。

以下のコマンドを使ってprotocのプラグインをインストールします。(protocコマンドはOSのパッケージマネージャーでインストールしてください)

mix escript.install hex protobuf --force

インストール後は~/.mix/escriptsにパスを通す必要があります。

PATH=~/.mix/escripts:$PATH

2. protoファイルから、構造体ファイル(.pb.ex)を作成

protoc --elixir_out=plugins=grpc:./lib/ helloworld.proto

IDLのhellowold.protoからは、.pb.exという拡張子で次のようなRequestとReply用のモジュールが生成されます。

hellowolrd.pb.ex
defmodule Helloworld.HelloRequest do
  use Protobuf, syntax: :proto3

  @type t :: %__MODULE__{
    name: String.t
  }
  defstruct [:name]

  field :name, 1, type: :string
end

defmodule Helloworld.HelloReply do
  use Protobuf, syntax: :proto3

  @type t :: %__MODULE__{
    message: String.t
  }
  defstruct [:message]

  field :message, 1, type: :string
end

defmodule Helloworld.Greeter.Service do
  @moduledoc false
  use GRPC.Service, name: "helloworld.Greeter"

  rpc :SayHello, Helloworld.HelloRequest, Helloworld.HelloReply
end

defmodule Helloworld.Greeter.Stub do
  @moduledoc false
  use GRPC.Stub, service: Helloworld.Greeter.Service
end

3.クライアント側コード

クライアント側のコードを対話環境で実行すると以下の通りとなります。

iex> GRPC.Server.start(Helloworld.Greeter.Server, 50051)
iex> {:ok, channel} = GRPC.Stub.connect("localhost:50051")
iex> request = Helloworld.HelloRequest.new(name: "grpc-elixir")
iex> {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(request)

4.サーバー側コード

また、サーバー側のコードは以下の通りです。
上記で定義したモジュールを使いながら記述していきます。

defmodule Helloworld.Greeter.Server do
  use GRPC.Server, service: Helloworld.Greeter.Service

  @spec say_hello(Helloworld.HelloRequest.t(), GRPC.Server.Stream.t()) ::
          Helloworld.HelloReply.t()
  def say_hello(request, _stream) do
    Helloworld.HelloReply.new(message: "Hello #{request.name}")
  end
end

以下のようにApplicationモジュールのSupervisorツリーにhelloworldサーバーを登録します

defmodule YourApp do
  use Application
  def start(_type, _args) do
    import Supervisor.Spec
    children = [
      # ...
      supervisor(GRPC.Server.Supervisor, 
    [{Helloworld.Greeter.Server, 50051}])
    ]

    opts = [strategy: :one_for_one, name: YourApp]
    Supervisor.start_link(children, opts)
  end
end

mix.exsにアプリケーションモジュールを登録します

mix.exs
  def application do
    [
      mod: {YourApp, []}, # これを登録
      extra_applications: [:logger]
    ]
  end

config.exsにアプリケーション設定するか、mixコマンドで待ち受けが可能です。

mix grpc.server

Docker-composeで双方向通信環境を作る

ここからは、docker環境を作り相互通信をするフェーズです。

2つ以上の言語を使った通信なので、環境まわりを楽にすべくDocker-composeを使います。といっても、ファイルを3種コピペすれば通常のDocker環境で実行できるので、気軽に読み進めて下さい。

ここでは2種類のコンテナを一気に立ち上げます
- Elixirコンテナ elixir-node
- Golangコンテナ go-node

Play with Dockerについて

無料で4時間Docker環境が使えるサービスです。Docker Hubに登録していれば誰でも使用できます。ホストVMのメモリも4GBでvCPU8コアなので、速度はかなり速いです(個人の感想です)。Docker Hubに登録すればすぐ使用できるので、ご登録をお勧めします。

詳しくはDocker 入門にはインストールなしで使える「Play with Docker」がいいと思うをご覧ください。

以下の説明はPlay with Dockerにて進めますが、ご自分のPCにDockerをインストールされている方も、ほぼ変わらない手順で動作します。(touch等必用ない分そちらが楽です)

コンテナ構築

Play with Dockerで行う場合は、内臓のEditorを使うためにtouchコマンドでまず空ファイルを3種作ります。(Vimも使えますが、コピペに難があります)

コンテナには、elixir-nodeとgo-nodeという名前を付けています。docker-composeだとホスト名として扱えるので生IPアドレスを扱わなくて済むので楽です。

まずは「ADD NEWINSTANCE」ボタンを押して下さい。ブラウザ内にLinuxコンソールが立ち上がります。そこで以下を入力します。

$ touch docker-compose.yml
$ touch Dockerfile-go
$ touch Dockerfile-elixir

image.png

Editorボタンを押すと、数秒後にフォルダ構成が出て来ます。

image.png

それぞれのファイルに以下をそれぞれコピペしてSaveボタンを押してください。

Dockerfile-elixir

公式のaplineコンテナを使用して、grpc-elixirのリポジトリをクローン。protobuf関連の準備をするコンテナです。

Dockerfile-elixir
FROM elixir:1.7.4-alpine

RUN apk -U update && apk --update --no-cache add protobuf git vim && \
    cd ~ && \
    git clone https://github.com/tony612/grpc-elixir.git && \
    mix local.hex --force && \
    mix local.rebar --force && \
    mix hex.info && \
    mix escript.install hex protobuf --force

ENV PATH /root/.mix/escripts:$PATH

CMD ["ash"]

Dockerfile-go

gRPCの公式コンテナを引用して、Goのパッケージを準備を行います。
各種サンプルを事前コンパイルして、helloworldのサーバーを実行して待機する内容です。

Dockerfile-go
FROM grpc/go:latest

RUN go get -u golang.org/x/net/http2 golang.org/x/net/context && \
    cd /go/src/google.golang.org/grpc/examples/helloworld/greeter_server && \
    go build main.go && \
    cd /go/src/google.golang.org/grpc/examples/helloworld/greeter_client && \
    go build main.go && \
    cd /go/src/google.golang.org/grpc/examples/route_guide/server && \
    go build server.go && \
    cd /go/src/google.golang.org/grpc/examples/route_guide/client && \
    go build client.go

CMD ["/go/src/google.golang.org/grpc/examples/helloworld/greeter_server/main"]

EXPOSE 50051 10000

docker-composer.yml

elixir-nodeとgo-nodeを定義します。2つのコンテナの由来や関連付けを行います。コメント化された一行は次回で使用します。

docker-compose.yml
version: '3'
services:
  elixir-node:
    build:
      context: .
      dockerfile: Dockerfile-elixir
    command: ash
    tty: true
    environment:
      - MIX_ENV=dev

  go-node:
    build:
      context: .
      dockerfile: Dockerfile-go
    # command: sh -c "cd /go/src/google.golang.org/grpc/examples/route_guide/ && server/server"

エディタを閉じコンソールに戻ります。
以下を起動すると、ビルドが始まりますのでしばらくお待ちください。

docker-compose.ymlを見てわかるように、elixir-nodego-nodeという2つのコンテナを同時に起動します。

docker-compose up -d

HelloWorld gRPC サンプル

Elixirのコンテナにashを起動します。

HelloWorld : Elixir -> Golang

$ docker-compose exec elixir-node ash

以下elixir-nodeのコンテナ内での作業です。フォルダ移動と依存関係の取得、コンパイルを行います。

# cd ~/grpc-elixir/examples/helloworld
# mix deps.get && mix compile

まずはクライアントを実行してみます。
priv/client.exsにクライアント用のコードが用意されているのですが、サーバーがlocalhost固定ですので、ここは動きを理解するためにも、手動で実行します。

# iex -S mix

まずはGRPC.Stub.connect関数でコンテナgo-nodeのgrpcサーバーとの接続を確立します。

iex(1)> {:ok, channel} = GRPC.Stub.connect("go-node:50051")
{:ok,
 %GRPC.Channel{
   adapter: GRPC.Adapter.Gun,
   adapter_payload: %{conn_pid: #PID<0.216.0>},
   cred: nil,
   host: "go-node",
   port: 50051,
   scheme: "http"
 }}

無事接続できました。protoファイルで定義されているsay_hello関数を実行します。

iex(2)> {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(Helloworld.HelloRequest.new(name: "grpc-elixir"))
{:ok, %Helloworld.HelloReply{message: "Hello grpc-elixir"}}

構造体にHello grpc-elixirという文字列が返ってくることが確認できました。

順調に行き過ぎてるように見えるので、接続先を変えてみます。

iex(3)> {:ok, channel2} = GRPC.Stub.connect("localhost:50051")
** (MatchError) no match of right hand side value: {:error, "Error when opening connection: :timeout"}

きちんと、タイムアウトでエラーとなりましたね。

ping代わりのテストなので大したことをやってるわけではないのですが、priv/client.exsを見てわかるとおり、スタブに対してsay_hello関数を呼び出すだけでRPCが実行できる簡単な流れとなっているのがおわかり頂けると思います。

Ctrl+C を2回押してiexから抜けてください。

HelloWorld : Golang -> Elixir

こんどは、Elixir側を待ち受けにしてGolang側からAPIを叩いてみます。

以下でgrpcサーバーが待ち受けとなります。

# mix grpc.server

Ctrl+p, Ctrl+q でコンテナを抜けて、go-nodeのコンテナに入ります。

$ docker-compose exec go-node bash

helloworld/greeter_clientフォルダーに移動します

cd /go/src/google.golang.org/grpc/examples/helloworld/greeter_client

サンプルプログラムはLocalhostのサーバーに対して、リクエストを送るようになっているので宛先をelixir-nodeに変えます。このコンテナにはvimが入ってないので、sedでlocalhostを強制変換して再コンパイルします。

# sed -i -e s/localhost/elixir-node/ main.go
# go build main.go

実行ファイルを起動します。

# ./main
2018/12/10 05:11:17 Greeting: Hello world

無事にElixirとの接続が確認できました。

コンテナを抜け、コンテナを終了させましょう。

# exit
$ docker-compose down

まとめ

protcのプラグインのインストールと、Stubの作成を乗り越えてしまえば、簡単に多言語と通信できることが理解頂けたのではないかと思います。今回は、HelloWorldのみでしたが、次回は1接続でストリーミングを行うコードを含んだRoute Guildサンプルを試してみます。お楽しみに。


 明日のfukuoka.ex Elixir/Phoenix Advent Calendar 2018 12日目の記事は, @tuchiro さんの「BehaviorとMix.Configで切り替え可能なStubを実装する」です。こちらもお楽しみに!

BehaviourとMix.Configで切り替え可能なStubを実装する

(この記事は、「fukuoka.ex Elixir/Phoenix Advent Calendar Advent Calendar 2018」の12日目です)

昨日は @twinbee さんのgrpc-elixirでGoと通信してみる #1でした。

今日は私からElixirでのStub実装のお話です。

Stub実装が欲しくなるケース

業務システムの開発をしていると、よそ様の開発したシステムに連結しなければ処理が実行できない要件などがしばしば登場します。
そうした場合、テスト実行のたびに相手のシステムへアクセスしたり、更新をかけたりすることが望ましくない場合も多く、自動テストの妨げになる事もあります。

そういったケースにおいてAdapterとStubを実装しておく事でテスト時の好ましくない依存関係を解消することができます。
以下で実装の流れを解説します。

プロジェクトを作成する

まずはサンプル実装のためのプロジェクトを実装します。
Elixirのバージョン確認します。

> elixir --version
Erlang/OTP 20 [erts-9.2.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Elixir 1.6.1 (compiled with OTP 20)

Phoenixプロジェクトを作成します。

> mix phx.new stub_sample --no-ecto --no-brunch
> cd stub_sample

mix定義に必要なライブラリを追加してdeps.getします。

mix.exs
def application do
    [
      mod: {StubSample.Application, []},
      extra_applications: [:logger, :runtime_tools, :httpoison] # <- :httpoison追加
    ]
end

defp deps do
    [
      {:phoenix, "~> 1.3.2"},
      {:phoenix_pubsub, "~> 1.0"},
      {:phoenix_html, "~> 2.10"},
      {:phoenix_live_reload, "~> 1.0", only: :dev},
      {:gettext, "~> 0.11"},
      {:cowboy, "~> 1.0"},
      {:poison, "~> 3.0"}, # <- 追加
      {:httpoison, "~> 1.4"}, # <- 追加
    ]
  end
> mix deps.get

まずは直接実装してみる

httpoisonを使ってQiitaのタイトル一覧を取得する簡単な処理を実装してみます。

lib/stub_sample/api_adapters/api_adapter_qiita.ex
defmodule StubSample.ApiAdapter.Qiita do

  def list_items(url) do
    resp = HTTPoison.get! url
    {:ok, contents} = Poison.decode(resp.body)
    Enum.map(contents, fn(content) -> content["title"] end)
  end

end

実行するとタイトル一覧が取得できます。

iex(1)> StubSample.ApiAdapter.Qiita.list_items("https://qiita.com/api/v2/items?page=1&per_page=10&query=Fukuoka.ex")
["【超残念】fukuoka.ex Elixir/Phoenix Advent Calendar 2018参加を断念",
 "[Elixir]Calendarモジュールを使って、DataFormatを作る方法を色々と試す。",
 "grpc-elixirでGoと通信してみる #1",
 "Elixirでニューラルネットワークを実装しようとした話",
 "Elixir(エリクサー)で数値計算すると幸せになれる",
 "プログラマでも「Figma」ならレイアウトやロゴが簡単にデザインできる",
 "\"show weather\" コマンド作ってみた",
 "データサイエンスプラットフォームEsunaの前処理UI/Elixirコードは、入力データ解析の後、データ内容から自動生成されている",
 "RustElixirで線形回帰を高速化した話",
 "【doctestつき】AtCoder に登録したら解くべき精選過去問 10 問を\"Elixir\"で解いてみた"]

AdapterとStubを実装する

先ほどの実装では、Qiitaにアクセスできない状態ではテストができません。
独立したテストができるようにAdapterとStubモジュールを実装していきます。

callbackとしてlist_items()/1を定義したモジュールを実装します。

lib/stub_sample/api_adapters/api_adapter.ex
defmodule StubSample.ApiAdapter do

  @callback list_items(url :: String) :: List

end

Qiitaアダプターにbehaviour定義を追加します。

defmodule StubSample.ApiAdapter.Qiita do

  @behaviour StubSample.ApiAdapter # <- 追記

  def list_items(url) do
    resp = HTTPoison.get! url
    {:ok, contents} = Poison.decode(resp.body)
    Enum.map(contents, fn(content) -> content["title"] end)
  end

end

同じくStubSample.ApiAdapterを定義してlist_items()/1を実装したStubモジュールを実装します。

lib/stub_sample/api_adapters/api_adapter_stub.ex
defmodule StubSample.ApiAdapter.Stub do

  @behaviour StubSample.ApiAdapter

  def list_items(url) do
    ["#{__MODULE__} stub was worked! #{url}"]
  end

end

上記の実装を呼ぶビジネスロジックを想定したモジュールを実装します。

list_itemsを実行するモジュールは直接aliasで定義せず、
configから取得するようにしています。

lib/stub_sample/api_call_sample.ex
defmodule StubSample.ApiCallSample do

  @adapter Application.get_env(:stub_sample, StubSample.ApiAdapter)[:adapter_module]

  def api_call(url) do

    @adapter.list_items(url)

  end

end

Configを定義する

ApiAdapterの実装モジュールとして
devではStubSample.ApiAdapter.Qiitaを、
testではStubSample.ApiAdapter.Stub
を定義します。

config/dev.exs
config :stub_sample, StubSample.ApiAdapter,
  adapter_module: StubSample.ApiAdapter.Qiita
config/test.exs
config :stub_sample, StubSample.ApiAdapter,
  adapter_module: StubSample.ApiAdapter.Stub

実行してみる

まずはMIX_ENVを指定せずに(MIX_ENV=dev)で実行してみるとStubSample.ApiAdapter.Qiitaの処理が実行されます。

> iex -S mix
iex(1)> StubSample.ApiCallSample.api_call("https://qiita.com/api/v2/items?page=1&per_page=10&query=Fukuoka.ex")
["【超残念】fukuoka.ex Elixir/Phoenix Advent Calendar 2018参加を断念",
 "[Elixir]Calendarモジュールを使って、DataFormatを作る方法を色々と試す。",
 "grpc-elixirでGoと通信してみる #1",
 "Elixirでニューラルネットワークを実装しようとした話",
 "Elixir(エリクサー)で数値計算すると幸せになれる",
 "プログラマでも「Figma」ならレイアウトやロゴが簡単にデザインできる",
 "\"show weather\" コマンド作ってみた",
 "データサイエンスプラットフォームEsunaの前処理UI/Elixirコードは、入力データ解析の後、データ内容から自動生成されている",
 "RustElixirで線形回帰を高速化した話",
 "【doctestつき】AtCoder に登録したら解くべき精選過去問 10 問を\"Elixir\"で解いてみた"]

次に、MIX_ENV=test で実行すると、StubSample.ApiAdapter.Stubの処理が実装されます。

> MIX_ENV=test iex -S mix
iex(1)> StubSample.ApiCallSample.api_call("https://qiita.com/api/v2/items?page=1&per_page=10&query=Fukuoka.ex")
["Elixir.StubSample.ApiAdapter.Stub stub was worked! https://qiita.com/api/v2/items?page=1&per_page=10&query=Fukuoka.ex"]

(2018/12/13 実行イメージがStubを直接実行しているケースを記載していたので修正しました。)

まとめ

  • @call_back と @behaviour で振る舞いを定義
  • cofigとApplication.get_env()を使えば環境毎にモジュールを切り替えることができる。

将来的に複数のサービスを環境毎に切り替えるようなケースでもこのパターンは使えるかと思います。

明日のfukuoka.ex Elixir/Phoenix Advent Calendar 2018 14日目の記事は, @kikuyuta さんの階段の上でも下でも電灯を点けたり消したりするです。こちらもお楽しみに!

階段の上でも下でも電灯を点けたり消したりする

(この記事は、fukuoka.ex Elixir/Phoenix Advent Calendar Advent Calendar 2018 の13日目です)

昨日は @tuchiro さんのBehaviourとMix.Configで切り替え可能なStubを実装するでした。

今日は今年取り組んだ Elixir 修行の集大成を書こうと思います。Elixir 修行することになった原因は Elixir を使うようになった経緯 〜電力システム制御の現場から〜 に書きましたので合わせて御覧ください。

題材として電灯のon/offのプログラムを披露します。2階建ての家にはよくある階段の電灯とそれを操作するスイッチです。階段の上と下とのどちらでもONにできてどちらでもOFFにできる… ってよくありますよね。広い部屋の電灯を部屋の両端のスイッチどちらでも操作できるってのもありますね。これ不思議な感じですが電気回路でやるとまあ簡単な回路でできるんです。そういうのをシミュレートするような Elixir プログラムを作ってみましょう。中に色々と小技を入れてみたので、どうぞお楽しみください。

仕様を考える

プログラムの外部仕様はこんなのです。

  • 階段の上と下とにスイッチがある
    • パチパチすると左か右を向いて安定する
    • スイッチ自体には on とか off とかの概念はない
  • 電灯がある
    • スイッチを1回パチンと切り替えると点灯・消灯が切り替わる
    • 階段の上のスイッチでも下のスイッチでも、どちらのスイッチでも操作できる

私の今年の集大成としてこういう技を入れてみてます。

  • プロセスで状態を表現する(GenStateMachineを使いました)
    • スイッチ2つそれぞれがプロセス
    • 電灯を制御するメインもプロセス
  • 大事なプロセスはスーパバイザの下にぶら下げる
    • 今回はメインをぶら下げました

あと、今まで使ったことのない Logger と Registry ライブラリを使ってみました。

入出力について

スイッチも電灯も物理的なデバイスでやりたいのはやまやまなんです。デジタルの入出力しかないので、ラズパイとかでも簡単にできるはず。しかしながら準備が間に合わず。

  • スイッチ操作:コンソールから特定の関数を呼び出す
  • 電灯:コンソールに状態を表示する

というところに留めました。

スイッチとボタン

とずっとここまで スイッチ と書いてきました。がここに来て、途中まで書いてたプログラムでは Button と記載していることに気づきました。しまった!
スミマセンがここからは ボタン と言わせてもらいます。

ちなみにスイッチは切替器なので安定な場所が2つ(ないしもっと)あって、人間の操作でどこかに落ち着くという感じでしょう。今回はこっちなんです。ボタンは安定してる場所があって、人間の操作で不安定な位置に行くけど、操作をやめるともとに戻る… 感じですね。いやいやまずった。

ログ出力とデバッグ出力について

ログやらは今回全部 Logger ライブラリの関数を呼んでます。Elixir の Logger 関数は error, warn, info, debug と4種類のレベルのメッセージを出せます。これを以下のように分類して出力するようにしました。

  • error: 本当にバグってるとき
  • warn: 電灯の点灯/消灯に関するメッセージ
  • info: ボタン操作に関するメッセージ
  • debug: プロセスの起動時に出すメッセージ

なお、Logger は外部ライブラリで拡張することができて、たとえば syslog に出力ができたりします。今回はそこまでやらずにコンソールにログを出力します。

syslog で脱線 (MACOSX と Elixir のライブラリ)

ここで電灯についてのメッセージをなぜ warning 扱いにするかというと、Mac OSX の syslog に吐かせると、info レベル以下を console に出力してくれないからです。ですので、MAC のコンソールでも必ず見ることのできるのは warn と error の2つだけになります。error は本当のエラーに割り当てたいので warn に割り当ててます。

なお MAC の syslog 関係はそれなりに改変されてて、info 以下はコンソールアプリにも出てきませんし、どのファイルにも残せません。いろいろ試したんですが、私の技量では解決できませんでした。普通の UNIX なら syslog で難なく全部のレベルが出せるでしょう。

Logger の分類が syslog の分類より少ないのも不思議です。なんで全部用意しないんでしょうね。大した手間でもないと思うのですが。拡張するのも難しくなさそうに思いますが、時間がないので今回はこのままでやります。

プログラムを作る

では実際に作ったプログラムを披露します。

準備をする

例によって、プログラムを作る前に作業ディレクトリの準備をします。

mix コマンドの実行

最初に作業したいディレクトリに行って mix new --sup stair を実行します。ここで stair は私が勝手に決めてますので、もちろんお好きなのをどうぞ。以降は stair であるものとして書き進めます。

$ mix new --sup stair
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/stair.ex
* creating lib/stair/application.ex
* creating test
* creating test/test_helper.exs
* creating test/stair_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd stair
    mix test

Run "mix help" for more commands.

とたくさん作ってくれました。

mix.exs を確認する

以下を確認してください。特に mod: {Stair.Application, []} が大事です。これは最初にどのモジュールが起動されるかが記述されてます。この例では Stair.Application.start/2 が起動されます。

mix.exs
  def application do
    [
      extra_applications: [:logger],
      mod: {Stair.Application, []}
    ]
  end

mix.exs を編集する

今回は外部ライブラリ(って言うのかな、要は標準じゃないライブラリ)の GenStateMachine を使います。

先程のディレクトリの下に stair ディレクトリができてますので、その下の mix.exs を編集します。1行だけ {:gen_state_machine, "~> 2.0.4"} を依存関係リストに入れてください。編集前と編集後の diff を示します。

$  diff -c mix.exs{.org,}
*** mix.exs.org 2018-12-13 08:11:52.000000000 +0900
--- mix.exs 2018-12-13 08:15:54.000000000 +0900
***************
*** 22,27 ****
--- 22,28 ----
    # Run "mix help deps" to learn about dependencies.
    defp deps do
      [
+       {:gen_state_machine, "~> 2.0.4"}
        # {:dep_from_hexpm, "~> 0.3.0"},
        # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
      ]

これをやったら mix deps.get を実行します。余談ですが、私 mix deps get とやっててハマったことがあります。

$ mix deps.get
Resolving Hex dependencies...
Dependency resolution completed:
New:
  gen_state_machine 2.0.4
* Getting gen_state_machine (Hex package)

うまく持ってこれるようになったようです。準備完了したので順に書いていきましょう。プログラムを lib/stair の下に創っていきます。

アプリケーション application.ex

このモジュールの start が最初に起動されるプログラムです。

application.ex
defmodule Stair.Application do
  use Application # これは雛形にもある。必ず use すること
  require Logger  # ログ出力用

  def start(_type, _args) do
    {:ok, _} = Registry.start_link(:unique, Stair) # プロセスの管理用に Registry を使う
    {:ok, pid} = Stair.Supervisor.start_link(:sweet) # スーパバイザの立ち上げ

    {:ok, _} = Stair.Button.start_link(:upstair)   # 階上のボタンのプロセスを起動
    {:ok, _} = Stair.Button.start_link(:downstair) # 階下のボタンのプロセスを起動
    {:ok, pid} # この関数の戻り値として Supervisor.start_link の戻り値を使うこと
  end
end

注意としては、まずは Registry を起動してください。これは次に立ち上げる Supervisor の中(の中の Main)で使うからです。あと、Supervisor を立ち上げた後にアレコレするので(この場合はボタンプロセスの立上げ)Supervisor の戻り値を pid に一旦バインドして、最後にこの関数自体の戻り値とするようにしてあります。

ログをコンソールに出すのに Logger ライブラリを使います。これはいずれ、ファイルとか syslog にログを吐かせることを意図しています。デバッグ用に IO.puts や IO.inspect を使う人は不要です。

なお本質的ではありませんが、メインのプロセスの名前を :sweet と、階上/階下のボタンのプロセス名を :upstair / :downstair としてあります。このあたりはお好みです。

スーパバイザ supervisor.ex

スーパバイザの振る舞いを記述します。コールバック関数というか監視対象に、処理の本丸の Stair.Main を指示しています。

supervisor.ex
defmodule Stair.Supervisor do
  use Supervisor

  def start_link(house_name) do
    {:ok, _sup} = Supervisor.start_link(__MODULE__, house_name)
  end

  def init(house_name) do
    children = [worker(Stair.Main, [house_name])]
    supervise(children, strategy: :one_for_one)
  end
end

ボタンのプロセス button.ex

階上と階下のボタンは同じ仕様です。application.ex には同じモジュールから2つのプロセスを生成するように記述してあります。

起動時に左に倒れていて、パチパチするたびに右に倒れて、また左に倒れます。パチるたびにメインの flop 関数を叩きに行きます。物理的なデバイスがないので、ここでは flip/1 関数を呼ぶことで、実際の操作に代えています。

button.ex
defmodule Stair.Button do
  use GenStateMachine        # 状態を扱うライブラリ
  require Logger             # ログ出力用

  def start_link(button_name) do # ボタンプロセス起動用です
    Logger.debug("Button ==#{button_name}== starts")
    GenStateMachine.start_link(__MODULE__, button_name, [name: button_name])
  end

  def flip(button_name) do       # ボタンをパチパチするときの動作です
    GenStateMachine.cast(button_name, :flip)
  end

  def get_status(button_name) do # ボタンの状態を見ます。
    GenStateMachine.call(button_name, :get_status)
  end

# ここまでがキレイに見せるためのラッパ関数、ここからがコールバック関数

  def init(button_name) do
    {:ok, :left, button_name} # 初期状態としてはボタンが左側に倒れてます
  end

  def handle_event(:cast, :flip, :left, button_name) do
    Logger.info("button ==#{button_name}== turns to RIGHT")
    Stair.Main.flop()                  # パチとなったらメインに伝えます
    {:next_state, :right, button_name} # 左に倒れてたのをパチると右に倒れます
  end

  def handle_event(:cast, :flip, :right, button_name) do
    Logger.info("button ==#{button_name}== turns to LEFT")
    Stair.Main.flop()                  # パチとなったらメインに伝えます
    {:next_state, :left, button_name}  # 右に倒れてたのをパチると左に倒れます
  end

  def handle_event(:cast, ope, state, button_name) do # ここには来ないはず
    Logger.error("#{button_name} illegal button operation: #{ope}")
    {:next_state, state, button_name}
  end

  def handle_event({:call, from}, :get_status, state, button_name) do # 状態を見るために使います
    {:next_state, state, button_name, [{:reply, from, {state, button_name}}]}
  end
end

get_status/1 や handle_event で :call を受け取ってるのは、実行中のデバッグ用で、今回は使いません。

ランプを点灯消灯するメインモジュール main.ex

ここまで↑はまずまずシンプルでした。これだけちょっと複雑になってます。

main.ex
defmodule Stair.Main do
  use GenStateMachine    # 状態を扱うライブラリ
  require Logger         # ログ出力用

  def start_link(house_name) do # メインの起動
    Logger.debug("#{__MODULE__} started named #{house_name}")
    GenStateMachine.start_link(__MODULE__, house_name, [name: house_name])
  end

  def change_state(house_name, event) do # デバッグ用:状態を手動で変更する
    GenStateMachine.cast(house_name, event)
  end

  def get_status(house_name) do          # デバッグ用:状態を見るために使います
    GenStateMachine.call(house_name, :get_status)
  end

  def flop() do # ボタンをパチると呼ばれる関数(詳細は後述)
    Registry.dispatch(Stair, __MODULE__, fn entries ->
      for {pid, _ignore} <- entries,
        do: GenServer.cast(pid, :change_lamp_state) end)
  end

# ここまでがキレイに見せるためのラッパ関数、ここからがコールバック関数

  def init(house_name) do
    Registry.register(Stair, __MODULE__, []) # プロセスを登録。上の flop/0 関数で使用する
    Logger.warn("Lamp is DARK")
    {:ok, :off, house_name}            # 電灯は初期状態では消灯している
  end

  def handle_event(:cast, :change_lamp_state, :off, house_name) do
    Logger.warn("Lamp becomes BRIGHT") # デバイスの状態を変更する
    {:next_state, :on, house_name}     # :off 状態から変化があると :on に
  end

  def handle_event(:cast, :change_lamp_state, :on, house_name) do
    Logger.warn("Lamp becomes DARK")   # デバイスの状態を変更する
    {:next_state, :off, house_name}    # :on 状態から変化があると :off に
  end

  def handle_event(:cast, event, state, house_name) do # ここには来ないはず
    Logger.error("No such transition #{event} at #{state} in #{house_name}")
    {:next_state, state, house_name}
  end

  def handle_event({:call, from}, :get_status, state, house_name) do
    {:next_state, state, house_name, [{:reply, from, {state, house_name}}]}
  end
end

Logger.warn("Lamp becomes BRIGHT")Logger.warn("Lamp becomes DARK") とある部分、今回は実際にはライトを点灯するデバイスがないので、代わりにこのデバッグプリントをしています。もしそういう物理デバイスがあるのなら、それを操作する関数をここで呼べば良いです。

なお change_lamp_state/2 はデバッグ時に手動で状態を変えるための関数で、今回は使いません。あと、ここでも get_status/1 や handle_event で :call を受け取ってるのは、実行中のデバッグ用で、今回は使いません。

プロセスを管理するのに Registry を使う

ここで、ちょっとだけ工夫した点があります。(良い工夫なのかちょっとわかりませんが。)

ボタンがパチられたとき、メインの関数を叩いて電灯の off/on の制御を要請します。ここなんですが、ボタン側はメインのプロセスとは独立して起動されてるプロセスです。メイン側の関数を叩くときに main.ex でどのような引数で start_link/2 をしたのかはわかりません。

具体的に言うと、handle_event/4 関数の最後の引数(ここでは house_name)がわからないので、呼べないのです。しかし、Button モジュールでは大域変数も持ってませんし、main を呼び出すためにずっと関数の引数に house_name を持ち回るのも冗長な感じがします。そこで用いたのが標準ライブラリにある Registry です。

main.ex
  def flop() do
    Registry.dispatch(Stair, __MODULE__, fn entries ->
      for {pid, _ignore} <- entries,
        do: GenServer.cast(pid, :change_lamp_state) end)
  end

  def init(house_name) do
    Registry.register(Stair, __MODULE__, [])
    Logger.warn("Lamp is DARK")
    {:ok, :off, house_name}
  end

全体の起動時に application.ex で {:ok, _} = Registry.start_link(:unique, Stair) を実行しています。これで Stair というレジストリが生成されています。
次に Main のプロセスの起動時にコールバックの init/1 が呼ばれます。ここで Registry.register(Stair, __MODULE__, []) を実行して、レジストリに自分自身を登録します。登録名はこのモジュール名すなわち Stair.Main です。
こうしておくと(名前さえ覚えておけば)後で Registry.dispatch/3 関数によって登録されてるプロセスの関数を呼び出すことができます。この例では Main.flop/0 です。このように記述すると、登録されてるプロセス全てに対して無名関数を適用できます。

実行してみる

では親ディレクトリに戻って実行してみましょう。

$ iex -S mix # 起動します
Erlang/OTP 21 [erts-10.1.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]


14:54:53.756 [debug] Elixir.Stair.Main started named sweet

14:54:53.759 [warn]  Lamp is DARK

14:54:53.760 [debug] Button ==upstair== starts

14:54:53.760 [debug] Button ==downstair== starts
Interactive Elixir (1.7.4) - press Ctrl+C to exit (type h() ENTER for help)

実行すると、デバッグプリントでメインと階上/階下のボタンのプロセスが起動したのが分かります。電灯は消灯状態です。パチパチをしてみましょう。

iex(1)> Stair.Button.flip(:downstair) # 階下でパチると点灯

14:55:41.668 [info]  button ==downstair== turns to RIGHT

14:55:41.668 [warn]  Lamp becomes BRIGHT
:ok
iex(2)> Stair.Button.flip(:downstair) # 階下でもう一回パチると消灯
:ok

14:55:45.851 [info]  button ==downstair== turns to LEFT

14:55:45.851 [warn]  Lamp becomes DARK
iex(3)> Stair.Button.flip(:upstair)   # 階上でパチっても点灯できる
:ok

14:55:51.127 [info]  button ==upstair== turns to RIGHT
iex(4)> 
14:55:51.127 [warn]  Lamp becomes BRIGHT
iex(4)> Stair.Button.flip(:upstair)   # 階上でも2回パチると消灯

14:55:53.058 [info]  button ==upstair== turns to LEFT
:ok

14:55:53.058 [warn]  Lamp becomes DARK

ここまでは同じ場所のボタンをパチってます。これが場所が違っても動くのかの確認をします。

iex(5)> Stair.Button.flip(:downstair) # 階下で点灯して…
:ok

14:55:56.499 [info]  button ==downstair== turns to RIGHT

14:55:56.499 [warn]  Lamp becomes BRIGHT
iex(6)> Stair.Button.flip(:upstair)   # 階上で消灯もできます
:ok

14:55:58.387 [info]  button ==upstair== turns to RIGHT

14:55:58.387 [warn]  Lamp becomes DARK
iex(7)> 

うまく動いてるようです。

考察

お見せしてるのは完成したやつで、途中の紆余曲折が見えないです。ここでは自分の思考履歴を思い出しながら、ポイントをまとめてみます。

有限状態機械について

今回は動作が簡単だったので、状態遷移と言っても、ボタンも本体もそれぞれ2状態しか持ちませんでした。本体側ではボタンの状態を持たないように作ったこともあり、本当に簡単なアルゴリズムに落ち着いています。

これがもう少し複雑になってきたら、処理をどのモジュールに持たせるか、あるいは物理デバイスとは別のモジュールに持たせるかとか、設計で頭をひねることになるかと思います。

分散環境

今回はボタンも電灯も一つのアプリケーション下にありました。これ、複数のアプリケーションや複数のプロセッサやさらに複数のホストで独立にプロセスが動くと素敵です。ちょっと実験はしてるのですが、次に書いてるプロセスの名前空間問題に引っかかって、キレイなプログラムに落とせてないです。来年の早いうちに扱えるようになりたいです。

プロセスの空間

今回何に一番ハマって苦しんだかというとプロセスへのアクセスです。別々に起動してしまったプロセス間でどうやりとりをするか。言うなればプロセスIDを持ってフラットな空間に散らばってしまってるので、なにか取っ掛かりを持ってないとなりません。{:ok, _pid} = ... なんてやった瞬間にプロセスにアクセスする鍵を捨ててしまってます。とはいえ、このプロセスID情報を持ってても、じゃあどれで持って管理するんだという問題が出ますし、異なるアプリケーションや異なるホストでプロセスを立ち上げたら、それにはどうやってアクセスするのかという問題が残ります。

これ、プロセスツリー、すなわちプロセスの呼出し/呼出され関係によってアプリケーション内にユニークな名前を振るのはできないことはなさそうです。しかしながら、Elixir(というか Erlang)のプロセスの構造に合わせると、そう望むものができるようには思えません。例えばロバスト性を重視して Supervisor を使うなら、そこはできるだけ簡単にして、フラットな構造でプロセスを作りたくなります。プロセス作って、それの子プロセス作って、さらに… とやってそれに準じる名前空間を持ってもあまりハッピーにならないでしょう。

Registry ライブラリを知る前には、複数の要素(ここでなら house_name と button_name)からアトムを構成する関数を作ったりしました。例えば :sweet と :downstair から :sweet_downstair というアトムを作って、ボタンのプロセスを生成するとかです。しかしこれはうまくない。アトムについている文字列に構造をもたせるので、くっつけたりバラしたり、プログラムが汚くなり読解性が悪くなります。

結局は Registry ライブラリを見つけてそれで実装しました。ただし、万事うまく解決したわけではありません。今回は Stair.Main のプロセスにアクセスするのに Stair.Main.flop/0 という関数を使ってます。これ、Stair.Main に閉じてるふうに書いていますが、じつは全然閉じてなくて、Registry を使えばどのプロセスからでも Stair.Main のプロセスを刺激できます。今回は Register 対象の名前にモジュール名を使って、かつ Stair.Main にしか書かなかったのでなんとなく閉じてる風にはできてます。

これは Elixir のアプローチがどうこう以前の根本的な問題です。UNIX のプロセスにしたって、独立した複数のプロセスに寄るプロセス間通信するときには何らかの共通な情報をプロセス同士で交換しないとなりません。Elixir のプロセスでも同じことです。関数型言語のきれいな環境に状態やら、それを表現するためのプロセスやら、入れてしまってるので、そこはグローバルフラットな空間でどう互いを扱うか問題が存在しているというところです。

まだ Registry ライブラリに触れて間もないので、もう少し習熟すると良いことがあるかもしれません。続きはまた今度。

おわりに

簡単な制御装置を Elixir を使って書いてみました。すべてのデバイスをモノごとに独立した Elixir プロセスとしてます。一番重要そうなのは一応スーパバイザ監視下においてます。プロセスから別のプロセスを叩くときに Registry を使ってみました。

まあ、Elixir 漬けになって半年。まずまずなんじゃないでしょうか。楽しんでいただけましたでしょうか。ただ、どうにもまだまだ修行が足りてない感が満載なので、「こう書いたらいいんじゃね?」ってなコメント、募集中です。

明日のfukuoka.ex Elixir/Phoenix Advent Calendar 2018 14日目の記事は, @kotar0 さんのLiveViewというウェブアプリを作る第三の選択肢です。こちらもお楽しみに!

参考

ことしの七転八起というか七転八倒というか、こちらの下にまとめてあります。
はじめてのElixir(0)

今回使った主なマニュアル類はこちら。
GenStateMachine
Elixir v1.7.4 GenServer
Elixir v1.7.4 Supervisor
Elixir v1.7.4 Logger
Elixir v1.7.4 Registry

Registryについては公式ドキュメントに加えてこちらが参考になるかと。
Elixir標準ライブラリRegistryを使ったPub/Sub by @niku さん
Registry について by @mururu さん

LiveViewというウェブアプリを作る第三の選択肢

(この記事は、fukuoka.ex Elixir/Phoenix Advent Calendar Advent Calendar 2018 の14日目です)

昨日は@kikuyutaさんの階段の上でも下でも電灯を点けたり消したりするでした。


LiveViewとはElixirConf2018でのKeynote中に発表された機能

Phoenixに将来搭載される機能として、ElixirConf2018で発表されました。
大まかにいうと、
「Elixirでフロントエンドのバリデーションとかアニメーションと書けちゃう。しかもリアルタイムに。」
っていう機能です。

ElixirConf2018が開かれたは2018年9月ですが
よく理解しないままになっていたのでこの機会に調査してみました。

ElixirConf2018 キーノート
ElixirConf 2018 - Keynote - Chris McCord

LiveViewをわかりやすく説明した記事。今回の記事のソースはほぼこれ。
Phoenix’s LiveView: Client-Side Elixir At Last?

今のウェブアプリの手法

昨今は新しくウェブアプリケーションを開発し始めるのであれば、
SPAはデファクトスタンダードのようになってると思います。
その背景には、ページ全体の再描画を伴わないページ遷移やページ内の部分的な再描画により、
リッチなUXをユーザーに提供できることがあります。
しかし一方で、SPAアプリケーションを組むとなるとフロントエンドの複雑度はあがり、
サポートするフレームワークの利用もほぼ必須となります。

バックエンドとフロントエンドどちらも1人のエンジニアが横断してみるという場合には、バックエンドの実装に加え、SPAのフロントエンドの実装も行うとなると、それは大変です。

そこで、第三の選択肢として、Phoenix.LiveViewを選ぶと良いとのことです。
立ち位置的には、SSRとSPAの間に位置すると言われています。

file-L6iTkejTl.png

フロントの処理をElixirで書ける

Phoenixで開発をしていて、言語を変えずにそのままフロントエンドのイベントに紐づく処理を書くことが出来ます。
たとえば、下記のコードはボタンを押したらインクリメントされたりデクリメントされます。
phx-{イベント名} でボタンクリックでエリクサーで書いた関数が動いちゃってます。
説明とDemo

Screen Shot 2018-12-14 at 21.23.18.png
Screen Shot 2018-12-14 at 21.26.13.png

これいいな~って思ったのが、バリデーションです。
フロントエンドでも、バックエンドでもバリデーションの役割は違えど、同じようなロジックのバリデーションも、それぞれ組むことがあると思います。
LiveViewではエリクサーで書いたバリデーションがそのまま使えるので、別の言語で2度書く必要がありません。

リアルタイム更新

フロントエンドの状態更新にWebSocketを使用しているので、
リアルタイムで更新されます。
別タブで開いてるページ内の情報も更新されます。
デモでは、2つタブを開いていて片方から、もう片方が見ている情報を削除しちゃってました。
情報を見ている方では消したタイミングで、ちゃんとブラウザリロード無しに「情報はありません」
状態になっていました。
Demo

エリクサーならではの利点、パフォーマンス

パフォーマンスに関することも話されていました。
ここでは、大量のdivタグを操作するアニメーションがDemoされていました。 
divタグのパラメーターの状態はバックエンドからプッシュするというものです。
このような場合でも、60FPSでヌルヌル動作するとのことです。

DemoScreen Shot 2018-12-14 at 21.42.29.png

選択する場合・選択しない場合

SPAと比較してメリット・デメリットがあるから案件の特性に合わせて選ぶことが大切とのこと。

メリット

  • 言語を単一でできることから、生産性向上が見込める
  • だから早くアプリをリリースできる
  • SPAほど大きいJSファイルではないので、ペイロードが小さい
  • SSRだからSEOに強い

デメリット

  • PWAにしてしかも、オフライン時でも使えるようにという用途には対応出来ない
  • ネイティブ機能(具体的にどの機能かはわかってない)を使うときや、複雑なUIの場合はSPAが良い

Screen Shot 2018-12-14 at 21.46.23.png

まだ使えない。追加ライブラリとしての開発されているそう。

まだ公開はされておらず、残念ながら試すことは出来ません。
Phoenixに同梱されて配布されるのではなく、Phoenixに追加するライブラリのような形で公開されるそうです。
しかし、触れる日が待ち遠しい機能です!


 明日のfukuoka.ex Elixir/Phoenix Advent Calendar 2018 15日目の記事は, @zacky1972 さんの「ZEAM開発ログ2018年総集編その2: Elixir 研究構想についてふりかえる(後編)」です。こちらもお楽しみに!

ZEAM開発ログ2018年総集編その2: Elixir 研究構想についてふりかえる(後編)

(この記事は「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」の15日目です)

ZACKY こと山崎進です。

「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」の14日目は, @kotar0 さんの「LiveViewというウェブアプリを作る第三の選択肢」でした。

さて,「ZEAM開発ログ2018年総集編その1: Elixir 研究構想についてふりかえる(前編)」では,下記の研究構想についてご紹介しました。

  • Hastega(ヘイスガ): 超並列高速実行処理系
  • micro Elixir / ZEAM: コード生成/実行基盤

今回の記事「ZEAM開発ログ2018年総集編その2: Elixir 研究構想についてふりかえる(後編) 」では,残りの下記の研究構想についてご紹介します。

  • AI/ML/各種数学ライブラリ
  • Sabotender(サボテンダー): 省メモリ並行プログラミング機構

AI/ML/各種数学ライブラリ

平成30年度 FAIS 新成長戦略推進研究開発事業 シーズ創出・実用性検証事業の研究助成金をいただいて「並列プログラミング言語 Elixir (エリクサー) における数学/AI/ML 基礎ライブラリの開発」を進めております。この研究では次のことを進めています。

  1. 線形回帰とニューラルネットワークとそれらに必要な数学ライブラリの Elixir 実装の開発
  2. 超並列高速実行処理系 Hastega の研究開発
  3. 1 に 2 を適用した時の性能検証プロトタイプの実装と性能評価
  4. 機械学習を用いた防災システム(光陽無線との共同研究開発)への 1,2 の適用の検討

現在表明できる研究開発内容については下記の記事にまとめています。

AI/ML/各種数学ライブラリ
|> 並列プログラミング言語 Elixir (エリクサー)を用いた機械学習ツールチェーン
|> Elixir(エリクサー)で数値計算すると幸せになれる

Sabotender (サボテンダー)

Sabotender は micro Elixir / ZEAM に搭載する予定の省メモリ並行プログラミング機構です。発祥としては,私たちが 2016年(Elixir研究を始めるより前)から進めていた Zackernel (ザッカーネル) の研究が原点です。

Zackernel の研究を始めてから Elixir の研究をスタートさせるまでの経緯と,Zackernel の原理については「ZEAM開発ログ2018ふりかえり第1巻(黎明編): 2017年秋の出会いから2018年2月にElixirを始めるに至った経緯について」に詳しく書きました。

また,2018年に Zackernel の省メモリ性能の評価と,Node.js・Zackernelと同じコールバック方式をネイティブコードを使わずに Elixir のみで実装した軽量コールバックスレッド(LCB)の実装と省メモリ性能の評価を行いました。その研究成果は次の通りです。

Nodeプログラミングモデルを活用したC++およびElixirの実行環境の実装

Nodeプログラミングモデルを活用したC++およびElixirの実行環境の実装

Nodeプログラミングモデルを活用したC++およびElixirの実行環境の実装

Zackernel は1スレッドあたり約200バイトという省メモリ性能を達成しました。これに対し LCB は1スレッドあたり約1.3KBでした。比較対象となる Elixir プロセスは1プロセスあたり約2.8KB,C++11 スレッドは1スレッドあたり約546KBでした。

さらに学生が取り組んだ研究では,Zackernel のアプローチでさらに省メモリ性を追求すると1スレッドあたり50バイト強の省メモリ性を達成できる可能性があることが示唆されました。

以上を踏まえた時に,Elixir における省メモリ並行プログラミング機構としては,LCB 方式,すなわち Erlang VM をそのまま用いた Elixir プログラミングによって実現するよりも,Zackernel 方式,すなわち並行プログラミング機構をネイティブコードで実装して Elixir から利用できるようにした方が,より省メモリ性能を追求できるということがわかりました。そこで,現在新規研究開発中の micro Elixir / ZEAM に Zackernel 方式の並行プログラミング機構を搭載する方向で研究開発を進めています。これが Sabotender (サボテンダー) です。

Sabotender も,Elixir や Phoenix,Esuna,Hastega などと同様,ファイナルファンタジー由来の名称です。ファイナルファンタジーのサボテンダーはモンスター・召喚獣の一種で,非常に素早く攻撃されてもなかなか当たらない,針を1,000本飛ばす攻撃技を持つ,といった特徴を備えています。このような特徴が省メモリ並行プログラミング機構のイメージに合致したので,Sabotender と命名しました。

Sabotender では,次のような研究開発目標を掲げています。

  • 省メモリ: Zackernel 方式の採用により1タスクあたり200バイト程度もしくは200バイトを切る省メモリ性能を備える
  • 同時セッション接続数: Phoenix に適用した時に従来の10倍程度以上の同時セッション接続数を達成する
  • コンテキストスイッチの効率性: Zackernel 方式の追求により従来方式よりも高速にコンテキストスイッチをできるようにする
  • マルチコアCPUの効率性: マルチコアCPUで効率よく実行できるような設計にする
  • キャッシュメモリの効率性: キャッシュメモリの利用効率を向上させるスケジューリングとコンテキストスイッチ時のプリフェッチを実装する
  • スケジューリングの効率性: Hastega と連携した 実行時間推定に基づくスケジューリング最適化機構を実装する
  • 耐障害性: GC を含むメモリ管理機構との連携と GenServer 互換の API の提供により,従来の Erlang VM で達成していた高い耐障害性を維持する
  • イベント処理能力: アクターモデルに基づく並行プログラミングスタイルを維持したまま,従来の Erlang VM で起こったイベントキューが「詰まる」問題を抜本的に改善する
  • 安全性: モデル検査と型検査などを駆使して,処理系の不具合によるデッドロックや不公平性,型不一致を防止する

それぞれ説明します。

省メモリ

Nodeプログラミングモデルに基づくコールバック方式のマルチタスク機構をネイティブコードで一から実装することで,Zackernel で達成できた1スレッドあたり約200バイトの省メモリ性能と同等以上の省メモリ性能を追求します。これにより従来の10倍以上の省メモリが達成できると考えています。

同時セッション接続数

Sabotender で従来の10倍以上の省メモリが達成できることにより,Sabotender を Phoenix に適用した時に,従来の10倍程度以上の同時セッション接続数を達成することができると期待しています。

コンテキストスイッチの効率性

従来方式では,コンテキストスイッチをするときにレジスタの退避やメモリ空間の切り替えなどを行う必要があります。

これに対し Sabotender では,コンテキストが関数へのコールバックとして表現されること,プリエンプションが無くコールバック単位での擬似プリエンプションであることから,各コールバックでレジスタ生存期間が完結するのでコンテキストスイッチのためにレジスタを退避する必要がないと考えています。

また,Sabotender では,メモリ空間の切り替えは関数の実行に必要な実行時環境の切り替えとして表現されます。

マルチコアCPUの効率性

現状でも数コア〜数十コアのオーダーのマルチコアCPUが市販されており,将来的には数百コア〜数千コアのオーダーのマルチコアCPUが実現する可能性もあると考えています。

Sabotender は,このようなマルチコアCPUでの運用を前提とし,効率よくマルチコアCPUを活用できるような設計,すなわち,従来の Erlang VM の設計で追求されてきたように,コア間の同期・排他制御を極力しないで済むような設計を追求します。たとえば Erlang VM では,プロセスごとに独立したメモリ管理を行う分散メモリ方式や,コアごとに独立したスケジューラーを採用することで,コア間の同期・排他制御を行う状況を排除しています。Sabotender でも,このような設計思想を継承し,徹底的にコア間の同期・排他制御を削減します。

またコア数が数十以上になってくると,計算量も問題になってきます。O(n)以上の計算量のアルゴリズムではなく,たとえばO(log n)のアルゴリズムの採用も必要になってきます。どうしても必要なコア間の同期・排他制御については,計算量の少ないアルゴリズムの採用を進めます。

キャッシュメモリの効率性

CPUとメモリの速度差が大きく開いている現状では,キャッシュメモリの効率化が高速化のためにとても重要になっています。

そこで,micro Elixir / ZEAM では,超インライン展開を行い,ある関数の中で必要になるメモリのプリフェッチを,その関数を実行する以前にスケジューリングすることで,キャッシュメモリの効率化を図ろうとしています。

この考え方を Sabotender にも適用したいと考えています。すなわち,コンテキストスイッチを行う以前に,コンテキストスイッチ後に必要となるメモリのプリフェッチをスケジューリングすることができないかを検討します。

このような最適化は,従来のプリエンプティブかつ動的にスケジューリングするマルチタスクではほぼ実現不可能です。これに対し,Sabotender ではコールバック単位の粒度でコンテキストスイッチするため,本質的にはノンプリエンプティブであり,ある程度静的にスケジューリングできる余地があります。このことを利用し,コンテキストスイッチの前後でメモリのプリフェッチの最適スケジューリングを図りたいと考えています。

スケジューリングの効率性

停止性問題,すなわち「任意のプログラムが有限時間内で終了(停止)するかどうかを,アルゴリズムによって証明することはできない」という定理が存在することにより,任意のプログラムについて,そのプログラムを実行するのにどのくらいの時間がかかるのかを推定することは不可能だとされていました。

しかし,MapReduce プログラミングスタイルを徹底し再帰呼出しを禁止することで,有限の長さのデータを走査するプログラム片を帰納的に構成したプログラムに限定できます。そのようなプログラムは有限時間内で終了することを証明できると考えられるので,実用的な範囲で停止性問題の限界を超えることができます。

また,Stream を用いると MapReduce プログラミングスタイルでも無限長のデータを操作するプログラムを記述できますが,有限時間内で終了するのかどうかについて安全かつ近似的な判定をすることは比較的容易ではないかと考えられます。

これらの考え方を応用すると,機械語命令の実行時間の数理モデルとデータ長が与えられた時に,プログラムの実行時間をデータ長の関数として推定することが可能なのではないかと考えています。これらを取りまとめて実行時間推定を設計・実装したいと考えています。

実行時間推定により,コールバック関数で表される各タスクの所要時間を推定することができます。これをもとに静的スケジューリングを行うことで,スケジューリングを最適化できる可能性が拓けてきます。

また Hastega と連携することで,CPU コアだけでなく,GPU も統合してスケジューリングを最適化できると考えています。

実行時間推定に基づく静的スケジューリングにより,従来のマルチプロセス/マルチスレッド方式の動的スケジューリングでは実現が困難だった,ハードリアルタイム性の保証や高度な実行効率の最適化,コンテキストをまたぐプリフェッチによるキャッシュメモリの効率化などが可能になるのではないかと期待しています。

耐障害性

Erlang VM の耐障害性を支える設計方針の1つとして,プロセスごとに独立したメモリ管理機構と GenServer によるプロセス管理機構の採用があります。

プロセスごとに独立したメモリ管理により,プロセスを再起動することで,たとえメモリのリークや不整合が起きていたとしても,プロセス単位でメモリの状態をリセットすることができます。また,いわゆる Stop the world すなわち Full GC がかかることによる全プログラムの一斉停止を避けることが可能になります。

また,GenServer によるプロセス管理機構により,各プロセスが正常に機能しているのか異常状態なのかを監視し,プロセスに異常が起こった時にエラーログに記録してプロセスを再起動するなどの適切な例外処理を行うことができます。

これらの特徴により,Elixir / Phoenix は高い耐障害性を実現しています。

Sabotender / micro Elixir / ZEAM でも,このような設計の思想と方針を継承し,Sabotender の導入に合わせて改良を重ねることで,高い耐障害性を維持・向上したいと考えています。

イベント処理能力

Elixir はアクターモデルに基づく並行プログラミングモデルを採用しています。アクターモデルの採用により,各プロセスの中では単一スレッドによる実行に集約され,同期・排他制御を行う必要がありません。Elixir の設計思想として,1つの資源の管理を1つの資源管理プロセスに集約し,資源に対するアクセスを資源管理プロセスに対する通信で表現することで,資源ごとの同期・排他制御を不要にしています。これにより,Elixir は効率の良い並行・並列処理を可能にしています。

しかし,その代償として,通信が集中する資源管理プロセスには慢性的に処理待ちのイベントが蓄積されやすく,イベントキューが「詰まって」処理が滞る不具合がしばしば発生します。

Sabotender で実現しようとしているのは,イベントキューが「詰まる」前に,アクセスが集中する資源管理プロセスを Sabotender によって多重化することで,処理が滞ることを未然に防止することです。しかも,アクターモデルに基づくプログラミングスタイルを堅持したままで,処理の多重化を実現しようと考えています。

そのためには,Node のように,資源への同期アクセスを徹底的に廃し,すべて非同期アクセスで統一することが肝要だと考えています。非同期アクセスを徹底するために構文を用意し,非同期アクセスの構文を活用した静的解析と最適化を進める方針を採ります。

また,アクセスの集中をできるだけ早く,できれば事前に予測して,適切にコアを割り当てて負荷分散を図ることも有効だと考えています。アクセス集中の予測のため,キャッシュメモリ同様に,資源アクセスのプリフェッチに相当する擬似命令を用意して,超インライン展開でプリフェッチ命令をスケジューリングするアプローチを検討しています。資源プリフェッチ擬似命令の存在により,事前にコアをスケジューリングする計画を立てることができるようになり,コンテキストスイッチをまたぐ資源プリフェッチのスケジューリングも可能性が拓け,アクセスが集中する前にコアをあらかじめ配置して備えることができるようになるでしょう。

さらに,資源管理プロセスと,資源管理プロセスへの通信を対応づけて印づけをすることで,プロセスのスケジューリングを行う際にアクセスが集中することが予測されるプロセスを優先的に多めの実行時間が割り当てられるようにスケジューリングすることも考えられます。

資源アクセスプリフェッチ擬似命令の発行に際し,前述の非同期アクセスと巧みに連携する形で実装できれば良いのではないかと思います。

以上のようなアプローチでイベント処理能力の向上を図りたいと考えています。

安全性

このような高度な処理系を信頼できるものにするためには,処理系自体に不具合が織り込まれない仕組みに設計することが肝要です。

想定される不具合としては,データ型の不一致による問題のほかに,並列プログラミングに起因するデッドロックや不公平性といった不具合が考えられます。

データ型の不一致などの問題については,型理論によって型安全性を追求するアプローチや,Alloy のような反例を例示することによるアプローチが有効です。また,並列プログラミングに起因する問題については,SPIN などのモデル検査のようなアプローチが有効です。

Sabotender の研究開発にあたり,このような理論的アプローチを積極的に導入して進めたいと考えています。

おわりに

2回にわたって私たちfukuoka.ex の Elixir 研究構想について紹介してきましたがいかがだったでしょうか? 近日中にfukuoka.exポータルサイトにて研究紹介のページを立ち上げますので,お楽しみに!

次に私がアドベントカレンダー記事を書くのは「FPGA Advent Calendar 2018」17日目の「RISC-V on FPGA と Elixir で究極のマルチコアシステムを構築しよう!」です。お楽しみに!

明日の「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」16日目は, @callmekohei さんの「BashScript を Elixir で書き直してみたっ(2倍速〜)」です。こちらもお楽しみに!

BashScript を Elixir で書き直してみたっ(2倍速〜)


(この記事は「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」の15日目です)

昨日は@zacky1972さんの
ZEAM開発ログ2018年総集編その2: Elixir 研究構想についてふりかえる(後編)
でした!


この記事は「古い写真のアルバムをスマホで見るようにしたお話」の後日談です





IMG_8367.jpeg

Summary

Exifデーターを編集するBashScriptのコードをElixirで書いてみました
実行時間が約半分になったよ

実行時間の比較

Elixir

$ time elixir foo.exs

real    5m48.747s
user    0m2.563s
sys     0m1.753s

BashScript

$ time bash foo.bash

real    9m2.675s
user    7m19.833s
sys     1m14.571s

Motivation

「古い写真のアルバムをスマホで見るようにしたお話」で写真を整理するためにBashScriptを書いたのですが結構遅かったので、もう少し早くならないかな〜と思ったので、、、

ってElixirってなに?

なんでしょう? callmekoheiもよくはElixirを知りません(すいませんっ)

今時点callmekoheiElixirについて知っていることは

  1. スクリプト言語だよ(手軽に書けるよ)
  2. 左から右にるるるる〜という言語だよ(|>があるよ)
  3. F#という言語にすごく似てるよ

ぐらいですっ

ってなんでElixir?

しょっちゅうTwitterのタイムラインに流れてくるからです

@piacere_ex さんというかたをフォローしたら怒涛のごとくElixirの情報が流れてきました(笑)

もくもく会というのがあって先日参加させていただいたらElixirに興味が出てきました!

fukuoka ex のもくもく会はここを覗いてみてください!
---> fukuokaex もくもく会

Elixir で Exif データーを編集してみる

F#を横目にElixirで「こんにちは世界」Elixirの「こんにちは世界」ができたのでさっそく実際にコード書いてみました!

古い写真のアルバムをスマホで見るようにしたお話 05-05 BashScriptで連番振りスクリプト書いてみた で書いたBashScriptのコードをElixirで書き直してみました!(もっといい方法があるかもしれません・・・・)

defmodule Foo do

  def sortedFileNameList( fp ) do

    exifToolParam = [
      "-quiet",
      "-fast2",
      "-s3",
      "-filename",
      "-fileorder datetimeoriginal",
      "-fileorder filename"
    ]

    bashCommand = [
      "-c",
      "cd " <> fp <> " ; echo *jpg | xargs exiftool " <> Enum.join( exifToolParam , " " )
    ]

    System.cmd("bash" , bashCommand )
    |> elem(0)
    |> ( fn x -> String.split(x ,"\n") end ).()
    |> Enum.drop( -1 )

  end

  def resetAllTime( fp ) do

    exifToolParam = [
      "-quiet",
      "-fast2",
      "-s3",
      "-d '%Y:%m:%d 00:00:00'",
      "-overwrite_original_in_place",
      "\"-alldates<alldates\""
    ]

    bashCommand = [
      "-c",
      "cd " <> fp <> " ; echo *jpg | xargs exiftool " <> Enum.join( exifToolParam , " " )
    ]

    System.cmd("bash" , bashCommand )
    |> elem(0)

  end

  def addOneSecond( fp ) do

    exifToolParam = [
      "cd " <> fp <> " ; exiftool",
      "-quiet",
      "-fast2",
      "-s3",
      "-overwrite_original_in_place -alldates+=0:0:"
    ]

    exifToolParam2 =
      Foo.sortedFileNameList( fp )
      |> Enum.with_index()
      |> Enum.map( fn { filename , idx } ->
        Enum.join( exifToolParam, " ") <> Kernel.inspect(idx) <> " " <> filename
      end )

    exifToolParam2
    |> Enum.each( &( System.cmd("bash",["-c" , &1]) ))

  end

  # 確認用
  def myCheck( fp ) do

    exifToolParam = [
      "-quiet",
      "-fast2",
      "-s3",
      "-filename",
      "-datetimeoriginal",
      "-fileorder datetimeoriginal",
      "-fileorder filename"
    ]

    bashCommand = [
      "-c",
      "cd " <> fp <> " ; echo *jpg | xargs exiftool " <> Enum.join( exifToolParam , " " )
    ]

    System.cmd("bash" , bashCommand )
    |> elem(0)
    |> ( fn x -> String.split(x ,"\n") end ).()
    |> ( fn lst -> lst -- [""] end ).()

  end

  def jpgFolderList(fp) do
      Path.wildcard( Path.expand( fp ) <> "/**/*.jpg" )
      |> Enum.map( &( Path.dirname( &1 )))
      |> Enum.uniq
  end

  def mainImpl( foo ) do

    " sortedFileNameList " |> IO.puts
    Foo.sortedFileNameList(foo)

    " resetAllTime " |> IO.puts
    Foo.resetAllTime(foo)

    " addOneSecond " |> IO.puts
    Foo.addOneSecond(foo)

    " myCheck " |> IO.puts
    Foo.myCheck(foo) |> Enum.each( &( IO.puts( &1 )))

  end

  # 写真データーのExifデーターを変更できるようにするために権限を変える
  def chmod644(fld) do
    System.cmd("bash" , ["-c", "cd " <> fld <> " ;  chmod 644 *jpg"] )
  end

end

defmodule Main do

  # 写真データーがあるフォルダを指定する
  baseFolder = "/Users/callmekohei/Desktop/tmptmp"

  # すべての写真データーのpermissionを644にする
  Foo.jpgFolderList( baseFolder )
  |> Task.async_stream( &( Foo.chmod644( &1 ) ) ,[ timeout: :infinity, max_concurrency: 1000] )
  |> Enum.to_list()

  # メイン部分を実行
  Foo.jpgFolderList( baseFolder )
  |> Task.async_stream( &( Foo.mainImpl( &1 ) ) ,[ timeout: :infinity, max_concurrency: 1000] )
  |> Enum.to_list()

end

まえに書いたBashScriptより早くなってますね。パラレルが効いてるのでしょうか?

はまったところ

下記のようにTask.async_streamを2重に使うと扱うデーター数が多くなるとタイムアウトでプログラムが止まってしまいます。今回の場合フォルダ数が5個以上の時にエラーが頻発しました

  def addOneSecond( fp ) do
    exifToolParam2
    |> Task.async_stream( &( System.cmd("bash",["-c", &1]) ))
    |> Enum.to_list()
  end


  Foo.jpgFolderList( baseFolder )
  |> Task.async_stream( &( Foo.mainImpl( &1 ) ) ,[ timeout: :infinity, max_concurrency: 1000] )
  |> Enum.to_list()

こんな感じのエラーがでます

15.png

(エラー回避)
内側の部分のアシンクを外します

  def addOneSecond( fp ) do
    exifToolParam2
    |> Enum.each( &( System.cmd("bash",["-c" , &1]) ))
  end

感想

ほむほむ

Elixir

いい感じ

冒頭のイラスト

ざっきー先生(山崎 進先生 @zacky1972さん )という、ヘイスガというGPUでエリクサーの演算をするライブラリ?を開発されているすごい方です!この前もくもく会でお見かけした時はずっとチョコフレーク?をもぐもぐされてましたよ。

GigalixirでSlackBotを動かしてみる

(この記事は「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」の17日目です)

昨日は@callmekoheiさんのBashScript を Elixir で書き直してみたっ(2倍速〜)でした!

はじめに

今回はGigalixirでSlackのBotを動かす方法(導入部)についてです。
自分は今年の8月頃にElixirを学び始めたのですが、そのきっかけがこのSlackのBotを作ることでした。
Elixirの魅力を伝えるにはあまりいい題材ではないかもしれないですが、ElixirでもPaaSを使って簡単にサービスを作れる点で取り組みやすい題材なので、記事にしてみました。

Gigalixirって何?という方はfukuoka.ex代表の@piacereさんの記事を見て頂くと非常にわかりやすいです。
https://qiita.com/piacere_ex/items/1a9cbcc740ca3707eaec6

(参考URLは記事の最後にまとめています。)

動作環境

  • Elixir 1.5.1(Gigalixirのデフォルトのバージョン)
  • Erlang 20.0(Gigalixirのデフォルトのバージョン)
  • 他依存関係は下記の通り(mix.exsのdeps定義)
      {:phoenix, "~> 1.3.0"},
      {:phoenix_pubsub, "~> 1.0"},
      {:phoenix_ecto, "~> 3.2"},
      {:postgrex, ">= 0.0.0"},
      {:phoenix_html, "~> 2.10"},
      {:phoenix_live_reload, "~> 1.0", only: :dev},
      {:libcluster, "~> 2.1"},
      {:distillery, "~> 1.5", runtime: false},
      {:gettext, "~> 0.11"},
      {:cowboy, "~> 1.0"},
      {:httpoison, "~> 1.0"},
      {:poison, "~> 3.1"},
      {:socket, "~> 0.3"}

実装からDeployまで

概要

今回はElixir/PhoenixのPaaSであるGigalixirでSlackのBotを作る導入として、SlackのRealTimeMessagingAPIを使った投稿機能の実装、GigalixirへのDeployまでを書いていきます。

1. 事前準備

事前準備として、Gigalixirの登録とSlackに投稿するためのtokenの取得を行います。
(手抜きですみませんが)ここは先人の良い記事があるので、Gigalixirの登録に関しては参考2の記事、Slackのtoken取得に関しては参考3の記事を参考にするとできます。

2. Gigalixirのテンプレートのclone

自分でPhoenix用のプロジェクトを一から作ってもよいのですが、公式のテンプレートをCloneすると自分で設定する内容を少なく作れるので、お試しで作ってみるには良いと思います。
テンプレートはPhoenixアプリケーションのプロジェクトで、そのままDeployするとGigalixir上でデフォルトのPhoenixアプリケーションが起動する様になっています。
こちらは参考4の記事に公式の手順がまとまっています。

3. Slackへの投稿処理の実装

2でCloneしたソースにSlackへの投稿処理を実装していくのですが、ここでは簡単に、RealTimeMessageを開始し、WebSocket通信を確立、Slackから最初に返されるhelloメッセージがきたら投稿する、という処理を実装します。(SlackのReal Time Message APIで必要な通信処理に関しては、参考5の公式ドキュメントを参照)
この部分は昨年のfukuoka.ex advent calenderの記事で非常に丁寧な解説があったため、そちらを元に下記の様に実装しました。

3.1. ライブラリの追加

HTTPの通信処理とReponseのParse用にhttpoison,poison, WebSocket通信用にsocketのライブラリを追加します。
2でcloneしたプロジェクトルート以下のmix.exsに以下を追加します。

mix.exs
defmodule GigalixirGettingStarted.Mixfile do
  ...
  defp deps do
    [
      ...(これより上はclone時点のライブラリそのまま)...
      {:httpoison, "~> 1.0"},
      {:poison, "~> 3.1"},
      {:socket, "~> 0.3"}
    ]
  end
  ...

追加したら、mix deps.getを実行し、依存関係の解決ができるか確認します。

3.2. RealTimeMessageのSession開始からSlackへのPostまで

RealTimeMessageのSession開始, WebSocketへの接続, Slackからのメッセージ受け取り, Slackへの投稿までを下記の様に実装します。
(参考5の内容を参考にさせて頂いています。)

slack_bot.ex
def SlackBot do
  # 共通で使用するSlackのURLの基礎部分
  @base_url "https://slack.com/api"

  # 1で取得したtokenを使用してsession開始用のURL組み立て用の関数(他のURLが必要な場合も同様に実装)
  def build_start_url(token) do
    "#{@base_url}/rtm.start?token=#{token}"
  end

  def connect(api_token) do
    fetch_body(api_token)
    |> parse_url()
    |> connect_web_socket()
    |> loop(api_token)
  end

  def fetch_body(token) do
    url = build_start_url(token)
    # Real Time Message開始処理のレスポンスコードによって処理を振り分け
    case HTTPoison.get! url do
      %{status_code: 200, body: body} ->
        Poison.Parser.parse!(body, keys: :atoms)
      %{error: "account_inactive"} = error ->
        raise "Failed to start real time message for slack!!"
    end
  end

  def parse_url(%{url: url}) do
    with %{"uri" => uri} <- Regex.named_captures(~r/wss:\/\/(?<uri>.*)/, url),
         [domain| path] <- String.split(uri, "/") do
      %{domain: domain, path: Enum.join(path, "/")}
    end
  end

  def connect_web_socket(%{domain: domain, path: path}) do
    Socket.Web.connect!(domain, secure: true, path: "/" <> path)
  end

  defp loop(socket, token) do
    case socket |> Socket.Web.recv!() do
      {:text, text} ->
        Poison.Parser.parse!(text, keys: :atoms)
        |> message(socket, token)
        loop(socket, token)
      {:ping, _} ->
        t = DateTime.utc_now() |> DateTime.to_string()
        socket |> Socket.Web.send!({:text, Poison.encode!(%{type: "ping", id: t})})
        loop(socket, token)
    end
  end

  # Hello Messageに対して返信
  defp message(%{type: "hello"} = m, socket, token) do
    socket |> Socket.Web.send!(
                {:text, Poison.encode!(
                  %{token: token,
                    channel: "MyChannelID", # ここは投稿先のチャンネルIDを記載
                    text: "Hello! from Elixir",
                    type: "message"
                  })})
    {:ok, 1}
  end

  # TODO: Hello Message以外でもエラーが発生しない様に一時的に空実装
  defp message(_, socket, token) do
    {:ok, 1}
  end
end

今回はあまり関係のないので余談になりますが、WebsocketのResponseのurlは30秒以内にWebSocket通信を確立しないと向こうになってしまうため、30秒以上経過してしまった場合、再取得が必要になります。

3.3. Bot用のWokerの起動処理

Slackの処理が失敗しても他のプロセスに影響が出ない様、Gigalixirで起動するアプリケーション上でbot用の別プロセスで起動できる様にしておきます。

まず、上で作ったslack_bot.exについてサーバプロセスとして起動できる様にGenServerを使用します。
ここでは、同時にconfig.exsにtokenの値を保存しておいてそこから呼び出すための処理も行なっています。
追記:
secretな値はソースコードに含まず、Gigalixirの環境変数から読み込むことを推奨しています。
下記のコマンドで設定し、config.exsで環境変数から読み込む様にします。

gigalixir config:set SLACK_API_TOKEN="<API TOKENの値>"
config.exs
config :gigalixir_getting_started,
  ecto_repos: [GigalixirGettingStarted.Repo],
  api_key: Map.fetch!(System.get_env(), "SLACK_API_TOKEN") # 環境変数から読み込み
slack_bot.ex
defmodule SlackBot do
  use GenServer

  # サーバープロセス起動時にSlackへの通信と投稿を行う
  def start_link() do
    GenServer.start_link(__MODULE__, [])
    # API KEYのconfigからの取得
    api_token = Application.get_env(:gigalixir_getting_started, :api_key)
    connect(api_token)
  end

  ...(以下の処理は同一)...

そして、このサーバプロセスをlib/gigalixir_getting_started/application.ex(2でcloneしたソースをそのまま使用する場合)のプロセス起動処理に追加します。

application.ex
defmodule GigalixirGettingStarted.Application do
  ...
  def start(_type, _args) do
    import Supervisor.Spec

    # Define workers and child supervisors to be supervised
    children = [
      # Start the Ecto repository
      supervisor(GigalixirGettingStarted.Repo, []),
      # Start the endpoint when the application starts
      supervisor(GigalixirGettingStartedWeb.Endpoint, []),

      # Slack Botのworker起動処理を追加
      worker(GigalixirGettingStarted.Slack.SlackBot, [])
    ]
    ...(以下の処理は同一)...
  end

  ...(以下の処理は同一)...

ここまで来たらDeploy前に一度ローカルで動作チェックし、Slackに投稿されるか確認してみます。
mix phoenix.serverを実行するとSlackに投稿されることが確認できます。

4. Deploy

最後にGigalixirにデプロイするとPhoenixアプリケーションが起動してSlackに投稿されます。
デプロイ方法は参考9にある通り、ソースコードの差分をgitでadd, commit, pushするだけです。

git push gigalixir masterを実行・成功したことを確認し、しばらく待つと以下の様なメッセージが指定されたチャンネルに投稿されます。数分程度待つこともありますが、大体はすぐに投稿されることが確認できます。

スクリーンショット 2018-12-16 22.52.12.png

これでSlackのbotをElixirで立てる導入段階まではできました。
後はOutgoingWebHookなどを使って返信機能の実装などを追加していけば、GASなどを使っていたBotを移植することもできます。

追記: 5. Projectの名前変更

プロジェクト名がそのままだと格好がつかないので、プロジェクト名も変更します。
この方法は参考10が参考になるのですが、rename用のファイルまで書き換えてしまわない様、ackコマンドの例外を下記の様に追記します。

rename_phoenix_project.sh
#!/bin/bash

set -e

CURRENT_NAME="CurrentName" 
CURRENT_OTP="current_name"

NEW_NAME="NewName"
NEW_OTP="new_name"

ack -l $CURRENT_NAME --ignore-file=is:rename_phoenix_project.sh | xargs sed -i '' -e "s/$CURRENT_NAME/$NEW_NAME/g"
ack -l $CURRENT_OTP --ignore-file=is:rename_phoenix_project.sh | xargs sed -i '' -e "s/$CURRENT_OTP/$NEW_OTP/g"

mv lib/$CURRENT_OTP lib/$NEW_OTP
mv lib/$CURRENT_OTP.ex lib/$NEW_OTP.ex
mv lib/${CURRENT_OTP}_web lib/${NEW_OTP}_web
mv lib/${CURRENT_OTP}_web.ex lib/${NEW_OTP}_web.ex

まとめと余談

今回はElixirでSlackのBotを実装し、それをElixir/Phoenix用のPaaSで動かす内容を書きました。
自前でサーバがなくGAS等でBotを立てていた方でも、Elixirで簡単にPaaSでBotを立てられることが伝わってくれれば(そしてElixirユーザが少しでも増えれば)と思っています。

また、今回プログラムを作成する上でfukuoka.exの@kobatakoさんの昨年のAdvent Calender記事をかなり参考にさせて頂きました。本当にありがとうございます。

余談ですが、自分がこのAdventCalenderに参加したのは、Elixirを始めたときからTweetを拾って丁寧に回答を返して頂いたり、分からないことがあった際記事を参考にさせて頂いたりとfukuoka.exの@piacereさんやコミュニティにお世話になり、自分でも記事を書いてみたいと思ったからでした。
fukuoka.exを始め、今各地でElixirのコミュニティが増えてきており非常に"熱い"言語だと思っていますが、自分もこの中で新しいことにチャレンジしてElixirの良さを伝えていけたらと思っています!

明日の「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」@twinbeeさんのgrpc-elixirでGoと通信してみる #2です! こちらもお楽しみに!

※まだElixirについて無知な部分が多く、内容に不備があれば指摘していただけると助かります。

参考

  1. Gigalixir
  2. 【Gigalixir編①】Elixir/Phoenix本番リリース: 初期PJリリースまで
  3. SlackのWebhook URL取得手順
  4. Gigalixir テンプレートのClone
  5. ElixirでSlack Botを作った with Qiita API
  6. Elixirのhttpoisonライブラリ
  7. Elixirのpoisonライブラリ
  8. Elixirのsocketライブラリ
  9. Gigalixirへのデプロイ
  10. Phoenixのプロジェクト名変更

grpc-elixirでGoと通信してみる #2

(この記事は、「fukuoka.ex Elixir/Phoenix Advent Calendar Advent Calendar 2018」の18日目です)

昨日は @artk さんの「GigalixirでSlackBotを動かしてみる」でした。本日は「grpc-elixirでGoと通信してみる #1」の続きです。

Route Guide サンプル

前回はGoとElixirのgRPC通信でhelloworldがうまくいきました。今回はroute_guideというサンプルを試します。

このサンプルは、ストリーミング(アップロード、ダウンロード、全二重)が含まれます。

この記事では触れませんが、grpcでストリーミングが必用になった場合はこのサンプルコードをあたると良いでしょう。

Elixirのroute_guideサンプルはこちら

Goのサンプルはこちらです。

プログラムのポイント

残念ながら現時点ではElixirのgRPCプログラミングガイドはないので、Goのものと見比べて実装を追う必要があります。

以下、双方向ストリーミングRPCのクライアントプログラムで見比べてみます。runRouteChatは、RPCのクライアントプログラムです。

Goではストリーミングにgoroutineを使って並列で処理をしています。

func runRouteChat(client pb.RouteGuideClient) {
    notes := []*pb.RouteNote{
        {Location: &pb.Point{Latitude: 0, Longitude: 1}, Message: "First message"},
        {Location: &pb.Point{Latitude: 0, Longitude: 2}, Message: "Second message"},
        {Location: &pb.Point{Latitude: 0, Longitude: 3}, Message: "Third message"},
        {Location: &pb.Point{Latitude: 0, Longitude: 1}, Message: "Fourth message"},
        {Location: &pb.Point{Latitude: 0, Longitude: 2}, Message: "Fifth message"},
        {Location: &pb.Point{Latitude: 0, Longitude: 3}, Message: "Sixth message"},
    }
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    stream, err := client.RouteChat(ctx)
    if err != nil {
        log.Fatalf("%v.RouteChat(_) = _, %v", client, err)
    }
    waitc := make(chan struct{})
    go func() {
        for {
            in, err := stream.Recv()
            if err == io.EOF {
                // read done.
                close(waitc)
                return
            }
            if err != nil {
                log.Fatalf("Failed to receive a note : %v", err)
            }
            log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude)
        }
    }()
    for _, note := range notes {
        if err := stream.Send(note); err != nil {
            log.Fatalf("Failed to send a note: %v", err)
        }
    }
    stream.CloseSend()
    <-waitc
}

一方Elixir側の対応する関数です。Task.async/Task.awaitで並列ストリーミングを行います。

 def run_route_chat(channel) do
    data = [
      %{lat: 0, long: 1, msg: "First message"},
      %{lat: 0, long: 2, msg: "Second message"},
      %{lat: 0, long: 3, msg: "Third message"},
      %{lat: 0, long: 1, msg: "Fourth message"},
      %{lat: 0, long: 2, msg: "Fifth message"},
      %{lat: 0, long: 3, msg: "Sixth message"}
    ]

    stream = channel |> Routeguide.RouteGuide.Stub.route_chat()

    notes =
      Enum.map(data, fn %{lat: lat, long: long, msg: msg} ->
        point = Routeguide.Point.new(latitude: lat, longitude: long)
        Routeguide.RouteNote.new(location: point, message: msg)
      end)

    task =
      Task.async(fn ->
        Enum.reduce(notes, notes, fn _, [note | tail] ->
          opts = if length(tail) == 0, do: [end_stream: true], else: []
          GRPC.Stub.send_request(stream, note, opts)
          tail
        end)
      end)

    {:ok, result_enum} = GRPC.Stub.recv(stream)
    Task.await(task)

    Enum.each(result_enum, fn {:ok, note} ->
      IO.puts(
        "Got message #{note.message} at point(#{note.location.latitude}, #{
          note.location.longitude
        })"
      )
    end)
  end

見比べてみてどうしたでしょうか? 関数名はGoの実装を踏襲してるため、一目でわかるのではないかと思います。(構造体の記述が多い分、Elixirのコードが長めになっていますね。)

前回の続き

さて、これまで説明してきたroute_guideサンプルでElixir-Go間の通信を行います。

コンテナから抜けて、2つのコンテナを終了しましょう。 docker-comopose downではコンテナの終了と破棄を同時に行ってくれます。

# exit
$ docker-compose down

Route Guide : Elixir -> Golang

Editorマークをクリックしてdocker-compose.ymlを編集をします。

image.png

docker-compose.ymlのcommandの#を外して、Saveボタンを忘れずに押してください。
(注記:command:の字下げは、build:のカラムに合わせてください)

  go-node:
    build:
      context: .
      dockerfile: Dockerfile-go
    command: sh -c "cd /go/src/google.golang.org/grpc/examples/route_guide/ && server/server"

これでGolang側のRoute Guideサーバー起動準備ができました。
以下、コンテナを立ち上げます。

※ エラーが出る場合は、エディターで右端が切れてないかをチェックしてみて下さい。

$ docker-compose up -d

docker psコマンドで、コンテナが2つ立ち上がってることを確認したら次へ進みます。コンテナが1つしかない場合は、docker-compose downでコンテナを落としてEditorでymlの編集内容を見直してください。

image.png

以下elixir-nodeのコンテナに入ります。

$ docker-compose exec elixir-node ash

今度はroute_guideのフォルダに移動、依存関係の取得・コンパイル。

# cd ~/grpc-elixir/examples/route_guide
# mix deps.get && mix compile

サーバー接続先を書き替えます

# vi priv/client.exs
client.exs
# 9行目
{:ok, channel} = GRPC.Stub.connect("localhost:10000", opts)
         ↓
{:ok, channel} = GRPC.Stub.connect("go-node:10000", opts)
# mix run priv/client.exs

実行すると、以下から始まる大量のテキストが流れれば接続は成功です。
ストリーミングを含む接続がうまく行ってるのが確認できました。

Getting feature for point (409146138, -746188906)
<name: "Berkshire Valley Management Area Trail, Jefferson, NJ, USA", location: <latitude: 409146138, longitude: -746188906>>
Getting feature for point (0, 0)
<name: nil, location: <latitude: 0, longitude: 0>>
Looking for features within %Routeguide.Rectangle{hi: <latitude: 420000000, longitude: -730000000>, lo: <latitude: 400000000, longitude: -750000000>}
<name: "Patriots Path, Mendham, NJ 07945, USA", location: <latitude: 407838351, longitude: -746143763>>
~ 略 ~

Route Guide : Golang -> Elixir

今度は逆方向をやってみます。

# mix grpc.server

これで、elixir-nodeのgRPCサーバーが起動するので、Ctrl+p Ctrl+qでコンテナからデタッチします。

go-nodeコンテナに入ります

$ docker-compose exec go-node bash

route_guideフォルダーに移動して

# cd /go/src/google.golang.org/grpc/examples/route_guide

route_guideクライアントをelixir-nodeに向けて実行します。こちらはhelloworldと違ってサーバーのオプション起動が付いているので楽です。
elixir-nodeのport=10000に対して、リクエストしてみます。

# client/client -server_addr elixir-node:10000

先ほどのように以下から始まるテキストが流れたら成功です。(Elixirと若干フォーマットが違いますね。)

2018/12/10 03:42:56 Getting feature for point (409146138, -746188906)
2018/12/10 03:42:56 name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 >
2018/12/10 03:42:56 Getting feature for point (0, 0)
・・・・

以上で、公式サンプルのgRPC双方向通信が確認できました。

終了

以下でコンテナを抜けることができます。

# exit

作業が終了したら、左上の「CLOSE SESSION」ボタンを押すと、コンテナホストごと消去されます。

お疲れ様でした。


明日は@piacere_exさんの「BASIC以来、35年間プログラミングしてないIT企業社長が、ElixirでWebアプリを作った」です。お楽しみに!

BASIC以来、35年間プログラミングしてないIT企業社長が、ElixirでWebアプリを作った

fukuoka.ex代表のpiacereです
今回もご覧いただいて、ありがとうございます:bow:

いつもと趣向違いですが、ウチのCEOが開発合宿中に行ったElixirチャレンジをお送りします

そうそう、CEOと私は、地味に、35年くらい前にプログラミングをスタートしたという共通点を持っています

その後、CEOはビジネスとマーケティングに進み、私はプログラミングとマーケティングに進みましたが、そんな2人が福岡で出会い、今は共に会社経営をしている訳です

それでは、CEOが開発合宿中でElixirに触れていくログをお楽しみくださいー

内容が、面白かったり、役に立ったら、「いいね」よろしくお願いします :wink:

CTOからの提案は、正直、気が重かった…しかし…

カラビナテクノロジー株式会社 でCEOをやっている福田です
image.png

私は普段、事業会社向けのITシステム導入・運用をお手伝いしつつ、エンジニアにとって最高の組織を作るべく邁進しています

私自信は、「BASIC」という言語で、35年前、プログラム開発を行っていましたが、今はすっかり、プログラミングそのものからは離れています

そんな私が、今回、エンジニア達と共に参加した開発合宿にて、35年ぶりにプログラミングに挑戦してみました

ウチのCTOから、「合宿でElixirの開発やってみませんか?」と提案されたときは、普段、どんな無理難題を出されても、馴染みの無いタスクをやるときも、「なるほどー」と前向きにクリアしていくところですが、正直、気が重かった(笑)

それも「Elixir」、先端の関数型言語への入門…行番号すら手入力するBASICと、あまりに違うのでは…
image.png
(ここが今回の合宿場所です)

しかし、実際にやってみると、Elixirによるプログラミングというものが、ただの「データ変換」であることや、その仕組みでWebアプリがいとも簡単に作れることを、僅かながら実感できました

さて、CTOから言われるがままに、「Excelから関数型言語マスター」を読むところからスタートします
image.png

まずは「事前準備:Elixirのインストール」から

私は、普段からWindowsを使っているので、インストーラをダウンロード、と
image.png
インストーラをダブルクリックで起動します
image.png
そのまま進めていきます
image.png

ん?Erlang?なんだこりゃ?

私「こんなのが出てきたけど、どうしたら良いです?」
image.png

CTO「あー、そのまま進めてください。基本、特記とか無ければ、そのまま進めてOKです」

Erlangが何なのかよく分からないけど、言われたまま進めます

ひとまずインストール完了、さて次は、と

私「次は、”ソースコードからビルドする”をやれば良いですか?」

CTO「いいえ、そこはやらなくてOKです。さっきインストールした奴で完了してるので」

あー、なるほど、良く見たら、インストーラか、ソースコードか、Dockerかって書いてあった:sweat:

続けて「Excelの『並べ替え』をElixirで書いてみる」

コマンドプロントを起動…えーと、Windowsメニュー右クリックで「検索」と…
image.png
あった
image.png
起動した
image.png
コマンドプロントの中で、「iex」コマンドを打ってみる
image.png
なんかElixirってのが出てきたから、これで良いのかな

私「これで合ってます?」

CTO「はい、OKです。その中で、Enum.sortのコードを実行してみてください」
image.png
ふむふむ、コレを打つのね
image.png
image.png
おぉ、数字が並べ替えされた

CTO「BASICだとFORとか書いて処理してたのが、Elixirだと、データの並びに対して、sortを掛けるだけです。ね?簡単でしょ?」

確かに簡単だ
image.png
(合宿中の様子、一番左が私)

プログラミングって、こんなにサックリしてたっけ?

その後、フィルタを試し、第2回に進み、「複数列のデータ」複数列データから「列の抽出」と試していくけど、特に詰まるところも無く、アッサリ済んでしまった

私「なんか、あんまりプログラミングしてる感じ、しないですねぇ」

CTO「そうです、そうです。ただデータ変換するだけの簡単なお仕事ですw」

なるほどね、Elixirでのプログラミングは、ただの「データ変換」なのか

おや?何やら休日なのに、急ぎ目のメールが届いている…

~ Coffee Break ~

私「スミマセン、けっこう進んだんで普通の仕事しても良いですか?」

CTO「仕事するのに許可もらうって斬新ですねw どーぞ」

~~ しばし、お仕事タイム ~~
image.png
CTO「そろそろ一段落です?」

私「はい、Phoenixのインストールから再開しますー」

※なお、「Phoenix」は、ElixirのWebフレームワークです

Phoenixのインストールから再開

Ctrl+cを2回押して、iexを抜ける、と
image.png
それから、書いてある通りに、コマンド入力
image.png
インストールはできたっぽい

次は、Phoenixプロジェクトの作成とのこと

image.png
私「これはYですか?」

CTO「Yですね」
image.png
image.png
プロジェクトも作成できたっぽい

よし、Phoenixサーバを起動…

image.png
あれ?could not be found?…

私「これ、エラーですかねぇ?」

CTO「あー、PJ作った後に、フォルダ入ってないですね。この『cd sample』が抜けてますね」

私「あぁ、そういうことか」
image.png
私「おっ、なんかうまくいった感じですかね?」

CTO「ですね。んで、このcdってのは、フォルダ移動で、エクスプローラでフォルダ入るのと同じですね」

私「なるほどー」

コマンドでやると、こういう感じなんだなー

いよいよブラウザでWebページを見てみる

さて、Phoenixも起動したようなので、ブラウザに、http://localhost:4000を入力すると…

image.png
やったー、表示できた!

私「できましたー」

CTO&メンバー達「おーっ」
image.png




と、まぁ、これは演出ですw

リアルはこんな感じでした
image.png

実際の、できあがった画面は、こんな感じ
image.png

ElixirでWebアプリが動かせるようになりました

この後、第3回まで進み、PostgreSQLをインストールしたり(スタックビルダはインストールしなくて良いそうです)、VSCodeのインストールをしたり、色々やり、合宿最後の成果発表です
image.png
VSCodeでElixirのコードをいじった様が、こんな感じで…
image.png
Phoenixで作ったWebページに、自分の名前や年齢などの情報が表示される改造を追加してみました

その後、メンバー達の成果発表も聞きました

image.png
AngularやFlutterのテスト方法、GAE/GCE/GKEでElixirを動かし、Cloud SQLに繋ぐ方法、Vue.jsで既存プロダクトを置き換え、Jestでテストする方法など、内容はサッパリ分かりませんでしたが、メンバーはとても楽しそうに発表していたので、良い合宿でした

ElixirとPhoenixをやってみた印象は…

なんか「プログラミング」というよりは、「データの操作」をするうちに、自然とWebアプリが作れてしまう、といった感じで、最新の言語は、BASICとは全く違うものなんだなー、と思いました

そんな様子の私に、最後、CTOから、こんな話をされました

CTO「ElixirとPhoenixは、こんな感じでサクッとWebアプリ開発できるんですが、Java等、他言語だと、こう簡単にはいかないので、案件取ったり、見積作るときは、この感覚の3~5倍で作るようにしてくださいね」

私「あ、はい…」

そんなもんなんですね…うーん、まだまだ奥が深い

終わり

さて今回は、いつもと趣向を変えて、ウチのCEOが、Elixir+Phoenixで、Webアプリを作る様をご覧いただきました

楽しんでいただけましたか?

今回、CEOに実践してもらったElixir+Phoenixチュートリアルは、35年間、プログラミングしていなかった方でも入門できる内容ですので、どうぞあなたも下の画像をクリックして、チャレンジしてみてください:kissing_smiling_eyes:
image.png

p.s.「いいね」よろしくお願いします

ページ左上の image.pngimage.png のクリックを、どうぞよろしくお願いします:bow:
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください!:tada:

[Elixir]をこれから学ぶ人に向けてPhoenix v1.3とv1.4を同時並行で触れる環境構築

この記事は、「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」の20日目になります。

昨日は、@piacere_exさんのBASIC以来、35年間プログラミングしてないIT企業社長が、ElixirでWebアプリを作ったでした。
本日は、ElixirとErlangバージョン管理の話しです。

この記事の構成

  1. 記事を書いた動機という名のポエム
  2. 記事の対象OS
  3. asdfのインストール
  4. asdf Erlang & Elixir のプラグインをインストール
  5. バージョン違いの環境の準備
  6. ① Phoenix v1.3の環境準備
  7. HEXのインストール
  8. Phoenix Frameworkをインストール
  9. ② Phoenix v1.4の環境準備
  10. 最後に

記事を書いた動機という名のポエム

以下、しばらくはポエムになりますので、本題を早く見たい方は飛ばしてください。w

最近、ElixirにハマってるYOSUKENAKA.meです。

という事で、最近は知り合いのママさんや高校生にElixir良いぜ!一緒に学ぼうぜ!と焚き付けて、Elixirすげーとか興奮しながら勉強会してますw

そんな事を、Twitterで興奮気味につぶやいていたら、ご縁あって、fukuoka.exに参加させて頂いてます。来年は、Elixirの修行をしに、福岡行こうと思います。

そして、先日のfukuoka.ex#17:Elixir実践テクニック公開します② のトラックAで紹介されたWebサービスバックエンドライブラリー Materiaというライブラリーが、今ちょうど作っていた認証系を既に簡単に揃えているというので、これは開発の短縮化できるじゃない!

ゲームで言うなら、チートですね。では、早速導入しようと思いましたが、僕の環境では使えませんでした。(T T)どうやら、以下の環境ではまだ対応してないようです。

Hex:    0.18.2
Elixir: 1.7.4
OTP:    21.1.4

上記に加えて、Phoenixフレームワークもv1.4の状態
かといって、v1.4もbrunchからWebpackに変更という事もあって、こっちも学習したいしなぁ。

という状況です。そこで、バージョン違いを混在させて、Phoenix Framework v1.3とv1.4をPCに混在した形で準備したいと思います。

記事の対象OS

mac OSX環境での構築について書いてます。

asdfのインストール

asdfの初期導入は、こちらの記事が参考になりました。
Elixirのバージョン管理環境をasdfを使って作った

そのため、解説は上記の記事を参考にください。
ここでは、簡単に手順だけ、記載したいと思います。

  1. 既にElixirをインストールして環境構築済みの方は、一度削除します。これから始める方は、この作業は不要になります。
$ brew uninstall --force erlang elixir
  1. ここからは、asdfのgithubのREADME通りに進めます。
$ brew install automake autoconf openssl libyaml readline libxslt libtool unixodbc

$ git clone https://github.com/asdf-vm/asdf.git ~/.asdf

$ echo -e '\n. $HOME/.asdf/asdf.sh' >> ~/.bash_profile
$ echo -e '\n. $HOME/.asdf/completions/asdf.bash' >> ~/.bash_profile

asdf Erlang & Elixir のプラグインをインストール

ターミナルを再起動して、次のコマンドを打ちます。

$ asdf plugin-add erlang
$ asdf plugin-add elixir

asdf plugin-listと打って、elixirやerlangが表示されれば成功です。

$ asdf plugin-list
elixir
erlang

バージョン違いの環境の準備

さて、いよいよ、erlangとelixirの2つの環境を準備したいと思います。
今回は、Materiaを導入できるバージョンの環境(Phoenix v1.3)と、新しいPhoenix v1.4が動く環境を2つ準備したいと思います。

Materiaの環境はこちらの記事を見ると以下のバージョンが前提のようですので、その構成を準備します。

Hex:    0.18.2
Elixir: 1.6.1
OTP:    20.2.4

まず、初めにフォルダ別で管理したいので、次のフォルダを作成しました。
※フォルダ名は、自分の管理しやすい名前で準備して大丈夫です。僕は以下のようにしました。

$ mkdir v1.6.1elixir
$ mkdir v1.7.4elixir

① Phoenix v1.3の環境準備

まず始めに、v1.6.1elixirフォルダから環境を作ります。

$ cd v1.6.1elixir
$ asdf list-all erlang
... 略
20.2.2
20.2.3
20.2.4
20.3
20.3.1
20.3.2
... 略

asdf list-all [名称]で バージョンの情報が列挙されます。
今回は、OTPが20.2.4のバージョンとなっているので、erlangは20.2.4をインストールします。

Elixirも同じようにlist-allで調べる事ができます。今回は、Materiaの環境に合わせてelixir 1.6.1にします。

asdf install erlang 20.2.4
asdf install elixir 1.6.1

インストールしたerlangとelixirはこのフォルダ内で有効にしたいので、次のコマンドを打ちます。

asdf local erlang 20.2.4
asdf local elixir 1.6.1

インストール状況を確認して見ましょう。asdf currentを入れて、set by ~となる.tool-versionsファイルがちゃんと、v1.6.1elixirにできてる事を確認しましょう。

$ asdf current
elixir         1.6.1   (set by /Users/username/v1.6.1elixir/.tool-versions)
erlang         20.2.4  (set by /Users/username/v1.6.1elixir/.tool-versions)

HEXのインストール

Elixir,Erlangのパッケージ管理ツールHexをインストールします。フォルダはv1.6.1elixirのままです。

$ mix local.hex

PhoenixFrameworkをインストール

HEXをインストールしたら、次にPhoenixFrameworkをインストールしましょう。

mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez

ここまで出来れば、Phoenix v1.3の環境構築は無事終了です。
最後に環境がちゃんとできてるか確認します。mix hex.infoのコマンドで以下が表示される事を確認してください。

$ mix hex.info
Hex:    0.18.2
Elixir: 1.6.1
OTP:    20.2.4

② Phoenix v1.4の環境準備

基本的に① Phoenix v1.3の環境準備と同じ手順で進めます。が、最後にちょっと付け足しの作業があります。
フォルダはv1.7.4elixirに移動し、下記のバージョンでインストールしました。

Hex:    0.18.2
Elixir: 1.7.4
OTP:    21.1.4

Phoenixフレームワークのインストール手順まで進めた後、追加で以下の事を行います。

asdf current.tool-versionsv1.7.4elixir/のフォルダにできてる事を確認します。

問題なければ、最後に以下のコマンドを実行します。

mix archive.install hex phx_new 1.4.0

これで、無事にPhoenix v1.3と v1.4の環境が作れました。

早速、それぞれのフォルダ内でプロジェクトを作って確認して見てください。

$ mix phx.new hello
$ cd hello
$ mix ecto.create
$ mix phx.server

無事にlocalhost:4000でサーバーが起動して入れば成功です。
※ ちなみに、Phoenix v1.3では少し、作業をしないとサーバーが起動せずにエラーになります。
解決方法はPhoenix 最初の第一歩 Mix phx.serverでerrorの解決方法に記載したので、コチラを確認ください。

最後に

Elixirで開発を楽しみたい仲間募集してます。また、毎週火曜日にオンラインやオフライン混在で勉強会してるので、
参加したい方、気軽にご連絡ください。

Hello! After World!! - 初心者がelixirでオンラインゲーム製作に挑戦してみた サーバ編(第0章)

・はじめに

最近elixirをはじめたGKBRです。elixirは、
- 耐障害が高い
- 平行・分散処理を実装しやすい
- ネットワーク処理を標準搭載

などの特徴を有し、大量のデータを安定的に処理し続ける必要があるIot分野などへの活用が期待されています。

今大注目の言語です!

早速流行に乗るためお約束のアレをやってみました。

iex> IO.puts("Hello World!")
Hello World!

おー!感動です。
ただ、いまいちモチベーションがわかない。どうしよう。

そうだ!個人的にの興味ある分野(ゲーム制作)を題材にすればやる気みなぎり学習もはかどるはず!ジャンルはelixirの特徴を生かせる??オンラインゲームにしよう!最終目標はWOWやFF14の類似作品にすることです。

無謀ですか?

私もそう思います。
でも何事も挑戦です!
当たって砕けてみたいと思います。

ちなみにqiita初投稿です。
緊張してきた…

・目次

サーバ編

クライアント編

開発ツール編

  • 2019年公開予定

・参考文献

No. 書籍名 著者 出版年
1 MMORPGゲームサーバープログラミング ナム ジェウク 2005
2 オンラインゲームを支える技術 壮大なプレイ空間の舞台裏 中嶋 謙互 2011
3 クラウドゲームを作る技術 マルチプレイゲーム開発の新戦力 中嶋 謙互 2018
4 ドラゴンクエストXを支える技術 大規模オンラインRPGの舞台裏 青山 公士 2018
5 プログラミングElixir Dave Thomas 2016

・開発環境

PC Mac Book Pro (Retina, 13-inch, Late 2013)
OS Mojave
言語 elixir v1.7

・今回の目標

今回は複数のユーザが同一マップ上を歩き回れるようにすることです。

・サーバ構成

サーバ構成です。ほぼすべてのゲームロジックをサーバサイドに実装するサーバ集約型を採用します。(参考文献1,2,3,4)

fig01.png

  • RelaySvr:リレーサーバ

    • クライアント-サーバ間のTCP/IP送受信処理を行う
    • パケットの暗号化・復号化を行う
    • パケットのシリアライズ・デシリアライズを行う。
  • GmSvr:ゲームサーバ

    • ゲーム内でキャラクタ移動や道具使用時のロジックを実装する
  • DBサーバ

    • プレイヤ情報の永続化を行う
  • ZONEサーバ

    • プレイヤの位置情報管理を行う  
  • チャットサーバ

    • ゲーム内のチャット処理を行う

今回は複数のユーザが同一マップ上を歩行できるのを目標としているため、上図の点線枠内についてのみ実装対象としました。その他のサーバについては後日追加実装する予定です。

・実装

  • RelaySvr
relay_svr.ex
defmodule RelaySvr do

  def accept(port) do
    {:ok, listen} = :gen_tcp.listen(port, [:binary, packet: 0, active: false, reuseaddr: true])
    loop_accept(listen)
  end

  defp loop_accept(listen) do

    {:ok, socket} = :gen_tcp.accept(listen)
    IO.puts "client: #{inspect socket}"

    #===========================
    # TCP Process start
    #===========================
    pid = spawn_link(RelaySvr.Svr,:start_link,[socket,[]])
    IO.puts "RlaySvr.Svr: #{inspect pid}"

    loop_accept(listen)
  end

end

defmodule RelaySvr.Svr do

  use GenServer

  def start_link(state,opts) do
    {_,pid} = GenServer.start_link(__MODULE__,state,opts)

    self_pid = self()


    GenServer.cast(:gmsvr1,{:svrin,pid})
    task_pid =spawn_link(RelaySvr.Svr, :serve,[state,pid])


    {status,msg} = :gen_tcp.controlling_process(state, task_pid)

    :ok

  end

  def init(state) do
    self_pid = self()

    {:ok,state}
  end

  #================================
  # サーバ接続時の処理
  #================================
  def handle_cast({:svrin},state) do
    IO.puts("RelaySvr.Svr:cast :svrin")
    self_pid = self()
    GenServer.cast(:gmsvr1,{:svrin,{self_pid}})
    {:noreply,state}
  end

  #================================
  # 処理結果をクライアントに返信
  #================================
  def handle_cast({:res,msg},state) do
    :gen_tcp.send(state, msg)
    {:noreply,state}
  end

  #===============================
  # クライアントに通知
  #===============================
  def handle_cast({:notify,msg},state) do
    :gen_tcp.send(state, msg)
    {:noreply,state}
  end



  #==============================
  # TCP Server
  #==============================
  def serve(socket,pid) do   
    socket |> read_line |> write_line(socket,pid)
    serve(socket,pid)
  end

  defp read_line(socket) do    
    {:ok, data} = :gen_tcp.recv(socket, 0)    
    data
  end

  defp write_line(line, socket,pid) do
    packet = Relay.Packet.conv(line)
    GenServer.cast(packet.svr_id,{:parse, {pid,packet.data}})

  end

end

#================================
# RelaySvr内のパケット定義
#================================
defmodule Relay.Packet do
  @enforce_keys [:svr_id, :data]
  defstruct [:svr_id, :data]

  def conv(
    <<
      svr_id::size(16),
      data::binary
    >> = packet
  )do
    case svr_id do
      1-> svr_id = :gmsvr1
      2-> svr_id = :gmsvr2
      3-> svr_id = :gmsvr3
      4-> svr_id = :gmsvr4
    end
    %Relay.Packet{svr_id: svr_id , data: data}
  end
end

defmodule Relay.RelayPacket do
  @enforce_keys [:svr_id, :data]
  defstruct [:svr_id, :data]

  def new(
    <<
      svr_id::size(16),
      data::binary
    >> = packet
  )do
    %Relay.RelayPacket{svr_id: svr_id, data: data}
  end
end
  • GmSvr
gm_svr.ex
defmodule GmSvr do

  use GenServer

  def start_link(init,opts) do
    {_,pid}=GenServer.start_link(__MODULE__, init , opts )

  end

  def init(state) do
    {:ok,state}
  end

  #=============================
  # Svrin
  #=============================
  def handle_cast({:svrin,pid_from},state) do

    Notification.add(:notify,pid_from)
    IO.puts "GmSvr:svrin"

    {:noreply,state}
  end

  #=============================
  # Packet Handling
  #=============================
  def handle_cast({:parse, {pid_from, data}},state) do
      GmSvr.Packet.conv(data)
      |> run(pid_from)
      #IO.puts("#{inspect packet}")
      #run(pid_from,packet)
      {:noreply,state}
  end

  #=============================
  # Move
  #=============================  
  def run(%GmSvr.MvPacket{}=packet, pid_from) do

    IO.puts "mv id=#{packet.id} (x,y)=(#{packet.pos_x},#{packet.pos_y})"

    Notification.notify(:notify,:notify,GmSvr.Packet.to_bin(packet))

  end

  #=============================
  # Jump
  #=============================
  def run(%GmSvr.JumpPacket{}=packet, pid_from) do

    IO.puts "jump id=#{packet.id} (x,y)=(#{packet.pos_x},#{packet.pos_y})"

    Notification.notify(:notify,:notify,GmSvr.Packet.to_bin(packet))

  end


  #=============================
  # Skill
  #=============================
  def run(%GmSvr.SkillPacket{}=packet, pid_from) do

    IO.puts "skill id=#{packet.id} (skill_id,to_id)=(#{packet.skill_id},#{packet.to_id})"

    Notification.notify(:notify,:notify,GmSvr.Packet.to_bin(packet))
  end

end

# =========================
# GmSvr内のパケット定義
# =========================

# =========================
# Packet MOVE
# =========================
defmodule GmSvr.MvPacket do
  @enforce_keys [:id, :pos_x, :pos_y, :pos_z]
  defstruct [:id, :pos_x, :pos_y, :pos_z]

  def new(
        <<
          id::unsigned-integer-size(16),
          pos_x::unsigned-integer-size(16),
          pos_y::unsigned-integer-size(16),
          pos_z::unsigned-integer-size(16)
        >> = packet
      ) do
    %GmSvr.MvPacket{id: id, pos_x: pos_x, pos_y: pos_y, pos_z: pos_z}
  end

end

# =========================
# Packet JUMP
# =========================
defmodule GmSvr.JumpPacket do
  @enforce_keys [:id, :pos_x, :pos_y, :pos_z]
  defstruct [:id, :pos_x, :pos_y, :pos_z]

  def new(
        <<
          id::unsigned-integer-size(16),
          pos_x::unsigned-integer-size(16),
          pos_y::unsigned-integer-size(16),
          pos_z::unsigned-integer-size(16)
        >> = packet
      ) do
    %GmSvr.JumpPacket{id: id, pos_x: pos_x, pos_y: pos_y, pos_z: pos_z}
  end
end

# =========================
# Packet  SKILL
#=========================
defmodule GmSvr.SkillPacket do
  @enforce_keys [:id, :skill_id,:to_id]
  defstruct [:id, :skill_id,:to_id]
  def new(
    <<
      id::unsigned-integer-size(16),
      skill_id::unsigned-integer-size(16),
      to_id::unsigned-integer-size(16)
    >> = packet
  )do
    %GmSvr.SkillPacket{id: id, skill_id: skill_id, to_id: to_id}
  end
end

defmodule GmSvr.Packet do
  def conv(
        <<
          pk_type::unsigned-integer-size(16),
          temp::binary
        >> = packet
      ) do
    case pk_type do
      1 -> p = GmSvr.MvPacket.new(temp)
      2 -> p = GmSvr.JumpPacket.new(temp)
      3 -> p = GmSvr.SkillPacket.new(temp)
    end
  end

  def to_bin(%GmSvr.MvPacket{}=packet)do
    <<
        1::unsigned-integer-size(16),
        packet.id::unsigned-integer-size(16),
        packet.pos_x::unsigned-integer-size(16),
        packet.pos_y::unsigned-integer-size(16),
        packet.pos_z::unsigned-integer-size(16)
    >>
  end

  def to_bin(%GmSvr.JumpPacket{}=packet)do
    <<
        2::unsigned-integer-size(16),
        packet.id::unsigned-integer-size(16),
        packet.pos_x::unsigned-integer-size(16),
        packet.pos_y::unsigned-integer-size(16),
        packet.pos_z::unsigned-integer-size(16)
    >>
  end

  def to_bin(%GmSvr.SkillPacket{}=packet)do
    <<
      3::unsigned-integer-size(16),
      packet.id::unsigned-integer-size(16),
      packet.skill_id::unsigned-integer-size(16),
      packet.to_id::unsigned-integer-size(16)
    >>
  end
end

  • Notification
notification.ex
defmodule Notification do
  def add(topic,val) do
    {:ok, _} = Registry.register(:reg, topic, val)
  end

  def notify(topic,type,val) do
    Registry.dispatch(:reg, topic, fn entries -> for {_, pid} <- entries, do: GenServer.cast(pid, {type, val}) end)
  end
end
application.ex
defmodule MmoSvr.Application do

  use Application
  def start(_type, _args) do
    # List all child processes to be supervised
    import Supervisor.Spec
    children = [
      supervisor(Task.Supervisor, [[name: RelaySvr.TaskSupervisor]]),
      worker(Registry,[ :duplicate,  :reg, [partitions: System.schedulers_online]]),
      worker(Task,[RelaySvr,:accept,[4040]]),
      %{
        id: :gmsvr_1,
        start: {GmSvr,:start_link,[1,[name: :gmsvr1]]},
        modules: [GmSvr]
      },
      %{
        id: :gmsvr_2,
        start: {GmSvr,:start_link,[2,[name: :gmsvr2]]},
        modules: [GmSvr]
      },
      %{
        id: :gmsvr_3,
        start: {GmSvr,:start_link,[3,[name: :gmsvr3]]},
        modules: [GmSvr]
      },
      %{
        id: :gmsvr_4,
        start: {GmSvr,:start_link,[4,[name: :gmsvr4]]},
        modules: [GmSvr]
      }
    ]
    opts = [strategy: :one_for_one, name: MmoSvr.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

・テスト

RelaySvr,GmSvrを起動し、RelaySvr宛に下表のパケットをTCP/IPで送信します。

No. 送信パケット(16進数) 予想される出力結果
1 00010001000100030004000a mv id=1 (x,y)=(3,4)
2 00010002000200130014000a jump id=2 (x,y)=(19,20)
3 00010003000300230024 skill id=3 (skill_id,to_id)=(35,36)
iex > mv id=1 (x,y)=(3,4)
iex > jump id=2 (x,y)=(19,20)
iex > skill id=3 (skill_id,to_id)=(35,36)

問題なさそうです。

・テストプレイ

簡単なクライアントプログラムを作成し、複数のクライアントをサーバに接続し移動してみます。
Hello! After World! 第0章 テストプレイ

・まとめ

  • 今回は複数のクライアントが同一マップ内を自由に歩行が可能な段階まで実装しました。
  • 実装したサーバ群は実績十分な古典的なオンラインゲームの実装手法(rpc)をelixirにほぼそのまま適用し実装しました。そのため現状ではelixirのメリットを活かした実装になっていません。
  • 今後はelixirの長所を最大限生かした実装に順次変更予定です。
  • オンラインゲームとして必要な機能を追加実装していきます。進捗はQiitaで毎月21日に報告いたします。
  • 来年度のアドベントカレンダで一年間の進捗/完成版報告を目標とします!

・最後に

  • Elixirで仕事したいです。

最後まで読んで頂き有難うございます。

Elixir: The only Sane Choice in an Insane World GOTOCON 2017 talk's [non-technical summary]

In the name of Allah(God), Most Gracious, Most Merciful.
Peace be upon you all.

Recently I have been learning about Elixir and found very informative talk on YouTube about Elixir by Mr.Brian Candarella.
Elixir talk on YOutube.jpg
Link to the video: https://youtu.be/gom6nEvtl3U
I decided to summarize his talk to make it easier to understand for non technical people. Who want to know why Elixir matters.

I will keep Mr.Brian's speech in a talk style to leave his personality in text. Everything written in from Mr.Brians perspective. All the respect and copyright belongs to Mr.Brian.
My opinion and text will be in italics text style. So let's get into the talk.

Definition of "Insanity"

Insanity is the amount of demand that is being put upon engineering team.

Some of those requirements are:
* Systems must be fast.
* Systems must scale to ever-increasing demand. As more of the world gets turned on and mobile device is getting faster internet connections.
* Global demand. There's going to be more demand put upon your applications.
* Requirements that have mix of distributed systems, mitigating downtime, self-healing systems, real-time etc. These are difficult problems to solve for the tools that we've been using for the past few years.

Erlang

Erlang it is a technology that was built by Ericsson on the telecommunications company of Sweden.

  • It was found in mid 80s late 90s. With good reputation and history

  • It was meant to handle global telecommunication systems with almost zero downtime

  • Systems that can monitor other systems to ensure that they're going to be bounced properly

  • Hot code loading. In Erlang you can actually have the system in production and you can push code deltas up without taking the production server down. It will absorb new code deltas and incorporate them into the runtime that's currently live in production.

These are really incredible and cutting-edge technologies today, that have been around for a long time. If that is the case why aren't we using them?

Why Erlang is not being used?

Mostly because of Prologue syntax
  • It it's like a sentence like a like programming structure. Each line is eliminated by a comma. The statement is finished with a period.
  • Something that should feel simple ends up looking difficult for those who used to more modern syntax.
Erlangs founders opinion about Elixir

Joe Armstrongs on his personal blog wrote: "what elixir brings the table is a complete different surface in text. Inspired by Ruby well, you might call a non-scary plum code syntax and loaded extra goodies"

Founding of Elixir

Elixir is created by a programmer named Jose Valine. He was Rubys core team member. He wanted to solve problems of concurrency with Ruby as the language. José Valim researched how to solve the problem and came across with Erlang and decided to write syntax on top of Erlang virtual machine. First, the syntax was Object Oriented Style. That did not work and then Jose decided to make it Functional and "light sugar".

Elixir itself really a surface level language. It doesn't do much on its own beyond once it is compiled into Erlangs virtual machine to handle all the job. Elixir is probably less involved with the nuts and bolts under the hood. Around 80%( 90% in 2018-11-21 ) of the language written in Elixir

Inherits from Erlang

These are what Elixir inherited from Erlang.

  • 30 year old tested technology and used by multi-billion corporations
  • Genservers which helps to manage distributed data
  • Supervisors which grants self-healing function
  • Scalability to meet tomorrows demand
  • Pattern matching

New features in Elixir:

  • Clean, familiar syntax
  • Mature tooling: Mix, debug , lint server, docs, dependency resolution,and etc., all built in
  • Built in unit testing
  • Meta programming. Which is also referred as dynamic code creation
  • Built-in documentation with doc tests
  • Pipes
  • Phoenix

Pattern matching:

...Simple non-technical explanation is under construction...

Pipes

Pipes allow you to clean up messes like this:

foo(bar(baz("hello world")))

You may try to clean up a little bit by doing something like this:

result = baz("hello world")
result = bar(result)
result = foo(result)

But Elixir let's you do this:

"hello world"
|> baz()
|> bar()
|> foo()

Which is kind of cool and easier to understand

Erlang VM (BEAM)

  • Monitors and Schedules it's own processes just like your operating systems monitors and schedules processes. When these processes die, BEAM makes sure that the right error code is being reported.
  • Can distribute processes across all available CPU cores. If the processes are kind of eating up a lot of resources, BEAM can actually distribute those processes across all available CPU cores. This is a huge performance gain if you have anything that is fairly CPU intensive and it happens automatically. So as long as you're running your application in a concurrent manner and using processes then all the processes will get split across available cores.
  • Each process only about 1kb. Erlang processes are incredibly cheap compared to operations and processes so each process is a little bit over a kilobyte so you can spin up by ton of them it's been a very very fascinating diver very quickly
  • Each process has its own garbage collector. This method speeds up the execution of new processes. Because everybody collects their own garbage. In other languages, there is general garbage collector which has to stop the world and do garbage collection while other processes wait until the garbage collection process finishes.

Concurrent connections cap (limit)

One Rackspace box that was able to scale up to two million concurrent connections. These connections are not just dummy connections. They were actually simulating real conversations and real message passion between each other the latency for distributing a message across all two million web sockets at that point was lucky two seconds. Such kind of

Rackspace box specs:
128 GB RAM
40 Cores

Case study - Bleachers Report

This is a case study of one company that was able to massive infrastructure savings by moving from one language over another - Elixir. Application was based on 8 year old Rails app. This application is a Sport News website where the company sends a lot of push notifications. So a big thing in the sports news world is, who pushes the sport who's pushes the sports news first.

The before they moved to Elixir/Phoenix
* 150 AWS instances
* Servers were jammed up with requests
* Large engineering team was needed per app
* Multiple complex caching strategies implemented
* Around 3 minutes to notify users about new news report

This is after, they moved to Elixir, Phoenix

  • 30 AWS instances
  • 10~30 ms averaged response times. This is very good response time
  • Largest average spikes: 400ms
  • Largest outlier spike: 800ms
  • One engineer per app
  • No caching needed
  • Around 3 seconds to notify user about new news report
Other examples:

These are some of the well known companies which used Elixir/Phoenix to solve their problems

  • Wat's App
  • Ejabberd
  • Riot Games

Difference from Object Oriented Programming

Here is classical OOP structure:
Screenshot 2018-12-22 at 9.41.30 PM - Edited (1).png
Which is so clean and makes so much sense.

Year or two from that time, this is what happens to it.
Screenshot 2018-12-22 at 9.42.05 PM - Edited (1).png
(source: https://files.gotocon.com/uploads/slides/conference_3/53/original/Elixir%20-%20The%20only%20sane%20choice%20in%20an%20insane%20world.pdf)

OOP gives you a lot of rope and flexibility. However, limitations are actually better there's power in limiting yourself and what what you can do. The big problem of object-oriented program is "state". You have objects that are carrying state around. They are not immediately accessible, but buried in the memory somewhere. Whereas functional programming is right there in your face

Ending notes:

So this is the end of the current article. Thank you for you interest. I will update this post by adding simplified definitions of genservers, supervisors, meta programming, and doc tests.
If you find any problems please consider those to be my shortcomings, and please feel free to contact me about those problems, i will fix them.
And I would like to express my gratitude to Mr.Brian Cardarella for sharing such a wonderful knowledge with us.

【小ネタ】Eixirのパイプ演算子使用中に第一引数の値を第二引数に渡す方法

この記事はfukuoka.ex Elixir/Phoenix Advent Calendar 2018 23日目の記事です。

こんにちは!Elixir学習中、業務では主に手動のテストをやっているyoshitakeです。
今回はAdvent Calendarの記事として、Elixirを学んで勉強中に知った小技を紹介します。

Elixirのパイプ演算子

僕が参考にしている、 @piacere_ex さんのExcelから関数型言語マスター1回目:データ行の”並べ替え”と”絞り込み” - Qiitaにもサラッと書かれていますが、Elixirにはパイプ演算子というものがあります。
これは、|>でつなぐと、左側に記述した変数や関数の出力が、右側に書かれた関数の第一引数になる。
というものです。

簡単にパイプ演算子を使った例を紹介します。

def mul(a, b)do
  a * b
end

def div(c, d)do
  c / d
end

上記のように、2つの数値の掛け算・割り算を行う関数mul, divがそれぞれあったとします。
この関数を使って、 (5 × 4) / 10 を計算しその結果を表示する場合、パイプ演算子あり、なしではそれぞれ以下のようになります。

# パイプ演算子なし
IO.puts div(mul(5, 4), 10)

# パイプ演算子あり
mul(5,4)
|> div(10)
|> IO.puts

このパイプ演算子のおかげでデータの流れや変化がわかりやすい、というのが個人的な感想です。
直感的でわかりやすいなぁ…と思います。

しかし、このパイプ演算子は 次の関数の第一引数としてデータを渡すので、第一引数以外の引数にデータを渡す場合はちょっと工夫が必要とのことです。
そこでこの記事では、私が教えてもらった3通りのやり方を紹介します。

パイプ演算子で第一引数以外にデータを渡す方法

以下の3つの方法があるそうです。

  1. swapする関数を作る
  2. 匿名関数(無名関数)を使う
  3. 匿名関数(無名関数) + 省略記法(ショートハンド)を使う

先程の例 div(c,d)に対して c/dではなく、d/cをしたい、という処理について考えてみます。

1. swapする関数をつくる

def swap_div(c, d)
  d / c
end

mul(5,4)
|> swap_div(10)
|> IO.puts

こんな感じです。
第一引数でもらうという事象は変更できないので、関数の中で順番を変更するですね。

2. 匿名関数(無名関数)を使う

mul(5, 4)
|> (fn n -> 10 /n end).()
|> IO.puts

Elixirには匿名関数(無名関数)をパイプ演算中に記述することができます。
それを記述し、その中で第一引数を他の処理に配置したコードを書いたらOKです。
■ 参考
関数 · Elixir School

3. 匿名関数(無名関数) + 省略記法を使う

mul(5, 4)
|> (&div(10, &1)).()
|> IO.puts

Elixirの匿名関数には省略記法があるそうで、省略記法を使うと第一引数を&1第二引数を&2という感じで使えるようになるそうです。

■ 参考
関数 · Elixir School

おわりに

さて、Elixir勉強中に習った小技を紹介しました。
これがなんの役に立ったのか?というと、Regex(正規表現)を使っているときに役立ちました。
Regexをパイプ中に使ってたのですが、データを第二引数に渡したい!と思ったんですよね。
それについてはまたの機会に。
では、ここまで読んでいただきありがとうございました!

※ 他にも方法ある場合教えていただけると嬉しいです。
よろしくお願いします。
■ 参考
Regex – Elixir v1.7.4

ElixirのProfiler比較

External article

デザイン出身の方やWebで開発を始めた方でも心が折れないVue.js SPA(ElixirによるAPI開発付き)

fukuoka.ex代表のpiacereです
ご覧いただいて、ありがとうございます:bow:

Vue.jsを始めると、Vue CLIやnpmを使ったり、webpackを使う解説やチュートリアルが多いですが、デザインから動的なページ作成を始めた方や、Webで開発を始めたのでサーバサイドプログラミングでの複雑な手順に慣れていない方には、本質に辿り付く前に心が折れそうになります

そこで、CDN … つまり、URL指定のライブラリ利用のみでも、Vue.jsでSPA(Single Page Application)が組める、ということを実感していただくチュートリアルを作ってみました

流れとしては、まずブラウザのみで動くページをCDN版のVue.jsで作成した後、簡単なDBアクセスAPIを作成(開発にはElixir/Phoenixを使用)し、Vue.jsからデータ追加/更新/削除するSPAもCDN版のVue.jsでこなそうと思います

なお、「Phoenix」は、ElixirのWebフレームワークです

内容が、面白かったり、役に立ったら、「いいね」よろしくお願いします :wink:

Vue.jsとHTMLの間でデータ受け渡しを行う

以下ファイルを適当な場所に作成します

index.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>

<div id="app">

<h1>Posts</h1>
<table border="1">
<tr v-for="result in results">
    <td style="padding: 10px;">{{ result.id }}</td>
    <td style="padding: 10px;"><input type="text" v-model="result.title"></td>
    <td style="padding: 10px;"><input type="text" v-model="result.body"></td>
</tr>
</table>
<button v-on:click="onView">データ表示</button>

</div>

<script>
var app = new Vue
( {
    el: '#app',
    data: 
    {
        results:   [], 
    }, 
    mounted()
    {
        this.results.push( { id: 1, title: 'title1', body: 'body1' } )
        this.results.push( { id: 2, title: 'title2', body: 'body2' } )
        this.results.push( { id: 3, title: 'title3', body: 'body3' } )
    }, 
    methods: 
    {
        onView: async function( evt )
        {
            console.log( this.results )
        }, 
    }, 
} )
</script>

ブラウザを起動し、上記ファイルをドラッグ&ドロップするか、オープンすると、以下の画面が表示されます
image.png

F12キーもしくはCtrl+Shift+iキーを押して、コンソールウインドウを開いた後、テーブル表示しているデータをVue.js側で保持している中身を「データ表示」ボタンでコンソールウインドウに出力します

コンソールウインドウに表示される、「(3) [{...}, {...}, …」が、Vue.js側で保持しているデータです
image.png

この「(3) [{...}, {...}, …」の左側の「▶」をクリックして開くと、「0: {__ob__: _e}」~「2: {__ob__: _e}」と出てきますが、これは画面上のテーブル3行に表示されているデータを表し、たとえば「0: {__ob__: _e}」の左側の「▶」をクリックして開くと、テーブルに表示されているデータと同じ値がコンソールウインドウで見れます
image.png

テーブルの入力フィールドで値を書き換え、「データ表示」ボタンを押すと、書き換わったデータがコンソールウインドウで確認できます
image.png

このような感じで、HTML側での入力値変更と、Vue.js側のデータ反映が、上記コードのみで実行できる訳です

大きく分けて、「①Vue.js側で保持しているデータ部分」と「②HTMLでのVue.js側データ表示」の2つによって実現されています

① Vue.js側で保持しているデータ部分(と初期化)

以下コードが、Vue.js側で保持しているデータ部分(data:の配下)と、その初期化(mounted)です

ページがロードされると、mountedが呼び出され、data:配下のresultsに、3件のデータが追加されます

index.html
    data: 
    {
        results:   [], 
    }, 
    mounted()
    {
        this.results.push( { id: 1, title: 'title1', body: 'body1' } )
        this.results.push( { id: 2, title: 'title2', body: 'body2' } )
        this.results.push( { id: 3, title: 'title3', body: 'body3' } )
    }, 

② HTMLでのVue.js側データ表示

以下HTMLで、上記で初期化されたVue.js側データを表示します

入力フィールドで変更されたデータをVue.js側データに反映するには、「v-model」という属性を使います

v-modelの中に書くのは、Vue.jsの「data:」部で定義したモデルとなります(実際には、v-forでバラされたモデル断片を指定しています)

なお、v-modelは、HTMLからVue.jsへの一方通行の反映では無く、Vue.jsからHTMLへの反映も兼ねているので、mountedでの初期化では、Vue.js側で「data:」部のモデルを書き換えることで、画面表示に自動反映されています(双方向データバインディングと呼ばれます)

index.html

<table border="1">
<tr v-for="result in results">
    <td style="padding: 10px;">{{ result.id }}</td>
    <td style="padding: 10px;"><input type="text" v-model="result.title"></td>
    <td style="padding: 10px;"><input type="text" v-model="result.body"></td>
</tr>
</table>
…

基本的には、ここまでのコードだけで、Vue.jsとHTMLとの間で、データの受け渡しが行なえます

このように非常に簡潔なコードにも関わらず、アプリ開発の根幹となる「表示」と「データ」の連携が自動的に行われることが、Vue.jsの最大の強みです

その他コード解説

さて、データの受け渡し以外の部分も見てみましょう

Vue.js側データのコンソールウインドウへのログ出力

以下HTMLとJSで、「データ表示」ボタンの表示と、クリック時のコンソールウインドウへのログ表示を行っています

「v-on:click」という属性を使うことで、クリック時のハンドラメソッドを指定できます

「v-on:click」以外にも、「v-on:change」や「v-on:focus」、「v-on:blur」等、JavaScriptで利用可能なハンドラと同じものをVue.jsでも利用できます

なお、引数を指定することもでき、引数指定無の場合は、クリックイベントが発生したパーツ(今回だとbutton)のDOMが、暗黙の引数として渡されます

index.html

<button v-on:click="onView">データ表示</button>
…
    methods: 
    {
        onView: async function( evt )
        {
            console.log( this.results )
        }, 
    }, 

Vue.jsの有効範囲

「var app = new Vue」で始まるブロックが、Vue.jsのメイン処理です

メイン処理の「el」で指定したものと同じidを、上方のdivタグで指定していますが、Vue.jsはこのdivタグの内部でのみ有効です

index.html

<div id="app">

</div>
…
var app = new Vue
( {
    el: '#app',

CDNでのライブラリ導入

先頭にある、URL指定で、Vue.jsと、APIアクセス用ライブラリ「axios」をロードしています

index.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>

このように、CDNのみでも、Vue.jsは利用できるので、心が折れること無く、気軽にVue.jsを始められます

Vue.jsと自前のAPIを連結する

では、このページをベースに、テーブルの中身のデータを、自前のAPIにすげ替えてみましょう

ここでは、自前のAPIをElixir+Phoenixで作ってみます

Elixirのインストール

Elixirを使い始めるのに、3種類の方法があります

  1. インストーラ/Homebrewを使う
  2. ソースコードからビルドする
  3. DockerでElixirイメージをインスト―ル (pull) する

Windows/macOSは1.、Linux含むUNIX系は2.、普段Dockerを使い慣れている方は3.がオススメです

1. インストーラ/Homebrewを使う

下記URLの手順に沿って、Elixirをインストールします
https://elixir-lang.org/install.html

Windowsはインストーラをダウンロードしてインストール、macOSはHomebrewでインストールと、簡単です

image.png

なお、Linux含むUNIX系の手順も記載されていますが、手順通りにすると、古いバージョンがインストールされるため、2. の方が良いです

2. ソースコードからビルドする

以下の手順通り、まずErlangをソースコードからビルドして、インストールします

なお、実施するタイミング次第では、新しいものがリリースされているかも知れないので、気になる方は、下記URLをチェックして、適宜、変更してください
http://erlang.org/download

wget http://erlang.org/download/otp_src_20.3.tar.gz
tar vzfx otp_src_20.3.tar.gz
cd otp_src_20.3/
./configure --enable-hipe
make && make install

次に、Elixirをソースコードからビルドして、インストールします

こちらも、Elixirのメジャーバージョンが新しくなっている場合は適宜、バージョンを変更してください

git clone https://github.com/elixir-lang/elixir/
cd elixir
git checkout v1.6
git pull
export PATH="${PATH}:/usr/local/bin"
make && make install
elixir -v

3. DockerでElixirイメージをインスト―ル (pull) する

下記URLを、「Docker Community Edition (CE)」までスクロールし、利用OS毎のDockerをインストールします
https://www.docker.com/get-docker

image.png

その後、以下コマンドでElixirイメージを入れます

docker pull trenpixster/elixir

以下コマンドで、Elixirイメージのコンテナを起動します

docker run -p 4000:4000 -i -t  trenpixster/elixir /bin/bash

PostgreSQLのインストール

下記OS毎のインストール手順を実施してください

なお、postgresユーザのパスワードは、「postgres」とするのを忘れず行ってください

Windows:https://eng-entrance.com/postgresql-download-install
macOS:https://qiita.com/okame_qiita/items/ac7b6a7d96d07ecbc50b
Ubuntu:https://qiita.com/eighty8/items/82063beab09ab9e41692
CentOS 7:https://weblabo.oscasierra.net/postgresql10-centos7-install/
CentOS 6:https://weblabo.oscasierra.net/postgresql-installing-postgresql9-centos6-1/

Phoenixのインストール

以下コマンドでPhoenixをインストールします

mix archive.install hex phx_new 1.4.0

Vue.js向けAPI用Phoenix PJを作成

Phoenix PJを作成します

mix phx.new vue_sample --no-webpack
Fetch and install dependencies? [Yn] (←y、Enterを入力)
…
cd vue_sample
mix ecto.create

Phoenixサーバーを起動します

iex -S mix phx.server

ブラウザで「http://localhost:4000」にアクセスすると、Phoenixで作られたWebページが見れます(この後も、このページを見ますので、閉じずに、開いたままにしておいてください)
image.png

PhoenixでAPIを作る

PhoenixでAPIを作るには、mixコマンドで、以下のように行います

Ctrl+cを2回押して、一度、Phoenixを停止してから、コマンドを入力します

mix phx.gen.json Api Post posts title:string body:text

以下ログと、実行後の作業指示が示されます

* creating lib/vue_sample_web/controllers/post_controller.ex
* creating lib/vue_sample_web/views/post_view.ex
* creating test/vue_sample_web/controllers/post_controller_test.exs
* creating lib/vue_sample_web/views/changeset_view.ex
* creating lib/vue_sample_web/controllers/fallback_controller.ex
* creating lib/vue_sample/api/post.ex
* creating priv/repo/migrations/20181021164507_create_posts.exs
* creating lib/vue_sample/api/api.ex
* injecting lib/vue_sample/api/api.ex
* creating test/vue_sample/api/api_test.exs
* injecting test/vue_sample/api/api_test.exs

Add the resource to your :api scope in lib/vue_sample_web/router.ex:

    resources "/posts", PostController, except: [:new, :edit]


Remember to update your repository by running migrations:

    $ mix ecto.migrate

まず、ルーティングにAPI用エントリーとして、上記「resources "/posts", ~」を、「get "/", ~」直下に追記します

lib/vue_sample_web/router.ex
defmodule VueSampleWeb.Router do
  use VueSampleWeb, :router
  
  scope "/", VueSampleWeb do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
    resources "/posts", PostController, except: [:new, :edit]
  end
  

マイグレートします

mix ecto.migrate

以下ログのように、テーブルが作成されます

01:51:55.226 [debug] Selecting all records by match specification `[{{:schema_migrations, :"$1", :"$2"}, [], [[:"$1"]]}]` with limit nil

01:51:55.282 [info]  == Running VueSample.Repo.Migrations.CreatePosts.change/0 forward

01:51:55.282 [info]  create table posts

01:51:55.310 [info]  == Migrated in 0.0s

router.exにデフォルト設定されているSCRF対策は、API利用時に不要かつ邪魔なので、解除します

lib/vue_sample_web/router.ex
defmodule VueSampleWeb.Router do
  use VueSampleWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
#   plug :protect_from_forgery
    plug :put_secure_browser_headers
  end
  

Phoenixを起動してください

iex -S mix phx.server

Vue.jsから自前のAPIを呼び出す

冒頭のVue.jsで作ったページをベースに、Phoenixで作成したAPIを呼び出すVue.jsへとindex.html.eexを置き換えます

処理概要は、以下の通りです

  • データ追加(1件ずつ)
    • POSTメソッドで追加APIを呼び出す
    • メソッドに「async」、axios.deleteに「await」を付け、「同期処理」化
    • 削除が完全に終わってから、データ取得するよう、「同期処理」にするため
    • 「非同期処理」だと、データ削除が完了する前にデータ取得する可能性がある
    • その後、データ取得し直すことで画面更新する
  • データ全件更新
    • v-modelで更新されたresultsを全件、PUTメソッドで更新APIを呼び出し続ける
    • results全件を更新に回す部分は、forEachを使うと、JSでも関数型っぽく書ける
  • 1件毎のデータ削除
    • DELETEメソッドで削除APIをid指定付きで呼び出す
    • 追加同様、「同期処理」化
    • その後、データ取得し直すことで画面更新する
lib/vue_sample_web/templates/page/index.html.eex
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>

<div id="app">

<h1>Posts</h1>
<table border="1">
<tr v-for="result in results">
    <td style="padding: 10px;"><input type="text" v-model="result.title"></td>
    <td style="padding: 10px;"><input type="text" v-model="result.body"></td>
    <td><button v-on:click="onDelete( result.id )">削除</button></td>
</tr>
<tr>
    <td style="padding: 10px;"><input type="text" v-model="new_title"></td>
    <td style="padding: 10px;"><input type="text" v-model="new_body"></td>
    <td><button v-on:click="onCreate">追加</button></td>
</tr>
</table>
<button v-on:click="onUpdate">全件更新</button>

</div>

<script>
    var app = new Vue
    ( {
        el: '#app',
        data: 
        {
            results:   [], 
            new_title: '', 
            new_body:  '', 
        }, 
        mounted()
        {
            axios.get( '/posts' )
            .then( response => { this.results = response.data.data } )
        }, 
        methods: 
        {
            onUpdate: function( evt )
            {
                this.results.forEach( ( result, i ) => 
                {
                    axios.put( '/posts/' + result.id, 
                        { 
                            'post':
                            {
                                'title': result.title, 
                                'body':  result.body, 
                            } 
                        } )
                } )
            }, 
            onDelete: async function( id )
            {
                await axios.delete( '/posts/' + id )

                axios.get( '/posts' )
                .then( response => { this.results = response.data.data } )
            }, 
            onCreate: async function( evt )
            {
                await axios.post( '/posts/', 
                    { 
                        'post':
                        {
                            'title': this.new_title, 
                            'body':  this.new_body, 
                        } 
                    } )

                this.new_title = ''
                this.new_body  = ''

                axios.get( '/posts' )
                .then( response => { this.results = response.data.data } )
            }, 
        }, 
    } )
</script>

上記ファイルを保存すると、データの追加/更新/削除ができるようになるので、色々遊んでみてください
image.png

p.s.「いいね」よろしくお願いします

ページ左上の image.pngimage.png のクリックを、どうぞよろしくお願いします:bow:
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください!:tada:

Browsing Latest Articles All 25 Live