Ruby on Rails 版 Worker

Rails アプリケーションの作成

公開しているソースコードか Docker イメージを利用する場合は、本ステップは不要です。

アプリケーションの作成

Rails アプリケーションを作成するため、まずは gem で Rails をインストールします。

$ gem install rails
Fetching: thread_safe-0.3.5.gem (100%)
Successfully installed thread_safe-0.3.5

...

Fetching: activemodel-4.2.4.gem (100%)
Successfully installed activemodel-4.2.4
Fetching: activerecord-4.2.4.gem (100%)
Successfully installed activerecord-4.2.4
Fetching: rails-4.2.4.gem (100%)
Successfully installed rails-4.2.4
29 gems installed

任意のディレクトリに移動して、新しく Rails アプリケーションを作成します。

$ cd /path/to/your-directory

ここではアプリケーション名を、rails-worker-example とします。データベースとして MySQL を使うためデータベース指定を mysql とします。

$ rails new rails-worker-example --database=mysql
      create
      create  README.rdoc
...

      run  bundle install
Fetching gem metadata from https://rubygems.org/............
Fetching version metadata from https://rubygems.org/...
Fetching dependency metadata from https://rubygems.org/..
Resolving dependencies.....

...

Bundle complete! 12 Gemfile dependencies, 53 gems now installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.
         run  bundle exec spring binstub --all
* bin/rake: spring inserted
* bin/rails: spring inserted

上記のように、rails new を実行すると、引数で指定した rails-worker-example と同名のディレクトリが作成されます。以降は、この rails-worker-example 内で作業していきます。

rails-worker-example ディレクトリに移動して、作成されたファイルとディレクトリの一覧を確認しておきましょう。一覧中に表示されている数値や日時は、環境によって異なります。また、作成されるファイルやディレクトリも Rails のバージョンによっては、異なることがあります。

$ cd rails-worker-example
$ ls -la
total 24
drwxr-xr-x 1 magellan staff  612  8月 25 07:23 .
drwxr-xr-x 1 magellan staff  136  8月 25 07:23 ..
-rw-r--r-- 1 magellan staff  399  8月 25 07:23 .gitignore
-rw-r--r-- 1 magellan staff 1499  8月 25 07:23 Gemfile
-rw-r--r-- 1 magellan staff 3801  8月 25 07:23 Gemfile.lock
-rw-r--r-- 1 magellan staff  478  8月 25 07:23 README.rdoc
-rw-r--r-- 1 magellan staff  249  8月 25 07:23 Rakefile
drwxr-xr-x 1 magellan staff  272  8月 25 07:23 app
drwxr-xr-x 1 magellan staff  238  8月 25 07:23 bin
drwxr-xr-x 1 magellan staff  374  8月 25 07:23 config
-rw-r--r-- 1 magellan staff  153  8月 25 07:23 config.ru
drwxr-xr-x 1 magellan staff  102  8月 25 07:23 db
drwxr-xr-x 1 magellan staff  136  8月 25 07:23 lib
drwxr-xr-x 1 magellan staff  102  8月 25 07:23 log
drwxr-xr-x 1 magellan staff  238  8月 25 07:23 public
drwxr-xr-x 1 magellan staff  306  8月 25 07:23 test
drwxr-xr-x 1 magellan staff  102  8月 25 07:23 tmp
drwxr-xr-x 1 magellan staff  102  8月 25 07:23 vendor

Rails アプリケーションの準備ができたので、データベース接続設定を記述します。

データベース接続設定の作成

データベースへの接続設定は作成した Rails アプリケーションディレクトリの config/database.yml に記述されています。

この設定をローカルマシンのデータベース設定に書き換えます。

ここでは、次のように host の設定を追記し、socket の記述をコメントにします。socket の記述がない場合は、host のみの設定となります。

  host: localhost
  # socket: /var/run/mysqld/mysqld.sock

以下に、hostsocket の修正を反映した config/database.yml の全体を示します。

# MySQL.  Versions 5.0+ are recommended.
#
# Install the MYSQL driver
#   gem install mysql2
#
# Ensure the MySQL gem is defined in your Gemfile
#   gem 'mysql2'
#
# And be sure to use new-style password hashing:
#   http://dev.mysql.com/doc/refman/5.0/en/old-client.html
#
default: &default
  adapter: mysql2
  encoding: utf8
  pool: 5
  username: root
  password:
  host: localhost
  # socket: /var/run/mysqld/mysqld.sock

