ラベル Rails の投稿を表示しています。 すべての投稿を表示
ラベル Rails の投稿を表示しています。 すべての投稿を表示

2017/10/07

Akatsuki を OSS 化した

Akatsuki を OSS 化して GitHub で公開しました。



Akatsuki とは琉球大学情報工学科のシステムです。具体的には
  • 学生のIP申請
    • 対応するドメインを内向き DNS へと登録
    • その IP を DHCP で配れるように
  • 学生のVM新規作成/起動
  • 学生のパスワード変更
    • unix login
    • 802.11X で無線に繋ぐ際の RADIUS を提供
といったことができます。


つまり今まで記事で部分部分を解説していたものの本体です。
関連している記事は以下。


公開にあたって

もちろん大学の内部情報をそのまま GitHub に投げるわけにはいきません。
なので、こいつは OSS 化用にいじってあります。
具体的には漏れたらまずい情報を消してあります。
あとライセンスは MIT だし気にせず自分でいじって公開していく。

その手順として
  • $ git rm very-important-information
    • で公開してはいけない情報を消して
  • $ git commit
    • コミットします。
  • $ git rebase -i --root
    • その後に rebase します。
    • 全部 fixup して一つのコミットにまとめてしまいます
    • fixup しないと復元ができてしまうので、消しても意味がない。
    • ということで、逆に言えばこの手順を踏めばアカンやつを隠蔽できます。


まとめ

所謂実績の一つとして見てもらえると良いかなー、と思って OSS 化しました。
ということで、大学生/大学院生時代にしていた仕事の1つが OSS になりました。

2017/10/06

Heroku の Hobby プランの PostgreSQL が一杯になったので Standard にアップグレードした

Rails を使った Web application を heroku の hobby プラン(無料枠) で運用中、DBへの書き込みができなくなる。

  • $ heroku logs

を漁ると

ActiveRecord::StatementInvalid (PG::InsufficientPrivilege: ERROR:  permission denied

とある。何事だろう。
調べたら、レコードが多すぎる時に発生するらしい。

  • $ heroku pg:info

すると無料枠の 10k レコードを越えていました。
というかぴったり 10k 制限じゃないんだ。優しさなのかなんなのか。

参照した記事の場合、レコードを消して対応していました。
私の場合、レコードを消すわけにはいかないので、 hobby からの upgrade をすることにしました。


コマンド

正直なことを言えば公式のドキュメントが最強です。
指示通りでそのまま問題なくアップグレードできます。
なので、コメント付きで公式ドキュメントをなぞるくらいの気持ちでログを残します。
  • $ heroku maintenance:on
    • Application を止めます。メンテナンスモードですね。
    • おお、これは所謂「停止メンテ」だ。何が起こるか分からないのが良いですね。
  • $ heroku addons:create heroku-postgresql:standard-0
    • DB を新規に作成。プランは standard にしました。
  • $ heroku pg:wait
    • 新規作成したDBを使えるようにします。
  • $ heroku pg:info
    • を見ると DB が増えています。
  • $ heroku pg:copy DATABASE_URL HEROKU_POSTGRESQL_BLACK_URL
    • 古いDBから新しいDBにコピー。
    • DATABASE_URL は置換するとかではなく、この文字列で正しい。
    • なので postgresql-lively-12345 とかに書き換える必要は無いです。
    • 公式ドキュメントだと PINK とかだったかな。どうやらネーミングは色らしい。
  • $ heroku pg:promote HEROKU_POSTGRESQL_BLACK_URL
    • 新しい standard の方をメインのDBにセットします
    • これで DB の移行は終わり
  • $ heroku maintenance:off
    • メンテナンスモード終了
    • これで DB がアップグレードされた状態で Web Application が上がります。
流石 PaaS 。サクサクメンテができるのでインフラに手間がかからない。
ということで淡々とアップグレードができたのでした。ぱちぱち。



参考

2017/08/31

Rails 5.1 の form_with に remote: false を指定する時に気をつけること

Rails で form を書いていて、「ここは非同期に ajax にしよう」となった時は form_for などに remote: true を渡すと実現できます。
逆に、同期の POST にしようと思ったら明示的に remote: false とする訳ですが、 Rails 5.1 で form_with に remote: false を書いても POST にならない。
調べてみると、local オプションというものになったようです。
加えてどうやらデフォルトが ajax の様子。ちょっと引っかかりました。

とはいえ実態は data-remote に false を設定しているだけなので

のどちらで書いても同期のPOSTリクエストになります。


環境

  • macOS Sierra 10.12.6
  • ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-darwin16]
  • Rails 5.1.3


