Openshiftでrubyなwebsocketアプリを動かす


# 前回までのあらすじ
herokuではwebsocketが公式サポートされていて、検索するとRubyでのwebsocketサンプルもいっぱいでてくる。一方OpenShiftの情報はあまりヒットせず、まだβサポートだから不安定だとか、node.jsでしか駄目だとか、いやいやできるよだとか、現状どうなっているのかさっぱりわからない。こうすればできるという情報もヒットしたのでそれを元にherokuでのwebsocketサンプルをOpenShift上で動かして実際に試すことにした。

# 結論
2013年12月現在、OpenShift上のruby1.9カートリッジでwebsocketの通信に成功。

# やったこと
OpenShiftでwebsocketのruby実装を使うには、[Ruby websockets on OpenShift](http://mfojtik.im/ruby-websockets-on-openshift)の情報がわりと詳しい。ただし、肝心のサンプルが今のOpenShiftでは動作させられなかった上に、独自ライブラリを使った実装なので動作しない原因を調べるのもかなりきつい。
そこで、比較的単純でわかりやすいherokuでのサンプル実装を、上記サイトに書いてある注意点を読みながらOpenShiftで動くように改変していった。

## おおまかな手順

1. herokuで動作するruby-websockets-chat-demoをOpenShiftに乗せる
2. herokuで動かした経験を元にredisを使わないようにサンプルを改変する
3. OpenShiftにあわせてGemfile等の設定を変える
4. OpenShiftのrubyカートリッジでpumaを動作させる
5. websocketのポートをOpenShiftの仕様にあわせる

# ruby-websockets-chat-demoをOpenShiftに乗せる
OpenShiftで適当にアプリケーションを作成

```sh
$ rhc app create websocketchat -t ruby-1.9
Application Options
-------------------
  Domain:     l1s
  Cartridges: ruby-1.9
  Gear Size:  default
  Scaling:    no

Creating application 'websocketchat' ...
(略)
Run 'rhc show-app websocketchat' for more details about your app.
```

ruby-websockets-chat-demoをmergeする。[Using WebSockets on Heroku with Ruby](https://devcenter.heroku.com/articles/ruby-websockets)にサンプルのgithubへのリンクがあるので、[How to Migrate Your Heroku Ruby Application to OpenShift](https://www.openshift.com/blogs/heroku-ruby-app-migration)を参考にしてさっき作った空のアプリケーションにmerge。

```sh
$ cd websocketchat
$ git pull -s recursive -X theirs https://github.com/heroku-examples/ruby-websockets-chat-demo.git
warning: no common commits
remote: Counting objects: 48, done.
remote: Compressing objects: 100% (29/29), done.
remote: Total 48 (delta 12), reused 47 (delta 12)
Unpacking objects: 100% (48/48), done.
From https://github.com/heroku-examples/ruby-websockets-chat-demo
 * branch            HEAD       -> FETCH_HEAD
Auto-merging config.ru
Auto-merging README.md
Merge made by the 'recursive' strategy.
 Gemfile                                               |    8 +
 Gemfile.lock                                          |   28 +
(略)
 views/index.html.erb                                  |   34 +
 21 files changed, 9615 insertions(+), 297 deletions(-)
 create mode 100644 Gemfile
 create mode 100644 Gemfile.lock
(略)
 create mode 100644 views/index.html.erb
```

もともとが起源の違う二つのリポジトリのマージなので、マージ後はコミットが妙なグラフにはなるが気にしない。

# redisを使わないようにサンプルを改変する
この時点ではローカルでもrackupできない。サンプルでは複数ノードにスケールした時でも受けとったメッセージを共有するためにredisを使うコードになっていて、ローカルではredisの環境を設定してないからだ。今は複数ノードなんていらないのでredisの部分をがさっと削る。

以下変更したところだけ抜粋。

```ruby:middlewares/chat_backend.rb
require 'faye/websocket'
require 'thread'
# require 'redis'
require 'json'
require 'erb'

module ChatDemo
  class ChatBackend
    KEEPALIVE_TIME = 15 # in seconds
    CHANNEL        = "chat-demo"

    def initialize(app)
      @app     = app
      @clients = []
=begin
      uri = URI.parse(ENV["REDISCLOUD_URL"])
      @redis = Redis.new(host: uri.host, port: uri.port, password: uri.password)
      Thread.new do
        redis_sub = Redis.new(host: uri.host, port: uri.port, password: uri.password)
        redis_sub.subscribe(CHANNEL) do |on|
          on.message do |channel, msg|
            @clients.each {|ws| ws.send(msg) }
          end
        end
      end
=end
    end

    def call(env)
      if Faye::WebSocket.websocket?(env)
        ws = Faye::WebSocket.new(env, nil, {ping: KEEPALIVE_TIME })
        ws.on :open do |event|
          p [:open, ws.object_id]
          @clients << ws
        end

        ws.on :message do |event|
          p [:message, event.data]
          # @redis.publish(CHANNEL, sanitize(event.data))
          @clients.each {|c| c.send(sanitize(event.data))}
        end
```

これでローカルでもproductionオプションをつけると起動するようになる(productionにしないと何故かpumaがこける)。

```sh:
$ bundle exec rackup -E production
Puma 2.6.0 starting...
* Min threads: 0, max threads: 16
* Environment: production
* Listening on tcp://0.0.0.0:9292
```

ようやくHTMLが表示されて喜ぶが、productionならhttpsだよね。だからwss使うね、というおせっかいコードのせいで、httpで立ちあげているとwebsocket接続ができない。さらにOpenShiftではwsの待受ポートが特殊なのでやはりwebsocket通信はまだできない。後でまとめて変更することにする。

# OpenShiftにあわせてGemfile等の設定を変える
ローカルではそれなりに動いたのでOpenShiftにpushしてみると変なところでエラーがでている。
herokuではGemfileにrubyのバージョンを指定する`ruby`オプションが使えるが、OpenShiftでは使えないからのようだ。というわけで削る。ついでにもう使わないredisも削っておく。

```ruby:Gemfile
source "https://rubygems.org"

# ruby "2.0.0"

gem "faye-websocket"
gem "sinatra"
gem "puma"
# gem "redis"
```

`bundle install`をしてリポジトリを更新してから`git push origin master`をするとOpenShiftにアップロードされる。ローカルと同様にHTMLは表示される。やったね。だがしかしwebsocketを使うチャット部分は何も動かない。

# OpenShiftでpumaを動作させる
以下、ほぼ[Ruby websockets on Openshift](http://mfojtik.im/ruby-websockets-on-openshift)の受け売り。

OpenShiftのruby環境ではapache+passengerが使われており、非常にトリッキーなことをしないとwebsocketの通信ができないらしい。ただしOpenShiftには抜け道が用意されていて、passengerの代わりに他のweb serverを利用する方法がある。それを使ってruby製のwebserver pumaを起動してやるとwebsocketの通信も比較的簡単にできるよ、ということのようだ。確かにherokuのサンプルでもpumaを起動するようになっていた。

具体的にはOpenShiftが用意しているフックがあるので、そこにpassengerの停止とpumaの起動、pumaの停止のスクリプトを置く。実行属性を付けるのを忘れないように。

```sh:/.openshift/action_hooks/post_start_ruby-1.9
#!/bin/bash
echo "Replacing the default Passenger server with Puma"
pushd ${OPENSHIFT_REPO_DIR} > /dev/null
${HOME}/ruby/bin/control stop
set -e
PUMA_PID_FILE="${OPENSHIFT_DATA_DIR}puma.pid"
PUMA_BIND_URL="tcp://${OPENSHIFT_RUBY_IP}:${OPENSHIFT_RUBY_PORT}"
PUMA_OPTS="-d --pidfile ${PUMA_PID_FILE} -e production --bind '${PUMA_BIND_URL}'"
bundle exec "puma $PUMA_OPTS"
exit 0
```

```sh:/.openshift/action_hooks/deploy
#!/bin/bash
if [ -f "${OPENSHIFT_DATA_DIR}puma.pid" ]; then
  echo "Stopping Puma..."
  PUMA_PID=$(cat "${OPENSHIFT_DATA_DIR}puma.pid")
  ps -p $PUMA_PID &> /dev/null
  [ "$?" == 0 ] && kill $PUMA_PID
fi
```

OpenShiftにpushして確認する。先程とは違いpush後の起動ログでPumaの起動メッセージがでているのが確認できる。

```sh:
(略)
remote: Activating deployment
remote: Starting Ruby cartridge
remote: Replacing the default Passenger server with Puma
remote: Stopping Ruby cartridge
remote: Waiting for stop to finish
remote: Puma starting in single mode...
remote: * Version 2.6.0, codename: Pantsuit Party
remote: * Min threads: 0, max threads: 16
remote: * Environment: production
remote: * Listening on tcp://127.8.152.129:8080
remote: * Daemonizing...
remote: Result: success
remote: Activation status: success
remote: Deployment completed with status: success
(略)
```

もうひといきだ！さあ後はポートの設定だけだ。

# websocketsのポートをOpenShiftの仕様にあわせる
OpenShiftでは外からのwebsocketのポートは8000と8443に割りあてられている([Try Out Low-Latency Connections with WebSockets](https://www.openshift.com/blogs/paas-websockets)参照)。前者がhttp用で後者がhttps用。

サンプルではwebsocketの通信も80と443で行うようになっているのでそこを変更する。テストなのでwsの8000ポートに固定しておく。

```javascript:/views/application.js.erb
//var scheme   = "<%= @scheme %>";
var scheme   = "ws://";
//var uri      = scheme + window.document.location.host + "/";
var uri      = scheme + window.document.location.hostname + ":8000/";
var ws       = new WebSocket(uri);
ws.onmessage = function(message) {
  var data = JSON.parse(message.data);
  $("#chat-text").append("<div class='panel panel-default'><div class='panel-heading'>" + da\
ta.handle + "</div><div class='panel-body'>" + data.text + "</div></div>");
  $("#chat-text").stop().animate({
    scrollTop: $('#chat-text')[0].scrollHeight
  }, 800);
};

$("#input-form").on("submit", function(event) {
  event.preventDefault();
  var handle = $("#input-handle")[0].value;
  var text   = $("#input-text")[0].value;
  ws.send(JSON.stringify({ handle: handle, text: text }));
  $("#input-text")[0].value = "";
});
```

これでpushすればようやくチャットサンプルが動作する。複数ブラウザを立ちあげて１人チャットを楽しもう。おつかれさまでした。

この手順で作ったサンプルそのものを http://websocketchat-l1s.rhcloud.com/ で動かしているので、気になる方はどうぞ。しばらくは残しておくつもりですが無料枠が足りなくなったら消してしまうのであしからず。

# まとめ
OpenShiftのwebsocketは一年ほど前にpreview versionとしてリリースされているが、その後のステータスがどうなっているかは確認できなかった。ただし、いろいろ調べると現状でもpreview version時の情報はそのまま生きていて、それに従うこと(+サーバ置き換えという荒技)をすればrubyでもwebsocketを楽しめる。現に、herokuのwebsocketなチャットサンプルをインターフェイスまわりだけ修正することで動作させることができた。
ポイントは、

- Pumaを動かす
- Portの指定に注意する

の2点。
これに気をつけていればherokuのwebsocketのコードがすこしの変更で動きそうで夢が広がる。

# 蛇足
そもそもこの記事を書こうとして、ブラウザ上で編集するにしても、エディタで書いた文章をブラウザにペーストするにしても、推敲や編集がやりにくくてたまらなかったので、先の投稿スクリプトを書いた。技術の力ですこし幸せになった。

# 参考情報
- [Using WebSockets on Heroku with Ruby](https://devcenter.heroku.com/articles/ruby-websockets)
 - herokuのサポートページのwebsocketサンプル
- [How to Migrate Your Heroku Ruby Application to OpenShift](https://www.openshift.com/blogs/heroku-ruby-app-migration)
 - OpenShiftのブログ、herokuからのrubyアプリの移植方法
- [Try Out Low-Latency Connections with WebSockets](https://www.openshift.com/blogs/paas-websockets)
 - OpenShiftのブログ、websocketの使い方
- [Ruby websockets on Openshift](http://mfojtik.im/ruby-websockets-on-openshift)
 - OpenShiftでの簡単なruby websocketの動かし方
- [Qiitaに投稿するRubyスクリプト](http://qiita.com/lassie116/items/7042c3ac1f955f67fe1a)
 - Qiitaへ投稿する拙作スクリプトの紹介