Quantcast
Channel: Ruby on Railsの記事一覧|TechRacho by BPS株式会社
Viewing all 1410 articles
Browse latest View live

Railsのフロントエンドのノウハウ#2: JavaScript編(翻訳)

$
0
0

概要

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

記事を2本に分割しました。日本語タイトルは内容に即したものにしています。

Railsのフロントエンドのノウハウ#2: JavaScript編(翻訳)

RailsにおけるJavaScriptについての洞察

Rails開発者の多くは、JavaScriptがうまく動作しない場合の振る舞いについてなかなか把握できないことがあります。しかもRailsの場合、JavaScriptのいくつかの振る舞いについても特定の方法で変更されます。

TurbolinksやSpringを嫌う人が多いという話を聞いたことがあるかと思います。私はたまたまどちらの技術も好きですが、これらの動作を理解しておかないと、百戦錬磨のJavaScript開発者ですら頭を抱えるような事態になるかもしれません。

これら2つのライブラリについて簡単にご説明いたします。

Turbolinksは、ページの読み込みを高速化したり、フォームや一部のCRUD操作で使えるUJS機能を素早く読み込めるようにしたりします。Turbolinksを導入するとJavaScriptの「page ready hook」が変更されます。このフックは"turbolinks:load"を行うのに使われます。Turbolinksを使っていると、標準のJavaScript「page ready hook」でいくつかの問題が生じます。

Springはアプリのコードを対象とするプリローダであり、Rubyコードとフロントエンドのアセットの両方について読み込んだものを自身のプロセスにキャッシュし、次回のリクエストで使えるようにします。JavaScriptのエラーハンドリングについての知識がないと、解決に多大な日数を要するバグを追いかけるはめになるかもしれません。Springは、最初のページ読み込みから最初に操作を行った「後で」これらを機能させるためです。したがって、サイトが読み込まれるときに一部が動かないことがあっても、リンクをクリックすると動き出します。

Springの役割を詳しく調べる前に、JavaScriptがひどいコードをどのように扱うかを最初に見てみることにしましょう。

Javascriptが何らかのひどいコードに遭遇したり、評価すべきでないものを評価したりすると、JavaScriptは現在の作業を停止して、何か問題が生じていることをブラウザのコンソールに出力します。すなわち、アプリにJavaScriptの何か新しいライブラリを導入し、そこにひどいコードがあると、それ以降のコードはライブラリであろうと自分のコードであろうと動かなくなります。これはJavaScriptの通常の振る舞いであり、ひどいコードを実行する時点でコードの事項を停止するためのものです。

しかしSpringを追加した場合、アセットがキャッシュに(Turbolinksもろとも)読み込まれてひどいJavaScriptコードが評価された後、SpringはJavaScriptコードをの実行を回避しません。つまり最初のページを読み込むと、SpringやTurbolinksがアセットやライブラリをキャッシュしてより高速読み込みに備えるのです。

最初のリンクを操作してたどるまではまったく問題ないように見えることがあります。というのも、SpringはひどいJavaScriptコードの後でよいJavaScriptコードを生成するためです。最初のページ読み込みでJavaScriptの機能が不要であれば、何か問題が起きているかどうかをコンソールで確認しない限り、何も問題が起きていないように見えます。そして(productionで)コードを公開すると、「ローカルではちゃんと動く」にもかかわらず、サイトの一部がまったく突然にダウンして期待どおりに動かなくなります。

この手の「ローカルと同じ結果をproduction環境で得られない」問題のせいで、多くの人が髪の毛をかきむしりまくっています。

ここでJavaScript開発者にひとつ耳寄りな小技をご紹介します。アプリに新しいライブラリなり新しいJavaScriptコードなりを追加する場合は、常にその末尾にconsole.log("Seems okay ¯\_(ツ)_/¯");を追加しておくというものです。こうしておけばブラウザコンソールでコードがちゃんと動いていることを確認できるようになります。コンソールログにこれが出力されていれば、自分のJavaScriptコードが読み込み中に「ひどいコード」として評価されていないことがわかります。

私はBootstrap 4ライブラリをいろんなバージョンで試しましたが、その中に「ひどい」と評価されるJavaScriptコードも含まれていたことがありました。しかも私はそれを、アプリで必要とされる他のJavaScriptライブラリより前に配置していたのです。おかげで最初のページ読み込みでJavaScript関連のコードが軒並み動かず、ページをリフレッシュしたり何らかのリンク操作を行ったりした後でどうやら動くという事態になりました。私がBootstrap 4ライブラリをTurbolinksより前に置いていたために、コードの振る舞いがそのようになってしまったのです。Springがあっても最初のページ読み込みで問題なく動くように、CoffeeScriptでささやかなハックを仕込みました。

if !Turbolinks?
  location.reload

Turbolinks.dispatch("turbolinks:load")

これはローカル環境では問題なく動作したのですが、productionでは動きませんでした。Rails開発者は、"turbolinks:load"トリガがWebサイトの最初のページ読み込みで当初呼び出されないという問題をちょくちょく踏んでいます。

Turbolinks.dispatch("turbolinks:load")は、JavaScriptのその部分のコードが評価された後できるだけ早い段階でこのWebフックを即座にトリガするための方法です。これは、最初のページ読み込みの時点でJavaScriptやCoffeeScriptのスクリプトをトリガしたい方にとって役に立ちます。

ページ読み込み時にCoffeeScriptをトリガする場合、ちょっと一手間かける必要があります。というのも、CoffeeScriptコードはすべて1つの関数内にラップされていて、スコープが保護されているためです。Turbolinks.dispatch("turoblinks:load")メソッドを使うことで、このあたりのややこしさが少しマシになります。

Springを使っている場合に有用なハックはif !Turbolinks?; location.reloadしかありません。これが必要になるのは、JavaScriptの何らかの「ひどいコード」が評価されるdevelopment環境でJavaScriptコードを読み込む場合だけです。これを使うのではなく、先ほどのconsole.logテクニックを用いてJavaScriptコードの実行に成功したかどうかを確認しましょう。その後で、「ひどいコード」として評価される状況を改善しましょう。

最後に

Capybaraは、フロントエンドのエクスペリエンス全体のテストをシンプルに書ける素晴らしいライブラリです。RailsはCapybaraテストの大半を代わりに生成してくれるので、それを読んで学べば同じようなテストを今後も書き続けられます。面倒な設定はRailsが肩代わりしてくれるので、フロントエンドのテストを始めるのがとても簡単になります。

Rails 5のもうひとつうれしい点は、システムテストで例外が発生すると、ブラウザウィンドウの内容のスクリーンショットをtmp/screenshots/に自動保存してくれるようになったことです。画面とスクリーンショットを比較してテストの問題を特定しやすくなりました。

Railsのライブラリと併用した場合のJavaScriptの振る舞いを理解しておけば、地雷を避けてデバッグ時間を大幅に節約できるようになります。JavaScriptの振る舞いを正しく理解すれば、これまでうんざりさせられていた強力なツールたちをさらに使いこなせるようになります。TurbolinksSpringは、効用をと利用法を理解しておけば素晴らしいツールになるのです。

おかげで近頃はRails開発者でよかったと思えます。フロントエンドテストで大いに楽しみましょう!

Twitter: x“Rails Frontend Testing with JavaScript Insights” via @6ftdan

関連記事

Rails 5.1以降のシステムテストをRSpecで実行する(翻訳)

Rails 5: WebSocketのマルチセッションをMiniTestとシステムテストでテストする(翻訳)


Rails: モデルの外では名前付きスコープだけを使おう(翻訳)

$
0
0

概要

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

Rails: モデルの外では名前付きスコープだけを使おう(翻訳)

前回の記事では#whereメソッドで「hashスタイル」を使おうというお話をいたしましたが、あまり自分が現実に使うことのなさそうなコード例でした💦

次のようには書かないこと

コントローラやビューで#whereスコープを使う。

class PostsController < ApplicationController
  def index
    @posts = Post.where(status: 'published')
  end
end

次のように書くこと

名前付きスコープはモデルでのみ定義する。

class Post < ApplicationRecord
  scope :published, -> { where(status: 'published') }
end

コントローラなどでは次のように書く。

class PostsController < ApplicationController
  def index
    @posts = Post.published
  end
end

そうすべき理由

この手法を用いることで、コード編成が改善されます。次の2つの点において、長期的な生産性向上のために実に有用です。

  • その1: 自分が作り出す概念が名前として具現化されます。それ自体が明快に語ってくれる適切な名前を付けることで、あなたの同僚はもちろん、「未来の自分」にとってもしばしばありがたいものになります。
  • その2: 探す場所が1箇所に絞られます。スコープをモデルの外で定義していると、思いつくままのスコープやら条件やらの定義がコードベースのあちこちにばらまかれることになります。すべての条件が定義される場所がわかっていれば、リファクタリングやデータベースパフォーマンスの最適化でどこをチェックすればよいかで迷うことがなくなります。

そうすべきでない理由があるとすれば

#limitや単純な#orderやページネーションに関連するスコープぐらいであれば、その場限りの特定のスコープを作ってもほとんど困ることはありません。ActiveRecord::Relationのメソッド群の構文はかなり明快だからです。

スコープに名前付けするメリットは、明快さをさらに高める場合にのみ有用です。#whereを使わないクエリの場合、シンプルなActiveRecord::Relationのメソッド群をスコープ内にラップしてもそれ以上理解しやすくなるとは限りません。

scope :by_title, -> { sort(:title) }                    # メリットないかも
scope :by_updated_at, -> { sort(:updated_at) }          # 名前ひどすぎ
scope :recently_updated, -> { sort(updated_at: :desc) } # たぶんこれはやる価値あり

順序が複雑な場合や、ソート順が#whereクエリで指定される条件と強く関連している場合であれば、名前付きスコープは依然としてよい選択肢である点は覚えておくとよいでしょう

#order#limitを使う名前付きスコープを作成するかどうかの判断に使える、うまいヒューリスティック(発見的方法)があります。「その概念には簡単に名前を付けられるかどうか」「既存のActiveRecord::Relationメソッド群よりもよい結果が得られるかどうか」です。

関連記事

Ruby/Railsのプロ開発者としての5年間を振り返る(翻訳)

Rails: ActiveRecord::Relationで生SQLは避けよう(翻訳)

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

$
0
0

概要

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

週刊Railsウォッチで絶賛された記事です。太字は訳で追加いたしました。

Rails: コントローラから@ハックを消し去る(翻訳)

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

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

「Rails Way」のおさらい

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

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

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メソッドであり、オブジェクト指向としてはごく普通の考え方です。このメソッドは別のテンプレートからも使えます。editテンプレートとshowテンプレートのどちらもuserを欲しがっているのであれば、どちらも同じメソッドを呼べばよいのです。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のライブラリから影響を受けていることをここに認めるものであります。このライブラリのおかげで最初の着想に触れることができました(ダジャレにあらず)。decent_exposureで好きになれなかったのは「暗黙の読み込み」の部分でした。これをやると隠蔽が甚だしくなり、カスタマイズが難しくなるからです。

訳注: 「exposed me to the idea」とdecent_exposureのシャレと思われます。なおdecent exposureは「個人情報の適度な露出」という流行語です。

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

関連記事

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

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

週刊Railsウォッチ(20180615)TTY gemとHTTPClient gemは優秀、Rubyの謎フリップフロップ、ちょいゆるRubyスタイルガイドほか

$
0
0

こんにちは、hachi8833です。仙台疲れが今になって来たような気がしないでもありません。雨の降らない国に行きたいです。

晴耕雨読のウォッチ、いってみましょう。

各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ

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

今回も主に公式のコミット情報からです。

⚓TableDefinition#columnでカラム定義が重複すると例外を出すように修正

# activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L354
       def column(name, type, options = {})
         name = name.to_s
         type = type.to_sym if type
         options = options.dup

-        if @columns_hash[name] && @columns_hash[name].primary_key?
-          raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table."
+        if @columns_hash[name]
+          if @columns_hash[name].primary_key?
+            raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table."
+          else
+            raise ArgumentError, "you can't define an already defined column '#{name}'."
+          end
         end

         index_options = options.delete(:index)
         index(name, index_options.is_a?(Hash) ? index_options : {}) if index_options
         @columns_hash[name] = new_column_definition(name, type, options)
         self
       end

つっつきボイス:TableDefinitionなんてのがあるのね」「マイグレーションがらみのようです」「create_tablet.columnが呼ばれるときの重複チェックを今回修正したと: テストコードがわかりやすい↓」「普通やらかさないエラーだろうけどraiseはしとかないとね😋

# activerecord/test/cases/migration/change_schema_test.rb#L199
+      def test_create_table_raises_when_defining_existing_column
+        error = assert_raise(ArgumentError) do
+          connection.create_table :testings do |t|
+            t.column :testing_column, :string
+            t.column :testing_column, :integer
+          end
+        end
+
+        assert_equal "you can't define an already defined column 'testing_column'.", error.message
+      end

参考: Ruby on Rails 5.2 / ActiveRecord::ConnectionAdapters::TableDefinition — DevDocs

columnがconnection_adapters/abstract/schema_definitions.rbにあるということは、abstractすなわち複数のコネクタで共通で使われるメソッドなんでしょうね: ぱっと見ですが」「abstractクラスなのでそれで合ってると思います🧐: 細かくはちゃんと追わないといけないですが、上のエラーももしかするとDBMSの種類によっては通っちゃうことがあったのかもしれないですね: SQLite3とか割と作りが雑なんでカラム名かぶっても通っちゃうとかありそう、知らんけど😆」「😆

「そういえば先週のウォッチでもSQLite3ラブラブ❤な意見が出てました」「ちなみにSQLite3、かなり速いっすよ🕶: Win32アプリみたいなPCローカルアプリで使うDBMSとしては読み込み速度とかめっちゃ優秀」「あー、そういえば以前いた職場の社内ツールでも中でSQLite3が走ってました」「ただSQLite3って型とかが割と雑で、確か内部では数値なんかも文字列で保存してたような覚えがあるんですよ」「『保存してSQLっぽく検索できればそれでいいのっ』って感じなんでしょうね😓」「だからこそ速いんでしょうけどね🤓

参考: SQLite Home Page


sqlite.orgより

⚓ActiveSupport::Time.zone.atTime::atの引数の不揃いを解消

# 同PRより
# Before:
Time.at(946684800, 123456.789).nsec       #=> 123456789
Time.zone.at(946684800, 123456.789).nsec  #=> ArgumentError (wrong number of arguments (given 2, expected 1))
# After:
Time.at(946684800, 123456.789).nsec       #=> 123456789
Time.zone.at(946684800, 123456.789).nsec  #=> 123456789
# activesupport/lib/active_support/values/time_zone.rb#L357
-    def at(secs)
-      Time.at(secs).utc.in_time_zone(self)
+    #
+    # A second argument can be supplied to specify sub-second precision.
+    #
+    #   Time.zone = 'Hawaii'                # => "Hawaii"
+    #   Time.at(946684800, 123456.789).nsec # => 123456789
+    def at(*args)
+      Time.at(*args).utc.in_time_zone(self)
     end

つっつきボイス:Time.atTime.zone.atを揃えたようです」「おや?これで見るとTime.zone.atの方を変えてる?: Time.zone.atの方が使用頻度多いし(つかRailsではそもそもこっちを使うべき👮‍♂️)、Time.atをじかに使うことってほとんどないと思うんだけどなー」「それもそうですね…🤔」「breaking changeになりそうに見える🕶」「そのときはRailsアップグレードガイドに載るんでしょうね」

参考: Ruby on Rails 5.2 / Time::at — DevDocs
参考: Ruby on Rails 5.2 / Time::zone — DevDocs

⚓.dup.freeze-ショートハンドに置き換え

# activemodel/lib/active_model/attributes.rb#L103
       def self.set_name_cache(name, value)
         const_name = "ATTR_#{name}"
         unless const_defined? const_name
-          const_set const_name, value.dup.freeze
+          const_set const_name, -value
         end
       end

つっつきボイス: 「出たな-🤣」「あ、これ以前のウォッチでもみんなで『キモチワルイー』って言ってた文字列dup&freezeのショートハンドですね😆」「確かRuby 2.5の機能じゃなかったっけ?」

毎度のことですが、記号は検索しづらいです😭。マイナス記号-です。

参考: instance method String#-@ (Ruby 2.5.0)

selfがfreeze されている文字列の場合、selfを返します。 freezeされていない場合は元の文字列のfreezeされた (できる限り既存の) 複製を返します。
ruby-lang.orgより

参考: NEWS for Ruby 2.5.0 (Ruby 2.5.0)
参考: Feature #13077: [PATCH] introduce String#fstring method - Ruby trunk - Ruby Issue Tracking System

⚓記号の雑学

ついでながら、マイナス記号とハイフン記号とダッシュ記号は本来は別の記号だったのですが、記号を増やしようがないタイプライターの時代に1つのキーで代用されてしまい(長いemダッシュは--としたり)、それがそのままASCIIなどにも持ち込まれてハイフンマイナスという苦し紛れな名前が付けられ、Unicodeで別記号が用意された今も地味に混乱を呼び続けてます。ハイフン記号とマイナス記号とダッシュ記号(emダッシュとenダッシュ)の使い分けには闇が横たわっています。ヨーロッパの一部には、ハイフンとダッシュを取り違えて使うとマジギレする国がありますのでご用心。見た目1ドットぐらいしか違わないんですけどね…😫



Dash vs. Hyphen ~ IngliszTiczer.plより

参考: ハイフンマイナス - Wikipedia
参考: ハイフン - Wikipedia
参考: ダッシュ (記号) - Wikipedia

⚓重複した子レコードを親がsaveしないよう修正

# 同issueより再現コード
# モデル

class Parent < ApplicationRecord
  has_many :children
end

class Child < ApplicationRecord
  belongs_to :parent
  validates :name, uniqueness: true
end

# コード
parent = Parent.new(children: [Child.new(name: 'Wiske'), Child.new(name: 'Wiske')])
parent.save
parent.errors.any?  # エラーになるはずが、ならない

つっつきボイス:validates uniqueness: trueだから本来エラーにならないといけないやつ: へー、この書き方↓するとすり抜けちゃってたのか😇」「😇」「これは下手すると既存のアプリでも知らないうちにエラーを作り込んじゃってたりすることがあるかも?自分はこの書き方はしないけどねっ🕶

Parent.new(children: [Child.new(name: 'Wiske'), Child.new(name: 'Wiske')])

「ついでに聞いちゃいますけど、今見ているコードにあるreflection↓っていう言葉は、ここではどういう意味が込められているんでしょうか?(英語的にいっぱい意味がありすぎるので🤯)」「reflectionというと、メソッドオブジェクトを取ってきてそれをゴニョゴニョしたいなんてときに使いますね」「あー、メタプログラミング的な?」「たぶんプログラミング関連ではそれ以外の意味でreflectionという言葉が出てくることはあまりないと思うので💦

# activerecord/lib/active_record/autosave_association.rb#L381
       def save_collection_association(reflection)
         if association = association_instance_get(reflection.name)
           autosave = reflection.options[:autosave]
...
               if autosave != false && (@new_record_before_save || record.new_record?)
                 if autosave
                   saved = association.insert_record(record, false)
                 elsif !reflection.nested?
-                  association_saved = association.insert_record(record)
                   if reflection.validate?
-                    saved = association_saved
+                    valid = association_valid?(reflection, record, index)
+                    saved = valid ? association.insert_record(record, false) : false
+                  else
+                    association.insert_record(record)
                   end
                 end
               elsif autosave

「たとえばですが、reflectionという名前の変数があるとすると、その中には状況によってさまざまなオブジェクトが動的に入る可能性があるよ、という意図が強調される感じですかね」「おー、『あのクラスとあのクラスだけが入ると思うなよ』っていうイメージですか」

「あくまでイメージというか概念寄りの話ですが、reflectionという言葉が使われていると、そこにどんなオブジェクトが入るかわからないし、どんなオブジェクトが来てもいいという感じ: C言語の(void *)キャストとか、Javaの(object)キャストなんかが概念的に似ているかな」「なるほどー!😀

参考: C言語: 汎用ポインタ
参考: 強く型付けされているJavaの理解に必修の“型変換” (2/3):【改訂版】Eclipseではじめるプログラミング(18) - @IT

「このコードならreflection.validate?とあるから、普通ならreflectionにはActiveRecordオブジェクトやActiveModelのインスタンスが入るんだろうけど、それ以外のオブジェクトが入る可能性だってあるんだぜ、という主張を込めて自分なら使うかな😎、ここでは知らんけどw」「😃😃

reflectionには「反射」「反省(self reflection)」「反映」などなどいろんな意味がありますが(「反」の文字が共通してますね🧐)、ポイントは意味が受動的なところかなと思いました。反映もapplyで表すと人間が能動的にやるイメージですが、reflectだと受動的というか性質に従って自動的に行われる、というニュアンスが強いと思います。reflectionがメタプロの文脈でよく使われるのもそれなのかなと。

人間の態度や状況が何かにreflectするとは、そうした態度や状況(またはそう思われるもの)がそこに存在することが示されていることを表す。
Cobuldより大意

⚓初期化ブロックがbuild_recordにあるのをbefore_addに移動

以下はコミットログから見繕いました。

# https://github.com/rails/rails/commit/c256d02f010228d260b408e8b7fda0a4bcb33f39#diff-20f545c453ee24942b6f7ae565e9e369R104
       def build(attributes = {}, &block)
         if attributes.is_a?(Array)
           attributes.collect { |attr| build(attr, &block) }
         else
-          add_to_target(build_record(attributes)) do |record|
-            yield(record) if block_given?
-          end
+          add_to_target(build_record(attributes, &block))
         end
       end

つっつきボイス: 「改修範囲が広いので取りあえずこれだけ引用してみました」「ここだけ見て言うと、add_to_targetというのはおそらくフックを追加するメソッドかな: で、そういうコールバック登録の枠組みが既にあるならそれに揃えようよってことなんだと思う」「😀

# rails/activerecord/lib/active_record/associations/collection_association.rb#L282
      def add_to_target(record, skip_callbacks = false, &block)
        if association_scope.distinct_value
          index = @target.index(record)
        end
        replace_on_target(record, index, skip_callbacks, &block)
      end

⚓force_equality?をpublic APIに

43ef00eの続きだそうです。

# https://github.com/rails/rails/pull/33067/files#diff-e66146ba2ab968e251944a764e4ed967R54
+      def force_equality?(value)
+        coder.respond_to?(:object_class) && value.is_a?(coder.object_class)
+      end

つっつきボイス:@kamipoさんが何度かに分けて修正したようです」「force_equality?ってメソッドがあるのか: どうやらrespond_to?(:object_class)でオブジェクトのクラスまで含めて等しいかどうかをチェックするやつみたいですね」「おー」「たとえばシリアライズしたときのパラメータのリストは同じでも、オブジェクトの種類が違うものがやってくることがあったりするけど、そういうのも検出するということでしょうね」「😃

「このメソッドなら、たとえばSTI(Single Table Inheritance)なんかでそれが親クラスのオブジェクトなのか子クラスのオブジェクトなのか、なんてのも見分けられるはず」「なるほど!」「オブジェクトが参照しているデータベースが同じだとシリアライズした後では区別できなくなっちゃうけど、シリアライズより前にそういうところをチェックするときに使えるんじゃないかな?ってね🤓」「😋」「これが欲しい気持ち、よくわかる」

Rails: STI(Single Table Inheritance)でハマったところ

参考: Rails ActiveRecordのSTI(Single Table Inheritance)の使い方 | EasyRamble

⚓Rails

⚓Bundler 2リリース間近のチェックリスト

Bundler関連ということで関連してこちらも。


つっつきボイス: 「リリースはまだみたいなので、こういうことをやりますリストですね」「そういえば今はもうRubyをバージョンアップすると最新のBundlerが入るようになったんだっけ?どっちだったっけ?」

そういえば2.5でのBundler標準化は延期されたのでした↓。

「チェックリストをざざーっと見た感じでは互換性の心配ほとんどなさそうというか、こっちでやることあまりないかも?: つか互換性なかったらRubyバージョンを単純に上げたときに即死だし😇」「Bundlerといえば、HerokuのBundlerが最新でないのでwarning出たりたまにつっかえたりするんですよね(今はどうだったかな💦)」「まーHerokuの場合Herokuの環境に身を任せないといけないので、面倒を見てくれる範囲は大きいけど自分はコンテナでやりたいかなー😂」「😀」「Linuxサーバーの面倒なところ触りたくないっという人にはもちろんHerokuはおすすめですね: Railsチュートリアルは今もHeroku使ってるんだったかな?」「今は違ってたような…?」

後で調べたら、Railsチュートリアルは今もHeroku使ってました

⚓graphql-ruby: Railsとも連携


graphql-ruby.orgより

今回のウォッチは何となくShopify成分が多めでした。


つっつきボイス: 「これ扱ったことなかったっけ?」「なかったっぽかったので(と思ったらありました💦)」「このロゴは素晴らしいですね😍」「グラフ理論のグラフをうまくあしらってるし😎

参考: apollo + rails(graphqlサーバー)でファイルをアップロードするmutationを作る方法 - Qiita

⚓Active Record 5.2のメモリ肥大化を探る

このSam Saffaronさんのブログは良記事多いですね。翻訳許可をいただいたので今後出したいと思います。

# 同記事より
a = []
Topic.limit(1000).each do |u|
   a << u.id
end

つっつきボイス: 「あー、↑こうやって書けばそりゃ確かに肥大化するナ」「eachを使ってるから?」「eachだけじゃなくてu.idを参照してるから: このu.idを評価しないといけないのでu.idが呼ばれるたびにTopicというモデルのオブジェクトが生成される」「そっちかー」「しかもそれをa <<で代入しちゃってるからこのループの間このモデルオブジェクトがまったく解放されない」「😲」「pluckすると速いって記事に書いてあるのもたぶんそれで、pluckならモデルオブジェクトを保持する必要ないはずだし」「pluckは配列を返しさえすればいいですしね」

「確かこの間のRails Developers Meetup 2018でもまさにこの辺の話をしていた人がいたと思う」「自分見てなかったかも😓

後でmorimorihogeさんからセッション名を教えていただきました↓。

参考: railsdm2018で「ActiveRecordデータ処理アンチパターン」を発表しました - Hack Your Design!

「まーでもActiveRecordの挙動とかlazy loadingのあたりなんかを忖度できるようにならないと、上のような書き方がヤバイ理由は見た瞬間にはわからないでしょうねー🧐」「そこが経験が必要なところかー😃」「あかんのはu.idという参照の仕方です😎: 終わってからGCすれば消えますけど(たぶんね)」「覚えます!」

参考: Ruby on Rails 5.2 / Enumerable#pluck — DevDocs

最近知ったRailsの便利なメソッド

⚓Rails 5で「関連付け先のないレコード」を使う(Ruby Weeklyより)


つっつきボイス: 「記事のスタイリングとかシンタックスハイライトがちと読みづらい…」「関連付けのない場合ってことなのかな?ざざっと見た感じ、assosiation先のレコードがなくても取得したい場合にLEFT JOINでの外部結合をActiveRecordでどう書くかって話のように見える」「時間ないので次行きましょうー」

  • Rails 4までは生SQLでJOIN
# 同記事より
User.join('left outer join posts on posts.user_id = users.id')
  .where(posts: {user_id: nil})
  .first
  • Rails 5はちょっとだけクリーンで読みやすい気がする
# 同記事より
User.left_joins(:posts)
  .where(posts: {user_id: nil})
  .first

⚓Rails 5.2のcredentialはセキュアではない?(RubyFlowより)


つっつきボイス: 「GitHub Wikiにざざっと書いた、1ページで収まる内容ですね」「これはわかりみあるヤツ: Rails 5.2のcrenentialは、言ってみれば例のTwelve-Factor Apps↓の原則3『設定を環境変数に格納する』に反してるんですよ」「あー」

参考: The Twelve-Factor App (日本語訳)


12factor.netより

「原理主義者としては許せないでしょうけど、そこまで行かなくても自分もこの辺は微妙: マスターキーの扱いっていろいろ難しいんですよ」「というと?」「チーム開発をしていると、開発メンバーが入れ替わるたびにマスターキーを変えないといけなくなるから: そして現実にメンバーの出入りはよくあることだし」「あー!🤫」「しかもマスターキーの変更履歴がリポジトリに残るのも、どうもねー🤔」「記事の人はnot secureとか書いてるけど、単にcredentialが好きじゃないんでしょうねー😆自分も使うつもりたぶんないし」「😆

⚓deep_pluck gemがRails 5.2に対応


つっつきボイス:pluckの入れ子版みたいなこのdeep_pluckウォッチで扱ったことありましたね: たぶんバッチとか書くときなんかに優秀なヤツ👍」「😀」「これもActiveRecordオブジェクトを生成しないから速いんだとしたら、自分なら生SQL書いちゃう、かなー?😆」「😆

⚓その他Rails

週刊Railsウォッチ(20170407)N+1問題解決のトレードオフ、Capybaraのテスト効率を上げる5つのコツほか


そろそろJSの組み合わせが爆発しそう。


つっつきボイス: 「ちょうど今日のチームミーティングでもWebpackについて発表ありましたけど、やっぱりWebpackerを知るにはWebpackそのものを知っておかないと結局後で困ることになりますね😆」「😆


webpack.js.orgより



つっつきボイス: 「テストって最初のうちはどの程度までみっちり書けばいいのか真剣に悩んじゃいました」「それはもう誰かに決めてもらうのが早い😎」「🤣」「ただ思うのは、よほどシビアなアプリでもない限り、カバレッジの数字だけを当てにしてもあまり意味がないかなってことですね」「カバレッジの値が増えたからといって本当に必要な部分がカバーできてるかどうかはまた別なのか🙂



つっつきボイス: 「2位のMagentoって知りませんでした」「自分も😁」「それにしてもShopify、いつの間にかこんないい位置についてるなー: ユーザー数はともかく、捌いているトラフィック数は凄い👁」「Shopify、Railsですしね🤠」「EC-CUBEと比較してみたいところだけど、日本製でほぼ日本市場だけだろうから比較は難しいかなー」



つっつきボイス:mizchiさんいいこと言うなー: フロントエンドの強い人で今は確かフリーランスだったかな?」「さっきのreflectionの話もそうでしたけど、コードを書いた人の意図を読み取る能力と、読み取れるようにコードを書く能力ってやっぱり重要ですね🧔🏽

そのうちIPA試験あたりにコード読解問題とか出たりするんでしょうか。


⚓Ruby trunkより

⚓Unicode 11のグルジア語の大文字小文字の取り扱いをどうしよう?


http://www.unicode.org/versions/Unicode11.0.0/ch07.pdf (Section 7.7, Georgian, pp. 320-321) より

Geogianが「グルジア語の」とも「ジョージアの」とも読めてしまうので、いちいち断りが必要です。グルジアというとスターリンの出身国というイメージ。なおグルジア語とロシア語には共通部分がほぼありません。


https://ja.wikipedia.org/wiki/%E3%82%B0%E3%83%AB%E3%82%B8%E3%82%A2%E8%AA%9E#/media/File:Kartvelian_languages.svg より

参考: グルジア語 - Wikipedia


つっつきボイス: 「これはもう多言語マニアがいないとどうしようもない😇」「issueでもそう言ってました😆

⚓GRND_NONBLOCKを設定せずにgetrandomが複数呼ばれるとロックする

