RubyVM::AbstractSyntaxTree.ofをevalやirbで動かすために調べたこと

調査結果を書いただけで、あまり良い解決方法は見つけられていない・・・。

調べたきっかけ

RubyKaigi 2019でRubyVM::AbstractSyntaxTree.ofメソッドを 悪用 活用している方々の発表を聞いて、自分でも試してみようとirbでメソッドを使ったら、無慈悲にもエラーメッセージが表示された。

irb(main):001:0> pp RubyVM::AbstractSyntaxTree.of(proc { 1 + 2 })
Traceback (most recent call last):
        5: from /Users/sinsoku/.rbenv/versions/2.6.2/bin/irb:23:in `<main>'
        4: from /Users/sinsoku/.rbenv/versions/2.6.2/bin/irb:23:in `load'
        3: from /Users/sinsoku/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
        2: from (irb):1
        1: from (irb):1:in `of'
Errno::ENOENT (No such file or directory @ rb_sysopen - (irb))

「自分の使い方が悪い・・・?」「いや、空気を読んで動いてくれないRubyが悪い」

という気持ちになったので、ソースコードを読んで直せないか調べてみた。

RubyVM::AbstractSyntaxTree.of のコードの概要

どうやらast.cの中のrb_ast_s_ofの関数が呼ばれているっぽい。

VALUE path, node, lines;
int node_id;
const rb_iseq_t *iseq = NULL;

if (rb_obj_is_proc(body)) {
    iseq = vm_proc_iseq(body);

    if (!rb_obj_is_iseq((VALUE)iseq)) {
        iseq = NULL;
    }
}
else {
    iseq = rb_method_iseq(body);
}

if (!iseq) return Qnil;

path = rb_iseq_path(iseq);
node_id = iseq->body->location.node_id;

コード片(ブロックで渡されたコード)を受け取って、iseqを作って、node_idを取り出している。

if (!NIL_P(lines = script_lines(path))) {
    node = rb_ast_parse_array(lines);
}
else if (RSTRING_LEN(path) == 2 && memcmp(RSTRING_PTR(path), "-e", 2) == 0) {
    node = rb_ast_parse_str(rb_e_script);
}
else {
    node = rb_ast_parse_file(path);
}

return node_find(node, node_id);

これらの分岐は「ソースコード全体をASTにしたnodeを取得する」処理です。 つまり、 ソースコード全体のASTからコード片と同じnode_idのASTを探して返す ような実装になっています。

コード片だけではファイル内の行列数が分からないため、ソースコード全体のASTを一度作る必要があるのかな。

エラーの原因

エラーメッセージの「Errno::ENOENT (No such file or directory @ rb_sysopen - (irb))」で分かるように、 rb_ast_parse_file が例外を出しています。 これを何かしらの方法で避ける必要がありそうです。

SCRIPT_LINES__ を使う処理

ソースコード全体のASTを取得する処理は以下の3つの分岐になっています。

  1. SCRIPT_LINES__ で取得した配列を使う処理
  2. ruby -e に対応するための処理
  3. ソースコードのファイルを読み込む処理

script_lines関数のコードを読むと分かるように、 SCRIPT_LINES__ の定数からソースコードの配列を取得しています。 つまり、evalやirbで渡した文字列をSCRIPT_LINES__に配列として突っ込めば、良い感じに動きそうです。

eval を上書きしてみる

module AbstractSyntaxTreeOfPatch
  REPL_FNAME = ["(eval)", "(irb)"].freeze

  def eval(expr, bind = nil, fname = "(eval)", lineno = 1)
    return super unless REPL_FNAME.include?(fname)

    defined = Object.const_defined?(:SCRIPT_LINES__)
    Object.const_set(:SCRIPT_LINES__, {}) unless defined
    Object::SCRIPT_LINES__[fname] = [expr]

    super.tap do
      if defined
        Object::SCRIPT_LINES__.delete(fname)
      else
        Object.send(:remove_const, :SCRIPT_LINES__)
      end
    end
  end
end
Object.prepend(AbstractSyntaxTreeOfPatch)

# コード内
pp RubyVM::AbstractSyntaxTree.of(proc { 1 + 2 })
# eval
pp eval('RubyVM::AbstractSyntaxTree.of(proc { 1 + 2 })')
binding.irb
# irbで `pp RubyVM::AbstractSyntaxTree.of(proc { 1 + 2 })` を試す
puts

こんな感じでevalを上書きすることで、それっぽいASTが返ってくるようになった。

いろんな問題

byebugやpryもevalを使っているだろうし、evalを上書きすれば他も簡単に対応できる....と思っていた。 世の中はそんなに甘くなかった。

良い感じに直せたら、ruby/rubyにプルリクを作れるかも・・・?とか思ったけど、色々な問題が起きるので厳しい感じ。

まとめ

あまり良い成果は出ていないけど、とりあえず最近調べていた事をブログにまとめておいた。
irbやevalでASTを作れると、色々と捗りそうなんだけど難しい。

別解: irbでRubyVM::AbstractSyntaxTree.ofを動かす方法

@hanachin_さんがTracePointを使って動かす方法を書かれているので、興味ある方はそちらを読んでみると良いかと思います。

qiita.com

Rails Developer Meetup 2019で"自己修復的なインフラ"という登壇した #railsdm2019

3/22(金)、3/23(土)に開催されていたRails Developer Meetup 2019に参加し、登壇させて頂きした。

色々と記憶があるうちにブログに書いておく。

スライド

www.slideshare.net

イベントの感想

参加者は250人以上で、登壇者も3トラックx2日ととても多かった。 これだけの規模のイベント運営はとても大変だと思うし、運営スタッフならびにカルパスさん本当にありがとうございました。

登壇者の方がアップロードしていた資料をいくつか読みましたが、自分が知らない知見が色々と書かれていて勉強になります。 まだ全てに目を通せてないので、楽しみにしながら少しずつ読みたいと思う。

他にも感想を書きたいところなのですが、実は2日目の午後からの参加だったので、あまり感想を挙げらない...。すまない。

登壇のふりかえり

いくつかあるので、将来の自分のためにふりかえりをしておく。

😀良かった: Decksetでスライドを作成した

今回のスライドでは初めてDecksetを使いました。

慣れているMarkdown形式で書けるし、シンタックスハイライトにも対応していて、便利でした。 ただ、画像の位置調整には癖があるので、そこは慣れが必要そう。

次のスライドもDecksetを使ってみようと思っている。

😣失敗した: テーマを詰め込みすぎた

スライドの中で、話したい事として以下4つを挙げていました。

  1. AWS IAMの基礎とロール設計
  2. インフラ開発環境の構築
  3. Railsアプリケーションのデプロイ
  4. 自己修復的なインフラという話

2月にテーマを入稿したときには「いっぱい知見あるので、色々と伝えるぞ!」と思って詰め込みました。

そして、スライドを作り始めてから「これ、1つの項目で30分くらい必要だぞ...」みたいになった。

  • 深掘りしすぎると30分に全く収まらない
  • テーマとして挙げたのに触れないのも微妙

と、スライド構成を決めるのに苦戦していました。 もうちょいテーマを絞って、1点突破みたいな内容にするべきだった。

😀良かった: 「参考になった」という感想を頂けた

前述のようにスライド構成に不安があり、しかも部屋が満員という状況での登壇だった。

あまり話すの得意でも無いので説明が分かりづらい点もあったとは思いますが、登壇を聞いた方に何か知見を伝えられたようで良かった。

😣失敗した: AWSの構成図は時間かかる

AWSの構成図を作るのは地味に時間がかかるので、普段のスライド作りよりも少し時間を多く見積もるべきだと思った。

Cacooで図を作って、pngでエクスポートして、Decksetの画面で文章とあわせて確認して....を繰り返していた気がします。

話せなかった内容について

スライドの最後に書いたように、E2Eテストを試したり、自動revertプルリクは近いうちに挑戦してみようと思っている。

ただ、それ以外にもいくつかスライドに含められなかった内容があったりする。

  • Terraformの詳細な話
  • DevSecOps的な話
  • ECS(Fargate) + Railsの詳細

この辺りは別の機会を見つけて、そのうちアウトプットしていきたい。

神戸市の働き方に関する勉強会に参加した #forkwell

Forkwellの中の人からイベントに誘われ、リモートワークや地方の働き方には少し興味あったので参加してみた。

forkwell.connpass.com

トークで印象だった話

話聞いてたときにメモったりしてなかったので、自分の記憶に残っている話題だけ書いておく。

ACALL株式会社

www.acall.jp

  • ACALLのサービス紹介
    • 入退室の自動化は便利そう
    • 導入企業が多くて、順調に成長してる
  • 使っている技術はRailsやVue.js、AWS
  • CTOが唎酒師(ききさけし)
  • ブログ書いてる時に気づいたけど、 freee さんの受付のやつじゃん!
    • 使ったことあった

株式会社職人さんドットコム

www.shokunin-san.com

  • 職人の需要に対して、供給が足りてない
    • ITで作業効率を上げる方針なのすごく良い
  • 職人向けの商品を扱うお店の検索ができる
    • 狙いが尖っていて、ビジネス的には良さそう
  • マネタイズ対象は企業やショップで、職人は無料で使える
    • 広告などが収益源
  • 使っている技術はPHPGCPを使っていたような気がするけど記憶に自信がない)
  • 本社は神戸に置きつつ、東京に開発拠点を作ろうとしている
    • エンジニアを募集中

神戸市

kobeliveandwork.org

  • 神戸の良いところの紹介
  • 山から見える景色の良い写真を紹介しながら「出社前に登ったりできます」
    • いや、出社前は流石に疲れるのでは...と思ったけど、体力ある人なら気持ち良い出社かもしれない
  • 市でスタートアップの支援を色々としてる

住みやすさ、食事の美味しさ、交通の利便性、...など神戸のQOLは高そうという印象を受けた。

交流会

神戸市の美味しい食べ物を頂きつつ、個人的に興味のあった神戸の金銭事情についていくつか聞いた。

  • 神戸市のエンジニアの年収帯
  • 家賃の相場感
  • スタートアップの支援について
  • 社内や神戸のエンジニアの人数に関する話
  • ....など

答えにくい直球なお金の話に、しっかり回答して頂けて個人的には満足した。

全体の感想

普段はなかなか聞けない話をお聞きできて、面白い会でした。 神戸がスタートアップ支援してるの知らなかったし、聞けてよかった。

ただ、企業のトーク時間が短くて、プロダクトの紹介で終わっていたのは少し残念。 東京では出来ない働き方や会社の制度、技術的な取り組みをもう少し聞きたかったなーと。

東京以外の勉強会の少なさによる不安

勉強会で好きな技術に関して話すのは楽しいので、その機会が減りそうなのは少し気になってる。

あと、自分の勉強する機会が減りそうという不安もある。
東京だと勉強会で野生のコミッターにぶつかって、新機能やバグ修正を本人から聞けるイベントに遭遇したりする。 こういうイベントで「アウトプットしよう」とモチベーションが高まったりする。

東京以外だとQOL高いのは魅力的なんだけど、東京を離れるデメリットも結構多くて、まだ東京を離れる選択はできない。

RailsアプリでElasticsearchを扱うならchewyがおすすめ

Twitterでツイートしたり、表参道.rbの懇親会では紹介していたけど、ブログに書いていなかったので今更ながらまとめておきます。

github.com

Chewyの利点

READMEを一通り読んでもらうと 最高に便利なのが分かる とは思うのですが、それだと身も蓋もないので個人的に良いと思ってる機能を3つだけ紹介します。

  1. 複数のストラテジ
  2. ゼロダウンタイムのインデックス更新に対応したrakeタスク
  3. Named scopes

1. 複数のストラテジ

Chewyにはデフォルトで複数のストラテジが用意されてあります。

  • :atomic: 一括でインデックスを登録する
  • :urgent: 1つずつインデックスを登録する
  • :bypass: インデックスに登録しない
  • :active_job: 非同期処理でインデックスに登録する( :sidekiq などを直接使う事も可能)
  • ...など

これらのストラテジは最初から良い感じに設定してあります。

また、一時的にストラテジを切り替えることもできます。

Chewy.strategy(:bypass) do
  City.popular.map(&:do_some_update_action!)
end

2. ゼロダウンタイムのインデックス更新に対応したrakeタスク

READMEから説明を引用して紹介します。

Performs zero-downtime reindexing as described here. So the rake task creates a new index with unique suffix and then simply aliases it to the common index name. The previous index is deleted afterwards (see Chewy::Index.reset! for more details).

要は、「新しいインデックスを作って、エイリアスで切り替えるとゼロダウンタイムで更新できる」というもので、これに対応したrakeタスクが標準で用意されています。 しかもrakeタスクは最初から並列実行に対応しています。

あと、フィールドに updated_at が含まれていれば chewy:sync のタスクでDBとElasticsearchの同期を簡単に行うことができます。 基本的には使わないのですが、何かしらの理由でデータ不整合が起きた時に簡単に修正できて便利です。

参考: https://github.com/toptal/chewy/blob/v5.0.0/lib/chewy/type/mapping.rb#L192

3. Named scopes

ChewyではActiveRecordのようにScopeを定義することができます。

class UsersIndex < Chewy::Index
  def self.by_name(name)
    query(match: { name: name })
  end
end

UsersIndex.limit(10).by_name('Martin')
#=> <UsersIndex::Query {..., :body=>{:size=>10, :query=>{:match=>{:name=>"Martin"}}}}>

これは検索機能のように複数の条件を組み合わせるときに便利です。

class UsersIndex < Chewy::Index
  def self.by_age(name)
    # 検索フォームに未入力の場合を考慮
    return all if name.blank?

    query(match: { name: name })
  end

  def self.in_followers(user)
    query(match: { follower_ids: user.follower_ids })
  end
end

UsersIndex.by_name(params.dig(:q, :name)).in_followers(current_user)
#=> <UsersIndex::Query {..., :body=>{:query=>{:bool=>{:must=>[{:match=>{:name=>"Martin"}}, {:match=>{:follower_ids=>[1, 2]}}]}}}}>

まとめ

RailsでElasticsearchを使う場合、elasticsearch-railsを選ぶ人が多いとは思いますが、是非Chewyも検討してみてください。

私は去年の11月にchewyを見つけたけど、READMEを読んでelasticsearch-railsからchewyへの乗り換えを決めました。

「privateメソッドのテストについての考え方」を読んで #yochiyochirb

highwide.hatenablog.com

を読んで、「自分なら設計を変えて、publicにしてからテストを書くなー」と思ったので、考え方・直し方の一例としてブログを書く。

元の設計

元記事のスライドの途中に出てくるコードはこんな感じで、バッチ処理などでよくある設計。

module Tasks
  module Hoge
    class Sender
      def self.execute
        data = aggregate_data
        processed_data = process_data(data)
        send_s3(processed_data)
      end
    end
  end
end

コードの臭い

個人的なリファクタリング原則で「引数が1つのメソッドは、その引数のインスタンスメソッドに書き換えられる」がある。*1

あと、元コードだと「データ収集、加工、s3に置く」のが1メソッドになっていてテストし辛いので、そこも分ける。

module Tasks
  module Hoge
    class Sender
      def self.execute
        instance.process_data.send_s3
      end

      def self.instance
        data = aggregate_data
        new(data)
      end

      def self.aggregate_data
        # データを集める処理
      end

      def initialize(data)
        @data = data
      end
      attr_reader :data

      def process_data
        processed = data.group_by(&:first)
          .sort
          .map { |key, value| [values[0], values[1]] }

        # Immutableの方がメンテしやすいので、新しいインスタンスを返す設計にしてある
        Sender.new(processed)
      end

      def send_s3
        # s3に置く処理
      end
    end
  end
end

この構造にすると、データ収集・加工・s3アップロードをそれぞれテストしやすくなる。

まとめ

単にpublicに変えるわけじゃなくて、設計を見直した結果publicになるものかなと思ってます。

*1:個人的によく使う原則なんだけど、何か名前あったりするのかな

Railsアプリでrakeタスクのログを見やすくする

rakeタスクのログを調べやすくするために、ActiveSupport::TaggedLoggingを使って読みやすくする方法のメモ。

# lib/rake_logger_rails.rb

module RakeLoggerRails
  # rakeタスクでログを出力するとき、自動的にタグ付けを行います。
  #
  #   task foo: :environment do
  #     logger.info('hello')  # Logs "[RAKE] [foo] hello"
  #   end
  def execute(*)
    if Rails.logger
      Rails.logger.tagged('RAKE', name) { super }
    else
      super
    end
  end
end
Rake::Task.prepend(RakeLoggerRails)

def logger
  Rails.logger
end

Rakefile の中で上のファイルを読み込む。

# Rakefile

require_relative 'config/application'
require 'rake_logger_rails'

Rails.application.load_tasks

これで、タスク内では logger を簡単に使える。

task foo: :environment do
  logger.info 'hello' #=> [RAKE] [foo] hello
end

Gitで更新頻度の高いファイルを見つける方法

というツイートを見かけて、ブログの下書きに眠っていたこの記事を公開した。

$ git log --name-only --oneline | grep -v ' ' | sort | uniq -c | sort -n
(...略)
  10 .rubocop_todo.yml
  10 README.md
  10 app/models/authentication.rb
  11 app/views/dashboard/show.html.slim
  11 config/environments/production.rb
  13 config/application.rb
  13 spec/rails_helper.rb
  14 circle.yml
  14 config/initializers/omniauth.rb
  14 db/schema.rb
  24 .rubocop.yml
  31 config/routes.rb
  68 Gemfile
 132 Gemfile.lock

可視化はしてないけど、どのファイルが変更されるのかは分かる。

上の出力で「bundle updateしてるけど、機能作ってない個人Railsアプリ」ってのが分かる。