jemallocを利用したRubyのDocker Imageを作る
以下の記事で説明されているようにメモリアロケータをjemallocにすることで、Rubyのメモリ効率をよしなにできる場合があります。是非使いましょう!!!!!!
さて、時は2019年であり、世界はコンテナに包まれました。
RubyのDocker Imageもjemallocオプションを有効にしてビルドしたものが提供されてあろうこと期待していたのですが、公式イメージとしては提供されていません。
代わりに以下のIssueが見つかりました。要するにサポートされていません。
そのため、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
Ruby: Hash -> Structの変換
こんな感じで書けます
def deep_struct(hash) foo = hash.values.map do |v| case v when Hash deep_struct(v) when Array v.map { |x| deep_struct(x) } else v end end Struct.new(*hash.keys).new(*foo) end
OOPっぽく書きたければHash, Arrayに deep_struct
のようなメソッドを拡張するのもアリです
gemを作ってrubygems.orgでリリースする
作ったもの
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のリリース
- rubygemsにサインアップ後、 https://rubygems.org/profile/edit の下部に書かれた以下のようなコマンドを実行。
curl -u shotat https://rubygems.org/api/v1/api_key.yaml > ~/.gem/credentials; chmod 0600 ~/.gem/credentials
bundle exec rake release
を実行。
以上の手順でリリースが完了します。当初の想定の2兆倍くらい簡単なプロセスでした。
gem化のメリデメ
汎用的な機能のgem化のメリデメについて(私見)
メリット
- プロダクトコード内の
lib
やconcern
の肥大化を防げる - 車輪の再発明を防げる(gem自体が再発明でない場合に限る)
- UTをプロダクトコードと完全に分離できる
- 見知らぬ強い人がenhance, bug fixしてくれる可能性がある
- 楽しい
デメリット
- インタフェースの変更に大いなる責任が伴う
- やりすぎるとプロダクトコード側が苦しくなる(謎gemに大量に依存する状況になるとつらい)
オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方
- 作者: Sandi Metz,?山泰基
- 出版社/メーカー: 技術評論社
- 発売日: 2016/09/02
- メディア: 大型本
- この商品を含むブログ (1件) を見る
Rubyのobject_idからobjectを取得する
備忘録です
[1] pry(main)> hoge = "foo" => "foo" [2] pry(main)> hoge.object_id => 70163083352460 [3] pry(main)> ObjectSpace._id2ref(70163083352460) => "foo"
yeah!
Concurrent Rubyで並行処理プログラミング
Rubyで並行処理を書きたかったのですが、自前でスレッドセーフなプログラムを書ける気がしないのでgemを探して来ました。
Concurrent Ruby
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サポーターズ,すがわらまさのり,寺田玄太郎,三村益隆,近藤宇智朗,橋立友宏,関口亮一
- 出版社/メーカー: 技術評論社
- 発売日: 2013/08/10
- メディア: 大型本
- この商品を含むブログ (22件) を見る
- 作者: Paolo Perrotta,角征典
- 出版社/メーカー: オライリージャパン
- 発売日: 2015/10/10
- メディア: 大型本
- この商品を含むブログ (2件) を見る
Gemfileとpackage.jsonでみるバージョン指定の読み方
セマンティック・バージョニング(SemVer)によると
バージョンナンバーは、メジャー.マイナー.パッチとし、バージョンを上げるには、
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(^)指定で問題ないかと思います。
- 作者: 大川ぶくぶ
- 出版社/メーカー: 竹書房
- 発売日: 2015/12/07
- メディア: コミック
- この商品を含むブログ (7件) を見る
Effective Ruby 第六章まとめ テスティング
Effective Ruby 第五章 メタプログラミング まとめ その2 - Memento memo. の続き。
Effective Rubyのテスティング章をまとめていきます。
MiniTestはRuby標準のテスティングライブラリで、 主要なコンポーネントは以下の3つです。
- ユニットテスト
- スペックテスト
- モック
require('minitest/autorun')
でライブラリ全体をロードすると、上記のコンポーネントも含まれます。
テストのファイル名は tests/xxx_test.rb
といった名前で格納するとrailsの作法に乗れて良いみたいです。
ユニットテストとスペックテスト(ビヘイビアスペック)はどっちでもいいらしいですが、 RubyだとRSpecがデファクト感あるのでスペックテストの方が馴染みがありそうです。
- 作者: Peter J.Jones
- 出版社/メーカー: 翔泳社
- 発売日: 2015/01/19
- メディア: Kindle版
- この商品を含むブログ (4件) を見る
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)
- 作者: David Chelimsky,Dave Astels,Zach Dennis,角谷 信太郎,豊田 祐司,株式会社クイープ
- 出版社/メーカー: 翔泳社
- 発売日: 2012/02/22
- メディア: 大型本
- 購入: 7人 クリック: 141回
- この商品を含むブログ (19件) を見る
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#loud
は Foo
の外で呼ぶことはできません。
使い方はこんな感じになります。
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版 も読めていないのですが、健全なメタプロパワーを高めていきたいです。
- 作者: Peter J.Jones
- 出版社/メーカー: 翔泳社
- 発売日: 2015/01/19
- メディア: Kindle版
- この商品を含むブログ (4件) を見る
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になる予定なので、その互換のためのようです。便利な上に移行も楽そうですね。
参考:
対処
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'
- 作者: Peter J. Jones,arton,長尾高弘
- 出版社/メーカー: 翔泳社
- 発売日: 2015/01/09
- メディア: 大型本
- この商品を含むブログ (13件) を見る