koukiblog

たぶんweb系の話題

Rubyのマイナーバージョンを上げたらSyntaxErrorになった件

あるプロジェクトのRubyのバージョンを 2.4.0 から2.4.3に上げたら今まで問題なかった構文がSyntaxErrorになるようになりました。経緯が気になって調べたのでそのメモです。

問題の構文は

let :product { create(product)}

のようなものです。(letの括弧を省略して、{}でブロックを渡している)

これが、Ruby 2.4.1までは、

$ruby -ce "m :a {}"
> Syntax OK

となってok 2.4.2(2.4.3と同じ挙動でした)に上げると

$ruby -ce "m :a {}"
>-e:1: syntax error, unexpected '{', expecting end-of-input
m :a {}
      ^

m(:a {}) と解釈されるみたいでエラー

マイナーバージョンアップで後方互換性壊すことなんてあるのかなーと思ってソースコードを見てみると、それっぽいコミットを発見しました。ちなみに僕は全くRubyのコードは読めません。ただ、構文解析周りだろうなーと思って2.4.1から2.4.2のparse.yに変更があったコミットを調べて見つけました。

github.com

このコミットから辿れるruby-coreがこれで

[ruby-core:81037] [Ruby trunk Bug#13547] [].delete 1 { 'NG' }

バグチケットがこれです

Bug #13547: [].delete 1 { 'NG' } - Ruby trunk - Ruby Issue Tracking System

内容を確認すると、2.4.2へのバージョンアップで壊れたのではなく、2.4.0, 2.4.1でだけ意図せず使えていたのがバグで、本来の挙動が文法エラーだったようです。

僕が遭遇した状況と同じようなバグチケットもいくつか確認できました。

Bug #13939: Ruby 2.4.2 has issue supporting Seattle.rb style for define_method - Ruby trunk - Ruby Issue Tracking System

Bug #13898: Block parsing regression - Ruby trunk - Ruby Issue Tracking System

Bug #14023: SyntaxError on array argument and block - Ruby trunk - Ruby Issue Tracking System

というわけで、対応としてはRubyはバージョンアップに問題はない。エラーになる構文は今まで通っていたのが間違いだったので全て修正する、ということに。

文法エラーになるようなバグに遭遇したのは初めてだったので、普段何気なく使っているRubyも日々いろんな方の労力で維持・開発されてるんだなーと実感できた出来事でした。 今後はまめにバージョンアップして、何かエラーがあれば報告するぐらいの貢献は行なっていきたい。

RubocopとParserの関係

今日遭遇した問題に対応するのに、RubocopとParserの関係がよくわかっていなくて混乱したのでメモ。

rubocop?

github.com こっちはみんな知ってると思いますがruby用の静的解析ツールです。

Parser

github.com

rubyで書かれたrubyのパーサーです。rubyソースコードを読み込みASTを作ります。rubocopの内部で利用されています。 rubocopが利用しているParserのバージョンは -Vオプションで表示できます

rubocop -V
> 0.52.0 (using Parser 2.5.0.2, running on ruby 2.4.1 x86_64-darwin16)

今回遭遇した問題

ある日突然SideCIでだけrubocopのエラーが検出されるようになりました

Lint/Syntax: unexpected token tRCURLY
(Using Ruby 2.4 parser; configure using TargetRubyVersion parameter, under AllCops)

のようなエラーです。 SideCIに問い合わせてみたところ、Parserのバージョンの可能性を教えてもらい確認してみたところ手元の環境でも再現し、問題を特定することができました。

Parserは、Ruby2.5系に対応するためにバージョンアップしており、SideCIは2.5系のコードを解析するためにParserのバージョンアップが必須。手元の環境はGemfileでParserのバージョン指定していなかったので古いになっててエラー検出されず、という状態でした。

対応

結局できることはなくて、様子見になっています。

Parserにはissue報告済み。修正できるならやってみようと思ったのですが、Rubyのパース処理は僕の理解を超えてました。同じような報告が過去にいくつかあがっててメンテナンス大変そうです。

unexpected token tLCURLY at 2.5.0.2 · Issue #454 · whitequark/parser · GitHub

SideCIには、Parserのバージョンを利用者側で指定できるようにしたいっていう要望を伝えたところ検討してもらえるようでした。

Lint/Syntax 系のエラーはrubocopではなくparser由来のものってところだけ覚えておくと、色々対応できると思います。 http://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Lint/Syntax

データ分析を始める前に読んだ本と読んだ後にやったこと

きっかけ

1年ほど前に受託開発をやっている会社からwebサービスを運営している会社に転職したのですが、いきなり新しいサービスの立ち上げに参加しそのあとの運用も担当することになったためです。 僕と事業担当者で施策出したりしてたのですが、勘と雰囲気でやってもなかなか進まないし、ここはデータ分析かなーと。 データサイエンティストとして仕事してる友人におすすめの書籍を教えてもらって全部読んでみました。

読んだ本

データドリブンマーケティング

Amazonの社員が読んでるらしい。LTV、CPA、解約率など基本的な単語の意味を知ることができる www.amazon.co.jp

ロジカルデータ分析

何のためにデータ分析するのかがよくまとまっている。帯に「パッション」ってあるように各所エモい。 www.amazon.co.jp

確率思考の戦略論

データ分析というかマーケティングの本。データを使ってどうしたいのかという点で参考にできる www.amazon.co.jp

戦略的データサイエンス概論

Webエンジニアがぱっと読んですぐ実践できる内容ではない。 データサイエンティストを雇うとこういうことをやってくれるのか!という学びがありました。 www.amazon.co.jp

ビッグデータ分析・活用のためのSQLレシピ

SQLのレシピ集。普段の業務では一番役に立つ。これがあるだけで様々な指標をSQL一発で出せます。 あらためてSQL勉強し直しました。特に分析関数。 www.amazon.co.jp

やったこと

いきなりかっこいいデータ分析をすることはできないが、ログ設計してデータ分析をするところまではやるべき、となってそこまでは一気に進めました。たしかその判断したのがリリース1週間前ぐらいだった気がしますが、多少壊れててもユーザーには影響ないしいいかと思ってやってしまいました。 やったことは以下の3つです

GitHub - kaizenplatform/fluent-plugin-bigquery これ使えばさくっとできました。感謝。

  • EmbulkでRDBのデータを全てBigQueryにinsert

RDBに格納しているデータを個人情報はマスクした状態でBigQueryにinsertしています。 cronを設定して深夜に全件洗い替えしてます。

  • データの可視化にはGoogleDataStudioを利用

無料で使えたのでさくっと導入。

現状

データをがっつり活用できているかというとまだ全然そんなレベルではないのですが、何か仮説があったときに実際のデータに対してSQLを利用して数値を確認し、必要に応じてレポート化して定期的にモニタリングするような運用ができるようになりました。 いずれはフルタイムでデータサイエンティスト雇って一緒に何かやりたい!

Mediumで独自ドメインを使う時

ここ(https://help.medium.com/hc/en-us/articles/115005579728-Get-started-with-custom-domains)に書いてある通りなのですが、publicationを作成し、作成したpublicationの設定画面から独自ドメインの設定を行うことができます。 金額は$75です。 "Use your own domain"の遷移先はクレジットカード情報の入力フォームになっていて、決済が完了するとMediumからメールが送られてきます。 そのメールにDNSの設定情報が記載されているので、その通りに設定すれば完了です。

medium.com この公式っぽい記事が古くて、問い合わせフォームから問い合わせそうになってしまったのでメモ。

vueのXSSの件について考えてみた

↓の記事のことです。 qiita.com

vue管理のテンプレートに、UGC(User Generated Content)を含めない。 というのが原則だとは思うのだけど、既にRailsレンダリングしてるviewのイベント周りだけvueに任せたい、など、vueを薄く使っている場合はサーバサイドのテンプレートエンジンでvueのテンプレートを出力することは結構あるんじゃないでしょうか。

vue側の反応

fix #4223, add interpolation config. by alfa-jpn · Pull Request #6203 · vuejs/vue · GitHub

However, there is no silver bullet for server side vue template, especially when users want to mix user generated content with programmer’s template. IMHO, Vue’s users can and should think over how to separate UGC and template. On the other hand, Vue’s API is already large enough. Vue.config.interpolation can be easily replaced by v-pre that already exists. Users can even provide non-matching regex to mock this pull request. While we thank your contribution, we might still keep our API surface not too large. (Vue’s API is already not as small as it used to be). For more reasoning, please see #6004 (comment).

ユーザー側でなんとかしてほしいということで、まぁそうだねーいう感じ。

vueには、v-preという機能が既にあり、v-pre配下はvueのコンパイルがスキップされるのでそれを使えばよいということです。 https://jp.vuejs.org/v2/api/index.html#v-pre

{{ }} をサーバー側で防ぐ

ぱっと思いついたのは、サーバ側でhtml escapeするときにvueのマスタッシュ構文にも対応するという方法です。 ただ、HTMLエスケープは、Railsのように自動でHTMLエスケープしている環境ではパフォーマンス上重要な箇所( Escape Velocity · GitHub )であり、そこに追加するのはあまりよくない気がしました。

なので、Railsでやるとすれば↓のようにマスタッシュ構文を削除するなりスペース挟むなりするhelper作って明示的に防ぐことになるのかなーと思うのですが

<div>
  <%= v_safe(@user.name) %>
</div>

その場合

<div v-pre>
  <%= @user.name %>
</div>

とほぼ同じだしv-pre使った方が確実でvueのコンパイルもスキップできてパフォーマンス上も有利だし、わざわざ自前でhelper書く意味はなさそう。

結論

vueのテンプレートと、HTMLは違うコンテキストを持っている、ということを意識してやっていくしかなさそう。仕組みで防げなくて微妙な感じもしますが、PRへのコメントにもあった通り銀の弾丸はないってことなんでしょう。

interpolation無効化するのは超単純なアプリの場合はそれでいいかもしれないけど、ある程度複雑なアプリになると初期表示後のイベントとかどこかで必要になるんじゃないかなと思いました。

MySQLのテーブルをEmbulkを利用してBigQueryに転送する

MySQLのデータをBigQueryに転送する方法はいくつかありますが、Embulkを利用してMySQLから直接データを取得してBigQueryに流し込んでみました。 別途Fluentdで集めているアクセスログRDBのデータを突き合わせて分析するのが目的です。

Embulkとは

TreasureData製のDataLoaderです。

Embulk は、リアルタイムなログ収集では常識となった fluentd のバッチ版のようなツールで、ファイルやデータベースからデータを吸い出し、別のストレージやデータベースにロードするためのコンパクトなツールです。

とのことです。( 並列データ転送ツール『Embulk』リリース! - Blog by Sadayuki Furuhashi に記載あり)

fluentdのようにプラグインを追加することで様々なデータソースに対応することができます。

Embulkのセットアップ

インストールは↓に記載があります。Javaで開発されているため、Javaが必要です。 GitHub - embulk/embulk: Embulk: Pluggable Bulk Data Loader. http://www.embulk.org

MySQL→Embulk→BigQuery という流れになるので、inがMySQL、outがBigQueryになります。それぞれプラグインが存在しているのでそれを利用します。

それぞれ、

embulk-input-mysql

https://github.com/embulk/embulk-input-jdbc/tree/master/embulk-input-mysql

embulk-out-bugquery

GitHub - embulk/embulk-output-bigquery: Embulk output plugin to load/insert data into Google BigQuery

を利用します。

インストールは、

embulk gem install embulk-out-bigquery

のように行えます。

embulk-input-mysql

embulkを実行するサーバからMySQLに接続できるようになっている必要があります。 inputプラグインなので、inセクションに記述します。 下記のように設定します。

 in:
   type: mysql
   user: "MySQLのユーザー"
   password: "MySQLのパスワード"
   database: "データベース名"
   table: "テーブル名"
   host: "MySQLのホスト"
   select: "セレクトするカラム(カンマ区切り)"
   parser:
    type: json

parserがjsonになっているのは、BigQueryに転送するときにこちらの方が都合がよいからです。(デフォルトはCSV) CSVの場合、データ内に改行コードが含まれている場合の考慮が必要なのですが、jsonであればエスケープされるため楽になります。BigQueryにCSVをimportするときは行の区切りがCRLFでないといけないようで、データ内にCRLFが存在していた場合、CSVだとうまく取り込むことができませんでした(しっかり検証したわけではないので、設定次第では取り込めるかもしれません) embulk-input-mysql のオプションでwhere句を追加したり、select後にupdate文を実行することもできます。

embulk-output-bigquery

embulkを実行するサーバからBigQueryに接続できるようになっている必要があります。 outputプラグインなので、outセクションに記述します。 下記のように設定します。

out:
  type: bigquery
  mode: replace
  auth_method: json_key
  json_keyfile: "json_keyのパス"
  path_prefix: tmp/
  file_ext: .jsonl.gz
  source_format: NEWLINE_DELIMITED_JSON
  project: "プロジェクトID"
  dataset: "データセット名"
  auto_create_table: true
  table: "テーブル名"
  schema_file: "スキーマファイルのパス"
  formatter: {type: jsonl}
  encoders:
  - {type: gzip}

modeをreplaceにすると、BigQuery側のテーブルをクリアして再生成されます。毎回全件を転送する想定だったのでreplaceにしてあります。 replace以外にもいくつかあります( https://github.com/embulk/embulk-output-bigquery#mode )が、ログデータでなければ追記していく場合はあまりないと思うので、基本的にはreplaceでよいと思います。 前述の通り、MySQLから取得したデータはjsonにしているので、formatterとsource_formatをJSONに設定します。 また、BigQuery側テーブルのスキーマを定義するファイルを別途用意する必要があります。

[
  {
    "name": "id",
    "type": "INTEGER"
  },
 ....
]

のようにカラム名と型を記述していきます。ここのnameとinput-mysqlのselect オプションで指定したカラム名は一致している必要があるようです。

ここまでで、 in/out 共に設定が終わっているので、embulkを実行するとMySQLからデータを取得し、BigQueryにデータが転送されますと思います。

設定ファイルの共通化

MySQLのテーブルごとに設定ファイルを記述していくのですが、上記の設定方法では、MySQLへの接続情報、BigQUeryへの接続情報など重複する部分が出てきます。 Embulkの設定ファイルでは実はLiquidというテンプレートエンジン( – Liquid template language ) が利用できるのでそれを利用して共通化します。

注意: 0.8.28, 0.8.29では、includeができなくなっているようです( Embulk 0.8.28 and 0.8.29 doesn't work Liquid {% include %}. · Issue #757 · embulk/embulk · GitHub)。0.8.18を利用しましょう。embulk selfupdate 0.8.18 でダウングレードすることできます。→ 0.8.30 で解消されました

↓にあるように、includeを利用して共通部分を別ファイルに切り出すことが可能です。 qiita.com 僕の場合は、

in:
  type: mysql
  user: "ユーザ名"
  password: "パスワード"
  database: "データベース名"
  table: {{ table_name }}
  host: "ホスト"
  select: {{ select_columns }}
  parser:
   type: json
out:
  type: bigquery
  mode: replace
  auth_method: json_key
  json_keyfile: "keyfileのパス"
  path_prefix: tmp/
  file_ext: .jsonl.gz
  source_format: NEWLINE_DELIMITED_JSON
  project: "プロジェクト名"
  dataset: "データセット名" 
  auto_create_table: true 
  table: {{ bq_table_name }}
  schema_file: bq_schemas/{{ bq_schema_name }}.json
  formatter: {type: jsonl}
  encoders: 
  - {type: gzip}

という共通で利用する設定ファイルを作成し、各設定ファイルで下記のようにincludeして利用するようにしました

{% include 'partials/mysql2bq',
   table_name: 'MySQLのテーブル名',
   select_columns: "セレクトするカラム",
   bq_table_name: 'BigQueryのテーブル名',
   bq_schema_name: 'スキーマファイルのパス'
  %}

ステージング環境、本番環境向けの接続設定の切り替えもLiquidを利用して分岐することもできるようなのですが、僕の場合はそこはchefでやることにしました。yamlファイルを生成すればよいだけなのでいろいろやり方はあると思います。 twitterで聞いてみたところyaml_masterもよく使われているみたいです。

特定のディレクトリの設定ファイルを一括で実行する

embulk の runコマンドにはワイルドカードを指定できなかったので↓のようにしました

ls configs/*.yml.liquid | xargs -I {} embulk run {}

まとめ

プラグインインストールしてちょっと設定するだけで動くのでよい感じでした。 input-mysql, output-bigquery 共にいろいろオプションがあるのですが、数万件程度のテーブルをBigQueryに転送する用途であればこのままで使えています。 まだ分析がきちんと立ち上がっているわけではないので、cronの定期実行&失敗はcronのログで検知で進めてますが、今後はDigDag,Airflowなどのワークフローエンジンの導入を検討しようと思ってます あと、他にはAWSのDataPipelineも検討したのですが、GUIが難解でしかも重すぎるので挫折しました。。

RailsからRDSを利用するときの設定

RailsからRDSを利用する場合、そのままコネクションプーリングを行うと、RDSのfailover時に利用していないサーバに接続してしまいます。

コネクションプーリングを止めてる事例( http://blog.livedoor.jp/sonots/archives/38797925.html とか ) がいくつか見つかりますが、ここで利用されているgem ( https://github.com/sonots/activerecord-refresh_connection ) は、マルチスレッドに対応していないため、アプリケーションサーバにpumaを利用している場合、このgemを使うことはできません。

そこで、なんとかできないかなーと調べてみたところ、ActiveRecordにreaping_frequencyという設定があるのを発見しました。
rails/connection_pool.rb at 9337f646902955a76dd1975b1201032042ed5004 · rails/rails · GitHub
この設定を行うと、設定した値(秒)を過ぎたコネクションはプールから削除されるようになります。
database.ymlに↓を追記すればokです。

  reaping_frequency: 10

設定してみたところサーバに接続がない状態だと、DBへの接続数が減っていることを確認できました。
RDSがfailoverしてendpointのIPが変わった場合でも、しばらく待てば切り替わるはずです。
ほとんどの場合これでなんとかなるんじゃないでしょうか。