if which ruby >/dev/null && which gem >/dev/null; then
    PATH="$(ruby -e 'puts Gem.user_dir')/bin:$PATH"
fi

つっつきボイス:GRND_NONBLOCKって、またLinuxに新しいフラグが出たのか」「(Linuxでしたか😓)」「よく見つけたなこれ感」

参考: getrandom(2) - Linux manual page

⚓Procの行番号/カラム番号、できればfirstやlastも取りたい

# rspec-parameterized gemのサンプル

  describe "lambda parameter" do
    where(:a, :b, :answer) do
      [
        [1 , 2 , -> {should == 3}],
        [5 , 8 , -> {should == 13}],
        [0 , 0 , -> {should == 0}]
      ]
    end

    with_them do
      subject {a + b}
      it "should do additions" do
        self.instance_exec(&answer)
      end
    end
  end

# 出力例

  lambda parameter
    a: 1, b: 2, answer: -> {should == 3}
      should do additions
    a: 5, b: 8, answer: -> {should == 13}
      should do additions
    a: 0, b: 0, answer: -> {should == 0}
      should do additions

tagomorisさんとjoker1007さんからのリクエストです。


つっつきボイス: 「最初気づかなかったけど、joker1007さんがそういう感じのgem作ってたそうです↓」「RSpecでwhere(:a, :b, :answer)みたいにwhereで指定したいってことのようだ: Procが複数になるとつらいとか、これは確かにやるなら言語でサポートする方がいいでしょうね😋

⚓Ruby

⚓Relaxed Ruby Style: ちょいゆるRubyスタイルガイド

Version 2.3とあるのはRubyのことなのかこのスタイルガイドのことなのか、どちらなんでしょう?


つっつきボイス: 「rubocop.ymlに軽くパッチを当てて使うようです」「パッチじゃなくて、元々rubocopにはinherit_fromというデフォルト設定を読み込む機能があるので、それを使って読み込んだ上で自分達のルールとの差分を上書きする、みたいな使い方ですね」「あー、そうでした😓」「うん、こういうのいいよね: BPS社内でも標準Rubocop.yml定めたいと思ってる🤓

⚓ramda-ruby:

# 同リポジトリより: トランスデューサー
  appender = R.flip(R.append)

  xform = R.map(R.add(10))
  R.transduce(xform, appender, [], [1, 2, 3, 4]) # [11, 12, 13, 14]

  xform = R.filter(:odd?.to_proc)
  R.transduce(xform, appender, [], [1, 2, 3, 4]) # [1, 3]

  xform = R.compose(R.map(R.add(10)), R.take(2))
  R.transduce(xform, appender, [], [1, 2, 3, 4]) # [11, 12]

  xform = R.compose(R.filter(:odd?.to_proc), R.take(2))
  R.transduce(xform, R.add, 100, [1, 2, 3, 4, 5]) # 104)
  R.transduce(xform, appender, [], [1, 2, 3, 4, 5]) # [1, 3])
  R.into([], xform, [1, 2, 3, 4, 5]) # [1, 3])

JavaScriptのRamdajsをRubyに移植したものだそうです。名前からlambdaのもじりという感じで、ramdajs.comには「育ちすぎたram(子羊)じゃないんだなー、たぶん」とありました。ramだ。


つっつきボイス: 「ramda: いいのかその読み方で😆」「たぶん子羊じゃないといいつつ、シンボルマークは羊ですね😄」「関数型の追求というよりは、フィルタ的な便利ツール集という印象」


ramdajs.comより

参考: ラムダ計算 - Wikipedia

トランスデューサーそのものはめちゃめちゃ意味が広いですが、ここでは関数型言語よりの何かなんでしょうね。何も見ずに「translator + reducer」かなと推測。

参考: トランスデューサー - Wikipedia

⚓Rubyとmallocの問題

⚓TTY: Ruby錬金術師の秘薬

RubyKaigi 2018で自分が見てなかったやつです(´・ω・`)。


つっつきボイス: 「お、これ自分見ましたよ: いい内容👍」「😃」「TTYってマルチスレッド対応のプログレスバーとかいろいろ機能揃ってますよ」

「このTTYってrails new的にファイルをどさっと作るんで、腰を据えて本格的なCLIアプリを作るためのテンプレート的なものでしょうね: 気楽にさっと書くにはかなり重装備な感じ」「この間のウォッチで取り上げたoptparseとはまた違うんでしょうか?」「optparseは引数処理を楽に書けるヤツなのでまた違いますね🕶
「TTYは、railsコマンドとかsystemdコマンドみたいなものすごく複雑なCLIを見通しよく作れますね: その気になればaptyarnだって作れちゃう😆」「🤣」「TTY上でmarkdownすら書ける」「何だかすげー」

「ただまあ、今こんなごついCLI書くぐらいならWebアプリにするよなという気もすると言えばする」「確かにー」「でも最初CLIで完成させてからそれをWebアプリ化するという流れはありかなとも思うし: CLIならバッチとかとっても扱いやすいし、融通が利きやすいし」「たぶんデバッグもしやすいし😃

「『こういうCLIはRubyで作ればいいじゃん』っていう文化になったらいいなーと思うし」「こうやってTTYなんかで作ったRubyのCLIを、mrubyとかでコンパイルして配布できればいいのにとも思うし: 結局RubyのCLIって配布がだいたい問題になるんですよね」「確かにー」「CLIでのGo言語の強みはrun anywhereですからね🕶

⚓go-mruby: mrubyのGoバインディング

// 同リポジトリより
package main

import (
    "fmt"
    "github.com/mitchellh/go-mruby"
)

func main() {
    mrb := mruby.NewMrb()
    defer mrb.Close()

    // Our custom function we'll expose to Ruby. The first return
    // value is what to return from the func and the second is an
    // exception to raise (if any).
    addFunc := func(m *mruby.Mrb, self *mruby.MrbValue) (mruby.Value, mruby.Value) {
        args := m.GetArgs()
        return mruby.Int(args[0].Fixnum() + args[1].Fixnum()), nil
    }

    // Lets define a custom class and a class method we can call.
    class := mrb.DefineClass("Example", nil)
    class.DefineClassMethod("add", addFunc, mruby.ArgsReq(2))

    // Let's call it and inspect the result
    result, err := mrb.LoadString(`Example.add(12, 30)`)
    if err != nil {
        panic(err.Error())
    }

    // This will output "Result: 42"
    fmt.Printf("Result: %s\n", result.String())
}

ついでにquartzという、RubyからGoプログラムを呼び出すgemも見つけたのですが、RubygemにGoのnative extensionを含める感じではなさそう…なぜかこの方面はさっぱり発展しません(´・ω・`)。

⚓httprbはいいぞ(Ruby Weeklyより)


同リポジトリより

# 同リポジトリより
HTTP.post("http://example.com/upload", form: { file: HTTP::FormData::File.new(io) })

つっつきボイス: 「このhttprb/httpってどうでしょう?」「少なくとも表の青いところにある標準のNet::HTTPはまず使うことはないと思う、というかつらすぎて使いたくない😆」「😆」「😆


同記事より

参考: class Net::HTTP (Ruby 2.5.0)

「この表の中で一番メジャーなHTTPクライアントって、赤いところにあるHTTPClient↓なんじゃないかなー: 少なくとも自分的にはベスト」

「ただ、HTTPClientはREADMEがそっけないんですが、実はそこから地味にリンク貼られているRDocのAPIドキュメントがすごくしっかり書かれてるんですよ↓」「ほー!!」「ここ読むとわかりますけど、プロキシもちゃんと対応してるし、BASIC認証の先にあるBASIC認証の対応とか、環境変数からプロキシ渡すなんてのもできます」「いいこと聞いた!😍」「😍

「表の緑色のところにあるFaraday↓もひと頃流行ったけど、Faradayの抽象化は人によって好みが分かれるかもしれない: クローラーを書くとかならFaradayが向いてますね」「逆にステータスコードが取りたいとかヘッダを確認したいとかならHTTPClientがいい」

「で、本題のhttprbは表の分類からもHTTPClientと同じところを狙ってるというのが取りあえずは見て取れますね」「😃」「おっ?httprbのインストールはgem install httpだって: この名前よく取れたなー😲」「Rubygems.orgで決めてるんでしたっけ?」「早いもの勝ちです😎

⚓Rubyで書かれたワールドカップ試合情報CLI(RubyFlowより)

# 同リポジトリより
$ footty              # Defaults to today's world cup 2018 matches

今日は以下でした。

#1 Thu Jun/14       Russia (RUS) vs Saudi Arabia (KSA) Group A  @ Luzhniki Stadium, Moscow

つっつきボイス: 「試合結果というより、『今日ある試合は何だったっけ?』用っぽいです」「こういう文化っていいですよねー」

⚓Rubyの謎機能: フリップフロップ

# 同記事より
irb(main):021:0> (1..10).each {|i| puts i if i==3..i==5 }
3
4
5
=> 1..10

つっつきボイス:Less Feature-Rich, More Funという記事を翻訳していて、一番最後にRubyのフリップフロップという謎の機能について言及されていたので」「何だこれ…確かに謎🤔

「あー、if i==3..i==5..の前と後の条件がフリップフロップになってるのか!😲」「😲」「😲」「こんなのがコードレビューに出てきたらつらすぎ😭」「Ruby Gold試験になら出そうですね…😓

⚓その他Ruby




RubyKaigiの応募要項です。


ほのっとしちゃいました。その後英語版↓も出ました。

⚓クラウド/コンテナ/Linux

以下時間切れのため、つっつきはここまでです🙇。後追いで何か追記するかもしれません。

⚓Linuxのloadavgの問題を追求

はてブで見つけました。


つっつきボイス: 「これはとてもいい記事」

⚓Googleのgvisorすごいかも

この間のウォッチで軽く取り上げたgvisorですが、TCFM #22の前半がこの話題でもちきりでした。

⚓ワンライナー特集

⚓GitLabが立て続けに機能を拡大

最新の記事ではありませんが一応。


参考: GitLabがGoogleのKubernetes Engineを統合、コンテナアプリケーションのデプロイが超簡単に | TechCrunch Japan

⚓その他クラウド/コンテナ/Linux

⚓SQL

⚓PostgreSQLのメモリ設定(Postgres Weeklyより)

⚓ロシア発: PostgreSQL 10の論理レプリケーションの復旧(Postgres Weeklyより)


同記事より

かなり長いです。

⚓はじめてのマテリアライズド・ビュー(Postgres Weeklyより)


同記事より

参考: マテリアライズドビュー - Wikipedia

⚓JavaScript

⚓Vue Native: VuejsでネイティブWebアプリを作るフレームワーク


vue-native.ioより

⚓PhantomJSの開発が正式に終了し、アーカイブ化(JSer.infoより)

昨年のウォッチでもお伝えしたPhantomJSが正式に終了しました。お疲れさまです。

週刊Railsウォッチ(20171026)factory_girlが突然factory_botに改名、Ruby Prize最終候補者決定、PhantomJS廃止、FireFoxのFireBug終了ほか

参考: PhantomJSの開発が終了しリポジトリがアーカイブ化された - JSer.info

⚓mobx: JSのステート管理ライブラリ(JSer.infoより)


mobx.js.orgより

JavaScriptのステート管理はVuejsにもあったりReduxもあったりと賑やかですね。js.orgに集結している?


redux.js.orgより

JavaScript: Reduxが必要なとき/不要なとき(翻訳)

⚓ExcelでJavaScriptがサポート

TypeScriptを直接サポートするつもりはないそうです。

参考: Microsoft、Excelカスタム関数としてJavaScriptのサポートを発表

⚓その他JavaScript

⚓CSS/HTML/フロントエンド/テスト

⚓直感に反する「なぜか覚えられないUI」(Frontend Weeklyより)

↑著名な認知心理学者のDonald NormanことDon Normanの名前から「Norman door」と呼ばれているそうです。てっきりこのドアの発明者かと思ってしまいました。
引くかと思ったら押す、押すかと思ったら引くような、「押す/引く」表示がないとガンガンに間違えるドアだそうです。

参考: Urban Dictionary: Norman Door
参考: ドナルド・ノーマン - Wikipedia

⚓テスティングを楽しく学びたい人向けのサイトなど

  • 元記事: テストラジオ — テスティングの話題が豊富なポッドキャスト

既に60回を超えているんですね。凄い。


testerchan.hatenadiary.comより

⚓Apollo Clientとは


apollographql.comより

⚓言語よろずの間

⚓Mage: Goのタスクランナー


同リポジトリより

英語の古語にある「メイジ(魔法使い)」だよなと思いつつ、日本人なのでつい「マゲ」かと思ってしまいました。

⚓Fo: Goで関数型やってみる言語


play.folang.orgより

なお、Goでジェネリクスを欲しいと思ったことが今のところありませんでした。

参考: ジェネリックプログラミング - Wikipedia

⚓Stack Overflowのデベロッパーアンケート結果2018年版

毎度お騒がせ。

参考: 2018年 人気&嫌われプログラミング言語トップ25- Stack Overflow | マイナビニュース

⚓その他言語

参考: 依存型 - Wikipedia





⚓その他

⚓Gitコマンドを異世界転生モノで解説するよ

⚓らめぇそれ

⚓「不正指令電磁的記録に関する罪」とは

はてブで知りました。

⚓Windowsに新しいアプリインストール形式「MSIX」が登場予定

⚓その他のその他





社内勉強会のお題が「トランザクションとロック」だったので、そういうときについ思い出す動画です。

⚓番外

⚓今どきの法科学

⚓あれそんなにヤバイ物質だったのか

⚓学校でがっつり教えられた人の立場は

⚓香害

⚓リアルポケモン認定したい

⚓火星で有機物発見か

地球に巨大隕石がぶつかったか何かで火星に飛来した小型隕石によるコンタミの可能性が気になります。南極は一面真っ白で月の石や火星の石がちょくちょく見つかるので、逆もありそうな気がしてしまいます。

参考: 南極サイエンス基地 > 南極隕石


今週は以上です。

バックナンバー(2018年度後半)

週刊Railsウォッチ(20180608)特集「RubyKaigi 2018後の祭り」、`Enumerable#index_with`は優秀、コントローラから`@`を消し去るほか

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

Rails tips: Railsアプリにシンタックスハイライト機能を追加する(翻訳)

$
0
0

概要

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

Rails tips: Railsアプリにシンタックスハイライト機能を追加する(翻訳)

コードのシンタックスハイライト機能は、開発者向けブログやWebサイトでコードスニペットを表示するのになくてはならないものです。JekyllやWordPressなどのブログエンジンを使っているなら簡単にシンタックスハイライト機能を追加できますが、Railsアプリにも簡単に追加する方法があります。

今回のねらい

美しいシンタックスハイライト機能を追加すると同時に、コードをデータベースに安全に保管して簡単に編集できるようにもしてみたいと思います。必要な要件をすべてリストアップしてみました。

  • コードのハイライト機能
  • ハイライトのテーマを簡単に変更できること
  • アプリに送信したコードを安全に保管できること
  • 簡単に編集できること
  • さまざまなプログラミング言語のハイライトをサポートすること

コードを安全にデータベースに保存する最も楽な方法は、markdown構文を用いることです。こうした構文であれば、ロジックの編集や改修も楽になります。

markdown

markdown構文をサポートするには、redcarpet gemのインストールが必要になります。このgemの利用法はいたってシンプルです。次の例で使い方をご覧いただけます。

Redcarpet::Markdown.new(Redcarpet::Render::HTML).render("This is *bongos*, indeed.")

上のコードから以下の出力を得られます。

<p>This is <em>bongos</em>, indeed.</p>

Gemfileには以下を追加する必要があります。

gem 'redcarpet'

続いてbundle installを実行すれば、markdown構文を用いて記事を書き、以下のコードでビューに表示できるようになります。

<%= Redcarpet::Markdown.new(Redcarpet::Render::HTML, fenced_code_blocks: true).render(@article.content).html_safe %>

シンタックスハイライト

markdown構文のサポートが片付いたので、シンタックスハイライトに集中できるようになりました。ここではrouge gemを用いることにします。このgemは、markdown構文のサポートに用いているredcarpetとの相性も完璧です。それではGemfileに以下を追加してbundle installを実行しましょう。

gem 'rouge'

お気づきかと思いますが、markdown構文のパースにはRedcarpet::Render::HTMLクラスを用いています。rougeとredcarpetを接続するには、独自のレンダリング用クラスを作成しなければなりません。/lib/blog_render.rbというファイルを作成し、そこに以下のコードを追加します。

require 'redcarpet'
require 'rouge'
require 'rouge/plugins/redcarpet'
# markdown構文向けの独自のレンダリングクラス
class BlogRender < Redcarpet::Render::HTML
  include Rouge::Plugins::Redcarpet
end

続いて、新しいレンダリング用クラスをRedcarpet::Markdownの新しいインスタンスに渡す必要があります。

<%= Redcarpet::Markdown.new(BlogRender, fenced_code_blocks: true).render(@article.content).html_safe %>

fenced_code_blocksオプションによって、GitHubにありそうなコードブロックを使えるようになります。後はCSSスタイルを更新すれば完了です。私のGistにあるCSSをコピペすれば、私のブログと同じテーマで表示されるようになります。

関連記事

Slackのシンタックスハイライト付き「スニペット機能」は使わないと損

Rails tips: DeviseとOmniAuth認証でLinkedIn機能にサインインする(翻訳)

$
0
0

概要

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

Rails tips: DeviseとOmniAuth認証でLinkedIn機能にサインインする(翻訳)

本記事ではRuby 2.5 + Rails 5.1.4を用います。執筆時点ではいずれも最新です。まずはGemfileにDevise gemを追加しましょう。

gem 'devise'

続いてbundle installを実行してPCにgemをインストールしなければなりません。しかしインストールはこれだけでは終わりません。以下のコマンドを実行して、Deviseのイニシャライザファイルと翻訳ファイルを生成しなければなりません。

rails generate devise:install

モデルの生成

これでモデルを生成できるようになりました。認証データを持つモデル名はUserとするのが普通なので、この名前にします。

rails generate devise user

上のコマンドでDeviseがモデルのクラスとマイグレーションとカラのテストを生成してくれます。Userモデルの内容は次のようになっています。

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
end

アプリではLinkedInの認証のみを許可したいので、不要なコードを削除します。

class User < ApplicationRecord
  devise :trackable, :omniauthable
end

マイグレーションの内容は次のようになっているはずです。

# frozen_string_literal: true

class DeviseCreateRecruiters < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      t.string :email,              null: false, default: ""

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.inet     :current_sign_in_ip
      t.inet     :last_sign_in_ip

      # LinkedIn
      t.string :provider
      t.string :uid, unique: true

      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
  end
end

ここではメールとパスワードによる認証は使いたくありませんが、後で使えるようにアプリにはユーザーのメールを保存しておきたいと思います。データベースにテーブルを作成するために以下のマイグレーションを実行しなければなりません。

bundle exec rake db:migrate

不要なカラムが残っていることに気づいた場合は、マイグレーションをロールバックしてファイルを修正し、再度マイグレーションを実行できます。

bundle exec rake db:rollback STEP=1

OmniAuthストラテジーによるLinkedIn認証では、providerカラムとuidカラムを使います。

LinkedInの設定

認証にはomniauth-linkedin gemを使いますので、最初にインストールしておきましょう。Gemfileに以下の行を追加してbundle installを実行します。

gem 'omniauth-linkedin'

今度はアプリでconsumer_keyconsumer_secretキーが必要になります。アプリにこれらがない場合はこちらの記事を参考に作成しておいてください。

config/initializers/devise.rbにあるDevise gemイニシャライザを開いて、credenntialを追加します。

config.omniauth :linkedin, "consumer_key", "consumer_secret"

これで、Userモデルを更新して認証プロバイダとしてLinkedInを使うよう指定できるようになります。

class User < ApplicationRecord
  devise :trackable, :omniauthable, omniauth_providers: %i[linkedin]
end

ルーティング

config/routes.rbにdevise_for :usersエントリがあれば、Deviseによって自動的に以下の2つのパスが追加されます。

  • user_omniauth_authorize_path(provider)
  • user_omniauth_callback_path(provider)

これで、LinkedIn認証を開始するためのリンクを追加できます。

<%= link_to "Sign in with LinkedIn", user_linkedin_omniauth_authorize_path %>

このリンクをクリックするとLinkedIn認証ページにリダイレクトされます。今度は、コントローラでリクエストを受けて、指定のユーザーをアプリ側で認証する準備をしなければなりません。

認証コントローラ

認証アクションを扱うために、別のコントローラを作成することにします。このコントローラの名前はAuthorizationsControllerにしました。LinkedInリクエストを処理するために、linkedinメソッドとfailureメソッドの追加が必要です。1番目のメソッドはレスポンスが成功した場合を扱い、2番目は失敗の場合を扱います。

class AuthorizationsController < Devise::OmniauthCallbacksController
  def linkedin
  end

  def failure
    redirect_to root_path
  end
end

上のコードが機能するには、ルーティングファイルを更新して、LinkedIn認証に使うコントローラがどれであるかをDeviseに伝えなければなりません。

devise_for :users, controllers: { omniauth_callbacks: 'authorizations' }

それでは、ユーザーの作成と検索を担当するコードを実装しましょう。

def self.from_omniauth(auth)
  where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
    user.email = auth.info.email
  end
end

コントローラを更新して、認証が完全に動作するようにしましょう。

class AuthorizationsController < Devise::OmniauthCallbacksController
  def linkedin
    @user = User.from_omniauth(request.env["omniauth.auth"])

    sign_in_and_redirect @user, event: :authentication
  end

  def failure
    redirect_to root_path
  end
end

これでおしまいです。LinkedInネットワーク経由でユーザーを認証できる非常にシンプルな基本アプリを作りました。保存されるのはメールだけです。名前や画像といった他のデータを集めることもできます。可能性は無限なので、何をするかはあなた次第です。

ユーザーが認証されれば、コントローラやビューでcurrent_userを介してアクセスできるようになります。

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

[Rails] Devise Wiki日本語もくじ1「ワークフローのカスタマイズ」(概要・用途付き)

週刊Railsウォッチ(20180622)Railsの需要未だ巨大、Unicode 11.0リリース、WebDriverがW3Cで勧告、Flutter.io、2封筒問題ほか

$
0
0

こんにちは、hachi8833です。私が気まぐれに応援するサッカーチームは必ず負けるので、どこも応援しないようがんばります。

各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
まだ検討段階ですが、週刊Railsウォッチの刊行日を月曜日に移動するかもしれません

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

特記ない限りRails 6.0向けです。

既存association削除時の探索をハッシュに変えて高速化

# activerecord/lib/active_record/associations/collection_association.rb#L391
         def remove_records(existing_records, records, method)
           records.each { |record| callback(:before_remove, record) }

           delete_records(existing_records, method) if existing_records.any?
-          records.each { |record| target.delete(record) }
+          hashed_records = records.group_by { |record| record }
+          target.select! { |record| !hashed_records[record] }

           records.each { |record| callback(:after_remove, record) }
         end

GDPR対応でユーザーや「いいね👍」をごっそり削除することになった人、いるよね? dependent: destroyオプションのアルゴリズムが更新されて(計算量が)二次から一次に削減されました。これ大好き😋
公式ニュースより大意


つっつきボイス:eachで全回しでdeleteしてたのを、group_byしてからhashed_recordsで削除したと」「quadratic(二次)からlinear(一次)って、ここでは計算量のことでいいんですよね?」「ですね🧐O(n^2)からO(n)に削減されたやつ」「お、例のsgrifさんが『self.target -= recordsだけでいけるんじゃ?』ってコメントしてる🤔」「おー、確かにこの書き方は演算子がオーバーライドされてればできるな: 面白い😋」

参考: ランダウの記号 - Wikipedia
参考: 一次方程式 - Wikipedia — linear equation
参考: 二次方程式 - Wikipedia — quadratic equation

storeアクセサで従来のprefixの他にsuffixも使えるようになった

# 同PRより
store :settings, accessors: [ :two_factor_auth ], coder: JSON, _prefix: true
# => accessor will be model.settings_two_factor_auth
store_accessor :settings, :secret_question, _prefix: 'config'
# => accessor will be model.config_secret_question
store :settings, accessors: [ :login_retry ], _suffix: 'setting'
# => accessor will be model.login_retry_setting

つっつきボイス: 「おー、storeにsuffixもねー: 既存のデータベースでsuffixを使うやつがあるんだろうし、prefixが使えるならsuffixも欲しいというのはまあワカル」「指定したsuffixが自動で追加されるんですね」「自分はあんまり使わないかなー?😆」

Active Modelのデフォルトのエラーメッセージ表示方法を改良

#{attribute} #{message}#{message}, full_messageに変更し、以下のどれでも使えるようにしつつ高速化も図ったようです。

en:
  errors:
    format: '%{message}'
en:
  activemodel:
    errors:
      models:
        person:
          format: '%{message}'
en:
  activemodel:
    errors:
      models:
        person:
          attributes:
            name:
              format: '%{message}'

つっつきボイス:full_messageがオーバーライドできるようになって、しかもi18nなのか!そういえば今まではデフォルトのエラーメッセージが生書きされてたような気がする」「この改修はどの辺がうれしいんでしょうか?」「今まではデフォルトのエラーメッセージが完全に英語の語順になっちゃってたんで、日本語環境だとeachで回してゴニョゴニョしないといけないとか、いろいろ使いにくかったんですよ」「あー!なるほど😲ローカライズでもメッセージ内のプレースホルダーの位置なんかを言語によって変えないといけないけど、まさにその問題か」「そのあたりをi18nでもう少し何とかできるようになったということのようだ」「上はyamlファイルなんですね?」「ですです: i18nのfull_messageのテンプレートをyamlでオーバーライドできるようになったということでしょうね: 嬉しい人には嬉しいのかも?」「私は嬉しいです😄」

developmentモードのeager loadingを修正

Rails 5.1/5.2でconfig.eager_load = trueするとサーバーがロックすることがあったそうです。

# actionpack/lib/action_dispatch/journey/routes.rb#L51
       def ast
         @ast ||= begin
           asts = anchored_routes.map(&:ast)
-          Nodes::Or.new(asts) unless asts.empty?
+          Nodes::Or.new(asts)
         end
       end

つっつきボイス:config.eager_load = trueでロックとか恐ろしいw💀」「修正はめちゃシンプル: astだから抽象構文木ですね」「どうやらこいつが何かのはずみで同時に呼ばれるとヤバかったんだろうなー: コミットメッセージを見ると、Railsエンジンが複数ある場合にRouteSetsが空になる問題だったのか!」「😲」「自分はあまり踏まなそうなバグかな?」

参考: Rails エンジン入門 | Rails ガイド
参考: 抽象構文木 - Wikipedia

テスト中のパラメータエンコーディングにto_queryではなくRackを使用

# actionpack/lib/action_controller/test_case.rb#L104
           case content_mime_type.to_sym
           when nil
             raise "Unknown Content-Type: #{content_type}"
           when :json
             data = ActiveSupport::JSON.encode(non_path_parameters)
           when :xml
             data = non_path_parameters.to_xml
           when :url_encoded_form
-            data = non_path_parameters.to_query
+            data = Rack::Utils.build_nested_query(non_path_parameters)
           else
             @custom_param_parsers[content_mime_type.symbol] = ->(_) { non_path_parameters }
             data = non_path_parameters.to_query
           end

お馴染みAaron Pattersonさんによる修正です。


つっつきボイス: 「ちょうどMatz vs Aaron Patterson対談の記事があったので貼ってみました↑💎」「GitHub Satellite!😃HTTPのクエリパラメータのあたりで、to_queryだとエンコーディング前にソートされてたのをRackのに切り替えて修正したということかな」

associationのコレクション更新に-=を使うよう修正

これは自分で見繕いました。

この修正によってメモリ上で変更されたassociationもloadedとしてマーキングされるので、次回のアクセスでデータベースクエリの発生を回避できる。
同commitより大意

# activerecord/lib/active_record/associations/collection_association.rb#L399
         def remove_records(existing_records, records, method)
           records.each { |record| callback(:before_remove, record) }

           delete_records(existing_records, method) if existing_records.any?
-          hashed_records = records.group_by { |record| record }
-          target.select! { |record| !hashed_records[record] }
+          self.target -= records

           records.each { |record| callback(:after_remove, record) }
         end

つっつきボイス: 「あ、これちょうど今回の一番上の#29939の続きじゃないですか❤️」「ほんとだ: sgrifさんのサジェスチョンどおりですね🙂」「どこまで最適化されてるかはわかりませんが😆」「😆」

Rails

Railsへの需要は未だ巨大

サクッと読める記事です。


つっつきボイス: 「実際に使われている既存のRailsアプリがとても増えたし、エンジニアの需要も普通に多いというか減ってないという実感」「LinkedInの求職の件数(サンフランシスコのベイエリアなど)をチェックするというのはうまいですね: 言語ではRubyは必ずしも上位ではないけど、フレームワークでの求職数は圧勝しているというあたり」「ところでMatzがRailsネタでツイートするのは割と珍しいかも?」「確かにー」

tokaido: RailsアプリをmacOSアプリにする

Railsガイドに載ってました。東海道?


つっつきボイス: 「単独のMacアプリにできるということみたいです: こういうのがあったって知りませんでした」「自分も😆」「よく見ると最後の更新が3年前…Railsをまるっと飲み込んだら結構なサイズになりそう?」

AMPって今どうよ(Frontend Weeklyより)


つっつきボイス: 「AMPはもう普通に使われてると思うし: あと最近AMP JSも使えるようになるみたいだし」「あ、そうだった😲」

参考: AMP とは – AMP


ampproject.orgより

GraphQLは未来なのか?(Frontend Weeklyより)

「GraphQL is not your data model」という見出しがとりあえず気になりました。


つっつきボイス: 「日本だとGraphQLに夢を見ている人が多い印象🌠」「😆」「日本はフロントエンドとサーバーサイドを別の人がやってることが多いという事情もあるし: もしかするとソシャゲ界隈でGraphQL使いたい人が多いかもね🕶」

RailsでGraphQL APIをつくる: Part 1 – GraphQLとは何か(翻訳)

アセットパイプラインディレクトリの脆弱性


つっつきボイス: 「Herokuの記事でした」「productionでconfig.assets.compile = trueしなければ大丈夫だそうです(オレオレアプリもセーフ🤑)」「アセットのコンパイルって、ごくごく稀にオンにしてみることがあったりしたナ: コンパイルがうまく動かなかったときとか😆」「やったことなかった…」「Directory Traversalはなかなかエグい☠️けど、アプリがコンテナに置かれててchrootしているとかならそこまで深刻ではないのかもしれないけど、ね」

参考: ディレクトリトラバーサル - Wikipedia

git push -fはヤバい

参考: git push -f をやめて --force-with-lease を使おう - Qiita


つっつきボイス: 「実はこの間Gobyちゃんのリポジトリでgit push -fやらかしちゃって😓」「マジでw🤣」「今後やらかさないようgit config --system receive.denyNonFastForwards trueを唱えました」「master権限持っててぶっ壊したらどうしようもないけど、開発者ならprotectedブランチ的なものを立てるとか、masterブランチとdevelopブランチに直接pushできないようプロテクトしておくべきではあったかな😎」「そうでした😵」

