0

HerokuでMongoDB+Sinatra

試しにメモ帳作った。
http://shokai-memo.herokuapp.com

ソースコード https://github.com/shokai/heroku-sinatra-mongo-memo


MongoHQMongoLabを使う。

MongoHQはHerokuが管理してるっぽくて16MBまで無料。MongoLabはObjectLabs Corporationがやってて240MBまで無料らしい。
MongoLabの方が明らかに得だけど、容量を2GBまで増やすとMongoHQの方が安くなる。


Herokuのプロジェクト作る

git init
heroku create --stack cedar


MongoDBのアドオンを有効にする

とりあえず無料プランで、どちらかをaddする
heroku addons:add mongolab:starter
heroku addons:add mongohq:free

するとMONGOLAB_URIかMONGOHQ_URLという環境変数が増えるので、確かめる
heroku config


RubyではMongoid2.4をMongoのラッパーとして使うのが良いと思う。
Herokuにデプロイしたら環境変数にMongoDBのアドレスとパスワード等が入った状態でSinatraが起動するので、読み込ませる
Mongoid.configure do |conf|
conf.from_hash {'uri' => ENV['MONGOLAB_URI'] || ENV['MONGOHQ_URL']}
end


最終的にこうなった。
mongoidは普通mongoid.ymlから設定を読む。
MongoLabやMongoHQの接続設定が環境変数にあったらそっちを使うし、無ければmongoid.ymlを読んでローカルのMongoDBに接続する。
Mongoid.logger.level =
case ENV['RACK_ENV']
when 'production'
Logger::WARN
else
Logger::DEBUG
end

Mongoid.configure do |conf|
h = {'uri' => ENV['MONGOLAB_URI'] || ENV['MONGOHQ_URL']}
unless h['uri']
yaml = YAML.load(open(File.expand_path 'mongoid.yml', File.dirname(__FILE__)).read)
h = yaml[ ENV['RACK_ENV'] || 'development' ]
end
conf.from_hash h
end



メモ帳作る

Memo modelを定義する
class Memo
include Mongoid::Document
field :created_at, :type => Time, :default => lambda{ Time.now }
field :body, :type => String, :default => ""

def self.find_by_id(id)
self.where(:_id => id).first
end

def self.latests(num=10)
self.all.desc(:created_at).limit(num)
end

def to_s
"#{body} - #{created_at}"
end
end


適当にSinatraで書いて完成。
get '/' do
mems = Memo.latests(100).map{|m|
"<p>#{Rack::Utils.escape_html m.body} - <a href='/#{m.id}'>#{m.created_at}<a><p>"
}.join('')

"<html><form method='POST' action='/'><input type='text' name='body' size=70 /><input type='submit' /></form>#{mems}</html>"
end

post '/' do
m = Memo.new :body => params[:body]
m.save!
redirect '/'
end

get '/:id' do
m = Memo.find_by_id params[:id]
halt 404, 'not found' unless m
"<html><p><a href='/'>top</a></p><p>#{Rack::Utils.escape_html m.to_s}</p></html>"
end


ローカルのRubyからHerokuのMongoDBに接続する


手元で起動したSinatraも、環境変数さえセットされていればHerokuのMongoDBに接続される
heroku config --shell | ruby -lane 'puts "export "+$_'
でてきたMONGOLAB_URIとかをターミナルにコピペすれば環境変数をセットできる。


管理画面

https://api.heroku.com/myappsからアプリを選んで、右上のaddonsからMongoLab/MongoHQの管理画面に行ける。

0

スケール設定してないRubyで書かれたHerokuアプリを止める方法

いままでPassenger+Apacheとか、あらかじめforkさせて複数プロセス待機させておくとかしていたから気づかなかった。
Herokuにお金を払ってwebサーバー複数プロセス起動させているなら問題ないと思います。


1リクエストずつしか処理できない

HerokuでRailsやSinatraを使うときは、Rubyで書かれたwebサーバー(thinとか)が1プロセスだけでそのまま動いているのだが、webrick/mongrel/thinあたりは1リクエストずつ順番にしか処理できない。


つまりこれとかがそうなんだけど、
http://shokai.herokuapp.com/base64img
リクエスト処理中のアプリ自身にリクエストを送ってしまうと固まってしまう。引数にURLを渡されて別のサイトにHTTPリクエストするのを想定して作られているアプリとかに有り得るミスだと思う。

上は画像のURLを渡されるとbase64encodeしたimgタグを作ってくれるページなんだけど、ここにhttp://shokai.herokuapp.comとか入れると固まるのだった。
(今は固まらないようにしてある)