development:
  <<: *default
  database: rails-worker-example_development

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: rails-worker-example_test

# As with config/secrets.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
# Instead, provide the password as a unix environment variable when you boot
# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
# for a full rundown on how to provide these environment variables in a
# production deployment.
#
# On Heroku and other platform providers, you may have a full connection URL
# available as an environment variable. For example:
#
#   DATABASE_URL="mysql2://myuser:mypass@localhost/somedatabase"
#
# You can use this database configuration with:
#
#   production:
#     url: <%= ENV['DATABASE_URL'] %>
#
production:
  <<: *default
  database: rails-worker-example_production
  username: rails-worker-example
  password: <%= ENV['RAILS-WORKER-EXAMPLE_DATABASE_PASSWORD'] %>

データベース接続設定を記述したら、db:create を実行して開発用のデータベースを作成します。

次のコマンドを実行する前に、MySQL を起動しておいてください。

$ rake db:create

MySQL コマンドラインクライアント(mysql)を使って、MySQL に接続してデータベースが作成されていることを確認します。

$ mysql -uroot
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 5.6.26 MySQL Community Server (GPL)

Copyright (c) 2000, 2015, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

show databases; と入力して、rails-worker-example_developmentrails-worker-example_test が一覧表に表示されることを確認します。

mysql> show databases;
+----------------------------------+
| Database                         |
+----------------------------------+
| information_schema               |
| mysql                            |
| performance_schema               |
| rails-worker-example_development |
| rails-worker-example_test        |
+----------------------------------+
5 rows in set (0.00 sec)

quit; と入力して、MySQL コマンドラインクライアントを終了します。

mysql> quit;
Bye

サーバーを起動して確認してみます。

$ rails server
=> Booting WEBrick
=> Rails 4.2.4 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
=> Ctrl-C to shutdown server
[2015-08-25 07:32:10] INFO  WEBrick 1.3.1
[2015-08-25 07:32:10] INFO  ruby 2.2.3 (2015-08-18) [x86_64-linux]
[2015-08-25 07:32:10] INFO  WEBrick::HTTPServer#start: pid=1988 port=3000

ブラウザーから http://localhost:3000/ にアクセスして、Rails の “Welcome aboard” 画面が表示されたら準備は完了です。

アプリケーションの実装を行なっていきます。

ルーム機能

本チュートリアルでは、固定で 10 個のルームを準備し、ユーザはその中から任意のルームを選択してチャットを行なうことができるようにします。

ルームモデルの実装

まずは、ルームモデルを作成します。

ルームモデルは以下のようなカラムを持つものとします。

カラム名 説明
name string ルームの名前
$ rails generate model room name:string
      invoke  active_record
      create    db/migrate/20150824223624_create_rooms.rb
      create    app/models/room.rb
      invoke    test_unit
      create      test/models/room_test.rb
      create      test/fixtures/rooms.yml

マイグレーションファイルが作成されるので、マイグレーションを実行します。

マイグレーションファイルの日時部分(20150824223624)は、実行するタイミングによって異なります。

$ rake db:migrate
== 20150824223624 CreateRooms: migrating ======================================
-- create_table(:rooms)
   -> 0.0117s
== 20150824223624 CreateRooms: migrated (0.0133s) =============================

本サンプルでは固定でルームを用意するため、予めデータを投入できるようにします。

db/seeds.rb を開いて以下を追記します。

# 10個のルームを作成する
10.times do |i|
  Room.create({name: "Room#{sprintf('%03d', i)}"})
end

記述したら db:seed コマンドを実行してデータを投入します。

$ rake db:seed

MySQL コマンドラインクライアントでテーブルの内容を確認します。

$ mysql -uroot

use rails-worker-example_development と入力して、操作対象のデータベースを rails-worker-example_development に切り替えます。

mysql> use rails-worker-example_development
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

show tables; と入力して、rooms テーブルが作成されていることを確認します。

mysql> show tables;
+--------------------------------------------+
| Tables_in_rails-worker-example_development |
+--------------------------------------------+
| rooms                                      |
| schema_migrations                          |
+--------------------------------------------+
2 rows in set (0.00 sec)

select * from rooms; と入力して、rooms テーブルにデータが投入されてることを確認します。