「最近よく聞くのは、『featureブランチでは基本的にpush -fしない』という運用ですね: 重複更新が入ってもとりあえずコミットでつなげて、最後マージするときにsquashする」「なるほど!」「もちろんプロジェクト次第ですけどね: 個人的には、そのfeatureブランチを自分しか触ってないならpush -fしてもいいんじゃね?とは思うけど」

「いつもは自分のブランチでpush -fしてたんですが、今回はうっかりst0012さんのPRブランチにpushしちゃって💦」「人のブランチにgit push -fは、そりゃ戦争🔫💣ものだな🤣」「さすがにムッてたと思います…ほんとすみません🙏」「最初にfeatureブランチができたらそれ以外の人はpushできないようになったりするといいかもしれないですけどね😎、まあgit push -fは使わざるを得ないときもあるんでドンマイ」「😃」

Goby: Rubyライクな言語(2)Goby言語の全貌を一発で理解できる解説スライドを公開しました!

Railsアプリのデータベースロジックを失わない方法(RubyFlowより)

# 同記事より
app/
  sql/
    application.sql
    user.sql
    product.sql

つっつきボイス: 「sql/ディレクトリ掘ってるのが気になって」「それは普通に生SQLを置くところでしょうね: ははぁこれはマイグレーションについての記事か」「というと?」「通常のdb:migrateに限らない、データベースの更新とかも含むマイグレーションで使う生SQLはこういうところに置こうよという趣旨」「😃」

「ついでですが、これみたいに1回しか実行しない生SQLをどこに置くかっていつも悩ましいんですよ」「確かにー」「1回こっきりならスニペットで書くか、あるいはActiveRecord使うならrakeタスクにするとか: SQL一発だけならこの記事みたいな管理方法もありかなとは思う」

5つの手順で完璧なenumを作る(RubyFlowより)


つっつきボイス: 「完璧なenum🤣: まずはarrayじゃなくてhashでやろうとある、自分もhashでやるのが好きだし😋」

# 同記事より
class Catalog < ActiveRecord::Base
  enum localization: [:home, :foreign, :none]
end
0 -> home
1 -> foreign
2 -> none

「そうそう、業務システムなんかで、enumをデータベースに0とか1の値で入れることにこだわるケースがあったりするんですが、それだとソース見ないと値の意味がわからないんじゃね?って思うことしばしば😆」「それ確かにー」「自分はstringで保存したい派: PostgreSQLだと確か文字列enumが使えて、MySQLにもenum型が確かあって、そういうのを使えばできるし」

# 同記事より
class Catalog < ActiveRecord::Base
  enum localization: { home: 0, foreign: 1, none: 2 }
end

「お、この記事はPostgreSQL enum使ってるじゃん↓: エライ!」「これだとデータベース内部は数値でもクエリでは文字列が使えるんですね」「そうそう、やるなら自分はぜひこっちにしたい😋」「😃」

# 同記事より
class AddStatusToCatalogs < ActiveRecord::Migration[5.1]
  def up
    execute <<-SQL
      CREATE TYPE catalog_status AS ENUM ('published', 'unpublished', 'not_set');
    SQL
    add_column :catalogs, :status, :catalogs_status
  end

  def down
    remove_column :catalogs, :status
    execute <<-SQL
      DROP TYPE catalog_status;
    SQL
  end
end

「で究極のソリューションは、と: おー、これはたぶんPostgreSQLの機能だと思うんですが、型を追加してますね、CREATE TYPE catalog_status AS ENUMで」「😃」「createのenum定義に直接型を書く代わりに、catalog_statusという名前を付けてそれを使ってマイグレーションをすると: これは確かにキレイに書ける」「おー、ぽすぐれ側でやろうということですね」「まあ今度はこのcatalog_statusって定義はどこなんだ?と思われるかもしれないし、ここまでぽすぐれに頼っていいんだろうかとも思いますが😆」

# 同記事より
# マイグレーション
class AddStatusToCatalogs < ActiveRecord::Migration[5.1]
  def up
    execute <<-SQL
      CREATE TYPE catalog_status AS ENUM ('published', 'unpublished', 'not_set');
    SQL
    add_column :catalogs, :status, :catalogs_status
    add_index :catalogs, :status
  end

  def down
    remove_column :catalogs, :status
    execute <<-SQL
      DROP TYPE catalog_status;
    SQL
  end
end

# ValueObject:
class CatalogStatus
  STATUSES = %w(published unpublished not_set).freeze

  def initialize(status)
    @status = status
  end

  # what you need here
end

RailsアプリでReactをホットリロードに使う(RubyFlowより)


同記事より


つっつきボイス: 「ホットリロードは最近のWebアプリではよく見かけますね: フロントだと欲しいヤツ」「Webpackも使ってるし」

Railsのcredential

先週Railsのcredentialの話が出てたので。


つっつきボイス: 「この間の話は要するにcredentialをリポジトリにコミットする場合にマスターキーをチームでどううまく扱うかという問題でしたが、この記事みたいにマスターキーをAWSのKMSに保存すれば、KMS内の生の鍵には開発者のアクセスを許す必要がないし、KMSに置かれた鍵をIAM経由で使うようにすれば、プロジェクトから抜けたメンバーをIAM Groupから外すだけでマスターキーにアクセスできなくなるということですね」「😃」

Rails APIとJWT認証とVuejsでSPAする(RubyFlowより)


つっつきボイス: 「JWTはWeb界隈では新し目の認証方式ですね」



jwt.ioより

最近よかったRuby/Rails記事: 2018年前半(RubyFlowより)


つっつきボイス: 「ざっと見たところこれまでウォッチで既に取り上げたり翻訳したりした記事も載ってて、ちょっとだけ『勝った』感ありました😆」「😆」

RabbitMQはSidekiqと同等以上だと思う: 前編(翻訳)

search_flip: ElasticsearchでクエリをチェインするDSL(RubyFlowより)

Finally, SearchFlip supports ElasticSearch 1.x, 2.x, 5.x, 6.x. Check section Feature Support for version dependent features.
同リポジトリより


つっつきボイス: 「よくあるやつなのかなと思って」「お、これElasticsearchの6.xに対応してるのがエライですね!: たしかこの間のRails Developers Meetupだったかな、Elasticsearch向けの何かのRubyのライブラリがElasticsearchの5.xまでしか対応してなくて新しい機能はまだ使えないねみたいな話があったと思ったんだけど、6.xに対応しているという理由でこのgemを使う人がいるかも?」

ActiveSupport::MessageEncryptor


つっつきボイス: 「そうそう、Rubyでencrypter/decrypter使うとだいたいこんな感じになりますね: しかしActiveSupportの割にはローレベルな書き方😆」

# 同記事より
ENCRYPT_CIPHER = 'aes-256-cbc'
ENCRYPTOR = begin
  key_len = ActiveSupport::MessageEncryptor
            .key_len(ENCRYPT_CIPHER)
  key = ActiveSupport::KeyGenerator
        .new("<環境から挿入したkey>")
        .generate_key("<環境から挿入したsalt>", key_len)
  ActiveSupport::MessageEncryptor.new(
    key,
    cipher:     ENCRYPT_CIPHER,
    digest:     'SHA1',
    serializer: Marshal,
  )
end

「その後に『デフォルト値には頼らない』って書いてますけど、これは使う処理系やライブラリによってデフォルト値が違うことがあるんですよ」「というと?」「以前一度はまったのが、PHPのライブラリでencryptしたものをRubyのライブラリでdecryptしようとしたら、どっちかのライブラリでゼロフィル(ゼロ値で埋めること)やられてて復元できなかったことあったんですよ😭: たしか相当無理やりに突破した」「ありゃ~」「ライブラリを素直に無加工で使ってくれればいいものを、そうやってデフォルト値変えられたりするとつらい」

「そういえば私も正規表現ライブラリを異なる言語間で共通で使おうとして似たようなハマり方したことありました😢」「それもあるある: eregなのかpregなのかとかね、といっても最近pregしか見かけないけど😎」

参考: preg_match関数と正規表現の理解を再度見直す - Qiita

その他Rails



trello.comより

Ruby trunkより

提案: 第2のGCヒープ「Transient heap」

ko1さんによる提案です。

mallocが管理するヒープに代わる第2のGCヒープ「Transient heap」のMRIへの導入を提案します。利用されるGCアルゴリズムは「世代的」「コピー」GCアルゴリズムに近いものです。これによってmallocされたヒープの問題を軽減できると見込みます。
同issueより大意


つっつきボイス: 「mallocの手前に別のメモリ管理レイヤを置くという感じ: 結局OSのメモリ管理とRubyのメモリ管理という2層があるから、mallocだといろいろうまくいかないところがあるんでしょうね」「ふーむ🤔」「今日の社内勉強会でキャッシュのしくみの話をしたけど、キャッシュの場合と似た感じで、同じ仕事をする層が複数あるとそれぞれがてんでに局所最適に動作して全体としてうまくいかない、なんてことが起きがちですね」

Ruby: mallocでマルチスレッドプログラムのメモリが倍増する理由(翻訳)

Time.strptime%jを使うユリウス暦で正しく動かない

# 同issueより
require "time"
require "date"

### Works for Dates ###
parsed_date = Date.strptime("15300", "%y%j")
expected_date = Date.new(2015, 10, 27)
# Does not raise
raise "dates not equal" if expected_date != parsed_date


### Does not work for Time ###
parsed_time = Time.strptime("15300", "%y%j")
expected_time = Time.new(2015, 10, 27)
# Raises
raise "times not equal" if expected_time != parsed_time

つっつきボイス:Dateだと動くのにTimeだと動かないというバグ」「Julian date=ユリウス暦の日付」

参考: ユリウス暦 - Wikipedia

ローマ教皇グレゴリウス13世が1582年、ユリウス暦に換えて、太陽年との誤差を修正したグレゴリオ暦を制定・実施したが、今でもグレゴリオ暦を採用せずユリウス暦を使用している教会・地域が存在する。
Wikipediaより

Timeにタイムゾーンを設定する公式のAPIが欲しい

# 同issueより
>> ENV['TZ'] = 'America/New_York'
>> Time.now.zone
=> "EDT"
>> ENV['TZ'] = 'Europe/London'
>> Time.now.zone
=> "BST"

ActiveSupport::TimeWithZoneでやってるようなのを定めたいそうです。

参考: ActiveSupport::TimeWithZone


つっつきボイス: 「タイムゾーンは誰がというかどこで一元管理するのかという問題がそもそもあるんですけど、最終的にはOSの環境変数が頼りですね: この機能はActiveSupportから持ってきてもいいと思う」

Ruby

StripeのRuby lint


sorbet.runより


つっつきボイス: 「Stripeはこの辺に力入れてますね: RubyKaigiのこの発表見られなかった😢」「この間のウォッチでもちょっとだけ扱いましたが一応」「sig(foo: Integer).returns(String)みたいにシグネチャを指定するのか: Matz好みでないやり方😆」「構文変わっちゃいますもんね…」

# sorget.runより
class A
  sig(foo: Integer).returns(String)
  def bar(foo)
    foo.to_s
  end
end

def main
  A.new.barr(91)
  A.new.bar("91")
end

グループ名がない場合にスキップする修正

Windows対応のようです。

mruby-meta-circularとは


つっつきボイス: 「RiteVMってmrubyのVMなんですね: Riteというとつい春の祭典を連想してしまいます🙂」

参考: mruby Virtual Machine(RiteVM) — mrubook 1.0 documentation

「今回のRubyKaigi 2018を見てて思ったんですが、mrubyって組み込み系という印象がそれまで強かったけど、最近はむしろH2Oみたいなミドルウェア系での活躍が目立っててそちらで見直されてる感」「確かにH2Oの成功がアピールした感じありますね😃」

参考: H2O - the optimized HTTP/2 server


h2o.examp1e.netより

別件ですがこんなのも。

dining-table: Rubyでテーブルをきれいに作るgem

# 同リポジトリより
class CarTableWithConfigBlocks < DiningTable::Table
  def define
    table_id = options[:table_id]  # custom option, see 'Options' above

    presenter.table_config do |config|
      config.table.class = 'table-class'
      config.table.id    = table_id || 'table-id'
      config.thead.class = 'thead-class'
    end if presenter.type?(:html)

    presenter.row_config do |config, index, object|
      if index == :header
        config.tr.class = 'header-tr'
        config.th.class = 'header-th'
      elsif index == :footer
        config.tr.class = 'footer-tr'
      else  # normal row
        config.tr.class = index.odd? ? 'odd' : 'even'
        config.tr.class += ' lowstock' if object.stock < 10
      end
    end if presenter.type?(:html)

    column :brand
    column :stock, footer: 'Footer text'
  end
end

つっつきボイス: 「名前が😆」「一度こういう感じに作っておけばHTMLテーブルの他にも後でcsvとかxlsxとかいろんな形式で出せるのか: カスタマイズしたテーブルを扱うときなんかにちょっといいかも😋」

MRuby-Zest: mrubyのオーディオGUIフレームワーク

長ったらしいif-else条件のリファクタリング(RubyFlowより)

# 同記事より
# Before
# Metrics/MethodLength: Method has too many lines. [13/10]
def foo1(number)
  if number == 1
    'one'
  elsif number == 2
    'two'
  elsif number == 3
    'three'
  elsif number == 4
    'four'
  elsif number == 5
    'five'
  else
    'many'
  end
end

# After
DICTIONARY = {
  1 => 'one',
  2 => 'two',
  3 => 'three',
  4 => 'four',
  5 => 'five'
}.freeze

def foo2(number)
  DICTIONARY[number] || 'many'
end

つっつきボイス: 「うん、これは定番のリファクタリングですね: よく使うというか最初からこう書くし」「そうでしたか!😲」「DICTIONARYを作ることで意味が明確になるし、条件を後で変えたり追加したりするのも楽だし、何より読みやすい❤️」

「↓こんなふうにlambda使うのはちょっとスゴイな😉、でもこれも自分使ってるわ」「😀」「こうやってlambda使ったり出力をevalしたりするとかすると最強感💪」

# 同記事より
# After
module SearchService2
  PARAMS_MAP = {
    'id' => ->(value) { { id: value } },
    'first_name' => ->(value) { { 'first_name' => value } },
    'last_name' => ->(value) { { 'last_name' => value } },
    'email' => ->(value) { { 'email' => value } },
    'city' => ->(value) { { 'city' => value } },
    'gender' => ->(value) { { gender: value } },
    'height_after' => ->(value) { { height: { '$gt' => value } } },
    'height_before' => ->(value) { { height: { '$lt' => value } } },
    'weight_after' => ->(value) { { weight: { '$gt' => value } } },
    'weight_before' => ->(value) { { weight: { '$lt' => value } } },
  }.freeze
...

その他Ruby



つっつきボイス: 「言われてみればそうだったわー😲: あんまり使わないけど…」






TCFM出演?


RubyKaigi 2018での生ペアプロ動画も上がってました。

クラウド/コンテナ/インフラ/Linux

Linux高速化の話題2つ


つっつきボイス: 「最近のLinuxネットワークの詳細は正直よくわからん😆: まともに追いかけてたのはカーネル2.6ぐらいまでだったし」「あー、じゃLinuxも結構がっつり更新されてるんですね」「ちゃんと更新されてますよ😎」

書籍「実践 パケット解析 第3版」

OSI階層とは

このネタ割と前からあったんですね。

参考: OSI参照モデル: 比喩 - Wikipedia

Google I/O 2018全動画

その他クラウド/コンテナ/インフラ/Linux





つっつきボイス: 「これ定期的に出回るネタ😎」「🤣」




SQL

試さないと損するPostgreSQLの機能(Postgres Weeklyより)

AWS RedshitとPostgreSQLはどこが違うか(Postgres Weeklyより)

pgdeltastream: PostgreSQLの更新をリアルタイムに観測(Postgres Weeklyより)


同リポジトリより

PostgreSQLの関数

はてブで見かけました。

JavaScript

AirbnbがReact Nativeをやめた話

はてブなど各所で話題ですね。ついAirBnBと書いてしまいそうです。