参考

2016/12/06

何故 Rails では View から Controller のインスタンス変数にアクセスできるのか

この記事は Okinawa.rb Advent Calendar 2016 6日目の記事です。


はじまり

むかしむかし、あるところでひとりのあっとんさんが Okinawa.rb Advent Calendar のネタをどうるか考えていました。
そこへ @CodeHex さんがあらわれ、ちょっと Rails について質問をしても良いですかと尋ねてきました。

CodeHex さん「Controller で @ を付けた変数は View からアクセスできるんですか?」
あっとんさん「Rails ではそうですね。ちなみに Ruby では @ が付いた変数はインスタンス変数なのです。」
CodeHex さん「なるほど。 Perl では @ があると配列なので違和感がありますね。」

さて、質問された時に Rails ではそういうものだという解答をしたのですがふと思いました。
「別にインスタンス変数だからと言って View から見えるのは関係が無いよね」
そこで調査班は実装を見付けるために Rails へと踏み込むことにしました。


View はどのようにインスタンス変数にアクセスしているのか

まずは View のクラスを確認してみます。
単に Controller を継承しているだけなのかもしれません。
それならこの記事は終わってしまいますがまず確認しないことには始まりません。

ということで Rails のプロジェクトを作ります。
$ rails -v # 5.0.01
$ rails new advent2016
$ cd advent2016
$ rails generate scaffold user uid:string
ここで User#index に binding.pry を仕込んで self.class を確認してみます。
[2] pry(#<#<Class:0x007ffdce310d48>>)> self.class
=> #<Class:0x007ffdce310d48>
どうやら特異クラス。さて ancestors を確認してみると
となっていました。
継承しているものに Controller らしきものはパッと見は無いようですね。
調査は続行しそうです。

そこで self を確認していると
とそのまま @_assigns というインスタンス変数があることを発見。あやしいですね。
もちろんこの段階では @users は存在しているので
$ self.instance_eval{@_assigns= nil}
としても @users は変更されません。


@_assign を追いかける

@_assign はどこで変更されるのか。
調査隊はさらに Rails の奥へと踏み込むことを決意します。
$ git grep @_assigns
するとactionview/lib/action_view/base.rb の一つしかありません。
しかもやっていることは instance_variable_set なので怪しさしかありません。
assigns は引数なのでどこかで new されているはず。
次はそれを探します。


ActionView::Base.new はどこだ

まずは素直に git grep します。test もあったので grep -v で除外。
$ git grep ActionView::Base.new | grep -v test
actionpack/lib/action_controller/metal/helpers.rb が引っかかりました。
しかしこいつは module でこれが呼ばれるのを探すのは面倒。
また pry に戻って探してみることにします。

backtrace を覗く

また User#index の binding.pry に戻ります。
User#index の段階では view は用意されてるのでどこかで既に new されているはず。
なのでまず backtrace を眺めてみます。
pry-byebug を入れて pry-backtrace と打つと backtrace が見られます。
さて覗いてみると

多い。111もあります。
折角 byebug で repl も動くことなので up していきます。
途中で String を freeze しているころを見付けます。
噂の物体を副産物で見付けた気分ですね。
どんどん up していきます。
actionview/lib/action_view/renderer/template_renderer.rb の render_with_layout くらいから view という変数がちらほら出てきますね。あやしい。
view は context という名前になったりしながら連れ回されているようです。
actionview/lib/action_view/rendering.rb の _render_template までそれっぽいのがありますが、actionpack/lib/action_controller/metal/streaming.rb の _render_template あたりから出てこなくなります。
この辺りに真相が潜んでいそうです。
というか metal は見覚えがありますね。 ActionView::Base.new しているのも metal の helper でした。


結局どこでインスタンス変数を渡しているのか

最後に view っぽいのを扱っているのは actionview/lib/action_view/rendering.rb の _render_template でした。
context に view_context を代入しています。
view_context の定義に view_assigns という怪しさ満点のものを発見。
しかし rendering.rb に view_assigns は無い。
名前が分かればとりあえず git grep します。
actionpack/lib/abstract_controller/rendering.rb に定義を発見しました。
instance_variables を取ってきてhash にしています。
slice しているのは @ を取りのぞくためですね。
ここでおもしろいのは protected_vars を reject していること。
@_formats などは controller から渡さないようにしているようです。
view_assigns は AbstractController::Rendering に定義されています。
ということは ApplicationController が AbstractController::Rendering を継承していれば、ユーザ定義のコントローラは view_assigns を経由でインスタンス変数を取得して view を生成していそうですね。
となれば気になるのは UsersController の ancestors です。
と AbstractController::Rendering がばっちりありますね。56行目です。
という訳で「何故 View は Controller のインスタンス変数へとアクセスできるのか」を発見できました。
それは「Controller が View を Rendering する時に自分のインスタンス変数を渡してやっているから」です。


view_assigns を上書きするとどうなるか

実装が分かれば遊びたくなるのが常。
自分はきちんとコードが読めたのかも含めてちょっと遊んでみます。
内容は @users という変数を無理矢理作ってやるというもの。
この状態で user#index にアクセスすると DB の中身に関係無く、定義された内容が表示されました。
やはり view に渡すためのインスタンス変数を取得するメソッドは AbstractController::Rendering#view_assign で間違いない様子。
ということで無事 Rails の中からお目当てのものを見付けられました。
めでたしめでたし。


おわり

というおはなしなのでした。
読むのに pry-byebug が便利でした。
むしろ無かったら見付けられなかったかもしれない。
結論としては Controller で view_assigns を上書きすると大変なことになりそう、って感じです。


みなさま良い年末をー。

2016/05/22

akatsuki 読み会をした

2016/05/15(Sun) に akatsuki 読み会を開催しました。

akatsuki とは私の所属する琉球大学情報工学科で運用しているユーザ用 Web コンソールです。
IP/VMの申請や作成といった学科のシステム運用に必要な様々な機能が入っている Rails Application です。
2015/09 辺りに学科サーバの更新があり、それに伴い学科新システムの一部として開発しました。
学科のユーザ向けの説明会の資料はこちら。
特定の人々向けなのでかなり情報を省いています。
なお、この記事を書いている段階ではソースが学科内限定公開になっています。


読み会について

読み会を行なったのは主に後輩への引き継ぎのためです。
akatsuki の機能は学科向けの機能も多く、学科システムを把握しておかないとメンテナンスがつらいかなという懸念もあったので、各機能の実装箇所の概観を把握のために読み会を開催しました。
システム管理するメンバー向け資料はこちら。さらに情報が省かれています。


この記事について

読み会の補足用です。やっぱり特定の人用になっています。
実は akatsuki で使っているノウハウを切り出してこのブログにまとめていて、


勝手にFAQ

  • どうして名前が akatsuki なの?
    • システム管理チームのメンバーのイニシャルをまとめると作れたから。
    • 名付けの時にいた人達の趣味であるという説もある。一人前のレディ的な。
  • デザインは?
    • ちゃんと bootstrap 当ててるじゃないですか。
    • それはさておきデザイン担当の人がいたのでその人が当ててくれるかも。
  • LocalRecord ってなに? Localしか無くない?
    • 元は外向けのDNSも akatsuki で管理しようかという考えもあったので
    • GlobalRecord とか SakuraRecord とか作るつもりだった名残です。
  • さくらクラウドの管理は?
    • fog-sakuracloud とかあるのでその辺でどうにかなるかも

まとめ

ということで特定の人達向けの資料を充実させるための記事でした。

2015/10/19

FreeRADIUS を使って DHCP サーバを立てて Rails から管理してみる

デバイスごとに静的なIPを振ってそれをDHCPで配ろう、ということに。
デバイスは多々ありそうなので静的IPの登録を Rails 経由でやってみる。
FreeRADIUS を使うと DHCP の静的IPの設定を DB 側に保存できるらしいので使ってみた。

ということでサンプル的なアプリケーションを書いてみた。(https://github.com/atton-/freeradius_dhcp_with_rails)
IP の最後のオクテットと、デバイスの MacAddress を入力すると、DHCP サーバから IP を取得できます。


環境

  • OSX Yosemite 10.10.5
  • Ruby 2.2.3
  • Rails 4.2.4
  • Docker 1.8.1
  • FreeRADIUS 3.0.4
  • PostgreSQL 9.4.5


使いかた

まず Docker で PostgreSQL と FreeRADIUS 用のコンテナを起動。
FreeRADIUS 用のコンテナに直接入って、起動しているコンテナの情報に合わせて /etc/raddb/sites-enabled/dhcp.relay を書き換えてから radiusd を起動。
Rails 側でクライアントとなるデバイスの Mac Address と IP を登録。
そうすると DHCP で特定の Mac Address 宛に Static な IP が振られます。


解説とか

まず DHCP の設定は このページを参考に FreeRADIUS の設定。
基本的な設定はデフォルトのままでもOKです。
ちょっと問題なのが mods-config/sql/main/postgresql/queries.conf の設定。
元々のサンプルは /etc/raddb/mods-config/sql/main/postgresql/setup.sql や /etc/raddb/mods-config/sql/main/postgresql/schema.sql を利用しているのですが、これで使っている attribute という column name が ActiveRecord 的にアウト。
なので適当に名前を変えます。

今回使うテーブルは radcheck と radreply 。こいつの構造を保ったまま名前を変えます。
元の schema は username, attribute, value, op の 4 column + id。
username が mac address に相当して, value, op, attribute は config の key, op, value に対応してるっぽいです。
例えば attribute = 'DHCP-Your-Address', op = '=', value = '192.168.100.1' とか。
元は DHCP-Your-Address = 192.168.100.1 みたいに書いてたんだろうなー、とか想像したり。なのでいろんな情報とか突っこめそう。
それはさておき column の name を以下に変更。 op と value は変更無し。
  • username => mac_address
  • attribute => radius_attribute
対応するように queries.conf の query を書き換えます。
あとは Rails 側で radcheck 相当のモデル radius_check_information を作って、  mods-available/sql で参照している radcheck を radius_check_informations に変更。
同じように radreply もやってしまいます。
これで DHCP の Static IP の情報を DB に突っこむのはできました。

あとは rails 側でよしなにフォーマットに合うようにレコードを追加してあげたらOK。


引っかかったところ

Docker 側で dhcp とかを扱おうとすると arp table とかをいじろうとしてコケることが。
その場合は docker run する時に --privileged してやると良いみたいです。
あと Docker だと FreeRADIUS の実行ユーザとグループを root にしておかないと dhcp の返事ができないっぽいです。


具体的なコマンドとか

Docker で起動時の IP を使って FreeRADIUS の設定とかをしなかったので、ある程度手動で設定する必要があります。
なので具体的なコマンドとかを載せておこうと思います。

  • $ git clone https://github.com/atton-/freeradius_dhcp_with_rails
  • $ cd freeradius_dhcp_with_rails
  • $ docker build -t pg94 docker/postgres
  • $ docker run -itd --name postgres-server -p 5432:5432 -e POSTGRES_PASSWORD=hogehoge pg94
  • $ docker build -t freeradius_with_rails/freeradius-dhcp docker/freeradius-dhcp
  • $ docker run --privileged --link postgres-server:postgres-server --name dhcp-server -it freeradius_with_rails/freeradius-dhcp
    • これで bash が起動します。
    • ip a してその IP Address を確認。 (ex: 172.17.0.50)
    •  ip r してクライアントの Router を確認 (ex: 172.17.42.1)
      • 今回はクライアントも Docker で上げてみます。
      • なので Router は Docker Private Network の gateway です
    • /etc/raddb/sites-enabled/dhcp.relay を編集
      • DHCP-Domain-Name-Server に DNS を設定します (ex: 8.8.8.8)
      • DHCP-DHCP-Server-Identifier に DHCP サーバの IP を設定します (ex: 172.17.0.50)
      • DHCP-Router-Address に DHCP クライアントのゲートウェイのIP を設定します (ex: 172.17.42.1)
      • DHCP-Subnet-Mask に DHCP クライアントのネットワークのサブネットマスクを設定します (ex: 255.255.0.0)
    • radiusd -X
      • で FreeRADIUS を起動します
  • $ docker run --privileged -it centos bash
    • DHCP クライアント相当の Docker container です
    • $ yum install -y dhclient
    • $ ip addr
      • とかして mac address を確認します (ex: 02:42:ac:11:00:37)
  • $ rake db:create db:migrate
  • $ rails server
    • mac address と IP の値を入れます。 (ex: 02:42:ac:11:00:37, 100)
      • 今回は 172.17.0.0/16 のIPを振ります。
  • $ dhclient -d
    • docker run --privileged -it centos bash のコンテナで実行します
    • 172.17.0.100 が振られます。やったね。

まとめ

これでデバイスごとに静的IPを触れるDHCPサーバを Rails で管理できました。
前の記事にも書いた KVM を Ruby から触る機能と登録したIPに対応するDNSとかと連携すると、IPを申請したら静的にIPが振られてドメインが登録されたり、VMを template から clone してそのIPとドメインが自動申請されるようなシステムが作れます。やったね。


参考

2015/10/10

Rails と bind-sdb を連携してみる

DB と連携した DNS を立てることになったので、そのDBの操作を Rails でやってしまおう、というお話。

環境

  • OS : Mac OSX 10.10.5
  • Docker : 1.8.1 (docker-machine)
  • Ruby : 2.2.3
  • Rails : 4.2.4
  • PostgreSQL : 9.4
  • Bind-SDB : 9.9.4

Bind-SDB について

通常の bind だと zone ファイルとかを更新したりする必要があるのですが、 sdb インターフェースというものを使うと zone ファイルではなくてDBのレコードを使って名前解決とかできるらしいです。
具体的には CentOS とかだと bind-sdb(bind-sdb-chroot) パッケージ。
バックエンドはデフォルトでは PostgreSQL なので PostgreSQL で。 一応 MySQL のアダプタもあります。
レコードに要求されるカラムは以下の4つ。
  • name : DNS を引く時の key。 A record ならFQDNとか。
  • rdtype : レコードの種類。 A とか AAAA とか SOA とか。
  • rdata : DNS を引く時の value。 A record ならIPとか。
  • ttl : レコードのTTL
この4つがきちんと引けていれば動くようなので、この構造を持ったモデルを作ってしまえば Rails で管理できそうだな、ということで実際に作ってみました。

サンプルアプリケーション

サンプルとしてちょこっと書いてみました。(https://github.com/atton-/bind_sdb_with_rails)
*.hoge.com を解決できるようなDNSを作れます。
一応逆引きもできるようにしてるのでアドレス帯域は 10.100.200.0/24 くらい。
rails server して、 domain を入れて IP の最後のオクテットを数値で入れると名前が解決できます。
例えば、 rake db:seed では aaa.hoge.com というレコードを入れるので
$ dig aaa.hoge.com @<your-dns-ip>
とかすると 10.100.200.1 と返します。
実行コマンドは README.md に書いてますが、コンテナのIPが環境によって違うのでちょっと書き換えが必要です。
あと DOCKER_HOST が設定されていること前提。OSX で docker-machine を使っていれば設定されているはずです。

やってること

やっていることはドメインの文字列とIPの組から A レコードと PTR レコードを作ってるだけです。
ドメインとIPが入る record モデルを作って
A レコードが入る forward_record モデルと PTR レコードが入る reverse_record モデルを作って has_one で持たせる。
record モデルの after_save くらいで forward_record と reverse_record を生成してやります。
あとは bind-sdb 側がやってくれます。

bind-sdb 側の設定として、 /var/named/chroot_sdb/etc/named.conf にDBサーバのIPを指定する必要があります。
どうやら名前で書くのはダメなようで、IPで直接指定します。
今回は docker container で DB を持っているので、 docker の host か DB の container の IP を書いてから build するようにしてます。ちょっと面倒。

bind-sdb は特定の zone を特定のテーブルから引く、という処理をするようなので、正引き用の zone は forward_records テーブルを、逆引き用のzoneは reverse_records テーブルを見るようにします。
あとは forward_records/reverse_records に SOA/NS を突っこんでやれば動きます。
ただ、ゾーンを転送する際はテーブルの中に入っているもの全てを転送してしまうようなので、複数のゾーンを1つのテーブルに入れてしまわないように注意。一応転送しないのなら動きはしますが……
ちなみに select するだけのようなので、 read only の view とかを切って複数のゾーンに対応することもできます。そうすれば Rails 側の association が多くなることも無いです。
もちろん AAAA レコードも追加できるので IPv6 も対応可能。今回はゾーンを書くのが面倒だったので v4 のみで。
あとは転送とかするのなら SOA のシリアルの管理もきちんと。 record モデルの after_update くらいに SOA のシリアルを上げる処理を書いてやる形とか。

ということで、 Rails でドメインとIPを申請すると名前が解決できるようになりました。

実行コマンド例

一応置換してね、みたいな記述をしているつもりなのですけれど、具体的な値で実行してみた時の例を載せておきます。
  • $ git clone https://github.com/atton-/bind_sdb_with_rails
  • $ cd bind_sdb_with_rails
  • $  echo $DOCKER_HOST
    • tcp://192.168.99.100:2376
    • docker のホストのIP は 192.168.99.100 のようなのでこれを使います。
  • $ docker build -t pg94 docker/postgres
  • $ docker run -itd --name postgres-server -p 5432:5432 -e POSTGRES_PASSWORD=hogehoge pg94
  • $ rake db:create db:migrate db:seed
    • ここで aaa.hoge.com, 10.100.200.1 が追加される。
  • $ rails server
    • http://localhost:3000 でレコードの追加とかができます。
  • $ sed -i -e 's/postgres-server/192.168.99.100/'
  • $ docker build -t bind_sdb_with_rails/bind-sdb docker/bind-sdb
  • $ docker run -itd --name name-server -p 53:53 -p 53:53/udp bind_sdb_with_rails/bind-sdb
    • この段階でDNSが立ち上がります
  • $ dig @192.168.99.100 aaa.hoge.com
    • 正引きチェック
  • $ dig @192.168.99.100 -x 10.100.200.1
    • 逆引きチェック

参考

2015/10/09

ActiveLdap を使っている時に PG がエラーを吐いて RSpec が落ちる

RSpec を使って Rails の並列リクエスト込みのテストを書いていると、

 PG::ConnectionBad: PQsocket() can't get socket descriptor
とか言われてテストがコケる。
テストだけじゃなくて Webrick でもエラーを吐く時は吐く。
実行する環境が Mac だとどうやっても通って Linux だとどうやっても通らない。

結論から言うと、 ActiveLDAP にタイムアウトを付けていると ActiveLDAP が fork するのでなんやかんや PG のソケットが足りなくなるから PG 側がエラーを吐いて死ぬっぽい。
fork が Process.fork してるのでたぶん OS 依存なバグになってややこしかった形。
対症療法としては activeldap の config の  timeout を 0 にして無効化すれば発生しなくなる。

環境

具体的に試した環境としては

OKな環境(Mac)

  • Rails : 4.2.4
  • Ruby : 2.2.2, 2.1.5, 2.0.0-p645
  • DB
    • PostgreSQL :  9.2.13, 9.3, 9.4 on Docker (1.8.0) on VirtualBox (5.0.0) on Mac OSX Yosemite
    • MySQL : 5.7 on Docker (1.8.0) on VirtualBox (5.0.0) on Mac OSX Yosemite
  • OpenLDAP : 2.4.39 on Docker (1.8.0) on VirtualBox (5.0.0) on Mac OSX Yosemite
  • ActiveLDAP :  4.0.4
  • pg :  0.18.2, 0.18.1, 0.18.0, 0.17.1, 0.17.0
  • RSpec 実行OS Mac OSX Yosemite
OK な環境(Linux)
  • Rails 4.2.4
  • Ruby : 2.2.2
  • MySQL : 5.7 on Docker (1.8.0) on VirtualBox (5.0.0) on Mac OSX Yosemite
  • OpenLDAP : 2.4.39 on Docker (1.8.0) on VirtualBox (5.0.0) on Mac OSX Yosemite
  • ActiveLDAP :  4.0.4
  • pg :  0.18.2, 0.18.1, 0.18.0, 0.17.1, 0.17.0
  • RSpec 実行OS : CentOS 7, Fedora 20
NG な環境(Linux)
  • Rails 4.2.4
  • Ruby : 2.2.2, 2.1.5, 2.0.0-p645
  • PostgreSQL : 9.2.13, 9.3, 9.4 on Docker (1.8.0) on VirtualBox (5.0.0) on Mac OSX Yosemite
  • OpenLDAP : 2.4.39 on Docker (1.8.0) on VirtualBox (5.0.0) on Mac OSX Yosemite
  • ActiveLDAP :  4.0.4
  • pg : 0.18.2, 0.18.1, 0.18.0, 0.17.1, 0.17.0
  • RSpec 実行OS : CentOS 7, CentOS 7 and Fedora 20 on VirtualBox, boot2docker 1.8.0
まとめると
  • Mac なら DB を PostgreSQL にしても MySQL にしても何でもOK
  • Linux だと DB を PostgreSQL にするとたまに再現
  • Linux だと DB を MySQL にすると再現せず
  • postgresql-server や postgresql-devel のバージョンを変えても問題は解決せず

原因調査の流れ

まずは gem のバージョンを変える。なるべく最新へ。解決せず。
gem のバージョンを過去のバージョンにもする。解決せず。
次に PostgreSQL も最新にする。解決せず。
クライアント側の postgresql-devel も最新に。 解決せず。
とりあえず pg を読む。
 PG::ConnectionBad: PQsocket() can't get socket descriptor
は 'pg' の gem の native extension で、PostgreSQL への socket が取れない時に投げられるらしい。
socket が取れないとのことなので pool を増やしたりもしたけれど解決せず。
MySQL にしてみる。 Linux でも動く。どうやら問題はDBだとアタリを付ける(結果的にははずれ)
動かす Linux の kernel を上げたりする。(CentOS でも Fedora でも動かず。 2.x でも 3.x でもダメ)
Linux を物理マシンに乗っけるも動かず。
試しに rails new したやつで並列にリクエストをしても捌く。どうやら原因はデフォルトの gem では無いらしい。
そうこうしてると Linux 側でコアダンプ吐いて Ruby が死ぬ。 stack trace を見ると ActiveLDAP の様子。
ということで覗いて見ると Process.fork とかしてるらしい。
fork した先で PG の connection を使いまくって PG が正しいソケット取れないのかな、と推測してとりあえず timeout を切る。
これで解決。再現せず。長かった……

とりあえず怪しいのは ActiveLDAP の fork 方法か Ruby の Linux 側の fork 。原因を追求したい気もするけれど今はちょっと放置。

2015/08/01

Capybara で同時に複数のリクエストを受け取った時のテストをする

Capybara を使って Rails Application のテストを書いていた時に同時にリクエストを受け取った時のテストを書きたくなったのでそのログ。

環境


  • OSX Yosemite 10.10.4
  • rbenv 0.4.0
  • ruby-build 20150719
  • Ruby 2.2.2
  • Rails 4.2.2
  • Rspec 3.3.1
  • Capybara 2.4.4
  • Postgresql 9.2.13


状況と解説と解決策


想定としては

  • ユーザが resource を new する
  • resource に対して Rails 側が unique な値を追加する
  • unique な値は DB 内では重複してはいけない
といった感じです。

とりあえずアバウトなサンプルとして
  • 本の名前と価格をユーザが入れる
  • ISBNをRails側が順番に振っていく
  • もし本の登録が無くなったらそのISBNは再利用
みたいなサンプルを書いてみました。


まず、リクエストを並列に投げるには spec 内で fork しちゃうと良いみたいです。
validates_uniqueness_of をすり抜ける程度には近いタイミングで実行されるようなので、同時に受け取ったテストと考えて良いかな、と思っています。
実際、 SQLite3 + validates_uniqueness_of のみを書いてある commit では、2つのリクエストを同時に投げると unique に振らないといけない ISBN に対して同じ値でレコードが2つ追加されてしまいます。


DB内で重複を排除するには unique な index を貼ることで回避してます。
そうすることで SQLite3 でも重複された値は格納されないようになるのですが
  • /Users/atton/.rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/sqlite3-1.3.10/lib/sqlite3/statement.rb:108:in `step':
  •  SQLite3::BusyException: database is locked: INSERT INTO "books" ("name", "price", "created_at", "updated_at") VALUES (?, ?, ?, ?)
  •  (ActiveRecord::StatementInvalid)
と言われてしまってテストがコケちゃいます。
ActiveRecord::StatementInvalid を rescue で拾ってから再格納しようとしても BusyException が出るのでうまくいかず。


ということで DB を PostgreSQL に変えてみます。
そうすると、 validates_uniqueness_of をすり抜けて重複した値が入ってきた時には ActiveRecord::RecordNotUnique が返ってきます。
こいつを rescue してきちんとした値にしてやればどうにか目的は達成できました。
ちなみにちゃんと rescue するロジックを書いた場合だとテストで同時に80リクエストくらい捌けました。fork しまくったせいでメモリを数GBとか使うので80くらいが限界。


コマンドとか

Github に上げたサンプルをとりあえず動かすには
  • git clone https://github.com/atton-/sample_of_rspec_to_concurrent_requests_using_capybara.git
  • cd sample_of_rspec_to_concurrent_requests_using_capybara
  • git checkout sqlite3_and_validates_uniqueness_of
  • RAILS_ENV=test bundle exec  rake db:drop db:create db:migrate spec
    • sqlite3 と validates_uniqueness_of のみ。
    • unique であって欲しい column に同じ値が入ってしまう
  • git checkout use_postgresql
  • RAILS_ENV=test bundle exec  rake db:drop db:create db:migrate spec
    • PostgreSQL + Unique Index
    • unique であって欲しい column に同じ値は入らない
    • 同じ値を入れようとすると ActiveRecord::RecordNotUnique
  • git checkout handle_record_not_unique
  • RAILS_ENV=test bundle exec  rake db:drop db:create db:migrate spec
    • PostgreSQL + Unique Index + ActiveRecord::RecordNotUnique を rescue
    • ActiveRecord::RecordNotUnique をきちんと処理してやれば unique にしてあげられる
とかです。
PostgreSQL の設定は環境変数に設定するか database.yml を変更してください。


まとめ

  • 特定の column の値を Unique にしたかったら unique index を貼っておくと validates_uniqueness_of をすり抜けられてもDBに入るのは止められる
  • SQLite3 だと Unique index がある table に同時に insert とかすると BusyException とか出ちゃう
  • PostgreSQL だとほぼ同時なリクエストでも捌けた
  • PostgreSQL だと返ってくる exception は ActiveRecord::RecordNotUnique に変わる


気になるところ

  • Unique Index を貼る方法で Uniqueness を確保する方法って一般的なのかな
  • 特定の column の値が [1,2,3,4, 6,7,8,9] みたいな状態で 5 を取ってくる SQL ってあったっけ(今は 1 から順に ActiveRecord#exists? で回してるので O(n) なんだよなー)
  • 並列にリクエストを投げるのを SQLite3 でできると楽なんだけれど何か方法無いかな

2015/04/13

pry-rails がマルチバイト文字を含むヒストリを追加できない現象の対症療法

Rails Console で 'ほげ' とかのマルチバイト文字を入れると、ヒストリへの書き込みができなくて落ちる問題にぶちあたったので対症療法と調べたログ。

環境

  • OSX 10.10.3
  • rbenv 0.4.0
  • ruby-build 20150319zf
  • ruby 2.2.1
  • rails 4.2.0
  • pry 0.10.1
  • pry-rails 0.3.3

問題

日本語を含むコマンドを rails console で打つと以下のエラーで rails console が落ちる。

たぶんこれを踏んでる。

history.rb の save_to_file でどうやら保存できない模様。

$ Pry.history.instance_eval {def hoge line; binding.pry end; @saver = method :hoge}
$ にほんご
$ Pry.history.instance_eval {@saver = method :save_to_file}
とかして無理矢理に pry で pry に割り込む。
そしたら line が ASCII-8bit になってるっぽい。
ので正しいエンコーディングに変換してしまえば保存できた。
.pryrc に書くとすると


とか。

これでヒストリへの保存時に落ちなくなったけれど、プロンプトはやっぱりおかしいまま。
どこか根幹の原因があるっぽい。
pry と pry-rails と rails の console 回りを軽く読んだけれど、 encoding 回りを触ってるのは見付けられなかったのでとりあえずの対症療法としてブログにまとめる。

読んでる時にちょっとおもしろかったものとか

  • Rails::Configuration に attr_accesor :x とかがあった。このパッと見 x が何か分からない感。
  • Pry.REPL#start ってメソッドがあって、 repl の loop を始めるメソッドらしいんだけれど
    • prologue
    • Input の handling
    • ensure epilogue
  • とかしていてなんかネーミングセンス良いなー、とか

根幹の原因

対症療法は分かったんだけれど、根幹の原因が分からないなー、ブログに書くかー、と思って再現する最小限の Gemfile を書いてたら、どうやら pry-rails そのものが問題じゃなくて rb-readline と組み合せるとダメみたい。
ちなみに再現する Gemfile は以下。

そのうちまた調べるかなー。