mysql> select * from rooms;
+----+---------+---------------------+---------------------+
| id | name    | created_at          | updated_at          |
+----+---------+---------------------+---------------------+
|  1 | Room000 | 2015-08-24 22:40:19 | 2015-08-24 22:40:19 |
|  2 | Room001 | 2015-08-24 22:40:19 | 2015-08-24 22:40:19 |
|  3 | Room002 | 2015-08-24 22:40:19 | 2015-08-24 22:40:19 |
|  4 | Room003 | 2015-08-24 22:40:19 | 2015-08-24 22:40:19 |
|  5 | Room004 | 2015-08-24 22:40:19 | 2015-08-24 22:40:19 |
|  6 | Room005 | 2015-08-24 22:40:19 | 2015-08-24 22:40:19 |
|  7 | Room006 | 2015-08-24 22:40:19 | 2015-08-24 22:40:19 |
|  8 | Room007 | 2015-08-24 22:40:19 | 2015-08-24 22:40:19 |
|  9 | Room008 | 2015-08-24 22:40:19 | 2015-08-24 22:40:19 |
| 10 | Room009 | 2015-08-24 22:40:19 | 2015-08-24 22:40:19 |
+----+---------+---------------------+---------------------+
10 rows in set (0.00 sec)

日時部分は、マイグレーションを実行したタイミングの日時となります。例示とお手元の表示とでは異なります。以降、このデータを引用している例示はすべて同様となります。

quit; と入力して、MySQL コマンドラインクライアントを終了します。

mysql> quit;
Bye

ルームコントローラの実装

コントローラを作成します。

$ rails generate controller rooms
      create  app/controllers/rooms_controller.rb
      invoke  erb
      create    app/views/rooms
      invoke  test_unit
      create    test/controllers/rooms_controller_test.rb
      invoke  helper
      create    app/helpers/rooms_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/rooms.coffee
      invoke    scss
      create      app/assets/stylesheets/rooms.scss

ルーム一覧の実装

ルーム一覧を返すため index アクションを実装します。

app/controllers/rooms_controller.rb を開き、以下の実装を行ないます。

class RoomsController < ApplicationController

  # GET: /rooms
  def index
    rooms = Room.all
    render json: rooms
  end
end

Room モデルを通じてデータを全て取得し、JSON データとして返すようにします。

Routing の追加

/rooms でアクセスできるように、config/routes.rb にルーティング設定を追加します。

  resources :rooms, only: [:index] do
  end

curl コマンドなどを利用して、データを取得してみます(ブラウザーでもアクセス可能です)。

サーバーを起動していない場合は rails server としてサーバーを起動してください。

$ curl http://localhost:3000/rooms.json
[{"id":1,"name":"Room000","created_at":"2015-08-24T22:40:19.000Z","updated_at":"2015-08-24T22:40:19.000Z"},{"id":2,"name":"Room001","created_at":"2015-08-24T22:40:19.000Z","updated_at":"2015-08-24T22:40:19.000Z"},{"id":3,"name":"Room002","created_at":"2015-08-24T22:40:19.000Z","updated_at":"2015-08-24T22:40:19.000Z"},{"id":4,"name":"Room003","created_at":"2015-08-24T22:40:19.000Z","updated_at":"2015-08-24T22:40:19.000Z"},{"id":5,"name":"Room004","created_at":"2015-08-24T22:40:19.000Z","updated_at":"2015-08-24T22:40:19.000Z"},{"id":6,"name":"Room005","created_at":"2015-08-24T22:40:19.000Z","updated_at":"2015-08-24T22:40:19.000Z"},{"id":7,"name":"Room006","created_at":"2015-08-24T22:40:19.000Z","updated_at":"2015-08-24T22:40:19.000Z"},{"id":8,"name":"Room007","created_at":"2015-08-24T22:40:19.000Z","updated_at":"2015-08-24T22:40:19.000Z"},{"id":9,"name":"Room008","created_at":"2015-08-24T22:40:19.000Z","updated_at":"2015-08-24T22:40:19.000Z"},{"id":10,"name":"Room009","created_at":"2015-08-24T22:40:19.000Z","updated_at":"2015-08-24T22:40:19.000Z"}]

ユーザ機能

ユーザがルームに入室したことを管理するためにユーザ機能を実装します。

ユーザ機能では以下のような機能をもったコントローラを実装します。

  • ユーザがルームへ入室する
  • ユーザがルームから退室する