↑先越されちゃいました(´・ω・`)。


つっつきボイス: 「これもmizchiさんが早速アンサー記事↓書いてくれてますね」

参考: いつ ReactNative を使っても大丈夫か - mizchi’s blog

flutter.ioとAndroid fuchsia OS

社内Slackで密かに見守られています。Dart言語ベースだそうです。


より

参考: Dart - Wikipedia

Flutterが何らかの形でFuchsia OSに採用されるかもしれないとも。

参考: iOSとAndroid、両方のアプリを一度に作れちゃう優れモノ! Googleの隠れた戦略が見える「Flutter SDK」 | ギズモード・ジャパン

Parcel 1.9.0リリース(JSer.infoより)

その他JavaScript


CSS/HTML/フロントエンド/テスト

「WebDriver」がW3CでRECに

参考: 「WebDriver」がW3Cの勧告に到達。Webブラウザのテスト自動化などを実現 - Publickey

GDPRどころじゃないのかも

Creative Commonsも反対を表明しています。つかTechRachoも他人事ではありません。

参考: ハイパーリンクを貼るだけで著作権料がかかる通称「リンク税」がEUで導入されようとしている - GIGAZINE

CSS+JSのアニメーションロゴ

Unicode 11.0リリース


blog.unicode.orgより


つっつきボイス: 「とりあえず絵文字増えましたね❤️」「海老🍤」

参考: Emoji Recently Added, v11.0


unicode.orgより

リグレッションとデグレーションの違い

その他CSS/HTML/フロントエンド/テスト


言語よろずの間

マイクロソフトのBlazorとは

GoからPHPに帰ってきた話

話は少し逸れますが、先日のRubyKaigi 2018のドリンクアップでPHP開発者の方とお話しする機会があり、PHPでは英語情報を探さないといけない状況がほぼまったくないと伺いました。

書籍「Go言語でつくるインタプリタ」の続編「Go言語でつくるコンパイラ」がリリース

↓参考まで: こちらは例のGobyちゃんに最も強く影響を与えたリファレンスです。

SHA1コリジョン

各所でバズってますね。

モンティ・ホール問題より手ごわい「2封筒問題」

BPS社内でも盛り上がりました。

↑この問題について最も引用されているという短い論文です。読んでもいませんが、三浦俊彦『可能世界の哲学』によると、「交換すると双方が得になるのは事前期待値が無限大の場合に限られる」ことがこの論文で示されているんだそうです。

同書によると、この2封筒問題を扱うサイトや書籍は必ず炎上するらしく、出版界では密かに疎まれているんだそうです。Railsウォッチもついに炎上?

参考: 数学 - 二封筒問題
参考: 可能世界論 - Wikipedia

同書で紹介されていた以下の本もちょっと気になります。命名の助けになるか混乱の元になるかわかりませんが。

参考: 藤川直也『名前に何の意味があるのか 固有名の哲学』 - logical cypher scape

Rust 1.27リリース

// 同記事より
// SIMDなし
let lots_of_3s = (&[-123.456f32; 128][..]).iter()
    .map(|v| {
        9.0 * v.abs().sqrt().sqrt().recip().ceil().sqrt() - 4.0 - 2.0
    })
    .collect::<Vec<f32>>();

// SIMDあり
let lots_of_3s = (&[-123.456f32; 128][..]).simd_iter()
    .simd_map(f32s(0.0), |v| {
        f32s(9.0) * v.abs().sqrt().rsqrt().ceil().sqrt() - f32s(4.0) - f32s(2.0)
    })
    .scalar_collect();

参考: SIMD - Wikipedia

その他言語


たぶん反語?
私は.vimrcは最小限しか設定してません。



あらゆるGoリポジトリにgofmt -w .をかけてまわるようです。



その他

3D XPointメモリとは

macOSの「クイックルック」は残る

SQLite使ってるって初めて知りました。

参考: macOSの「クイックルック」は暗号化ドライブのファイルまでキャッシュしており、データは永続的に保管されいつでも閲覧できる - GIGAZINE

人感センサー

これ改造がとっても楽なんだそうです。

その他のその他


純粋に欲しいです。けどCD-ROM販売…









つっつきボイス: 「あーこれ何だっけ??」「タイガー式計算機ですね: 自分も実物見たことありませんが😭」

朝永振一郎のエッセイでこれと朝晩格闘してたという記述を見たぐらいでした。

参考: 機械式計算機 - Wikipedia



↑後はワイヤレスになってくれれば完璧ですね。


番外

氷河期の写実的な壁画は自閉症スペクトラムの表れという説

参考: 氷河期の壁画が驚くほどに写実的だったのは「描き手が自閉症だったから」という説明 - GIGAZINE

明和電機健在

参考: 明和電機 - Wikipedia

特大ステッカー

変わり筐体

極太マジックハンド

過酷な環境で酸素を生むバクテリア

参考: 人類の火星移住で酸素の供給源として活躍するかもしれないバクテリアが発見される - GIGAZINE

WiFiで壁の向こうを透視

既にWiFiでの室内の物体の位置特定ってできてた気も。

材料は星

極大から極小までをインタラクティブに


htwins.netより

元になった以下の「Powers of Ten」はかなり昔にIBMが制作した定番の教育コンテンツだそうです。


つっつきボイス: 「やっぱり最後は宇宙ネタ?」「そう決めてるわけではないんですが、何となくで😆」


今週は以上です。

おたより発掘

バックナンバー(2018年度後半)

週刊Railsウォッチ(20180615)TTY gemとHTTPClient gemは優秀、Rubyの謎フリップフロップ、ちょいゆるRubyスタイルガイドほか

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

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

Rails公式ニュース

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

JavaScript Weekly

javascriptweekly_logo_captured

JSer.info

jser.info_logo_captured

Rails: ElasticSearchとRedis Streamsのストリームを使う(翻訳)

$
0
0

概要

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

Rails: ElasticSearchとRedis Streamsのストリームを使う(翻訳)

Redis Listsは、データをメインのデータストアからElasticSearchに移動するジョブのキューとして使えます。時系列データをRedisに残し、かつElasticSearchにコピーする必要がある場合はどうでしょうか。

前回の記事では、国内小売りチェーン向けのRails Webサイトを構築しました。このときは、Redis ListsをElasticSearchへのETLデータキューとして用い、検索頻度の高い郵便番号を記録するためにRedisSortedSetsも用いました。さらに、day_of_weekhour_of_dayによる検索でRedis Stringsによるカウンタを用いました。今回の新たな要件は、各検索とパラメータ(クエリごとの郵便番号や製品)の正確な検索時刻を追えるようにすることです。この機能は、どの製品をどのテンポにストックしておくべきかを顧客が決定する上で役に立ちます。

Redis Streams

通常、この検索データはGETリクエストの一部としてアプリのログに出力され、Logstashを用いてこれをElasticSearchに保存できます。しかし今回はこのデータをStreamsで最初にRedisに保存します。Streamsは、Redis 4.0.xでリリース予定の新しいデータ構造です。

class StoreLocator
  def initialize zipcode:, query:
    @zipcode = zipcode
    @query = query
    ...
  end
  def perform
    # search code here
    record_stats
  end
private
  def record_stats
    key = "search_log:#{Time.now.strftime("%Y-%m-%d")}"
    REDIS_CLIENT.xadd(key, '*', 'zip', @zip, 'query', @query)
  end
end

今回は新しいxaddコマンドを用いてRedisの新しいストリームを作成し、キー/バリューペア(郵便番号とクエリ)を用いて項目をそこに追加します。キーはタイムスタンプを元に1日に1件作成され、ちょうどログのローテーションに似ています。Redis内のデータは次のような感じになります。

# launch redis-cli
xrange search_log:2018-01-08 - +
1) 1) 1515465841276-0
   2) 1) "zip"
      2) "98168"
      3) "query"
      4) "Spilt Light"
2) 1) 1515465842278-0
   2) 1) "zip"
      2) "98114"
      3) "query"
      4) "Wake-up Volcano"
...

xrangeは、ストリームから項目を取得できる新しいコマンドです。-+を指定すると最初から最後まですべての項目を取り出せます。ストリームのIDは、Unixのエポックタイム(msec単位)とシーケンス番号を元に自動生成されます。REDIS_CLIENT.xadd(key, numeric_id, ...)で固有のIDを使うこともできます。

ETLからElasticSearchへ

データ量がかなり莫大なので、直近7日間のデータをRedisに残し、それより古いデータをElasticSearchに移すことにします。Streamsのスキーマは柔軟で、ElasticSearchインデックスと相性のよいさまざまなフィールドが使えます。

xrange

これに対応するElasticSearchインデックス(”search_log:YYYY-MM-DD”など)を作成し、これら項目に対してバッチでループを回します。また、ストリームの項目IDをElasticSearchのドキュメントIDとして指定します。

ES_CLIENT = Elasticsearch::Client.new ...
# app/jobs/
class RedisElasticEtlJob < ApplicationJob
  def perform(num_days=7)
    key = "search_log:#{(Time.now - num_days.days).strftime("%Y-%m-%d")}"
    count = 100
    starting_id = '-'
    while true
      items = REDIS_CLIENT.xrange(key, starting_id, '+', 'count', count)
      items.each do |item|
        # => ["1515258610192-0", ["zip", "98134", "query", "Express Mug"]]
        hash = Hash[item.second.each_slice(2).to_a]
        # => {"zip": "98134", "query": "Express Mug"}
        hash['@timestamp'] = Time.strptime(item.first.to_i.to_s, '%Q')
        ES_CLIENT.index index: key, type: 'default', id: item.first, body: hash
      end
      break if items.count < count
      last_id = items.last.first.split('-')
      starting_id = [last_id.first, (last_id.second.to_i + 1).to_s].join('-')
    end
  end
end

または、REDIS_CLIENT.xrevrange('search_log:YYYY-MM-DD', '+', '-'を用いて項目を逆順で取得する方法も考えられます。Redisのデータは、別のストリームキーにTTLを設定するか、REDIS_CLIENT.del(''search_log:YYYY-MM-DD'')を手動実行することで削除できます。

xread

RedisからElasticSearchへのリアルタイムデータパイプラインがさらに欲しい場合はどうすればよいでしょうか?デイリージョブをスケジューリングしなくても、新しいxreadコマンドでデーモンを構築できます。

class RedisElasticStreamConsumer
  def perform
    while true
      key = "search_log:#{Time.now.strftime("%Y-%m-%d")}"
      data = REDIS_CLIENT.xread('BLOCK', 5000, 'STREAMS', key, '$')
      # => [["search_log:2018-01-07-21-35", [["1515389726944-0", ["zip", "98178", "query", "Red Select"]]]]]
      hash = Hash[data.first.second.first.second.each_slice(2).to_a]
      # => {"zip"=>"98178", "query"=>"Red Select"}
      id = data.first.second.first.first
      hash['@timestamp'] = Time.strptime(id.to_i.to_s, '%Q')
      ES_CLIENT.index index: key, type: 'default', id: id, body: hash
    end
  end
end

この方法で困難なのは、ElasticSearchでドキュメントを作成するスピード(ディスクアクセス)よりも、Redisがストリームに項目を記録するスピード(RAMアクセス)の方が遥かに上である点です。ジョブをスケジューリングする方がこの困難が和らげられますが、これはどちらかというと既存のETLプロセスに向いています。

データを用いる

データにアクセスするために、データソースとしてRedisとElasticSearchのいずれかを選択するロジックを別のクラスにカプセル化します。

class SearchDataSelector
  def initialize date:
    @date = date
  end
  def perform
    elasticsearch if @date > Date.today - 7.days
    redis_streams
  end
private
  def redis_streams
    REDIS_CLIENT.xrange(@date, ...)
  end
  def elasticsearch
    ES_CLIENT.search(index: @date, ...)
  end
end

必要なデータをRedisとElasticSearchの両方から特定の期間について扱えるようにするには、このコードにもう少し手を加える必要があるでしょう。ElasticSearch Kibanaを使えば、ElasticSearchでデータを興味深い形でビジュアル表示できます。上述のLogstashでは、Redis内のデータにアクセスする入出力プラグインが使えます。現時点でサポートされているのはリストとチャンネルのみですが、いずれStreamsに統合されるものと期待しています。

Streamsのその他のコマンド

xlen

xlen search_log:2018-01-08とすると、ストリーム内の項目数を取れます。日付スタンプを持つキーをループすれば、日付と検索件数を表示するテーブルをUIで表示できるでしょう。

maxlen

REDIS.xadd('last_1000_searches', 'maxlen', '~', 1000, '*', 'zip', zip, 'query', query)とすると、長さの上限を指定してストリームを作成できます。~を指定することで、直近の約1000項目をRedisに保持してパフォーマンスを上げられます。これによって、私たちの顧客に直近の検索結果を表示できるようになります。

参考リンク

関連記事

Rails: RedisキャッシュとRackミドルウェアでパフォーマンスを改善(翻訳)

RabbitMQはSidekiqと同等以上だと思う: 前編(翻訳)


Rails: Kubernetes内のRubyプロセスをrbspyでデバッグする(翻訳)

$
0
0

概要

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

Rails: Kubernetes内のRubyプロセスをrbspyでデバッグする(翻訳)

コンテナ化されていないアプリのデバッグはまったくもって簡単です。ホストにssh接続してプロセスに対してrbspyだのstraceだのgdbだのを駆使するもよし、rails consoleでproduction環境で何かを再現するもよしです。

Kubernetesだとデバッグがつらくなる理由

プロセスがコンテナ内で実行されていると、デバッグが少しばかり面倒になります。最初にdocker exec--interactive --ttyの追加も忘れずに!)でコンテナに入る必要がありますし、実行中のプロセスのinspectはコンテナ内でないとできません。

Docker内でrbspyを使おうとする場合はさらにトリッキーになります。ptrace(rbspyが依存しているシステムコールの一種)はデフォルトではコンテナ内で利用できないからです。--cap-add=SYS_PTRACEを指定してコンテナを実行しておく必要があり、そうしておかないとdocker execで入ってrbspyを実行できません。

Kubernetesの便利スニペット

続いて、Kubernetesについてちょっとしたコツをご説明します。本記事は、私が毎日利用しているスニペットのコレクションをご紹介するという形で書かれており、Rubyプロセスを実行して状態を調べる必要が生じた場合にはこれらのスニペットを使っています。

$ kubectl get pods
NAME                         READY     STATUS    RESTARTS   AGE
secretland-c796bf9df-gmt97   1/1       Running   0          13d
secretland-4a145b44d-6xw11   1/1       Running   0          13d

前回の記事で用いたsecretlandアプリの複製が2つあります。

コンテナに入ってみましょう。

$ kubectl exec -i -t secretland-c796bf9df-gmt97 /bin/bash
root@secretland-c796bf9df-gmt97:/app# ls
Dockerfile  Gemfile  Gemfile.lock  README.md  Rakefile  app  bin  config  config.ru  db  lib  log  package.json  public  script  storage  test  tmp  vendor
root@secretland-c796bf9df-gmt97:/app# bin/rails console
Loading production environment (Rails 5.2.0.rc2)
irb(main):001:0>

次のようにRailsコンソールを直接実行することもできます。

$ kubectl exec -i -t secretland-c796bf9df-gmt97 /app/bin/rails console
Loading production environment (Rails 5.2.0.rc2)
irb(main):001:0>

rbspy

rbspyは、先ごろJulia Evansが公開したRubyプログラム向けのサンプリングプロファイラです。rbspyはRuby世界でここ数年求められていた素晴らしいツールであり、Juliaのおかげでこうした作業がとても快適になりました。

あるKubernetes Pod内で実行中のプロセスに対してrbspyを使う方法をご説明します。最初に、rbspyがptrace(3)に依存していることを知っておく必要があります。ptraceが有効になっているコンテナを実行するには、コンテナにSYS_PTRACE特権を付与する必要があります。Deployment specのsecurityContext.capabilitiesフィールドをご覧ください。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: secretland
  labels:
    app: secretland
spec:
  selector:
    matchLabels:
      app: secretland
  template:
    metadata:
      labels:
        app: secretland
    spec:
      containers:
      - image: kirshatrov/secretland:v1
        name: rails
        ports:
        - containerPort: 3000
        securityContext:
          capabilities:
            add:
            - SYS_PTRACE # <-- この特権

低速なエンドポイントをエミュレーションするために、フィボナッチ数を計算するサンプルアクションを作成しました。このようにして、Ruby世界でどのプロセスが時間を使っているかを観察できます。

新しいDeployment specが配置されれば、そのWebコンテナにexecをかけてrbspyを試せます。少なくとも私の場合、Pumaプロセスのpid1でした。

$ kubectl exec -i -t secretland-8dc689458-jstp2 /bin/bash
root@secretland-8dc689458-jstp2:/app# rbspy record --pid 1
Time since start: 9s. Press Ctrl+C to stop.
Summary of profiling data so far:
% self  % total  name
100.00   100.00  <c function> - unknown
  0.00    79.41  block in start! - /usr/local/bundle/gems/puma-3.11.3/lib/puma/thread_pool.rb
  0.00    20.59  run_internal - /usr/local/bundle/gems/puma-3.11.3/lib/puma/reactor.rb
  0.00    20.59  block in run_in_thread - /usr/local/bundle/gems/puma-3.11.3/lib/puma/reactor.rb

ブラウザで/slowpathエンドポイントを表示すると、このプロファイルが変化します。

% self  % total  name
 70.56   100.00  <c function> - unknown
 29.44    29.44  fibonacci - /app/app/controllers/helloworld_controller.rb
  0.00    67.89  block in start! - /usr/local/bundle/gems/puma-3.11.3/lib/puma/thread_pool.rb
  0.00    32.11  block in spawn_thread - /usr/local/bundle/gems/puma-3.11.3/lib/puma/thread_pool.rb
  0.00    30.97  process_client - /usr/local/bundle/gems/puma-3.11.3/lib/puma/server.rb
  0.00    30.97  block in run - /usr/local/bundle/gems/puma-3.11.3/lib/puma/server.rb
  0.00    29.44  tagged - /usr/local/bundle/gems/activesupport-5.2.0.rc2/lib/active_support/tagged_logging.rb
  0.00    29.44  slow - /app/app/controllers/helloworld_controller.rb
  0.00    29.44  serve - /usr/local/bundle/gems/actionpack-5.2.0.rc2/lib/action_dispatch/routing/route_set.rb
  0.00    29.44  serve - /usr/local/bundle/gems/actionpack-5.2.0.rc2/lib/action_dispatch/journey/router.rb
  0.00    29.44  send_action - /usr/local/bundle/gems/actionpack-5.2.0.rc2/lib/action_controller/metal/basic_implicit_render.rb
  0.00    29.44  run_callbacks - /usr/local/bundle/gems/activesupport-5.2.0.rc2/lib/active_support/callbacks.rb
  0.00    29.44  process_action - /usr/local/bundle/gems/actionpack-5.2.0.rc2/lib/action_controller/metal/rescue.rb
  0.00    29.44  process_action - /usr/local/bundle/gems/actionpack-5.2.0.rc2/lib/action_controller/metal/rendering.rb
  0.00    29.44  process_action - /usr/local/bundle/gems/actionpack-5.2.0.rc2/lib/action_controller/metal/params_wrapper.rb
  0.00    29.44  process_action - /usr/local/bundle/gems/actionpack-5.2.0.rc2/lib/action_controller/metal/instrumentation.rb
  0.00    29.44  process_action - /usr/local/bundle/gems/actionpack-5.2.0.rc2/lib/abstract_controller/callbacks.rb

やった!動きました。

Dockerに到達するには

Dockerデーモンを直接触りたい場合はどうすればよいでしょうか?describe podすると、Dockerデーモンが実行中のNodeが表示されるので、そのインスタンスにsshで接続します。

$ kubectl describe pod secretland-8dc689458-jstp2 | grep Node
Node:           gke-kirs-jobs-default-pool-4a145b44-t690/10.128.0.3
Node-Selectors:  <none>

$ gcloud compute ssh gke-kirs-jobs-default-pool-4a145b44-t690 --zone us-central1-a

kir@gke-kirs-jobs-default-pool-4a145b44-t690 ~ $ docker ps
CONTAINER ID        IMAGE                                                                                                                    COMMAND                  CREATED             STATUS              PORTS               NAMES
fc10153238a0        kirshatrov/secretland@sha256:2e6d8341f51ebe7393d2a7c770c29fbaf959e3317b628d0dc5ebbb19c923d29c                            "rails server -b 0

私はGoogle Cloudを使っていますので、直接sshする代わりにgcloud compute sshを使っています。

gdb

gdbは、MRIコールスタックをダンプする場合(Rubyプロセスが立ち往生してしまう理由を調べたい場合など)に役に立つことがあります。同僚のScottが作成したコールスタックダンプ用のscriptをご覧ください。

私はKubernetes Podからgdbを正しく実行する方法をまだ見つけられずにいます。gdbがRubyのシンボルを探索できないためです。

$ kubectl exec -i -t secretland-8dc689458-jstp2 /bin/bash
root@secretland-8dc689458-jstp2:/app# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  1.5 709816 58080 ?        Ssl  Apr07   0:08 puma 3.11.3 (tcp://0.0.0.0:3000) [app]
root        43  0.0  0.0  18204  3280 ?        Ss   02:29   0:00 /bin/bash
root        53  0.0  0.0  36636  2808 ?        R+   02:31   0:00 ps aux
root@secretland-8dc689458-jstp2:/app# gdb --pid 1
GNU gdb (Debian 7.12-6) 7.12.0.20161007-git
...
Attaching to process 1
[New LWP 6]
...
[New LWP 16]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
pthread_cond_wait@@GLIBC_2.3.2 () at ../sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
185     ../sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S: No such file or directory.

とは言うものの、最近リリースされたrbspyのおかげで、gbdを前ほど頻繁には使わなくなるのではないかと思えます。rbspyはgdbと違ってプロセスを一時停止しませんし、rbspyの方がユーザーフレンドリーだからです。

他にも便利なスニペットをご存知でしたらぜひお知らせください。本記事に追加したいと思います。

関連記事

Rails 5.2のcredential機能をKubernetesで使う(翻訳)

Ruby: eBPFでメモリアロケーションをプロファイリングする(翻訳)

週刊Railsウォッチ(20180702)Ruby 2.2メンテ正式終了、Ransackがつらくなるとき、書籍『Domain-Driven Rails』、GitHubの高可用MySQLほか

$
0
0

こんにちは、hachi8833です。これまで金曜夕方に刊行していた週刊Railsウォッチを月曜夕方に移動いたします。今後ともどうぞよろしくお願いします。

7月最初のウォッチ、いってみましょう。

各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ

⚓Rails: 先週の改修

というわけで「今週の改修」改め「先週の改修」となります。

先週は公式の更新情報はなかったのでmasterから見繕いました。今回は一息ついたのかドキュメント更新の方が盛んです。

⚓Date#wdayの対応付けを修正

DeepCoverというカバレッジツールによると、wday != 0 ? wday - 1 : 6一貫していない部分があったそうです。

# activesupport/lib/active_support/core_ext/date_and_time/calculations.rb#L5
 module DateAndTime
   module Calculations
     DAYS_INTO_WEEK = {
-      monday: 0,
-      tuesday: 1,
-      wednesday: 2,
-      thursday: 3,
-      friday: 4,
-      saturday: 5,
-      sunday: 6
+      sunday: 0,
+      monday: 1,
+      tuesday: 2,
+      wednesday: 3,
+      thursday: 4,
+      friday: 5,
+      saturday: 6,
     }
     WEEKEND_DAYS = [ 6, 0 ]

つっつきボイス:monday始まりがsunday始まりに変わったみたいです」「この辺ってPOSIXで決められてなかったっけ?🤔

後で調べたら以下のようになってました。

%w Weekday as a decimal number [0,6] (0=Sunday).
pubs.opengroup.org dateより

「なんでもDeepCoverというカバレッジツールで見つかったんだそうです(後述)」「あーRubyKaigi 2018で説明されてましたねそれ😀: 確かにcurrent_day_number = wday != 0 ? wday - 1 : 6だと悲しい😆」「😆」「😆」「この数値がデータベースに入ってたりするとつらいだろうけど、そのまま使うことはないだろうしねー」

「そういえば週の日曜始まりと月曜始まりも文化圏で違ってますね」「そういえば🧐

参考: 曜日 - Wikipedia

週が始まる日を、日曜日とすることもあり、また月曜日からとするところもある。アメリカ製のカレンダー、ヘブライ語、ポルトガル語、アラビア語、ペルシア語、ベトナム語などでは前者であり、フランス製のカレンダーなどは後者である。イスラム圏では金曜日が公休日になっていて、カレンダーも土曜日から始まるものがある。【中略】日本で販売されているカレンダーには日曜始まりのものと月曜始まりのものが混在している。

⚓fetchでエラーハンドリングを改善

# activesupport/lib/active_support/core_ext/date_and_time/calculations.rb#L265
     def days_to_week_start(start_day = Date.beginning_of_week)
-      start_day_number = DAYS_INTO_WEEK[start_day]
+      start_day_number = DAYS_INTO_WEEK.fetch(start_day)
       (wday - start_day_number) % 7
     end

これも上のdd7f8caに関連しているっぽいです。


つっつきボイス: 「これは普通に#fetchでやるやつですね🕶: fetchだとハッシュの値がnilならデフォルトでnilを返す」「ぼくちょうど今日#fetch調べてました❤たしか第2引数にデフォルト値を設定できるんですよね」「お〜いいタイミング!😋

#fetchってstrong_parametersで使われたりしますよね?」「まあそれに限らずハッシュにアクセスするときには#fetchっていろいろ便利ですね」「😃

Rubyスタイルガイド「7-14【統一】ハッシュのキーの存在を前提にする場合はHash#fetchを使うこと」から一応。


heroes = { batman: 'Bruce Wayne', superman: 'Clark Kent' }
# 不可 - キーが無効でもエラーにならない
heroes[:batman] # => 'Bruce Wayne'
heroes[:supermann] # => nil

# 良好 - KeyErrorでキーがないことを検出できる
heroes.fetch(:supermann)

参考: instance method Hash#fetch (Ruby 2.5.0)

# ruby-lang.orgより
h = {one: nil}
p h[:one],h[:two]                        #=> nil,nil これではキーが存在するのか判別できない。
p h.fetch(:one)                          #=> nil
p h.fetch(:two)                          # エラー key not found (KeyError)
p h.fetch(:two,"error")                  #=> "error"
p h.fetch(:two){|key|"#{key} not exist"} #=> "two not exist"
p h.fetch(:two, "error"){|key|           #=> "two not exist"
    "#{key} not exit"                    #  warning: block supersedes default value argument
  }                                      #  警告が表示される。

h.default = "default"
p h.fetch(:two)                          # エラー key not found (KeyError)

⚓ドキュメント更新関連


つっつきボイス: 「今回は夏枯れなのか、ドキュメントの細かな更新が多かったです🌞」「話逸れるけどbogdanvlvivというお名前が凄っ」「ボグダンってブルガリアとか東ヨーロッパのあたりの名前な気がしますね」「プロフィール見るとウクライナでした」

ついこの人の名前を思い出してしまいました↓。

参考: Bogdan Maglich - Wikipedia — MIGMAを考案したユーゴスラビアの核物理学者(故人)
参考: Migma - Wikipedia — 原子を1個ずつ核融合させる発電方式

MIGMA、未だに実用に達していないようで残念です😢

⚓Rails

⚓書籍『Domain-Driven Rails』

翻訳記事でお世話になっているArkencyが昨年出した本です。


同記事より

  • クラスがいつしかカラム50個/メソッド数百個になる理由と回避方法
  • Service Object/Form Object/シリアライザなどの水平層を加えるだけではうまくいかない理由: むしろ垂直の境界に着目すべし
  • ドメインからオブジェクトをモデリングする3つの技
  • ドメインロジックに関係ないコードをどこに置くか
  • Service Objectには2種類ある: どちらをどう使い分けるか
  • コマンドとイベントは何が違うか
  • Service Objectの爆発を避けて凝集度を下げないようにする方法
  • システムを拡張可能にし、癒着をドメインイベントやハンドラで切り離す方法
  • 長大なビジネスプロセスの流れを整理する
  • 副作用をハンドラに切り出してクラスをスリムにする
  • 「控えめなレポート出力」を構築する
  • イベントソーシングを行うドメインで、一時的な側面に対する洞察を提供する

つっつきボイス: 「昨年からあった本なのに気づいてませんでした💦: 上は概要を雑に訳したものですが、以前から社内でおっしゃってた『Service Objectは実は2種類あるのでは?』的なことが書かれてるようです: これ他では今まで見かけたことがなくって」「おー、そう思ってるのが自分だけじゃなくてちょっと安心したー😋: 何というか、みんながService Objectと信じているものが実は2種類あって、それが混じったまま話されるからややこしくなってるんじゃないかなって😆」「どんな2種類でしたっけ?」「まあざっくり思っているのはひとつがFacadeパターン的なもので、もうひとつはCommandパターン的なもの」「ともあれこの本は良さそう❤クーポンどっかなかったかな…」「これ読みたい人きっと多そうですね(翻訳してみたい…)」

「この本の価格体系見ると、練習問題とか動画付きのもあるナ: 動画使ってまで見ようとはあまり思わないけど😎」「大学のコースなんかでは動画は重宝されそうですね」「動画は見るのに時間かかるんで😆」「字幕があればいいんですけどね」

⚓Ransackで検索フォーム

<-- 同記事より -->
<%= search_form_for @q do |f| %>
  <%= f.search_field :id_eq %>
  <%= f.submit  %>
<% end %>

つっつきボイス: 「これはまあ普通のRansack紹介記事かな」「ですね: これは枕で、Ransackってどのあたりからつらくなるのかなというのがちょっと知りたくて」「なるほど: Ransackはですねー、割と早い段階でつらくなります😎」「あーやっぱり😆

「Ransackはいわゆる検索フォームを簡単に作れるやつで、上のようにsearch_form_forを使って、たとえば_eqなら完全一致とか_gteqなら大なりイコールというふうに条件を指定できる」「ふむふむ」


READMEより

「で、READMEに載っているようなマッチャー↑をシンプルに使って単純なフォームを作っているうちはいいんですが、Ransackで複雑なSQLを検索しようとするとどんどん大変になっていく」「😓」「JOINとかやりはじめるとデータ構造がみるみる複雑になっていくんですよこれが: 一応できるんだけど、つらい😭」「Ransackを1個のテーブルに対して使っているうちは全然大丈夫なんですが、JOINすると一気にフォームパラメータがネストで読みづらくなる」「ひえー🤮

「ちょうど今日の勉強会でも話したんですが、JOINしたテーブルをRansackで使うなら、まずビューでJOINしておいてそれに対してRansackを使えばずっとシンプルになるはず、理論上は🕶」「なるほどー」

# READMEより
def index
  @q = Person.ransack(params[:q])
  @people = @q.result.includes(:articles).page(params[:page])

  # or use `to_a.uniq` to remove duplicates (can also be done in the view):
  @people = @q.result.includes(:articles).page(params[:page]).to_a.uniq
end

「上みたいに元テーブルに対してincludesなんかでJOINしたものをひとたび作ってしまうと、フォーム側がname_or_description_or_email_or_articles_title_contみたいに長大になってしまうんですよ↓」「…これはひどい😰

# READMEより
<%= search_form_for @q do |f| %>

  # Search if the name field contains...
  <%= f.label :name_cont %>
  <%= f.search_field :name_cont %>

  # Search if an associated articles.title starts with...
  <%= f.label :articles_title_start %>
  <%= f.search_field :articles_title_start %>

  # Attributes may be chained. Search multiple attributes for one value...
  <%= f.label :name_or_description_or_email_or_articles_title_cont %>
  <%= f.search_field :name_or_description_or_email_or_articles_title_cont %>

  <%= f.submit %>
<% end %>

「上はまだRailsのヘルパーだからましなんですが、これをAjaxで扱おうとすると途端にJSONの構造まで複雑になる: えーとどこかにそれっぽいものはあるかな…これだっ↓こういうJSONになっちゃう」「😹

参考: Ransackで簡単に検索フォームを作る73のレシピ - 猫Rails

params[:q]
=> {
  "c" => {
    "0" => {
      "a" => { "0" => { "name" => "id" } },
      "p" => "eq",
      "v" => { "0" => { "value" => "1" } }
    }
  }
}

「あとRansackでANDとかORを使うのもヤバイです(特にOR): この構造ならconditionで絞り込んだビューを作っておけば楽にできるんだけどなあ…」「勉強になった!😀

⚓SPAでCSRFを効かせるには(Ruby Weeklyより)


つっつきボイス: 「へーみんなどうやってんのかな?: ちなみにRailsのCSRFトークンはMETAタグの中に入るんで取り出すこと自体は難しくないですが、ただCSRFトークンには何回でも使えるCSRFトークンを使う方法と、1回しか使えないCSRFトークンを毎回発行する方法の2種類があって、後者はSPAでたぶん毎回Ajaxで更新しないといけないでしょうね: その分後者の方がセキュアですが」「😀」「ページを戻ってPOSTすると失敗するのはたいていこれで、自分の好きでないやつ😤

# 同記事より
class ApplicationController < ActionController::Base

  after_action :set_csrf_cookie

  def set_csrf_cookie
    cookies["my_csrf_token"] = form_authenticity_token
  end
end

⚓RubyMoney gemシリーズ

# 同リポジトリより
require 'money'

# 10.00 USD
money = Money.new(1000, "USD")
money.cents     #=> 1000
money.currency  #=> Currency.new("USD")

# Comparisons
Money.new(1000, "USD") == Money.new(1000, "USD")   #=> true
Money.new(1000, "USD") == Money.new(100, "USD")    #=> false
Money.new(1000, "USD") == Money.new(1000, "EUR")   #=> false
Money.new(1000, "USD") != Money.new(1000, "EUR")   #=> true

# Arithmetic
Money.new(1000, "USD") + Money.new(500, "USD") == Money.new(1500, "USD")
Money.new(1000, "USD") - Money.new(200, "USD") == Money.new(800, "USD")
Money.new(1000, "USD") / 5                     == Money.new(200, "USD")
Money.new(1000, "USD") * 5                     == Money.new(5000, "USD")
...

以下の記事にも載っているmoney-rails以外にもあるんですね。

Ruby on Railsで使ってうれしい19のgem(翻訳)


つっつきボイス: 「もう名前で遊んでる🤣」「monetizeとかMoneyを作るとか😆」「新しい動詞が誕生した感🕶

⚓RailsのDateTimeフォーマットのサイト


同サイトより


つっつきボイス: 「開いてみるとわかりますが、本当にこの情報だけが載ってます: もしかしてDateTimeでググる人をキャッチする用かなと?」「😆」「これ何だったっけなーと思ったら、DateTimeのi18n変換にこうやってシンボルを指定したときにそれぞれこういうフォーマットで表示されるってことね: まあ普通手元で試すけどねっ😎

「これ見てて、以前MMDDYYとかDDMMYYみたいな日付表記で地雷踏んだのを思い出しちゃいました: ずっと前に海外の複数拠点と初めて仕事でやりとりしたとき、日付が02-05-08みたいな感じで指示出しされて2月5日なのか5月2日なのかが解釈が入り混じってとうとう…😭」「あーこういうやつですね↓」「それそれ」「DMYMDYYMD…こんなにあるし…」「その時以来日付は必ず02-May-2008みたいに誤解の余地のない書き方にしてます😤


Date format by country - Wikipediaより

⚓maily: 送信メールをプレビューするRailsエンジン


同リポジトリより


つっつきボイス: 「おーHTMLメールもプレビューできるのか: 悪くなさそう❤」「このmailyという名前を見てて思い出したんですけど、いつだったか『Rubyのgemはキラキラネームが多い』みたいな記事を見たのを思い出しちゃいました😆」「まー他所のライブラリだってそんな変わらないし😆: ただよく言われるけどライブラリにいい名前をつけるって実はとっても大事で、いい名前がつくと開発のモチベーションが続いて最後まで作り切るみたいなところがありますね」「😃」「ときどき名前から全然機能が推測できないライブラリとかありますけど…」「よほど単機能ならともかく、機能をズバリ推測できる名前を付けるのがこれまた難しいという: 後はもうSEO的に頑張るぐらいしか😆」「😆

⚓その他Rails


つっつきボイス: 「これも名前つながりで」「IDLってなんだっけ: インタフェース記述言語か」「まあ確かにGraphQLってクエリか?っていうとそうも言い切れないし、雰囲気的にはIDLとかXSLT的なものに似てる感ちょっとある: 型を指定するとそれに合わせて返ってくるあたりが」「😀」「ともあれGraphQLという名前がキャッチーなのは間違いないっすね: ResouceIDLだったら果たしてどうだったか😆」「😆


ちょうどFirebaseで流出ありましたね。

つっつきボイス: 「FaaS(Function as a Service)ってまた新しい言葉が😆」「何とかaaSって多すぎ🤣」「中身はだいたい既存の概念だったりすること多いし: FaaSもRPCとどう違うんだか😆


つっつきボイス:🤣」「🤣

⚓Ruby trunkより

⚓DevelopersMeeting20180718Japan

いつもと毛色の違うIssueが掲載されていたので何だろうと思ってしまいました。コアコミッターがオフラインで集まってチケットを解決するというイベントのようです。

参考: Main - Ruby - Ruby Issue Tracking System
参考: DevelopersMeetingRealtimeGuidelines - Ruby - Ruby Issue Tracking System — ガイドライン


つっつきボイス: 「そうそう、前からこれ定例でやってますよ: RubyKaigiでも話してなかったっけ?」「今まで全然気づいてなかった💦」「2014年から結構回数重ねてますね😃

⚓O_CLOEXECフラグが使えないLinuxがある


つっつきボイス: 「お、また知らないフラグが😲フラグってほんといろいろあるからなー」「このissueはRubyのせいではなかったと」

参考: Man page of OPENO_CLOEXECはLinux 2.6.23 以降となっています。

デフォルトでは、新しいファイルディスクリプターは execve(2) を実行した後も オープンされたままとなる (つまり、 fcntl(2) に説明がある FD_CLOEXEC ファイルディスクリプターフラグは最初は無効である); 後述の O_CLOEXEC フラグ を使うとこのデフォルトを変更することができる。 ファイルオフセット (file offset) はファイルの先頭に設定される (lseek(2) 参照)。
上man pageより

⚓Process.waitとMJITコンパイラ

Rubyが子プロセスをウェイトしようとすると、MJITのgcc/clangプロセスがそのメソッド呼び出しに捕まってしまう可能性がある。これはRubyユーザーにとってもMJITワーカースレッドにとってもうれしくないので、Process.waitやその関連機能は何らかの形でウェイトを避けるべき。
同issueより


つっつきボイス: 「スレがかなり伸びてて今はちょっと追えない感じ」「例のnormalpersonさんも登場している」「お、normalpersonさんの名前がカッコ書きで表示されてる: 前はなかった気がするけど」「ほんとだ」「これで謎の人感が少しは和らぐかな😉

⚓Ruby

⚓Ruby 2.2がメンテナンスモードも終了(Ruby公式ニュースより)

今年3月のウォッチでRuby 2.2がメンテナンスモードに入ったことをお知らせしましたが、この度完全にサポート終了しました。

  • 2.5シリーズ: 現在通常メンテナンス
  • 2.4シリーズ: 現在通常メンテナンス
  • 2.3シリーズ: セキュリティメンテナンス(極めて重要なセキュリティ修正のみ対応)

つっつきボイス: 「もう2.2終わりかー、月日の流れるのは早い🌙🌠」「お知らせに気づくの遅れました😓

⚓deep-cover: Ruby向けカバレッジツール

Rubyビルトインのカバレッジより手広くチェックしてくれるそうです。

Feature MRI DeepCover
Line coverage partial
Node coverage no
Branch coverage partial
Method coverage ~
Slowdown < 1% ~20%
Platform support Ruby 2.5+ Ruby 2.1+, JRuby
Branch coverage MRI DeepCover
if / unless / ?:
case / when
❘❘ / && no
foo&.bar
{❘foo = 42, bar: 43❘} no
while / until !

⚓TruffleRubyがRVMとruby-buildでサポート(Ruby Weeklyより)

TruffleRubyといえばRubyKaigi 2018のキーノートで圧倒的な印象を残しましたね。今更ですが、RubyKaigiの動画またはスライドはひととおり出揃ったようです。


同リポジトリより

TruffleRubyをインストールするには原則としてGraalVMが、GraalVMをインストールするにはLLVMやzlibやlibsslが必要だそうです。

以下を参考にrbenvでインストールしてみました。LLVMさえインストールしておけば割とすっとできました。

参考: TruffleRubyを動かしてみた - Qiita

しかしbundle installでRails 5.2をインストールしようとするとこれと同じエラーが発生してnokogiriでコケました(´・ω・`)。


つっつきボイス: 「TruffleRubyはOracleが手がけてるJavaベースのRuby実装でむちゃくちゃ速い: あれ?rbenvでは前から動いてなかったっけ?」「そういえばそんな気も…」「さすがにTruffleRuby上でRailsはとりあえず動きませんでした↑」「そりゃまあねー、結局JavaなんでgemのC拡張に対応してないから🧐」「やっぱりそうだったかー」「RubyのCライブラリは.soのマッピングとか要するに普通のLinuxプロセスのマッピングになってるからそのままじゃまず動かないと思いますよ: 確かMySQLクライアントなんかも動かなかったと思うし(最近はどうだったかな…)そういうのも動かないのはなかなかキツイ🤔」「ふーむ」「将来は対応する予定とか何とか言ってたと思うけど😉

JRubyもTruffleRubyと同様Javaベースで速いみたいですね」「JRubyはエンタープライズ領域で使われてるし歴史もあるし」「JRubyでしか起きないエラーを踏んだときの解決がつらいって何かの発表で聞いた覚えあります」「そこはもうマイナーの宿命で、本家Rubyがリファレンスですからねー😎


jruby.orgより

⚓Rubyコードをフローチャート化(Ruby Weeklyより)


同記事より


追いかけボイス: 「もしかしてダックタイピングってフローチャートにしづらい?」「フローチャートには乗せようがないヤツ🕶

参考: ダック・タイピング - Wikipedia

⚓Rubyにパターンマッチング構文がまだないから試しに作ってみた

おなじみzverokさんの記事です。

Ruby: ありそうでなかったRubyリファレンスの決定版を作った(翻訳)


つっつきボイス: 「あー、この↓感じ、Elixirか何かで見たような覚えが🤔」「記事はあくまで提案で、これをたたき台に議論しましょうという趣旨ですね」「テストできるシンプルなAPIなんかをこれっぽく書けたりするといいかも」

# 同記事より
when ((Numeric, Numeric), Hash) # nested sequences
  # call-sequence may have been `parse_coordinates([57.0, 32.0], strict: true)`
when (:skip, _, _, Numeric)     # as in method args, _ has a special meaning of "ignore this"/match anything
when (*Numeric)                 # array of any size, but all numerics
when (*/\d+(\.\d+)?/)           # array of number-alike strings, of any size
when (Numeric, Numeric, radius: Numeric, **) # are we too far yet?..

⚓型を変数名に含めるとは

一度は誰しも考える気がしました。

参考: ハンガリアン記法 - Wikipedia


Wikipediaより


つっつきボイス: 「変数名に型をねー😅、と思ったら、抄録↓見ると構文からの型推論が難しいから名前から型推論しようよって趣旨か」「ハンガリアン記法って何だったっけと思ったら、iで始まると整数、みたいなやつでした↑」「RubyでそれやったらRubyの特徴が失われちゃう気がしますね」「まあRubyだとそうなっちゃいますけど、この感覚はJavaの世界なら割とありますよ: 一時的に使うインスタンスなんかでクラス名を_なんちゃらみたいに付けたりとか」「へー!😲」「その書き方を常に強制されるんならやめて欲しいけど、特定の書き方に沿っていれば型チェックしてくれるぐらいのレベルなら元Java書きとしてはまあ悪くないんじゃね?とは思う」

型推論は,構文パターンから型を推論する方法で,型アノテーションなしで静的型付けを実現する.ただし,構文パターンからの型推論はアルゴリズムが複雑になりがちですべての言語に採用しにくい問題がある.本発表は,よりお手軽に型推論を実現するため,名前からの型推論を提案する.まず,実際のソース・コードを解析し,型と名前の法則性を調べる.それに基づき,名前からの型を推論するシステムと言語設計を定義した.我々は,これらのアイディアを関数型スクリプト言語konoha 5λに実装し,その使いやすさを検証し報告する.
IPSJ-TPRO1102006.pdf論文抄録より

⚓Rubocopのケツカンマ


つっつきボイス: 「スタイルはどう転んでも揉めますね😓」「まー自分も個人的にはtrailingカンマありが好きなんだけどRubocopのデフォルトはそうじゃないし: これはもうプロジェクトに置いてあるrubocop.ymlにみんなで従うしかないっしょ😇

Rubyスタイルガイドを読む: コレクション(Array、Hash、Setなど)

⚓書籍「Kestrels, Quirky Birds, and Hopeless Egocentricity」


leanpub.comより

ただいま翻訳中のFunctional Programming in Ruby — State – Brandon Weaver – Mediumという記事で紹介されていました。Rubyのメタプログラミングでコンビネータ論理する本だそうです。

参考: コンビネータ論理 - Wikipedia


つっつきボイス: 「ブログ記事を書籍化したものらしくて、無料でダウンロードできるというので❤」「謎の本…」

⚓その他Ruby

つっつきボイス: 「↑このDevMemoっていうサイト、本当にRubyのEnumerableを学ぶためだけのサイトなんだそうです😆」「単機能シングルドメイン🤣」「ログインして使ってみないとわかりませんが」


参考: Turing Complete FMミートアップ|IT勉強会ならTECH PLAY[テックプレイ]

つっつきボイス:先週のウォッチTCFMミートアップのことを取り上げたのがきっかけで私もしれっと参加したんですが、Matzが本当に楽しそうでよかったなーと思っちゃいました❤」「😃」「Matzは普段よそのイベントに仕事抜きで顔を出す機会がなかなかなさそうなんでなおさらかなと」

なお、最新のTCFM第25回をうつらうつら聞いてたら、そのうちMatzがTCFMに出演するかもと言ってたような気がします(違ってたらごめんなさい)。


⚓クラウド/コンテナ/インフラ/Linux

⚓WPA3といえば


つっつきボイス: 「今度のWPA3って大丈夫なんかいな〜🤤」「IPA試験でWPA2が出題された翌日に脆弱性が見つかったんでしたっけ?」「そうそうw あれ翌日だからよかったけど前日に見つかってたら『安全性の高い方式を選べ』問題が解なしになるところだったし😆」「😆

参考: Wi-Fi Protected Access - Wikipedia — WPA2やWPA3の解説

