Quantcast
Channel: hachi8833の記事一覧|TechRacho by BPS株式会社
Viewing all 1838 articles
Browse latest View live

Rubyオブジェクトの未来をつくる「シェイプ」とは(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。本記事はRubyKaigi Takout 2021 Day2キーノートのスピーチ原稿につき、最終的な発表内容はこのとおりでない部分もあります。流れを把握するために必要と思われる部分については訳注で補足いたしましたが、わかりにくい場合は動画と合わせてご覧ください。

本記事ではshapeの仮訳として「シェイプ」を採用しています。

oracle/truffleruby - GitHub

Rubyオブジェクトの未来をつくる「シェイプ」とは(翻訳)

これはRubyKaigi 2021で行ったセッションの原稿につき、スライドやコードを見ながら話しているかのような口語体で書きました。

皆さんこんにちは、Chris Seatonです。ShopifyのシニアスタッフエンジニアとしてRubyのパフォーマンス研究に従事しており、非常に高度に最適化されたRuby実装であるTruffleRubyを立ち上げました。また、Rubyに関する研究のまとめサイト『The Ruby Bibliography(rubybib.org)』も運営しています。

本日は、TruffleRubyがRubyオブジェクトを実装するのに用いている既存のいくつかのアイデアのうち、MRIや他のRubyの実装への応用が見込めるものについて紹介したいと思います。これらのアイデアの由来や歴史の解説、TruffleRubyがオブジェクトをどのような形で実装しているか、それによって達成できたこと、どうすれば同じことをMRIで実現できるかについてお話しいたします。

それから将来も視野に入れ、こうしたアイデアの上にどんなものを構築できるかについてお話しいたします。

Rubyの「もしも〜だったら」とは

Rubyが他の言語に比べてときどき遅くなることがある理由を聞かれたら、私なら「もしも〜だったら」を筆頭の理由に挙げます。

Rubyの実装では、プログラムを実行するときに「もしもこうだったら、ああだったら」を考えるのに多くの時間を費やします。足し算のたびに「オーバーフローしたらどうするか」、メソッドを呼び出すたびに「メソッドがモンキーパッチされていたらどうするか」、コードの1行1行で「トレースが有効にされていたらどうするか」といったことを判断しなければなりません。

大量の「もしも〜だったら」のコストを削減するために、現在も多くの作業が進められており、MJITJRubySorbet CompilerYJIT、TruffleRubyなど、いずれも何らかの形でこの問題に取り組んでいます。しかしJRubyとTruffleRubyを除けば、Rubyオブジェクトの扱いはどれも同じであり、そこではRubyオブジェクトの「もしも〜だったら」は未だに解決していません。

Rubyのオブジェクトには「もしも〜だったら」が多数関連付けられています。オブジェクトがfrozenだったらどうするか、欲しいインスタンス変数がオブジェクトになかったらどうするか、オブジェクトが複数のRactorで共有されていたらどうするかなど、常に判断を下さなければなりません。今から説明する「オブジェクトシェイプ(object shape)」というアイデアは、可能な限りこうした「もしも〜だったら」の排除を試みる手法です。オブジェクトシェイプという次なる手段によって、MJITやSorbet CompilerやYJITなど、Rubyでその他のパフォーマンス向上に取り組むときの効果も高まると考えている方が何人もいます。

この後説明するように、オブジェクトシェイプは時間的なパフォーマンス向上のみならず、Rubyプログラムの実行に必要なメモリ空間上のメリットも得られる可能性があります。

Rubyオブジェクトとは

ここでお話しするRubyのオブジェクトは、特に「インスタンス変数を持つオブジェクト」を指します。また、インスタンス変数がオブジェクト内にどのように格納される方法についても解説します。オブジェクトには他にも興味深い点がたくさんあり、本セッションを通じてより詳しく説明しますが、「ビッグアイデア」の部分ではインスタンス変数に絞ってお話しいたします。

Rubyのオブジェクトは目的に応じたさまざまなキーバリューペアの入れ物になります。Rubyの Hashと異なり、これらのキーはすべてシンボルで、キーバリューペアはいかなる方法でも順序付けられません。

インスタンス変数は@構文で読み出したり設定したりできますが、instance_variable_getメソッドやinstance_variable_setメソッドでも同じことができます。また、instance_variables などのメソッドですべてのキーを取得することも、defined?などのキーワードでキーが設定されているかどうかを知ることもできます。こうしたインスタンス変数の取得や設定に使える同様のツールはデバッガにも装備されていることがあります。理想的には、これらの構文やメソッドの重要性や最適化の価値はどれも同じであると考えたいと思います。それによって、どの方法が速いかを気にすることなく、自分が解決しようとする問題に適したツールを使えるようになります。

ここでぜひとも強調しておきたいのですが、Rubyのインスタンス変数は「概念的にはオブジェクトの中に存在する」ことが重要なポイントです。つまり、インスタンス変数はオブジェクトのクラスやメタクラスの一部でもなければクラスの中で定義されるのでもなく、そのまま使われるということです。

Ruby実装でのオブジェクトの扱い

オブジェクトとインスタンス変数の動作について、いくつかのRubyの実装を見てみましょう。

MRI

(なお、このセッションでお見せするコードは、わかりやすくするために大幅に簡略化されており、クラスインスタンス変数、埋め込みインスタンス変数、ジェネリックインスタンス変数といった議論と無関係な多くのエッジケースや最適化を省略してあります。)

MRICRuby)は,C言語で実装されたRubyの標準的な実装です.最初にMRIのデータ構造を見てみましょう.

struct RObject {
  VALUE   klass;
  int     numiv;
  VALUE[] ivptr;
};

struct RClass {
  struct st_table *iv_index_tbl; // Hash of Symbol -> index
};

オブジェクトには、「クラスへの参照」「オブジェクト内にある多数のインスタンス変数への参照」「インスタンス変数の値の配列への参照」があります。

クラスはインスタンス変数名のハッシュを、インデックス(ivptr)へのシンボルの形でオブジェクト内に持ちます。

それでは、これらのデータ構造にアクセスするMRIのコードを見てみましょう。ここでは、すべてが順調な場合に実行される「高速パス(fast path)」すなわち「ハッピーパス」にのみ注目します。なお、Rubyらしく書かれたコードはほとんどがこのパスを通ります。それ以外のすべてを処理する「低速パス」すなわち「フォールバック」コードもありますが、ここでは扱いません。

このコードでは、class_serialが前回のアクセス時に記録された「期待されるclass_serial」と同じであることを確認していることがわかります。class_serialはクラスのバージョン番号で、クラスが変更されるとインクリメントされます。また、オブジェクトのサイズが同じクラスの他のインスタンスのサイズと異なる可能性もあるので、必要な数のインスタンス変数スロットがオブジェクトにあるかどうかもチェックする必要があります。

VALUE vm_getivar(VALUE obj, IVC inline_cache) {
  if (inline_cache && inline_cache->class_serial == obj->klass->class_serial) {
    int index = inline_cache->index;
    if (obj->type == T_OBJECT) && index < obj->numiv) {
      return obj->ivptr[index];
    } else {
      // 低速パス
    }
  } else {
    // 低速パス
  }
}

VALUE vm_setivar(VALUE obj, VALUE val, IVC inline_cache) {
  if (inline_cache && inline_cache->class_serial == obj->klass->class_serial) {
    int index = inline_cache->index;
    if (obj->type == T_OBJECT) && index < obj->numiv) {
      obj->ivptr[index] = val;
    } else {
      // 低速パス
    }
  } else {
    // 低速パス
  }
}

理解しやすさのために以下のような疑似Rubyコードで考えてもかまいません。

訳注

上のパラグラフは以下のように詳しく説明されています。

so to get an ivar we have a cache which is passed by the interpreter and we have the receiving object that contains instance variable we check that the cache’s serial number is the same as the object’s actual serial number at the moment
and we check that the expected number of instance variables is less than the number that has and then we can go ahead and read it
otherwise we use a slow path
and the code for setting is very similar
just instead of returning reading from ivptr we write into it
and again we have a slow path for writing
6:06字幕より(整形)

def vm_getivar(cache, obj)
  if cache.serial == obj.klass.serial && cache.index < obj.numiv
    obj.ivptr[cache.index]
  else
    # 低速パス
  end
end

def vm_setivar(cache, obj, val)
  if cache.serial == obj.klass.serial && cache.index < obj.numiv
    obj.ivptr[cache.index] = val
  else
    # 低速パス
  end
end

JRuby

JRubyは、RubyのJava再実装です。MRIのさまざまなJITと異なり、オブジェクトの振る舞いについても再実装されています。

JRubyは、@構文で参照される変数についてすべてのメソッド(継承されたメソッドを含む)を探索することで、クラスで使われる可能性のあるインスタンス変数を静的に推測します。そして実行時に、Rubyのインスタンス変数ごとに1つのJavaフィールドを含む新しいJavaクラスを生成します。生成されたこれらのJavaクラスは、同じ個数の変数を含むすべてのRubyクラスで共有されます。

続いて各Rubyクラスは、インスタンス変数名とそれに対応するJavaフィールドのマップを持ちます。

class TwoVariablesObject {
  Object var0;
  Object var1;
}

JRubyでは、コードがこれらの変数に効率的にアクセスするためにinvokedynamicと呼ばれるJVMのメカニズムを利用しています。ここではJavaのコードには触れずに、効果的な結果をRuby擬似コードでスケッチしてみましょう。

訳注

上のパラグラフに続いて以下も説明しています。

so for the java version of vm get ivar
we check that the caches id which is like a class serial is the same as the
meta classes
real classes id
and then again we can object we can index into the object
and again there’s a slow path
動画7:59字幕より(整形)

def vm_getivar(cache, obj)
  if cache.id == obj.getMetaClass().getRealClass().id
    obj[cache.index]
  else
    # 低速パス
  end
end

このアプローチのメリットは、インスタンス変数の個数が静的に割り当てられるため、オブジェクトのクラスが期待どおりであることだけを確認すればよく、MRIで行われていた「オブジェクトがインスタンス変数のための十分なメモリー領域が確保されているかどうかの確認」を削減できることです。オブジェクトのクラスidは、キャッシュされている値と比較されます。残念ながら、これはオブジェクトから3ホップ離れています。これが効率よく処理されるには、「オブジェクト」と「そのメタクラス」および「その論理クラス」がすべてCPUキャッシュに乗っていることを期待しなければなりません。

もうひとつの制限は静的な推測です。変数やクラスがメタプログラミングで設定されたために推測を誤ると、インスタンス変数を読み書きする低速パスにフォールバックしてしまいます。この振る舞いはRubiniusでも同じです。

JRubyによるこのトレードオフには賛否両論ありますが、私たちは「シェイプ(shape)」を用いることで両方の長所を最大限に活かせると考えています。

シェイプの歴史

オブジェクトシェイプの歴史は、プログラミング言語「Smalltalk」と「Self」までさかのぼります。この2つの言語は言語設計や歴史の話によく登場すると思いますが、シェイプはこれらの言語を研究していた研究者によって生みだされた技術の好例です。

訳注

動画では上のパラグラフの途中で以下も言及されています。

people are always very keen to say their language is inspired by self and smalltalk in the same way they’re keen to say it’s inspired by haskell
動画09:41字幕より(整形)

私たちの分野の歴史について多くを語るつもりはありませんが、この機会にこのアイデアの元となった論文を少し見てみましょう。

そもそもの出発点はSmalltalkでした。

今でこそSmalltalkはシンプルでエレガントな言語と考えられていますが、当時は複雑すぎるという意見もありました。その反動で、開発者の表現力や生産性の向上(今で言う「開発者の幸せ」)を目的としたSelfというよりシンプルな言語が開発されました。

Selfでシンプルさを追求するために行われた改善のひとつが、クラスの代わりにプロトタイプを追加することでした(プロトタイプはJavaScriptのオブジェクトと同じように振る舞います)。Rubyにもクラスはありますが、個別のインスタンスメソッドやインスタンス変数の依存先は論理的なクラスだけではありませんので、(訳注↓)

訳注

上のパラグラフ1文目は実際には以下のように話しています。

one of the improvements for simplicity they added was prototypes instead of classes
and that’s prototypes as in the way javascript objects work if you have experience with javascript as well
so javascript doesn’t have classes it simply has objects which have properties
and to inherit you point to another object as your fallback for reading properties if they’re not in the object you directly access
動画10:39字幕より(整形)

また、同パラグラフの末尾では以下が補足されています。

so ruby is somewhat more like prototypes than pure objects
動画11:04字幕より(整形)

当初のSelfは、開発者の幸せを優先したことと引き換えに低効率でした。開発者の幸せを優先する言語は、残念ながらパフォーマンスで多少の犠牲を払うことになりますが、その点はRubyと似ているかもしれません。しかしこれが研究者たちにとって、Selfの効率を高め、導入されたコストを克服して自分たちの望む設計を実現する方法を見出そうとするモチベーションにつながりました。

この研究が、現在も影響の大きい「ポリモーフィックインラインキャッシング(polymorphic inline caching)」などの主要な開発をリードしました。ここではこの技術について説明しませんが、現代のJavaやJavaScriptの高パフォーマンスの鍵となっています。TruffleRubyでは、Rubyのメタプログラミングを最適化するために「ディスパッチチェイン(dispatch chain)」と呼ばれる新しい形のポリモーフィックインラインキャッシングを採用しています。

私たちの興味をそそった主要な開発は「シェイプ」です。Selfでは「マップ(map)」と呼ばれていますが、現代のJavaScriptの文脈では「隠しクラス(hidden class)」と呼ばれることもあります(「マップ」だとハッシュと混同される可能性があるので私たちは「シェイプ」という用語を使っています)。

ここで用いたのは「インスタンス変数のキーと値の分離」というアイデアでした。キーは「マップ」に保存され、これによってオブジェクト内の値のインデックスを取得できます。オブジェクトはマップを参照しているので、ある変数にどの値を読み込めばいいのかはマップを用いて調べられます。

なお、元々このアイデアが導入された動機の一部は、実際にはスピードではなくメモリ空間でした。各オブジェクトがキーのコピーを保持する必要がなくなるからです。

訳注

上のパラグラフ後半は実際には以下のように話していました。

because when they switched to prototypes they realized all objects had a full set of keys as well as the values
and they realized that by separating them out they no longer needed to keep all those copies because of objects with the same keys could share them
動画13:10字幕より(整形)

ビッグアイデア

以上が、元の研究者たちによって記述された歴史です。ここからは、もっと具体的にRubyと関連する用語や図を用いてこのビッグアイデアをひととおり説明していきます。

ここでは「シェイプ」と呼ばれるものを使います。ひとつのシェイプには、インスタンス変数名から、そのシェイプを持つすべてのオブジェクトにおけるインデックスへのマップが含まれます。上の図ではシェイプを破線で、オブジェクトを実線で表しています。このオブジェクトはクラス(ここでは重要ではありません)とシェイプを参照します。

訳注

上のパラグラフ後半は動画でもう少し詳しく説明されています。

so there’s an arrow from the object to the shape
and the the class and the shape pointers together represent the header of the object with same the object then contains just the values
and the shape contains this mapping from names to indexes so it doesn’t contain a value of instance variables
it just contains an index saying where to find them
動画14:12字幕より(整形)

各オブジェクトには必ず「シェイプ」があります1。オブジェクトにシェイプが与えられると、そのシェイプで必要となる適切な量のメモリ空間も与えられます。空間がどのように使われるかはシェイプによって記述されます。

訳注

上のパラグラフは動画でもう少し詳しく説明されています。

every object has a shape
so objects initially created with a special empty shape for example
when an object is given a shape is also given the right amount of space that shape requires
so it may require resizing the object or using a separate data structure such as an object array to store if the shape needs to be if the object needs to be increased in size
how the space in the object is used is described by the shape so you think of the shape as metadata something that describes the the data and how it can be used or you can think of it as a schema from a database context
動画14:39字幕より(整形)

あるインスタンス変数を取得するには、概念上はシェイプのインデックスを探索してからそのスロットをオブジェクトから読み取ります。

index = obj.shape[:name]
obj[index]

以前に設定されたことのあるインスタンス変数を設定する場合、概念上は同じことを行いますが、値を書き込む点が異なります。

index = obj.shape[:name]
obj[index] = value

オブジェクト内で未設定のインスタンス変数を設定した場合の挙動については後述します。

これはMRIの振る舞いとどう違うのでしょうか?

訳注

動画では上と下のパラグラフのつながりを以下のように話しています。

so how is this different to what mri is doing because it seems sort of similar
well the key differences start with that shapes are immutable and we know that immutability is often a good way to do design software
動画16:26字幕より(整形)

主な違いは、シェイプはイミュータブル(不変)という点です。つまり、過去のある時点でどんなシェイプだったかがわかれば、以後そのシェイプに関するすべてを確実に把握できるようになります。しかもこの仮定はハードコードできます。

また、シェイプはクラスから切り離されており2、同じクラスの2つのインスタンスがそれぞれ異なるシェイプを持つことも可能です。つまり、他のインスタンスが変更されたからといって背後のオブジェクトを変更する必要はありません。

以上のすべてが、あるシェイプを自分が期待するシェイプと比較できることを示しています(おそらくプロセッサ上のシンプルなワード比較が使えるでしょう)。シェイプは、MRIでクラスのインスタンス変数テーブルが変更されるときのような形では変更できません。

未設定のインスタンス変数を設定する場合は、「遷移(transition)」と呼ばれるものを使います。シェイプは、インスタンス変数を追加・削除・変更したときにどのようなシェイプになるかについても認識しています。この図の場合、このシェイプは「height変数が追加されたら別のシェイプに遷移する」ことを認識しています。これで、このプログラムで行われる時間遷移をすべて記述するシェイプのグラフを得られます。

オブジェクトの遷移は低速パスではありません。既に得られたシェイプの情報のみを用いて素早く実行できます。

new_shape = obj.shape.transitions[:add_height]
index = new_shape[:height]
obj.shape = new_shape
obj.resize
obj[index] = value

以上のすべてのしくみは、マシンコードへのコンパイルと組み合わせたときに最大の効果を発揮します。マシンコードにはハードコードされた値を含められるので、比較するオブジェクトシェイプのアドレスをハードコードでき、利用するインデックスもそれに基づいてハードコードできます。これによって、マシン語のワード比較と読み取りが利用可能になります。比較に失敗した場合は、プロセッサが比較の成功を事前に予測して読み取りを続行可能なので、遠くを参照する低速パスのコードも同時に実行されます。

slow_path unless obj.shape == 0x12345678
obj[4]

TruffleRubyはシェイプをどう扱うか

TruffleRubyにおけるシェイプの実装方法については、研究論文『An Object Storage Model for the Truffle Language Implementation Framework』(Andreas Wößほか)に記載されています。

ここでは、TruffleRubyがRubyプログラムの解釈と最適化に利用する一種の内部グラフデータ構造(「中間表現」と呼ばれます)を示す形でTruffleRubyの機能を紹介します。グラフの可視化にはShopifyが開発したSeafoamというツールを用いました。

def getter
  @ivar
end

上のコンパイラグラフ断片では、ノードが操作を、太い赤線が制御フローを、細い緑の線がデータフローをそれぞれ表しています。これは、Rubyの暗黙の部分を明示的に表したRubyコードのフローチャート版です。

ここでのインスタンス変数の読み込みは、シェイプの読み込みと既知のシェイプとの比較に続いて、「ガード」または「比較が失敗した場合は低速パスへのジャンプ」のいずれかを経て、オブジェクト内の既知の場所primitive1を読み込む形で行われていることがわかります。

対応するマシンコードでは、予測されたシェイプとの比較、同じでない場合は低速パスにジャンプ、変数の読み込みと進むことがわかります。以下の3つのマシン語インストラクションがそれです。

0x1242ec600:    cmp dword ptr [rax*8 + 0xc], expected_shape
0x1242ec60b:    jne slow_path
0x1242ec611:    mov r10d, dword ptr [rax*8 + index]

上のグラフ断片には、既にオブジェクト内に存在するインスタンス変数を設定するセッターコードがあるので、ここではシェイプの変更が不要であることがわかります。このコードはゲッターと同じですが、最後の2つのオペランドが読み込みではなく書き込みになっている点が異なります。

def setter(value)
  @ivar = value
end
0x11fd28a00:    cmp dword ptr [rsi*8 + 0xc], expected_shape
0x11fd28a0b:    jne slow_path
0x11fd28a1f:    mov qword ptr [rsi*8 + 0x20], r10

インスタンス変数がまだ存在していない場合は、現在のシェイプからの遷移に沿う形でシェイプを変更します。シェイプにあるものはすべてハードコードされているので、遷移もハードコードされます。つまり新しい値を書き込むのと同様に新しいシェイプを書き換えればよくなります。

class Klass
  def initialize(a)
    @a = a
  end

  def transition(value)
    @b = value
  end
end

loop do
  instance = Klass.new(14)
  instance.transition rand(100)
end
0x12b53bb00:    cmp dword ptr [rax*8 + 0xc], expected_shape
0x12b53bb0b:    jne slow_path
...
0x12b53bb33:    mov dword ptr [rax*8 + 0xc], new_shape
...

シェイプに型付けする

これで、Ruby でシェイプを適用するためのより高度なアイデアについて説明する準備が整いました。TruffleRubyのシェイプは、インスタンス変数名と保存場所のマッピング(対応付け)を行うほかに、それらの型についてもマッピングできます。

現在のMRIでは、あらゆるインスタンス変数が完全なVALUEオブジェクトとして保存されます。小さな整数は、インスタンス変数として保存するためにタグ付け(tagged)されなければならず、取り出すときにはタグ解除(untagged)されます。コンパイラはタグ付けやタグ解除を避けたいので、インスタンス変数が常に整数であることをシェイプに書き込んでおけば、そこに保存する値にタグ付けする必要もなく、取り出す値をタグ解除する必要もないことがわかります。後でその変数が別の何かに変わった場合は、新しい変数を追加するときと同じようにシェイプを遷移させます。

この動作はTruffleRubyで確認できます。

以下のaddルーチンは1個のオブジェクト内に置かれ、2つのインスタンス変数は常に小さな整数です。生成されたマシンコードは、シェイプをチェックしてからオブジェクトから値を読み取りますが、タグ解除もアンボクシング(unboxing: 開封)も行わず、シンプルに値を利用していることがわかります。これが可能なのは、これらの変数が小さい整数型であることをシェイプからコンパイラに伝えているからです。シェイプをチェックするときに型もチェックされるので、1回のチェックで2つの変数を同時にカバーできました。これで「Cコンパイラから出力したかのようなコードが得られます。

訳注

アンボクシングについて動画では以下のように補足しています。

unboxing is java’s version of untagging
動画30:12字幕より(整形)

また、上のパラグラフの「これが可能なのは〜」以後は実際には以下のように話しています。

so it reads from the object into esi and eax which are two registers
then it does an add operation on them
but it doesn’t untag them normally you’d have to shift the value
to get rid of the tag data
so it knows when they come out they’re untagged
and they can do whatever you need to do for that point
you can just use them as it is in this case
um or it might want to tag them again to pass them on somewhere else
when we checked the shape we also checked the types right so that one check covered both variables at the same time and checked covering their types
because of the the shape includes the type information of all the variables
so just checking the shape checks all the types at once
and also checks the layout of the object
and also that that a shape check is already there for the layout
so we get it for free for checking the rest of them
so when we check this shape we also checked the types
and that one check covered both variables at the same time
so now we’re getting code that looks more like it came out of a c compiler than that it came out of a ruby
動画30:19字幕より(整形)

def add
  @a + @b
end
0x12258d380:    cmp dword ptr [rax*8 + 0xc], expected_shape
0x12258d38b:    jne slow_path
0x12258d391:    mov esi, dword ptr [rax*8 + index_a]
0x12258d398:    mov eax, dword ptr [rax*8 + index_b]
0x12258d39f:    mov r10d, eax
0x12258d3a2:    add r10d, esi
0x12258d3a5:    jo  overflow

シェイプの他の使いみち

さらに工夫を重ねるとすれば、他にどんなものをシェイプに移せるでしょうか。インスタンス変数と「同型(isomorphic: ここではインスタンス変数と少し似ているというニュアンス)」のオブジェクトのプロパティは、他にもクラスへの参照やfrozenステートなどがあります。これらをシェイプに保存すると2つのメリットが得られます。1つ目は、オブジェクトに保存するワードとビットが1つずつ減り、スペースを節約できることです。2つ目は、メソッド呼び出しでの「クラスチェック」「frozenチェック」「インスタンス変数書き込みチェック」という3種類のチェックをシェイプチェック一発で完了できることです。

TruffleRubyでは既にfrozenチェックをシェイプに取り込んでいます。上述のセッターの例で言うと、オブジェクトがfrozenかどうかをチェックするコードは以下のどこにも見当たりません。

0x11fd28a00:    cmp dword ptr [rsi*8 + 0xc], expected_shape
0x11fd28a0b:    jne slow_path
0x11fd28a11:    mov eax, dword ptr [rdx + index]

しかしチェックはしっかり行われています。この expected_shapeは統計的にfrozenでないことが判明しています。オブジェクトがfrozenの場合は、このシェイプではなく、frozenとマークされた別のシェイプということになります。これは、Ruby言語機能のオーバーヘッドを「完全に」取り除けることが示された素晴らしい例です。

現在のTruffleRubyは、シェイプにクラスを取り込んでいません。理由は、メソッド呼び出しで「クラスチェック」と「インスタンス変数のチェック」を混同するのはよろしくないと考えられているからです。つまり、複数のオブジェクトが異なるインスタンス変数を持っているという無関係な理由で、メソッド呼び出しでポリモーフィズムが発生してしまう可能性があるということです。

訳注

上のパラグラフに続いて以下も述べられています。

so two objects which you could dispatch the method called in the same way
you’d be looking differently just because they’re different instance variables which is unrelated
動画30:29字幕より(整形)

TruffleRubyでは、オブジェクトがスレッド間で共有されているかどうかについてもシェイプでマークしており、該当する場合は同期のためにロックを有効にします。同じアイデアはRactorのチェックでも利用可能でしょう。

オブジェクトで他に何かできるか

一般に、Rubyの最適化作業の多くは、Rubyの実行モデルの最適化です。MJIT、YJIT、Sorbet Compilerは、インタープリタをJITコンパイルされたマシンコードに置き換えますが、VMの他の部分には手を付けません。MJITとSorbet Compilerは、標準のMRIに接続するためにコードから事実上のC拡張を生成しており、YJITもだいたい似たような感じですが、(訳注↓)

訳注

動画では以下が続いています。

but it doesn’t work at the level of whole methods
動画34:27字幕より(整形)

私は、Ruby最適化を頑張るなら、もっとオブジェクトに目を向ける必要があると考えています。Shopifyではガベージコレクタのコンカレント圧縮やオブジェクトの可変長アロケーション(VWA: variable width allocation)、新しいガベージコレクタといったホットなアイデアに取り組んでいます。TruffleRubyではハッシュや配列がさらに最適化されたことで、サイズや内容に応じた特殊化が可能になりました。TruffleRubyでは、partial escape analysisと呼ばれる非常に強力な最適化をいくつか行っています。これにより、小さなメソッドグループ内でしか使われないオブジェクトが仮想化され、決してアロケーションされないようになります。

実用的な提案

私からの実用的な提案の筆頭は「MRIにオブジェクトシェイプを実装しよう」というものです。実際には実装の複雑さは比較的小さいと思いますし、現在のMRIインタプリタの高速化にも即効性が期待できるでしょう。今後MJITやYJITの開発を進める上でもきっと役に立つはずです。

このアイデアがうまくいくことはTruffleRubyで示されていますし、さらにすごいものを構築できる可能性もあります。この実践的な実装の詳細については先ほど紹介したTruffleRubyの論文で述べられており、そこで達成されたパフォーマンスについても他のいくつかのRubyメソッドと比較されています。

これについて解決の必要となる大きな問題はないと思います。本当に必要なのは、1980年代初頭からの一連の研究成果を元に構築してくれる勇者たちでしょう。

訳注

上に続いて以下も述べられています。

that we can follow that is proven that we can implement in ruby
and as we showed when we talked about the history
ruby is already philosophically aligned to the languages where it came from
and that the problems it’s trying to solve
both in terms of developer happiness and optimizing for that
but now trying to cover some of the performance
and we’re aligned to what ourselves are trying to do
so it makes sense to to keep using their techniques
動画36:42字幕より(整形)

MRIへのオブジェクトシェイプ導入について私からのお話は以上です。ご清聴ありがとうございました。

参考資料

  • L. Peter Deutsch and Allan M. Schiffman. 1984. Efficient implementation of the Smalltalk-80 system. In Proceedings of the 11th ACM SIGACT-SIGPLAN symposium on Principles of programming languages (POPL ’84).
  • David Ungar and Randall B. Smith. 1987. Self: The power of simplicity. In Conference proceedings on Object-oriented programming systems, languages and applications (OOPSLA ’87).

  • Elgin Lee. Object Storage and Inheritance for Self. Engineer’s thesis, Electrical Engineering Department, Stanford University, 1988.

  • C. Chambers, D. Ungar, and E. Lee. 1989. An efficient implementation of SELF a dynamically-typed object-oriented language based on prototypes. In Conference proceedings on Object-oriented programming systems, languages and applications (OOPSLA ’89).

  • Urs Hölzle, Craig Chambers, David Ungar. Optimizing Dynamically-Typed Object-Oriented Languages With Polymorphic Inline Caches. In ECOOP ’91.

  • David Ungar and Randall B. Smith. 2007. Self. In Proceedings of the third ACM SIGPLAN conference on History of programming languages (HOPL III).

  • Andreas Wöß, Christian Wirth, Daniele Bonetta, Chris Seaton, Christian Humer, and Hanspeter Mössenböck. 2014. An object storage model for the Truffle language implementation framework. In Proceedings of the 2014 International Conference on Principles and Practices of Programming on the Java platform: Virtual machines, Languages, and Tools (PPPJ ’14).

  • Stefan Marr, Chris Seaton, and Stéphane Ducasse. 2015. Zero-overhead metaprogramming: reflection and metaobject protocols fast and without compromises. In Proceedings of the 36th ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI ’15).

  • Benoit Daloze, Stefan Marr, Daniele Bonetta, and Hanspeter Mössenböck. 2016. Efficient and thread-safe objects for dynamically-typed languages. In Proceedings of the 2016 ACM SIGPLAN International Conference on Object-Oriented Programming, Systems, Languages, and Applications (OOPSLA 2016).

  • 本プレゼンテーションのスタイルの一部はBenoit Daloze氏による以下のシェイプに関する過去スライドにインスパイアされました。