リクエストうける→自分自身にリクエストしてbase64encodeしようとする→1リクエストずつしか処理できないので、待たされる→最初のレスポンスを返せない!!
となる。


対策

上の画像をbase64エンコードするアプリの例
get '/base64img' do
halt 400, 'bad request' if params[:url] =~ /^https?:\/\/#{env['HTTP_HOST']}#{env['SCRIPT_NAME']}/

## 処理続き〜〜
end

こんな感じでリクエスト送る前にチェックしてhaltすると良いと思う。

でもこれだけだとbitly等の短縮URLを使うとすり抜けられる。
どのHTTPライブラリを使うかにもよるけど、自動的に30x系リダイレクト処理をしてくれるライブラリを使う場合はいちいちHTTPヘッダのLocationの指す先を調べないと、HTTPリクエスト処理中の自分自身にHTTPリクエストしてしまう。


ちなみにNode.jsだと複数リクエスト同時に処理できるので全然問題無かった。

0

HerokuでSinatra+Memcached使う

Memcached使ってみたかった。なんかキャッシュしなきゃならんので
ブラウザ <---> Heroku <---(cache)---> Twitter
なものを作った。

できたもの http://twiticon.herokuapp.com/

twitterのユーザーアイコンをHTMLに簡単に貼れるやつ。ソースコードはここ

こういうHTMLでアイコンがでる。
<img src="http://twiticon.herokuapp.com/shokai">


小さいのや大きいのも貼れる。

テキストや http://twiticon.herokuapp.com/shokai.txt
JSONでも取得できる http://twiticon.herokuapp.com/shokai.json


使い方は http://twiticon.herokuapp.com に詳しく書いてある。


Twitterのアイコン

ここにAPIがある
GET users/profile_image/:screen_name | Twitter Developers
ユーザーが新しいアイコンをアップロードする毎にURLが変わる。
例えば今の俺のアイコンは https://si0.twimg.com/profile_images/2328443341/tmp_normal.png になってる。


Memcached

ふつうのSQL DBみたいにHDDにデータは保存しないけど、メモリ上で高速に動作する。
あとExpireする期限を決めれる。今回は保存したアイコンURLは12時間で消滅するようにした。
12時間1秒経過すると、再度Twitter APIを使ってURLを取りに行く。


Herokuでmemcached使う

Memcache | Heroku Dev Centerにドキュメントがある。
Herokuのmemcachedはユーザー名とパスワードによる認証があって、SASLというプロトコルを使っているので対応しているgemじゃないと使えない。
dalliというpure rubyのgemが推奨されている。

いつも使ってるCで書かれてるmemcached gemはSASLサポートしてるって記述があるんだけど、使い方がドキュメントに書いてないし面倒臭くなってdalliに落ち着いた。


Herokuにmemcached addon追加した。無料だけどクレジットカード番号登録しないと使わせてもらえない。
% heroku create --stack cedar
% heroku addons:add memcache:5mb


パスワードとかは
% heroku config
で見れる。
MEMCACHE_PASSWORD, MEMCACHE_SERVERS, MEMCACHE_USERNAMEがそれ。
これがHerokuで動かしてるアプリの環境変数に入るので、ENV[‘MEMCACHE_SERVERS’]とかで取り出す。


dalliでMemcached使う

認証なし
require 'rubygems'
require 'dalli'
cache = Dalli::Client.new 'localhost:11211'

Heroku用
cache = Dalli::Client.new ENV['MEMCACHE_SERVERS'], {:username => ENV['MEMCACHE_USERNAME'], :password => ENV['MEMCACHE_PASSWORD']}


で、あとはcacheにset/getすればよい。
# cache 1 hour
cache.set('icon_shokai', 'https://si0.twimg.com/profile_images/2328443341/tmp_normal.png', 3600)

# get icon
puts cache.get('icon_shokai')


ちなみにHerokuのmemcachedは自分のローカル環境からも接続できる。
addons:add memcachedしたらすぐに起動するので、手元で開発してるプログラムで
cache = Dalli::Client.new 'xxxxx.ec2.northscale.net', {:username => '123456heroku.com', :password => 'asdf122345hujiko'}
とか書いても動かせる。太平洋横断してるから遅いけど。


ローカル開発環境にMemcachedをインストール

homebrewやapt-getでインストールできる。
% brew install memcached
% memcached -vv -p 11211
これでlocalhost:11211で起動する。


SinatraでMemcachedを使う

ふつうに上に書いたのを組み合わせてredirectするだけなのでgithub見ればわかると思う。
https://github.com/shokai/twiticon