⚓GitLabがAzureからGCPに移行中

⚓aws-sdk-ruby


つっつきボイス: 「これはもうすっかり定着?」「ですね: 何しろ公式なんで最新のAPIにすぐ対応してくれるのがエライ😋」「後は早くLambdaでRubyが動くようにして欲しいな〜😆」「😆

⚓APIゲートウェイにEnvoyを選んだ理由(WebOps Weeklyより)


つっつきボイス: 「Envoy?知らないなー🤔Nginxやもちろん、HAProxyも前から使われているけど」「こういうハイパフォーマンスを要求されるミドルウェアはGo言語で書くのもひとつの手かも」「記事の中に出てくるL7プロキシって何でしょう?」「まあ多少余分に仕事をするプロキシというか」「コンテンツに手を突っ込むとか?」「あーそこまでは普通しないかな(やることもなくはないけど)ヘッダーを書き換えたりとか」

Nginxは気をつけないと容易に地獄化するヤツ: RubyKaigi 2018でもNginxの設定があまりにつらいからmrubyで書く方がいいよみたいな発表があったぐらい、Nginxの設定はマジつらい😵

「もしかしてsendmail.cf並につらい?」「あそこまでひどくはないけど🤣、sendmail.cfは人間が読むもんじゃないw」「機械語みたいな?」「いやーいわゆる古典的なマクロなんですね: あるいはSELinuxのポリシーファイルを生で読むのに近いレベルというか、機械にやさしくて人間につらい書き方😹

参考: Envoy Proxy - Home


envoyproxy.ioより

参考: Envoy (Envoy proxy)、Istio とは? - Qiita


haproxy.orgより

⚓その他クラウド/コンテナ/インフラ/Linux

つっつきボイス: 「あーちょうど今日こんなやりとりしてたわ: 『SSL化』って言葉と『HTTPS化』って表記ゆれしてるなーと思いながら資料書いてた😅」「😆」「何しろ今はSSLというものはもう世の中にはないということになってるから🤣」「せっかく世間に浸透し始めた言葉なのにハシゴ外されてしまった感😆

「そういえば応用情報処理試験の教科書には『SSL/TLS化』って書いてましたね」「おーさすがIPA: BPS社内でIPAの試験を推している理由のひとつは、問題を作っている人たちのレベルが高くてそのあたりの正確さをちゃんと担保しているからなんですね」「確かにー」「誰が読んでも誤読しようのない問題を作っているところとかマジ凄い: 別の某認定試験なんかだと『これ、答え存在しないんじゃ?』としか思えない問題ありますからねー😎」「歴史が長いから?」「というよりレビューがみっちり行われているからでしょうね: あの問題をきれいに解けるようになった人なら、そういう表記ゆれ/解釈ゆれのない文章を書けるようになるんじゃないかな」「😃

「SSLって用語も俗称として残しちゃえばいいんじゃ?って思っちゃいます😆」「『狭義のSSL』と『広義のSSL』とかね😎」「JavaScriptだって厳密なことを言えば『狭義のJavaScript』はNetscapeのJavaScriptしかないわけですから」「あ、そっか!😲」「広義の意味ではECMAScriptってことになるけど、ただしECMAScriptという実装は、ないw」「🤣

⚓SQL

⚓PostgreSQLが遅いと思ったらやること(Postgres Weeklyより)


つっつきボイス: 「PostgreSQLが遅くなる原因で割とあるのがコネクションが詰まるというパターンですね: だからなのかPostgreSQLはコネクションプールも一緒に使われることが多くて、詳しくはわからないけどMySQLではあまりコネクションプールって使われないんですよ」「おー😲」「もしかするとMySQLと比べて新しいコネクションを張るのが重い傾向があるのかもしれないですね」

「記事にもあるけどPostgreSQLはpg_stat_*系が充実してて、ここをチェックするのがいいですね: MySQLはこの辺の情報があまりなくて、スロークエリをゼロ秒にして全クエリを再生して統計を取るとかPercona Toolkitを使うとかしないと追いかけづらい」「ふむぅ🤔

「ちな最近PerconaはPostgreSQLもサポートしてます↓: あとPercona版のMySQLも出しててクエリチューニングのときに便利😋」「へー!」「MySQLで1個のクエリが遅いだけなら単独でもわかるんですが、たとえばとてもよく似たクエリが100個あって結果として全体が遅い、なんてのはスロークエリを調べても取れないので統計を調べないといけない」

参考: Percona Support for PostgreSQL


percona.comより

「記事の4.1にあるSystem load↓、普通これを真っ先に見ると思うな😎

# 同記事より
load average: 3.43, 5.25, 4.85

「あと可用性がめちゃめちゃ高いデータベースだとログ出力のところが詰まるというのもよくある話で、syslogだとそれをさばききれなくてつらくなるとかありますね」「😃

⚓PostgreSQLで日付を扱うときのコツ(Postgres Weeklyより)


つっつきボイス:interval演算子!こういうのがあるからぽすぐれエライ!」「ほんと何でもある感じですね❤」「ほんにそう:やばいぐらい何でもある」

「前にも話したし今日の勉強会でも話したんですけど、データベース研究者が参考実装にPostgreSQLを使うというのが長年広く行われていて、新機能がOracleよりも先に導入されることもしょっちゅうだし: まあ商用データベースは新機能をほいほい入れないものですけど、PostgreSQLは論文が出たと思ったらもう機能足されてるなんてざらで、その分バグもあったりしますが😆」「😆

「そういえば最近Google CloudのBigQueryはこのintervalみたいな柔軟なクエリが多くてなかなかいいと思いましたねー」「おー😲」「ウィンドウ関数とかでちょっと感心するようなものがいろいろある: 使いこなしには気合いが必要でしょうけど🕶