メモ

撮影担当のSam Tamに感謝いたします。

  • ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-darwin20]
  • jruby 9.2.19.0 (2.5.8) 2021-06-15 55810c552b OpenJDK 64-Bit Server VM 11.0.2+9 on 11.0.2+9 +indy +jit [darwin-x86_64]
  • truffleruby 21.2.0 (ruby 2.7.3に似ている)GraalVM CE JVM [x86_64-darwin]

関連記事

Rubyのヒープをビジュアル表示する(翻訳)


  1. 「物体には必ず形がある」というシャレにもなっています。 
  2. 原文のdivorcedには「離婚」の意味もあります。isolatedをカジュアルな話し方にしたと思われます。 

The post Rubyオブジェクトの未来をつくる「シェイプ」とは(翻訳) first appeared on TechRacho.


Railsの技: email_address_with_nameで表示名付きのメールを送信する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

Railsの技: email_address_with_nameで表示名付きのメールを送信する(翻訳)

ほとんどのメールソフトでは、メールアドレスの直前に以下のように表示名を追加できます。

To: Matt Swanson <matt@example.com>

小さな工夫ですが、表示名があるとメールアドレスが読みやすくなります。Railsには、文字列を手動で操作せずにこのスタイルでメールアドレスを整形するヘルパーユーティリティが用意されています。

利用法

email_address_with_nameを使うと、以下のようにメールアドレスの直前に名前を標準的な形で追加できます。

ActionMailer::Base.email_address_with_name("swan3788@gmail.com", "Matt Swanson")
#=> "Matt Swanson <swan3788@gmail.com>"

このヘルパーはRailsのあらゆるメイラーで利用できます。

class UserMailer < ApplicationMailer
  default from: 'notifications@example.com'

  def welcome_email
    @user = params[:user]

    mail(
      to: email_address_with_name(@user.email, @user.display_name),
      subject: 'You have a new message'
    )
  end
end

オプション

このヘルパーは以下のようにnilもいい感じに扱ってくれます。

ActionMailer::Base.email_address_with_name("swan3788@gmail.com", nil)
#=> "swan3788@gmail.com"

さらに文字のエスケープも自動で対応します。

ActionMailer::Base.email_address_with_name("mike@example.com", "Michael J. Scott")
#=> "\"Michael J. Scott\" <mike@example.com>"

ActionMailer::Base.email_address_with_name("chip@example.com", 'John "Chip" Smith')
#=> "\"John \\\"Chip\\\" Smith\" <chip@example.com>"

参考資料

関連記事

The post Railsの技: email_address_with_nameで表示名付きのメールを送信する(翻訳) first appeared on TechRacho.

Rails 7のActive Record暗号化機能(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

Rails 7のActive Record暗号化機能(翻訳)

Rails 7のActive Recordに、実にクールな新機能が導入されることになりました。モデル内で使える強力なencrpyts宣言によって呼び出される、アプリケーションレベルの暗号化機能です。この新機能は、アプリケーションコードとデータベースの間に暗号化の層を提供します。要するに、ActiveRecord::Encryptionを用いたデータがActive Recordオブジェクトに読み込まれると平文になり、データベースに置かれると暗号化されます。

本記事では、この新機能の使い方の概要を説明し、いくつかの優秀な機能を紹介するとともに、制限事項についても触れます。

本題に入る前に、Edgeガイドの素晴らしいドキュメントを紹介しておかないといけませんね。

参考: Active Record Encryption — Ruby on Rails Guides

本記事(英語版)では簡単のため、この新機能を単に「暗号化(encrypts)」と呼ぶことにします(他の呼び方を思いつかないので😉)。

暗号化機能登場の背景

暗号化機能は、@jorgemanrubiaによるプルリク#41659の形でRailsにマージされました。プルリクの説明では、HEY(訳注: Basecampのサービス)で用いられている暗号化機能を抽出したものだそうです。この機能が導入するまでの経緯に関心のある方は、Jorgeによる以下の興味深いブログ記事をどうぞ。

決定論的暗号化1について

ここで、決定論的暗号化(deterministic encryption)と非決定論的暗号化(non-deterministic encryption)について簡単に触れておきます。いたって単純な話ではありますが、暗号化機能の利用方法を理解するうえで重要なポイントとなります。

ここで言う暗号化とは、「何らかのテキスト入力(平文)に対してある関数を適用し、テキスト出力(暗号文)を得ること」とお考えください。

この関数が決定論的である場合、同じテキストに対してその関数を適用すると必ず同じ結果が得られます。

この関数が非決定論的である場合、ある値を暗号化したときの出力は予測できなくなります。理論上は1回目と2回目で同じ出力を得られる可能性もありますが、その確率は極めて小さくなります。非決定論的な暗号化がデフォルト設定となっている場合、同じ平文を暗号化するたびに、ほぼ確実に異なる暗号文が出力されます。

モデルの属性を暗号化する場合に決定論的暗号化を用いると、同じ平文をデータベース内で2つの行に保存すると、暗号文の値も同じになります。逆に非決定論的暗号化を用いると、同じ平文をデータベース内で2つの行に保存したときの暗号文の値は一般に異なります。後ほど説明しますが、この決定論的/非決定論的の違いは、暗号化済みデータをクエリできるかどうかに影響します。

セットアップ

暗号化を使うための設定はそれほど多くありませんが、注意しておきたい点がいくつかあります。

キー

主に必要となるのは、キーセットを生成してcredentialファイルに追加することです。bin/rails db:encryption:initを実行すれば、ファイルに追加するためのキーが以下のように生成されます。

Add this entry to the credentials of the target environment:

active_record_encryption:
  primary_key: zxMXS0hBbpa5BzRKPv9HOSF9etBySiHQ
  deterministic_key: 0pM2UHHBQr1kf1irO6JgakcSOXu0r1Vn
  key_derivation_salt: I5AkViD0UJhSqK3NY49Zvsls3ZoifyXx

primary keyは、非決定論的暗号化で用いるルート暗号化キーを導出するのに使われます。なお、credentialファイル内のprimary_keyの値には、キーをリストで複数書くこともできます。

deterministic_keyは、決定論的暗号化で用いられます。上述のとおり、このキーを用いて同じデータに対して暗号化を行うと何度やっても同じ結果が得られます。現時点の暗号化機能では、決定論的暗号化で用いるキーをリスト形式で複数書くことをサポートしていません。決定論的暗号化を完全に無効にしておきたければ、このキーを提供しないでおくのが確実です。

key_derivation_saltは、暗号化キーの導出に使われます。

アプリの設定

暗号化APIではさまざまなオプションが用意されていて、どのオプションもconfig.active_record.encryption名前空間の下で定義されています。これらのオプションを使う場合は、このAPIドキュメントを熟読することをおすすめします。一読すれば、ほとんどのオプションに合理的なデフォルト値が設定されていることがわかるでしょう。

config.active_record.encryption.extend_queriesについて少し解説を加えます。このオプションはデフォルトではfalseになっていますが、これをtrueにすると以下が許可されます。

  • 暗号化済みカラム内で平文データをクエリできるようになる(config.active_record.encryption.support_unencrypted_dataも有効にする必要あり)
  • 暗号化スキームを複数サポートできるようになる
  • uniquenessバリデーションのサポートが有効になる

データベース

暗号化された文字列やテキスト属性がデータベースに保存されるときには、通常の文字列やテキストではなく、書き込み時にシリアライズされ読み出し時にデシリアライズされる複雑なデータ構造として保存されます。このデータ構造のおかげで暗号化済みテキストに加えていくつかのメタ情報も保存でき、アプリがテキストの暗号化方式を知る手がかりをある程度得られるようになります。

メタ情報が追加されるため、ストレージで最大250バイトのオーバーヘッドが発生します。

edgeガイドでは、カラムを暗号化する場合は255バイトのstringフィールドを510バイト2に増やしておくことを推奨しています。textフィールドのオーバーヘッドについては、一般に無視できる範囲であるとされています。

呼び出し

いよいよ暗号化を使うときが来ました。

最も基本的なユースケースでは、あるカラムを暗号化するのに必要なのは、モデル内の暗号化したい属性にencrypts宣言を追加することだけです。たとえばDogモデルにあるtoy_locationというフィールドを暗号化したい場合は以下のように書きます(イヌはよくおもちゃを隠しますよね)。

class Dog < ApplicationRecord
  encrypts :toy_location

簡単でしょう?

書き込み

暗号化された属性への書き込みは、完全に透過的に行なえます。いつもRailsでやっているように書き込めばよいのです。

> dog = Dog.create!(name: 'Bruno', toy_location: 'top secret')

データベース内に保存されている内容を直接表示してみると、次のようになります。

> result = Dog.connection.execute('SELECT toy_location FROM dogs LIMIT 1').first
   (1.4ms)  SELECT toy_location FROM dogs LIMIT 1
#=> {"toy_location"=>"{\"p\":\"oVgEJvRaX6DJvA==\",\"h\":{\"iv\":\"WYypcKysgBY05Tum\",\"at\":\"OaBswq+wyriuRQO8yCVD3w==\"}}"}

この値はシリアライズされたJSONなので、以下のようにparseしてみましょう。

> JSON.parse(result['toy_location'])
#=> {"p"=>"oVgEJvRaX6DJvA==", "h"=>{"iv"=>"WYypcKysgBY05Tum", "at"=>"OaBswq+wyriuRQO8yCVD3w=="}}

するとハッシュが得られました。ハッシュ内にあるキーのほとんどはActiveRecord::Encryption::Properties::DEFAULT_PROPERTIESで定義されています。pはペイロードで、平文を暗号化した暗号文を表します。hは、暗号化操作に関連する情報を含むヘッダーのハッシュです。ivは平文を暗号化したときの初期化ベクトル(initialization vector)です。詳しくは次のセクションで説明します。atは、復号化の際に暗号文が改変されていないことを確認するためのauth_tagです。暗号化の設定や利用方法によっては、DEFAULT_PROPERTIESハッシュ以外にもヘッダーが追加されることがあります。

読み出し

暗号化された属性を持つモデルをRailsで読み込むと、暗号化された値がシームレスに復号化されます。上で作成したDogモデルを名前で検索してみましょう。

> Dog.find_by!(name: 'Bruno').toy_location
#=> <Dog id: 1, name: "Bruno", toy_location: "top secret", created_at: "2021-05-28 22:41:23.142635000 +0000", updated_at: "2021-05-28 22:41:23.142635000 +0000">

ご覧のように、暗号化された値がモデルインスタンス上で自動的に人間が読める形の属性に変換されます。なかなかよくできています。

検索

Brunoというイヌを、名前ではなくtoy_locationで検索したいときは、以下のようにフィールドが暗号化されていないときと同じように行なえます。

> dog = Dog.find_by!(toy_location: 'top secret')
  Dog Load (2.1ms)  SELECT "dogs".* FROM "dogs" WHERE "dogs"."toy_location" = ? LIMIT ?  [["toy_location", "{\"p\":\"oVgEJvRaX6DJvA==\",\"h\":{\"iv\":\"WYypcKysgBY05Tum\",\"at\":\"OaBswq+wyriuRQO8yCVD3w==\"}}"], ["LIMIT", 1]]
#=> #<Dog id: 1, name: "Bruno", toy_location: "top secret", created_at: "2021-05-28 22:41:23.142635000 +0000", updated_at: "2021-05-28 22:41:23.142635000 +0000">

クエリの文字列が、先ほどデータベースの内容を覗いたときに見えた暗号化済みJSON文字列に自動的に変換されていることにご注目ください。

初期化ベクトルと決定論について

決定論的暗号化を使う場合、同じ平文値を持つすべてのレコードで、暗号化に同一の初期化ベクトルが使われます。これはActive Recordが同一入力に対して同じ暗号文を生成するためのものであり、暗号化済みデータを検索するにはこれが前提条件となります。Railsの内部では、決定論的に暗号化されたデータの場合は平文から初期化ベクトルを生成しますが、そうでない場合は初期化ベクトルをランダムに生成します。

同じ平文を持つ2つの行が、それぞれ異なる初期化ベクトルで暗号化されると、データベースに保存されるシリアライズ済みJSONはまったく異なるものになります。

暗号化済みデータを検索可能にするには、ここで保存される値を完全に同じにする必要があります。

つまり、同じ平文を持つ2つの行では、シリアライズされたハッシュに保存される値がすべて同一になる必要があります。そしてRailsはまったく同一のハッシュをその場で再計算して、検査したい文字列にマッチする行を見つけられます。

まさに決定論の最たるものですね。

平文の検索について

最初から暗号化しておく余裕を取れない場合はどうすればよいでしょう。たとえば、既に存在するDogのテーブルにあるtoy_locationカラムが暗号化されていない場合はどうすればよいでしょうか。

上で生成したクエリを見ればわかるように、Dogのレコードのtoy_locationカラムに「top secret」という平文がある場合、この平文はクエリで検索できません。また、平文が保存されているDogのレコードをメモリに読み込もうとすると、平文の復号化を試みるときに問題が発生する可能性が高いでしょう。

ひとつの方法は、平文データを事前に暗号化データに変換しておくことです。私にはこれが理想的な方法に思えます。しかし何らかの理由でそうしたデータ移行を避けたい事情が生じるかもしれません。

そのような場合は、平文値を引き続き平文のままで保存し、新規または更新データを暗号化するオプションを利用できます。暗号化データと平文データの共存サポートを有効にするには、Rails設定のconfig.active_record.encryption.support_unencrypted_dataオプションをオンにします。

この挙動を有効にすると、平文の復号化を試みたときにエラーの発生を防止でき、カラムの平文と暗号文の間にデータのミスマッチがある場合にも検索できるようになります。

この設定を有効にして先ほどのクエリを再実行してみると以下のようになります。

Dog Load (0.3ms)  SELECT "dogs".* FROM "dogs" WHERE "dogs"."toy_location" IN (?, ?) LIMIT ?  [["toy_location", "{\"p\":\"Bd+/TzEysF2CCQ==\",\"h\":{\"iv\":\"R2IUJJ+EmnDnZvQP\",\"at\":\"zqG5WAJql1zgctRCPpoBkQ==\"}}"], ["toy_location", "top secret"], ["LIMIT", 1]]

これで、暗号化済みコンテンツを持つレコードや、そのコンテンツの平文を持つレコードを検索できるようになりました。完璧ですね。

大文字小文字を区別しない検索

デフォルトの検索では大文字小文字が区別されます。何らかの理由で大文字小文字を無視して検索したい場合は、いくつかのオプションがあります。

  • オプション1

Dog.where(toy_location: ['Top secret', 'top secret'])のように、マッチする必要のある大文字小文字のバリエーションをすべて含んだクエリを送信します。

  • オプション2

encrypts宣言でdowncase: trueを指定します。これによって、テキストを小文字(downcase)に変換してから保存するようになります。Active Recordは、クエリ実行時に検索テキストを自動的に小文字に変換します。この方法の欠点は、大文字の情報がすべて失われてしまうことです。下世話な話(downer)で恐縮です。

  • オプション3

encrypts宣言でignore_case: trueを指定し、さらにoriginal_カラム名original_toy_locationなど)をデータベースに追加します。

以下のように、大文字を含む「Top secret」というテキストを登録したとします。

Dog.create!(name: 'Max', toy_location: 'Top secret')

このときtoy_locationカラムには小文字に変換された「top secret」が保存され、original_toy_locationカラムには大文字を含んだままの「Top secret」が保存されます。

これで常にtoy_locationカラムに対して検索が行われるようになり、toy_location属性はoriginal_toy_locationからメモリに読み込まれて生成されるようになります。

ここでひとつ知っておきたい点があります。このtoy_locationカラムは決定論的に暗号化されています(だからこそ検索が効くわけです)が、original_toy_locationカラムは非決定論的に暗号化されるようです。original_toy_locationカラムの検索をサポートする必要はないので、これは理にかなっています。同じ平文値を持つ2つのレコードでtoy_locationカラムとoriginal_toy_locationカラムの値を比較してみると、このことを確認できます。以下のようにtoy_locationカラムには同じ値(初期化ベクトルやペイロードなど)が保存されていて、検索可能かつ小文字変換済みになっています。しかしoriginal_toy_locationカラムの値は互いに異なっており、検索不能かつ大文字小文字が維持されています。

{ "toy_location"          => "{\"p\":\"Bd+/TzEysF2CCQ==\",\"h\":{\"iv\":\"R2IUJJ+EmnDnZvQP\",\"at\":\"zqG5WAJql1zgctRCPpoBkQ==\"}}",
  "original_toy_location" => "{\"p\":\"5syLqDK6GCbBDw==\",\"h\":{\"iv\":\"KBGp4FrI7oL4/a3p\",\"at\":\"JnH6hxLX35cAwroImk2XqQ==\"}}" },
  "toy_location"          => "{\"p\":\"Bd+/TzEysF2CCQ==\",\"h\":{\"iv\":\"R2IUJJ+EmnDnZvQP\",\"at\":\"zqG5WAJql1zgctRCPpoBkQ==\"}}",
  "original_toy_location" => "{\"p\":\"0246w4+SSqqlJw==\",\"h\":{\"iv\":\"1uEnjlCNot9sYNgR\",\"at\":\"UhkhK6YlOTxJg75juqIMGA==\"}}" }

その他のクールな機能

Railsの暗号化には、これまで紹介した機能以外にも多くの機能が備わっています。本を書いているわけではないので詳しくは解説しませんが、そうした機能もいくつか紹介しておきます。

これまで見てきたのは単純な文字列の暗号化でしたが、実はリッチテキスト属性も暗号化可能です。

また、以前利用していた暗号化スキームの利用についてもサポートされています。つまり、当初は非決定論的に暗号化していたカラムを、後で決定論的な暗号化に切り替えられるということです。この機能を使う前には、ぜひドキュメントの隅々まで目を通しておくことをおすすめします。

(非決定的な)キーのローテーションも可能です。素晴らしい機能ですが、現時点では決定論的暗号化には対応していない点にご注意ください。

キーのローテーションに関連する話として、暗号化に用いたキーへの参照を暗号化データ自身に保存する設定も可能です。

決定論的暗号化を使っている場合は、unique制約がサポートされます。暗号化済みカラムの一意性を担保する必要がある場合はいくつか注意点がありますので、使う前に必ずガイドを読んでください。

暗号化済みカラムは、デフォルトでRailsのログから自動的に除外されます。この機能を無効にするオプションも提供されています。

ここで触れておきたいのは、この実装はモジュール化されていて、かなりのレベルでカスタマイズ可能な点です。暗号化オプションの多くは、属性単位でもグローバルなレベルでも設定可能です。

暗号化機能の制約

魅力たっぷりの暗号化機能にもいくつかの制約があります。私が気になった制約を以下にリストアップしました。暗号化の機能は多岐に渡っていて、適用可能なユースケースもさまざまなので、気になる制約も人によって変わってくるでしょう。

  • あいまい検索: 暗号化が提供する検索機能では、検索テキストとの完全一致が必須になります。すなわちLIKEクエリなどが使えないということです。これは、暗号化済みカラムに対するクエリは、生SQLではなく、すべてRailsとActive Recordを経由する必要があるということでもあります。
  • リッチテキストの検索: リッチテキストを暗号化できるのは明らかですが、現時点では非決定論的にしか暗号化できません。つまりリッチテキストは検索できません。
  • 決定論的検索は複数キーをサポートしない: 決定論的な暗号化および検索を使う場合、同時に2つ以上のキーを利用できない点に注意が必要です。キーの変更が必要な場合は、何らかの特殊な操作が必要になりそうです。
  • Railsコンソールでデータが見える: 自明のことと思われるかもしれませんが、万一悪意のある人がRailsコンソールにアクセスすれば、暗号化済みデータをオブジェクトに読み込んで一日中平文を見ることができてしまう可能性があります。Jorge氏のブログ記事によると、HEYでは暗号化に加え、コンソール拡張機能を用いてコンソールアクセスを保護および監査しているそうです。残念ながらこの拡張機能は同社のプライベートgemであり、現時点ではRailsで利用できません3
  • 決定論的暗号化はセキュリティが低下する: 実装そのものの問題ではないと思いますが、決定論的暗号化を用いれば、暗号化された属性に同じ値を持つ2つの行は、暗号を逆解析することはできなくても、一方の行の平文値を突き止められれば他方の行の平文値も判明します。非決定論的暗号化にはこのような弱点はありません。

まとめ

一言でいうと、Railsの暗号化は相当興味をそそられる機能です。他の人たちがこの暗号化機能をどう活用し、今後どう進化するかを見ていくのは素晴らしいことだと感じています。私の予感では、この暗号化機能を使い始める人は今後増え、その人たちがコードを深く調べていくうちに(決定論的暗号化で複数キーがサポートされていないなどの)さまざまな問題が解消されるのではないかと睨んでいます。

関連記事

Rails 7 Alpha 1、2がリリースされました(リリースノート翻訳)


  1. 訳注: 「決定論的暗号化」「非決定論的暗号化」は仮訳です。決定論という語についてはWikipediaを参照。 
  2. 訳注: 原文では512バイトでしたが、edgeガイドに記述されている510バイトとしました。 
  3. 訳注: その後Basecampからリリースされました。Privacy-aware Rails consoles with console1984 and audits1984 

The post Rails 7のActive Record暗号化機能(翻訳) first appeared on TechRacho.

週刊Railsウォッチ: Railsリポジトリで進行中のPropshaft、inverse_ofを自動推論ほか(20211018前編)

$
0
0

こんにちは、hachi8833です。Kaigi on Rails 2021が今週の金曜と土曜に開催されますね。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

以下の公式更新情報を中心に見繕いました。

🔗 コントローラのコールバックでinstance_execを回避

procが(条件またはコールバック自身として)コールバックに渡されると、このprocがコントローラのインスタンス上でinstance_execを用いて評価される。instance_execを呼ぶとそのオブジェクトのシングルトンクラスが新たに作成され、インラインメソッドキャッシュが新たに要求される。

このコミットは、:only:exceptがコールバックに渡された場合にコントローラで余分なシングルトンクラスが作成されるのを回避する。
この効果を誇張した以下のベンチマークも作った。
https://gist.github.com/jhawthorn/bded5bc1d5f1afd4cdd7fb5b800312e1
同PRより


つっつきボイス:「なるほど、改修前のようにprocで処理するとオブジェクトのシングルトンクラスが生成されてしまう↓」

# actionpack/lib/abstract_controller/callbacks.rb#L77
      def _normalize_callback_option(options, from, to) # :nodoc:
        if from = options.delete(from)
-         _from = Array(from).map(&:to_s).to_set
-         from = proc { |c| _from.include? c.action_name }
+         from = ActionFilter.new(from)
          options[to] = Array(options[to]).unshift(from)
        end
      end

「代わりにActionFilterクラスを定義しておいてそれで処理する方が、procで回すよりも生成を削減できて高速化につながるということのようですね」「なるほど、そういえばJeremy Evansさんの『Polished Ruby Programming』にもこんな話がどこかにあった気がします」「考え方としてはValue Objectを連想しますね」

「呼び出される回数がものすごく多いコードをこうやって最適化すると、改善がたとえ数%であっても顕著な差が出る」「たしかにコールバックは呼び出しが多そう」

# actionpack/lib/abstract_controller/callbacks.rb#L38
+   class ActionFilter
+     def initialize(actions)
+       @actions = Array(actions).map(&:to_s).to_set
+     end
+
+     def match?(controller)
+       @actions.include?(controller.action_name)
+     end
+
+     alias after  match?
+     alias before match?
+     alias around match?
+   end

🔗 スコープ付き関連付けでinverse_ofを自動推論

  • スコープ付き関連付けでinverse_ofを自動的に検出できるようになった

inverse_ofの自動検出がスコープ付き関連付けで動くようになった。たとえば、以下のcomments関連付けで自動的にinverse_of: :postが検出されるので、このオプションを渡す必要がなくなる。

class Post < ActiveRecord::Base
  has_many :comments, -> { visible }
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

ただし、まだこの自動検出は逆関連付けにスコープがあると動作しない。この例では、post関連付けにスコープがあると、Railsがcomments関連付けの逆関連付けを探索できなくなる。

これはRails 7の新規アプリでデフォルトになる。オプトインするには以下の設定を用いる。

config.active_record.automatic_scope_inversing = true

Daniel Colson, Chris Bloom
同Changelogより


つっつきボイス:「お、can_find_inverse_of_automaticallyというメソッドを改修することで、スコープ付きの関連付けでinverse_ofを自動検出するようにしたんですね↓」「こんなメソッドがあったとは」

このコミットはcan_find_inverse_of_automaticallyを変更して、関連付けにスコープがあるが逆の関連付けにスコープが存在する可能性がない場合にinverse_ofを自動検出するようにした(can_find_inverse_of_automaticallyは関連付けのリフレクションで最初に呼び出され、trueが返されれば逆のリフレクションを探索し、最終的に逆のリフレクションでそのメソッドを再び呼び出すことでそのメソッドを確実に呼び出せるようにする)。
同PRより抜粋

inverse_ofを毎回手動で書くのは割と手間だったので、副作用がないならこういうのはありがたい👍」「Changelogには逆の関連付けにスコープがあると動かないと書かれていますね」「単純なものなら手動で書けば動かせますけど、逆の関連付けにスコープが付いていると自動でコードを生成するのは簡単ではなさそう」

「コミットメッセージによるとGitHubには関連付けが171個もあるので、これを使ってinverse_ofを自動追加したいとありますね: 確かにこれだけたくさんあると手動でやってたら間違えそう」「コミットしたcomposerinteraliaさんはGitHubのスタッフなんですね」

🔗 コネクションのスキーマキャッシュをlazy loadingできる機能を追加

  • コネクションのスキーマキャッシュをlazy loadingするオプションを追加

従来は、Active Recordでスキーマキャッシュを読み込むには起動時にRailtieを用いる方法しかなかった。このオプションは、コネクションが確立した後でコネクションのスキーマキャッシュを読み込める機能を提供する。コネクションの確立後にキャッシュが読み込まれるので、コネクションのキャッシュを遅延読み込みする機能はマルチプルデータベースを用いるRailsアプリケーションで重宝するだろう。現時点では、Railtiesは起動前にコネクションにアクセスできない。

このキャッシュを用いるには、アプリケーションのコンフィグでconfig.active_record.lazily_load_schema_cache = trueを設定する。さらに、デフォルトの”db/schema_cache.yml”パスを使いたくない場合は、データベースコンフィグにschema_cache_pathも設定するべき。
Eileen M. Uchitelle
同Changelogより


つっつきボイス:「コネクションのスキーマキャッシュをlazy loadingする機能、通常はそれほど必要なさそうに見えるけど、マルチプルデータベースで便利な可能性か、なるほど」「あくまでオプションなので、使いたければ使えるという感じですね」

🔗 pg:dumpでCOMMENTの出力を回避

このプルリクは、PostgreSQL利用時にCOMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';のような行がdb:structure:dump実行時に出力されないようにする。
この種のCOMMENTがあってもあまり価値はないように思える。
しかしこのCOMMENTがあると、#36816で指摘されているようにdb:structure:loadがDBのスーパーユーザーでないと動かなくなり、ActiveRecord::StatementInvalid: PG::InsufficientPrivilege: ERROR: must be owner of extensionのような一般的なエラーが出力されてデバッグがやりにくくなる点がよくない。
同PRより


つっつきボイス:「PostgreSQLバージョンが11以上のときはdb:structure:dumpでCOMMENTを出力しないようにしたんですね」

「なるほど、COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';みたいな行があるとスーパーユーザー以外はdb:structure:loadが失敗するので、失敗しないようにCOMMENTを抑制したということか」「なるほど」「作ったダンプが読み込めないのかと思って焦りそうなので、余分なエラーがなくなるのはいい👍」「ですよね」

🔗 PostgreSQLのクエリパラメータ構文を利用できるようになった

従来は以下を実行すると

ActiveRecord::DatabaseConfigurations::UrlConfig.new(:production, :production, 'postgres:///?user=user&password=passwd&dbname=theapp_production', {}).configuration_hash

以下が返された。

{ :user=>"user", :dbname=>"theapp_production", :adapter=>"postgresql" }

uri.passwordnilなのでpassword属性がnilを返し、このハッシュをマージしたものが上の結果となる。

このプルリクはこの点を修正して以下を返すようになる。

{ :user=>"user", :password=>"passwd", :dbname=>"theapp_production", :adapter=>"postgresql" }

この問題は#42797で指摘された。なお同issueでは、postgres://user:passwd@/theapp_productionはPostgreSQLのURIとして有効という指摘もあった。現在は、以下を実行すると

ActiveRecord::DatabaseConfigurations::UrlConfig.new(:production, :production, 'postgres://user:passwd@/theapp_production', {})

以下のエラーが出力される。これはURIが有効なRFC 2396実装でないことが原因。これを修正するアイデアがあれば求む。

/home/abeid/.rbenv/versions/3.0.1/lib/ruby/3.0.0/uri/generic.rb:207:in 'initialize': the scheme postgres does not accept registry part: user:passwd@ (or bad hostname?) (URI::InvalidURIError)

同PRより