controllerでは
redirect icon user, size
とだけ書いて、iconって関数は適当にキャッシュもしてくれるように書かれている


あと開発時はローカルのlocalhost:11211のmemcachedを見て欲しいので、環境変数が無かったらconfig.ymlから同じ値を探すようにしたらはかどった。


感想

memcache無料の5MBぐらいだと、tmp_cache gem使ったほうが設定もいらないし楽なのでは・・・

0

sinatra-contrib

sinatra-content-forの0.2がrubygemsから消えた。
どこ行ってしまったのかと思ったら
https://github.com/sinatra/sinatra-contrib
最新版はsinatra-contribに移動してた。

インストールして

gem install sinatra-contrib

いつも通り使える。
require 'sinatra/content_for'

sinatra-reloaderもsinatra-contribに入ってた。

0

Sinatra+Haml+jQueryテンプレートにDataMapper版とMongoid版を追加した

去年作ったSinatra+Haml+jQuery入門のテンプレ、なにげにしょっちゅうアップデートしている。
内容は↑にも書いてあるがおみくじを引くだけの超単純なアプリだ。
新規プロジェクトを開始する時はコレをcloneして使うので、余計な物を付けない様にしている。

githubのリポジトリはここ https://github.com/shokai/sinatra-template


■使っている部品
今はこういう構成になっている

  • Ruby 1.8.7
  • Sinatra 1.3
  • Haml + sinatra-content-for
  • Sass
  • jQuery
  • foreman
  • bundler


master以外のブランチにmongoiddm-mysqlの2つがある。
それぞれDataMapper+MySQLと、Mongoid+MongoDBをバックエンドにしたやつ。


DataMapper+MySQL版はこんなファイル構成にしている。(Mongoidもほぼ同じ)
.
├── Gemfile # 必要なGEMを書いておくファイル。bundle installで一気にインストールされる。
├── Procfile # webサーバーと同時に起動するプロセスを書いておく
├── README.md
├── bin
│   ├── console.rb # DB接続してModel読み込んだ状態でIRBが起動する
│   ├── db_migrate.rb # DBを初期化するツール
│   └── db_upgrate.rb # model更新した時にDBスキーマを更新するツール
├── bootstrap.rb # これを読み込むとconfig.ymlを読んだりmodels/controllers/hellpersを一括読み込みしたりできる
├── config.ru # RackUp用の設定
├── config.yml # 設定ファイル
├── controllers # sinatraのルーティングメソッドを書いたファイルを入れるディレクトリ
│   ├── css.rb
│   └── main.rb
├── helpers # 便利関数を入れるディレクトリ
│   └── helper.rb
├── inits # DB接続とか、一番最初に実行すべき処理を書く
│   └── db.rb
├── models
│   └── omikuji.rb # DBのQueryとかを集約する
├── public
│   └── js
│   ├── jquery.js
│   └── main.js # index.hamlから読まれるJS
├── sample.config.yml # config.ymlにリネームして使う
└── views
├── index.haml # レイアウト抜きのメインコンテンツ部分
├── layout.haml # レイアウト
└── main.scss # cssのテンプレ


■DataMapperとMySQLを使う
MySQL版はこんなの(デモ)

MySQLのセットアップは以前書いた


この下に書いてある事はREADMEにも書いてある。


cloneしてくる
% git clone git://github.com/shokai/sinatra-template.git
% cd sinatra-template
% git branch -a
% git checkout -b dm-mysql remotes/origin/dm-mysql


設定
% cp sample.config.yml config.yml
config.ymlのMySQLのパスワード等を変更する


DB作る。最初のDB作る部分はログインして自分で作らないとならない。
% mysql -u your_name -p
mysql> create database sinatra_template
% ruby bin/db_migrate.rb
DB初期化するけどいいか?と訊かれるのでYesと答えよう。


起動
% gem install foreman bundler
% bundle install
% foreman start
http://localhost:8080 で起動する。


なんかすごい当たり前の話なんだけど、単純なWebアプリではなく、クローラとか色々と組み合わせたアプリを書くことが多い。
クローラとWebアプリが同じModelを読み込んで、同じ設定ファイルを読んで同じDBに接続するようにすると、綺麗に実装できる。

conosle.rbみたいな書き方をすると良い。

#!/usr/bin/env ruby
require 'rubygems'
require 'bundler/setup'
require File.dirname(__FILE__)+'/../bootstrap'
Bootstrap.init :inits, :models
これだけでDB接続してModelを読み込めるので、あとは普通に書けばいい。

設定ファイル(config.yml)の中身も、main.rbでやっているように
@title = Conf['title']
result = Conf['omikuji'].choice
みたいに取り出せる。