Googleクラウドのロゴ引用は何だか面倒くさそうなので止めておきます(´・ω・`)。

⚓BDE

これで合ってるのかな?

参考: BDE,SQL Linkとは

⚓GitHubのMySQL可用性が高い秘密(WebOps Weeklyより)


githubengineering.comより


つっつきボイス: 「GitHubがMySQL使ってるって初めて知った!😲」「😲」「😲」「これ翻訳してみたいです」「あくまで推測だけど、GitHubは単一のクラウドじゃなくてマルチクラウドでやってるんじゃないかなーって想像: あれだけの規模だし、シングルクラウドには依存しないんじゃないかな?」「それにGitHubはユーザーアクセスにローカリティがあるしブロードキャスト的なものもないから、分割するのは実はそんなに大変じゃないのかもしれない: あくまで推測ですが」

⚓JavaScript

⚓Node.jsのworker

⚓RedashがReactへの移行を検討?


github.com/getredash/redashより


つっつきボイス: 「Redashはねー、ユーザー管理機能がもうちょっと使いやすければいいのにと思ったりする」

一瞬Reduxと取り違えそうになりました。

⚓Vue.jsプロジェクトを爆発させる方法


つっつきボイス: 「私はとても参考になりました😋」「Vue.jsってそもそもそんなに小さく作るものじゃないし、ねー: Reactならもっと縛りはかけられるけど、何を使おうと結局スコープをどこまで絞れるかにかかってくるし、爆発させようと思えばどれだって爆発させられるし💣」「😆

⚓CSS/HTML/フロントエンド/テスト

⚓バグだらけのWebアプリでバグを理解する

つっつきボイス: 「セキュリティでおなじみの徳丸先生が脆弱性をわざと仕込んだサンプルアプリを作ってたけど本物には勝てないってツイートしてたのを思い出したので」「そういえば何のイベントだったかなー、インフラをひたすらチューニングしていく競技イベントなんかでもこんなふうに出発点をひどい設定にしておかないと競技が成り立たないから、同じようなつらさがあるかも😆」「そういうひどいコードをわざと書くのって手が拒否したりしません?」「するするw」「普段から普通のコードを書くことだけを心がけてると、普通じゃないコードを書くのが極端に難しくなる🤣」「🤣

↑バイオリンの神演奏者ハイフェッツが映画の中でわざと下手に演奏してみせた動画です。ニコリともせずに笑いを取ってて最高です。

参考: ヤッシャ・ハイフェッツ - Wikipedia

「まー確かに、習得していない人が習得した人のふりをするってのはもしかするとハナモゲラ的に可能なのかもしれないけど、習得した人が習得してない人のフリをするってのは逆に難しい気がする」「そういえば名人の寿司職人が何もできないふりをして海外の寿司屋に弟子入りしてみせるみたいなTV番組ありましたね😆」「そんでつい無意識にまな板をキレイに拭き上げてしまってすぐ見破られちゃったりとかね🤣」「🤣」「本当にちょっとした仕草でバレるヤツ😇」「そういうのって隠すのも難しいしアレルギーとか蕁麻疹出ちゃいそう😵

⚓Faye: pub/subベースのWebメッセージング


同サイトより

Bayeux(バイユー)というプロトコルを用いるそうです。RubyやNode.jsで使えます。

参考: The CometD Reference Book – 3.1.4 — Bayeux 1.0の仕様

参考: Bayeux プロトコルの仕様について - Green Software Engineer Blog


つっつきボイス:ActionCable以外にもこんなのがあるんだなと思って: Bayeuxってもろフランス語だけど読みすぐ忘れちゃう💦」「へー、ミニマムなpub/subってことかな: pub/subそのものは実は割とそんなに難しくなく作れるんですが、タイムアウトやらID重複やらの仕様を固めていく方が大変ですね」「😃

「やっぱりCometか、久々に聞いたなこの名前: Cometだと確かにHTTP/2でなくても通るみたいな良さはあるし、サーバー間で使うぐらいならCometでもいいのかも?」「CometというのはAjax的なものなんでしょうか?」「昔のAjaxというかAjaxの手法のひとつですね: サーバーがレスポンスをすぐに返さないで、イベントが発生したら初めて返すみたいな形でTCPセッションを持ちっぱなしにするんですね」「へー!」「当然TCPセッションが開きっぱなしだしサーバーもワーカーを使いっぱなしになるしで、どうしてもリソースを食ってしまう: ま昔の技術ですね😎

参考: Comet - Wikipedia
参考: Comet, Pub Sub, ActionCable - Women Who Code.

⚓CSS Basic User Interface Moduleが勧告へ(Frontend Focusより)


つっつきボイス: 「また新しい仕様が」「outlineとかresizeとかcursorとかcaret-colorとか、いろいろプロパティが追加されるようだ」

⚓その他CSS/HTML/フロントエンド/テスト

つっつきボイス: 「パラレルCSSパース!: あーでも最近のCSSはminifyしても数KBになったりすることがあるからパラレル化も効果あるのかな?」

⚓言語よろずの間

⚓一般ピープル向けの「λ」


lambdaland.codemiller.comより


つっつきボイス: 「これはプレゼンでしゃべるときのスライドっぽいですね😋

⚓flutterリンク集


同リポジトリより


つっつきボイス: 「これは社内flutter勢に教えを乞うか😋」「よろしくお願いしまーす❤

⚓Haskellを学んでRubyコードを改善しよう(Ruby Weeklyより)

⚓Emojicode: 絵文字でプログラミング


同サイトより

あるかなと思ったらやっぱりありました。個人的には囲み系の記号(⟦⟧とか⟬⟭とか⟪⟫)がもう少しASCIIにあればよかったのにと思います。

🐇 🙋 🍇
  🍰 name 🔡

  🐈 🆕 🍼 name 🔡 🍇🍉

  🐖 🌕 🍇
    😀 🍪🔤Good night, 🔤 name🍪
  🍉

  🐖 ☀ 🍇
    😀 🍪🔤Howdy, 🔤 name🍪
  🍉
🍉

🏁 🍇
  🍦 greeter 🔷🙋🆕 🔤Spencer🔤
  🌕 greeter  👴 Prints “Good night, Spencer” to the console
🍉

つっつきボイス: 「出たー🍪🍇🍉」「前にもこんなの見た気が」「Rubyなら絵文字使うDSL作ればすぐできますね」
「ところで囲み記号を増やすのはパーサーそのものに影響するから単純な絵文字を足すより大変だと思う: それこそRubyでパーサーを書いてその上で動かすぐらいの勢いが必要かも😆」「Rubyだけでは難しいこともあると」

⚓その他言語

つっつきボイス: 「たまにはPythonもと思って」「日本だとRubyはWebアプリを書く軽量な言語として結構メジャー感あるけど、海外だとPythonでDjangoとかの方がメジャーですね」
「ところで日本の大学は最近RubyよりはPythonを教えたがる傾向があって、言語そのものを追求するならRubyの方が楽しみが多いと思うけど、データ処理のツールとして使うとなるとPythonの方に分があるからかもしれないですね」「ふむふむ」「Pythonで学んだことはRubyでもだいたい使えるけど、Rubyで学んだことは必ずしもPythonで使えるとは限らないという面もありますしね」「それはあるかも」「なるほどー😀」「今でもC言語を教えているところがあるのもたぶんそれで、C言語でできることは間違いなく他の言語でもできるから」「たしかにー😃

⚓その他

⚓ASIMOも終了→終了しません


つっつきボイス: 「これは誤報だったってやつ↓」「おやー」「そうでした」

⚓はやぶさ2も現地到着

⚓Planned poolingとは

編み物とプログラミングって接近しそうでしないですね。


つっつきボイス: 「おー、一本の糸が途中で色が変わってたりするのか!😲」「動画の途中で毛糸を切って繋ぎ変えていたのもびっくりでした」「それはいいのかw」「イカサマ?🎲

⚓その他のその他



⚓番外

⚓行き来できるワームホール?


同PDFよりp3

「因果律は損なわれない」という一文が目に付きました。


indico.oist.jpより

Strings 2018というイベント名が素敵すぎます。沖縄科学技術大学院大学で開催されたそうです。

⚓45年に渡る研究

⚓これマジおすすめ

参考: 【非常時に】「布ガムテープをコンパクトに収納する方法」を警視庁が伝授! さらに “ガムテのプロ” 元テレビ局ADが女子向けの裏ワザを教えてくれたよ | Pouch[ポーチ]


今回は以上です。ハードだった…

バックナンバー(2018年度後半)

週刊Railsウォッチ(20180622)Railsの需要未だ巨大、Unicode 11.0リリース、WebDriverがW3Cで勧告、Flutter.io、2封筒問題ほか

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

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

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

WebOps Weekly

webops_weekly_banner

Postgres Weekly

postgres_weekly_banner

Frontend Focus

frontendfocus_banner_captured

Rails: RedisとRediSearchで時系列データを扱う(翻訳)

$
0
0

概要

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

Rails: RedisとRediSearchで時系列データを扱う(翻訳)

前回の記事では、時系列データをRedisとElasticSearchの間で統合する方法を探りました。今回はさらにその先に進んで、RediSeatchモジュールを用いるRedisで時系列データを検索する方法をご紹介します。

私たちは、Ruby on Railsフレームワークを用いて構築された国内規模の小売チェーン向けのと同じものを概念実証(POC)アプリとして用いることにします。Webサイトではユーザーがさまざまな対話的操作で検索(サイトに来るユーザーの郵便番号だったり、ユーザーが探している製品だったり)を行えるようにしたいと思います。

時系列データでよくあるアプローチは、何らかの周期(1日単位が多い)でインデックスを作成することです。続いて、古いインデックスを削除する(または別のデータストアに移動する)プロセスを定期的に実行して、プライマリのRedis DBには直近X日のデータのみを保持します。

インデックスを日毎に分割する

ロジックをカプセル化するため、レコードを作成して適切なインデックスに挿入するクラスを別途ひとつ作成します。

# config/initializers/redis.rb
REDI_SEARCH = Redis.new host: 'localhost', ...
# app/services/
class RediSearchClient
  def initialize time: nil, index_per_day: nil, index_pattern: nil
    @time = time || Time.now
    @index_per_day = index_per_day || true
    @index_pattern = index_pattern || 'search_log'
  end
  def create
    return if index_exists? == true
    REDI_SEARCH.call('FT.CREATE', get_index, 'SCHEMA', 'zipcode', 'TEXT',
      'product', 'TEXT')
  end
  def add id: , zipcode: , product:
    create
    REDI_SEARCH.call('FT.ADD', get_index, id, '1.0', 'FIELDS',
      'zipcode', zipcode, 'product', product)
  end
private
  def index_exists?
    return true if REDI_SEARCH.call("FT.INFO", get_index)
    # インデックスがない場合のエラーハンドリングが必要
  end
  def get_index
    return "#{@index_pattern}:#{@time.strftime("%Y-%m-%d")}" if @index_per_day
    return @index_pattern
  end
end

データ処理が遅延する可能性があるのと、昨日のデータを本日のインデックスに挿入したくないため、渡すのが遅れないようにしています。インデックスで使う命名パターンを決定して日毎のインデックスを作成するかどうかを決定する(これはインデックスパターンに日付スタンプを追加することで行う)ためのオプションパラメータも渡します。

複数のインデックスに対してクエリをかけるには、FT.SEARCHコマンドでRedisに個別のリクエストを作成し、それらの結果をコードでマージしなければなりません。インデックスのリストを返すために、パターンにマッチするRedisキーを取得します。

class RediSearchClient
  ...
  def search query: , limit:
    output = {}
    get_indexes.each do |index|
      result = REDI_SEARCH.call('FT.SEARCH', index, query, 'LIMIT', 0, limit).drop(1)
      output.merge! ( Hash[result.each_slice(2).to_a] ) unless result.empty?
    end
    return output
  end
private
  ...
  def get_indexes
    REDI_SEARCH.call("keys", "idx:#{@index_pattern}*").map do |index|
      index.split(':').drop(1).join(':')
    end
  end
end

Redis内のデータは以下のような感じになります。日付ごとにft_index0がひとつ、ft_invidxが複数ある形です。

idx:search_log:YYYY-MM-DD      ft_index0
ft:search_log:YYYY-MM-DD/java  ft_invidx
ft:search_log:YYYY-MM-DD/redis ft_invidx

古いインデックスを廃棄するために、FT.DROPに適切な日付(Time.now - X.days)を渡してクラスの初期化を呼び出します。

class RediSearchClient
  ...
  def drop
    REDI_SEARCH.call('FT.DROP', get_index)
  end
end

インデックスが廃棄されると、RediSearchはft_index0ft_invidxキー(複数)を削除するほか、ドキュメント自身を保管するRedisハッシュも削除します。

RedisSearch内の他のメソッドをサポートしたり、インデックスのSCHEMAやドキュメントフィールドをさらに抽象化するには、このコードにまだまだ手を加える必要があります。しかしこのコードは単に、アプリ内でこうした複数の関連インデックスを管理できるようになることを示すためのものです。

1つのインデックスですべてのデータをカバーする

複数のインデックスを管理し、複数の検索結果をマージするのに手間暇はかけたくないものです。代わりに、1つのインデックスを用い、古いドキュメントを削除するロジックを構築する手も考えられます。

今のところ同じクラスを使っているので、レコード作成時にインデックス名から日付スタンプを除外するために@index_per_day = falseを指定しています。

FT.SEARCHは、クエリに一致するレコード件数を最初のパラメータとして返します。これを用いてインデックス内の全ドキュメントをループし、ID(タイムスタンプから導出)をチェックし、FT.DELコマンドを実行して各ドキュメントを削除します。

class RediSearchClient
  ...
  def purge
    ttl = ((Time.now - X.days).to_f.round(3)*1000).to_i
    deleted_count = 0
    num = 10
    total_docs = REDI_SEARCH.call('FT.SEARCH', get_index, '*', 'NOCONTENT').first
    (total_docs/num).times do |i|
      offset = (num * i) - deleted_count
      document_ids = REDI_SEARCH.call('FT.SEARCH', get_index, '*', 'NOCONTENT',
        'LIMIT', offset, num).drop(1)
      document_ids.each do |id|
        if id.to_i < ttl
          REDI_SEARCH.call('FT.DEL', get_index, id, 'DD')
          deleted_count += 1
        end
      end
    end
  end
end

DDを指定すると(Redisハッシュ内に保存されている)ドキュメントも削除されます。ft:search_logキーとredis ft_invidxキーについてはそのままです。

この手法の大きな欠点は、クエリの実行やドキュメントの削除のためにRedis呼び出しを多数実行する必要がある点です。この手法は、Redisでキーの期限切れを用いるTTLアプローチと比べて著しく複雑になります。

参考リンク

関連記事

Railsのフラグメントキャッシュを分解調査する(翻訳)

RabbitMQはSidekiqと同等以上だと思う: 前編(翻訳)

Rails: beforeバリデーションをやめてセッターメソッドにしよう(翻訳)

$
0
0

概要

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

Rails: beforeバリデーションをやめてセッターメソッドにしよう(翻訳)

以下のようなコードは多くのプロジェクトで目にします。

class Something
  before_validation :strip_title

  def strip_title
    self.title = title.strip
  end
end

しかし、この要件を満たす書き方は他にもあるのです。

class Something
  def title=(val)
    @title = val.strip
  end
end

あるいは以下のようにも書けます。

class Something
  def title=(val)
    self['title'] = val.strip
  end
end

.あるいは以下のようにも。

class Something
  def title=(val)
    super(val.strip)
  end
end

クラス内でのデータ保持方法に応じたものを選べばよいのです。さまざまなgemでさまざまな方法が使われています。

私がこの書き方を好む理由は以下のとおりです。

  • valnilの場合にコケる。そう、そして私はこれがよいことだと思っています。自分たちのフロントエンド側からnilがタイトルとして送信されることなどめったにないので、たまたまnilになったときに何かがコケて例外を発するのは問題ありません。いずれにしろこんなことはまず起こりません。私のプログラマーとしてのトカゲ脳はひととおりのエッジケースを察知していますし、私のトカゲ脳のこういうところが大好きです。しかしこのトカゲ脳がごくたまに私を裏切って、まず起こりえないようなエッジケースに焦点を合わせてしまうことがあります。
  • マジックが少なくて済む。Railsバリデーションのコールバックはクールですし、何度となく使ってきました。だからといって、スペースを取り除くのにわざわざコールバックを持ち出す必要はありません。
  • 利用範囲が広い。フィールドに値を設定した後に、途中でsaveなどしなくても、またはバリデーションを(何らかの理由で)実行しなかった場合でも、フィールドを読み出すときに動作してくれます。
something.code = " 123 "
something.code
# => 123

something.save(validate: false)

私は、Command ObjectForm Objectといった境界を超える用途に使われるオブジェクトに対して、バリデーションを消し去るルールを挿入する手法が特に気に入っています。

まとめ

以上で、ささやかなレッスンはすべて終了です。もっとお知りになりたい方は、元記事末尾のメーリングリストに加入いただくか、私どものFearless Refactoringの購入をぜひお願いします。

関連記事

Rails tips: 知らないと損する4つのバリデーションレベル(翻訳)

Rails: :before_validationコールバックの逸脱した用法を改善する(翻訳)

Rails: Active Record 5.2のメモリ肥大化を探る(翻訳)

$
0
0

概要

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

翻訳には含めませんでしたが、元記事のコメントも興味深い内容です。

Rails: Active Record 5.2のメモリ肥大化を探る(翻訳)

Active Recordの現在のパターンはリソースの大量消費につながります。以下はRails 5.2を用いた分析です。


Ruby 3×3計画は、Matz率いるRubyコミュニティの高貴な目標のひとつです。このアイデアは、Rubyインタプリタを3倍高速にできる現代的な最適化を広範囲に渡って用いることであり、野心的かつ崇高かつエキサイティングな目標です。このムーブメントは、just-in-timeコンパイラメモリ肥大化の軽減にかかわる革新的な作業といったRubyコアにおける多数の興味深い実験につながりました。もしRubyが高速化とメモリ削減を実現したら、パフォーマンスの恩恵を誰でも無償で受けられることになり、それこそが私たちが望んでいるものです。

しかし大きな問題は、Rubyがさらなる高速化を達成できるマジック以外のものを当てにできないことです。Rubyがコードの奥深くに潜む(低速な)バブルソートを魔法のように修正できるようになる予定はありません。Active Recordの内部には、世のRubyアプリのほとんどを大きく高速化するために修正されるべき多くの無駄が潜んでいます。Rubyの最大消費者は結局Railsであり、そのRailsはActive Record次第なのです。

悲しいことに、Active RecordのパフォーマンスはRails 2の時代からそれほど向上していません。実際、遅い場合やめちゃくちゃ遅い場合が少なからずあるのです。

無駄の多いActive Record

まずはごく小さなサンプルコードから始めたいと思います。

Topicに含まれる30カラムのテーブルがあるとしましょう。

以下を実行すると、Active Recordのアロケーションがどのぐらいになるかおわかりでしょうか。

a = []
Topic.limit(1000).each do |u|
   a << u.id
end
Total allocated: 3835288 bytes (26259 objects)

上を、同じぐらい非効率な生SQL版と比較してみましょう。

sql = -"select * from topics limit 1000"
ActiveRecord::Base.connection.raw_connection.async_exec(sql).column_values(0)
Total allocated: 8200 bytes (4 objects)

驚くべき無駄の量です。これは次の2つからなります。

  • 著しいメモリ使用量
  • パフォーマンスの低下

訳注: 上のa << u.idという書き方には問題があります。詳しくは週刊Railsウォッチをご覧ください。

しかしこれがActive Recordの残念な点

ここで、私が「遅い」Active Recordコードを書いて、大きく最適化された生SQLコードと比較するのはインチキだという反応がたちどころに起こるでしょう。

次のように書くべきと言われるでしょう。

a = []
Topic.select(:id).limit(1000).each do |u|
  a << u.id
end

この場合の結果は以下のようになるでしょう。

Total allocated: 1109357 bytes (11097 objects)

次のように書けばさらによい結果になるとも言われるでしょう。

Topic.limit(1000).pluck(:id)

この場合の結果は以下のようになるでしょう。

Total allocated: 221493 bytes (5098 objects)

ここまでを取り急ぎまとめてみます。

  • 「生SQL版」で割り当てられるのはわずか4オブジェクトであり、1000個のIntegerを直接返すことができます。これらはRubyのヒープに個別に割り当てられることもありませんし、GC対象スロットになることも多くありません。
  • 「ネイティブ版」Active Recordでは26259個のオブジェクトが割り当てられます。
  • 「わずかに最適化された」Active Recordでは、11097個のオブジェクトが割り当てられます。
  • 「大きく最適化された」Active Recordでは、5098個のオブジェクトが割り当てられます。

上のいずれについても、差は4桁を上回ります。

ナイーブな実装とlazyな実装でのオブジェクトアロケーション数の差

Active RecordがSequelを大きく上回っていると謳っている機能のひとつに、組み込みのlazinessがあります。

何らかの理由で背後のActive Recordで余分なSELECTが行われていた場合、Active Recordのカラムは実際に使われるまでは日付に「キャスト」されることはありません。Sequelはこの点を認識しており、かつ慎重を期しています。

いいえ、Sequelは型変換を遅延実行(defer)しません。型変換はモデルレベルではなく、データセットの取得レベルで発生します。Sequelはその代わりにlazy_attributesプラグインを提供します。これはクエリの途中ではカラムをSELECTせず、必要に応じて新しいクエリを実行…(略)

Sequelが信じられないほど高速かつ高効率であるにもかかわらず、極めて慎重に検討しなければActive RecordからSequelへの移行が途方もなく困難なのは、この理由に尽きます。

高効率なlazyセレクタの「最速」サンプルというものはありません。今回の場合、idを1000個消費しているので、アロケーションが1020個程度に収まる極めて効率の高い実装であれば、Topicオブジェクトのアロケーションは仕方がないと予想するでしょう。26000個ものアロケーションは想定していません。

それをとりあえず試しに実装してみたのが以下のコードです(以下は本記事のアイデアを証明するために書いたものであり、productionレベルのシステム向けではありません)。

$conn = ActiveRecord::Base.connection.raw_connection

class FastBase

  class Relation
    include Enumerable

    def initialize(table)
      @table = table
    end

    def limit(limit)
      @limit = limit
      self
    end

    def to_sql
      sql = +"SELECT #{@table.columns.join(',')} from #{@table.get_table_name}"
      if @limit
        sql << -" LIMIT #{@limit}"
      end
      sql
    end

    def each
      @results = $conn.async_exec(to_sql)
      i = 0
      while i < @results.cmd_tuples
        row = @table.new
        row.attach(@results, i)
        yield row
        i += 1
      end
    end

  end

  def self.columns
    @columns
  end

  def attach(recordset, row_number)
    @recordset = recordset
    @row_number = row_number
  end

  def self.get_table_name
    @table_name
  end

  def self.table_name(val)
    @table_name = val
    load_columns
  end

  def self.load_columns
    @columns = $conn.async_exec(<<~SQL).column_values(0)
      SELECT COLUMN_NAME FROM information_schema.columns
      WHERE table_schema = 'public' AND
        table_name = '#{@table_name}'
    SQL

    @columns.each_with_index do |name, idx|
      class_eval <<~RUBY
        def #{name}
          if @recordset && !@loaded_#{name}
            @loaded_#{name} = true
            @#{name} = @recordset.getvalue(@row_number, #{idx})
          end
          @#{name}
        end

        def #{name}=(val)
          @loaded_#{name} = true
          @#{name} = val
        end
      RUBY
    end
  end

  def self.limit(number)
    Relation.new(self).limit(number)
  end
end

class Topic2 < FastBase
  table_name :topics
end

続いて以下を用いて測定します。

a = []
Topic2.limit(1000).each do |t|
   a << t.id
end
a
Total allocated: 84320 bytes (1012 objects)

つまり、オブジェクトアロケーション数が1012個の同じようなAPIを管理できるということです。オブジェクトアロケーション数が26000個にも達するAPIではなく。

それは果たして問題か

簡単なベンチマークを取ってみればわかります。

Calculating -------------------------------------
               magic    256.149  (± 2.3%) i/s -      1.300k in   5.078356s
                  ar     75.219  (± 2.7%) i/s -    378.000  in   5.030557s
           ar_select    196.601  (± 3.1%) i/s -    988.000  in   5.030515s
            ar_pluck      1.407k (± 4.5%) i/s -      7.050k in   5.020227s
                 raw      3.275k (± 6.2%) i/s -     16.450k in   5.043383s
             raw_all    284.419  (± 3.5%) i/s -      1.421k in   5.002106s

Railsが75回繰り返す間に、上でmagicと書かれている私たちの実装では256回の繰り返しを実行しています。この実装は、繰り返しにおいてRailsの実装よりも著しく改善されており、速度の向上とメモリ割り当ての著しい削減によるプロセスメモリの削減の両面で成果を出しています。余分なSELECTという理想的とは言い難い手法を用いているにもかかわらず、です。実際私たちの実装は、慎重に1カラムだけをSELECTしたRailsすら上回るほど高速です。

言ってみれば、Rubyにまったく手を加えずに達成できるRails 3×3です🎊。

もうひとつ興味深いのは、Railsが提供しなければならないターボの利いたpluckが、生SQLと比べてかなり遅い点です。実際Discourseでは、まさにこの理由でpluckにモンキーパッチを当てました(これのRails 5.2版もあります)。

この肥大化が起きる理由

メモリのプロファイルを見てみると、この肥大化が起きる理由が複数見当たります。

  1. Railsのlazinessが微妙:ここでの1000個もの文字列アロケーションは普通は決して見かけないものです。これでは「lazyアロケーション」ではなく半端な「lazyキャスティング」です。
  2. どの行もオブジェクトアロケーションを3個ずつ追加している(記録とマジックのために): 追加されるのはActiveModel::Attribute::FromDatabaseActiveModel::AttributeSetActiveModel::LazyAttributeHashです。カラムへのインデックスを結果セットに保持する配列を1つ使い回せば、これらはいずれも不要になります。
  3. Railsは、取り出したデータが既に「正しいフォーマット」(数値など)である場合にも、キャストをヘルパーオブジェクトにディスパッチすることにこだわる: これにより余分な帳簿が生成されます。
  4. 使われるあらゆるカラム名がクエリで2度アロケーションされる: これは簡単にキャッシュして再利用可能でしょう(SELECTされたそれらのカラム名をクエリビルダが認識していれば、その結果セットを再度問い合わせる必要はありません)

ではどうするべきか

Active Recordの内部を注意深く点検し、行あたりのオブジェクトアロケーションを著しく削減する実装を検討する必要があるように思えます。また、PG gemのネイティブ型キャストを活用してデータベースから文字列を再度取得することを避け、単に数値に逆変換することも試すべきです。

本記事で評価に用いたスクリプトはGistでご覧いただけます。

関連記事

Ruby/Railsのプロ開発者としての5年間を振り返る(翻訳)

Rails: ActiveRecord::Relationで生SQLは避けよう(翻訳)

Rails: Event Storeの新しいAPIを解説する(翻訳)

$
0
0

概要

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

Rails: Event Storeの新しいAPIを解説する(翻訳)

Rails Event Store v0.26に新しい素敵なAPIがあります。変更点の一部をざっと見てみましょう。

永続的なサブスクライバとハンドラー

以前のsubscribeは、handler(インスタンス/クラス/procのいずれか)とevent_type(ハンドラーがサブスクライブされたイベントの種類)という2つの引数を取りました。

class OrderSummaryEmail
  def call(event)
    order = Order.find(event.data.fetch(:event_id))
    OrderMailer.summary(order).deliver_later
  end
end

client = RailsEventStore::Client.new
client.subscribe(OrderSummaryEmail.new, [OrderPlaced])

今回の変更で、以下のように書けるようになりました。

client.subscribe(OrderSummaryEmail.new, to: [OrderPlaced])

to:という名前付き引数のおかげでずっと読みやすくなったと思います。

さらに、Procのサブスクライブ方法が大きく改善されています。従来は以下のように書きました。

OrderSummaryEmail = -> (event) {
  order = Order.find(event.data.fetch(:event_id))
  OrderMailer.summary(order).deliver_later
}

client = RailsEventStore::Client.new
client.subscribe(OrderSummaryEmail, [OrderPlaced])

今後は以下のようにブロックを直接渡せるようになります。

client = RailsEventStore::Client.new
client.subscribe(to: [OrderPlaced]) do |event|
  order = Order.find(event.data.fetch(:event_id))
  OrderMailer.summary(order).deliver_later
end

一時的なサブスクライバとハンドラー

以前の一時的なサブスクライバに用いられていたAPIはどうも好きになれません。以前は以下のような感じでした。

client = RailsEventStore::Client.new
client.subscribe(OrderSummaryEmail, [OrderPlaced]) do
  PlaceOrder.call
end

2つのコードブロックをきれいに渡す方法が定まっておらず、不便でした。サブスクライバ用のコードブロックと、一時的なサブスクライバの中で有効にしておきたいコード片用のコードブロックは今まで次のような感じで書きました。

order_summary_email = -> (event) {
  order = Order.find(event.data.fetch(:event_id))
  OrderMailer.summary(order).deliver_later
}

client = RailsEventStore::Client.new
client.subscribe(order_summary_email, [OrderPlaced]) do
  PlaceOrder.call
end

興味深いことに、ActiveSupport::Notificationsにも同様の制約があります。

subscriber = lambda {|*args| ... }
ActiveSupport::Notifications.subscribed(subscriber, "sql.active_record") do
  # ...
end

今後使える新しいAPIでは次のようになりました。

client = RailsEventStore::Client.new
client.within do
  PlaceOrder.call
end.subscribe(to: [OrderPlaced]) do
  order = Order.find(event.data.fetch(:event_id))
  OrderMailer.summary(order).deliver_later
end.call

チェイン可能なAPIはコントローラで使うことも、内部で起きたことをインポートして調べることもできます。

client.within do
  PlaceOrder.call
end.subscribe(to: [OrderPlaced]) do |ev|
  head :ok
end.subscribe(to: [OrderRejected]) do |ev|
  render json: {errors: [...]}
end.call
success = 0
failure = 0
client.within do
  ImportCustomer.call
end.subscribe(to: [CustomerImported]) do |_|
  success += 1
end.subscribe(to: [CustomerImportFailed]) do |_|
  failure += 1
end.call

もちろんサブスクライバは従来どおり最初の引数に渡せます。サブスクライバはブロックでなくても構いません。

client.within do
  PlaceOrder.call
end.subscribe(order_summary_email, to: [OrderPlaced]).call

AggregateRoot#on

AggregateRootで、(オブジェクトに適用されようとしているイベントに応答する)ハンドラーメソッドを簡単に定義できるようになりました。これにより、デフォルトの規則であるdef apply_order_submitted(event)などのアンダースコアを含むメソッド名を使わなくとも、on OrderSubmitted do |event|と書けば済むようになりました。

以下は従来の書き方です(現在も使用可能)。

class Order
  include AggregateRoot
  class HasBeenAlreadySubmitted < StandardError; end
  class HasExpired < StandardError; end

  def initialize
    @state = :new
  end

  def submit
    raise HasBeenAlreadySubmitted if state == :submitted
    raise HasExpired if state == :expired
    apply OrderSubmitted.new(data: {delivery_date: Time.now + 24.hours})
  end

  def expire
    apply OrderExpired.new
  end

  private
  attr_reader :state

  def apply_order_submitted(event)
    @state = :submitted
    @delivery_date = event.data.fetch(:delivery_date)
  end

  def apply_order_expired(_event)
    @state = :expired
  end
end

以下は新しい書き方です。

class Order
  include AggregateRoot
  class HasBeenAlreadySubmitted < StandardError; end
  class HasExpired < StandardError; end

  def initialize
    @state = :new
  end

  def submit
    raise HasBeenAlreadySubmitted if state == :submitted
    raise HasExpired if state == :expired
    apply OrderSubmitted.new(data: {delivery_date: Time.now + 24.hours})
  end

  def expire
    apply OrderExpired.new
  end

  on OrderSubmitted do |event|
    @state = :submitted
    @delivery_date = event.data.fetch(:delivery_date)
  end

  on OrderExpired do |_event|
    @state = :expired
  end

  private

  attr_reader :state
end

on OrderSubmitted do |event|という記法のありがたい点は、コードベースのどこでOrderSubmitted使われているかをgrepで探しやすいところです。

Rails Event Storeをさらに使いやすく書きやすくするためのアイデアをもっと知りたい方は、必要に応じて以下もご覧ください。

もっと読む

本記事を気に入っていただけましたら、ぜひニュースレターの購読をお願いします。私たちが日頃Railsアプリを安心してメンテナンスできるようにするための取り組みやソリューションをご覧いただけます。

合わせて以下の記事もどうぞ。

弊社の最新刊『Domain-Driven Rails』もぜひどうぞ。特に、巨大で複雑なRailsアプリを扱ってる方に有用です。

関連記事

Rails: ElasticSearchとRedis Streamsのストリームを使う(翻訳)

Rails: RedisとRediSearchで時系列データを扱う(翻訳)

Rails: Event Storeライブラリの非推奨APIをparser gemで書き直す(翻訳)

$
0
0

概要

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

Rails: Event Storeライブラリの非推奨APIをparser gemで書き直す(翻訳)

次にリリースされるRails Event Storeでは、既存のリーダーメソッドを非推奨にする予定です。これらのメソッドはもっとスムースなクエリインターフェイス(Active Recordで有名になった方式)に置き換わります。移行を少しでも楽にするため、指定のコードベースで新しいAPIを使うよう移行するスクリプトを用意しました。

手短に申し上げると、従来の6種類のクエリメソッドはイベントのストリームを順方向/逆方向で読み出し、イベントの特定の限界に達したらそこで終わるか、あるいは終わらずにストリームの最初や指定の位置に戻ることができました。以下はコード例です。

client.read_events_backward('Order$1', limit: 5, start: :head).each do |event|
  # Order$1ストリームから逆方向に最大5つのイベントを読み出して何かする
end

今回、このインターフェイスを次のように変更することに決めました

spec = client.read.stream('Order$1').from(:head).limit(5).backward
spec.each do |event|
  # Order$1ストリームから逆方向に最大5つのイベントを読み出して何かする
end

APIの非推奨化は難しくなさそうでした。古いメソッド呼び出しでwarningを表示し、そしておそらく新しい利用法を示せばよさそうです。

specify do
  expect { client.read_events_backward('some_stream') }.to output(<<~EOS).to_stderr
    RubyEventStore::Client #read_events_backward は非推奨化

    Use following fluent API to receive exact results:
    client.read.stream(stream_name).limit(count).from(start).backward.each.to_a
  EOS
end

しかしエンドユーザーからすれば、移行作業は見た目よりも面倒になります。

  • コードベース全体で多数使われている
  • 非推奨warningを表示するために、関係のあるコードのパスをすべて調べて回らないといけない可能性がある
  • 利用法がすべて同じとは限らず(リーダーメソッドのキーワード引数が異なる)、デフォルト値も考慮に入れなければならない可能性もある(暗黙で最大100の制限があるなど)

もちろん、コードベースでの利用法を調べ上げて手動で置き換えるかsedを少々駆使すれば何とかなるかもしれませんが、もっとよいやり方があります。すなわちRubyを使ってRubyを書き換えられるのです。この素晴らしいparser gemを導入しましょう。

gem ins parser

このgemは、置き換えたいコードがAST(抽象構文木)でどのような形になるかを最初にすべて分析します。先ほどの例を食わせてみましょう。

ruby-parse -e "client.read_events_backward('Order$1', limit: 5, start: :head)"

(send
 (send nil :client) :read_events_backward
 (str "Order$1")
 (hash
   (pair
     (sym :limit)
     (int 5))
   (pair
     (sym :start)
     (sym :head))))

上の結果から、:read_events_backwardは何らかのクライアントレシーバーと思われるものに送信されたメッセージであることがわかります。引数、位置、キーワードの表現方法もASTノードとして示されます。

パズルの次のピースはParser::Rewriter(最新リリースのparserならParser::TreeRewriter)です。これは以下を用いてASTノードを改変できます。

insert_after(range, content)
insert_before(range, content)
remove(range)
replace(range, content)

引数は何でしょうか?(引数の)内容はコードの文字列です。この場合、client.read.stream('Order$1').from(:head).limit(5).backward.each.to_aになるでしょう。rangeを用いると少々複雑になります。ruby-parse -Lを用いてさらに秘密に迫ってみましょう。

ruby-parse -L -e 'client.read_events_backward(\'Order$1\', limit: 5, start: :head)'

s(:send,
  s(:send, nil, :client), :read_events_backward,
  s(:str, "Order$1"),
  s(:hash,
    s(:pair,
      s(:sym, :limit),
      s(:int, 5)),
    s(:pair,
      s(:sym, :start),
      s(:sym, :head))))
client.read_events_backward('Order$1', limit: 5, start: :head)
      ~ dot
       ~~~~~~~~~~~~~~~~~~~~ selector                         ~ end
                           ~ begin
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ expression
s(:send, nil, :client)
client.read_events_backward('Order$1', limit: 5, start: :head)
~~~~~~ selector
~~~~~~ expression
s(:str, "Order$1")
client.read_events_backward('Order$1', limit: 5, start: :head)
                            ~ begin ~ end
                            ~~~~~~~~~ expression
s(:hash,
  s(:pair,
    s(:sym, :limit),
    s(:int, 5)),
  s(:pair,
    s(:sym, :start),
    s(:sym, :head)))
client.read_events_backward('Order$1', limit: 5, start: :head)
                                       ~~~~~~~~~~~~~~~~~~~~~~ expression
s(:pair,
  s(:sym, :limit),
  s(:int, 5))
client.read_events_backward('Order$1', limit: 5, start: :head)
                                            ~ operator
                                       ~~~~~~~~ expression
s(:sym, :limit)
client.read_events_backward('Order$1', limit: 5, start: :head)
                                       ~~~~~ expression
s(:int, 5)
client.read_events_backward('Order$1', limit: 5, start: :head)
                                              ~ expression
s(:pair,
  s(:sym, :start),
  s(:sym, :head))
client.read_events_backward('Order$1', limit: 5, start: :head)
                                                      ~ operator
                                                 ~~~~~~~~~~~~ expression
s(:sym, :start)
client.read_events_backward('Order$1', limit: 5, start: :head)
                                                 ~~~~~ expression
s(:sym, :head)
client.read_events_backward('Order$1', limit: 5, start: :head)
                                                        ~ begin
                                                        ~~~~~ expression

ruby-parse-Lスイッチを付けると、ASTノードごとに範囲を親切に示してくれます。パースされたコードの特定の位置を参照するのにこれを使えます。

たとえば、以下のASTではnode.location.selectorclient.('Order$1', limit: 5, start: :head)の間の領域を参照していることがわかります。

s(:send,
  s(:send, nil, :client), :read_events_backward,
  s(:str, "Order$1"),
  s(:hash,
    s(:pair,
      s(:sym, :limit),
      s(:int, 5)),
    s(:pair,
      s(:sym, :start),
      s(:sym, :head))))
client.read_events_backward('Order$1', limit: 5, start: :head)
      ~ dot
       ~~~~~~~~~~~~~~~~~~~~ selector                         ~ end
                           ~ begin
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ expression

さらに、複数の範囲を結合することもできます。node.location.selector.join(node.location.end)を呼び出すと、read_events_backward('Order$1', limit: 5, start: :head)の範囲がわかります。探していたのはまさしくこれです!

ここまでは順調ですが、さてreplaceする正確なnodeを取得するにはどうしたらよいでしょうか。このParser::RewriterクラスはParser::AST::Processorクラスの子孫です。このクラスにパース済みASTとソースバッファを与えると、一致するツリーが見つかったときに即座にメソッドハンドラを呼び出します。

class DeprecatedReadAPIRewriter < ::Parser::Rewriter
  def on_send(node)
    _, method_name, *args = node.children
    replace_range = replace_range.join(node.location.end)

    case method_name
    when :read_events_backward
      replace(replace_range, "read.stream('Order$1').from(:head).limit(5).backward.each.to_a")
    end
  end
end

上のコード例では、read_events_backwardメソッドに渡された引数について何も考慮していません。私たちはTDDフローで最初の例に着目し、より詳細なテストexampleを与えることでコードをさらに一般化できるので、これは好都合です。

ちゃんと動くよう、インフラを完成させます。

RSpec.describe DeprecatedReadAPIRewriter do
  def rewrite(string)
    parser   = Parser::CurrentRuby.new
    rewriter = DeprecatedReadAPIRewriter.new
    buffer   = Parser::Source::Buffer.new('(string)')
    buffer.source = string

    rewriter.rewrite(buffer, parser.parse(buffer))
  end

  specify 'take it easy' do
    expect(rewrite("client.read_events_backward('Order$1', limit: 5, start: :head)"))
      .to eq("read.stream('Order$1').from(:head).limit(5).backward.each.to_a")
  end
end

この記事でやったことをまとめると、RubyコードをパースしてASTにし、それを別の新しいものに変換するための知識を読み取る方法を学びました。そしてこれはまだ氷山の一角でしかありません。

学習のためにRails Event Storeリポジトリにある完全なDeprecatedReadAPIRewriterスクリプトとspecをご覧いただけます。

もっと知りたい方へ

本記事を気に入っていただけた方は、Arkencyのニュースレターの購読をお願いします。開発者を悪い意味で驚かせないRailsアプリを構築するための弊社の日々の取り組みのノウハウを届けします。

以下の関連記事もお楽しみいただけるかと思います。

弊社の最新刊『Domain-Driven Rails』もぜひどうぞ。特に、巨大で複雑なRailsアプリを扱ってる方に有用です。


blog.arkency.comより

関連記事

Rails: Event Storeの新しいAPIを解説する(翻訳)


Ruby: Rack PushでリアルタイムWebアプリをWebから切り離す(翻訳)

$
0
0

概要

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

Ruby: Rack PushでリアルタイムWebアプリをWebから切り離す(翻訳)

原注: (更新情報: プルリクが更新されたので本記事に反映しました)

何だか面白くなってきました。

最近はWebSocketや、その年上の従兄弟であるEventSourceやらServer-Sent Events(SSE)の話題でもちきりです。Fayeや(Railsの)ActionCableも大流行で、リアルタイム更新処理がかつてないほどやりやすくなりました。

しかしリアルタイム更新処理は混乱の極みであり、セットアップもメンテナンスも困難です。パフォーマンスはこの際横に置いておくとしても、要するに既存の設計のコストが増大します。開発工数もハードウェアコストも高くつきます。

しかしRackリポジトリに出現した新しいPR #1272は、これらが今後すべて変革されることを約束しています(訳注: 同PRは翻訳公開時点でopenの状態です)。

このPRは、私たちのコードベースを短縮し、実時間のパフォーマンスを改善してリアルタイムWebアプリの総コストを削減するうえで大きな一歩となります。

これはWebアプリをWebから切り離すための重要な一歩です。

RackはRailsやSinatraなどのRubyフレームワークのインターフェイスであり、Webアプリはこれを用いてRubyアプリケーションサーバーとやりとりすることを思い出しましょう。Rackは広く使われているので影響力は小さくありません。

問題点のまとめ

現状の標準的なアプローチの問題点を簡単にまとめると、リアルタイム機能のサポートのためにリアルタイムアプリの各プロセスがサーバーを2つずつ実行しなければならないという点です。

2つのサーバーは同じポートをリッスンする可能性もあれば、何らかのgemによって隠蔽される可能性もありますが、最終的には2つの異なるI/Oイベントハンドリングの単位が同時に走っていなければならないのです。

「それはまたどうして?」とお思いの方向けにお答えいたします(気にならない方は問題解決のセクションまでスキップしてください)。

一時的なhijackの話

Rackでここ5年間唯一利用可能な「標準」ソリューションである、一時的な間に合わせのソリューションについてお話しします。

Rackの歴史上のある時点で、long pollingなどのHTTPテクニックをサポートする方法が仕様上必要とされていました。特にRails 4.0は「ライブストリーミング」機能的なものを必要としていました。

そのためにRackチームはhijack APIアプローチを採用しました。

このアプローチは迫りくるニーズのための間に合わせの修正で、Rack 2.0がリリースされるためのあくまで一時的な措置でした(Rackプロトコルのバージョンは5年経った今も1.3です)。

アプリはhijack APIを用いることでソケットを完全に制御できます。サーバーのソケットをハイジャックするだけで、たちまちlong pollingやSSEなどをサポートできました。

そしてここから問題がややこしくなっていったのです。

当時はソケットを扱うために大量のネットワークロジックをサーバー層からアプリ層にコピーしなければなりませんでした(write呼び出しのバッファリング、着信データ、プロトコル管理、タイムアウト処理など)。

これは明らかにSOLIDの「S」(単一責任の原則)に違反しています。アプリやフレームワークにI/Oハンドリングの責務を押し付けているからです。

そしてDRY原則にも違反しています。I/Oハンドリングのロジックが複製されてしまうからです(1つはサーバー内、もう1つはアプリ/フレームワーク)。

さらにこのアプローチはネットワークプロトコルとアプリが複雑に絡み合うため、HTTP/2接続でいくつもの問題が生じます。

hijackの明らかなコスト

hijackアプローチは多大なコストを要します。隠れたコストもありますが、目に見えてわかるコストはそれを上回ります。

最も目に付きやすいコストはメモリ、パフォーマンス、そして開発工数です。

hijackベースのソリューションではコードが重複して余分な処理が発生するため、メモリ消費量が増加し、パフォーマンスも低下します(システムコール数の増加やコンテキストスイッチの増加など)。

require 'faye'でアプリにWebSocketが追加されますが、このgemの読み込みだけで9Mbを要します(しかもサーバーが実際に動き出す前の時点です)。

一方、agoo HTTPサーバーまたはiodine HTTPサーバーを用いれば、WebSocketとSSEの両方がアプリに追加され、しかも余分なメモリを消費しません。

具体的には、iodineのメモリ消費量は2MbでPumaをわずかに下回り、しかもHTTPとリアルタイム機能の両方を提供してくれます。

hijackの隠れたコスト

さらに微妙なコストは、ハードウェアが高価なことと、hijack時の1台のマシンで扱えるクライアント数が少ないことです。

どうしてこうなるのでしょうか。

hijackアプローチはパフォーマンス面で劣るのみならず、一部のHTTPサーバーがselectシステムコールに依存してしまいます(前回ざっと見たときにはPumaがselectを使っていました)。

このselectシステムコールを用いると、オープンファイル数の上限が1024にまで落ちてしまい、プロセスあたりのオープン接続数が最大1024に制限されると見込まれます。

ある接続がハイジャックされると、そのソケットはWebサーバーが期待するほど速く閉じられず、最終的にオープンファイル数が1024を超えたときに破損またはクラッシュの可能性があります。

解決方法: コールバックとイベント

Rackで提案された新しいPR #1272は、WebSocketやSSEを効果的に実装する素晴らしい方法を提供し、かつアプリがサーバーのことを一切意識せずに済むようになります。

新しい提案ではネットワークやI/Oのハンドリングはサーバー側の責務のままとしたことで、アプリのコードベースがシンプルになり、ネットワークプロトコルから切り離されています。

アプリはあらゆるイベントの通知をコールバックオブジェクトを用いて受け取ります。これによりアプリがネットワーク層から解放され、データに専念できるようになります。

コールバックオブジェクトは、アプリを実行するサーバーや背後のプロトコルについて一切関知する必要がありません。

コールバックオブジェクトは、Rubyのextendアプローチを用いて正しいAPIに自動リンクされるので、アプリはサーバーについて何も気にする必要がありません(原注: PRはその後更新され、extendアプローチはclientオブジェクトをさらに追加する方法に置き換えられました)。

しくみ

あらゆるRackサーバーは、ハッシュ型オブジェクトを用いてRackアプリとやりとりします。

RailsやSinatra、そしてあらゆるRackアプリやフレームワークはこのしくみの上に構築されています。これはRackの現在の仕様に記載されています。

Rackを用いたシンプルなhello worldは次のようになります(config.ruというファイルに保存します)。

# 通常の HTTP response
RESPONSE = [200, { 'Content-Type' => 'text/html',
          'Content-Length' => '12' }, [ 'Hello World!' ] ]
# `env`変数を取り出す
APP = Proc.new {|env| RESPONSE }
# アプリの実行に使うRack DSL
run APP

新しい提案では、env['rack.upgrade?']という変数が導入されます。

通常、この変数はnilに設定されます(envハッシュに含まれません)。

しかしWebSocket接続の場合はenv['rack.upgrade?']:websocketに設定され、EventSource(SSE)接続の場合は:sseに設定されます。

コールバックオブジェクトを設定するには、env['rack.upgrade']を導入します(メソッド名に?がない点にご注意)。

設計は次のような感じになります。

# config.ruに配置
RESPONSE = [200, { 'Content-Type' => 'text/html',
          'Content-Length' => '12' }, [ 'Hello World!' ] ]

# Callbackクラスの例
class MyCallbacks
  def on_open client
    puts "* Push connection opened."
  end
  def on_message client, data
    puts "* Incoming data: #{data}"
    client.write "Roger that, \"#{data}\""
  end
  def on_close client
    puts "* Push connection closed."
  end
end

# `env`変数を取り出す
APP = Proc.new do |env|
  if(env['rack.upgrade?'])
    env['rack.upgrade'] = MyCallbacks.new
    [200, {}, []]
  else
    RESPONSE
  end
end
# アプリの実行に使うRack DSL
run APP

このアプリをagooサーバーまたはiodineサーバーで実行し、マジックを花開かせます。

たとえばiodineを使う場合は以下のようにします。

# iodineをインストール(バージョン0.6.0以降)
gem install iodine
# シングルスレッドモードで起動
iodine -t 1

localhost:3000をブラウザで開き、ブラウザコンソールでJavaScriptを少々テストしてみましょう。

まずはEventSource(SSE)接続から(以下をブラウザコンソールで実行)。

// SSE実行例
var source = new EventSource("/");
source.onmessage = function(msg) {
  console.log(msg.id);
  console.log(msg.data);
};

もちろん通知を送信してませんのでこれだけでは何にも起きませんが、これでSSE接続がオープンしました。

WebSocketでもやってみましょう(以下をブラウザコンソールで実行)。

// WebSocket実行例
ws = new WebSocket("ws://localhost:3000/");
ws.onmessage = function(e) { console.log(e.data); };
ws.onclose = function(e) { console.log("closed"); };
ws.onopen = function(e) { e.target.send("Hi!"); };

おお!Rubyコンソールを見てみるとWebSocketが確かに動いています。実に簡単ですね。

同じコード例はagooサーバーでも完全に動作します(agooもiodimeもRack Push提案を既にサポートしています)。

以下をお試しください。

# agooサーバーをインストール(バージョン2.1.0以降)
gem install agoo
# 起動
rackup -s agoo -p 3000

余分なgemも、余分なコードも、大量のメモリも使わずに、Rubyサーバーと生Rackだけでできるのです(あえてフレームワークを用いませんでした)。

素晴らしいPushを味わう

シンプルさはこれでわかりますが、まだどれだけ強力なのかがピンときません。

株式相場表示やタイマーを実装するとしましょう。ここではタイマーで考えます。

# config.ruに配置
RESPONSE = [200, { 'Content-Type' => 'text/html',
          'Content-Length' => '12' }, [ 'Hello World!' ] ]

# グローバルなライブ接続ストレージ
module LiveList
  @list = []
  @lock = Mutex.new
  def <<(connection)
    @lock.synchronize { @list << connection }
  end
  def >>(connection)
    @lock.synchronize { @list.delete connection }
  end
  def any?
    # 「live list」への接続を削除
    @lock.synchronize { @list.any? }
  end
  # 同じプロセスを共有する全接続にメッセージを1件送信する
  # (クラスタモードでは部分的なブロードキャストのみとなるのでこの方法はスケールしない)
  def broadcast(data)
    # リストをコピーして、クリティカルセクションを長時間実行しないようにする
    tmp = nil # スコープのこの部分をtmpに置く
    @lock.synchronize do
      tmp = @list.dup # リストをtmpにコピー
    end
    # クリティカルセクションの外でリストを反復
    tmp.each {|c| c.write data }
  end
  extend self
end

# まさにこの瞬間に時刻をブロードキャストするが...
# クラスタモードではスレッドが「中断する」
@thread = Thread.new do
  while(LiveList.any?) do
    sleep(1)
    LiveList.broadcast "The time is: #{Time.now}"
  end
end

# 静的なCallbackモジュールの例
module MyCallbacks
  def on_open client
    # 「live list」への接続を追加
    LiveList << client
  end
  def on_message(client, data)
    # ブロードキャストの単なる例
    LiveList.broadcast "Special Announcement: #{data}"
  end
  def on_close client
    # 「live list」への接続を削除
    LiveList >> client
  end
  extend self
end

# Rackアプリ
APP = Proc.new do |env|
  if(env['rack.upgrade?'])
    env['rack.upgrade'] = MyCallbacks
    [200, {}, []]
  else
    RESPONSE
  end
end
# アプリの実行に使うRack DSL
run APP

iodineサーバーをシングルプロセスモードで起動する(iodine -w 1)と、小さなタイマーが点滅します。

正直、上で私が書いたサンプルコードは好きになれません。何だか長くて頼りないし、iodineをクラスタモードで使えません。

そこで次のコード例として、(コメント含みで)32行のチャットルームをこしらえました。

LiveListモジュールやタイマースレッドを回避するため、iodineのpub/sub拡張APIを使うことにします。タイマーは別に欲しくないのでIodine.run_everyメソッドはスキップします。

また、やり取りの相手はWebSocketクライアントに限定します。私が動かせることを示すためです。

こちらの方が新しいenv['rack.upgrade']アプローチがもたらす真価がよくわかりますし、クラスタモードでもちゃんと動作します。

残念ながら、今度のコード例はagooサーバーでは今のところ動作しません。

# config.ruに配置
RESPONSE = [200, { 'Content-Type' => 'text/html',
          'Content-Length' => '12' }, [ 'Hello World!' ] ]
CHAT = "chat".freeze
# Callbackクラス
class MyCallbacks
  def initialize env
     @name = env["PATH_INFO"][1..-1]
     @name = "unknown" if(@name.length == 0)
  end
  def on_open client
    client.subscribe CHAT
    client.publish CHAT, "#{@name} joined the chat."
  end
  def on_message client, data
    client.publish CHAT, "#{@name}: #{data}"
  end
  def on_close client
    client.publish CHAT, "#{@name} left the chat."
  end
end
# 実際のRackアプリ
APP = Proc.new do |env|
  if(env['rack.upgrade?'] == :websocket)
    env['rack.upgrade'] = MyCallbacks.new(env)
    [200, {}, []]
  else
    RESPONSE
  end
end
# アプリの実行に使うRack DSL
run APP

コマンドライン(ターミナル)でアプリを起動します。

iodine

ブラウザコンソールで以下を実行します。

ws = new WebSocket("ws://localhost:3000/Mitchel");
ws.onmessage = function(e) { console.log(e.data); };
ws.onclose = function(e) { console.log("Closed"); };
ws.onopen = function(e) { e.target.send("Yo!"); };

原注: agoo 2.1.0からpub/sub拡張が実装されましたが、使われているセマンティクスが若干異なります。同じコード例がどちらのサーバーでも動くよう手を尽くしてみました。

なぜ今まで誰も思いつかなかったのか

実は、これはまったく新しいアイデアというわけではありません。

hijack API自体が提案されたときですら、別のアプローチが提案されていました

数年前にも別の提案が試みられていました。

しかし最終的に状況が変わり、agooサーバーとiodineサーバーがどちらも既に新しいアプローチをサポートするようになりました。

期待できそうです。

原注: 本記事のコード例は、Rack仕様へのPR変更に基づいて更新しました。

関連記事

Railsで学ぶSOLID(1): 単一責任の原則(翻訳)

技術的負債を調査する10のポイント(翻訳)

Rails: productionでCPU usageが100%になる問題をデバッグする(翻訳)

$
0
0

概要

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

Rails: productionでCPU usageが100%になる問題をデバッグする(翻訳)

production環境のRailsシステムでCPU usageの上昇をデバッグするときはどうやっていますか?


今日、ある顧客のコンテナでCPU usageが異常に高くなっていることに気づきました。

# top -bn1

190 discour+  20   0 2266292 205128  15656 S  86.7  0.3   9377:35 ruby

# ps aux

discour+   190 19.4  0.3 2266292 207096 ?      Sl    2017 9364:38 sidekiq 5.0.5 discourse [1 of 5 busy]

どうもsidekiqのジョブが詰まっているようです。

詰まっている場所はどこでしょうか。

普通ならそこで話はおしまいで、次のような他の疑問に進むところです。

  • staging環境やdevelopment環境で再現できるか?
  • 最近変更されたコードはどれか?
  • perf traceでろくな情報が取れないのはなぜか?
  • Sidekiqのログ出力はどのぐらいイケてるか?
  • 魔法の占い杖はどこかにないのか?

Julia Evansが取り組んでいる素敵なプロファイラを使えば、この種の問題の回答をたちどころに得ることができます。しかしそれまでの間、さしあたって何か打つ手はないものでしょうか?

rbtrace + stackprof

DiscourseのGemfileにはrbtracestackprofが常備されています。

gem 'rbtrace'
gem 'stackprof', require: false

私たちのproduction環境ではrbtraceを常備しているので、多種多様なデバッグをproductionレベルで行えます。stackprofは必要に応じて読み込みます。

今回のような場合なら以下を実行すれば一発です。

# rbtrace -p 190 -e 'Thread.new{ require "stackprof"; StackProf.start(mode: :cpu); sleep 2; StackProf.stop; StackProf.results("/tmp/perf"); }'

これでstackprofをグローバルに有効にする新しいスレッドが1つ注入され、最終的に/tmp/perfにパフォーマンスデータが出力されます。

ダンプの解析は簡単です。

# stackprof /tmp/perf
==================================
  Mode: cpu(1000)
  Samples: 475 (0.63% miss rate)
  GC: 0 (0.00%)
==================================
     TOTAL    (pct)     SAMPLES    (pct)     FRAME
       473  (99.6%)         473  (99.6%)     #<Class:0x00007fac08218020>::Typography.to_html_french
         4   (0.8%)           1   (0.2%)     #<Module:0x00007fac080c3620>.reap_connections

実は、stackprofではバックトレースの収集はもちろん、フレームグラフまで生成できるので、READMEをしっかり読んでおくことを強くおすすめします。

いたいた、犯人はこんなところにいました。

text.gsub(/(\s|)+([!?;]+(\s|\z))/, ' \2\3')

正規表現がproductionでCPUを100%使い切っていたのです。🧙‍の最初のお手柄でした。

関連記事

TestProf: Ruby/Railsの遅いテストを診断するgem(翻訳)

週刊Railsウォッチ(20180723)Railsdm Day 3 Extremeを後追い、PSDにはZeplin.io、好みの分かれるJSX、負荷テストツール比較ほか

$
0
0

こんにちは、hachi8833です。2週間ぶりのご無沙汰でした。暑さでもうろうとしています☀

各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ

今回のつっつきも、BPSの福岡拠点であるウイングドアの皆さまと画面共有しながら行いました。いつもありがとうございます。

⚓【お知らせ】週刊Railsウォッチ「公開つっつき会 第1回」を8/2(木)に開催

週刊Railsウォッチを日頃ご愛顧いただきありがとうございます。
ウォッチを出す前に毎週木曜に社内で「つっつき会」を催していますが、このたび8/2(木)に「公開つっつき会 第1回」を開催するはこびとなりました。以下のTECH PLAYページからエントリーいただけますので、つっつき会を覗いてみたい方はお気軽にご参加ください。普段から質問や発言も自由な気楽な集まりとして行っていますので、普段なかなかできない質問やツッコミなどもお気軽にどうぞ。

⚓特集: Rails Developer Meetup 2018 Day 3 Extremeその後

このボリュームを一日でやるとは…😭今週のウォッチはその分後半のエントリを意識的に減らしました。


つっつきボイス: 「文字どおりエクストリームなボリュームでしたね…キャンセル待ちもならず😢」「スケジュールの密度ちょっと高いかなー😅」「今年3月のRailsdmのときは自分たちも発表しました↓が、タバコ吸ったら休憩時間終わっちゃう感じでした🚬」「ともあれ、Railsdmは技術プレゼンの質もいいし、それでいて技術系に限定されないバラエティ豊かな点も貴重😋」「😃」「あと現場のつらみの話をいろいろ聞けるのもいいですねー: 単なる新しい技術の話とかはググれば済むけど、試してはまった話は実際にやってみた人じゃないとできないので」「毎回出席は体力的に大変ですが」「後追いで興味のあるプレゼンのスライドやYouTube動画で確認するだけでも役に立つし」「ストリーム中継の時点でYouTubeだからそのまま動画として残ってるのはありがたいです」

「TechRachoの舞台裏」をRails Developers Meetup 2018で発表してきました

⚓kamipoさん動画のさわり


つっつきボイス: 「とりあえずkamipoさんのActiveRecord話を見たかったので後追いでごくあっさりとチェックしてみました: 冒頭『何にも準備しなくていいと聞いてたんで』と言ってたぐらいで、その場でコミット眺めながら進めてました」

Q: 仮にMySQLだけをサポートするARを作れるとしたらどんな最適化ができそう?
A: RDBMSを絞れば確かに最適化はしやすい: mysql2 gemは既にいろいろやりすぎてて線引きがしにくいけどpg gemはやりすぎてないのでこれに絞れるなら最適化の余地は大きそう。
14:38

「PostgreSQLのRails向け拡張って結構いろいろあるし便利な機能が本当にいっぱいあるんだけど、Railsにそういうのを導入するとPostgreSQLでしか動かなくなっちゃうのでRails標準でやるのは難しいでしょうねー🤔」「やはり」「個人的にはポスグレで行って欲しいけどねっ😎

Q: #30000の次にキリ番ゲットしたら直したいARは?
A: もし取れたら、ロールバックしたときにdirty valueもロールバックしたい(今は4種類のステートだけロールバックされる)。
19:05

「dirty valueというものがあったとは…」「dirty value、自分はなるべく使わないようにしてるんだけどな…どこでどう変わるか予測しきれないことあるし」「😲」「1つ手前に戻すぐらいならいいんだけど、コミットとかし始めるとどこまで確実にロールバックできるのか正直不安だし😔」「それもそうですね…」「自分は直接dirty valueを触るよりは別の変数に取っておきたい派🤓

Q: MySQLのdefault character setをutf8からutf8mb4に変えるというのはどうでしょう?
A: MySQLのinnodbにはkey prefix lengthの制限があるので厳しいです: この問題をユーザーに押し付けていいんだったら変えられる。
22:25

「key prefix lengthの件はそのとおりで、MySQLだとinnodbのkey prefix lengthをケアしないとインデックス付けた瞬間にコケるとかあるんですよ」「ありゃー」「MySQLをバージョン8にすればいいんじゃね?っと思ったけど、database.ymlでデフォルトでutf8って書かれたらダメだろうし…実際はどうだったかな?」(RubyMineで探し始める)「あったあった: あー、やっぱりymlテンプレートのデフォルトエンコーディングはutf8か↓」「残念😢」「誰もが新しいバージョンのMySQLを使ってくれていれば問題なくデフォルトをutf8mb4に変えられるんですけど、MySQL 5.6の初期バージョンあたりを使う人たちはkamipoさんの言うようにkey prefix lengthの問題を踏むことになっちゃうので、5.7より前のサポートを打ち切るとかしないとねー🕶

# https://github.com/rails/rails/blob/bd7b61aefb4382f3cf2113c883f12bc5a3e99ae5/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml#L14
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
...

MySQLのencodingをutf8からutfmb4に変更して寿司ビール問題に対応する

Q: 非推奨にしたいActiveRecord APIは?
A: 6.0までにということだったらscoping(クラスメソッド)を殺したい(#32380の例を参照)。他にも片手ぐらいある。
24:12


Q: ActiveRecordにぜひ入れたいまったく新しい機能は?
A: 若い人に任せたい気持ちだけど、1つあるとすれば#12937。typecastしないpluckが欲しい。(年に数件は同じようなissueが上がる)。
34:56

「kamipoさんがリモート参加だったらしく音声がちょっと聞き取りにくくて、最初crackって言ってるのかなと思ったらpluckだったみたいです」「(issueを見ながら)あー、たとえばsumしたものをpluckすると勝手にtypecastされちゃうってやつか」「なのでtypecastしないバージョンのpluckが欲しいということなんですね」「確かにSQLのSELECTを手動でカスタマイズしているときなんかは生のSQL結果に近いデータが欲しいことが多いし、そういうときに余計なtypecastやられると邪魔なんですよね」

Q: 「これを殺したら速くなる」機能は?
A: (中略)たぶんdirty tracking(before_type_cast)を殺せばインスタンス生成がものすごく減ると思う。(中略)つまりAttributes APIをなくせばいい(🤣)。ちなみにDiscourseのSam Saffronが作ってるいろんなgemは、使ってないARの機能を殺すことで速くしている。
40:17

「dirty trackingは余分なこといろいろやってそうではある」「上は相当端折ってますが、dirtyに対応するために相当いろんなインスタンスが生成されてるみたいです」「でしょうねー: でないとあんなにたくさんのdirty attributesに対応できるはずがないし」「Attributes APIをなくす云々はギャグで言ってる感」「そこにありますからね😉

「Sam SaffronさんはTechRachoでも翻訳記事いくつか出してます↓」「使わないARの機能を殺すというのはそれはそれでありかもね😎」「後でRailsをアップグレードするのが大変そう」「アップグレードよりは、他のgemがその機能を使ってるかもしれないからそのあたりをケアしないといけないのが面倒かも」

Rails: productionでCPU usageが100%になる問題をデバッグする(翻訳)

⚓Form Objectの話

「全部追う時間はないので他にとりあえず気になったものとしては諸橋さんのForm Objectの話: 以前のウォッチでForm Objectの必然性がひとつ減ったかもという話が出てたので」「RailsのForm Objectは、それがうまくはまるときに使えばいいんじゃないかな: 特に最近はフォームが複雑なときはVue.jsなどのSPA的なライブラリで作ることが増えたので、その意味でもForm Objectの活躍の機会は減りつつあるかも」「😲」「フロントエンジニアが組み立てたJSONオブジェクトがPOSTされるから、Form Objectを作る必要はあまりないし、Form Objectを使う大きな理由のひとつはフォームヘルパーを使いたいからだけど、フォームヘルパーを使わなくてもフォームを生成できるなら正直どちらでやっても大差ないかなと」「確かにー」「最近Form Objectを使う人が減っているのはそういう理由もあるんじゃないかなー」「😃」「Form Objectの立ち位置はやや中途半端になりつつある感があるけど、個人的には全部サーバーサイドでやれるという点でForm Objectは好き❤

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

2週間分になってしまいました(´・ω・`)。