つっつきボイス:「postgres:///?user=user&password=passwd&dbname=theapp_productiみたいにpostgres:///で始まるURIを指定できるようになったんですね」「こんなふうにuserやpasswordを指定するのか」「これはたぶん本当のURI形式とは違うんじゃないかな?」「あくまでURI風ということなのかも」「JDBCとかでこの形式のURIを使った覚えがあります」「そうそう」

参考: Java Database Connectivity - Wikipedia

🔗Rails

🔗 RailsリポジトリにあるPropshaft


つっつきボイス:「この間Railsウォッチをレビューいただいたときに(ウォッチ20211004)このPropshaftの存在を教わったので取り上げてみました」「そうそう、アセットパイプラインの新しい選択肢のひとつが作り中のはずと思って探してみたら、Railsのリポジトリの中にこのProfshaftがあったんですよね」

rails/propshaft - GitHub

「アセットパイプラインで従来のSprocketsの代わりにProfshaftも使えるようになるということみたい」「Propshaftって造語かと思ったらプロペラシャフトでした↓」「Sprocketsもそうですけど、機械の部品っぽい命名にしてるんでしょうね」「くるくる回り続ける部品感ある」

参考: propshaftとは何? Weblio辞書
参考: sprocketの意味・使い方・読み方 | Weblio英和辞書

「PropshaftのREADMEを見ると、アセットのバンドルを頑張らなくてよくなった時代のためのアセットパイプラインライブラリという感じの触れ込みですね」「Rails 7ではアセットのプリコンパイルを避ける方向に持っていきたいはずなので、Propshaftはその一環ということでしょうね: Rails 7で例のimport mapが導入されることで、Sprocketsのときよりも機能を軽量化できた感じ」「なるほど」

「READMEの末尾にある『PropshaftはRails 7に入るのか?』というFAQに『入る可能性は高いけど当面Sprocketsのサポートも必要』とありますね」「まだしばらくかかりそうかな」

Rails 7: import-map-rails gem README(翻訳)

🔗 Active Supportの#descendantsメソッドを深掘りする(Ruby Weeklyより)

参考: ActiveSupport::DescendantsTracker


つっつきボイス:「descendantsメソッドやancestorsメソッドは1〜2年おきぐらいに話題になる感じ」「クラスやモジュールの読み込みリストをチェックするメソッドですね」「記事にもあるように、モジュールをincludeprependした結果の順序などが罠になることがあります」

🔗 SeleniumによるシステムテストをCupriteに移行してみた(Ruby Weeklyより)


つっつきボイス:「ChromeでテストするならCupriteでいいんじゃないかな: 手順が丁寧に説明されていてよさそう👍

「記事でも取り上げていますけど、Ajax/Fetch周りが割と複雑」「記事ではテストを書き直さないといけなかったそうです」「イベントを取得するとか、通信が発生して書き換えを待つような動作のテストは難しい部分ですね」

「そういえば少し前にもCupriteに変えてみた記事があったのを思い出しました↓」「こういう実際に動かしてみた記事が増えてくると助かります🙏

参考: 2021年6月現在、Cupriteで”正しい”システムテストはできるのか?

🔗 SidekiqをActive Job経由ではなく直接使う(Ruby Weeklyより)

# 同記事より: Sidekiqを直接使う場合
class DoThingsInBackgroundJob
  include Sidekiq::Worker
  Sidekiq_options queue: "default"

  def perform(id)
    an_active_record_object = ActiveRecordObject.find_by(id: id)
    an_active_record_object.do_things
  end
end

つっつきボイス:「タイトルで言いたいことはだいたい理解できた😆」「気持ちわかります」「Active Jobは抽象化されている分機能が少な目なので、生のSidekiqを使う方が話が早い」

参考: Active Job の基礎 - Railsガイド

「記事の末尾に『SidekiqはPro版にするのがおすすめ』とありました」「Sidekiqの有料版を使ったことはないけど、十分普及していて経営も順調なサービスならサポートなどの面を考えてもお金を払う価値はあるでしょうね」「たしかに」

参考: Sidekiq: Simple, efficient background jobs for Ruby.

「お、Sidekiqの価格表に支払い方法が書かれているのがちょっと珍しいかも: Enterprise版は請求書払いができるとありますよ」「ほんとだ」「請求書払いに対応していないと日本企業でなかなか決済が通らなかったりしますよね」「そういう傾向はありますね」「最近はさすがに減りつつあると思いますけど」

🔗 RSpecのsubject


つっつきボイス:「RSpecのsubjectは、うまく適合するケースの方が少ないなと自分も思います: itがたくさんある場合にはsubjectがあると明らかに繰り返しが減りますけど、記事にもあるように以下のような使い方はたしかに最悪↓」「う、subjectの中でincrementしてる」「subjectの中での改変はやめて欲しい」

# 同記事より: subjectが向いてないケース
class Counter
  attr_reader :count

  def initialize
    @count = 0
  end

  def increment
    @count += 1
  end
end

describe 'Counter#increment' do
  let(:counter) { Counter.new }
  subject { counter.increment }
  it do
    subject
    expect(counter.count).to eq 1
  end
end

「行数の多いテストにsubjectがあると、このテストのsubjectはどこだっけと探さないとわからなくなりますよね」「たしかに」「subjectには名前を付けて呼び出せる機能もあるので、それを使うならsubjectがあってもいいかなと思います: 名前付きsubjectは嫌いじゃない」「なるほど」

参考: Explicit Subject - Subject - RSpec Core - RSpec - Relish

🔗 その他Rails

つっつきボイス:「上はr7kamuraさんのRailsアップグレード記事ですね」

「r7kamuraさんはたしか以前からRailsアップグレード作業を請け負っていますね↓」「お〜」「Railsのアップグレードは社内の人がなかなかやりたがらないこともあったりするので、Railsアップグレードに特化して業務を請け負うのはひとつの生存戦略だなと思いました」「そうそう、アップグレードのノウハウも蓄積できますよね」

参考:『Railsアップグレード百景』という題で発表した


前編は以上です。

バックナンバー(2021年度第4四半期)

週刊Railsウォッチ: Ruby 3.1にYJITマージのプロポーザル、Rubyのmagic historyメソッド、JSのPartytownほか(20211012後編)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: Railsリポジトリで進行中のPropshaft、inverse_ofを自動推論ほか(20211018前編) first appeared on TechRacho.

週刊Railsウォッチ: ruby/debugをChromeでリモートデバッグ、Rubyアプリの最適化ほか(20211019後編)

$
0
0

こんにちは、hachi8833です。発表された新型MacBook Proのノッチが…

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Ruby

🔗 ruby/debugのChrome用リモートデバッグ機能


つっつきボイス:「BPS社内Slackに貼っていただいたツイートです」「これはいい機能👍

ruby/debug - GitHub

「これは?」「ruby/debugの中からHTTPサーバーを起動してそこにChromeでアクセスすると、Chromeからruby/debugを制御できるという、ruby/debugの新機能です」「お〜動画でChromeの中にデバッガコンソールが見えた」「いわゆるリモートデバッグ機能そのもの」「Chrome拡張を使うのかと思ったら不要なんですね」「Chrome Devtoolsで公開されているChromeの独自機能を使っているようです」

現時点ではmasterブランチに入っています。

🔗 Rubyアプリを最適化する(Ruby Weeklyより)


つっつきボイス:「ちょっと眺めた限りではシングルスレッドで複数のIOをチェインして同時処理させる話のようですね」

参考: class Thread (Ruby 3.0.0 リファレンスマニュアル)

Backend#chainを使うと、こんなふうにI/O操作をチェインできるらしい↓」

# 同記事より
Thread.current.backend.chain(
  [:write, @conn, "#{len.to_s(16)}\r\n"],
  [:splice, r, @conn, len],
  [:write, @conn, "\r\n"]
)

「このあたりのRESPOND_FROM_IO_PROGRAMの内側がインタプリタとかASTパーサーっぽく見える↓」

# 同記事より
# program references:
# 0 - headers
# 1 - io
# 2 - @conn
# 3 - pipe r
# 4 - pipe w
# 5 - chunk_size
RESPOND_FROM_IO_PROGRAM = [
  [:write, 2, 0],
  [:loop,
    [:splice, 1, 4, 5],
    [:break_if_ret_eq, 0],
    [:store_ret, :len],
    [:write_cte_chunk_size, 2, :len],
    [:splice, 3, 2, :len],
    [:write, 2, "\r\n"]
  ],
  [:write, 2, "0\r\n\r\n"]
]

def respond_from_io(request, io, headers, chunk_size = 2**14)
  formatted_headers = format_headers(headers, true, true)
  r, w = IO.pipe
  Thread.backend.submit(RESPOND_FROM_IO_PROGRAM, formatted_headers, io, @conn, r, w)
end

「お、同じ部分を今度はDSL化して読みやすくした感じですね↓」

# 同記事より
RESPOND_FROM_IO_PROGRAM = Polyphony.io_program(
  :headers, :io, :conn, :pipe_r, :pipe_w, :chunk_size
) do
  write :conn, :headers
  io_loop do
    splice :io, :pipe_w, :chunk_size
    break_if_ret_eq 0
    store_ret :len
    write_cte_chunk_size :conn, :len
    splice :pipe_r, :conn, :len
    write :conn, "\r\n"
  end
  write :conn, "0\r\n\r\n"
end

「最後のまとめを見ると、この記事ではトップの層でRubyのデータ構造を使い、I/O操作のような単純な処理は下の層のCで操作することで並列実行しやすくしたようですね」「Rubyでもこんなふうにすると速くできるよと」「Linuxのio_uringと似たアプローチとありますね」

本記事ではRubyアプリのパフォーマンス最適化のためにプログラムを2つの層に分離するアプローチを紹介しました。つまり、Rubyのデータ構造を用いて低レベル処理を表現するRubyのトップ層と、それらの処理を最適な形で実行するCの実装の層です。このアプローチでは、chunkedエンコーディングによるHTTPレスポンス送信、受信データ解析、I/Oのループ処理などの複雑な処理を長時間行う場合に特に有効です。
上述のようにこのアプローチはLinuxのio_uringで用いられているものと似ています。考え方は同じで、(I/O)操作をデータ構造で表現し、その実行を最適化された下の層(io_uringではカーネル、Rubyの場合はC拡張)に移行しています。
同記事より

参考: Transfer-Encoding - HTTP | MDN
参考: io_uringで高速IO処理(?) | κeenのHappy Hacκing Blog

「ところでさっきのRESPOND_FROM_IO_PROGRAMの中身はリテラルになっていますけど↓、こうするとRubyのインタプリタが仕事しなくて済むようになってJITが効きやすくなるのかもと思いました」「たしかにリテラルですね」

# 同記事より抜粋
RESPOND_FROM_IO_PROGRAM = [
  [:write, 2, 0],
  [:loop,
    [:splice, 1, 4, 5],
    [:break_if_ret_eq, 0],
    [:store_ret, :len],
    [:write_cte_chunk_size, 2, :len],
    [:splice, 3, 2, :len],
    [:write, 2, "\r\n"]
  ],
  [:write, 2, "0\r\n\r\n"]
]

🔗 HTTPI: RubyのHTTPクライアント向けの共通インターフェイス(Ruby Weeklyより)

savonrb/httpi - GitHub


つっつきボイス:「HTTPI、見たことなかった」「新しそうなライブラリですね」

# 同リポジトリより
require "httpi"

# create a request object
request = HTTPI::Request.new
request.url = "http://example.com"

# and pass it to a request method
HTTPI.get(request)

# use a specific adapter per request
HTTPI.get(request, :curb)

# or specify a global adapter to use
HTTPI.adapter = :httpclient

# and execute arbitrary requests
HTTPI.request(:custom, request)

「これは何をするものなんでしょう?」「Rubyにはこのサイトにも書かれているようなHTTPClientやNet::HTTPのようないわゆるHTTPクライアントライブラリがたくさんありますけど、それぞれのインターフェイスはまちまちなので、それらを統一して呼べるようにして、HTTPI.adapter = :httpclientみたいにアダプタを切り替えるだけでHTTPクライアントライブラリを切り替えられるということでしょうね」「なるほど、RubyのHTTPクライアントライブラリ向けのラッパーでしたか」

「ただ、HTTPクライアントライブラリを使い分ける機会はそうそうないと思いますけど」「ライブラリをこれと決めたら普通はそのまま使いますよね」「Active Jobを経由せずにSidekiqを直接使う話(ウォッチ20211018)と似ているかも」「MySQLからPostgreSQLに移行することがめったにないのもそうですね」「ライブラリごとに機能も違ってくるので、ライブラリ間の差異を共通化レイヤで吸収するのはそれなりに大変」

「HTTPIは新しいライブラリですし、今すぐproductionで使うものでもないので、もしかすると練習用として作ってみたのかなと想像してみました: こういうのを自分でやってみると楽しく勉強できると思います」「特定のHTTPライブラリのバグを切り分けるのに使えるかもしれませんね」

🔗 その他Ruby


つっつきボイス:「ruby/specリポジトリで、Ruby 3.0の新機能や機能変更のspecを書いて欲しいという募集だそうです」

#823を見ると、specを書いて欲しい3.0の機能がちゃんとリストになっているのがいいですね」「完了のチェックボックス、この間見たときより増えてるみたいです」「これならコントリビュートしやすそう」「やってみようかな」

その後もチェック済みは着々と増えているようです: Pull requests · ruby/spec

