Memento memo.

Today I Learned.

jemallocを利用したRubyのDocker Imageを作る

以下の記事で説明されているようにメモリアロケータをjemallocにすることで、Rubyのメモリ効率をよしなにできる場合があります。是非使いましょう!!!!!!

techracho.bpsinc.jp

さて、時は2019年であり、世界はコンテナに包まれました。

RubyのDocker Imageもjemallocオプションを有効にしてビルドしたものが提供されてあろうこと期待していたのですが、公式イメージとしては提供されていません。

代わりに以下のIssueが見つかりました。要するにサポートされていません。

github.com

そのため、jemallocを有効化したImageは自分で作る必要があります。

幸いにも上記Issueのコメントに作り方が書いてありました。

正解は以下です。

FROM ruby:2.6.2

# Enable jemalloc
RUN apt-get update && apt-get install libjemalloc1 && rm -rf /var/lib/apt/lists/*
ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1

ビルド時に指定する必要はないみたいですね。

FIN

gemを作ってrubygems.orgでリリースする

作ったもの

github.com

slash-force というかっこいい名前のgemを書きました。

Railsで特定のURLへとアクセスしたときに末尾にスラッシュを付けて強制的にリダイレクトさせるだけのプラグインです。

こんな感じでgemをリリースできました。

slash_force | RubyGems.org | your community gem host

gemの作り方

Gemの作り方まとめ 普通のgem編 - masarakki's blog を参考に。

  • bundle gem xxxxx -t でプロジェクトの雛形を作成
  • 適当に実装
  • gemspecのTODO部分を埋める

くらいです。

gemのリリース

curl -u shotat https://rubygems.org/api/v1/api_key.yaml > ~/.gem/credentials; chmod 0600 ~/.gem/credentials
  • bundle exec rake release を実行。

以上の手順でリリースが完了します。当初の想定の2兆倍くらい簡単なプロセスでした。

gem化のメリデメ

汎用的な機能のgem化のメリデメについて(私見)

メリット

  • プロダクトコード内の libconcern の肥大化を防げる
  • 車輪の再発明を防げる(gem自体が再発明でない場合に限る)
  • UTをプロダクトコードと完全に分離できる
  • 見知らぬ強い人がenhance, bug fixしてくれる可能性がある
  • 楽しい

デメリット

  • インタフェースの変更に大いなる責任が伴う
  • やりすぎるとプロダクトコード側が苦しくなる(謎gemに大量に依存する状況になるとつらい)

Concurrent Rubyで並行処理プログラミング

Rubyで並行処理を書きたかったのですが、自前でスレッドセーフなプログラムを書ける気がしないのでgemを探して来ました。

Concurrent Ruby

github.com

Be an 'unopinionated' toolbox that provides useful utilities without debating which is better or why

と書いてあるので、諸々の並行処理の実装があるみたいです。スレッドセーフらしいです。 READMEを読むとActorやらChannelやら書いてあります。

とりあえず今回は一番basicっぽいAsyncを使って並行でHTTPリクエストを投げる処理を実装してみます。

Async

Async: A mixin module that provides simple asynchronous behavior to a class. Loosely based on Erlang's gen_server.

らしいです。

とりあえずDocumentを読みつつコードを書いてみました。

1秒のレイテンシがあるmock serverに5回リクエストを投げる処理です。

通常の直列処理では5秒かかってる処理が、 Async使った並行処理では1秒で終わっています。

実装ポイント
  • Concurrent::Async をincludeする
  • initializerメソッドで super() をコールする
  • .async をメソッドチェインに挟んで非同期処理をdispatchする
  • .wait で処理の終了を待つ(ブロッキングなメソッド)
  • .value で結果を取り出す

まとめ

結構手軽に並行処理が書けて良さげです。

HTTPリクエストの並列化自体は Faraday + Typhoeus なんかでも簡単に書けるんですが、 Concurrent Rubyの方が汎用的ですね。

ActorとかChannelあたりはまだ触れてないので後日触ってみようと思います。

パーフェクトRuby (PERFECT SERIES 6)

パーフェクトRuby (PERFECT SERIES 6)

メタプログラミングRuby 第2版

メタプログラミングRuby 第2版

Gemfileとpackage.jsonでみるバージョン指定の読み方

Semantic Versioning 2.0.0

セマンティック・バージョニング(SemVer)によると

バージョンナンバーは、メジャー.マイナー.パッチとし、バージョンを上げるには、

  • APIの変更に互換性のない場合はメジャーバージョンを、
  • 後方互換性があり機能性を追加した場合はマイナーバージョンを、
  • 後方互換性を伴うバグ修正をした場合はパッチバージョンを上げます。
  • プレリリースやビルドナンバーなどのラベルに関しては、メジャー.マイナー.パッチの形式を拡張する形で利用することができます。

SemVerはバージョニングのルールみたいなものですね。

Gemfile(Ruby)やpackage.json(Node.js)等で依存関係を記述する時、 チルダ(~)やキャレット(^)を使って指定するかと思いますが、自分の中での解釈が曖昧だったのでまとめます。

Gemfile

Rubyの場合は ~> の記法が使われます。

これはpessimistic operator(悲観的バージョン演算子)というそうです。

解釈は以下のようになります。

  • 指定バージョンの一番右の数字を取り除く
  • 次に、一番右の数字をインクリメントしたものを上限とする
# e.g.
gem 'hoge', '~> 5.1.1'
# 次と等価
gem 'hoge', '>= 5.1.1', '< 5.2.0'

参考: Ruby's Pessimistic Operator

package.json

package.jsonではチルダ(~)とキャレット(^)を使います。

参考: semver | npm Documentation

Tilde Ranges

Allows patch-level changes if a minor version is specified on the comparator. Allows minor-level changes if not.

とのことで

  • 基本的にはマイナーバージョンまで固定
  • マイナーバージョンが指定されてない場合はメジャーバージョン固定

です。

e.g.
~1.2.3 := >=1.2.3 <1.(2+1).0 := >=1.2.3 <1.3.0
~1.2 := >=1.2.0 <1.(2+1).0 := >=1.2.0 <1.3.0
~1 := >=1.0.0 <(1+1).0.0 := >=1.0.0 <2.0.0

Caret Ranges

Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple. In other words, this allows patch and minor updates for versions 1.0.0 and above, patch updates for versions 0.X >=0.1.0, and no updates for versions 0.0.X.

  • 基本的にはメジャーバージョン固定(メジャーバージョンが0でない場合)
  • メジャーバージョンが0の場合マイナーバージョン固定
  • メジャー・パッチバージョンが0の場合はアップデートしない
^1.2.3 := >=1.2.3 <2.0.0
^0.2.3 := >=0.2.3 <0.3.0
^0.0.3 := >=0.0.3 <0.0.4

大抵の場合はメジャーバージョン固定のCaret(^)指定で問題ないかと思います。

Effective Ruby 第六章まとめ テスティング

Effective Ruby 第五章 メタプログラミング まとめ その2 - Memento memo. の続き。

Effective Rubyのテスティング章をまとめていきます。

MiniTestはRuby標準のテスティングライブラリで、 主要なコンポーネントは以下の3つです。

require('minitest/autorun') でライブラリ全体をロードすると、上記のコンポーネントも含まれます。

テストのファイル名は tests/xxx_test.rb といった名前で格納するとrailsの作法に乗れて良いみたいです。

ユニットテストとスペックテスト(ビヘイビアスペック)はどっちでもいいらしいですが、 RubyだとRSpecデファクト感あるのでスペックテストの方が馴染みがありそうです。

Effective Ruby

Effective Ruby

MiniTestユニットテストに慣れる

  • テストクラスを定義し、スーパークラスMiniTest::Unit::TestCase にする
  • 個々のテストケースはインスタンスメソッドとして記述し、"test_"プレフィクスをつける
  • ユニットテスト時はアサーションを使う
  • テストメソッドは短くする
  • アサーションメソッドは assert メソッドだけでなく、 assert_equal 等の適切なものを使う
  • assert_xxx に対応する refute_xxx で逆の動作を扱える
  • テストをまとめて実行するRake taskを使う(または作る)
class HogeTest < MiniTest::Unit::TestCase
  def test_hoge
    hoge = Hoge.new
    assert_equal(0, hoge.xxx)
  end
end

MiniTestスペックテストに慣れる

  • 基本的にはユニットテストをラップしてるだけ
  • describeメソッド呼び出しでクラスが自動的に定義されるため、自前でクラス定義を書く必要がない
  • アサート系メソッドの代わりにオブジェクトに注入されたエクスペクテーションメソッド( must_equal, wont_equal )を使う。
describe(Hoge) do
  describe('xxx') do
    before do
      @hoge = Hoge.new
    end
    it('returns initial value') do
      @hoge.xxx.must_equal(0)
    end
  end
end

モックオブジェクトで決定論をシミュレートする

  • 非決定的な処理(HTTPリクエスト等)からテストを切り離したいときはモックを使う
  • モックで交換するメソッドは外部ライブラリが提供している部分にすべき
  • テストメソッドを終える前に必ず verify を呼んで、モックメソッドが実行されていることを確認する。
  • MiniTest::Mockでモックを作成できるが、Mocha等の別ライブラリ使った方が高機能なのでおすすめ

インタフェースをテストする、という原則があるのですが、実装詳細に立ち入ってモックを使わざるを得ないケースもあると思います。その場合 verify でモックメソッドが確実に実行されていることをテストする必要があります。 すると、実装詳細がしれっと変わった場合でもテストで検知できるようになります。

効果的なテストを追求する

  • ハッピーパスと例外パスの両方を試すためにファズテスト、プロパティテストツールを使う
  • コードカバレッジを見て安心しない
  • 機能とテストは同時に書く(テストを後回しにしない)
  • テストは自動化する

用語等

ハッピーパステスト

テストしているコードのすべての前提条件を丁寧に準備して有効な入力しか与えないテスト。バグ発見の効果が薄い。

例外パステスト

さまざまな入力を送り込んでコードの全ての分岐先を確実に実行するテスト。一般に複雑になりすぎるが、ファズテストとプロパティテストで対処できる。

ファズテスト

プログラムや特定のメソッドにランダムデータを大量に送り込むことで、 クラッシュさせたり予想外の例外を発生させることができるかをチェックするテスト。

FuzzBert gem等で実行可能。基本的にほぼ無限にテストを行うため時間がかかる。

プロパティテスト

ファズテスト同様にランダムなデータを大量に送り込むが、コードが満たすべきプロパティ(性質)を満足するかをチェックするテスト。

MrProper gem等で実行可能。

参考: ソフトウェアの品質を学びまくる:Property-based Testing、そしてExample-based testing、とは


Rubyとは別でテスティングの理論的なところをあまり深く理解できていないので、もうちょっと勉強していきたいです。 あとは実践的なところでRSpecを使いこなせるようになりたいです。(適当にしか使えてないので。。。)

The RSpec Book (Professional Ruby Series)

The RSpec Book (Professional Ruby Series)

GitHubのリポジトリを直接指定してgemをinstallする

自作gemとかOSSのgemのリポジトリとかブランチとかを指定して使いたい場合の解決策です。

リポジトリ.gemspec ファイル等は設定済み前提で。

Gemfile使う方法とGemfile使わない方法の2通りあります。

Gemfileを使う場合

普通に指定できます。以下のようにGemfileに記述

gem 'hogehoge', git: 'git@github.com:foo/bar.git', branch: 'develop'

Gemfileを使わない場合

specific_install gemを使います。

specific_installのInstall

$ gem install specific_install

リポジトリからgem install

$ gem specific_install git@github.com:foo/bar.git develop

内部的にはcloneしてbuild & installしてローカルリポジトリ破棄してるっぽいです。

Effective Ruby 第五章 メタプログラミング まとめ その2

Effective Ruby 第五章 メタプログラミング まとめ その1 - Memento memo. の続きです。

モンキーパッチの代わりとなるものを検討する

モンキーパッチとは

  • モンキーパッチとは、実行時にコアクラスを拡張したり挙動を書き換える(パッチを当てる)こと。Active Supportが有名。
  • 複数パッチが衝突すると大事故になるのでできるだけモンキーパッチは避けたい

方法1 module関数を使う

愚直にmoduleを定義&オブジェクトをextendする方法です。

module OnlySpace
  ONLY_SPACE_UNICODE_RE = %r/\A[[:space]]*\z/
  def self.only_space? (str)
    if str.ascii_only?
      !str.bytes.any? do |b|
        b != 32 && !b.between?(9, 13)
      end
    else
      ONLY_SPACE_UNICODE_RE === str
    end
  end

  def only_space?
    OnlySpace.only_space?(self)
  end
end

str1 = "   \r\n"
puts OnlySpace.only_space?(str1)
# OOPっぽくなくなる

str2 = "hello"
str2.extend(OnlySpace)
puts str2.only_space?
# extendしないと使えない

方法2 新しい別のクラスを作成する

Stringクラスの代わりにStringExtraクラスを作成します。 継承ではなく委譲を使ってStringライクなStringExtraを実装します。(実装省略)

方法3 Refinements機能を使う

Ruby 2.1から入ったRefinements機能を使います。 Refinementsでは、パッチの適用範囲がレキシカルスコープの中に限定されます。以下の例で String#loudFoo の外で呼ぶことはできません。

使い方はこんな感じになります。

module Loud
  refine String do
    def loud
      "#{self}!!!"
    end
  end
end

class Foo
  using(Loud)
  def initialize(str)
    puts str.loud
  end
end

Foo.new("wei")
#=> wei!!!

参考

エイリアスチェイニングで書き換えたメソッドを呼び出す

  • 既存のメソッドに新しい名前を与え、元のメソッド名でメソッドを再定義して最終的に元のメソッドを呼び出す。
  • エイリアス作成時にメソッド名がユニークになるよう注意
  • エイリアスチェイニングを取り消すメソッドも作成する

alias_methodについて

alias_method(new_name, original_name) でmethodにエイリアスを貼ることができます。

参考: ref.xaio.jp

sample

module LogMethod
  def log_method(method)
    orig = "#{method}_without_logging".to_sym
    
    if instance_methods.include?(orig)
      raise(NameError, "#{orig} isn't a unique name")
    end

    alias_method(orig, method)

    define_method(method) do |*args, &block|
      $stdout.puts("calling method '#{method}'")
      result = send(orig, *args, &block)
      $stdout.puts("'#{method}' returned #{result.inspect}")
      result
    end
  end

  #エイリアスチェイニングを取り消すメソッドも作成する
  def unlog_method(method)
    orig = "#{method}_without_logging".to_sym
    
    if !instance_methods.include?(orig)
      raise(NameError, "was #{orig} already removed?")
    end

    remove_method(method)
    alias_method(method, orig)
    remove_method(orig)
  end
end

Array.extend(LogMethod)
#=> Array
Array.log_method(:first)
#=> :first
[1,2,3].first
# calling method 'first'
'# first' returned 1
#=> 1
Array.unlog_method(:first)
#=> Array
irb(main):008:0> [1,2,3].first
#=> 1

Procの引数の個数の違いに対応できるようにする

  • Procオブジェクト生成には"強いProc"と"弱いProc"がある。 lambda? メソッドで識別可能。
  • 弱い(Weak)Proc: 引数の扱いが緩く、間違った個数の引数を与えてもエラーにならない。e.g. block
  • 強い(Strong)Proc: 通常メソッド呼び出しと同じで、引数の個数が違うとArgumentError例外が発生する。e.g. lambda
  • Proc#arityメソッドを使うと、Procオブジェクトが期待する引数の数がわかる。
  • Proc#arityメソッドで引数の個数の違いをうまく吸収させる。

モジュールのprependを使うときは慎重に考える

  • 継承階層は ancestors メソッドで確認可能
  • includeは継承階層においてレシーバの後にモジュールを挿入する。
  • prependは継承階層においてレシーバのにモジュールを挿入する。
  • prependよりもalias_methodを使った方が柔軟になる

メタプログラミング面白いですね。Refinementsで局所的にクラス拡張するのは結構気に入りました。

まだ メタプログラミングRuby 第2版 も読めていないのですが、健全なメタプロパワーを高めていきたいです。

Effective Ruby

Effective Ruby

Ruby2.3系での"Missing frozen string literal comment"への対処

Missing frozen string literal comment

Ruby2.3系でRubocopを実行したら Missing frozen string literal comment と怒られました。

ファイルの先頭に以下のmagic commentが必要とのこと。

# frozen_string_literal: true

これによってRubyのStringリテラルが勝手にfreezeされてimmutableになるらしいです。 Ruby3系では基本的にimmutableになる予定なので、その互換のためのようです。便利な上に移行も楽そうですね。

参考:

qiita.com

対処

Rubocopで Missing frozen string literal comment が出てるファイルの先頭行にmagic commentを追記すればよいので、適当なワンライナーで一括対処できます。

rubocop | grep 'Missing frozen string' | cut -d: -f1 | xargs gsed -i -e '1i\# frozen_string_literal: true\n'

BSDsedのオプション苦手なのでgsed使ってます。

Effective Ruby

Effective Ruby