⚓SQLite3アダプタの「ReadOnly」オプションに対応

アダプタ側で:readonlyオプションが追加されたことに対応したそうです。

# activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L15
 module ActiveRecord
   module ConnectionHandling # :nodoc:
     def sqlite3_connection(config)
       # Require database.
       unless config[:database]
         raise ArgumentError, "No database file specified. Missing argument: database"
       end

       # Allow database path relative to Rails.root, but only if the database
       # path is not the special path that tells sqlite to build a database only
       # in memory.
       if ":memory:" != config[:database]
         config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root)
         dirname = File.dirname(config[:database])
         Dir.mkdir(dirname) unless File.directory?(dirname)
       end

+      db_opts = config.symbolize_keys.merge(results_as_hash: true)
+
       db = SQLite3::Database.new(
         config[:database].to_s,
-        results_as_hash: true
+        db_opts
       )
...

⚓#translateメソッドが_htmlサフィックスに対応

translatet)メソッドでは_htmlサフィックスが無視されていたために、html_safeを使うべきでない場所でもhtml_safeが乱用されがちだった。このPRでは訳文の配列のキーに_htmlサフィックスがある場合にhtml_safeをサポートする。
同PRより大意

# actionview/lib/action_view/helpers/translation_helper.rb#L60
       def translate(key, options = {})
         options = options.dup
         has_default = options.has_key?(:default)
         remaining_defaults = Array(options.delete(:default)).compact
...

        if html_safe_translation_key?(key)
           html_safe_options = options.dup
           options.except(*I18n::RESERVED_KEYS).each do |name, value|
             unless name == :count && value.is_a?(Numeric)
               html_safe_options[name] = ERB::Util.html_escape(value.to_s)
             end
           end
           translation = I18n.translate(scope_key_by_partial(key), html_safe_options.merge(raise: i18n_raise))
-
-          translation.respond_to?(:html_safe) ? translation.html_safe : translation
+          if translation.respond_to?(:map)
+            translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element }
+          else
+            translation.respond_to?(:html_safe) ? translation.html_safe : translation
+          end
         else
           I18n.translate(scope_key_by_partial(key), options.merge(raise: i18n_raise))
         end

つっつきボイス: 「arrayに対してtranslateするとhtml_safeされてなかった…これはバグですね🐞しかも場合によってはですが割とアブナイ」「😨」「yaml由来の内部データをtranslateで変換するなら基本的に大丈夫だけど、yamlじゃなくてデータベースに入れて運用してたりする場合は要注意🕶

RailsビューのHTMLエスケープは#link_toなどのヘルパーメソッドで解除されることがある

⚓rails notesコマンドの変更と一部非推奨化


つっつきボイス: 「rails notesコマンドってそもそも初めて知りました」「ワイも😲: あー、これはFIXMEみたいなコメントも拾えるやつか: JetBrainsのIDEを長年使っている自分としてはIDEで簡単にチェックできるから直接使うことはまずないかなー😎」 「確かにー」

Rails開発のイケてるIDE RubyMineを使う(1)紹介編

「まあコマンドでこういうコメントをチェックできるということは、たとえばCIサーバーなんかでFIXMEがあったらマージを許さないように設定するみたいな使い方はあるかもですね」「😃

⚓新規にアップロードされたファイルを代入時ではなくsave時に保存するようになった

24ファイルも変更されています。

従来、アップロード済みファイルがhas_one_attachedhas_many_attachedで追加されたライターメソッドによってレコードに代入されると、即座にストレージで永続化されていた。たとえば以下ではparams[:avatar]内のアップロード済みファイルが保存対象になっていた。

@user.avatar = params[:avatar]

レコードに追加されたblobのバリデーションをサポートしたいが、無効なファイルの保存を防がないとまともなバリデーションができない。ファイルがストレージに投げられた後で無効だとわかってもほとんど意味がない。AWSの請求額を抑えるために事前にファイルサイズのバリデーションを行いたくても、バリデーション前にファイルが保存されてしまえばコストを削減できない。

このPRでは、レコードに代入されたattachableのActive Storegeへの保存を、即座にではなくレコードのsave後に行うようになる。そのためにActive Storageの添付ファイルのトラッキングを導入し、上述のライターメソッドが使われるときにレコード内の添付ファイルの変更保留をトラッキングする。この変更はレコードがsaveされるとアプリのデータベースで永続化され、関連するattachableはsaveトランザクションがコミットされたときに初めてストレージにアップロードされる。
ついでに添付ファイルAPIのテストを追加/再編成しておいた。
同PRより大意


つっつきボイス: 「テンポラリの添付ファイルをどのタイミングで永続化するかはいつも悩ましい問題ですね」「AWSの課金にも影響するでしょうし」「そっちよりはむしろゴミファイルが残ることなんかの方が問題でしょうね: 添付ファイルがテンポラリ領域にある限りは消し忘れても後で消えてくれるんですが、保存のタイミングが早すぎるとその後でエラーが起きたときにひとりでに消えてくれないし」「そっかー😲

⚓Railsのログの標準出力へのリダイレクトの挙動を上書きできるようにした

// productionでも標準出力をオンにする
rails server -e production -l