「お、IBM720なんてエンコーディングがあるんですって(#16233)」「聞いたことないですね」

参考: MFT で使用できるコード・ページ - IBM Documentation

🔗DB

🔗 SpannerにPostgreSQL互換インターフェイスが追加(Publickeyより)


つっつきボイス:「そうそう、GoogleのSpannerにこの機能が入りましたね: Spannerはかなり高価だったんですが、最近値下がりして以前より使いやすくなりつつあるので、PostgreSQL互換のインターフェイスが使えるようになればより手を出しやすくなりそう」

参考: Cloud Spanner  |  Google Cloud

「AWSにはRDSやAurora PostgreSQLがありますけど、これまでGCPにはマルチAZやフェイルオーバーまですべて備えたようなRDS的なサービスというとCloud SQLしかなかったと思うので、今回の発表でSpannerがGCPでの選択肢に入ってきそうですね」「なるほど」「AWSからSpannerを使おうとするとネットワーク的に遠いという問題はありますが」「GCPからSpannerを使う方が無理がなさそうですね」「レイテンシはその方がよいでしょうね」

参考: Amazon RDS(マネージドリレーショナルデータベース)| AWS
参考: Amazon Aurora PostgreSQL の特徴 | MySQL PostgreSQL リレーショナルデータベース | アマゾン ウェブ サービス
参考: Cloud SQL ドキュメント  |  Google Cloud

「Spannerはスケーラビリティがすごく高くてオートスケールも強いんですが、これほどの高性能が必要な案件はなかなかないでしょうね」「あ〜」「あるとすれば、ソシャゲやワクチン予約受付サイトのようにユーザー数がいくらでも増える可能性やアクセスが短時間にものすごく集中する可能性もあって、かつランニングコストに見合うサービスかな」「なるほど」「そのぐらいの規模になるとAWSのオートスケールだとアクセス急上昇に間に合わないかも」「ソシャゲだとSpannerはちょっともったいなさそうですけどね」

「その意味では、AWSのAurora PostgreSQLは負荷に応じてオートスケールできて、しかもRDSと値段がほとんど変わらないのがいいんですよ」「たしかに値段がほぼ同じなら迷わずAurora PostgreSQLを選ぶでしょうね」「SpannerはDBインスタンスを立てるのに比べて安くないので悩ましいところ」

「まだGCPやSpannerは本格的に運用したことはありませんが、Spannerは選択肢のひとつとしておくとよさそう👍


「ところで元記事ではSpannerがNoSQLとして紹介されていたけど、NoSQLだったかな?」「公式ブログ↓を見ると、当初はNoSQLキーバリューストアとして設計されたけどリレーショナルモデルも採用したとありますね」「なるほど、自分の中ではSpannerは無限といってもいいぐらいにスケールできるRDBという位置づけだったけど、合ってるみたいでよかった」

参考: NoSQL から新しい SQL へ : グローバルなミッションクリティカル DB へと進化を遂げた Cloud Spanner | Google Cloud Blog

🔗クラウド/コンテナ/インフラ/Serverless

🔗 AWS Lambdaバトル


つっつきボイス:「これは自分も見ましたけど、ベンチマークのソースコードを見るとJSONを取ってきてDynamoDBに格納しているだけで、実行環境での起動以外ではほとんどリソースを使っていないんですよ」「そんなに軽い処理なんですか?」「コードに分岐すらありません」

Aleksandr-Filichkin/aws-lambda-runtimes-performance - GitHub

# aws-lambda-runtimes-performance/ruby-lambda/app.rb
require 'json'
require 'aws-sdk-dynamodb'

 $client = Aws::DynamoDB::Client.new

def create(event:,context:)
  body = event["body"]
  book =JSON.parse(body)
  id = SecureRandom.uuid
  book["id"]=id

  table_item = {
    table_name: "book",
    item: book
  }
  $client.put_item(table_item)
  { statusCode: 201, body: JSON.generate(book) }

end

参考: Amazon DynamoDB(マネージド NoSQL データベース)| AWS

「このベンチマークは実行環境のプロセス起動を比較しているようなものなので、ビジネスで使うサービスを運用するうえではあまり参考にならないかなと思います」「なるほど」「Rubyのグラフはメモリーリークっぽく上昇してはいますけど、自分の印象ではRuby十分速いと思いました」

「ちゃんとベンチマークするのは難しい…」「言語が違えば実装も変わりますし、特にJSONパーサーはライブラリによって速度がかなり変わります」「それもそうですね」


なお、LambdaバトルでNodeが遅い件について、AWS_NODEJS_CONNECTION_REUSE_ENABLEDがオンになっていない可能性があるのではというissueが立っている↓とBPS社内メンバーからメンションをもらいました。

参考: Optimize NodeJS function warm starts by enabling TCP connection reuse by bilalq · Pull Request #6 · Aleksandr-Filichkin/aws-lambda-runtimes-performance

🔗JavaScript

🔗 jQuery UIとjQuery Mobileが開発終了(Publickeyより)


つっつきボイス:「今回はたまたまPublickeyの記事が多めになりました」「jQuery UIとjQuery Mobileがついに」

「ツイート↓で見かけましたけど、jQueryは今でもすごく使われているんですね」「実際jQueryで十分な場合も多いですし、ライブラリの種類も豊富だし、デザイン系も含めるとjQueryを扱える人は多いですね」「なるほど」

「jQueryがこれだけ人気を得た理由のひとつは、複数ブラウザで共通に利用できたことでしょうね: 今はその役割はほぼ終わったと思いますが」「そうですね」「蓄積されたライブラリは簡単にはなくならないと思います」

「jQueryは複数ブラウザで使えるJSライブラリのさきがけだったんでしょうか?」「Prototype.jsも同じぐらいの時期だったかな: 一時期はPrototype.jsが優勢だったこともありましたけど今は消えた」「Prototype.jsは2015年までメンテされてたんですね」

参考: Prototype JavaScript Framework - Wikipedia

「そういえばjQueryとPrototype.jsを両方置くと名前空間が衝突するのを思い出した↓」「そうだったかも」「どちらも$を使うからですね」「jQuery.noConflict();、なつかしい」

参考: prototype.jsと同時に使うには - jQuery 日本語リファレンス

🔗言語/ツール/OS/CPU

🔗 計算量とオーダー


つっつきボイス:「はてブでバズっていました」「O(m)みたいに書くビッグオー記法以外にもΘΩとかいろんな記法があるんですね」「私もビッグオーしか知りませんでした」「みっちり書かれてる」

参考: ランダウの記号 - Wikipedia

「計算量やオーダーの概念はプログラマーにとって大事ですし、アルゴリズムごとにオーダーもありますけど、現実のユースケースだとデータや検索にlocality(局所性)が絡んだりして、きれいに正規分布しないことも多いですよね」「そうそう」「アルゴリズムの特徴を知っておくのは大事だけど、計算量を覚えるだけだとあまり有効でない気がします」

参考: 参照の局所性 - Wikipedia

「具体的な計算量はググればわかるので、むしろ計算量という概念があることは知ってて欲しいですよね」「コードレビューで3重ネストがあったときに、これだと計算量がこのぐらいになるから早期脱出を入れようとか、そういうやりとりはしますね」「そうそう、丸暗記しなくてもいいので、コードに関してやりとりするときにその視点は持ってて欲しい」

「計算量やオーダーは知っていて損はしないので読んでおくといいと思います👍


後編は以上です。

バックナンバー(2021年度第4四半期)

週刊Railsウォッチ: Ruby 3.1にYJITマージのプロポーザル、Rubyのmagic historyメソッド、JSのPartytownほか(20211012後編)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Ruby Weekly

Publickey

publickey_banner_captured

The post 週刊Railsウォッチ: ruby/debugをChromeでリモートデバッグ、Rubyアプリの最適化ほか(20211019後編) first appeared on TechRacho.

‘rails runner’セッションをプロらしく拡張する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

rails runnerセッションをプロらしく拡張する(翻訳)

Railsコンソールの思いがけない便利技(翻訳)

弊社のPawełが上の記事でRailsコンソールをチューンナップする方法を紹介していましたが、実はrunnerメソッドでランナーセッションを拡張できることを彼に伝えたいと思います。

ランナーセッションにはRails::ConsoleMethodsのようなモジュールがないので、拡張が少々不便です。そのため、ランナースクリプトで利用可能なメソッドを追加するのはそれほど簡単ではありませんが、ランナーセッションの開始時と終了時に実行されるコードを追加することなら可能です。

たとえば、スクリプトの評価が終了したかどうかをターミナルで確認しなくても済むよう何らかの通知を送信できますし、時間の計測や弊社のRails Event Storeのメタデータを設定すれば、セッション中の変更内容も簡単に確かめられます。

私たちのプロジェクトで使われている例を紹介しましょう。ここでは経過時間を記録し、Rails Event Storeのメタデータを設定し、Slack通知を送信します。これは以下のコードをconfig/application.rb に追加するだけでできます。

runner do
  session_id = SecureRandom.uuid
  script = ARGV.join(" ")
  Rails.configuration.event_store.set_metadata(
    causation_id: session_id,
    correlation_id: session_id,
    script: script,
    locale: I18n.locale.to_s,
  )
  t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  notify_slack(
    username: "Script runner",
    text: "[#{Rails.env}] Runner script session #{session_id} has started: '#{script}'",
    channel: "notifications",
    icon_emoji: ":robot_face:"
  )

  at_exit do
    notify_slack(
      username: "Script runner",
      text: "[#{Rails.env}] Runner script session #{session_id} has finished: '#{script}' (elapsed: #{Process.clock_gettime(Process::CLOCK_MONOTONIC) - t} seconds)",
      channel: "notifications",
      icon_emoji: ":robot_face:"
    )
  end
end

このスニペットを使えば、rails runnerでスクリプトを実行(あるいはインラインのRubyコードを評価)するたびに、Rails Event Storeインスタンスのメタデータが設定され、runnerセッションの開始時と終了時にSlack通知が送信されるようになります。

お知らせ

ARKADEMY.DEVに参加してArkencyのトップクラス教育プログラムコースにアクセスしましょう!「Railsアーキテクトマスタークラス」「アンチ”IF”コース」「忙しいプログラマーのためのブログ執筆コース」「Async Remoteコース」「TDD動画クラス」「ドメイン駆動Rails動画コース」以外にもさまざまなコースが新設中です。

関連記事

Rails: Zeitwerkオートロードの「1ファイルにクラスを複数置けない」問題を回避する

The post ‘rails runner’ セッションをプロらしく拡張する(翻訳) first appeared on TechRacho.

Rails 7: ActiveRecord::Base.loggerがclass_attributeで7倍高速化(翻訳)

$
0
0

Rails 7: ActiveRecord::Base.loggerがclass_attributeで7倍高速化(翻訳)

私たちの見解では、このプルリクはRails 7におけるきわめてシンプルかつ大きなパフォーマンス改善です。最近のRubyで、クラス変数の読み取りにインラインキャッシュが導入されました(#177631。これにより、クラス変数の値解決で複雑な継承ツリーをたどるかわりにキャッシュから値を読み取れるようになりました。Rubyでクラス変数が読み込まれると、継承ツリーにある各クラスをチェックして、そのクラス変数がツリー内の他のクラスに設定されていないことを確認する必要があります。

もうお気づきかと思いますが、これはO(n)問題になります。ツリー内のノード数が増えるにつれて、読み取りのパフォーマンスは線形に低下します。

それでは、1個のモジュールを継承するクラス、30個のモジュールを継承するクラス、最後に100個のモジュールを継承するクラスを使ったデモを見てみましょう。

require "benchmark/ips"

MODULES = ["B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "AA", "BB", "CC", "DD", "EE", "FF", "GG", "HH", "II", "JJ", "KK", "LL", "MM", "NN", "OO", "PP", "QQ", "RR", "SS", "TT", "UU", "VV", "WW", "XX", "YY", "ZZ", "AAA", "BBB", "CCC", "DDD", "EEE", "FFF", "GGG", "HHH", "III", "JJJ", "KKK", "LLL", "MMM", "NNN", "OOO", "PPP", "QQQ", "RRR", "SSS", "TTT", "UUU", "VVV", "WWW", "XXX", "YYY", "ZZZ", "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIII", "JJJJ", "KKKK", "LLLL", "MMMM", "NNNN", "OOOO", "PPPP", "QQQQ", "RRRR", "SSSS", "TTTT", "UUUU", "VVVV", "WWWW"]
class A
  @@foo = 1

  def self.foo
    @@foo
  end

  eval <<-EOM
    module #{MODULES.first}
    end

    include #{MODULES.first}
  EOM
end

class Athirty
  @@foo = 1

  def self.foo
    @@foo
  end

  MODULES.take(30).each do |module_name|
    eval <<-EOM
      module #{module_name}
      end

      include #{module_name}
    EOM
  end
end

class Ahundred
  @@foo = 1

  def self.foo
    @@foo
  end

  MODULES.each do |module_name|
    eval <<-EOM
      module #{module_name}
      end

      include #{module_name}
    EOM
  end
end

Benchmark.ips do |x|
  x.report "1 module" do
    A.foo
  end

  x.report "30 modules" do
    Athirty.foo
  end

  x.report "100 modules" do
    Ahundred.foo
  end

  x.compare!
end

キャッシュなしのRubyでは以下の結果になります。

Warming up --------------------------------------
            1 module     1.231M i/100ms
          30 modules   432.020k i/100ms
         100 modules   145.399k i/100ms
Calculating -------------------------------------
            1 module     12.210M (± 2.1%) i/s -     61.553M in   5.043400s
          30 modules      4.354M (± 2.7%) i/s -     22.033M in   5.063839s
         100 modules      1.434M (± 2.9%) i/s -      7.270M in   5.072531s

Comparison:
            1 module: 12209958.3 i/s
          30 modules:  4354217.8 i/s - 2.80x  (± 0.00) slower
         100 modules:  1434447.3 i/s - 8.51x  (± 0.00) slower

それではキャッシュありのRubyの結果を見てみましょう。

Warming up --------------------------------------
            1 module     1.641M i/100ms
          30 modules     1.655M i/100ms
         100 modules     1.620M i/100ms
Calculating -------------------------------------
            1 module     16.279M (± 3.8%) i/s -     82.038M in   5.046923s
          30 modules     15.891M (± 3.9%) i/s -     79.459M in   5.007958s
         100 modules     16.087M (± 3.6%) i/s -     81.005M in   5.041931s

Comparison:
            1 module: 16279458.0 i/s
         100 modules: 16087484.6 i/s - same-ish: difference falls within error
          30 modules: 15891406.2 i/s - same-ish: difference falls within error

Rubyのmasterブランチでは、モジュール100個をincludeするとモジュール1個のincludeの8.5倍遅くなります。しかしキャッシュを使えば、モジュール1個のincludeとモジュール100個のincludeのパフォーマンスは変わらなくなります。

それでは、Railsコアチームがこのパフォーマンス向上をどのようにRailsに取り入れたかを見てみましょう。

変更前

ActiveRecord::Base.loggerは継承ツリーに63個2のモジュールを持つcvar(クラス変数)です。以下を実行すればこのことを確かめられます。

ActiveRecord::Base.ancestors.size

# => 62

ActiveRecordコアのコードを見てみると、ロガーは以下のように定義されています。

mattr_accessor :logger, instance_writer: false

ここがクラス変数としてではなくmattr_accessorとして定義されているので、このままでは最新のRubyで導入されたパフォーマンス改善が効いてくれません。

変更後

Railsへのプルリク#42237によって、loggerの定義方法が以下のように変更されました。

class_attribute :logger, instance_writer: false

それではパフォーマンスを比較して改善を確かめてみましょう。

Calculating -------------------------------------
              logger      1.700M (± 0.9%) i/s -      8.667M in   5.097595s
             clogger     11.556M (± 0.9%) i/s -     58.806M in   5.089282s

Comparison:
             clogger: 11555754.2 i/s
              logger:  1700280.4 i/s - 6.80x  (± 0.00) slower

7倍近い高速化は大きな改善です!現実のRailsアプリケーションが強化された見事な例です。

注意

この変更にはいくつかの注意点があります。

ActiveRecord::Base.loggerclass_attributeになったため、@@logger で直接アクセスできなくなります。また、サブクラスに logger = を設定しても親クラスのロガーを変更できません。

logger はほとんどの場合単なるメソッドとして使われているので、きわめてささいな不都合に過ぎませんが、注意するに越したことはありません!

この改修が皆さんのRailsアプリのコードベースで効くかどうかを今のうちに調べておきましょう。

関連記事


  1. 訳注: Rubyのこの機能は2021年5月に#4340でmasterブランチにマージされているので、利用できるのはRuby 3.1以降となります。 
  2. 訳注: Rails 7 alpha2では70個でした。参考まで。 

The post Rails 7: ActiveRecord::Base.loggerがclass_attributeで7倍高速化(翻訳) first appeared on TechRacho.

Rails: 提案「コントローラから`@`ハックを消し去ろう」(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

週刊Railsウォッチで絶賛された記事です。Rails Wayから外れるのでRails中級以上向けですが、Rails初心者も知っておいて損はありません。

太字は訳で追加いたしました。

  • 2018/06/14: 初版公開
  • 2021/10/21: 更新

Rails: 提案「コントローラから@ハックを消し去ろう」(翻訳)

少し前に私が書いた記事で、Railsコントローラをメンテしやすくするために私が使っていた、伝統的でない戦略をいくつかご紹介しました。考え方そのものは今でもまったく問題ないと思っていますが、その中でもとりわけ気に入っているものについては、Railsで標準になるべきだとも思っています。

そのために本記事は、Railsのコントローラでデータの読み込みやアクション間での共有、ビューとのやりとりの手法を変更すべきであるという提案を皆さんに納得していただくための事例を作成するために執筆しています。

「Rails Way」のおさらい

私の事例を紹介する前に、「Rails Way」のどの点が素晴らしく、どの点が不十分であるかをしっかり理解しておくことが重要と考えます。それによって、私からの提案がさらに明瞭になればと願っています。

データの読み込みやビューとのデータのやり取りを、冗長な(Rails的でない)方法で行うと次のようになります1

def show
  user = User.find params[:id]
  render :show, locals: { user: user }
end

運のよいことに、Railsフレームワークの開発者はどのアクションも最終行が似たり寄ったりであることに気付いたので、テンプレートに渡す必要のあるデータを何度も書くことにきっと嫌気がさしたのでしょう。そしてこうした定型コード(boilerplate)を減らすために賢い方法を2つ編み出しました。

  • @変数のハック
  • 暗黙のレンダリング

では、これら2つのRails的手法を適用するとどうなるかを見てみましょう。

def show
  @user = User.find params[:id]
end

暗黙のレンダリングでは、レンダリングするテンプレートをアクション名で決定します。私はこちらのRails Wayを愛していますので、これは変えたくありません。問題にしたいのは、もうひとつの@変数のハックの方です。

@変数のハック

そもそも「@変数のハック」とは何でしょうか。Railsフレームワークの開発者は、何らかのハッシュ的なものを用いてビューに変数を渡すのではなく、その変数にマーキングするだけの方がよいだろうという決定を下したのです。

マーキングはどのようにして行われるかご存知でしょうか?Ruby自身には、変数にマーキングする方法が2とおりあります。変数の先頭に$記号または@記号を付けることでマーキングできます。

Rubyで$記号を付けると、値がグローバルになるのでよくありません。では@記号はどうかというと、通常であれば、その変数は同一インスタンス上のあらゆるメソッド呼び出しで共有されるべきであるという指定になります。これはRubyや多くのオブジェクト指向言語でネイティブサポートされている「インスタンス変数」と呼ばれるものです。

Railsフレームワークの開発者は、コントローラではインスタンス変数の使いみちがあまりないことに気付きました。コントローラは、概念上(開発者目線では、ですが)以下を行います。

controller = UsersController.new request
response = controller.show

もちろん内部では他にもいろいろやっているのは承知のうえで、概念上は以下にまとめられます。

  • インスタンスを1つ作成する
  • メソッドを1つ呼び出す
  • 作成されたオブジェクトを渡す

これは本質的にオブジェクト指向的というより関数型的です。

コントローラオブジェクトはさほど長生きしませんし、呼び出すメソッドはたった1つなので、コントローラではインスタンス変数の出番があまりありません。Railsフレームワークの開発者はここに目をつけて、@というマーカーを別の目的に転用する決断を下したのです。

技術的にはインスタンス変数であることは変わりませんが、Railsはこれらの変数を監視してビューにコピーします。これによって、ビューに渡す変数のハッシュを指定する必要がなくなりました。

ある機能を(言語設計者の)意図しない目的に転用するという意味で、私はこれをハックと呼んでいます。「ハックだからよくない」ということではありませんのでお間違いなきよう。定形コードを削減するために未使用の機能を転用するのは賢いやり方です。私が問題にしたいのは、このハックそのものではなく、実際にはこのハックでも満たせないニーズがあるという点に尽きます。

何が問題なのか

@変数のハックのどのあたりに問題があるのでしょうか?@マーカーを転用したことが問題なのではなく、読み込みのパターンが複数のアクション間で共有されてしまっていることが問題なのです。次のように、いくつものアクションが同じようなデータを欲しがるというのはよくあることです。

def show
  @user = User.find params[:id]
end

def edit
  @user = User.find params[:id]
end

他のupdatedestroyなどのメンバーアクションも同様です。Railsには、甚だしい繰り返しをコールバックで解決する手法があります。上のように書く代わりに、以下のように書けます。

before_action(only: %i[show edit]) { @user = User.find params[:id] }

def show
end

def edit
end

しかしコールバックによる手法にはいくつもの問題があります。

  • only:except:を用いて特定のアクションだけを対象にしようとするとエラーが発生しやすくなります。これらのリストがちゃんとメンテされていないばかりに誤ったデータを読み込んでいたアプリを山ほど目撃してきました。
  • アクションの実際の動作が見えづらくなります。アクションを眺めただけでは、一見何もしてないように見えてしまいます。
  • 現実世界の巨大なコントローラでコールバックを把握づらくなる可能性があります。コールバックが親クラスで定義されていたりモジュールとしてincludeされていればなおさらです。
  • ソースコードの順序がシビアになります(あるbefore_actionフックで別のbefore_actionで読み込んだデータが必要になるなど)

「メソッドでやろうよ」

私の提案するソリューションはいたってシンプルです。一言で言えば「メソッドでやろうよ」です。

私の提案では私たちのニーズがすべて勘案されているので、よくできた小さな定形コードを素早く構築できます。定形コードはまさに@ハックが殺そうとしていたものなので、これでは歴史を繰り返しているように思われるかもしれません。私のアイデアがお気に召すかどうかを皆さんにご検討いただくためにも、どうかもうしばらくお付き合いください。最終的には、ささやかなメタプログラミングを用いてあらゆる定形コードをラップし、(@ハックの)メリットを失うことなく簡潔なコードに作り変えます。

メソッドを定義する

典型的なオブジェクト指向システムにおいて、あるコンポーネントが他のコンポーネントからデータを取得する最もシンプルなソリューションと言えば何だかおわかりでしょうか?最初のコンポーネントが、その情報を担当する次のコンポーネントにメッセージを送信することです。この「メソッドを介するメッセージ送信」は、オブジェクト指向プログラミングの核となるアイデアです。

さて、データをビューに送信しようとするのではなく、両者の立場を逆転させて、ビューが自分の欲しいデータをコントローラに問い合わせる形にしてはどうでしょうか?つまり、データを要求するメッセージはビューからコントローラに送信し、コントローラはレスポンスでデータを返すことになります。

ビューでは以下のような感じになります。

<%= controller.user.name %>

そしてコントローラは次のような感じになります。

def user
  User.find params[:id]
end

これは単なるpublicメソッドであり、オブジェクト指向としてはごく普通の考え方です。このメソッドは別のテンプレートからも使えます。このuserをeditテンプレートとshowテンプレートのどちらも欲しがっているのであれば、どちらも同じメソッドを呼べばよいのです。indexテンプレートが欲しがっていなければ、indexテンプレートで呼ばなければよいのです。必要もないのにデータを読み込むことはしません。コールバックの定義でonly:except:のリストを今後もメンテし続ける必要もありません。

メモ化

userの属性を大量に出力したいが、コントローラで新しいインスタンスを毎回読み込むのは嫌だ。そんなときは次のように読み込みをメモ化(memoize)しましょう。

def user
  @user ||= User.find params[:id]
end

上のコードではあえてインスタンス変数を用いていますが、これはインスタンス変数の本来の用い方です(同一インスタンス内にある異なるメソッド呼び出し同士でデータを共有する)。上のコードではインスタンス変数がnilの場合が考慮されていませんので、もう少しちゃんと書くと次のようになります。

def user
  return @user if defined? @user
  @user = User.find params[:id]
end

ヘルパー

もはや概念上は「ハック」ではなくなりましたが、その分テンプレートはわずかに冗長になりました(コントローラも冗長になりましたが、これについては後述します)。変数アクセスごとにcontroler.変数のようなプレフィックスをいちいち付けたくありません。次のようなヘルパーメソッドでコントローラへプロキシしてみてはどうでしょうか。

module UsersHelper
  def user
    controller.user
  end
end

これでテンプレートは以下のように書くだけで済みます。

<%= user.name %>

これはこれでありがたいのですが、データ読み込み系メソッドごとにヘルパーメソッドをいちいち書くのはだるくて仕方ありません。ありがたいことに、Railsにはこんなときに使える手があります。コントローラのどのメソッドでも、helper_methodを呼んでおきさえすればRailsがヘルパーメソッドを代わりにこしらえてくれます。これでコントローラのデータ読み込み部分は次のように書けます。

def user
  return @user if defined? @user
  @user = User.find params[:id]
end
helper_method :user

メソッドを「代入可能」にする

これらのメソッドを代入可能にする(訳注: =で終わるいわゆるセッターメソッドを定義する)と、読み込みの振る舞いをアクション間でもっとうまく共有できることにも気が付きました。たとえば何らかのアクセス制御を行うとしましょう。定義はおそらく次のようになります。

def users
  return @users if defined? @users
  @users = policy_scope User.all
end
helper_method :users

def user
  return @user if defined? @user
  @user = users.find params[:id]
end
helper_method :user

ここでは、データ読み込みメソッド(users)のひとつを用いて、他のメソッド(user)の実装を支援しています。before_actionコールバックによる方法とは異なり、ソースコードの順序はまったく影響しません。単にメソッドを呼んでいるだけなので、userを先に定義しても構わないのです。

ここまでは何もかもうまくいってる感がありますね。今度はindexアクションで検索もできるようにしたいとしたらどうでしょうか?indexアクションの定義は次のようになるでしょう。

def index
  @users = users.search params[:q]
end

ここでやっと例の@ハックに立ち戻りました。もし(インスタンス変数でない)usersデータ読み込みメソッドに検索結果を代入すると、showアクションでも現状のuserの実装に合わせて自前で検索を行うはめになります。データ読み込みの振る舞いの一部(policy_scopeなど)はアクション間で共有したいが、その他の振る舞い(検索)は共有したくない、というのは一般によくある問題です。

この問題も、代入によって解決できます。次のように、あるアクションでデータを絞り込めるよう、別のメソッドを定義してみてはどうでしょう。

private

def users= assigned_users
  @users = assigned_users
end

これで次のようにindexアクションを定義できます。

def index
  self.users = users.search params[:q]
end

先ほどメモ化を実装しておいたので、同じインスタンス変数が使われれば(繰り返しますが、このインスタンス変数は同一インスタンス内のメソッド呼び出し間でデータを共有するのに使われます)、indexビューでusersを呼び出すとpolicy_scopeや検索が適用されたリストを取得できます。showアクションでuserを呼び出せば、policy_scopeのみが適用され、検索は除外されます。

この代入メソッド(users=)はprivateにしてあります。理由は、このメソッドはそのアクション内(さもなければbefore_actionフック)でしか使われないからです。このメソッドをコンポーネントの外(ビューなど)で使う理由はまったく思い当たりません。

テストを書く

この提案におけるもうひとつの絶大なメリットは、テストの書きやすさです。これらのメソッドはいずれもpublicなので、テストでまったく普通に呼び出せます。たとえばindexアクションで検索が正しく行われているかどうかをテストしたい場合、従来の方法では、おそらく以下のような感じのテストを書くでしょう(RSpec構文とFactoryBotを利用)。

it 'searches for the given query' do
  create :user, last_name: 'Doe'
  create :user, last_name: 'Jackson'

  get '/users', params: { q: 'Doe' }

  expect( response.body ).to have_content 'Doe'
  expect( response.body ).not_to have_content 'Jackson'
end

上のテストコードには、出力されたテンプレートにキーワードが含まれているかどうかをチェックすることでコントローラが正しく振る舞っていると見なすという、暗黙の前提があります。しかし、そのページに何かのはずみでJacksonという単語が紛れ込んでしまえば、テストは「正しく機能していない」という理由で失敗するでしょう。しかしこの失敗は本当の失敗ではなく、false positive(偽陽性)です。

それでは、同じテストを先ほどのpublicメソッドで書き直してみましょう。

it 'searches for the given query' do
  expected = create :user, last_name: 'Doe'
  create :user, last_name: 'Jackson'

  get '/users', params: { q: 'Doe' }

  expect( controller.users ).to eq [expected]
end

こちらのテストはさらに頑丈になりました。欲しいレコードが見つかることと、それ以外のレコードが検索されないことを確認しています。false positiveをうっかり引き起こすような副作用の起きる余地はありません。

定形コードを減らす

皆さまがここまで辛抱強く読んでくださり、そして私の推す提案を気に入っていただければ幸いです。しかし、まだ「定形コードを何度も書くのがだるい」という問題が残されています。何やかやで、現在のデータ読み込みメソッドは以下のようになっています。

def user
  return @user if defined? @user
  @user = User.find params[:id]
end
helper_method :user

private

def user= assign_user
  @user = assign_user
end

えっへん!Rubyには、この手の共通パターンをシンプルに書くのにうってつけのメタプログラミングという強い味方があるのです。上のコードで提供したいものは、結局次の2つに集約されます。

  • データ変数の名前
  • そのデータ変数の読み込み方法

開発者が普段から慣れ親しんでいるRSpecとある程度対になるよう、この新しいメソッドにletと命名しました。データ読み込みにメタプログラミングを用いると、次のような感じで書けます。

let(:user) { User.find params[:id] }

簡潔でありながら、先ほどの定形コードによる方法のメリットを何ひとつ失っていません。Lettableモジュールでどんなメタプログラミングが使われているのか、じっくりご覧ください(Gist)。

module Lettable
  def let name, &blk
    iv = "@#{name}"

    define_method name do
      return instance_variable_get iv if instance_variable_defined? iv
      instance_variable_set iv, instance_eval(&blk)
    end
    helper_method name

    define_method :"#{name}=" do |value|
      instance_variable_set iv, value
    end
    private :"#{name}="
  end
end

上のコードをapp/controllers/concernディレクトリに置き、ApplicationControllerでこのコードをextendすれば完了です。コードが引き締まり、巨大なライブラリを持ち出す必要もなくなりました。letという名前がお気に召さないのであれば、好きに変えていただいて構いません。

コード例

上のコードを用いるとコントローラをどんなふうに書けるか見てみましょう(Gist)。

class WidgetsController < ApplicationController
  let(:widgets) { Widget.all }
  let(:widget) { widgets.find_or_initialize_by id: params[:id] }

  def new
    render :form
  end

  def edit
    render :form
  end

  def create
    save
  end

  def update
    save
  end

  def destroy
    widget.destroy
    redirect_to widgets_url, notice: 'ウィジェットは削除されました'
  end

  private

  def save
    if widget.update secure_params
      redirect_to widget, notice: 'ウィジェットは保存されました'
    else
      render :form
    end
  end

  def secure_params
    params.require(:widget).permit :name
  end
end

私はcreateupdateのどうでもいいような差分を消し去るのが大好きなので、テンプレート名をformとし、パーシャルレンダリング用のダミーテンプレートは一切用いませんでした。もちろん、このあたりの書き方は好みに応じて変えていただいても一向に構いません。

このletによる手法を既存のコントローラに導入したとしても、コントローラで他の部分を書き直す必要はありません。皆さんのお好きなようにコーディングしていただければ結構です。letは、単にアクション間やコントローラ-ビュー間でデータ読み込みを共有し、テストを楽にするためのものに過ぎません。

影響を受けたgem

私の提案は、decent_exposure gemのライブラリから影響を受けていることをここに認めるものであります。このライブラリのおかげで最初の着想に触れることができました(ダジャレを狙ったわけではありません2)。decent_exposureで好きになれなかったのは「暗黙の読み込み」の部分でした。これをやると隠蔽が甚だしくなり、カスタマイズが難しくなるからです。

decent_exposureを用いることも検討しましたが、暗黙の読み込みはどうしても使いたくありませんでした。巨大なライブラリを導入しなくても、ひとかけらのメタプログラミングさえあれば十分やれることに気付いたときに、それをconcernに置くのがよいという決定を下したのです。

ツイートより

関連記事

Railsコードを改善する7つの素敵なGem(翻訳)

3年以上かけて培ったRails開発のコツ集大成(翻訳)


  1. 訳注: 本記事のコード例はあくまで説明のためのものです。週刊Railsウォッチ20181015『Rails初心者とバレる書き方』もご覧ください。 
  2. 訳注: 「exposed me to the idea」とdecent_exposureのシャレと思われます。なおdecent exposureは「個人情報の適度な露出」という流行語です。 

The post Rails: 提案「コントローラから`@`ハックを消し去ろう」(翻訳) first appeared on TechRacho.


RubyでHTTPサーバーをゼロから手作りする(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

RubyでHTTPサーバーをゼロから手作りする(翻訳)

何かを始めるときはとりあえず動かしてみることが大事ですが、プログラミングをレベルアップするには、それまで慣れ親しんできた抽象概念より数段下の裏舞台を知っておくことも肝心です。

Web開発ではHTTPのしくみを理解しておくことが重要です。そのためにはHTTPサーバーを自作するのが一番です。

そもそもHTTPとは

HTTPはTCP上で実装されたプレーンテキストのプロトコルなので、リクエストの内容を調べるのも簡単です(HTTP/2は実際にはプレーンテキストではなく、効率化のためバイナリになっています)。リクエストの構造を見る方法のひとつは、以下のようにcurlコマンド に -v(verbose)フラグを付けて実行することです。

curl http://example.com/something -H "x-some-header: value" -v

以下のようなリクエストが出力されます。

GET /something HTTP/1.1
Host: example.com
User-Agent: curl/7.64.1
Accept: */*
x-some-header: value

このときのレスポンスは以下のようになります。

HTTP/1.1 404 Not Found
Age: 442736
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Sat, 03 Jul 2021 15:02:03 GMT
Expires: Sat, 10 Jul 2021 15:02:03 GMT
...
Content-Length: 1256

<!doctype html>
<html>
<head>
...

実装の計画

実装で必要な手順を定義してみましょう。

  • 受信するTCPコネクションをローカルソケットでリッスンする
  • 受信したリクエストのデータ(テキスト)を読む
  • リクエストのテキストを解析して「HTTPメソッド」「パス」「クエリ」「ヘッダ」「本文」を抽出する
  • リクエストをアプリケーションに送信し、レスポンスを取得する
  • コネクション経由でリモートソケットにレスポンスを送信する
  • コネクションを閉じる

以上を踏まえて、プログラムの大まかな構成を考えてみましょう。

require 'socket'

class SingleThreadedServer
  PORT = ENV.fetch('PORT', 3000)
  HOST = ENV.fetch('HOST', '127.0.0.1').freeze
  # バッファに保存する受信コネクション数
  SOCKET_READ_BACKLOG = ENV.fetch('TCP_BACKLOG', 12).to_i

  attr_accessor :app

  # app: Rackアプリ
  def initialize(app)
    self.app = app
  end

  def start
    socket = listen_on_socket
    loop do # 新しいコネクションを継続的にリッスンする
      conn, _addr_info = socket.accept
      request = RequestParser.call(conn)
      status, headers, body = app.call(request)
      HttpResponder.call(conn, status, headers, body)
    rescue => e
      puts e.message
    ensure # コネクションを常にクローズする
      conn&.close
    end
  end
end

SingleThreadedServer.new(SomeRackApp.new).start

ソケットをリッスンする

listen_on_socketの「完全な」実装は以下のような感じになります。

def listen_on_socket
  Socket.new(:INET, :STREAM)
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
  socket.bind(Addrinfo.tcp(HOST, PORT))
  socket.listen(SOCKET_READ_BACKLOG)
end

ただし、これらについては定番の書き方が豊富に存在するので、以下のように書き換えられます。

def listen_on_socket
  socket = TCPServer.new(HOST, PORT)
  socket.listen(SOCKET_READ_BACKLOG)
end

リクエストを解析する

作業を始める前に、最終的な結果がどうあるべきかを定義しましょう。サーバーはRack互換にしたいと思います。以下は私が見つけた、Rackが環境にリクエストの一部として期待するパラメータの例です。

{“GATEWAY_INTERFACE”=>”CGI/1.1”, “PATH_INFO”=>”/”, “QUERY_STRING”=>””, “REMOTE_ADDR”=>”127.0.0.1”, “REMOTE_HOST”=>”localhost”, “REQUEST_METHOD”=>”GET”, “REQUEST_URI”=>”http://localhost:9292/”, “SCRIPT_NAME”=>””, “SERVER_NAME”=>”localhost”, “SERVER_PORT”=>”9292”, “SERVER_PROTOCOL”=>”HTTP/1.1”, “SERVER_SOFTWARE”=>”WEBrick/1.3.1 (Ruby/2.2.1/2015-02-26)”, “HTTP_HOST”=>”localhost:9292”, “HTTP_ACCEPT_LANGUAGE”=>”en-US,en;q=0.8,de;q=0.6”, “HTTP_CACHE_CONTROL”=>”max-age=0”, “HTTP_ACCEPT_ENCODING”=>”gzip”, “HTTP_ACCEPT”=>”text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8″, “HTTP_USER_AGENT”=>”Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36”, “rack.version”=>[1, 3], “rack.url_scheme”=>”http”, “HTTP_VERSION”=>”HTTP/1.1”, “REQUEST_PATH”=>”/”}

これらのパラメータをすべて返すつもりはありませんが、少なくとも重要なパラメータは返しましょう。

最初に必要なのはリクエスト行の解析(parse)です。この構造にはおそらく見覚えがあるでしょう。

MAX_URI_LENGTH = 2083 # HTTP標準に準拠

def read_request_line(conn)
  # 例: "POST /some-path?query HTTP/1.1"

  # 改行に達するまで読み取る、最大長はMAX_URI_LENGTHを指定
  request_line = conn.gets("\n", MAX_URI_LENGTH)

  method, full_path, _http_version = request_line.strip.split(' ', 3)

  path, query = full_path.split('?', 2)

  [method, full_path, path, query]
end

リクエスト行の次にはヘッダーが来ます。

ヘッダーがどのようなものかを思い出しましょう。以下のようにヘッダーごとに改行をはさみます。

Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Content-Length: 1256
MAX_HEADER_LENGTH = (112 * 1024) # WebrickやPumaなどのサーバーではこう定義する

def read_headers(conn)
    headers = {}
    loop do
        line = conn.gets("\n", MAX_HEADER_LENGTH)&.strip

        break if line.nil? || line.strip.empty?

        # ヘッダー名と値はコロンとスペースで区切られる
        key, value = line.split(/:\s/, 2)

        headers[key] = value
    end

    headers
end

これで以下を得られます。

{
    "Cache-Control" => "max-age=604800"
    "Content-Type" => "text/html; charset=UTF-8"
    "Content-Length" => "1256"
}

次は本文(body)の読み取りです。本文はすべてのリクエストにあるとは限らず、POSTとPUTにのみあることが期待されています。

def read_body(conn:, method:, headers:)
    return nil unless ['POST', 'PUT'].include?(method)

    remaining_size = headers['content-length'].to_i

    conn.read(remaining_size)
end

これでブロックがひととおり揃ったので、シンプルな実装が完成しました。

class RequestParser
  class << self
    def call(conn)
      method, full_path, path, query = read_request_line(conn)

      headers = read_headers(conn)

      body = read_body(conn: conn, method: method, headers: headers)

      # リモート接続に関する情報を読み取る
      peeraddr = conn.peeraddr
      remote_host = peeraddr[2]
      remote_address = peeraddr[3]

      # 利用するポート
      port = conn.addr[1]
      {
        'REQUEST_METHOD' => method,
        'PATH_INFO' => path,
        'QUERY_STRING' => query,
        # rack.inputはIOストリームである必要がある
        "rack.input" => body ? StringIO.new(body) : nil,
        "REMOTE_ADDR" => remote_address,
        "REMOTE_HOST" => remote_host,
        "REQUEST_URI" => make_request_uri(
          full_path: full_path,
          port: port,
          remote_host: remote_host
        )
      }.merge(rack_headers(headers))
    end

    # ... (上で実装したメソッド)

    def rack_headers(headers)
      # Rackは、全ヘッダーがHTTP_がプレフィックスされ
      # かつ大文字であることを期待する
      headers.transform_keys do |key|
        "HTTP_#{key.upcase}"
      end
    end

    def make_request_uri(full_path:, port:, remote_host:)
      request_uri = URI::parse(full_path)
      request_uri.scheme = 'http'
      request_uri.host = remote_host
      request_uri.port = port
      request_uri.to_s
    end
  end
end

レスポンスを送信する

Rackアプリの実装はひとまず後回しにして、先にレスポンスの送信を実装しましょう。

class HttpResponder
  STATUS_MESSAGES = {
    # ...
    200 => 'OK',
    # ...
    404 => 'Not Found',
    # ...
  }.freeze

  # status: int
  # headers: ハッシュ
  # body: 文字列の配列
  def self.call(conn, status, headers, body)
    # ステータス行
    status_text = STATUS_MESSAGES[status]
    conn.send("HTTP/1.1 #{status} #{status_text}\r\n", 0)

    # ヘッダー
    # 送信前に本文の長さを知る必要がある
    # それによってリモートクライアントが読み取りをいつ終えるかがわかる
    content_length = body.sum(&:length)
    conn.send("Content-Length: #{content_length}\r\n", 0)
    headers.each_pair do |name, value|
      conn.send("#{name}: #{value}\r\n", 0)
    end

    # コネクションを開きっぱなしにしたくないことを伝える
    conn.send("Connection: close\r\n", 0)

    # ヘッダーと本文の間を空行で区切る
    conn.send("\r\n", 0)

    # 本文
    body.each do |chunk|
      conn.send(chunk, 0)
    end
  end
end

これで以下のような例を送信できます。

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 53

<html>
<head></head>
<body>hello world</body>
</html>

Rackアプリ

Rackアプリはstatusheadersbodyを返す必要があります。ステータスは整数、本文は文字列(チャンク)の配列です。

以上を元に、リクエストパスに基づいてファイルシステムからファイルを読み込むアプリを作ってみましょう。

class FileServingApp
  # リクエストで受け取ったパスを元にファイルシステムからファイルを読み取る
  # 例: "/test.txt"
  def call(env)
    # セキュリティ的には非常によくないが、デモ用には十分
    path = Dir.getwd + env['PATH_INFO']
    if File.exist?(path)
      body = File.read(path)
      [200, { "Content-Type" => "text/html" }, [body]]
    else
      [404, { "Content-Type" => "text/html" }, ['']]
    end
  end
end

まとめ

かなりシンプルだと思いませんか?
それもそのはず、細かなエッジケースを丸ごとスキップしているからです。

もっと詳しく知りたい方は、ピュアRubyで実装されているWEBRickのコードをご覧になることをおすすめします。Rackについてはこちらの記事で詳しく説明されています。

今回書いたコードの完全版については、以下のGitHubリポジトリをどうぞ。

今後は、シングルスレッドサーバー、マルチスレッドサーバー、さらにはRuby 3のFibersとRactorなど、さまざまなリクエスト処理方法を試す予定です。パート2は以下をご覧ください。

関連記事

RailsのリクエストのライフサイクルとRackを理解する(翻訳)

The post RubyでHTTPサーバーをゼロから手作りする(翻訳) first appeared on TechRacho.

週刊Railsウォッチ: insert_allやupsert_allのタイムスタンプ自動更新、app/contextsにロジックを置くほか(20211025前編)

$
0
0

こんにちは、hachi8833です。供給そんなにヤバいのかしら。


つっつきボイス:「電子部品の他に鉄も値上がりしてると聞いてますね」「あ〜」「給湯器の値上がりが著しいとか」「新型MacBook、部品のあるうちに買っとくのがいいのかな…」「Appleはそれなりに部品の流通を確保していると思いますけど、どれかが滞ったら詰まったりして」「欲しいときに買うのが一番」

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

今回は以下の公式情報から見繕いました。

🔗 insert_allupsert_allでタイムスタンプを自動更新するオプションが追加

このプルリクは、insert_allまたはupsert_all(および関連するメソッド)でレコードが作成された場合にタイムスタンプのカラム(created_atcreated_onupdated_atupdated_on)を自動設定するオプションを提供する。現時点では、これらのカラムを確実に設定するクリーンな方法は、カラム自体にデフォルトを設定するか、さもなければ属性として明示的に渡したうえでさらに既存レコードのcreated_atを上書きしないようon_duplicateのSQLをカスタマイズするしかなかった。
同PRより


つっつきボイス:「insert_allupsert_allのタイムスタンプって自動更新なのでは?と思いましたけど、このプルリクが出たということは今までは自動更新じゃなかったんですね」「record_timestamps:オプションをtrueにすればinsert_allupsert_allでタイムスタンプが自動更新されるようになったようです↓」

# activerecord/lib/active_record/insert_all.rb#L10
-   def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil)
+   def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil, record_timestamps: nil)
      raise ArgumentError, "Empty list of attributes passed" if inserts.blank?

      @model, @connection, @inserts, @keys = model, model.connection, inserts, inserts.first.keys.map(&:to_s)
      @on_duplicate, @returning, @unique_by = on_duplicate, returning, unique_by
+     @record_timestamps = record_timestamps.nil? ? model.record_timestamps : record_timestamps

record_timestampsはデフォルトがnilか」「falseを明示的に設定すると今まで通りになるんですね」

# activerecord/test/cases/insert_all_test.rb#L410
+ def test_upsert_all_does_not_implicitly_set_timestamps_on_create_when_model_record_timestamps_is_true_but_overridden
+   with_record_timestamps(Ship, true) do
+     Ship.upsert_all [{ id: 101, name: "RSS Boaty McBoatface" }], record_timestamps: false
+
+     ship = Ship.find(101)
+     assert_nil ship.created_at
+     assert_nil ship.created_on
+     assert_nil ship.updated_at
+     assert_nil ship.updated_on
+   end
+ end

🔗 コントローラの_htmlサフィックスの挙動を修正


つっつきボイス:「これはi18n関連ですね」「以下のようにキーに_htmlというサフィックスを追加するとhtml_safe?がtrueになってそのままビューに出力される機能は前からありますね」「ウォッチでも何度か話題になりました(ウォッチ20180723)」「う、知らなかった」「以下のhelloはエスケープされるけど、hello_htmlhtml_safe?がtrue、つまりサニタイズ済みとして扱われる↓というものです」「なるほど〜」

# actionpack/test/abstract/translation_test.rb#L20
              translation: {
                index: {
                  foo: "bar",
+                 hello: "<a>Hello World</a>",
+                 hello_html: "<a>Hello World</a>",
+                 interpolated_html: "<a>Hello %{word}</a>",
+                 nested: { html: "<a>nested</a>" }
                },
                no_action: "no_action_tr",
              },

参考: 4.4 安全なHTML変換 — Rails 国際化 (i18n) API - Railsガイド

「そして今回のプルリクを見ると、今まではコントローラとビューで_htmlサフィックスの挙動が違っていたらしい」「え、コントローラでも使えるんですか?」

これは#27872をやり直したもの。

html_safeへの変換の一般的な動作を、Active Supportのprivateなモジュールに抽出するコミットを追加して、Action ViewとAction Packで異なっている挙動を合わせ忘れることのないようにした。

これにより、#39989で実現されたメモリ節約の一部が元に戻される(Action Viewの実装ではチェックが必要な可能性のあるキーごとにhtml_safeオプションをビルドするので)。Action Viewのループ内だけでオブジェクトのアロケーションをメモ化する方法が見つからなかったので、これについては妥協することにした。メモリを節約する方法についてアイデアがあれば求む。
同PRより

AbstractControllerが改修されているので↓、コントローラの実装はビューと別だったみたい: ActiveSupport::HtmlSafeTranslation.translateに切り出して共通化したことで修正したようですね」「なるほど」「この機能をコントローラで使ったことはなかったな〜」

# actionpack/lib/abstract_controller/translation.rb
# frozen_string_literal: true

+require "active_support/html_safe_translation"
+
module AbstractController
  module Translation
    mattr_accessor :raise_on_missing_translations, default: false

...

      i18n_raise = options.fetch(:raise, self.raise_on_missing_translations)
-     I18n.translate(key, **options, raise: i18n_raise)
+
+     ActiveSupport::HtmlSafeTranslation.translate(key, **options, raise: i18n_raise)
    end
    alias :t :translate

    # Delegates to <tt>I18n.localize</tt>. Also aliased as <tt>l</tt>.
    def localize(object, **options)
      I18n.localize(object, **options)
    end
    alias :l :localize
  end
end

🔗 ArelにFILTER句のサポートを追加


つっつきボイス:「PostgreSQLとSQLite3の場合にfilterメソッドが使えるようになった」「MySQLはサポートされてないのか残念」

機能リクエストの多かったrails/arel#518を再度オープンした(rails/arel#460にもある)。
以下のRubyコードを書くことで、

Model.all.pluck(
  Arel.star.count.as('records_total').to_sql,
  Arel.star.count.filter(Model.arel_table[:some_column].not_eq(nil)).as('records_filtered').to_sql,
)

以下のSQLが出力されるようになる。

SELECT
  COUNT(*) AS records_total
  COUNT(*) FILTER (WHERE some_column IS NOT NULL) AS records_filtered
FROM models

サポート対象はPostgreSQL 9.4以降(2014年12月、リリースノート)とSQLite 3.30以降(2019年10月、リリースノート
参考:

「FILTER構文って何でしょう?」「上のModern SQLサイトによると、以下のように集計関数に条件を指定できるとありますね: 集計関数をSELECT文で条件付けするよりも簡潔に書けそう」「お〜!」

# modern-sql.comより
SUM(<expression>) FILTER(WHERE <condition>)

「以下みたいにCASE WHENでもやれますけど↓、条件が増えてくるとどんどん行数が増えてしまう」「それは読みづらそう…」「filterメソッドはそういうときに便利でしょうね👍

# modern-sql.comより
SUM(CASE WHEN <condition> THEN <expression> END)

参考: Window関数のFILTER句を極める

🔗 ビューのplain textモードの箇条書きを改善


つっつきボイス:「to_plain_textの改善だそうです」「箇条書きがネストしたときの書式を改善したのね」「そもそもto_plain_textというメソッドがあったことを知らなかった」「plain textモードを使う人って少なそうですけど、いるんでしょうね」

# 同PRより
• Item 0
• Item 1
  • Item A
    1. Item i
    2. Item ii
  • Item B
    • Item i
• Item 2

「今実装を見てますけど、" " * (depth - 1)とか"\n#{text}"のあたりが何というか生々しいですね」「自分でゴリゴリ実装したときのような感じが出てる」

# actiontext/lib/action_text/plain_text_conversion.rb#L93
+     def indentation_for_li_node(node)
+       depth = list_node_depth_for_node(node)
+       if depth > 1
+         "  " * (depth - 1)
+       end
+     end
+
+     def list_node_depth_for_node(node)
+       node.ancestors.map(&:name).grep(/^[uo]l$/).count
+     end
+
+     def break_if_nested_list(node, text)
+       if list_node_depth_for_node(node) > 0
+         "\n#{text}"
+       else
+         text
+       end
+     end

🔗 CSRF防止戦略のカスタマイズをサポート

概要
このプルリクは、protect_from_forgeryでのカスタムCSRF防止戦略を渡すサポートをAPIドキュメントで公式に追加する。
その他
現在のRailsは、CSRF保護戦略のカスタマイズを偶然サポートしている。protection_method_classにはクラスまたはシンボルオブジェクトを渡せるし、.to_s.classify呼び出しも両方で同じように振る舞う。
そこで@rafaelfrancaに相談した結果、このメソッドの振る舞いを変更してcase/when文で既存のCSRF防止戦略を明示的に返すようにし、かつ早期リターンによってクラスを戦略として渡せるようにした。
同PRより


つっつきボイス:「protection_method_classでCSRF防止の挙動を変えられるようにしたんですね: csrf-tokenの生成ポリシーを修正したいことはあるかもしれないので」「どんなときに変更したいんでしょうか?」「CSRFトークンの生成をRailsサーバー側以外で行いたい時とかかなあ」「なるほど」「たぶん自分でカスタマイズすると相当複雑になると思いますが」

参考: 3 クロスサイトリクエストフォージェリ (CSRF) — Rails セキュリティガイド - Railsガイド

RailsのCSRF保護を詳しく調べてみた(翻訳)

🔗 has_secure_password利用時にpassword = nilしても値が残る問題を修正

# 更新情報より
user.password = 'something'
user.password = nil
# before:
user.password # => 'something'
# now:
user.password # => nil

つっつきボイス:「Active Modelのpassword=セッターでnilを代入してもpasswordリーダーで読み出すと消えていなかったのが修正されたのね」「これは修正しないといけないヤツ」「修正はinstance_variable_setを1行追加しただけなんですね↓」

# activemodel/lib/active_model/secure_password.rb#L95

        define_method("#{attribute}=") do |unencrypted_password|
          if unencrypted_password.nil?
+           instance_variable_set("@#{attribute}", nil)
            self.public_send("#{attribute}_digest=", nil)
          elsif !unencrypted_password.empty?
            instance_variable_set("@#{attribute}", unencrypted_password)
            cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
            self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
          end
        end

🔗Rails

🔗 Zeitwerkにアップグレードした話(Ruby Weeklyより)


つっつきボイス:「RailsアプリのオートローダーをZeitwerkにアップグレードしたときのノウハウ記事です」「ファイルやクラスのリネームも発生したのか」「今どきはたいていZeitwerkになっていると思いますけど、アプリが大きいと後からZeitwerkに乗り換えるのは大変でしょうね」

Rails: Zeitwerkオートロードの「1ファイルにクラスを複数置けない」問題を回避する

🔗 RailsにSorbetを導入

以下はつっつき後に見つけたツイートです。


つっつきボイス:「freee会計などを手掛けているfreeeがRubyの静的型チェッカーSorbetを導入した記事です」「記事でやっているようにビジネスロジックを中心に少しづつ型を追加していくのがよさそうですね」「SorbeとYARDを両方書くのは大変、たしかに」

「Sorbetを使っている会社のリストを見ると、開発元のStripe以外にShopifyなども使ってますね」「以前から型注釈を欲しいと思っていた会社が使っているんでしょうね」「SorbetはLanguage Server Protocolに対応しているのがありがたい」

参考: Official page for Language Server Protocol

先週土曜日のKaigi on Rails 2021のクロージングキーノートでも、RafaelさんがShopifyでSorbetを導入したことを話していましたね。

参考: Keynote by Rafael França - Kaigi on Rails 2021

🔗 Arel入門


つっつきボイス:「ArelはActive Record内部のクエリ生成APIですね」「Arel職人を目指す記事なのかな」「arel_tableとかを見ると昔のトラウマが😅

# 同記事より
Organization.where(
    Organization.arel_table[:id].in(
    Comment.where(
      Comment.arel_table[:user_id].eq(user.id)
    ).distinct.pluck(:organization_id)
  )
)

参考: ActiveRecordを支える技術 - Arelとは何者なのか? (全5回) その1 - TIM Labs

「Arelを使うとこんなふうに書ける↓」「gt(2)はgreater than 2なんですね」「自分は普通にwhereとプレースホルダ?で書きますけど、事情によってはArelで書くこともたまにあります」

# 同記事より
users[:id].in([1,2,3]).to_sql
=> "`users`.`id` IN (1, 2, 3)"

users[:id].gt(2).to_sql
=> "`users`.`id` > 2"

users[:id].eq(3).to_sql
=> "`users`.`id` = 3"

「Arelでjoinが絡んだりコンポジションしたりするうちにだんだん複雑になりがち」「そうそう」「Arelはありがたい存在だけど、毎回Arelで書く気にはなれないな〜」

# 同記事より
users.join(photos, Arel::Nodes::OuterJoin).on(users[:id].eq(photos[:user_id]))

「BPSだとkazzさんがよくArelを使ってたかも」「Railsで汎用的なモジュールを書こうとするとArelが必要になってくることがあるんですよ」「なるほど」

追記: 今週金曜日の銀座Rails#38で、@osyoさんが『AST を使って ActiveRecord の where の条件式をブロックで記述しよう』というタイトルでお話しされるそうです。

🔗 Railsのビジネスロジックを「contexts」で整理(Ruby Weeklyより)


つっつきボイス:「また新しめのパターン」「contextという言葉のメタ度が高くてどうとでも解釈できそうな感じ」

「contextは、Elixir言語で動くPhoenixフレームワーク↓が由来と記事に書かれていました」「Elixir、知らない世界」「Elixirは見た目がRubyに似てて、型が書けるそうです」

参考: Elixir (プログラミング言語) - Wikipedia

app/contexts/の下にファイルを作って、そこにビジネスロジックを置くスタイルなんですね」「見た感じでは、GoF本で言うところのFacade(ファサード)を普通にcontextsに置いている感じかな🤔

# app/contexts/accounts.rb
module Accounts
  def self.active_users
    User.all.active
  end

  def self.account_details(id)
    account = Account.find(id)
    # ...
  end
end

# app/contexts/accounting.rb
module Accounting
  def self.create_invoice
    # business logic magic
  end
end

参考: ギャング・オブ・フォー (情報工学) - Wikipedia — GoF

「ElixirのフォーラムにcontextsとFacadeのことが書かれている↓: まさにFacadeパターンですね」

参考: Contexts in Phoenix 1.3 and Facade Pattern - Phoenix Forum / Chat / Discussions - Elixir Programming Language Forum

「記事では、Service Objectを使いたくないのでcontextにしたそうです」「Service Objectはクラスがやたらと増える傾向があるので、Facadeパターンを使うのはわかる: 自分もその方が好みです」「たしかに」

参考: Facade パターン - Wikipedia

「そういえばService Objectに置くのはたいていFacadeか、もうひとつ何かのパターンのどっちかだと以前おっしゃってましたね」「もうひとつはCommandパターンですね: RailsでService ObjectというとこのCommandパターンを使ったものを指すことが多いようです」「なるほど」「Commandパターンだと基本的に1クラス=1機能になるけど、Facadeパターンはそこにこだわらない感じ」

参考: Command パターン - Wikipedia

Railsのパターンとアンチパターン4: コントローラ編(翻訳)

🔗 GitLabコメント欄にmermaid構文でグラフを書く


つっつきボイス:「GitLabのコメント欄でmermaidというグラフ生成構文を使ってグラフを生成できるそうです」「元記事を見るとGitLab 10.3と随分昔からあるようなので、最近これを発見したのかも」「PlantUMLも使えるのね」

参考: mermaid - Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.
参考: PlantUML: シンプルなテキストファイルで UML が書ける、オープンソースのツール

# docs.gitlab.comより: mermaidの例
graph TB

  SubGraph1 --> SubGraph1Flow
  subgraph "SubGraph 1 Flow"
  SubGraph1Flow(SubNode 1)
  SubGraph1Flow -- Choice1 --> DoChoice1
  SubGraph1Flow -- Choice2 --> DoChoice2
  end

  subgraph "Main Graph"
  Node1[Node 1] --> Node2[Node 2]
  Node2 --> SubGraph1[Jump to SubGraph1]
  SubGraph1 --> FinalThing[Final Thing]
end


docs.gitlab.comより

「コメント欄でちょっぴりグラフを書くのにいいのかも」「自分はDraw.ioを開いてスクショを貼る方が早いかな」「それもそうですね」

後で調べると、draw.ioドメインはセキュリティ上の理由でdiagrams.netドメインに移行していました。

参考: diagrams.net
参考: Blog - Open source diagramming is moving to diagrams.net, slowly


前編は以上です。

バックナンバー(2021年度第4四半期)

週刊Railsウォッチ: ruby/debugをChromeでリモートデバッグ、Rubyアプリの最適化ほか(20211019後編)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: insert_allやupsert_allのタイムスタンプ自動更新、app/contextsにロジックを置くほか(20211025前編) first appeared on TechRacho.

週刊Railsウォッチ: YJITがRuby 3.1向けにマージ、ripperのドキュメント化、crontabの罠ほか(20211026後編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Ruby

🔗 YJITリーダーによるYJIT紹介記事


つっつきボイス:「以下↓はYJITリーダーであるMaxime ChevalierさんによるYJIT紹介記事ですが、翻訳リクエストを受けてひとまず一次翻訳終えました: リクエストありがとうございます🙏」「お〜翻訳楽しみ」「Ruby 3.1でYJITがマージする流れになっていますね: YJITはデフォルトでオフのはずなので、使わないときは意識せずに済むはず」「お〜YJIT楽しみ」

「続編記事としてNoah GibbsさんによるYJITお試し方法の紹介記事も出ていました↓」

以下はYJITのベンチマークサイトです。

🔗 YJITがマージされた

「ちなみについさっき(注: つっつき時点の10/21夜)YJITがマージされたというツイート↓を見かけたんですが、YJITがでかくてコミット数が多かったせいかdev環境のSlackボットや通知周りがエラーになって、いったんcloseされていました」「ありゃ残念」「ドンマイ」「分解するのは大変そう」

つっつきの後、無事YJITがmasterにマージされました🎉

参考: ruby-trunk-changes 2021-10-21 - ruby trunk changes

その後、OpenBSDでYJITを無効にするコミットや、JITでMJITを有効にするオプションを追加するコミットも追加されていました(YJITとMJITは同時には利用できないそうです)。

🔗 safe_regexp: 正規表現をタイムアウト

grosser/safe_regexp - GitHub

# 同リポジトリより
# normal
/a/.match?('a') # -> true in 0.0001ms
SafeRegexp.execute(/a/, :match?, 'a') # -> true in 0.13568ms

# bomb
require "safe_regexp"
regex = /aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?/
value = "a" * 46
regex.match? value # false in ~60s
SafeRegexp.execute(regex, :match?, value) # -> SafeRegexp::RegexpTimeout

# methods without arguments
regex = "^(The '(?<first_group>.*)' parameter of the (?<second_group>.*))$"
SafeRegexp.execute(regex, :names) # -> ["first_group", "second_group"]

つっつきボイス:「以下のregular-expressions.infoというサイトで知りました↓: サイトの人がこのgemを試したかどうかまではわかりませんでしたが」「今日のBPS Webチーム内発表で触れていたgemですね」「SafeRegexpを使うことで、正規表現がデフォルトで1秒以上かかるとタイムアウトしてエラーをraiseするそうです」

参考: Preventing Regular Expression Denial of Service (ReDoS)

「今Ruby本家でも正規表現にタイムアウトを入れようかという話が出ていましたね」「はい、RubyKaigi Takeout 2021のMartin先生の発表↓で触れていた#17837などですね」「これは?」「最近流行りのcatastrophic backtracking攻撃に対してRubyが対抗策を議論しているissueのひとつです」

参考: Regular Expressions: Amazing and Dangerous by Martin J. Dürst - RubyKaigi Takeout 2021

「READMEではThreadTimeoutを使わずに作ったとありました」「同じくREADMEによると、正規表現用に別プロセスを立ち上げて、タイムアウトしたらkill -9で止めるということをやってるらしい: 正規表現エンジンの中にタイムアウトを仕込むのではなく、エンジンの外側でやるという戦略かな」「シェルで強制終了するような感じなんですね」

「そういえば手元でsafe_regexpをちょっと試してみたところ、ヤバいパターンを食べさせたらコンソールが固まって、Ctrl-Cでも戻らなくなっちゃいました😢」「kill -9で止めるしかないでしょうね: 別プロセスにしないとRubyのメインプロセスがCPUタイムを食い尽くしてしまうので、OSのことも考えて正規表現用のプロセスを分けたんでしょうね」「なるほど」

「重たい別プロセスをRubyで立ち上げるのは泥臭いですが、ひとつの方法でしょうね」「テスト環境で使うとか、特定の正規表現を手元で検証するときにはよさそう👍

はじめての正規表現とベストプラクティス10: 危険な「Catastrophic Backtracking」前編

🔗 ripperのドキュメント化が進行中


つっつきボイス:「RubyのパーサーライブラリであるRipperは以前からドキュメントがないと言われていましたが、この方が頑張ってコードを読んでドキュメントを書き進めているそうです」「お〜すごい!」「READMEの内部リンクがまだ切れてるのでファイルを直接開いてください」

参考: class Ripper (Ruby 3.0.0 リファレンスマニュアル)

「RuboCopの作者の@bbatsovさんも、ドキュメントがないなどもろもろの理由でRipperを使うのをやめたそうです↓」「ドキュメントがなくても使う人はいるでしょうけど、やっぱりドキュメント大事」「動きがだいたいわかってても、ドキュメントがないと不安になりますよね」「RuboCopも使えるものならRipper使いたかったでしょうね」「ドキュメント化大変だろうけど頑張って欲しいです🙏

RuboCop作者がRubyコードフォーマッタを比較してみた: 後編(翻訳)

🔗 Rubyのワンライナーcookbook


つっつきボイス:「Rubyのワンライナー向けオプションはたくさんある分、Perlの-pieオプションに比べると長めになってしまう傾向がありますけど、Rubyの-neオプションあたりなら使いますね」

# 同記事より
$ # sample stdin data
$ printf 'gate\napple\nwhat\nkite\n'
gate
apple
what
kite

$ # print all lines containing 'at'
$ # same as: grep 'at' and sed -n '/at/p' and awk '/at/'
$ printf 'gate\napple\nwhat\nkite\n' | ruby -ne 'print if /at/'
gate
what

$ # print all lines NOT containing 'e'
$ # same as: grep -v 'e' and sed -n '/e/!p' and awk '!/e/'
$ printf 'gate\napple\nwhat\nkite\n' | ruby -ne 'print if !/e/'
what

参考: Perlのワンライナーでテキストの一括置換 - console.lealog();

「Rubyは標準機能が強力だし普段からRuby書いてるので、ワンライナーにもいいですよね」「いいっす」「ただ自分がRubyであまりワンライナー書かないのは、カスタマイズできない作業環境でRubyが使えるとは限らないからというのもあるんですよ」「その意味ではPerl強いですよね」「Perlはたいていの環境で最初から使えますね」

「Pythonも入っているとは限らない」「あってもPython 2系か3系かという罠があったりしますし」「さすがに新しい環境でPython 2は減ったと思いますけど、古い環境だと油断できない」

参考: Python 2.7.x と 3.x の決定的な違いを例とともに | POSTD

「macOSだとRubyが入っているけど、バージョンが古いんですよね」「1.8とかだったらどうしよう」「1.8はもう別物😆

後でBig Sur備え付けのRubyバージョンを調べてみました。Ruby 2.6は2022年3月にEOL(end-of-line)を迎えますが、果たしてmacOSはちゃんとバージョン上げてくれるでしょうか?
参考: Ruby Maintenance Branches

$ /usr/bin/ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin20]

🔗CSS/HTML/フロントエンド/テスト/デザイン

🔗 セキュリティヘッダークイックリファレンス(StatusCode Weeklyより)


つっつきボイス:「HTTPのヘッダーのうち、セキュリティ関連のヘッダーのクイックリファレンスだそうです」「▼をクリックすると詳細が表示されるのね」

X-Content-Type-Optionsはたしかに使う」「X-Frame-Optionsもそういえばあった」

# 同サイトより
X-Content-Type-Options: nosniff
# 同サイトより
X-Frame-Options: DENY

「クロスオリジン関連のヘッダーにCross-Origin-Opener-Policy(COOP)やCross-Origin-Embedder-Policy(COEP)というのもあるんだ、へ〜細かい」「たしかに細かい」

# 同サイトより
Cross-Origin-Opener-Policy: same-origin-allow-popups
# 同サイトより
Cross-Origin-Embedder-Policy: require-corp

CORSはよく聞くヤツですね」「知ってるものが出てきた」「ちなみにCORSのヘッダーは、ない状態が最もセキュア」

# 同サイトより
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true

参考: Rails アプリケーションのセキュリティ対策(CORS/CSP/HSTS)

「こうしたセキュリティ関連ヘッダーは適切に設定すべきですね: 特にSPAは1つのページ内であらゆるデータを読み書きするようになるので、一箇所でも不備があると他の部分にも影響しがち」「たしかに」「セキュリティヘッダーのリファレンスとして便利そうなサイト👍」「ヘッダーが多すぎないのが嬉しいです😂

参考: シングルページアプリケーション - Wikipedia — SPA

🔗 Priority Hintsによるリソース読み込み最適化


つっつきボイス:「Priority HintsはWebページ内の要素の読み込みで優先度を指定できる新しめの仕様ですね」「以下の記事は2018年の時点でPriority Hintsをチェックしていました↓」

「お〜、Blink(Chromeなどで使われているレンダリングエンジン)ではこれらに優先順位を設定できるのね↓」「『Priority Hintsはヒントであってディレクティブ(指示)ではない』、なるほど」

参考: Blink (レンダリングエンジン) - Wikipedia


同記事より

「具体的には以下のようにタグでimportance="low"のように指定する↓と、ブラウザ側で優先順位を割り当てる」「"low""high""auto"の3つか」「画像のimgにも指定できるんですね」「その場合CSSで画像サイズをきちんと指定しておく方がいいかも」

<!-- 同記事より -->
<!-- include trial token in the <head> -->
<meta http-equiv="origin-trial" content="{Replace with origin trial token}">

<!-- Lower the priority of script file -->
<script src="script.js" importance="low"></script>

<!-- Alter the priority of images -->
<img src="Background.jpg" width="400" importance="low">
<img src="Sunset.jpg" width="400" importance="high">

<!-- Note that importance="auto" is the default based on the spec if not specified -->
<img src="Flower.jpg">

「優先順位の仕様↓は定められているけど、細部の解釈はブラウザ依存になる可能性があるかもしれませんね」「あ〜たしかに」


同記事より(一部)

importanceを全部Highにしたりするのはダメなのかな?」「無意味だと思いますよ😆 : 上の仕様の上下矢印などを見た感じでは、仮に全部Highにしたとしても優先順位は同じにならないでしょうね」「あ、たとえばHighに下向き矢印がある項目は、それより低くなる可能性があるということなんですね」

「Priority Hints指定なし(左)とあり(右)では以下のように変わるんですね↓」「わかりやすい〜」

「優先順位をここまで追い込んで使うことはすぐにはないかもしれないけど、importance="high"asyncを指定する↓のは効果高そうな感じなので、これなら書いてもいいかなと思いました」

<!-- 同記事より -->
<script src="async_but_important.js" async importance="high"></script>

🔗言語/ツール/OS/CPU

🔗 Crontab.guru


つっつきボイス:「なるほど、crontabのスケジューリングの設定を支援するサイトですか」「これ面白いですね」「たしかGitHubのドキュメントでこのCrontab.guruが参考としてリンクされてたのを見たことがありますよ」

参考: crontab - Wikipedia

後で見つけました。

crontab guru を使うと、クーロン構文の生成および実行時間の確認に役立ちます。 また、クーロン構文の生成を支援するため、crontab guru のサンプルリストもあります。
ワークフローをトリガーするイベント - GitHub Docsよりより

🔗 crontabの罠

「ところでcrontab形式って、実は標準というものがないんですよ」「え?」「そうなんです😢

「たとえば、Linuxだと曜日は日曜始まりで0-6が割り当てられていて、さらに7も利用可能なので、07が日曜日になる: これなら7で割った余りで曜日を出すみたいな処理がやりやすい」「なるほど!」

参考: crontab 曜日設定などメモ - Qiita

「でもAmazon EventBridge↓(旧Amazon CloudWatch Events)のCrontab形式はかなり特殊で、曜日が日曜始まりの1-7になってる」「え〜それヤバいじゃないですか!」「きっとハマる自信ある」「誰もが一度はハマります」

参考: ルールのスケジュール式 - Amazon CloudWatch Events

「さらに厄介なのは、EventBridgeではcrontabで指定できる時刻がUTCのみという点」「え〜!JSTとかで書けないんですか?」「ローカルタイムが指定できないので、時刻を指定するたびに変換してあげないといけません」「ややこしい…」

参考: 協定世界時(UTC) - Wikipedia

「ちなみにLinuxのcrontabだとTZ=タイムゾーン名でローカルタイムを指定できます↓(指定し忘れると悲惨ですが)」「そういえばそうですね」「Linuxのcrontabに慣れているほどAWSでハマりがち」

参考: cron - How do you set the timezone for crontab? - Ask Ubuntu

「ところが面白いことに、AWSのマネージメントコンソール経由だとEventBridgeでローカルタイムを指定できるんですよ(↓証拠写真)」「へ〜!」「これについてさんざん調べたんですが、APIレベルではできない😢」「残念…」

「そういったわけで、crontab周りはクラウドの設定をレビューするときの要チェックポイントです」


後編は以上です。

バックナンバー(2021年度第4四半期)

週刊Railsウォッチ: insert_allやupsert_allのタイムスタンプ自動更新、rails/contextsにロジックを置くほか(20211025前編)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

StatusCode Weekly

statuscode_weekly_banner

The post 週刊Railsウォッチ: YJITがRuby 3.1向けにマージ、ripperのドキュメント化、crontabの罠ほか(20211026後編) first appeared on TechRacho.

Rails 7: クエリ結果を任意の順序にできるActiveRecord::QueryMethods#in_order_of

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

週刊Railsウォッチ20210823 ActiveRecord::QueryMethods#in_order_ofを追加もどうぞ。

Rails 7: クエリ結果を任意の順序にできるActiveRecord::QueryMethods#in_order_of

RailsでActive Recordを利用していて、クエリの結果が特定の順序に並んでいることを期待する場合があります。

たとえば、ブックリーダー用のRailsアプリケーションがあり、読了した本や今呼んでいる本やこれから読みたい本をトラッキングできるとします。

このアプリケーションをシンプルに構築するには、UserBookを作成します。このモデルにはbook_iduser_idstatusという3つのカラムがあります。statusカラムはreadcurrently_readingto_readのいずれかの値を取れるようになっています。

変更前

ユーザーの本をto_readcurrently_readingreadの順で表示するには、以下のような実装が考えられます。

user = User.first

# Arelを利用
result = user.user_books.
           order(
             Arel.sql(
               %q(
                  case status
                  when 'to_read' then 1
                  when 'currently_reading' then 2
                  when 'read' then 3
                  end
               )
             )
           )

# クエリをかけてからレコードを並べ替える
result = user.user_books.where(status: %w[to_read currently_reading read])

# 以下のアプローチか
# 「オランダ国旗問題」のソリューションを使える
# https://en.wikipedia.org/wiki/Dutch_national_flag_problem

ordered_result = result.collect{ |user_book| user_book.status == "to_read" } +
                 result.collect{ |user_book| user_book.status == "currently_reading" } +
                 result.collect{ |user_book| user_book.status == "read" }

指定の順序で最終的な結果をマッピングするには、ArelでSQL文を書くか、クエリレコードを反復しなければなりません。

変更後

Rails 7のActiveRecord::QueryMethodsに、上述の問題を解決するin_order_ofメソッドが追加されました(#42061)。

この新しい変更を用いれば、上述の実装は以下のようになります。

user = User.first

result = user.user_books.in_order_of(:status, %w[to_read currently_reading read])

#=> #<ActiveRecord::Relation [#<UserBook id: 3, user_id: 1, status: "to_read">, #<UserBook id: 4, user_id: 1, status: "to_read">, #<UserBook id: 5, user_id: 1, status: "currently_reading">, #<UserBook id: 6, user_id: 1, status: "read">]>

UserBook.in_order_ofは以下のクエリを生成します。

SELECT "user_books".* FROM "user_books" /* loading for inspect */ ORDER BY CASE "user_books"."status" WHEN 'to_read' THEN 1 WHEN 'currently_reading' THEN 2 WHEN 'read' THEN 3 ELSE 4 END ASC LIMIT ?  [["LIMIT", 11]]

ただしMySQLの場合、CASEの代わりにFIELD関数が用いられます。

SELECT "user_books".* FROM "user_books" /* loading for inspect */ ORDER BY FIELD("user_books"."status", 'to_read', 'currently_reading', 'read') ASC

Rails 7には既に、ActiveRecordのin_order_ofと同様に振る舞うEnumerable#in_order_ofも追加されています(関連記事)。

Enumerable#in_order_ofはEnumeratorに対して動作しますが、ActiveRecordのin_order_ofActiveRecord::Relationオブジェクトに対して動作する点が異なります。

関連記事

The post Rails 7: クエリ結果を任意の順序にできるActiveRecord::QueryMethods#in_order_of first appeared on TechRacho.

週刊Railsウォッチ: Rails 7アセットパイプライン解説記事、ロジックをapp/operatorsで整理ほか(20211101前編)

$
0
0

こんにちは、hachi8833です。直近ですが、明日11/2(火)19:30より「大江戸Ruby会議09 出前Edition」がオンライン開催されます。Rails界隈で知られた「あの人」や「あの人」も登壇するそうです。皆さんもぜひ!


また、Kaigi on Rails 2021の全動画がYouTubeチャンネル↓で公開されました 🎉

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

以前の公式更新情報で拾いきれなかったものから見繕いました。

🔗 Action MailテンプレートにDOM idを追加

自分のチームでは、CapybaraとSeleniumの代わりにCypressですべてのエンドツーエンドテストをやっている。その中で、すべてのメイラーでMailer Previewのアクションがエラーなしに開いてレンダリングできることをテストしている。メイラーやメイラーのアクションがたくさんあるのでバグの早期発見に役立っている。
自分たちが直面した問題は、メールのtofromsubjectなどにある重要なdd要素のほとんどに一意のセレクタがなくDOMで追いかけづらいというもの。このプルリクはこの問題を修正する。
私たちのCypressによるテストは以下のような感じになっている。

context('Mailer Preview', () => {
  it('works for all mailer actions', () => {
    cy.visit('/rails/mailers')

    cy.get('li a').each($a => {
      const href = $a.attr('href');
      cy.log(href)
      cy.visit(href)

      cy.get('#from').then($dd => {
        cy.log('FROM :' + $dd.get(0).innerText)
        //expect($dd.get(0).innerText).to.eq('from@example.com')
      })

      cy.get('#to').then($dd => {
        cy.log('TO: ' + $dd.get(0).innerText)
        //expect($dd.get(0).innerText).to.eq('foo@bar.com')
      })

      cy.get('#subject').then($dd => {
        cy.log('SUBJECT: ' + $dd.get(0).innerText)
        //expect($dd.get(0).innerText).to.eq('Test subject')
      })

      cy.get('#mime_type').then($dd => {
        cy.log('MIME TYPE: ' + $dd.get(0).innerText)
        //expect($dd.get(0).innerText).to.eq('HTML email')
      })

      cy.get('[download="icon.png"]').should('exist')
    })
  })
})

同PRより


つっつきボイス:「Action MailテンプレートのDOM idを増やしたそうです」「そうそう、idがあるとシステムテストを書いているときにとても取りやすいんですよ」「たしかに」「順序で指定したりすると後で仕様が変わったときにハマりやすいので、これはありがたい修正👍」「自分もテストのためにこんな感じでフィールドにidを振ったりしたことがあります↓」

<!-- railties/lib/rails/templates/rails/mailers/email.html.erb#56 -->
<dl>
    <% if @email.respond_to?(:smtp_envelope_from) && Array(@email.from) != Array(@email.smtp_envelope_from) %>
      <dt>SMTP-From:</dt>
-     <dd><%= @email.smtp_envelope_from %></dd>
+     <dd id="smtp_from"><%= @email.smtp_envelope_from %></dd>
    <% end %>
    <% if @email.respond_to?(:smtp_envelope_to) && @email.to != @email.smtp_envelope_to %>
      <dt>SMTP-To:</dt>
-     <dd><%= @email.smtp_envelope_to %></dd>
+     <dd id="smtp_to"><%= @email.smtp_envelope_to %></dd>
    <% end %>

    <dt>From:</dt>
-   <dd><%= @email.header['from'] %></dd>
+   <dd id="from"><%= @email.header['from'] %></dd>

    <% if @email.reply_to %>
      <dt>Reply-To:</dt>
-     <dd><%= @email.header['reply-to'] %></dd>
+     <dd id="reply_to"><%= @email.header['reply-to'] %></dd>
    <% end %>

    <dt>To:</dt>
-   <dd><%= @email.header['to'] %></dd>
+   <dd id="to"><%= @email.header['to'] %></dd>

    <% if @email.cc %>
      <dt>CC:</dt>
-     <dd><%= @email.header['cc'] %></dd>
+     <dd id="cc"><%= @email.header['cc'] %></dd>
    <% end %>

    <dt>Date:</dt>
-   <dd><%= Time.current.rfc2822 %></dd>
+   <dd id="date"><%= Time.current.rfc2822 %></dd>

    <dt>Subject:</dt>
-   <dd><strong><%= @email.subject %></strong></dd>
+   <dd><strong id="subject"><%= @email.subject %></strong></dd>

    <% unless @email.attachments.nil? || @email.attachments.empty? %>
      <dt>Attachments:</dt>

🔗 BASIC認証に無効な認証文字列が渡されたときの挙動を修正

BASIC認証で保護されているコントローラに、コロンが抜け落ちている誤った認証情報でリクエストを送信すると、以下のようにNoMethodError: undefined method 'bytesize' for nil:NilClassエラーが発生する。

class UsersController < ApplicationController
   http_basic_authenticate_with name: "king", password: "secret"

   def index
     render plain: "Something"
   end
end
credentials=$(echo -n king secret | base64)
curl 'http://localhost:3000/users' -H "Authorization: Basic $credentials"

同PRより


つっつきボイス:「BASIC認証の認証情報が不備だった場合のエラーがこれまでNoMethodErrorだったのを、has_basic_credentials?がfalseを返す形に修正したようです」「たしかにNoMethodErrorだと違いますよね」「今まではNoMethodErrorがhas_basic_credentials?を突き抜けてしまっていたのか」

# actionpack/test/controller/http_basic_authentication_test.rb#115
  test "has_basic_credentials? should fail with credentials without colon" do
    @request.env["HTTP_AUTHORIZATION"] = "Basic #{::Base64.encode64("David Goliath")}"
    assert_not ActionController::HttpAuthentication::Basic.has_basic_credentials?(@request)
  end

🔗 inflectorに登録した略語を削除できるようにした

ActiveSupport::Inflectorの略語(acronym)を削除しようとすると実装が壊れ、別の略語を登録しようとするとTypeErrorが発生する。

require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "activesupport", require: "active_support/all"
end

ActiveSupport::Inflector.inflections do |inflect|
  inflect.clear :acronyms
  inflect.acronym "HTML" # => '[]=': no implicit conversion of String into Integer (TypeError)
end

これは#clearinstance_variable_set "@#{scope}", []のように@acronymsに新しいArrayを設定していたのが原因。デフォルトの初期値はHashになっている↓。

# activesupport/lib/active_support/inflector/inflections.rb#L78-L79
def initialize
  @plurals, @singulars, @uncountables, @humans, @acronyms = [], [], Uncountables.new, [], {} 

このプルリクでは、ActiveSupport::Inflector::Inflectionsの#clear#clear(:all)`を拡張して、従来できなかった略語の削除をできるようにもしておいた。
同PRより


つっつきボイス:「Inflectorは単数・複数形単語の登録や独自の固有名詞・略語の登録を行える機能ですね↓: #clearなどで削除できない問題と、その後で略語を再登録できない問題が修正された」「日本語だとあまり使わない機能だと思いますが、たしかに登録できるなら削除もできて欲しいですよね」

参考: Rails API ActiveSupport::Inflector::Inflections

# api.rubyonrails.orgより
acronym 'RESTful'
underscore 'RESTful'           # => 'restful'
underscore 'RESTfulController' # => 'restful_controller'
titleize 'RESTfulController'   # => 'RESTful Controller'
camelize 'restful'             # => 'RESTful'
camelize 'restful_controller'  # => 'RESTfulController'

acronym 'McDonald'
underscore 'McDonald' # => 'mcdonald'
camelize 'mcdonald'   # => 'McDonald'

🔗 ジェネレータのCSSプロセッサリストにBootstrapとBulmaを追加

# railties/lib/rails/generators/rails/app/app_generator.rb#L265
-     class_option :css, type: :string, desc: "Choose CSS processor [options: tailwind, postcss, sass]"
+     class_option :css, type: :string, desc: "Choose CSS processor [options: tailwind, bootstrap, bulma, postcss, sass... check https://github.com/rails/cssbundling-rails]"

つっつきボイス:「ci skipとあるのはだいたいドキュメント関連の改修」「そういえばCSSフレームワークのBulmaは、以前#43110でsass-railsへのデフォルト依存が削除されたときに見かけました(ウォッチ20210921)」「Bulmaは使ったことないな〜」

参考: Bulma: Free, open source, and modern CSS framework based on Flexbox

🔗 Railsガイドのスタイル改修


つっつきボイス:「Railsガイドのスタイルにいくつか細かな修正が入っていました」「地道な修正大事ですね」

「ガイドの目次ドロップダウンを開いたらEscキーで閉じられるようにする、なるほど」「edgeガイドに反映されていました」

// guides/assets/javascripts/guides.js
    document.addEventListener("keyup", function(e) {
      if (e.key === "Escape" && guides.classList.contains("visible")) {
        guides.classList.remove("visible");
      }
    });

#43250の修正はどこだろう?」「モバイル表示の左右マージンが調整されて、フッターのmargin-bottomがわずかに小さくなっていた↓」「diffを見る方が早いかも」


同PRより(編集部で横並びに変更)

#42989はダークモードのdiffが見づらかったのを修正」「お〜なるほど」「個人的にダークモードってどうも不要な文明感がありますけど😆」「同じく」



同PRより

🔗Rails

🔗 Rails 7.0のアセットパイプライン周り解説記事


つっつきボイス:「これはいい記事でしたね👍」「歴史と現状と見通しのまとめが凄いですね」「ふわっとさせずに詳細を解説しきっていて、Simpackerにも言及しているのがさすが」

hokaccha/simpacker - GitHub

参考: Simpacker: Rails と webpack をもっとシンプルにインテグレーションしたいのです - クックパッド開発者ブログ

「この間話題にしたPropshaft(ウォッチ20211018)も、記事によるとRails 7で主要な選択肢のひとつになりそうで、Propshaftは思っていたより進んでいるんですね」「Rails 7は今はAlpha2ですが、次のAlpha3あたりでPropshaftが入るかもしれませんね」「お〜」

後で調べると、Propshaftはv7.0.0.alpha2ブランチではまだジェネレータのapp_base.rbには取り込まれていませんでしたが、masterブランチapp_base.rbには取り込まれていました。

Rails 7: import-map-rails gem README(翻訳)

🔗 ビジネスロジックをapp/operatorsで整理(Ruby Weeklyより)


つっつきボイス:「記事ではTrailbrazer↓を使ったりしてみたけど自分に合わなかったのでapp/operatorsで整理したらしい」「Trailbrazerにも名前の似たOperationsという概念があるんですね」

trailblazer/trailblazer - GitHub

「app/operatorsは前回取り上げたapp/contextsに似ている感じでしょうか?(ウォッチ20211025)」「記事冒頭でも前回のcontext記事↓を引用しているので意識はしているでしょうね」

参考: Organizing business logic in Rails with contexts

「前回のcontextsもそうでしたけど、この記事のコードに出てくる何とかOperatorもまさにGoF本で言うFacade(ファサード)ですね↓」「なるほど、名前が違う感じですか」

# 元記事より
# app/operators/invoice_operator.rb

class InvoiceOperator < BaseOperator
  def update(params:)
    @record.assign_attributes(params)

    # do things before updating the invoice eg:
    # update/create related records (like InvoiceItems)
    # a few more examples:
    calculate_total 
    calculate_vat

    state = @record.save

    # do your other business eg.:
    # send emails,
    # call external services and so on

    Result.new(state: state, record: @record)
  end

  def create(params:)
    @record.assign_attributes(params)

    # do things before creating the invoice eg:
    # create related records (like InvoiceItems)
    # a few more examples:
    calculate_total
    calculate_vat

    state = @record.save

    # do your other business eg.:
    # send emails,
    # call external services and so on

    Result.new(state: state, record: @record)
  end

  private

  def new_record
    Invoice.new
  end

  def calculate_total
    # you can write the logic here, 
    # or call a class that handles the calculation
  end

  def calculate_vat
    # you can write the logic here, 
    # or call a class that handles the calculation
  end
end

参考: Facade パターン - Wikipedia

「よりシンプルなパターンを作る試みは常にありますけど、実際に使ってみたときにシンプルになるとは限らないのが悩ましいところなんですよ」「もしかするとService Objectも出た当初はシンプルだと言われてたのかも」

「コントローラにActive Recordのメソッドチェーンがたくさんあるのは確かに気持ちよくないので、operatorsやcontextsでFacadeにするのもわかる: 個人的にはRailsでActive Recordが使えるのが嬉しい点だと思うので、そこまでしてActive Recordを直接触らせない形にしなくてもいいかなという気持ちが少しあります」「たしかに」「規模が大きくなればまた違ってくると思いますが」

「元記事末尾によると、このoperatorsはまだ長期のバトルテストは経てないそうです」「アプリが育ってきたときの設計は一概にどれがいいとは言えませんが、大きくする予定のないアプリなら別にoperatorsでやってもいいんじゃないかな」

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

🔗 RailsアプリのJSテストコードのカバレッジ(RubyFlowより)


つっつきボイス:「RailsプロジェクトにおけるJSテストのコードカバレッジの記事のようですね」「記事に出てくるIstanbulはJS製のカバレッジツールなのか↓」「simplecov(Ruby製のカバレッジツール)も使ってる」

istanbuljs/istanbuljs - GitHub

simplecov-ruby/simplecov - GitHub

「記事ではIstanbulで集計したJSテストカバレッジのダンプをRSpecからトリガーしているようですね↓」

# 同記事より
# spec/rails_helper.rb

RSpec.shared_context "dump JS coverage" do
  after { dump_js_coverage }
end

RSpec.configure do |config|
  config.include_context "dump JS coverage", type: :system

  ...

「JSのテストコードが多いRailsプロジェクトで使いそうなテクニックですね: もっともフロントエンドとバックエンドでリポジトリが分かれているプロジェクトだと少し工夫が必要そうですが」「あ、たしかに」

🔗 GitLab 14.4リリース

つっつきボイス:「14.4が出たのでBPS社内のGitLabもアップデートしておくかな」「DASTって何だろうと思ったらDynamic Application Security Testingなんですね」「静的なセキュリティスキャン機能ですね: 最近のGitLabはこういった機能をよく追加していますね」「なるほど」「よく見たらDASTはGitLabのUltimate版のみなのでFree版では使えないことが判明」「う、残念」

参考: GitLab Pricing | GitLab

「他の機能はというと、VSCodeからのGitLabリモートリポジトリ参照」「おぉ?」「ローカルにcloneしていないプロジェクトをVSCodeから読み取り専用で参照できるようですね: おそらく動画↓で動いているVSCode拡張用のインターフェイスをGitLab側に用意したということかな」「リモートリポジトリをちょっと見たいときにローカルにcloneしなくてもVSCodeで見られるのはよさそうですね😋

「GitLabは機能が着々と増えていますし、セキュリティパッチもちゃんと出し続けているので、出たらとりあえず当てることにしています」

🔗 その他Rails


つっつきボイス:「この間のruby/debug + Chrome Devtoolsの記事にアンサーが付いてたので拾いました」「そうそう、この方は明日10/29(注: つっつきは前日10/28夜でした)の銀座Rails#38に登壇されるosyoさんです」

以下は登壇後の資料ツイートです。

「そしてその後もrdbgについてTwitter上でosyoさんとやり取りしました↓」「あ、続きがあったんですね」「とりあえずの結論としてはbundle exec経由だとGemfileに追加しないとrdbgが動かないけど、binstub経由ならGemfileに追加しなくてもrdbgが動くということになりました」「お〜そうだったんですね」「Ruby界隈はTwitterでつぶやくと即誰かがコメントしてくれるから便利😊」「そうそう😊

【翻訳+解説】binstubをしっかり理解する: RubyGems、rbenv、bundlerの挙動


前編は以上です。

バックナンバー(2021年度第4四半期)

週刊Railsウォッチ: YJITがRuby 3.1向けにマージ、ripperのドキュメント化、crontabの罠ほか(20211026後編)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

The post 週刊Railsウォッチ: Rails 7アセットパイプライン解説記事、ロジックをapp/operatorsで整理ほか(20211101前編) first appeared on TechRacho.

YJIT: CRuby向けの新しいJITコンパイラを構築する(翻訳)

$
0
0

概要

Shopify Engineeringの許諾を得て翻訳・公開いたします。本記事は公開前にShopify Engineeringにレビューをいただいています。

画像は元記事のものです。

本記事はTwitterにて@shiroemonsshiromemonsさまからのリクエストを受けて翻訳いたしました。リクエストありがとうございます!

なお、以下のissue #18229で、著者のMaxime Chevalierさんをコミッターに迎えるプロポーザルが出され、その後コミッターとして登録されました。おめでとうございます🎉

また、週刊Railsウォッチ20211026後編で報じたように、YJITがRuby 3.1向けにmasterブランチにマージされました。おめでとうございます🎉

Shopify/yjit - GitHub

YJIT: CRuby向けの新しいJITコンパイラを構築する(翻訳)

JIT: Building a New JIT Compiler for CRuby

1980年代〜1990年代にかけて、Perl、Ruby、Python、PHP、Javascriptなどのインタプリタ型かつ動的型付けのプログラミング言語が産声を上げました。これらの言語はパフォーマンスより使いやすさや柔軟性を重視していました。こうしたプログラミング言語はさまざまな意味で、当時を取り巻く状況を反映していました。90年代はドットコムが隆盛を極めていた時代で、CPUクロックスピードはおよそ18か月ごとに倍増していました。この成長ぶりは永遠に続くように思われ、ソフトウェアを高速に動かすための努力は不要でした。コンピュータはひたすら速くなっていたので、問題は自然に解決されたのです。しかし今日の状況はやや異なっています。現代のシリコン製造技術は限界に達していて、もはやシングルコアのパフォーマンス強化だけではパフォーマンス問題を解決できなくなっています。また、モバイルデバイスや環境への配慮という観点から、エネルギー効率の重要性も認識され始めています。

私は昨年のパンデミックの最中に、Ruby on Railsで動く巨大なサーバーインフラを運営するShopifyで職を得ました。大勢のソフトウェアエンジニアが所属するチームに参加して、CRubyインタプリタやCRubyのガベージコレクタの最適化から、Rubyの別実装であるTruffleRubyの実装まで、さまざまな方法でRubyコードのパフォーマンス向上に取り組みました。私はそのときから、ShopifyとGitHubのベテランエンジニアチームとともに、CRubyの新しい組み込みJIT(just-in-time)コンパイラであるYJITを手がけています。

スピードという機能は過小評価されているので、このプロジェクトはShopifyと世界中のRuby開発者にとって重要です。CRubyには、既にMJITと呼ばれるJITコンパイラがあり、3年前から開発が継続されています。MJITは小規模なベンチマークでは高速化を達成していますが、Ruby on Railsのように広く普及しているRubyアプリケーションを実際に高速化することについてはまだ成功していません。YJITは「データドリブン」アプローチを採用し、特にRailsやShopify Core(ShopifyのメインとなるRailsモノリス)のパフォーマンススポットにフォーカスしています。

YJITとは

YJITについてツイートするTobi Lütke

YJITは、CRuby内部にJITコンパイラを段階的に構築し、JITで実行されるコードを徐々に増やすことで、最終的に実行の大半でインタプリタを置き換えることを目指すプロジェクトです。このコンパイラは近日中にCRubyに正式に取り込まれる予定で、私が博士号取得中に開発を始めたJITコンパイラアーキテクチャである『Basic Block Versioning(BBV)』をベースにしています。YJITについては今年のMoreVMs 2021のワークショップで講演し(↓上の動画)、RubyKaigi Takeout 2021でも講演しました(↓下の動画)。私たちのアプローチについて詳しく知りたい方はぜひご覧ください。

現時点の結果

YJITプロジェクトは現時点で1周年を迎えようとしていますが、MoreVMsの発表から大幅に改善されたことに私たちは満足しています。 一連のベンチマーク結果では、CRubyインタプリタと比較してrailsbenchで20%、liquidテンプレートレンダリングで39%、Active Recordで37%のスピードアップを達成しています。また、YJITはウォームアップが非常に高速です。YJITはどのベンチマークでも1回のイテレーションでほぼパフォーマンスのピークに到達し、最初のイテレーションでもすべてのベンチマークでインタプリタと同等以上のパフォーマンスを達成しています。

A bar graph showing the performance differences between YJIT, MJIT, and No JIT.

インタプリタのパフォーマンスを1.0としたときのベンチマーク速度(イテレーション/秒)、高いほどよい

YJITをCRuby内部で構築するにはいくつかの制約があります。つまり、JITコンパイラをCで書かなければならず、高パフォーマンスのJITコンパイラ向けに作られていないCRubyコードベースの設計上の決定に対応する必要があります。しかしYJITは、既存のRubyコードやパッケージとの互換性をほぼ100%維持できるという重要なメリットがあります。約30,000件のテストで構成されているCRubyのテストスイートにパスし、300万行を超えるコードを含み500個以上のRuby gemに依存するコードベースのShopify Coreでも、GitHubバックエンドCIでも、すべてのテストにパスしました。しかも、Shopify社内にある一部のproductionサーバーにも実際に導入されています。

YJITのBBVアーキテクチャは、動的に型付けされたコードをコンパイルするうえでいくつかの重要なメリットがあると私たちは信じています。コード生成パイプライン全体をエンドツーエンドで制御することで、現在のGCCベースのMJITアーキテクチャではできないことが可能になります。特に、YJITは型情報を元にコードを素早く特殊化し、プログラム実行時の挙動に応じて実行時にコードにパッチを適用できます。コンパイル速度やウォームアップ時間についても優位を保っています。

次なるステップ

YJITチームは、このコンパイラをRuby 3.1にマージするようRubyのコア開発者たちから依頼を受けました。私たちの成果が正式にRubyに取り入れられることは、私にとっても同僚にとっても大変光栄です。数か月もすれば、すべてのRuby開発者がRubyバイナリにコマンドオプションを渡して実行するだけでYJITを試せるようになります。しかし私たちの旅はまだ終わっていません。YJITとCRubyをさらに高速化する計画が既に進行しています。

現時点では、railsbenchのCPUインストラクションのうちYJITで実行されるのは79%程度で、残りはRubyインタプリタで実行されています。つまり、現在の結果を改善する余地はまだ残されているということです。YJITが今後進む道筋は明確で、私たちはYJITが現在よりもずっと高いパフォーマンスを提供できると信じています。しかしYJITを構築するにはCRubyの実装を詳しく理解することが不可欠でした。そしてその結果、CRubyアーキテクチャの内部でさらに高いパフォーマンスを引き出すために改善可能と私たちが信じている、重要な要素をいくつか発見しました。発見された改善はYJITのみならずMJITにも有用で、中にはRubyインタプリタを高速化する改善もあります。そういったわけで、今後の改良作業の一部はYJITとは別に上流工程で行うことになるでしょう。

改良作業のいくつかについては、今後このブログ記事で紹介する可能性もありますが、ここでは私たちが取り組んでみたいと考えているCRubyの改善点を暫定的にリストアップします。

  • CRubyをオブジェクトシェイプベースのオブジェクトモデルに移行する
  • CRubyの型タグ付けスキームを変更して型チェックのコストを削減する
  • 定数のキャッシュ機構をより詳細な粒度で実装する
  • 呼び出しのコンベンションの高速化と軽量化
  • CのランタイムメソッドをRubyで書き換えて、JITコンパイラがインライン化できるようにする

Matz(まつもとゆきひろ氏)は最近のEuruko 2021で、Rubyは当面の間言語への新機能追加を控え目にすると述べています(動画↓)。言語を急激に変更すると、JIT実装を軌道に乗せて最新に保つのが難しくなる可能性があるので、これは賢明な判断だと思います。私たちの見解では、今後Rubyをより強固な言語とし、競争力の極めて高いパフォーマンスを提供するために内部の改修に集中するのは理にかなっていると思います。

皆さんが私たちと同様にYJITとRubyの未来に熱い期待を寄せていただければ幸いです。YJITを試してみたい方は、CRubyと同じオープンソースライセンスのもとでGitHubリポジトリから入手できます。バグが見つかったらissueをオープンして簡単な再現方法を添えていただけると助かります。YJITの試し方や、speed.yjit.org向けに構築したパフォーマンストラッキングシステムなどについて詳しくは、間もなくYJITの追加記事を2件公開しますので、どうぞご期待ください。

訳注

以下は元記事公開後にShopfy Engineeringブログで公開された、YJITの試し方の解説記事です。

著者について

Maxime Chevalier-Boisvert: 2016年にモントリオール大学でコンパイラ設計の博士号を取得し、動的型付けプログラミング言語に最適化したJITコンパイラアーキテクチャであるBasic Block Versioning(BBV)を開発。現在はShopifyでCRuby内部に構築される新しいJITコンパイラであるYJITプロジェクトを率いています。

お知らせ

あなたが世界のどこにいても、次の旅はここから始まります。現実世界の問題を解決するシステムをゼロから構築することに興味をお持ちでしたら、私たちがこれまで扱ってきたさまざまな課題を弊社のエンジニアリングブログでお読みいただけます。エンジニアリングキャリアページでは、現在弊社で募集中の職種やDigital by Defaultについて紹介しています。

関連記事

Rubyオブジェクトの未来をつくる「シェイプ」とは(翻訳)

The post YJIT: CRuby向けの新しいJITコンパイラを構築する(翻訳) first appeared on TechRacho.

週刊Railsウォッチ: 2021年度Rubyアソシエーション開発助成、Rails REST APIレベルで楽観的ロックほか(20211102後編)

$
0
0

こんにちは、hachi8833です。大江戸Ruby会議09 出前Edition、いよいよ今夜ですね。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Ruby

🔗 2021年度Rubyアソシエーション開発助成の結果発表

2021年度Rubyアソシエーション開発助成では、「picoruby-compiler: An alternative mruby-compiler(Monstarlab)」「MRIのWebAssembly対応よるポータブルなRubyプログラムの実現
(齋藤優太)」「“debug.gem”の利用体験・開発効率の改善(小野直人)」「Ruby formatter(Kevin Newton)」が採択されました。おめでとうございます 🎉


つっつきボイス:「今回採択された中に、MRIをスタンドアロンなWebAssembly(Wasm)バイナリにコンパイル可能にするプロジェクトがありました」「おぉ、以前もどなたかがデモしていたのを見たことがありましたが、そのままだと使いにくかったみたいですね: MRIをWasmワンバイナリで配布できるようになったらスゴい」「mrubyはサイズが小さいので以前からWasmで動かせますけどね↓」

mrubyをWebAssemblyで動かす(翻訳)

参考: Rubyアソシエーション: Ruby処理系の概要

MRI (Matz’ Ruby Implementation)
C言語で実装されたRubyの公式処理系。すべてのプラットフォームで動作可能。Rubyアソシエーションの理事長である まつもとゆきひろ により開発され始めたもので、最も広く使われている。
http://www.ruby-lang.org/
ruby.or.jpより

🔗 Rubyのメソッド呼び出しでレシーバの有無を判定


つっつきボイス:「この間のKaigi on Rails 2021冒頭のkamipoさんQ&A企画で、kamipoさんが『Active Recordのscopingを修正するために、”レシーバを付けて呼び出したか付けずに呼び出したかを判定する機能”がRubyに欲しいと思っていた』とお話ししていた↓のを自分も見て、その後で上のアンサー記事が出ました」

参考: Rails API scopingActiveRecord::Relation

「お〜、privateメソッドの仕組みをうまく使うことでwhereのレシーバありとなしを判定するとはテクニカル」

# 同記事より抜粋
class User
  def self.where
    "レシーバなし"
  end

  def self.where_with_receiver
    "レシーバあり"
  end

  class <<self
    private :where

    # private メソッドでレシーバありで呼び出された場合に method_missing が呼ばれる
    def method_missing(name, *args)
      if name === :where
        where_with_receiver(*args)
      else
        super
      end
    end
  end
end

参考: Module#private (Ruby 3.0.0 リファレンスマニュアル)

🔗 Rubyのハックよもやま

「こういう高度な技を実際に使うとなると、他の人が見たときにコードの意図がわからなくなる可能性はあるかも」「あ〜、それもそうか」「ActiveSupport::Concernでラップしてそれを使うなどすればよいかもしれませんが」

参考: Rails API ActiveSupport::Concern

「一時期流行ったCSSハックなどもそうですけど、ハック系の技ほどコンテキスト依存が強くなる、つまりハイコンテキストになってしまいがち」「ふむふむ」「Rubyの嬉しい点は『こう書いたら動くかな?』と思って書いたら本当に動くことだと自分は思っているので、あまりハイコンテキストな方向に進まない方が個人的には嬉しいかも」「たしかにこういう状況ではこう書かないと動かないみたいなのはRubyっぽさが下がりそうですね」

参考: CSSハックについて | GRAYCODE HTML&CSS

「kamipoさんの話を聞いたときは、Rubyでもできないことがあるのかと思っちゃいました」「安全その他の理由で言語には何らかの制約があるものなので、できないことはあるでしょうね」

🔗 ブロック内にreturnを書くとどうなるか(Ruby Weeklyより)


つっつきボイス:「ブロック内でreturnを書いたときの話ですね: 思わぬ挙動が起きるヤツ」「ここでハマる記事をよく見かけますね」

# 同記事より
def some_method(&block)
  block.call
  completed = true # won't be called if the block returns early
ensure
  if completed
    puts "ok"
  else
    puts "returned early"
  end
end

some_method { return } # => "returned early"
some_method { next } # => "ok"

参考: Ruby のブロック内で return すると… - Secret Garden(Instrumental)

「ところがブロック内のreturnって、とても書きたくなるときがあるんですよ」「おぉ?」「たとえばRSpecのexpectブロックに書く条件が複雑になってくるとif文が入ってきたりするんですが、Rubyのブロックが返すのは最後に評価された値なので、それを変えたくてreturnを書いてしまうと、そのブロックが終了するのではなく、そのブロックがあるメソッドそのものが終了する」「そういえばブロックの中に他にも制御文を書けたりしますね」「書けてしまうが故にうっかり書いてしまいがち」

参考: 制御構造 (Ruby 3.0.0 リファレンスマニュアル)

「個人的にはブロックにreturnを書いたらwarningを出してくれてもいいのにという気持ちになることもあります」「わかります」

🔗 その他Ruby


つっつきボイス:「チェリー本第2版の発売日が決まったんですね」「急遽ruby/debugの解説を加えたそうです」「発売後ひと月しないうちにRuby 3.1が出るのか…」「Ruby 3.1はツールチェインは増えるけど、書き方はあまり変わらないから影響は小さいでしょうね」

ruby/debug - GitHub

以下はつっつき後に見つけたツイートです。後ひと月で販売ですね。

🔗Rails

🔗 RailsのREST APIの楽観的ロック(Ruby Weeklyより)


つっつきボイス:「よくあるデータベースの楽観的/悲観的ロックの話かなと思ったら、よく見るとRailsのREST APIレベルでの楽観的/悲観的ロックの話のようですね」「あ、RESTレベルのロックでしたか😅: DBカテゴリに置いてましたがRailsカテゴリに変えておきます」

参考: 楽観ロック/悲観ロック(optimistic locking/pessimistic locking)とは - IT用語辞典 e-Words

「記事によると、ActiveRecord::Locking::Optimisticというまさに楽観的ロックのための機能があるらしい」「こんな機能もあったんですね」「そういえばlock_versionカラムがActive Reordで使えるのを思い出した: これを使って行ロックをかけられる」「お〜、lock_versionカラムを追加すればActive Recordの機能だけで使えるんですね」

参考: Rails API ActiveRecord::Locking::Optimistic

Active Recordは、lock_versionフィールドが存在する場合に楽観的ロックをサポートする。レコードが更新されるたびにlock_versionカラムがインクリメントされる。2回インスタンス化されたレコードは、最後に保存された方が更新されるとStaleObjectErrorをraiseする。
api.rubyonrails.orgより

# api.rubyonrails.orgより
p1 = Person.find(1)
p2 = Person.find(1)

p1.first_name = "Michael"
p1.save

p2.first_name = "should fail"
p2.save # Raises an ActiveRecord::StaleObjectError

「元記事では、最終的にHTTPのETagヘッダーをlock_versionと連携させて楽観的ロックを実現しているんですね↓: ensure_if_match_header_providedメソッドでIf-Matchヘッダーの存在を確認して、なければHTTP 428を返す、へ〜!」「HTTP 428は、Precondition Required(前提条件が必要)なんですね」「元記事の1つ前のコードは、If-Matchヘッダーがなくても呼べてしまってHTTP 500(内部エラー)になるので、こう変えたのか」

# 同記事より: 最終的なコード
class RentalsController
  before_action :ensure_if_match_header_provided, only: [:update]
  after_action :assign_etag, only: [:show, :update]

  rescue_from ActiveRecord::StaleObjectError do
    head 412
  end

  def show
    @rental = Rental.find(params[:id])
    respond_with @rental
  end

  def update
    @rental = Rental.find(params[:id])
    @rental.update(rental_params)
    respond_with @rental
  end

  private

  def ensure_if_match_header_provided
     request.headers["If-Match"].present? or head 428 and return
  end

  def assign_etag
    response.headers["ETag"] = @rental.lock_version
  end

  def rental_params
    params
      .require(:rental)
      .permit(:some, :permitted, :attributes)
      .merge(lock_version: lock_version_from_if_match_header)
  end

  def lock_version_from_if_match_header
    request.headers["If-Match"].to_i
  end
end

参考: ETag - HTTP | MDN

参考: 428 Precondition Required - HTTP | MDN

参考: 500 Internal Server Error - HTTP | MDN

「REST APIレベルで楽観的ロックをかけるこの書き方はたしかに正当なRESTful、面白い!」

参考: RESTful API(REST API)とは - IT用語辞典 e-Words

🔗 ETagとRESTful

「まだETagがわかっていないんですが、ETagは使わないこともできるんでしょうか?」「ETagはHTTPキャッシュ機構のひとつで、サーバーがETagを付けてレスポンスを返したらクライアントもETagを付けることになっていた気がしますが、ETagはもともとキャッシュのためのものだから、クライアントもETagに対応する義務はあるんだろうか?うん、この記事のような方法でETagを使うことは果たして正当なのかどうかを考えるのは議論として面白いですね」

参考: HTTP キャッシュ - HTTP | MDN

「MDNを見ると、If-None-Matchをヘッダーに含めることが”できます”と書かれてる↓」「英語版も同じ部分がcanになってますね」「つまりクライアントはETagを無視することも可能ということか」

ETag レスポンスヘッダーは strong validator として使用できる、ユーザーエージェントにとって不透明な値です。ブラウザーなどの HTTP ユーザーエージェントは、この文字列が何を表すかがわからず、またこの値が何になるかを予測することもできません。ETag ヘッダーがリソースのレスポンスの一部に含まれていたら、クライアントは以降のリクエストでキャッシュ済みリソースの確認を行うために If-None-Match をヘッダーに含めることができます
developer.mozilla.orgより

「今のMDNの説明はIf-None-Matchについてのものだから、元記事で使っているIf-Matchがどうなのかも見てみるか」

参考: If-Match - HTTP | MDN

「元記事の記述を見ると、この実装はクライアント側がETagのIf-Matchを実装していることを求めていることになりますね↓」「ふむふむ」「もちろんこれがRESTfulであることは変わりませんが」

If-Matchヘッダーを使えば、APIサーバーはリソース変更の有無を非常に手軽に確認できます。チェックサムやバージョン番号など、その他ETagとして選んだどれでも比較すればよいのです。
同記事より

「RESTfulはなるべくHTTPの機能でやろうという考え方なので、この記事のやり方はRESTfulの解釈のひとつとしてありだと思います👍」「こういうのはRails Wayでもあるんでしょうか?」「RESTfulですが、Rails Wayとは言いにくいかな」「なるほど」「クライアント側での処理を増やしてはいますが、RESTfulという視点ではキレイに決まっているので、Rails WayよりはRESTfulという思想に忠実な実装だと思います」「思想ですかなるほど」

🔗クラウド/コンテナ/インフラ/Serverless

🔗 Cloudflareの機能


つっつきボイス:「CloudflareのArgo Tunnelクライアントを使うとngrokみたいなことができる」「ngrokって、ローカルで起動したサーバーをネット上でいきなり公開できる便利ツールですね↓」「クライアント名はcloudflaredで、基本的にはCloudflare側にトンネリングするツールなのか」

cloudflare/cloudflared - GitHub

参考: ngrokが便利すぎる - Qiita

「Cloudflare Pagesは初めて知った」「Cloudflareには他にもツールチェイン的な機能がいろいろ増えている感じですね」

以下はつっつき後に見つけたツイートです。


後編は以上です。

バックナンバー(2021年度第4四半期)

週刊Railsウォッチ: Rails 7アセットパイプライン解説記事、ロジックをapp/operatorsで整理ほか(20211101前編)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Ruby Weekly

The post 週刊Railsウォッチ: 2021年度Rubyアソシエーション開発助成、Rails REST APIレベルで楽観的ロックほか(20211102後編) first appeared on TechRacho.


Railsアプリで実際にあった5つのセキュリティ問題と修正方法(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

週刊Railsウォッチ20181015『Railsアプリで実際に起きたセキュリティ問題5つ』もどうぞ。


  • 2018/10/03: 初版公開
  • 2021/10/28: 更新

Railsアプリで実際にあった5つのセキュリティ問題と修正方法(翻訳)

私は長年に渡って、Ruby on Railsアプリでさまざまなセキュリティ問題を発見・修正する機会が何度もありました。この経験を元に、皆さんのRailsアプリをさらにセキュアにする方法をご紹介いたします。ここでご紹介するセキュリティ問題が皆さまのアプリで発生していなければ幸いです。

原注

Rails組み込みのセキュリティ機能について詳しくは、公式のRailsセキュリティガイドをご覧ください。

1. 「セッションに期限を設定していない」問題

セキュリティ問題の解説

「Railsセキュリティガイド」には以下のように記載されています。

セッションを無期限にすると、攻撃される機会を増やしてしまいます (クロスサイトリクエストフォージェリ (CSRF)、セッションハイジャック、セッション固定など)。

ユーザーエクスペリエンスを考慮すれば(ユーザーはずっとサインインしたままになるので、アプリを開くたびにサインインしなくても済む)、無期限セッションの方が正しいアプローチと思われそうですが、無期限セッションは悪手です。公共の場所に置かれたPCでユーザーがアプリからサインアウトし忘れた場合に、何者かにユーザーセッションをハイジャックされる状況を防止するためには、セッションをできる限り早期に無効にするべきです。

解決方法

最も単純な解決方法は、config/initializers/session_store.rbイニシャライザで以下のようにセッションcookieの有効期限を設定することです。

Rails.application.config.session_store  :cookie_store, expire_after: 12.hours

これによって、セッションcookieは作成後12時間で期限が切れます。この方法は実装が容易なのですが、実は大きな欠点があります。有効期限はユーザーのブラウザに設定されるので、セッションcookieにアクセスできさえすれば誰でもcookieを書き換えて期限を簡単に延長できてしまいます

よりセキュアな方法でこの問題を解決するには、セッションの有効期限をサーバー側に保存すべきです。この方法は「Railsセキュリティガイド」でも提案されています。

セッションIDを持つcookieのタイムスタンプに有効期限を設定するという対応策も考えられなくはありません。しかし、ブラウザ内に保存されているcookieをユーザーが編集できてしまう点は変わらないので、やはりサーバー側でセッションを期限切れにする方が安全です。

ユーザー認証にDevise gemを使っているRailsアプリの場合、Timeoutableモジュールが組み込まれます。このモジュールは、ユーザーセッションが期限切れかどうかの検証を行います。これを利用するには、アプリでユーザーを表すモデルの中でこのモジュールを次のように有効にする必要があります。

class User < ActiveRecord::Base
  devise :timeoutable
end

続いてdeviseイニシャライザのtimeout_inオプションに必要な値(デフォルトは30分)を設定します。

# ==> Configuration for :timeoutable
# The time you want to timeout the user session without activity. 
# After this time the user will be asked for credentials again. 
# Default is 30 minutes.
config.timeout_in = 30.minutes

Devise gemを使っていない場合は、Sessionモデルを作成してcreated_atタイムスタンプとupdated_atタイムスタンプをそこに保存し、古くなったレコードはそこから削除するようにしておきます。繰り返しになりますが、このコード例は「Railsセキュリティガイド — セッションの期限切れ」にあります。

2018/10/09原文更新: InCaseOfEmergency氏からのredditのコメントで、組み込みのソリューションがRails 5.2で改善され、期限切れタイムスタンプがセッションcookieの一部に含まれるようになったとの情報をいただきました。素晴らしい!

2. 「アカウントロックの仕組みがない」問題

セキュリティ問題の解説

1人のユーザーが何回サインインに失敗したらユーザーを無効にしますか?失敗回数を「無制限」にすると、セキュリティホールが生じます。ユーザーがメールアドレスとパスワードをさまざまに変えてサインインしようとすることを許すと、攻撃者にそれを許したも同然です。辞書攻撃や総当たり攻撃を準備するスクリプトを使って、ものの数分で突破されてしまうでしょう。

  • 総当たり攻撃: ユーザー名/パスワードのあらゆる組み合わせを試す攻撃
  • 辞書攻撃: ありがちなパスワードのリストを元に推測する攻撃

この問題を修正するには、間違ったユーザー名/パスワードの組み合わせを入力できる上限回数を設定し、それを超えたらユーザーをブロックすべきです。

解決方法

Devise gemを使っていれば先ほどと同様に話は簡単です。Deviseには、サインイン試行回数を超えたユーザーをブロックできるLockableモジュールがあります。回数は自由に設定できますが、まずは5回に設定しておくのがよいでしょう。ユーザーから不満の声が上がったときに、いつでも値を変更できます。

Lockableモジュールは以下の2つのアンロック戦略を提供します。

  1. :time: 指定の時間が経過したらユーザーのブロックを自動解除する。
  2. :email: ユーザーがロックされた場合にロック解除のリンクをメールで通知する。

この2つの戦略はロック対策を何も行わないよりずっとましですが、最終的にどちらを選ぶかはあなた次第です。Deviseでは両方を同時に使うこともできます。詳しくはDeviseの公式ドキュメントをどうぞ。

Devise gemを使っていない場合は、自力で同じようなソリューションを実装できます(コードはネット上にいろいろあります🙂)。今使っているライブラリに同じようなソリューションがあるかどうかチェックするとよいでしょう。

CAPTCHAを実装することで総当たり攻撃や辞書攻撃の防止に役立てることもできます

出典: https://hakiri.io/blog/rails-login-securityより

3. 「ユーザーリストを取られる」「メールアドレスを推測される」問題

セキュリティ問題の解説

さほど問題には見えないかもしれませんが、深刻な問題です。試しに自分のアプリでめったに使われない「パスワードをリセットする」ページを開いてみてください。

入力したメールアドレスのユーザーは存在しません」といううかつなバリデーションエラーメッセージが表示されなければ幸いです。このメッセージがよくない理由はおわかりでしょうか?攻撃者がこれを使って、そのシステムに存在するメールアドレスのリストを集めることができてしまう可能性があるからです。

既存のメールアドレスのリストを数百万件のリクエストとしてアプリに送信し、その結果を元にリストのどのユーザーが本当にそのシステムに存在するかをチェックするスクリプトは実に簡単に作れます。もしロックアウトのしくみがなければ、実在ユーザーのメールアドレスリストを手に入れた攻撃者が上述のセキュリティホールを衝いてユーザーアカウントへのアクセスを奪取するかもしれません。

解決方法

ユーザーが入力するメールアドレスがアプリのユーザーに割り当てられた有効なものであっても、ランダムなメールアドレスであっても、アプリは同じレスポンスを返すべきです(APIからのJSONレスポンスか、確認メッセージを表示するページへのリダイレクトで)。こうすることで、攻撃者はアプリのユーザーのメールアドレスのリストを取れなくなります。

Devise gemを使っている場合は、コードのコメントにあるparanoidオプションが使えます。

入力したメールアドレスが正しいかどうかにかかわらず、確認やパスワード復元といったワークフローの振る舞いを同じにする。
Deviseコメントより抜粋

Deviseを使っていない場合は、入力したメールアドレスが正しいかどうかにかかわらず振る舞いが同じになるよう、自分でアプリを調整すべきです。

このようなレスポンスを返すと、non-existing-email@domain.come-mailというメールアドレスがどのユーザーにも使われていないことが攻撃者に知られてしまい、次のアドレスを試されてしまう可能性があります。

メールアドレスが漏洩せず、かつ明確なメッセージ(正確には上下2つのメッセージ)

Patron did not receive Password Reset Emailより

4.「権限昇格(権限のないリソースへのアクセス)」の問題

訳注

週刊Railsウォッチの「Rails初心者とバレる書き方」もご覧ください。

セキュリティ問題の解説

権限昇格は起きてはならないことですが、起きるときは起きます。あなたが仮に、ユーザーのプロジェクトをIDで参照できる、新しいAPIエンドポイントを作成したとしましょう。

GET https://my-rails-app.com/api/projects/:project_id

テストユーザーに割り当てたいくつかのプロジェクトIDを使うcUrlリクエストをいくつか試し、プロジェクトの詳細を含むJSONペイロードが首尾よく返されたので、ようやくこのエンドポイントをproductionにデプロイしました。しかしちょっと待った!別のユーザーに割り当てられたプロジェクトIDを使ってリクエストをこしらえたらどうなりますか?

残念でした。プロジェクトへのアクセスをcurrent_userに限定するのを忘れていたのです。

公式のRailsガイドには、このセキュリティ問題をまとめた秀逸な一文があります。

ユーザー入力は安全確認が終わるまではセキュアではなく、ユーザーから送信されるどのようなパラメータにも、何らかの操作が加えられている可能性が常にあります 。
Rails セキュリティガイド | Rails ガイドより「6.7 権限昇格」

解決方法

いかなる場合も、アクセスを可能な限り絞り込むことを忘れてはいけません。自分のアプリのコントローラでcurrent_userメソッドにアクセスできるのであれば、以下のように置き換えて修正します。

Project.find(params[:id])

上を以下のように置き換えます。

current_user.projects.find(params[:id])

複数のリソースへのアクセスをオブジェクト指向的に制御したい場合は、pundit gemかcancancan gemを利用できます。

私はpunditの方が使い慣れています。また、punditにはdevelopment環境で常に使用すべき便利な機能が1つあります。たとえば、他のコントローラに継承されるメインのコントローラに以下のフィルタを追加できます。

after_action :verify_authorized

こうしておけば、コントローラのアクションでauthorizeメソッド(リソースへのアクセスを基本的に制限する)を呼び忘れたときにpunditが知らせてくれるので、コードをリポジトリにプッシュする前でも対応できます。

punditもcancancanも使ったことのない方には、ぜひお試しいただくことをおすすめします。

5. 「弱いパスワードを排除していない」問題

セキュリティ問題の解説

アプリユーザーの大多数は1passwordKeePassも使っていません。これらはセキュアなパスワードを生成して安全に保存し、サインインのたびに自動入力してくれます。

覚えやすいパスワードをあらゆるアプリで使いまわしているのが平均的なユーザー像です。

私の個人的な意見ですが、たとえユーザーに少々不便を強いることがあっても、ユーザーを指導してセキュリティに配慮することが私たち開発者の義務だと思います

くれぐれも12345678qwertyなどという弱々しいパスワードの作成をユーザーに許してはいけません。このようなパスワードが使われていると、簡単に総当たり攻撃や辞書攻撃の餌食になってしまいます。

解決方法

パスワードポリシーを導入し、適用しましょう。

パスワードポリシーとは、強いパスワードの利用をユーザーに促して正しく運用することでコンピュータのセキュリティを高める目的で制定されたルールのセットのことである。
Password policy - Wikipediaより

基本的なポリシーは、Userモデルに以下のカスタムバリデーションメソッド追加するだけで適用できます。

validate :password_complexity
def password_complexity
  return if password.blank? || password =~ /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,70}$/
  errors.add :password, "パスワードの強度が不足しています。パスワードの長さは8〜70文字とし、大文字と小文字と数字と特殊文字をそれぞれ1文字以上含める必要があります。"
end

上のメソッドはDeviseのwikiから引用したものです。本記事を読んだ方は一刻も早くアプリのコードにこれを追加すべきです。

オーバーキル気味かもしれませんが🙂、もっと細かく制限したい方は、strong_password gemをチェックするとよいでしょう。

eBayやGmailやDropboxで使われているパスワード強度の表示です。ユーザーにポリシーをわかりやすく伝えることは、ポリシーそのものと同じぐらい重要です。

出典: https://css-tricks.com/password-strength-meter/

まとめ

Ruby on Railsアプリでこれまで発生した5つのセキュリティ問題をご紹介しました。これらは今後も発生する可能性があります。

本記事をお読みいただいている皆さまのアプリに、これらの問題が1つもないことを願っています。修正可能な問題が見つかった方が、本記事でご紹介したソリューションを活用してセキュリティホールを塞ぐことができれば幸いです。グッドラック🙂

リンク集

  1. The official OWASP Ruby on Rails security checklist
  2. Rails Security Checklist
  3. The SaaS CTO Security Checklist

関連記事

Ruby 2.5.2→2.5.3/2.4.5/2.3.8リリース(脆弱性修正)

RailsのCSRF保護を詳しく調べてみた(翻訳)

The post Railsアプリで実際にあった5つのセキュリティ問題と修正方法(翻訳) first appeared on TechRacho.

Ruby 3: FiberやRactorでHTTPサーバーを手作りする(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

FiberとRactorについては以下もどうぞ。

Ruby 3: FiberやRactorでHTTPサーバーを手作りする(翻訳)

はじめに

本記事はRubyでHTTPを学ぶシリーズ(part #1)の続編です。

RubyでHTTPサーバーをゼロから手作りする(翻訳)

動機

Rubyは歴史的にコンカレンシーを欠いていましたが、現在のRubyには「ネイティブ」スレッドがあります(Ruby 1.9より前は「グリーンスレッド」 のみでした)。ネイティブスレッドは、OS によって制御されるスレッドが複数存在することを意味しますが、一度に実行できるスレッドは 1 つだけで、GIL(Global Interpreter Lock)によって管理されます。ただし、ネイティブ呼び出しやI/O呼び出しはパラレルに実行できます。I/O呼び出しの間、スレッドは制御を手放し、I/Oが終了したというシグナルを待ちます。つまり、I/Oを多用するアプリケーションではマルチスレッドを使えるということです。

本記事では、HTTPサーバのさまざまなコンカレンシーモードについて解説します。

  • シングルスレッド
  • マルチスレッド
  • Fiber
  • Ractor

シングルスレッド

まずはシンプルに、シングルスレッドのHTTPサーバーがどのようなものかを見ていきましょう(完全なコード)。

def start
    socket = TCPServer.new(HOST, PORT)
    socket.listen(SOCKET_READ_BACKLOG)
    loop do
      conn, _addr_info = socket.accept
      request = RequestParser.call(conn)
      status, headers, body = app.call(request)
      HttpResponder.call(conn, status, headers, body)
    rescue => e
      puts e.message
    ensure
      conn&.close
    end
end

マルチスレッド

前述のように、RubyのスレッドはI/Oが多い場合にパフォーマンスが向上します。現実のアプリケーションのほとんどがそのようになっています。I/Oには「受信TCP接続の読み込み」「データベースの呼び出し」「外部APIの呼び出し」「TCP接続経由でのレスポンス」などがあります。

それでは、マルチスレッド版サーバーとスレッドプールをそれぞれ実装してみましょう(完全なコード)。

def start
  pool = ThreadPool.new(size: WORKERS_COUNT)
  socket = TCPServer.new(HOST, PORT)
  socket.listen(SOCKET_READ_BACKLOG)
  loop do
    conn, _addr_info = socket.accept
    # スレッドの1つがリクエストを実行する
    pool.perform do
      begin
        request = RequestParser.call(conn)
        status, headers, body = app.call(request)
        HttpResponder.call(conn, status, headers, body)
      rescue => e
        puts e.message
      ensure
        conn&.close
      end
    end
  end
ensure
  pool&.shutdown
end

スレッドプール(シンプルな実装)

class ThreadPool
  attr_accessor :queue, :running, :size

  def initialize(size:)
    self.size = size

    # 処理を管理するスレッドセーフなキュー
    self.queue = Queue.new

    size.times do
      Thread.new(self.queue) do |queue|
        # Rubyのcatch()はあまり知られていないが
        # 例外の伝搬と似た方法で
        # プログラムのフローを変える方法のひとつ
        catch(:exit) do
          loop do
            # popはキューに何かが入るまでブロックする
            task = queue.pop
            task.call
          end
        end
      end
    end
  end

  def perform(&block)
    self.queue << block
  end

  def shutdown
    size.times do
      # ここでスレッドを無限ループから脱出させる
      perform { throw :exit }
    end
  end
end

Webサーバーのマルチスレッドアーキテクチャについては、Pumaドキュメントの”アーキテクチャ”をご覧ください。私たちの単純な実装とは異なり、リクエストを読み込んでスレッドプールにコネクションを送信する追加のスレッドがあります。このパターンは後ほどRactorを扱うときに実装する予定です。

Fiber

Fiberはあまり知られていませんが、Ruby 3に追加された機能です(Fiber::SchedulerInterfaceを実装して使います)。Fiberは、goroutineのようなルーチンの一種と考えられます。Fiberはスレッドに似ていますが、Fiberを管理するのはOSではなくRubyです。Fiberのメリットは、コンテキストの切り替えが少ないことであり、OSによるスレッド切り替えよりもFiber切り替えの方がパフォーマンスのペナルティが小さくなります。

スレッドと異なるのは、Fiberを使う場合はスケジューラを実装する責任が生じる点です。ありがたいことに、以下のような既存のスケジューラgemがあります。

dsh0416/evt - GitHub

digital-fabric/libev_scheduler - GitHub

socketry/async - GitHub

それでは、Fiber版のHTTPサーバーがどうなるかを見てみましょう(完全なコード)。

def start
  # Fiberはスケジューラがないと動かない
  # スケジューラはカレントのスレッドでオンになる
  Fiber.set_scheduler(Libev::Scheduler.new)

  Fiber.schedule do
    server = TCPServer.new(HOST, PORT)
    server.listen(SOCKET_READ_BACKLOG)
    loop do
      conn, _addr_info = server.accept
      # 理想的にはFiberの個数をスレッドプールで制限する必要がある
      # 以下の理由から、リクエストを無制限に受け付けるのはよくない
      # ・メモリなどのリソースが不足する可能性がある
      # ・Fiberの個数に比べて戻りが減少する
      # ・リクエスト送信への背圧がないと適切なロードバランスや
      #   リクエストのキューイングが困難になる
      Fiber.schedule do
        request = RequestParser.call(conn)
        status, headers, body = app.call(request)
        HttpResponder.call(conn, status, headers, body)
      rescue => e
        puts e.message
      ensure
        conn&.close
      end
    end
  end
end

参考

Ractor

Ractorは、Ruby 3に追加された最も魅力的な機能です。Ractor はスレッドに似ていますが、複数のRactorをパラレルに実行「可能」で、Ractorごとに独自のGILを持つ点が異なります。

原注

残念ながら「おもちゃ」という言葉は暗喩ではなく、Ractorsが使えるようになるにはまだまだかかります。私の実験では、macOSでセグメンテーションフォールトが発生します。Linuxではセグメンテーションフォールトは発生しないものの、依然としてバグがあります。たとえばRactorの中で例外がrescueされていないとアプリケーションがクラッシュする可能性があります。さらに重要な点は、明示的にsharedとマーキングされていない限り、2つのRactorが同一のオブジェクトを扱えないということです。本記事執筆時点では、Ruby標準ライブラリのグローバルオブジェクトが多すぎるため、HTTPリクエストを送信できません。

それでは、Ractorを使うとどうなるかを見ていきましょう(完全なコード)。

def start
  # このキューは受信リクエストを
  # 公平にディスパッチするのに使われる
  # キューをワーカーに渡すと最初に空いたワーカーが
  # yieldされたリクエストを受け取る
  queue = Ractor.new do
    loop do
      conn = Ractor.receive
      Ractor.yield(conn, move: true)
    end
  end
  # ワーカーがコンカレンシーを決定する
  WORKERS_COUNT.times.map do
    # キューとサーバーを渡して
    # Ractor内部で利用可能にする必要がある
    Ractor.new(queue, self) do |queue, server|
      loop do
        # コネクションがyieldされるまでこのメソッドはブロックされる
        conn = queue.take
        request = RequestParser.call(conn)
        status, headers, body = server.app.call(request)
        HttpResponder.call(conn, status, headers, body)
        # エラーをrescueしないとRactorが死ぬだけでなく
        # `allocator undefined for Ractor::MovedObject`がランダムに発生して
        # プログラム全体がクラッシュすることがわかった
      rescue => e
        puts e.message
      ensure
        conn&.close
      end
    end
  end
  # リスナーは新しいコネクションを受け取ってキューに渡す
  # キュー内のyieldはブロッキング操作なのでこれを別のRactorに分けたが、
  # それまでのコネクションがすべて処理完了するまでは
  # 新しいコネクションを受け取れず、
  # リクエスト送信先のワーカーがビジーな可能性があるため
  # ワーカーへのコネクション送信にsendが使えない
  listener = Ractor.new(queue) do |queue|
    socket = TCPServer.new(HOST, PORT)
    socket.listen(SOCKET_READ_BACKLOG)
    loop do
      conn, _addr_info = socket.accept
      queue.send(conn, move: true)
    end
  end
  Ractor.select(listener)
end

参考

パフォーマンステスト

サーバーを実際に動かさないとこの記事は終わりません。

そこで以下のタスクについてサーバーごとにテストを行いました。

以下の表は1秒あたりのリクエスト数です(多いほどよい)。サーバーには4つのコンカレントなリクエストで負荷をかけました。

CPU負荷 ファイル読み込み HTTPリクエスト
シングルスレッド 112.95 10932.28 2.84
マルチスレッド 99.91 7207.42 10.92
Fiber 113.45 9922.96 10.89
Ractor 389.97 18391.25 1

結果について

CPU負荷が高いタスクについては、Ractorが他のサーバーを4倍近く(つまりコンカレンシーの個数)上回っていました。マルチスレッドのパフォーマンスは、スレッド切り替えのオーバーヘッドのためシングルスレッドよりも低く、Fiberはシングルスレッドとほぼ同等のパフォーマンスでした(オーバーヘッドが少ないということなので、よいことです)。
ひとつ非常に嬉しくなかった点は、以下のコードを実行するとRactorのパフォーマンスが悪化したことでした。原因については皆目見当がつきません。Rubyチームの仕事はまだまだ山積みです。

10.times do |i|
  1000.downto(1) do |j|
    Math.sqrt(j) * i / 0.2
  end
end

ファイル操作についても申し添えておかなければなりません。私が使っているNVMe SSDは極めて高速なので、待ち時間が非常に小さくなってCPU負荷タスクに近い結果が得られた可能性があります。

HTTPタスクで最も遅かったのは予想どおりシングルスレッドサーバーで、Fiberサーバーとマルチスレッドサーバーは4倍近く高速でした(繰り返しますが、コンカレンシーを4にするとさらに改善する可能性もあります)。

結論

一般的なRailsサーバーの動作を考えれば、単にマルチスレッド化するだけでも大きな成果が得られるでしょう。最も遅い部分はI/Oが足かせになることが多いためです。

CPU負荷の高い計算が必要な場合は、おそらくRactorsが答えになりそうですが、大幅な変更が必要なので実用化されるまでには何年もかかりそうです。

その意味で、実際に最も有用な追加機能はFiberかもしれません。スレッドより小さいオーバーヘッドで多数のWebリクエストを処理するなど、限られた範囲であればすぐにでもFiberを利用できます。ただしFiberには独自のスレッドローカル変数があるので引き続き注意が必要です。

関連記事

Ruby: Ractorによる安全な非同期通信の実験(翻訳)


  1. 原注: Ractorは上述の制約のせいで動かせず、標準ライブラリにはHTTPリクエスト送信機能がまだありません。サーバーからの読み込みをTCPで実装してみてもよかったかもしれませんが、ここでやめておきました。 

The post Ruby 3: FiberやRactorでHTTPサーバーを手作りする(翻訳) first appeared on TechRacho.

週刊Railsウォッチ:(20211108前編)RubyKaigiの過去講演動画が多数公開、Active ResourceとHerほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

今回は以下の公式更新情報から見繕いました。件数が多いので残りは次回に。

🔗 ActiveRecord::Base.prohibit_shard_swappingを追加

シャーディング化したデータベースをリクエストのライフサイクル全体で用いる場合、データベースのシャードが意図せず変更されないことが望ましいことがよくある。
このコミットはActiveRecord::Base.prohibit_shard_swappingを導入する。これはブロックを受け取って、ブロックが継続中にシャードのスワップが発生しないようにするというもの。
同PRより


つっつきボイス:「シャーディング(sharding: 水平分散)でスワップが発生しないようにするとは?」「データベースのシャーディングでは、複数あるシャードのどれにデータを置くかをハッシュ関数などで決定します」「ちょっとRAID 5みたいですね」

参考: DBのテーブル水平分割技術「シャーディング」とは | 若手エンジニアのブログ
参考: RAID - Wikipedia
参考: ハッシュ関数 - Wikipedia

「で、シャーディングのキーとなるカラムが更新されるとブロックの置き場所が変わるんですよ」「あ、そうか」「おそらくシャードのスワッピングはそのことを指しているんでしょうね: この改修は、prohibit_shard_swappingブロック内ではそういうシャードの移動が発生しないようにすることだと思います」「なるほど」

🔗 schema/structureダンプのファイルパスを指定可能に

従来のRailsでは、schema/structureダンプの生成がデータベースの設定名に基づいていた。discuss.rubyonrails.orgでの議論にもあるように、これはシャード間でdumpファイルを共有するうえで問題がある。
このプルリクによる機能は、スキーマダンプのファイル名を設定するカスタムソリューションを既に書いていて、それを使いたい場合に便利。スキーマキャッシュのパスを指定できるのだから、スキーマダンプでもそうしない手はないだろう。
そのために、古いschema_fileデータベースタスクを非推奨にして、代わりに新しい方法でdb_configを渡せるようにした。db_configからファイル名を決定するコードはここで重複することになるが、将来はこれがファイル名決定の正しい方法になる。これらはコンフィグに設定されるのでアプリのコンフィグでもアクセス可能なはず。
なおschema_dumpはアルファ版以外のどのリリースにも含まれていないので、schema_dumpの振る舞いを非推奨化の手続きに乗せる必要はなかった。ただしfalsenilの場合の振る舞いは同じ。
* #43173をクローズ
* #43240に取って代わる
共著: Ryan Kerr leboshi@gmail.com
同PRより


つっつきボイス:「なるほど、schema/structureのダンプ先を指定できるようになったんですね: これはいいことだと思う👍」「ありがたい🙏」「今まではできなかったとは」「スキーマダンプのコマンドはrails db:schema:dumpでしたね」

🔗 Rails 7でselenium-webdriver 4以上とwebdrivers 5をサポート

新しいRails 7.0のジェネレータが生成するGemfileでは、Ruby 3.0をサポートするselenium-webdriver 4.0.0以上(#43270)と、それを必要とする最新のwebdrivers 5.0.0が使われる。

https://rubygems.org/gems/webdrivers/versions/5.0.0
そろそろRails 7.0でselenium-webdriver 3.xのサポートを廃止するとき。
同PRより


つっつきボイス:「selenium-webdriverのバージョンがRails 7から上がる: 既存アプリによってはRails 7にアップグレードするときのシステムテストの更新がちょっと大変になるかも」「システムテストをみっちり書いているアプリほどつらそう…」「自分はそこまでシステムテストをびっちり書かないかな」

🔗 monotonic_timeをRubyネイティブの機能に置き換え


つっつきボイス:「これはリファクタリングか」「Ruby公式のProcess.clock_gettimeがあるならそれを使おうという流れのようですね」

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L530
          newly_checked_out = []
-         timeout_time      = Concurrent.monotonic_time + (@checkout_timeout * 2)
+         timeout_time      = Process.clock_gettime(Process::CLOCK_MONOTONIC) + (@checkout_timeout * 2)

          @available.with_a_bias_for(Thread.current) do
            loop do
              synchronize do
                return if collected_conns.size == @connections.size && @now_connecting == 0
-               remaining_timeout = timeout_time - Concurrent.monotonic_time
+               remaining_timeout = timeout_time - Process.clock_gettime(Process::CLOCK_MONOTONIC)
                remaining_timeout = 0 if remaining_timeout < 0
                conn = checkout_for_exclusive_access(remaining_timeout)
                collected_conns   << conn
                newly_checked_out << conn
              end
            end

参考: Process::CLOCK_MONOTONIC (Ruby 3.0.0 リファレンスマニュアル)

「従来のmonotonic_timeはconcurrent-rubyの機能なのかな?」「ググって出てきた過去記事↓を見ると、Rails 6でconcurrent-rubyがActive Supportに導入されているので(#32034)そのようですね: インターフェイスが少し変わるのでバックポートまではしなさそうですが」

RailsアプリでConcurrent Rubyを使う(翻訳)

ruby-concurrency/concurrent-ruby - GitHub

🔗 CI環境でデフォルトのeager loadingをサポート

このパターンはShopifyで長年使われている。
テストをローカル実行するときはほとんどの場合テストの一部しか実行しないので、最初のテスト結果が出るまでの時間を短縮するには読み出すコードができるだけ少ない方がよい。
CIの場合はいずれにしろすべてのテストコードを読み込むのだし、テストスイートが複数のランナーに分散する場合でも副作用のあるコードファイルを確実に読み込むために、アプリケーションをeager loadingする方が望ましい。
これにより、オートロードされた定数がCI上で適切にテストされていない場合であっても、少なくともデプロイ時より前にCIで読み込まれてSyntaxErrorなどのエラーが明示的に出るようになる。
同PRより


つっつきボイス:「config.eager_load = ENV["CI"].present?がコンフィグにデフォルトで追加されるのね」「CIという環境変数は使ったことなかったな〜」

🔗 Classic->Zeitwerkガイド


つっつきボイス:「お〜、Zeitwerkへの移行ガイドがedgeに上がった」「本家英語版ガイドではまだですが、日本語版Railsガイド向けに日本語訳をWIPで準備中です↓」「Railsガイドってこういうふうに翻訳してるのか〜」「なるほど、英語版のmarkdownファイルと別にjpディレクトリに日本語版markdownファイルを置いているんですね」「そうしないと自分がバージョンを追えなくなってしまいそうなので😅

参考: [WIP]Zeitwerkアップグレードガイド by hachi8833 · Pull Request #1093 · yasslab/railsguides.jp

🔗Rails

🔗 RubyKaigiの過去動画が多数公開


つっつきボイス:「先週の大江戸Ruby会議09出前Editionで、RubyKaigiの過去の講演動画アーカイブが多数お披露目されましたね(イベント後に公開されました)」「2006〜2008年でこれだけの数の動画スゴい」「貴重な動画を掘り起こしてくれてありがたいです🙏」「紹介されていた動画がどれもすんごくよかった」「全部見る時間あるかな…」「YouTubeなら英語字幕も付くので気楽に見てみるといいと思います」「今見るとさらに増えているみたい」「あ、ホントだ」

🔗 DHHによるRails解説「One controller, many ins, many outs」

「圧巻は、冒頭でみんなで視聴した若き日のDHHによるRails解説↓」「このときのDHHは20代だったのが信じがたい」「CRUDやRESTとの関連など、今見てもまったく色あせない内容なのがホントすごい」

🔗 Matzを説得する方法

「このHow to persuade matzという動画もとてもおもしろかった↓」

🔗 The Island of Ruby

「Dave Thomas氏がRubyへの愛を熱烈に語った動画もよかった↓」「Rubyは今後こういうことをしなければならないという話をこの時代にしていたのが改めてすごいと思いました」「大江戸Ruby会議09は、他にも自分の知らない時代のRubyの話が山盛り」「そうそう、知らない話が続々出てきました」

参考: #8 達人プログラマー Dave Thomas(前編) RubyにはMatzの受けた教育,宗教とかすべてが反映されている:小飼弾のアルファギークに逢いたい♥|gihyo.jp … 技術評論社

🔗 HTTPClient gem

「こちらはVimeoに上がっている動画ですが、自分が大好きなHTTPClient gemの作者である@nahiさんの発表が個人的にとても刺さりました↓」「そういえば以前からHTTPClientを推してましたね」「HTTPClientで自分のやりたいことが全部できるのは、この方が既存のHTTPクライアントで何がサポートされて何がサポートされていないかをこんなに詳しくサーベイしていたからなんだなというのがとてもよくわかる動画」「お〜これは細かい!」「この動画を見て、改めて今後もHTTPClientを使おうという気持ちになりました: 自分でRubyのHTTPクライアントライブラリを選定したことのある人には響くんじゃないかな」

大江戸HTTPクライアント絵巻 / @nahi from ogi on Vimeo.

nahi/httpclient - GitHub

「視聴中に寝落ちしてこのあたりを見られませんでした…」「自分も懇親会の記憶があまりない😆

🔗 Active ResourceとHer

「ところで、DHHの講演の中でActive ResourceというものがRailsの機能として登場していて”こんな機能あったかな?”と思ったら、v3.2.6あたりを最後にgemに切り出されていたんですね↓」「Active Resourceは、Railsが描いた未来の姿のひとつとしてかなり昔からあります」

rails/activeresource - GitHub

なお、apidock.comにもActive Resourceはありませんでした。

「ちなみに、Active Resourceのような機能を今やるのであれば、herというライブラリがかなりメジャーです↓」「her、そういえば以前もお話しされてましたね」「自分は今もよくherを使っています」

remi/her - GitHub

# remi/herより
User.all
# GET "https://api.example.com/users" and return an array of User objects

User.find(1)
# GET "https://api.example.com/users/1" and return a User object

@user = User.create(fullname: "Tobias Fünke")
# POST "https://api.example.com/users" with `fullname=Tobias+Fünke` and return the saved User object

@user = User.new(fullname: "Tobias Fünke")
@user.occupation = "actor"
@user.save
# POST "https://api.example.com/users" with `fullname=Tobias+Fünke&occupation=actor` and return the saved User object

@user = User.find(1)
@user.fullname = "Lindsay Fünke"
@user.save
# PUT "https://api.example.com/users/1" with `fullname=Lindsay+Fünke` and return the updated User object

「RESTfulに実装されているAPIサーバーであれば、あたかもActive Recordのようなインターフェースでデータの取得・更新ができて、たとえばfind(1)してsaveすると"https://api.example.com/users/1をPUTしたりする」「お〜、そういうことができるんですか、これいいな〜」「ただしwhereのようなメソッドはAPI側が対応する必要がありますし、当然orjoinみたいなことはできないので、Active Recordとすべて同じというわけにはいきませんが、idで取ってくるような処理なら十分使えます」「なるほど」

「Active Resourceはほとんど更新されなくなっていますね、もっともherも直近のリリースは2019年ですが」「たしかに」「herは以下のようにアダプタがモジュラブルになっていて使いやすいですし↓、Active Recordを継承しているわけではないのでRailsへの依存も小さいはず: 今Active Resourceのようなことをするならherだと自分は思っています」

# remi/herより
# config/initializers/her.rb
Her::API.setup url: "https://api.example.com" do |c|
  # Request
  c.use Faraday::Request::UrlEncoded

  # Response
  c.use Her::Middleware::DefaultParseJSON

  # Adapter
  c.use Faraday::Adapter::NetHttp
end

「Active Resourceという概念は今でも好きなので、それもあって今もherを使ってます」「マイクロサービスが流行っている今Active Resourceがカムバックしてもよかったんじゃないかというコメントを見かけましたけど、それもちょっとわかるかも」「Active Resourceは、ローカルのリソースも外部リソースも同じインターフェイスで扱えるところが強みなんですよ: おそらくDHHも自分が作ったActive Resourceという概念が念頭にあるから今もRESTfulにこだわっているのかなという気持ちに少しなりました」「ふーむ」

参考: Representational State Transfer(REST)- Wikipedia

🔗 Railsのトランザクション入門(Ruby Weeklyより)


つっつきボイス:「Railsのデータベーストランザクションの基本的な解説のようです」「トランザクションとは何か、トランザクションの作成、ロールバック、失敗したトランザクションのキャッチ、まさに入門の導入部という感じですね」

🔗 ルーティングをRESTfulにする(Ruby Weeklyより)


つっつきボイス:「ネステッドなルーティングに関する記事のようですね」

# 同記事より: RESTfulでない例
# config/routes.rb
Rails.application.routes.draw do
  resources :repositories, only: %i[index show] do
    resources :collaborators, only: %i[index] do
      post :accept_invite
      post :decline_invite
      post :invite
      get :show_invite
    end
  end
end

「上はrepositoriesの下にcollaboratorsがあるけど、なんちゃら_inviteに対応する中間モデルがないので、以下のようにinvitationsという概念を設けることでRESTfulになる、という流れですね」「なるほど」

# 同記事より: RESTfulな例
# config/routes.rb
Rails.application.routes.draw do
  resources :repositories, only: %i[index show] do
    resources :collaborators, only: %i[index]
    resources :invitations, only: %i[show create update destroy]
  end
end

「ルーティングがRESTfulになっていないということは、中間モデルが概念化されていないということ: さっきのDHHの講演動画でもまさに同じことを説明してます😆」「お〜そこにもつながるとは」

🔗 その他Rails

つっつきボイス:「Rubyのprotectedを使うときがあったとは」「正しく使える気がしない機能」「Javaを最初に勉強するとRubyのprotectedの意味がJavaと違いすぎて頭抱えますよね」「そういえばjnchitoさんの大昔の記事でも頭抱えてました↓」「今でもわからないレベル」

参考: 呼び出し制限 — クラス/メソッドの定義 (Ruby 3.0.0 リファレンスマニュアル)

参考: JavaやC#の常識が通用しないRubyのprivateメソッド - give IT a try


前編は以上です。

バックナンバー(2021年度第4四半期)

週刊Railsウォッチ: 2021年度Rubyアソシエーション開発助成、Rails REST APIレベルで楽観的ロックほか(20211102後編)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ:(20211108前編)RubyKaigiの過去講演動画が多数公開、Active ResourceとHerほか first appeared on TechRacho.

週刊Railsウォッチ: JSON.parseの機能、Opal 1.3、async gem、Linuxコマンドチートシートほか(20211110後編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗 Ruby

🔗 RubyのJSON.parseのあまり知られていない機能(Ruby Weeklyより)


つっつきボイス:「Ruby標準のJSONライブラリはたしかにかなり強力」

参考: Ruby API JSON.#parse (Ruby 3.0.0 リファレンスマニュアル)

「JSONの仕様にはキーを二重引用符"で囲む形式と囲まない形式が両方あるんですが、JSON.parsesymbolize_namesを有効にするとキーを文字列形式からシンボル形式に変換できる↓」「え〜知らなかった!」

# 同記事より抜粋
> JSON.dump([{ foo: { bar: "baz" } }])
#=> "[{\"foo\":{\"bar\":\"baz\"}}]"

> JSON.parse(json, symbolize_names: true)
#=> { foo: { bar: "baz" } }

「ちょうど今日Webチーム内発表で話題になったpretty_generateを使うと、JSONをインデント付きで出力できる↓」「Rubyだとこれがデフォルトで使えるのは強いですね」「Ruby標準のJSONライブラリは、ドキュメントを丁寧に読むとこういう欲しい機能がちゃんとあったりするんですよ」「APIドキュメントちゃんと読まないといかんな〜」

# docs.ruby-lang.orgより
require "json"

hash = { "name": "tanaka", "age": 19 }
puts JSON.generate(hash)
# => {"name":"tanaka","age":19}

puts JSON.pretty_generate(hash)
# => {
#      "name": "tanaka",
#      "age": 19
#    }

puts JSON.pretty_generate(hash, space: "\t")
# => {
#      "name":  "tanaka",
#      "age": 19
#    }

参考: JSON.#pretty_generate (Ruby 3.0.0 リファレンスマニュアル)

「なお、記事で言及しているRailsのdeep_symoblize_keysも地味にありがたい機能です↓」

参考: Rails API Hash#deep_symoblize_keys

🔗 Opal 1.3リリース

opal/opal - GitHub


つっつきボイス:「Opalって何でしたっけ?「RubyコードをJavaScriptに変換してブラウザで動かせるトランスパイラですね」「最近Opalの更新が活発だな〜」「どれぐらい使われているのか知りたい」

「え、Rubyのrefinementをほぼサポート?」「refinementが動くのスゴい」「ここまでやるとは思わなかった」

RubyのRefinement(翻訳: 公式ドキュメントより)

「Rubyのフリップフロップまでサポートしてる↓」「あれ、フリップフロップは消えたはずの機能では?(ウォッチ20180615)」

# opalrb.comより
a=b=c=(1..100).each do |num|
  print num, ?\r,
    ("Fizz" unless (a = !a) .. (a = !a)),
    ("Buzz" unless (b = !b) ... !((c = !c) .. (c = !c))),
    ?\n
end

後で手元の環境で調べると、Ruby 3.0でフリップフロップが普通に動きました↓。当時のissue #5400を見るとその後Matzがフリップフロップを消さないことに同意していました。知らなかった…

~$ ruby -v
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-darwin20]
~$ irb
irb(main):001:0> (1..10).each {|i| puts i if i==3..i==5 }
3
4
5
=> 1..10

🔗 Async gem

socketry/async - GitHub


つっつきボイス:「今のOpal 1.3でもJSのasync/awaitのサポートが実験的に入っていましたけど、こちらのasync gemはJSとは無関係にRubyコードの中でasync/await的な書き方ができるもののようですね」「そのasync gemが標準ライブラリに入るかもしれないという話なんですね」

なお、元記事によると標準ライブラリにはまだ取り込まれていないそうです。また、ちょうど出たばかりのRuby 3.1.0-preview1にもasyncはまだ見当たりませんでした。以下のissueが関連しているようです。


「async gemのビジネス上の使いみちはすぐには思いつかないけど、たとえばRailsもSinatraも使わずに、Rackアプリのクラスを、それこそnode.jsのサーバーを直接書くように書きたいという人にとっては、このasync gemがズバリ欲しいものになるかもしれませんね」「おぉ?」

「Node.jsで作るサーバーはRackのコードを生で書いているのに近い感じなんですが、Rubyでasync/awaitが使えるなら、そこそこ普通の機能を持つマルチスレッドのWebサーバーをRackだけで今よりも簡単に書けるんじゃないかな」「お〜」

「そういうコードでサーバーを書くと嬉しい点は、コードベースが小さくなるのでlambdaとかにも置きやすくなること」「なるほど」「asyng gemがどういう流れで標準に取り込まれることになったのかはわかりませんが、もしかするとそういう将来構想があるのかなと想像してみました」「面白そう!」

🔗 gem公開時にYubiKeyで認証する(Ruby Weeklyより)

Yubico/yubikey-manager - GitHub


つっつきボイス:「YubiKeyを使うことでgemを公開するときにGitHubにワンタイムパスワードを何度も入力せずに済むようにしたという@tenderloveさんの記事」「YubiKeyはハードウェア認証デバイスの名前なんですね」

参考: YubiKey - Wikipedia
参考: ワンタイムパスワード - Wikipedia

元記事は、最近のha-parser-jsというnpmパッケージのリポジトリがハイジャックされた↓のを受けて書かれていました。

参考: Security issue: compromised npm packages of ua-parser-js (0.7.29, 0.8.0, 1.0.0) - Questions about deprecated npm package ua-parser-js · Issue #536 · faisalman/ua-parser-js

🔗 その他Ruby

つっつきボイス:「jnchitoさんのチェリー本(プロを目指す人のためのRuby入門)の改訂2版を記念して、Rubyプログラミング問題を解く企画がQiitaのアドベントカレンダー枠で募集中です(編集部注: その後同カレンダーはひととおり埋まりました)」「自分も記念にエントリしました」「お〜入ってますね」「お題は点字メーカープログラムをRubyで書くというもの」

以下はつっつき後のツイートです↓。

🔗 点字よもやま話

「気になって探してみると、やはり言語ごとに点字のエンコードも違うそうです↓」

参考: 世界の点字|メガネくん@盲学校/特別支援学校からの発信|note

そういえば点字の英語は考案者の名前を取ってBraille(ブライユ)と呼ぶのでした。

「中国語の点字は漢字にマッピングするのが不可能なので音(ピンイン)で取ってると知りました」「記事の言語数と詳しさがヤバい」「点字のサイズが国によって違うとか、ここまで調べた人スゴい」「自分にとって当たり前の情報でも、こうやって記録して公開されないと本当に失われてしまうので、記事にするの大事だなと改めて思いますね」

参考: 拼音 - Wikipedia

「エントリするときに点字の資料を見ましたけど、6ビットの単位を組み合わせてひらがなを表現してるんですよね」「英語のモールス符号は最も頻度が高いEが.のように短くエンコードされますけど、点字は指先で読むのがメインの用途なので、発信と受信の両方を想定しているモールス符号とはエンコーディングの発想が違うかも」「たしかに点字で喋ったり手で書いて渡したりすることは考えにくそうですね」「点字だと、たとえばQRコードなどもそうですが、ひっくり返したときに誤認しにくいようなグラフィカルな側面にも気を遣っていそう」

参考: モールス符号 - Wikipedia
参考: QRコード - Wikipedia

🔗クラウド/コンテナ/インフラ/Serverless

🔗 Linuxコマンドチートシート


つっつきボイス:「記事のサイトから『この記事取り上げてみてくれない?』という英文メールが届いたので拾ってみました」

ifconfig

「さすがに今の時代にifconfigは使わないかな」「今はipコマンドだからifconfigは非推奨でしたっけ」「古いの知ってるけどつい打っちゃいます」「ipコマンドも記事に載ってた」

参考: Ubuntu 17.04 その132 - ifconfigからipコマンドへ移行しよう・ipコマンドはifconfigを置き換える - kledgeb

netstat

netstatは難しい」「雰囲気で使ってたヤツです」「自分はオプションと一緒に覚えてます」

参考: TCP/IP通信の状態を調べる「netstat」コマンドを使いこなす:Tech TIPS(1/2 ページ) - @IT

lsof

lsofはプロセスが掴んでいるファイルを調べられるので知っておくと便利」

参考: 【 lsof 】コマンド――オープンしているファイルを一覧表示する:Linux基本コマンドTips(298) - @IT

dig

「さすがにnslookupは載っていなかった」「入ってるのはWindowsぐらいですね」「今はdigだけど、もっと新しいコマンドがあったようななかったような」「digはLinux環境によっては入ってないときありますよね」「そうそう、何とかutilsパッケージをインストールしないといけなかったりする」

参考: 【 dig 】コマンド――ドメイン名からIPアドレスを調べる:Linux基本コマンドTips(158) - @IT
参考: networking - How do I install dig? - Ask Ubuntu

chmod

chmodでディレクトリのパーミッションを4桁で指定するときありますよね」「4桁目のsticky bitはときどき重要なヤツ」「sticky bitがわかるとsudoができる理由とかもわかる」

参考: Linux: SUID、SGID、スティッキービットまとめ - Qiita

kill

killコマンドはもう定着してしまいましたけど、本来はシグナルを送信するコマンドに過ぎないのでsigとかsignalにしておいた方がよかったのかなと思うときもありますね」「killわかるけど響きがちょっと物騒ですよね」「今だったら通らないかも」

参考: シグナルと kill コマンドについてちゃんと調べてみた | Basicinc Enjoy Hacking!

ps

psコマンドのオプションにハイフン-を付ける付けないで流派がありますよね」「BSDとSystemVで違っていたのが元でしたっけ」「tarオプションのハイフンにも付ける人と付けない人いますね」「ハイフン付けたことなかった」「ハイフン付けてます」

参考: psコマンドに潜むBSDとSystemVの系譜

lsusb

「お、lsusbコマンドも載ってる: これも昔からあって、たとえば正体のわからないUSBデバイスを接続してlsusbでベンダーIDを調べたりしましたよ」「知らなかった〜」「そのベンダーIDをmodprobe.confファイルに書いたりして無理やりUSBデバイスを動かしてた、懐かしい」「modprobe久しぶりに聞いたかも」「昔はドライバがないこともちょくちょくありました」

参考: 【 lsusb 】コマンド――USBデバイスの一覧と詳細情報を表示する:Linux基本コマンドTips(273) - @IT
参考: modprobe.conf - ファイルのフォーマットと規約の説明 - Linux コマンド集 一覧表

aptapt-get

「記事にapt-getが載っているところに親近感を感じてしまった」「今はaptだけでいいんですよね」「aptの方が機能が多いのはわかっているんだけど、結局慣れてるapt-get使ってます」「自分もです」

参考: 「apt-get」はもう古い?新しい「apt」コマンドを使ったUbuntuのパッケージ管理 | LFI

🔗CSS/HTML/フロントエンド/テスト/デザイン

🔗 Chrome DevToolsでユーザー操作の録画・再生・測定

つっつきボイス:「BPS社内Slackに貼っていただいたツイートです」「ユーザー操作をChrome DevToolsで記録再現できるってスゴいですよね」「しかも中でPuppeteerが動いている↓ので、exportするとPuppeteerファイルになるのは実用的」「これいいな〜」「これでQA部門の人にテストを作ってもらえるようになったらいいかも」「テスト書けなくてもテスト自動化できるのは夢ですよね」

puppeteer/puppeteer - GitHub


後編は以上です。

バックナンバー(2021年度第4四半期)

週刊Railsウォッチ:(20211108前編)RubyKaigiの過去講演動画が多数公開、Active ResourceとHerほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Ruby Weekly

The post 週刊Railsウォッチ: JSON.parseの機能、Opal 1.3、async gem、Linuxコマンドチートシートほか(20211110後編) first appeared on TechRacho.

Ruby 3.1.0-preview1がリリースされました

$
0
0

Ruby 3.1.0-preview1がリリースされました。

詳しくはリリース情報をご覧ください。なお3.1.0のChangelogはリポジトリ内でまだ見当たりませんでした。

現時点の主な新機能

preview版ということもあり、主な新機能に関連するTechRachoの過去記事や関連記事などを貼りました。今後変わる可能性があります。

YJITがオプションで導入

なおYJITとMJITはいずれもデフォルトではオフで、一方しかオンにできません。

YJIT: CRuby向けの新しいJITコンパイラを構築する(翻訳)

週刊Railsウォッチ: YJITがRuby 3.1向けにマージ、ripperのドキュメント化、crontabの罠ほか(20211026後編)

MJIT改良

参考: Ruby 3 JIT can make Rails faster. I’ve wondered Why Rails becomes slow… | by k0kubun | Medium

ruby/debugが組み込みに

Rubyの新しいデバッガの機能を先行紹介(翻訳)

週刊Railsウォッチ: Rails 7でbyebugがruby/debugに変更、GitHub Codespacesをサポートほか(20211004前編)

ruby/debugのChrome Devtools連携をRailsで動かす

IRBの改良

error_highlightによるエラーの発生位置明示など。

ruby/error_highlight - GitHub

ES2015風のハッシュのショートハンド記法

週刊Railsウォッチ: Rails 7 Alpha 1と2が公開、Rubyハッシュのショートハンド記法、iCare Dev Meetupほか(20210921)

TypeProfとRBSの改良

参考: rbenvでの3.1.0-preview1セットアップ

rbenv環境が前提です。以下を実行して3.1.0-preview1をインストールします。

$ git -C "$(rbenv root)"/plugins/ruby-build pull
$ rbenv install 3.1.0-preview1

rbenv localまたはrbenv global3.1.0-preview1を有効にします。

なお以下は3.1.0-preview1というディレクトリでrbenv localを実行する場合の例です。

mkdir 3.1.0-preview1; cd 3.1.0-preview1
rbenv local 3.1.0-preview1

ruby --enable-yjit -vを実行して+YJITが表示されればYJITが使えることを確認できます。MJITもruby --enable-jit -vで同様に確認できます。

$ ruby --enable-yjit -v
ruby 3.1.0preview1 (2021-11-09 master 5a3b2e6141) +YJIT [x86_64-darwin20]
$ ruby --enable-jit -v
ruby 3.1.0preview1 (2021-11-09 master 5a3b2e6141) +JIT [x86_64-darwin20]

関連記事

Ruby 3.0.2/2.7.4/2.6.8セキュリティ修正がリリースされました

The post Ruby 3.1.0-preview1がリリースされました first appeared on TechRacho.

Viewing all 1838 articles
Browse latest View live