より安全にadhoqを実行したい
追記:だいたいの内容が反映された、adhoq v0.4.0がリリースされていますので、ご利用ください。
adhoqを使うと、管理画面などで任意のSQLができるようになって便利そうだ。 ただ調査をしていると、sandbox機構は、Transactionを実行してRollbackする、という方式になっているので、アプリケーション実行ユーザーがDROP TABLEできる権限で動いていたりすると、DROP TABLEなどすることができてしまうので、通常のアプリケーションユーザーとは別のユーザーで実行したかった。(アプリケーションユーザーがDROP TABLEできない方がのぞましいが。
また、本番と同じ系統で任意のSQLを実行できるようにした場合、重いSQLなどを実行されてしまったときに本番で別の部分がSlow queryになってしまうので、サービスから隔離した系統で実行させたいというのもあった。
Adhoqの設定には、 config.database_connection
というものがあって、ここを置きかえればよさそうにも思えるが、
https://github.com/esminc/adhoq/blob/master/lib/adhoq.rb#L17
config.database_connection = proc { ActiveRecord::Base.connection }
ここを置きかえただけだと、Adhoqのユーザーの入力したSQL実行終了後に、Adhoqのレコード入れるときなどのconnectionを元に戻すことができないので、うまくいかないっぽかったのでAdhoqそのものを変更した。
Comparing esminc:master...walf443:connection_switchable · esminc/adhoq · GitHub
インターフェースを変えてしまうのでPRにして通るかどうかわからないし英語で書いてうまく伝わるかなー、といった感じだったのでひとまずブログで書いてみた。
なお、手元ではreadonlyな接続を利用するのに、switch_pointとあわせて利用していて、上記のパッチに加えて、
# config/initializers/adhoq.rb Adhoq.configure do |config| config.hidden_model_names = %w[SwitchPoint::Proxy::MainReadonly] end module AdhoqReadonlyable def with_connection result = nil SwitchPoint.with_readonly_all do result = yield(ApplicationRecord.connection) end result end end Adhoq::Executor::ConnectionWrapper.prepend(AdhoqReadonlyable)
といった感じにして、connectionまわりを差し変えている。
また、触っていて、いくつか気になったところを見つけたのでいくつか別途PRにした。
- クエリのページのN+1クエリの修正 github.com
- テーブル一覧を表示しようとすると全テーブルのcountを取得しようとする問題の修正 github.com
https://github.com/esminc/adhoq/commit/c104c36018744f22fccd537177b8cfa38051a27b
SQLが実行できなかったときに失敗状態にならない問題を修正 Fix exexution failure handling by walf443 · Pull Request #160 · esminc/adhoq · GitHub
preloadがうまくいかないときに何とかするbulk_loaderというライブラリを作ってみた
というやつが先々週のgithub trendで流行っていたんですが、もっとうまく書けそう、という気がしたので書いてみました。 (GraphQL使ってないので同じ用途に使えるかは今のところ不明ですが。
# app/models/post.rb class Post < ApplicationRecord include BulkLoader::DSL bulk_loader :comment_count, :id, default: 0 do |ids| Comment.where(id: ids).group(:post_id).count end end
こんなふうに書いておくと、次のように使えます。
# app/controllers/posts_controller.rb class PostsController < ApplicationController def index @posts = Post.limit(10) # load comment_count blocks with mapping by #id # you can avoid N+1 queries. Post.bulk_loader.load(:comment_count, @posts) render(json: @posts.map {|post| { id: post.id, comment_count: post.comment_count } }) end end
まだ試していないですが、Polymorphic Associationの関連を効率良く読みこんだり、memcachedとかにcacheしつつなかったときにloadしたりするのにも使えると思います。
bulk_loaderの定義時に、:idをキーに持つHashを返すのがポイントで、:idの情報を利用して元のobjectに紐づけてくれるので、mappingの処理を泥くさくなく書くことができます。(もちろんこのサンプルは非常にきれいに書けるパターンですが。
ActiveRecordに特に依存しないように実現しているので、drapper などでも定義できるはずですし、railsのupdate時に困ったりする心配も特にはないはずです。
今日からあるRubyKaigi 2017 に参加しているのでご意見などいただけると幸いです。 pixivブースでPawooの象さんのTシャツを着ている人がいたらたぶん私です。
bundle update 2016-01-25
activerecord-import v0.11.0
https://github.com/zdennis/activerecord-import/compare/v0.10.0...c0a4393a
AR 5.0 betaまわりの対応を進めているもよう。ただテスト一部通ってないのでまだ微妙かもしれない。
capistrano-rails v1.1.6
https://github.com/capistrano/rails/compare/v1.1.5...v1.1.6#files_bucket
- zshだとassets:cleanが適切に走らないことがあるのを修正
- assets:clobberを実行できるようになった
- deploy:migratingにhookすることで、rake db:migrate以外のことをできるようになった
twitter v5.16.0
https://github.com/sferik/twitter/compare/v5.15.0...v5.16.0#files_bucket
- http gemを更新
- mp4のアップロードに対応
mp4のアップロード対応は、入ったとおもってたけど、masterのみにあって、v5.16.0には含まれていないっぽい。
http v1.0.2
https://github.com/httprb/http/compare/v0.9.8...v1.0.2#files_bucket
http 1.0.0のリリース時に一部のobsolateなAPIが削除されているので使っている場合は、注意が必要
nokogiri v1.6.7.2
https://github.com/sparklemotion/nokogiri/compare/v1.6.7.1...v1.6.7.2#files_bucket
libxml2のセキュリティアップデートへ追従
rubocopがもっさりするときは除外ディレクトリを見直してみるとよいかもしれない
前のエントリで書いたrubocopがもっさりする問題を調べてみると、どうやら走査する必要のないディレクトリのファイルまでチェックしてしまっていて非常に遅くなっているのが原因だったようだ。
なお、rubocopは**/*で終わる文字列のパターンをディレクトリを除外する設定と認識して先に除外する、ということをやっているため、ディレクトリを指定するか、他のパターンで指定するかどうか、というのはパフォーマンスにけっこうな影響があるようだ。
AllCops: Exclude: - 'public/**/*' - 'tmp/**/*' - 'log/**/*'
あたりは最初に自動生成したファイルには含まれていないが、railsプロジェクトであれば入れておくとよいようだ。
vendor/**/*だけは、Rubocopのdefaultの設定に含まれているので何もしなくても除外してくれる。tmp/**/*もdefaultでこの候補に入れてくれるとうれしいケースが多いんじゃないかと思いつつ、どう提案しようかな... (今回は、tmp以下に大量にファイルがあるが対象から除外されていないのが遅い原因だった。)
guard-rubocopでsystem経由の呼出しを止めると快適になった
guard-rubocopの中身を見たらsystemで呼びだしていたので、直接Rubocop::CLIを呼びだして起動するようにしてみたら爆速になって快適になりました。
もっさりするのでテストのあとにrubocopを実行するようにしていたけど、これなら先に実行してもいいんじゃないかなーという速度感。
rubocop.ymlのsyntaxエラーとかの場合のエラーがうまくハンドリングできてないっぽいですが、通常の実行には(まだちょっとしか使ってないけど)問題なさそうです。
お試しは、Gemfileに
gem 'guard-rubocop', github: 'walf443/guard-rubocop', branch: 'do_not_system'
でどうぞ。
bundle update 2016-01-13
aasm v4.7.0
https://github.com/aasm/aasm/compare/v4.5.1...v4.7.0#files_bucket
新しくhookポイントが追加されている。 全イベントの前後やTransactionの前後にイベントが仕込めるようになったもよう。
カラム名が存在しないやつに設定してしまったときのエラーがNoMethodErrorではなくわかりやすいエラーになった。
tilt v2.0.2
https://github.com/rtomayko/tilt/compare/v2.0.1...v2.0.2#files_bucket
babel対応が入っているもよう
sidekiq v4.0.2
https://github.com/mperham/sidekiq//compare/v4.0.1...v4.0.2#files_bucket
テスト用のSidekiq::QueueというAPIが追加されたけど、ActiveJob経由から使うのであまり使わなそう。
spring v1.6.2
https://github.com/rails/spring/compare/v1.6.1...v1.6.2#files_bucket
bin/rakeなどでエラーになっていたのが治ったもよう
Unicornでslow restart
Unicornの同時接続数がわりと少ないのに気づいたので、えいやとworker_processesを増やしたら今度はdeploy時にちょっと詰まり気味になってしまった。
これは、deployのタイミングで重いシステムコールであるforkを連発するため、と思われる。
そこで、Startletの--spawn-intervalのようなことをunicornでもやりたい。
設定ファイルで、
# config/unicorn.rb before_fork do # 起動時や再起動で一気にforkしまくると遅くなるので、intervalを加える if Unicorn::HttpServer::WORKERS.size > 10 sleep 0.4 end end
のようにやれば、起動時に連発してforkしなくなってよくなったっぽい。
もちろんbefore_forkは起動時のみに呼ばれるわけではなく、子プロセスの数が減って調整したタイミングや、unicorn-killerとかで殺されて再度起動した、みたいなケースでも呼ばれうるので、そういう場合にsleepが挟まっても問題ないか、は考慮する必要がありそう。