// developmentで標準出力をオフにする
rails server -e development -l false
# railties/lib/rails/commands/server/server_command.rb#L165
         def server_options
           {
             user_supplied_options: user_supplied_options,
             server:                using,
-            log_stdout:            @log_stdout,
+            log_stdout:            log_to_stdout?,
             Port:                  port,
             Host:                  host,
             DoNotReverseLookup:    true,
...
           options[:early_hints]
         end

+        def log_to_stdout?
+          options.fetch(:log_to_stdout) do
+            options[:daemon].blank? && environment == "development"
+          end
+        end

つっつきボイス: 「rails server -lが使えるようになるのね、なるほどなるほど」

⚓和暦にも使えるlabels_for_year_optionsオプションをdate_selectに追加

kamipoさんのPRです。

# 同PRより
date_select('user_birthday', '', start_year: 1998, end_year: 2000, labels_for_year_options: ->year { "Heisei #{ year - 1988 }" })
    <select id="user_birthday__1i" name="user_birthday[(1i)]">
    <option value="1998">Heisei 10</option>
    <option value="1999">Heisei 11</option>
    <option value="2000">Heisei 12</option>
    </select>
    /* 略 */

つっつきボイス: 「これは個人的におおっと思っちゃいました」「ははー、年号のステップを刻めるようにしたってことか: 年号は和暦でも何でもお好きなものをどうぞってことで🕶」「😆」「lambdaが使えるってのは結構よさそう」

Unicodeにおける日本の元号の開始日・終了日の定義について

「和暦というか元号対応って細かいところがいろいろ面倒なんですよねー: やったことあります?」「もうライブラリに任せてますー🤓」「😆」「さりげに面倒なのが『平成1年』だけ『平成元年』って表示しないといけない場合: 『昭和64年』→『平成元年』の方はたいていのライブラリで対応できるんですけど、『平成1年』=『平成元年』の方を正規表現で無理やり何とかしようとしてもたまにカバーできなかったりとか: ちなみに上のコード例もそこは未対応ですね」「つらそう…」「我ながら何やってるんだろうと思いながら😅」「次の年号でもその手の対応が必要なところは多そうですね😅

「あそうそう、その名もwarekiっていうgemありますよ」「欲しい人絶対いますよねー」「全元号対応といいつつそこまでではなかった気もするけど…あー今見ると対応元号めちゃ増えてる!」「ホントだー: マニア御用達の皇紀まであるし🤓」「😆」「😆」「しかもいつの間にか1年=元年も対応してるし: やるなー」「うるうの表示まであるし」「元号対応を自前で実装するぐらいならwarekiを使うべき」

  • リポジトリ: sugi/wareki — ruby 向け和暦ライブラリ。和暦と標準Dateの双方向変換をサポート。全元号対応。
# 同リポジトリより
require 'wareki'

Date.today.strftime("%JF")              # => "平成二十七年八月二十二日"
Date.civil(1311, 7, 20).strftime("%JF") # => "応長元年閏六月四日"

Date.today.to_wareki_date # => Wareki::Date インスタンス
Wareki::Date.new("明治", 8, 2, 1).to_date # => 標準 Date インスタンス 1875-02-01

Wareki::Date.parse("正嘉元年 うるう3月 12日") # => Wareki::Date インスタンス
Date.parse("正嘉元年 うるう3月 12日)" # => 標準 Date インスタンス 1257-04-27

参考: 神武天皇即位紀元 - Wikipedia

⚓HTTP Cache-Controlサポートを追加

# actionpack/lib/action_dispatch/http/cache.rb#L205
-            extras  = control[:extras]
+            extras = control[:extras]
             max_age = control[:max_age]
+            stale_while_revalidate = control[:stale_while_revalidate]
+            stale_if_error = control[:stale_if_error]

             options = []
             options << "max-age=#{max_age.to_i}" if max_age
             options << (control[:public] ? PUBLIC : PRIVATE)
             options << MUST_REVALIDATE if control[:must_revalidate]
+            options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
+            options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
             options.concat(extras) if extras

つっつきボイス: 「Cache-Controlヘッダサポートがさらに追加されたのか: stale-while-revalidateとかstale-if-errorとか初めて見たし🤔

参考: RFC 5861 - HTTP Cache-Control Extensions for Stale Content

⚓ActiveRecord::Relation#pluckのメモリ割り当てを削減

# activerecord/lib/active_record/result.rb#L99
     def cast_values(type_overrides = {}) # :nodoc:
-      types = columns.map { |name| column_type(name, type_overrides) }
-      result = rows.map do |values|
-        types.zip(values).map { |type, value| type.deserialize(value) }
-      end
+      if columns.one?
+        # Separated to avoid allocating an array per row
+
+        type = column_type(columns.first, type_overrides)

-      columns.one? ? result.map!(&:first) : result
+        rows.map do |(value)|
+          type.deserialize(value)
+        end
+      else
+        types = columns.map { |name| column_type(name, type_overrides) }
+
+        rows.map do |values|
+          Array.new(values.size) { |i| types[i].deserialize(values[i]) }
+        end
+      end
     end

つっつきボイス: 「#pluckはよく使われるし、この最適化はありですね🧐

⚓サービスURLのデフォルト期限をカスタマイズ可能にした

# activestorage/app/controllers/active_storage/blobs_controller.rb#L10
   def show
-    expires_in ActiveStorage::Blob.service.url_expires_in
+    expires_in ActiveStorage.service_urls_expire_in
     redirect_to @blob.service_url(disposition: params[:disposition])
   end

つっつきボイス: 「これはActiveStorageのファイルの置き場所のURLですね: それをサービスURLと呼んでるってことか」「ローカルならfile://とかだったり」

⚓AC::Parametersのtransform_valuesの結果を修正

# actionpack/lib/action_controller/metal/strong_parameters.rb#L642
-    def transform_values(&block)
-      if block
-        new_instance_with_inherited_permitted_status(
-          @parameters.transform_values(&block)
-        )
-      else
-        @parameters.transform_values
-      end
+    def transform_values
+      return to_enum(:transform_values) unless block_given?
+      new_instance_with_inherited_permitted_status(
+        @parameters.transform_values { |v| yield convert_value_to_parameters(v) }
+      )
     end

     # Performs values transformation and returns the altered
     # <tt>ActionController::Parameters</tt> instance.
-    def transform_values!(&block)
-      @parameters.transform_values!(&block)
+    def transform_values!
+      return to_enum(:transform_values!) unless block_given?
+      @parameters.transform_values! { |v| yield convert_value_to_parameters(v) }
       self
     end

つっつきボイス: 「AC=ActionController」「テストにassert_kind_ofがある↓ってことは、これまでの挙動がバグだったってことか」「おー😲

# actionpack/test/controller/parameters/accessors_test.rb#L193
+  test "transform_values converts hashes to parameters" do
+    @params.transform_values do |value|
+      assert_kind_of ActionController::Parameters, value
+      value
+    end
+  end
+
+  test "transform_values without block yieds an enumerator" do
+    assert_kind_of Enumerator, @params.transform_values
+  end
+
+  test "transform_values! converts hashes to parameters" do
+    @params.transform_values! do |value|
+      assert_kind_of ActionController::Parameters, value
+    end
+  end
+
+  test "transform_values! without block yields an enumerator" do
+    assert_kind_of Enumerator, @params.transform_values!
+  end

⚓touchオプションの振る舞いをPersistence#touchに合わせた

これもkamipoさんです。

increment!#27660)とupdate_counters#26995)に追加されたtouchオプションの挙動がPersistence#touchと同じでない。
touchオプションを属性名に渡しても、Persistence#touchのようにupdate_atupdate_on属性が更新されない。
Persistence#touchincrement!touchオプションに変更されたために、counter_cacheで属性名が渡されるtouchオプションでupdate_atupdate_on属性が更新されないという問題が#31405で再発した。
この不整合は意図したものではないと思われるため、touchオプションでupdate_atupdate_on属性が更新されるようにして不整合を解消する。
同PRより大意

ちょうどRailsdmの動画でもkamipoさんがこのあたりに言及していました。


つっつきボイス: 「touchはUnixのtouchコマンド↓のアナロジーでしょうか?」「Railsのはupdate_atなんかを強制的に適用やつなので、由来はたぶんそうですね: カウンタキャッシュのアップデートなんかにも使うし」

参考: touch (UNIX) - Wikipedia

⚓Rails

⚓「データドリブンエンジニアリング」とは


同記事より


つっつきボイス: 「Code Climateの久々の記事だったので」「定量的なデータを元に改善を着実に進めていこうっていう趣旨みたい」「Data-Driven Engineeringと対になる概念がNarrative-Driven(ストーリーに基づく)ってことみたいで、それよりもメリットが大きいぞってことですね」「上の認知バイアスのチャート、面白そうだけどめっちゃ細かくて読みづらい💦

参考: 認知バイアス - Wikipedia

認知バイアス(にんちバイアス、英: cognitive bias)とは、認知心理学や社会心理学での様々な観察者効果の一種であり、非常に基本的な統計学的な誤り、社会的帰属の誤り、記憶の誤り(虚偽記憶)など人間が犯しやすい問題である。
Wikipediaより

⚓Active Recordの読み書きを洗練させる

# 同記事より
class Product < ApplicationRecord
  # executed daily
  def self.recompute!
    where("
      announce_on    = :today OR
      preorder_on    = :today OR
      publication_on = :today
    ", today: Date.current).find_each do |p|
      p.recompute_dependent_columns
      p.save!
    end
  end

  def self.visible
    where(is_visible: true)
  end

  def self.buyable
    where(is_buyable: true)
  end

  def visible?
    is_visible?
  end

  def buyable?
    is_buyable?
  end

  def publication_on=(val)
    super.tap{ recompute_dependent_columns }
  end

  def preorder_on=(val)
    super.tap{ recompute_dependent_columns }
  end

  def announce_on=(val)
    super.tap{ recompute_dependent_columns }
  end

  def recompute_dependent_columns
    self.is_buyable = [
      preorder_on, 
      publication_on
    ].reject(&:nil).min <= Date.current
    self.is_visible = [
      announce_on, 
      preorder_on, 
      publication_on
    ].reject(&:nil).min <= Date.current
  end  
end

つっつきボイス: 「普通の書き方、書くのは少し面倒だけど読みやすい書き方、さらにシンプルな書き方↑という順序で説明しています」「ふむむー、気持ちはわかるけど最終的なコードを見た感じはちょっとオーバーキルだよなー: と思ったら記事にも同じこと書いてるし↓w」「🤣」「🤣

Depending on your preferences this might seem even easier than the previous solution. Or, it might look ugly or like an over-kill.
同記事より

⚓ActiveRecordとArelでクエリの流れを追う(Ruby Weeklyより)


同ブログより


つっつきボイス: 「いいこと書いてありそうなタイトルだったので」「ふむむー、ARとArelの関係をもっと掘り下げるのかなと思ったら#to_sqlすればだいたいわかるようなチュートリアル的内容ですね😎」「その分初級中級向けにはいいのかも?🤔

⚓Apache SolrとElasticsearchを比較する


同サイトより

2018年5月のおすすめ情報↓

  • Solrはこんなときに
    • Javaプログラマーが多い
    • ZooKeeperを既に使ってる
    • Javaを既に使ってる
    • 特定のニュアンスの関連度が必要な検索アプリを作る
    • eコマース/求人情報/製品の検索エンジンを作る
    • 検索機能を機能やエクスペリエンスの中心に据えたい
  • Elasticseachはこんなときに
    • Ruby/PHP/Pythonフルスタックプログラマが多い
    • JSONがないと生きていけない
    • Kibana/ELKでログを管理している
    • 分析機能が中心のアプリ
  • 悩み中の人に
    • 私がこれまで手がけた高度な検索アプリはどれも検索ワークフローをカスタマイズ/チューニングしないといけなかったし、現時点でもElasticsearchはそのためにがっつりハックが必要。迷ったらSolrをどぞ。

つっつきボイス: 「solr-vs-elasticsearch.comってドメインをわざわざ取ってたので」「最近こういう感じのドメイン名を使ったサイトちょくちょく見かけますね」「どういうときにどっちを選ぶかという目安を上に雑に訳してみました↑」「だいたいこのとおりで、Solrは検索エンジンを自分たちで作るという要素が強くて、Elasticsearchはデータ処理系につなぐときに結構便利」「サービスの中核としてカスタム検索エンジンが欲しいときはSolr、Elasticsearchはカスタマイズが大変って書いてますね」「カスタマイズしたいならSolrでないとつらい: でも現場レベルではデフォルトで使う方が多いですけどね😉

⚓Cucumberをおすすめしない理由(Ruby Weeklyより)

このCode with Jasonというサイトはよさそうでした。


cucumber.ioより


つっつきボイス: 「Cucumberへのヘイトがたまってそうな記事でした」「Cucumberはねー、英語ネイティブにとってはまだいいんですが日本語でテスト書こうとするともろに地獄💀」「やっぱりー」「日本語で書くならturnip↓の方がまだまともに書けた気がする: 一応更新もされてるみたいだし」「turnip、以前使ったことあったそういえば」「ま、今ならシステムテストでいいんじゃね?って思うけど😉

[RSpec][Turnip] 一般的に使えるTurnipステップ集

Rails 5.1以降のシステムテストをRSpecで実行する(翻訳)

⚓コントローラアクションのパフォーマンス改善記事とORMパフォーマンス測定論文(RubyFlowより)


つっつきボイス: 「上の論文を記事の方で絶賛しつつ引用していたので」「ICSE(International Conference on Software Engineering) 2018の論文か: ICSEの位置付けが知りたいなー👀


icse-conferences.orgより

「(しばらくググって)ざざっと調べた感じICSEはランキングも割と高いしかなりちゃんとしたカンファレンスみたい」「おー、そうやって信頼度を調べるんですね😲」「あんまりマイナーなカンファレンスの論文をありがたがって読んでもしょうがないんで😆


icse2018.orgより

「アブストラクトをざっと見たところ、ORMのいくつかの実装についてパフォーマンスを計測・調査した論文ですねこれは」「😃」「しかもこれかなり面白そう: 会議論文として査読も通ってるはずだし、時間のある人はちゃんと読むととても参考になるんじゃないかな」「おー」


同論文より

「で記事の方はコントローラのアクションのパフォーマンスを落とすアンチパターンとそれを特定する方法についてみたいです」「Scoutで測定してますね」「そういえば以前ウォッチでも取り上げたことありました」


同記事より


github.com/scoutappより

⚓⭐switch_point: master/slaveデータベース接続で負荷分散⭐


つっつきボイス: 「お、switch_point」「今日のBPS社内勉強会で言及されてたので」「↓図を見れば一目瞭然ですが、たとえばslaveデータベースはリードオンリーで、masterデータベースは書き込み可能でというふうにできる: しかもuse_switch_pointモデルごとに接続先を指定できる↓のが地味に便利」「使いみちは?」「レプリカに対して使うものなので、基本は負荷分散のためですね: メンテもされているはずだし結構優秀なgemです💪」「😃


同リポジトリより

# 同リポジトリより
class Article < ActiveRecord::Base
  use_switch_point :blog
end

class Category < ActiveRecord::Base
  use_switch_point :blog
end

class Comment < ActiveRecord::Base
  use_switch_point :comment
end

「以前はActiveRecordのデータベース接続はグローバルにしか持てなかったのでswitch_pointみたいなものを使わないとこういうことがまともにできなかったんですが、確かRails 5.1のあたりでデータベース接続を複数持てるようになったので以前よりはやりやすくなってるはず」

久々の⭐を進呈いたします。おめでとうございます。

⚓その他Rails

⚓Ruby trunkより

⚓Rubyのパターンマッチング構文の提案

# 同issueより
case expr
in pat [if|unless cond]
  ...
in pat [if|unless cond]
  ...
else
  ...
end

pat: var                                                   # Variable pattern. It matches any value, and binds the variable name to that value.
   | literal                                               # Value pattern. The pattern matches an object such that pattern === object.
   | Constant                                              # Ditto.
   | var_                                                  # Ditto. It is equivalent to pin operator in Elixir.
   | (pat, ..., *var, pat, ..., id:, id: pat, ..., **var)  # Deconstructing pattern. See below for more details.
   | pat(pat, ...)                                         # Ditto. Syntactic sugar of (pat, pat, ...).
   | pat, ...                                              # Ditto. You can omit the parenthesis (top-level only). 
   | pat | pat | ...                                       # Alternative pattern. The pattern matches if any of pats match.
   | pat => var                                            # As pattern. Bind the variable to the value if pat match.

# one-liner version
$(pat, ...) = expr                                         # Deconstructing pattern.

つっつきボイス: 「zverokさんもパターンマッチングを提案してましたが、これは別の方でした」「あー、Rubyコードに対するパターンマッチ式を記述する構文を作ろうみたいな話ね」「zverokさんのときもMatzが『やるならなるべくいい構文にすべき』ってissueに書いてたと思います」「構文が変わるとなるとあのめちゃ長いparser.yとの格闘がつらそう😢

zverokさんの記事は近々翻訳を公開します。

⚓Ruby

⚓Staytus: アプリのステータスを表示するサービス(Ruby Weeklyより)


同サイトより


つっつきボイス: 「Staytusは『ステータス』と読ませたいみたい?」「あーなるほど、AWSのService Health Dashboard)↓みたいなのを表示できるサービスってことね」「ですです: 自分で作り込むよりは楽そう」「値段次第ですね💰


status.aws.amazon.comより

⚓Bash/Zshのタブ補完をRubyで書く(Ruby Weeklyより)

# 同記事より
  def matches(command_line)
    prepare_argv!(command_line)
    parse_and_consume_options!

    project_name_prefix = extract_project_name_prefix
    project_names = find_project_names
    filtered_names = filter_names(project_names, project_name_prefix)

    puts filtered_names
  rescue GetoptLong::InvalidOption
    # getoptlong prints the error automatically
    exit(1)
  rescue => error
    STDERR.puts error.message
    exit(1)
  end

つっつきボイス: 「Gobyのコントリビュータsaveriomiroddiさんの記事だったので拾ってみました」「これは何をしようとしてるのかな…?ははあ、Bashとかのタブ補完をRubyで書いているのかなるほど: 自分でタブ補完書いてみたい人に😋

⚓Wkhtmltopdfは有害(RubyFlowより)


同サイトより


つっつきボイス: 「Wkhtmltopdfって私は知りませんでしたが、昔からあるヤツですか?」「ですです: これ今でも使ってる人いるんだろうか🤔」「ビルドが面倒とかJavaScriptがないとページ番号扱えないとかドキュメントが貧弱とかつらみが書かれてますね」「Wkhtmltopdfはマジ古くからあるからまあいろいろあるでしょうね: 一応速いんじゃなかったかな?」「よく見るとビルドにQtが必要って書いてある😲」「マジで!?」「QtのGUI表示は本当にきれいなんですがめちゃでかくてビルド大変💦


qt.ioより

⚓Rearmedシリーズ(RubyFlowより)

# westonganger/rearmed-rbより
hash.join{|k,v| "#{k}: #{v}\n"}

hash = {foo: 'foo', bar: 'bar', other: 'other'}
hash.only(:foo, :bar) # => {foo: 'foo'}
# or without monkey patch: Rearmed.hash_only(hash, :foo, :bar)

hash.only!(:foo, :bar)

hash.to_struct
# or without monkey patch: Rearmed.hash_to_struct(hash)

# Only monkey patched if using Ruby 2.2.x or below as this method was added to Ruby core in 2.3.0
items = [{foo: ['foo','bar']}, {test: 'thing'}]
items.dig(0, :foo, 1) # => 'bar'
# or without monkey patch: Rearmed.dig(items, 0, :foo, 1)

# Only monkey patched if using Ruby 2.3.x or below as this method was added to Ruby core in 2.4.0
hash.compact
# or without monkey patch: Rearmed.hash_compact(hash)
hash.compact!

つっつきボイス: 「これはどうやらActiveSupport的なものを自分で作ってみた感じ」「オレオレモンキーパッチというか🐒」「RubyとRailsとJSとそれぞれ作ってますね」「業務で使うのはちょっと考えるなー🤔: こういうパッチは割と簡単に書けますけどね😉

[Rails5] Active Support::Inflectorの便利な活用形メソッド群

⚓フィボナッチヒープをRubyで実装(RubyFlowより)

require 'fibonacci_heap'

heap = FibonacciHeap::Heap.new
foo = FibonacciHeap::Node.new(1, 'foo')
bar = FibonacciHeap::Node.new(0, 'bar')
baz = FibonacciHeap::Node.new(2, 'baz')
heap.insert(foo)
heap.insert(bar)
heap.insert(baz)
heap.pop
#=> #<FibonacciHeap::Node key=0 value="bar">
heap.decrease_key(baz, 0)
heap.pop
#=> #<FibonacciHeap::Node key=0 value="baz">

つっつきボイス: 「フィボナッチヒープというデータ構造をRubyで実装したそうで、ダイクストラアルゴリズムと相性がいいそうです」

参考: フィボナッチヒープ - Wikipedia
参考: ダイクストラ法 - Wikipedia


ダイクストラ法 - Wikipediaより

⚓Ruby 2.6のString#splitはブロックを渡せる

# 同記事より
fruits = []

input_str = "apple, mango, potato, banana, cabbage, watermelon, grapes"

input_str.split(", ") { |value| fruits << value if is_fruit?(value) }
=> "apple, mango, potato, banana, cabbage, watermelon, grapes"

fruits
=> ["apple", "mango", "banana", "watermelon", "grapes"]

つっつきボイス: 「久々のBigBinaryさん記事でした」「おー、split後の結果処理をブロックで渡せるのはナイスなショートハンドかも😋」「splitの後でeachするよりラク👍

⚓mrubyの「Hakoniwa」とは


同サイトより

# 同サイトより
Haconiwa::Base.define do |config|
  config.name = "new-haconiwa001" # to be hostname

  config.cgroup["cpu.shares"] = 2048
  config.cgroup["memory.limit_in_bytes"] = "256M"
  config.cgroup["pid.max"] = 1024

  config.add_mount_point "/var/another/root/etc", to: "/var/your_rootfs/etc", readonly: true
  config.add_mount_point "/var/another/root/home", to: "/var/your_rootfs/home"
  config.mount_independent_procfs
  config.chroot_to "/var/your_rootfs"

  config.namespace.unshare "ipc"
  config.namespace.unshare "uts"
  config.namespace.unshare "mount"
  config.namespace.unshare "pid"

  config.capabilities.allow :all
  config.capabilities.drop "cap_sys_admin"
end

つっつきボイス: 「Hakoniwaという名前がいかにもコンテナな感じ」「あーこれどこかで見たナ」「コンテナ設定をmrubyのDSLで書けるみたい」

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

⚓OpenAPI(旧Swagger)


swagger.ioより


つっつきボイス: 「Swaggerも今日のBPS社内勉強会で話題が出たので」「SwaggerというかOpenAPIはそろそろ知っておかないとヤバイですね: 使ったことはなくても名前ぐらいは押さえておきたい😎

参考: Swaggerの概要をまとめてみた。 - Qiita
参考: Rails + swagger-blocks で OpenAPI 形式の API ドキュメントを作成する - Qiita

swagger: {自動} : 威張って歩く、自慢する

⚓readコマンドでパスワードを履歴に残さないようにする


つっつきボイス: 「readコマンドを知りませんでした😅」「readコマンドをバッチにすると知らずにそこで止まっちゃうことあるので個人的にはあまりこの使い方は好きではないです: バッチにするなら対話的なものは含めない方がいいと思うし🧐

参考: readコマンド(標準入力から読み込んで変数に格納する) : JP1/Advanced Shell

⚓Amazon Translateに日本語など追加

// 同記事より
import boto3
translate = boto3.client("translate")
lang_flag_pairs = [("ja", "🇯🇵"), ("ru", "🇷🇺"),
                   ("it", "🇮🇹"), ("zh-TW", "🇹🇼"),
                   ("tr", "🇹🇷"), ("cs", "🇨🇿")]
for lang, flag in lang_flag_pairs:
    print(flag)
    print(translate.translate_text(
        Text="Hello, World!",
        SourceLanguageCode="en",
        TargetLanguageCode=lang
    )['TranslatedText'])

つっつきボイス: 「ほほーAmazon Translateなんてのがあるとは」「今ならこういうAPIがあっても不思議でない」「ちなみにこの手の機械翻訳をチェックする時は割と長めの文章を食わせて下の方を見るようにしてます: 長文の出だしは整ってても下に行くと崩壊しているとかよくあるので😆」「今なら翻訳エンジンも複数選べるし、候補のひとつとして押さえておくのはいいと思う」

⚓SQL

⚓pgmongo: MongoDBをPostgreSQLに置き換える(Postgres Weeklyより)


同リポジトリより

まだproduction readyではないとあります(´・ω・`)。


つっつきボイス: 「MongDBをポスグレに置き換えるってもしかしてJSONBに入れてるだけなんじゃ?と思ったらどうもそれっぽい↓」「🤣」「🤣」「どんな人が使うんだろか?」「うっかりMongoDBにしちゃったけどPostgreSQLに引き返したい人なんじゃ?やっぱり🕶」「🤣」「諦めて作り直す方が早いきっと」「諦めが肝心」

# 同リポジトリより
db.createCollection('users')  ->  CREATE TABLE IF NOT EXISTS "users" (data jsonb)
db.users.find({ lastLogin: { $lte: '2016' } })  ->  SELECT data FROM "users" WHERE data->>'lastLogin'<='2016'
db.users.update({}, { $set: { active: true } })  ->  UPDATE "users" SET data = jsonb_set(data,'{active}','true'::jsonb)
db.users.find({}, { firstName: 1 } )  ->  SELECT jsonb_build_object('firstName', data->'firstName', '_id', data->'_id') as data FROM "users"
db.blogs.insert({ title: 'first post', comments: [] })  ->  INSERT INTO "blogs" VALUES ('{"_id":"5b45b641eb4bd93896d57888","title":"first post","comments":[]}'::jsonb)
db.blogs.remove({ 'state.trash': true })  ->  DELETE FROM "blogs" WHERE data->'state'->'trash'='true'::jsonb


mongodb.comより

⚓JavaScript

⚓JSXとは何か(JavaScript Weeklyより)

<!-- 同記事より -->
<div><span>Hello</span> <span>World</span></div>;
React.createElement("div", {
  children: [
    React.createElement("span", null, "Hello"),
    " ",
    React.createElement("span", null, "World")
  ]
});
// Note: babel uses the third argument for children:
React.createElement(
  "div", // type
  null, // props
  // children are the rest:
  React.createElement("span", null, "Hello"),
  " ",
  React.createElement("span", null, "World")
);

つっつきボイス: 「今更ですがJSXって何だったかなと思って」「JSXはReactと一緒に使うヤツで、前からありますよ: ただねー、JSXの記法↑は個人的にどうにもキモくて😅…この書き方どう思います?」「いやーそうでも…使ってみるとなかなかいいヤツですよ❤」「結構好みが分かれてるみたい」「長らくPHPやってからRubyに来たときに『カンマがない!』って思ったのとちょっと似てるかなーって」「そうですかー、自分は個人的にスクリプト全体が{ }か何かで囲まれてないとシームレスすぎてどうにも不安になるんですよ: Reactで一番耐え難かったのがこのあたり🤮」「どっちもわかるわー🤔」「ズボンの下にパンツ穿いてない気持ちになりそう」「jQueryなんかでも書こうと思えばこうやって書けそうな気がしないでもない」「とはいえJSXって既にかなり普及してるし、今からBackbone.jsやるよりは全然いいですけどね😋


jsx.github.ioより

参考: JSX - Wikipedia

追記

JSXがいくつもあったとは…失礼いたしました。https://jsx.github.io/がDeNAによるJSXというAltJS言語なんですね。

参考: 「JSX」って名前のものが色々あって混乱する - Qiita

⚓CSS/HTML/フロントエンド/テスト

⚓負荷テストツール8種比較(RubyFlowより)

以下のツールのメリットとデメリットが紹介されています。

  • JMeter
  • Locust
  • Loader.io
  • BlazeMeter
  • Radview WebLOAD(有料)
  • Micro Focus LoadRunner(有料)
  • IBM Rational Performance Tester(有料)
  • SmartBear LoadUI Pro(有料)

つっつきボイス: 「こういうツールの名前をとりあえず知っておくと後で探せますね😋
「とりあえずJMeterは筆頭で歴史もとても長い: ちょうど最近BPSに入社したインフラエンジニアの人も今使ってますね」「おー😲」「そしてJMeterはめちゃめちゃ重いw」「重いんですか?負荷テストツールなのに?」「負荷テストやってるとたいていクライアントが先に音を上げて死ぬという😆: かなり強いサーバーをJMeterの負荷で落とそうとするとJMeterをクラスタ化したりしないといけなかったり」「はりゃー相打ちというか😮」「ただJMeterはプロキシモードで使えるというメリットがあって、JMeterのプロキシサーバー経由でブラウザ画面をぽちぽち操作するとその操作でシナリオを作れるんですよ」「それはありがたい😊」「負荷テストのちゃんとしたシナリオ、それもステートフルなシナリオが作りやすいのがJMeterのいいところ💪: まあ今ならRailsのシステムテストがあるからそっちでもやれますけどね🕶」「😃」「それがなかった頃はJMeterは重宝しました」

「後はあんまり知らないなぁ…BlazeMeterはJMeterと連携できるのか…おや?よく使われるAB(Apache Benchmark)とかskipfishとかがこのリストにないなあ」「AB?また探しにくそうな」「ABはひたすらPOSTリクエストをもの凄いレートで投げられるんで、一瞬でサーバーを落とすことすら可能🧐」「すげー!」「skipfishは?」「skipfishはGoogleのツールで、セキュリティチェックが主だから負荷テストとは少し違うといえば違うかな: WordPressサイトなんかを目がけてかければいろいろ結果を出してくれる」「へー!」

参考: ab - Apache HTTP server benchmarking tool - Apache HTTP Server Version 2.2
参考: セキュリティチェックツールskipfish入門 | apps-gcp.com

⚓Zeplin: PhotoshopなしでPSD確認/コメント付け


同サイトより


つっつきボイス: 「この間の新入社員歓迎会の席で話題になってたZeplinです: PhotoshopのPSDファイルをPhotoshopなしで開けるしコメントも付けられるしで、いつの間にかBPS社内で結構使われてるので」「ZeplinはWindows版もありますしね: この種のツールはSketchとかAdobe XDとかInVisionとか他にも山ほどあるんですが、最終的に大事なのはデザイナーが使っているツールときちんと連携できるかどうかがポイント」「そういえば今日も別のよく似たツールの話してたのに、そのツールの名前が思い出せない…😢」「何しろほんといっぱいあるから」「ともあれZeplinみたいなツールは、今やないとデザイナーと共同作業やってられないぐらいには必要ですね」

↓割とよく間違えられますがこれは「ヒンデンブルグ号」です。

参考: ヒンデンブルク号爆発事故 - Wikipedia
参考: ツェッペリン - Wikipedia


追いかけボイス: 「ちなみに類似ツールを探すときはツール名 alternativeでググると探しやすいです: こんなサイトもあったりしますし↓」

参考: Zeplin Alternatives and Similar Software - AlternativeTo.net

⚓言語よろずの間

⚓RubyistもElixirを学ぶべき理由(Ruby Weeklyより)


つっつきボイス: 「台湾だとRubyとElixirがよく合同でカンファレンスやってますよね」「ウォッチでも何度か取り上げました↓」


2018.rubyconf.twより

⚓Artifact 2.0: Rustで書かれた設計ドキュメント化ツール

designという語を見ると「(ソフトウェア)設計」なのか「デザイン」なのか毎度身構えます。


つっつきボイス: 「こういうのはまず間違いなく『設計』でしょう」「ですよね😳」「↑プロモーション動画が凝ってる❤

⚓Go言語のガベージコレクタを徹底解説(公式ブログ)


つっつきボイス: 「Go言語のロゴが変わったからGopherくんいなくなったのかなー?可哀想に…と思ったらちゃんといた😋↓」「えかったえかった」「元はスライドみたいですがめちゃめちゃ長いです」


同ブログより

⚓その他

⚓VS IntelliCodeのAIサジェスチョンが強化


つっつきボイス: 「何でも今回のアップデートではコーディング規約まで推測するらしくて: 『あなたのところはきっとこんなルールでやってるのよね』って感じで推測するのかなと」
「ちょうど今目についたんだけど、コーディング規約といえばEditorConfigに機能が増えるべきだよなと常々思ってる」「EditorConfig?: もしかしてそれってエディタを超えて使えるんですか?」「そうですよ、たぶんどのエディタでもこのファイルがあれば読み込んでくれる: 知りませんでした?」「知らなかったー😅

# 同サイトより
# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true

# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,py}]
charset = utf-8

# 4 space indentation
[*.py]
indent_style = space
indent_size = 4

# Tab indentation (no size specified)
[Makefile]
indent_style = tab

# Indentation override for all JS under lib directory
[lib/**.js]
indent_style = space
indent_size = 2

# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2

「標準のEditorConfigってシンプルなコンベンションしかなくて機能がまだまだ足りないんですよ: Rubyで言うなら通常の2スペースのインデントまではできても、行が長くなりすぎて折り返したときのインデント(コンティニュエーションインデント)を自分は4スペースにしてブロックと区別したいのにそれが設定できない💦: JetBrainsのIDEになら当然のようにある設定なのに…」「ありゃー😢」「EditorConfigは言語とも独立しているし汎用性重視だからだと思うんですけど、やっぱり物足りない」

参考: どんなエディタでもEditorConfigを使ってコードの統一性を高める - Qiita

参考: コーディングをAIが支援してくれる「Visual Studio IntelliCode」がアップデート。既存コードからコーディング規約を推測し、適切な設定ファイルを生成 - Publickey

⚓新型Macbook Proはアツい?

参考: 新MacBook Pro+Core i9でマシンがアチアチになるとの報告 | ギズモード・ジャパン


つっつきボイス: 「さて新型Macbook Proに70万かけるべきかどうか💰」「高!」「それだけかけてもデスクトップマシンよりは速くならないだろうし🐢

⚓番外

⚓自転車向け新型シャフトドライブ


つっつきボイス: 「これは動画を見る方が早いです: 伝達効率99%がアツい」「へー」「大根をおろせそうなギアも🥕

参考: 斬新なギア構造が決め手! 動力の伝達効率が99%のシャフト・ドライブ自転車 | ギズモード・ジャパン

⚓AIによる自律的殺人兵器非開発の誓約書


つっつきボイス: 「ターミネーターは作りません、みたいな?」

参考: 「AIによる自律的殺人兵器非開発の誓約書」発表。Google Deepmind設立者やイーロン・マスクら署名 - Engadget 日本版


今回は以上です。

バックナンバー(2018年度後半)

週刊Railsウォッチ(20180702)Ruby 2.2メンテ正式終了、Ransackがつらくなるとき、書籍『Domain-Driven Rails』、GitHubの高可用MySQLほか

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Serverless Status

serverless_status_banner

JavaScript Weekly

javascriptweekly_logo_captured

Railsアプリと技術ブログにGDPRコンプライアンスを追加する(翻訳)

$
0
0

概要

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

画像は元記事の引用です。

参考: EU一般データ保護規則 - Wikipedia

Railsアプリと技術ブログにGDPRコンプライアンスを追加する(翻訳)

GDPR EU規則を表すハンマー

EU一般データ保護規則(GDPR EU)が後2週間もしないうちに施行されます。本記事では、私が自分のRails SaaSアプリに加えたGDPRコンプライアンス対応とこのブログ自身について解説します。

アプリは、たとえEU圏内に配置されていないとしてもこの新しい規則に準拠しなければなりません。アプリにヨーロッパのユーザーがいれば該当します。

免責事項: 私は法律の専門家ではありません。また本記事は法的な助言に基づいたものではありません。

GDPRコンプライアンスが必要なもの

GDPRでは、いわゆる個人識別情報(PII: Personally Identifiable Information)を問題にします。これは、ユーザーの身元を直接または間接的に特定できるあらゆる情報断片です。

自分のアプリ群を監査した結果、これらのアプリが収集する以下のデータ片がPIIに類別され、対応すべき対象であることが判明しました。

  • アプリケーションログに保存されているIPアドレス
  • Google Analyticsが集めるIPアドレス
  • メーリングリスト登録者のメールアドレス
  • Abotサブスクリプション購入者のメールアドレス

私の取った対応をデータの種類ごとに説明します。

ログとGDPR

NGINXログのローテーション

NGINXのアクセスログにはユーザーのIPアドレスが含まれます。規則によると、これはユーザーの特定に利用可能な情報断片となるため、保護および不要時の削除を行うべきです。

新しい規則に準拠するため、ユーザーのIPアドレスがメンテナンス目的で収集及び保存される旨を周知する情報をAbotのプライバシーポリシーに追加しました。

アクセスログは、アプリで問題が生じた場合の調査が行えるよう、2週間保存することにしました。14日経過したNGINXログを自動的に削除するため、VPSサーバーの/etc/crontabファイルに以下を追加しました。

@daily root find '/var/log/nginx/' -mtime +14 -type f -delete

アクセスログの保存期間は個別のユースケースによります。新しい規則では「正しい保持期間」というものは指定されていません。

NGINXアクセスログの匿名化

NGINXアクセスログの匿名化(anonymize)をオプションとして検討してもよいでしょう。これを確かめるためにGDPR侵害の罰金2百万ユーロを費やすつもりはありませんが、IPアドレスを含まないログが個人識別情報と解釈されることはなくなるでしょう。

アクセスログからユーザーのIPアドレスを削除するために、http設定コンテキストで以下のNGINXディレクティブを使えます。

log_format anonymized '127.0.0.1 - [$time_local]  '
    '"$request" $status $body_bytes_sent '
    '"$http_referer" "$http_user_agent" $request_time';

access_log /var/log/nginx/access.log anonymized;

Railsアプリのログ

標準のアプリケーションログには、ユーザーを特定できる情報が含まれている可能性があります。

Railsアプリの場合、イニシャライザファイルを1つ追加することでアプリケーションログに含まれる内容を制限できます。

config/initializers/filter_parameter_logging.rb

Rails.application.config.filter_parameters += %i(
  password text
  user_name user_id
  token payload
)

明らかに、ログの利便性と匿名化は常にトレードオフの関係にあります。

Railsのアプリケーションログのストレージの場所は、インフラの設定に依存します。私はDokkuを使っており、この場合ログは次のパスに保存されます。

/var/lib/docker/containers/[CONTAINER_ID]

このCONTAINER_IDを取得するには、以下のコマンドを実行しなければなりません。

dokku ps

特定のユースケースでログをより長期間保存する必要が生じている場合、おそらくログをAmazon S3バケットにエクスポートしていることでしょう。この場合、バケットがEU圏内のAWSデータセンターに配置されているかどうかをダブルチェックしましょう。GDPRではEU圏内に配置されているサーバーをよりセキュアなものとして取り扱います。バケットの内容が暗号化されていることも確認しておきましょう。

なお、詳しくは別記事「AWS S3バケットをよりセキュアに動かす方法」でもご覧いただけます。

Google AnalyticsのプライバシーとGDPR

Google Analyticsのデフォルト設定ではユーザーのIPアドレスが含まれます。Google Analyticsトラッキングのプライバシーをより高めるには、以下のコードを追加します。

ga('set', 'anonymizeIp', true);

これによりリクエストのIPが匿名化され、ユーザーを特定できなくなります。この設定を追加すると、分析データの地理情報の精度が落ちることもお忘れなく。

静的なランディングページ

私のiOSアプリ向けに、ランディングページ(LP)でGoogle Analyticsを使い続けています。

これらのサイトでのユーザートラッキングは私にとってほとんど価値がないので、すべて取り外すことに決めました。トラッキングコードを削除し、Google Analyticsのパネルでデータの削除をスケジューリングしました。この程度の静的ページのために規約やポリシーを追加する必要は感じられません。

このブログとAbotのランディングページ

このブログや、Rails SaaS製品であるAbotのランディングページのトラフィックは引き続きトラッキングしたいと考えています。そのため、Abotの利用規約とプライバシーポリシーの変更と、このブログへの利用規約の追加が必要になりました。

どの情報がどこに保存されているかという正確な情報を追加し、ユーザーが自分のデータをすべて削除するリクエストを出せる連絡先についても利用規約に記載しました。Slackチームからbot統合を削除したチーム向けに、Abotにデータの自動削除も追加しました。

ブログやAbotに保存されている個人データはさほど複雑ではありません。より高度なソフトウェアプロジェクトではどうなるかについては、Konfeoオンラインイベント登録システムの規約をご覧いただくとよいでしょう。

利用規約の明示的または暗黙の受諾について

GDPRによる重大な変更点は、有効なプライバシー規約を用意するだけでは不十分だという点です。ユーザーは規約を明示的に受諾しなくてはなりません。たとえば、受諾のチェックボックスは事前に選択済みにしてはならず、ユーザー自身の操作でチェックボックスをオンにしなければなりません。

JavaScriptをちょっぴり使って、Abotのログインボタンの隣にチェックボックスを追加しました。ユーザーはログイン前に利用規約を受諾することが求められます。

PaddleのAbotチェックアウトフォーム

月毎のサブスクリプション支払い

Abotでは2週間の試用期間終了後にサブスクリプションベースの支払いが行われます。支払いのプロバイダはPaddleを使っており、ここはGDPRコンプライアンスのチェックアウトフォームを用意しています。

Paddleからのマーケティング情報送信をユーザーが許可する場合はオプションでチェックボックスをオンにできます。私のメーリングリストではこの属性を元に扱いを分けています。

GDPR regulations meme funny

Paddleのチェックアウトフォームでは、マーケティングの許諾を明示的に選択できます。

ブログのメーリングリスト

このブログのメーリングリスト構築にはMailchimpを使っています。ここが提供しているGDPRコンプライアンスフォームは、ユーザーからのマーケティング情報収集をオプションで許可します。Paddleの場合と同様、許諾があったかどうかに応じてユーザーの扱いを分け、適切にメールを送信しています。

GDPR regulations meme funny

Mailchimpでは、従来の登録ユーザー向けにGDPR許諾メールのテンプレートを提供しています。これを使って、マーケティングの許諾やサブスクリプションの解除の設定更新をユーザーに行ってもらうことができます。

Mailchimp GDPR consent email

皆さんもおそらくこうしたメールをいくつも受け取っていることでしょう。

まとめ

GDPRコンプライアンスを私のすべてのプロジェクトに追加する作業はまだ終わっていません。何しろ一人でやってるので少々面倒ではあります。皆さんにご紹介した私の施策にGDPR関連の重大な抜け穴がありましたら、ぜひ修正方法をお知らせください。

関連記事

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

Ruby製Webアプリのcookie-onlyセッション破損でセキュリティリスクの可能性(翻訳)

Railsドメイン設計: イベントソーシングでCQRS Read Modelが基本的に必要な理由(翻訳)

$
0
0

概要

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

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

用語

  • CQRS: コマンドクエリ責務分離(Command Query Responsibility Segregation)

参考: CQRS + ESについてのまとめ - 記録

  • CQRS Read Model: Query Modelとも呼ばれ、データの読み出しをデータの書き込みから分離します。CQRS Write Modelと対になります。

参考: The Read Model · Nick Chamberlain

Railsドメイン設計: イベントソーシングでCQRS Read Modelが基本的に必要な理由(翻訳)

イベントソーシング(Event Sourcing)は一定のメリットを得られる優れた技法ですが、1つ大きな制約があります。現在のステートの概念を簡単に得られないため、たとえば出荷可能数が10個以下の全製品が欲しいという問い合わせの回答を得るのが簡単ではありません。

ID=1ProductとしてイベントのProduct-1ストリームを読み出し、それを用いてこの製品の現在のステートを再構成して、その製品の出荷可能数が10個より少ないかどうかという回答を得ることも一応可能ですが、すべての製品について回答を得るには、すべてのProduct-*ストリームを列挙し、全製品の保存済みドメインイベントをすべて処理する必要があります。この操作には膨大な時間とコストがかかるでしょう。

普段の業務で目にする次のユースケースはいずれも困難が少し増すでしょう。

  • 登録ユーザーの最新10人を表示する
  • 顧客をメールか住所で検索する
  • 今月からの全トランザクション数
  • 顧客のライフタイムバリュー
  • 「blue pillow」というテキストを含む全製品

他にいくらでも考えられます。

どうしてこうなってしまうのでしょうか。

その理由は、エンティティ(Entity)や集約(Aggregate)がイベントソーシングされると、オブジェクトのリポジトリへの問い合わせ方法が1つに限定されてしまうためです。その方法とはすなわちfind_by_idです。

他の場所(他のエンティティなど)で得たこのidが、その場所への参照(またはUIからの参照、APIからの参照、リクエストからの参照)を持つことは理解できるので、次のように書けます。

id = params[:id]
product = ProductRepository.find_by_id(id)

このリポジトリは、自分が読み取るべきイベントのストリーム(Product-1など)を認識し、それらのイベントがProductのインスタンスに適用されることで1つの製品の現在のステートが再構成されます。以上。

では上述のユースケースをすべて解決する方法があるとしたらそれは何だかおわかりでしょうか?それがRead Modelです。

e-コマースアプリの製品リストを顧客に表示し、AddToBasketProductなどのコマンドをイベントソーシングしたいのであれば、ProductのRead Modelが1つ必要になります。このRead Modelは個別の要件次第で、ElasticsearchやSQLなどどんなデータベースにも配置できます。

あるRead Modelを手順に沿って構成するにはどうしたらよいでしょうか。

  1. 製品の更新は、新しいドメインイベントを保存することで行う
  2. イベントハンドラがトリガされる
    • イベントハンドラの保存後、イベントにプッシュしたメッセージキューでトリガ可能になる
    • 最もシンプルなケースはActiveJobで実装可能、もっと複雑なシナリオではKafkaやRabbitやAmazon SQSで実装可能
    • または別プロセス(プロジェクション)が保存済みドメインイベントを定期的にイテレーションして処理対象を選ぶ
    • ドメインイベントの保存にEventStore DBを用いた場合は非常にシンプルに行える
  3. イベントハンドラは、発生するイベントと処理するドメインイベントの種類に応じてそのRead Modelを更新する

例を1つご紹介します。

ProductRegisteredイベントは、ProductListというActiveRecordベースのRead Modelへの新しい要素の追加を発生させます。

ProductList.create!(
  id: event.data[:product_id],
  name: event.data[:name],
  price: BigDecimal.new(event.data[:price]),
)

ProductPriceChangedイベントは、リスト上の価格の更新を発生させます。

ProductList.
  find_by!(id: event.data[:product_id]).
  update_attributes!(
    price: BigDecimal.new(event.data[:price]),
  )

他にいくらでも考えられます。

そして価格の高い製品上位10種を表示したい場合は、このProductListというRead Modelに基づいてアプリの読み出し側で行えばよいのです。

ProductList.order("price DESC").limit(10)

アプリの書き込み側では、イベントソーシングされているProductクラスの書き込みで変更をトラッキングしつつビジネスルールを保護します。書き込み側は、ProductRegisteredProductPriceChangedを公開する責務を持ちます。

お知らせ: もっと詳しく知りたい方へ

イベントハンドラやRead Modelやイベントソーシングについてもっと詳しく知りたい方は、私たちの近刊「Domain-Driven Rails ebook」をぜひ手にお取りください。

関連記事

Ruby/Railsのプロ開発者としての5年間を振り返る(翻訳)

Viewing all 1410 articles
Browse latest View live