ルームユーザモデルの作成

まずは、ルームユーザを作成します。

ルームユーザモデルはルームに入室しているユーザのデータを保持します。

rails generate でルームユーザモデルを以下のようなカラムで作成します。

カラム名 説明
room_id integer ルームのID
name string ユーザの名前
$ rails generate model room_user room_id:integer name:string
      invoke  active_record
      create    db/migrate/20150824224510_create_room_users.rb
      create    app/models/room_user.rb
      invoke    test_unit
      create      test/models/room_user_test.rb
      create      test/fixtures/room_users.yml

マイグレーションファイルも作成されているので、room_id と name を必須項目とするため、null: false を追記します。 db/migrate/20150824224510_create_room_users.rb を開いて以下のように編集します。

ファイル名の日時部分(20150824224510)は、異なります。ご自身の環境で生成されているファイルの日時に読み替えてください。

class CreateRoomUsers < ActiveRecord::Migration
  def change
    create_table :room_users do |t|
      t.integer :room_id, null: false
      t.string :name,     null: false

      t.timestamps null: false
    end
  end
end

rake db:migrate を実行します。

$ rake db:migrate
== 20150824224510 CreateRoomUsers: migrating ==================================
-- create_table(:room_users)
   -> 0.0105s
== 20150824224510 CreateRoomUsers: migrated (0.0112s) =========================

room_users テーブルが作成されます。

room_id と name を必須項目にするためバリデーションを追加します。

app/models/room_user.rb を開き以下のように編集します。

class RoomUser < ActiveRecord::Base

  validates :room_id, presence: true
  validates :name,    presence: true

end

ユーザコントローラの作成

ユーザのルームへの入室、退室を実装するためにコントローラを実装します。

rails generate を使ってコントローラを生成します。

$ rails generate controller users
      create  app/controllers/users_controller.rb
      invoke  erb
      create    app/views/users
      invoke  test_unit
      create    test/controllers/users_controller_test.rb
      invoke  helper
      create    app/helpers/users_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/users.coffee
      invoke    scss
      create      app/assets/stylesheets/users.scss

Routing の編集

ユーザの機能にアクセスできるようにルーティング設定を追加します。 ユーザはルームの下にあるため、先に作成した config/routes.rbresources :rooms ブロック内に以下のように記述します。

  resources :rooms, only: [:index] do
    resources :users, only: [:create, :destroy, :index] do
    end
  end

入室処理の実装

app/controllers/users_controller.rb を開いて以下の処理を実装します。

# coding: utf-8
class UsersController < ApplicationController

  # POST: /rooms/:room_id/users
  # ユーザのルームへの入室を記録
  def create
    room = Room.find(params[:room_id])
    username = params[:name]
    user = RoomUser.new(room_id: room.id, name: username)
    user.save!
    render json: {result: true, id: user.id, name: user.name}
  rescue ActiveRecord::RecordNotFound
    render json: {result: false, error: "Room is not found!!"}
  rescue ActiveRecord::RecordInvalid => err
    render json: {result: false, error: err.message}
  end

end

このまま POST や PUT メソッドを送信すると ActionController::InvalidAuthenticityToken が発生するため、 app/controllers/application_controller.rb を開き、以下のように編集します。

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  # protect_from_forgery with: :exception
  protect_from_forgery with: :null_session
end

curl コマンドを使って確認してみます。

$ curl -F "name=tarou" http://localhost:3000/rooms/1/users
{"result":true,"id":1,"name":"tarou"}

データが作成されているかを rails console を使って確認します。

$ rails console
Loading development environment (Rails 4.2.4)
irb(main):001:0> RoomUser.all
  RoomUser Load (0.4ms)  SELECT `room_users`.* FROM `room_users`
=> #<ActiveRecord::Relation [#<RoomUser id: 1, room_id: 1, name: "tarou", created_at: "2015-08-24 22:50:29", updated_at: "2015-08-24 22:50:29">]>

入室してデータが作成されました。

退室処理の実装

次に退出処理を実装します。

app/controllers/users_controller.rb に以下のメソッドを追記します。

  # DELETE: /rooms/:room_id/users/:id
  # ユーザのルームからの退出を記録
  def destroy
    room = Room.find(params[:room_id])
    user = RoomUser.find_by(room_id: room.id, name: params[:id])
    if user
      user.destroy
      render json: {result: true}
    else
      render json: {result: false, error: "User is not found!!"}
    end
  rescue ActiveRecord::RecordNotFound
    render json: {result: false, error: "Room or User is not found!!"}
  rescue ActiveRecord::RecordInvalid => err
    render json: {result: false, error: err.message}
  end

curl コマンドを使って確認してみます。

$ curl -X DELETE http://localhost:3000/rooms/1/users/tarou
{"result":true}

データが作成されているかを rails console を使って確認します。

$ rails console
Loading development environment (Rails 4.2.4)
irb(main):001:0> RoomUser.all
  RoomUser Load (0.5ms)  SELECT `room_users`.* FROM `room_users`
=> #<ActiveRecord::Relation []>

データが空になり正常に削除されていることが確認できました。

同じユーザを削除しようとするとエラーになります。

$ curl -X DELETE http://localhost:3000/rooms/1/users/tarou
{"result":false,"error":"User is not found!!"}

ルームのユーザ一覧の取得処理

次に指定したルームに入室中のユーザの一覧を返すメソッドを実装します。

再度、app/controllers/users_controller.rb を開き以下を追記します。

  # GET: /rooms/:room_id/users
  # ルームに入室しているユーザの一覧を返す。
  def index
    room = Room.find(params[:room_id])
    users = RoomUser.where(room_id: room.id).all
    users_json = users.map{|user| {id: user.id, name: user.name, login_at: user.created_at} }
    render json: {result: true, users: users_json}
  rescue ActiveRecord::RecordNotFound
    render json: {result: false, error: "Room is not found!!"}
  end

curl コマンドで入室と一覧作成を確認します。

$ curl -F "name=tarou" http://localhost:3000/rooms/1/users
{"result":true,"id":2,"name":"tarou"}
$ curl http://localhost:3000/rooms/1/users
{"result":true,"users":[{"id":2,"name":"tarou","login_at":"2015-08-24T22:54:22.000Z"}]}
$ curl -X DELETE http://localhost:3000/rooms/1/users/tarou
{"result":true}
$ curl http://localhost:3000/rooms/1/users
{"result":true,"users":[]}

Subscriber の作成

MAGELLAN では、MQTT でのメッセージのやり取りが可能です。

通常は MQTT でメッセージの送信 (Publish) が行なわれた場合、送信時に指定した名前 (Topic) を購読 (Subscribe) しているクライアントに対して、メッセージを送信します。

MAGELLAN では メッセージをクライアントだけではなく、Worker に対しても送信することができます。

tips用アイコン

現時点では、MQTT は QoS 0 のみの対応となっています。 また、Worker にメッセージを送信する場合は、Topic 名を worker/ から始める必要があります。

MAGELLAN の MQTT 仕様については、「MQTT の仕様 」を参照してください。

本サンプルでは以下の機能をもつ Subscriber を実装します。

  • ユーザの Subscribe 開始時にユーザ名をルームのユーザ一覧に追加する
  • ルームに対する発言のログを保存する

RoomsSubscriber の実装

MQTT からの Publish メッセージを受け取って処理を行なうには、 Rails のコントローラではなく、MQTT 用の Subscriber を作成する必要があります。

Subscriber の作りはコントローラと似ており、Magellan::Subscriber::Base を継承することで Subscriber として動作します。 Subscriber は app/subscribers に格納するため、まずは app/subscribers ディレクトリを作成します。

$ mkdir app/subscribers

次に以下のようなファイルを app/subscribersrooms_subscriber.rb として作成します。

class RoomsSubscriber < Magellan::Subscriber::Base

  # PUBLISH: worker/rooms/*
  def logging
    logger.info("topic: #{topic}, message: #{body}")
  end
end

この Subscriber は、単純にトピックとメッセージをログに出力するだけです。Subscriber 内では、次のメソッドが使えます。

メソッド 説明
topic Publish されたトピック名を返します。
body Publish されたメッセージを返します。

subscriptions.yml の作成

Publish されたメッセージをハンドリングした後、Topic に合わせてどの Subscriber のどのメソッドを実行すべきかを subscriptions.yml というファイルに設定する必要があります。

config/subscriptions.yml というファイルを作成し、以下の内容を記述します。

worker/rooms/#: "rooms#logging"

ここまで作成したら基本的な機能は実装したので、一度 MAGELLAN 環境にデプロイを行ない、動作確認を行ないます。