PK/Unique KEY が UUID v4 だと INDEX 作成効率が悪い

zenn.dev

プライマリキーにUUIDを採用した場合のINSERT時間のペナルティ

っていう話があったので、へーって思って見てみた。対象の MySQL はただ docker run mysql:8.0 したヤツです。

string が主キーで、PK があるものと無いもの、UUID v4 と v1 とで比べれば良かろう。(v1 は値が偏るので)

create table bar(id varchar(255) not null, primary key(id));
create table baz(id varchar(255) not null, primary key(id));
create table qux(id varchar(255) not null);
INSERT_SQL = <<~SQL
INSERT INTO %s(id) values
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?),
  (?), (?), (?), (?), (?), (?), (?), (?), (?), (?)
SQL

COUNT_NUM = 50_000; # 回 *100 行の INSERT

def execute(table_name, &block)
  COUNT_NUM.times do |i|
    puts "#{table_name}\t#{i}\t#{Time.now.to_i}" if i % 1000 == 0 # 10万行 INSERT ごとに時間を出力
    db.xquery(INSERT_SQL % table_name, *100.times.map{ block.call })
  end
end

uuid = UUID.new
execute("bar") { SecureRandom.uuid } # UUID v4
execute("baz") { uuid.generate }     # UUID v1
execute("qux") { SecureRandom.uuid } # UUID v4 PK なし

100 行ずつ 5 万回 INSERT、1000 INSERT (=10 万行)ごとに時間を取得しました。

結果をグラフにするとこう。横軸が総 INSERT 行数、縦軸は 10 万行の INSERT にかかった秒数。

f:id:onk:20201013163946p:plain

へー。たしかに 100 万レコードを過ぎた辺りから徐々に遅くなってるのが分かる。 もうちょっと右側 (数千万レコード級) でどうなるかも見てみたい……。

UUID 文字列を使うのが間違い (例えば UUID_TO_BIN() 使えよとか) という話もなくはないし、1 行あたりだと 1ms 切るので数千万程度なら大して気にはしないと思うけど、とりあえず。

自分では snowflake 系に慣れきっちゃってるので、コレを見てもなんともだなぁ。

おもしろかった。

ISUCON10 予選敗退^H^H突破した

id:uzullaid:moznion と共に curl gotti というチーム名で出場しました。このメンバーでやるのは去年に引き続き 2 回目。

三行で

  • 最高得点 2125 で予選通過ならず
  • ほぼいつもの力が出せた。ので実力不足である。。
  • とても楽しめた良問だった。ありがとうございます

多分この辺の施策をやったんだと思う

  • condition.json を静的に返す
  • 雑に INDEX 張る
  • features の LIKE 検索を正規化して撲滅
  • xxxRangeId カラムを追加
  • なぞって検索の N+1 改善
  • PHP のチューニング
  • DB のチューニング
  • ORDER BY popularity DESC, id ASC をどうにかする
  • DB 2 台構成に
  • Bot に 503 を返す

自分が考えたことや手を動かしたことは分かるんだけど、それ以外のチームメンバーがやったことはあまり把握していないので事実とは違う可能性がある。

アプリケーション概観を掴んで作戦を立てる

とりあえず読み慣れてる Ruby でコードを全部読んだ。LeafCage/foldCC.vim は最高。

f:id:onk:20200913164533p:plain
vim の fold が快適で他のエディタに移れない

チームの scrapbox にエンドポイント一覧ページを作り、各エンドポイントが何をやっているかをそこに書き込んでいった。

f:id:onk:20200913164736p:plain

全部読んだのでアプリケーション概観を書いて情報共有。

f:id:onk:20200913164807p:plain

今思うと GET /api/recommended_estate/:idSQL 素通りしたのマジかよってなるんだけど、当時はザッと見て「見てるカラム数少ないし INDEX 張るだけでなんとかなるんちゃう」って思っていた。。

この辺りでベンチ実行できたので alp, pt-query-digest を採取。まぁ GET /api/chair/search, GET /api/estate/search から倒そうねって感じになり、

  • レギュレーション通りに Bot に 503 を返せるように& condition.json を静的に返す
  • 雑に INDEX 張って次を考える
  • features の LIKE 検索を正規化

の 3 ルートに分岐した。僕は INDEX を担当したので

-- GET /api/estate/low_priced 用
alter table isuumo.estate add index rent_and_id(rent, id);

-- GET /api/recommended_estate/:id 用
-- door_width, door_height は一旦無視
alter table isuumo.estate add index popularity(popularity);

-- GET /api/chair/low_priced 用
-- order by 狙いで stock は無視
alter table isuumo.chair add index price_and_id(price, id);

を張った。ここでも recommended_estate の SQL を考えるのを諦めていることがわかってツラい。

xxxRangeId カラムを追加

「検索をどうしたら良いか何も分からん。単体で INDEX 張りまくって INDEX マージに任せるか! MySQL 8.0 にしたら賢くなって勝つるんじゃね?」みたいな雑談をしつつ (割と本気だったけど、今回クエリキャッシュもかなり効いていたので、8.0 選ばなくて良かったのかもしれない)、どんな複合インデックスにしたら良いのか何も分からんのでとりあえずベンチマーカーが送ってくるクエリパラメータを眺める。

   8 depthRangeId=0&page=0&perPage=25
   1 depthRangeId=0&page=1&perPage=25
   1 depthRangeId=0&page=2&perPage=25
   4 depthRangeId=0&page=3&perPage=25
   1 depthRangeId=0&page=4&perPage=25
  15 depthRangeId=1&page=0&perPage=25
   3 depthRangeId=1&page=1&perPage=25
   3 depthRangeId=1&page=2&perPage=25
   3 depthRangeId=1&page=3&perPage=25
   6 depthRangeId=1&page=4&perPage=25
   9 depthRangeId=2&page=0&perPage=25
   2 depthRangeId=2&page=1&perPage=25
   5 depthRangeId=2&page=2&perPage=25
   2 depthRangeId=2&page=4&perPage=25
   8 depthRangeId=3&page=0&perPage=25
   2 depthRangeId=3&page=1&perPage=25
   5 depthRangeId=3&page=2&perPage=25
   3 depthRangeId=3&page=4&perPage=25

あれ、depth や width そのものじゃなくて ID 送ってきてるんだ! なるほど min,max 取り出してたコードはそういうことか、って言いつつ、「初期データ弄るねー」って言ってカラム追加してデータ移行。

alter table isuumo.chair add column depth_range_id integer, add column height_range_id integer, add column price_range_id integer, add column width_range_id integer;

update isuumo.chair set depth_range_id = 0 where depth < 80;
update isuumo.chair set depth_range_id = 1 where depth >= 80 and depth < 110;
update isuumo.chair set depth_range_id = 2 where depth >= 110 and depth < 150;
update isuumo.chair set depth_range_id = 3 where depth >= 150;
...

add column を 2 個以上書くときの SQL の書き方が分からん、とググりながらやっている (地力不足)。短期決戦なんだから綺麗に書くよりも 「動けば良い」を合い言葉にしておくと良いのだろうなぁ、まだ気持ちを捨て切れてない。

blog.sushi.money

また、この過程で「ひょっとして condition.json から ranges 減らしたらベンチマーカーが送ってくるクエリパラメータの分散が減るんじゃね?」と思って試してみたがさすがにそんな甘い話は無かった。

SQL がまだホットスポット

[width_range_id, popularity] で複合インデックスを張ったが、1 要素だけの検索でも Using filesort が消えない。

mysql root@127.0.0.1:isuumo> explain select * from estate where door_width_range_id = 1 order by popularity desc, id asc limit 25 offset 50\G
***************************[ 1. row ]***************************
id            | 1
select_type   | SIMPLE
table         | estate
partitions    | <null>
type          | ref
possible_keys | door_width_range_id_and_popularity
key           | door_width_range_id_and_popularity
key_len       | 4
ref           | const
rows          | 5253
filtered      | 100.0
Extra         | Using index condition; Using filesort

1 row in set
Time: 0.005s

試しに id ASC を削ると Extra がめっちゃ綺麗になる。

mysql root@127.0.0.1:isuumo> explain select * from estate where door_width_range_id = 1 order by popularity desc limit 25 offset 50\G
***************************[ 1. row ]***************************
id            | 1
select_type   | SIMPLE
table         | estate
partitions    | <null>
type          | ref
possible_keys | door_width_range_id_and_popularity
key           | door_width_range_id_and_popularity
key_len       | 4
ref           | const
rows          | 5253
filtered      | 100.0
Extra         | Using where

1 row in set
Time: 0.005s

SHOW INDEX から popularity のカーディナリティはかなり高いのを知っていたので、「これ unique にできるんじゃね?」という作戦に出た。

具体的には

  • 10 倍する
  • id が大きい方から 0, 1, 2, ... を足す
  • ORDER BY popularity DESC, id ASCpopularity DESC のみに変更

をやった。

レスポンスを返すときに 1/10 にするのを忘れていて、ベンチがコケて気づいて一瞬慌てた。シンプルに 10 倍したので計算で返せて助かった……。

この 10 倍して 1/10 して返す、みたいなのは、浮動小数点数を避けるために整数で計算したい!ってときなんかに実務でもよく使う手なので、自然と思いつけた。

突然登場する ActiveRecord

require "erb"
require "active_record"

class Chair < ActiveRecord::Base
  self.table_name = :chair
end
class Estate < ActiveRecord::Base
  self.table_name = :estate
end

env = "development"
path = File.join(__dir__, "database.yml")
specs = YAML.load(ERB.new(File.read(path)).result)
ActiveRecord::Base.configurations = specs.stringify_keys
ActiveRecord::Base.establish_connection(env.to_sym)

Estate.group(:popularity).having("count(1) > 1").count.each do |popularity, _|
  Estate.where(popularity: popularity).order(id: :desc).each_with_index do |e, i|
    e.popularity = e.popularity + i
    e.save
  end
end

Chair.group(:popularity).having("count(1) > 1").count.each do |popularity, _|
  Chair.where(popularity: popularity).order(id: :desc).each_with_index do |e, i|
    e.popularity = e.popularity + i
    e.save
  end
end

DB 2 台構成に

SQL だいぶ綺麗にしたけどまだ DB の CPU がボトルネック。というので

  • 101 は nginx/php
  • 102 は chair 用 DB
  • 103 は estate 用 DB

という使い分けに変えよう、という会話がシュッと行われて、id:uzulla にやって貰った。コネクション 2 本持って使い分けるという方針は即決まったんだけど、PHP で書ける自信が無かったので、手を動かせる人が居て助かった。

スコア変遷

f:id:onk:20200913165217p:plain

なんと 20 時までは 872 が最高得点だったが、ラスト 1 時間で

  • ボトルネックが DB の CPU 使用率
    • DB の CPU を使わせない、短時間でできることをとにかく考える
  • 与えられた 3 台をうまく使う

という改善ができて、2125 まで爆上げできた。

20 時でスコアが固定された段階では 2100 ぐらいがボーダーっぽかったので、2125 は「他のチームが上げてきていたら無理だなー、でも少し夢があるなー」という会話ができるスコアで、ここまで辿り着けて良かったですね。いやーしかし悔しいな。

あと 1 手あれば 200 点ぐらい足せた可能性があり、ヌルポインターマリアユニバースが 2335 で本戦出場 ということを考えると、いやーこれは行けなくもなかったなー……。

コミュニケーション

  • Discord でボイスチャット繋ぎっぱなしにしていた
  • scrapbox にどんどんページを作っていった
    • 最新の alp や pt-query-digest の結果が勝手に貼られていくのは最高に便利だった
  • 画面は結局シェアすることが無かった
  • ペアプロすることもほぼ無かった

パラレルで動けるように次にやることを考える、というムーブをそれぞれが取れていたんじゃないかなぁ。つまり指揮官 3 人体制なのであった。

改善するとしたら

  • 一度も自分でベンチマークの enqueue をしなかったので、dstat を眺めるということもやらなかった
    • 次の改善点の実感が足りない
    • 2 時間に 1 回ぐらいは自分で enqueue する、もしくはチーム全員で enqueue して dstat を見守る。
      • チーム全員で眺めると自然と方針を固める時間も取れて一石二鳥な気がする
  • ブラウザでは結局動かさなかった
    • アクセスログとコードしか見てないので僕だけコミュニケーションが難しかった
  • ペア作業を恐れない
    • 詰まってたら悲鳴上げてねーぐらいの声掛けはできていたので、信頼して任せているということなのかもしれないけど、まったく画面シェアが無いのはまぁ異常だろう
  • PHP 慣れ
    • preload 周りは id:uzulla しかできない作業になっちゃってるンだよなぁ
    • 今回 AppArmor で少しハマってそうだけど手が出せなかった
  • SQL 改善したらボトルネックが移るはず」と思ってなかなか最終構成の見極めができなかった
    • いやー、でもこれはそういうモノだと思っているな
    • 2 時間前ぐらいに決めようという緩い合意が取れていたことが Keep なのだろう

お疲れさまでした!

いやー、良問だった。データ量が大したことなくても検索は大変というのがよく分かる。 流れる SQL 一覧を作った瞬間に「GIS !!!!!????」という驚きがあったし、椅子が通るかどうかという recommend 検索も最高に面白かった。

過去最多の 1 日 500 組という暴力を捌いた ISUCON10 運営チームの皆さま、ありがとうございました。

2020-09-20 追記

isucon.net

まさかの繰り上がり当選した。やってまいります。

放っておくと進まない仕事を進めるために

はてなは 7 月決算なので期のふりかえりをやっていたんだが、今期は「放っておくと進まない仕事を進めるために、時間を確保する」ことで進むようになった期だった。ちなみに前期は「放っておくと進まない仕事を進めるために、締切を設定する (強制力を持たせる)」ことで進めようとしていた期だった。

放っておくと進まない仕事とは、優先順位が低い仕事のこと。やってもやらなくても直ちに影響はない仕事をどうやったら進められるのかというのは長年ずっと課題だったが、やっと自分の中である程度答えが見えてきたかもしれないので記しておく。書き出してみたら至って普通なんだけど、以下をやっている。

  1. 見積もる
  2. 締切を作る
  3. 締切を宣言する
  4. 時間を押さえる

見積もる

チームで見積もりを行い、まずはタスクの重さや実現方法についての共通認識を持つ。

タスクを分解する、とも言い換えられる。やるべきなんだけど気が重い仕事はとにかく分解しまくる。それでも手が付かないならもっと分解する。最終的には「作業ディレクトリに cd する」まで分解する。

なお、この 5 秒で終わる状態になっていてもまだやらないことが経験により分かっている。これは自分が内心で cdだけやっても成果が出ないことを知っているからだろう。

だったら分解する意味は無いのか?と思うかもしれないがそんなことはなく、リストを人に見せておくと「 cd した?」という問い掛けが発生するので、作業に取りかかるキッカケを増やす役に立つ。僕のことは赤ちゃんだと思ってマイクロマネジメントして欲しい。

締切を作る

締切が無いから際限なく延びるので、まずはタスクの締切を設定する。ここで無理な締切を置いても結局実行できない (何度も痛い目を見た) ので、前段でちゃんと見積もられている必要がある。

締切を作ってタイムラインに載せることで、後ろにも仕事が詰まっていることを実感させ、本当の締切 *1 や裏の締切 *2 が実際には存在しないことを可視化する。

また締切を置くことで、他人からも遅延が見えるようになる。これも他人にマイクロマネジメントして貰うコツだと思う。

締切を宣言する

締切を作ったところで、守る気が無かったら意味が無い。脳内の優先度を上げる策として、前々期は昼会の中に「id:onk が今日終わらせるタスクを宣言する」というコーナーを作った。

この脳内優先度ってヤツが厄介で、ADHD 特性を持っていると、新しいイベントが常に優先度最高で割り込んできてしまう。なんとか対抗するために 一貫性の原理 を使い、自ら宣言することで、強い気持ちで優先度を高く保ち続けようとした。

「今日中にコレとコレが終われば」と言い聞かせることで、帰る/寝る前に本当に終わっているかを見返すことが可能になり、進捗するという算段だったのだが、この作戦を採る段階ではまだ「睡眠時間を犠牲にすればなんとかなるんじゃないか」のような甘い考えが傍らにあるので、次の「時間を抑える」も行う必要がある。

当時のふりかえりを見るとボロクソに言われている

時間を押さえる

これはカレンダー上で作業予定を入れてすべてをブロックしてしまうと言う作戦。

新しいイベントが常に優先度最高で割り込んでくるなら、Slack を落とすことで刺激を減らせば良い。反応できないことを宣言するためにカレンダーも埋めておく。

ただこれだけで進むかというとやっぱりダメで、Slack を落としてもすぐに起動してしまう (完全に手癖) ので、更にペア作業時間として相互監視を行うことで進捗するようになる。完全にペア作業じゃなくても、配信することで緊張感が生まれるようだ。コロナ禍によりさぎょいぷが流行ったのは良い効果があった。

一人でもなんとかできるようになりたくて、リズムを作れば行けるはずだと思ってポモドーロは毎年何度も挑戦しているんだけど、まだ上手くいってない。何らかの強制力が閾値を超えないのだろう。例えば Blurred のようなツールと組み合わせてどんどん刺激を下げていくと、どこかで閾値を超える日が来ると思って模索はし続けている。

イベントにする

もう一つ、「イベントにする」という作戦にも触れておきたい。具体的には開発合宿とかバグ退治 Day とかがコレに当たる。これは僕の特性というよりも「チームで優先度が上がらない仕事」に対応する方法。

ロードマップに載せるほど優先度が上がりきらないが、やりたいタスクというのはどうしても存在する。

優先度が上がらないのは他の重要度が十分に会話されているタスクに押しのけられてしまうからで、本当は「重要度を説明する」が正しい進め方だと思う。しかしそんなコスト高いことはやってられないので、すべてサボって信頼だけで進めたい。そこで開発合宿という「なぜか分からないが成果が出ることが分かっている」という信頼貯金を使って技術的スパイクを実施し、プロジェクトチーム内からプロトタイプを作るコストを軽減する。

イベントにすることで非日常感を作れ、周りが協力的になるので一石二鳥。

最近のはてな社内は、イベント仕立てをうまく使うチームが増えてきたように思う。個人の危機感をチームのタスクに変えていく術として、みんなの引き出しに収まったということなのだろう。

*1:バッファをすべて食い潰した締切

*2:輪転機が稼働する直前のこと。本当の締切の後に設定される

久々に sinatra app を作った

「いつもの」が結構ありそうなので書いておく。

app.rb ペラ 1 でツラくなったときの対策はだいたい sonots パイセンの ちっちゃくはじめておっきく育てる sinatra アプリの作り方 に書いてあって、これは今でも有効なので読んでおくと良いです。

ディレクトリ構成

REPO
├── app.rb
├── bin/
├── config/
│  ├── database.yml
│  ├── initializers/
│  └── locales/
├── config.ru
├── Gemfile
├── Gemfile.lock
├── helpers/
├── models/
├── public/
└── views/
  • sinatra らしさをなるべく残してある
    • 例えば config/boot.rb を用意するかは非常に悩んだのだけれど、起点は app.rb であって欲しい
  • models/, helpers/ は分けた
    • app.rb に直接書いていると 5 model ぐらいしか無くても数百行になってさすがに見通しが悪いので
  • config/initializers/ を置いた
    • 置き場所に迷ったものを放り込みやすくて便利

もう一声大きくなったらもっと Railsディレクトリ構造に近づける *1 けど、一旦こんな感じで。

セキュリティ対策

erubi を使う

XSS 対策のために escape: true を使いたい。

https://github.com/jeremyevans/erubi/blob/1.9.0/lib/erubi.rb#L57

erubi gem を入れた上で

class App < Sinatra::Base
  set :erb, escape: true
  ...

しておくと <%= ... %> がデフォルトで escape される。

String ごとに html_safe フラグを付けられるわけではないので ActiveSupport::SafeBuffer ほど便利ではないが、無いより 100 億倍良い。

これは完全に余談なんだけど Padrino は SafeBuffer が導入されていて羨ましい。

ところで Erubi には「escape が優先される」と書いてあるので escape: true って使ったんだけど、検索しても 1 件も引っかからないのな。

escape_html: true で検索するとめちゃくちゃ尊い PR が出てきた。(先月じゃーん

github.com

Rack::Protection を使う

デフォルトで使われているんだけど、remote_token であって authenticity_token ではないので置き換える。

set :protection, use: %i[authenticity_token], except: %i[remote_token]

remote_token は referrer が同じだったら通している。 https://github.com/sinatra/sinatra/blob/v2.0.8.1/rack-protection/lib/rack/protection/remote_token.rb#L17-L19

authenticity_token は (GET, HEAD, OPTIONS, TRACE 以外は) 常にチェックする。 https://github.com/sinatra/sinatra/blob/v2.0.8.1/rack-protection/lib/rack/protection/authenticity_token.rb#L98-L106

これで例え XSS があったとしても CSRF されなくなって安心。

ところでこの記事書こうと思ってついでに自社の過去事例を漁っていたら 14 年前の面白記述を見つけた。サードパーティ製ツールを大事にしている精神が感じられて最高でした。

diary.hatenastaff.com

SessionStore を Cookie から変更

クライアント側に構造体を保存しているとセッションの無効化が困難、みたいなアレです。

ritou.hatenablog.com

ユーザごとのログインセッションを任意に破棄できるようにしようと思うとログイン処理のたびに 1 レコード増えることになって、「素朴な Web アプリケーションが許される時代は終わったなぁ」って気がしますね。

よく使う extension, middleware

ほとんど Sinatra::Contrib だな。

Sinatra::ConfigFile

YAML を置いておくとアプリケーション内から設定にアクセスできるマン。 大枠で言うと Rails::Application.config_for と同じ使い心地です。

Sinatra::ContentFor

# layout.erb
<title><% if content_for?(:title) %><%= yield_content(:title) %> | <% end %>APP_NAME</title>
# users/show.erb
<% content_for :title, @user.name %>

みたいなヤツ。 title の例だけで無いとツラいのが分かるはず。

Sinatra::JSON

json @obj

すると JSON dump して content-type 付けて返してくれるヤツ。

Sinatra::Reloader

development で有効にすると開発が楽で幸せ。

configure :development do
  register Sinatra::Reloader
  also_reload "models/**/*"
  also_reload "helpers/**/*"
end

Rack::Flash

rack-flash3 gem でやってる。

redirect 時に notice を渡す書き方もしたいので、Padrino::Flash からパクっておきます。

https://github.com/padrino/padrino-framework/blob/0.15.0/padrino-core/lib/padrino-core/application/flash.rb#L179-L215

REPL

bin/console にこれを置いておくだけでだいぶ楽。

#!/usr/bin/env ruby
require "bundler/setup"
require "irb"
require File.expand_path("../app", __dir__)

def reload!
  Sinatra::Reloader.perform(App)
end

IRB.start(__FILE__)

reload!ActiveSupport::Dependencies と違って丁寧に remove_const とかをやっているわけではなくただ require し直しているだけだけど、model を読み込み直したいぐらいの用途なら足る。

initialize

ディレクトリ構成のところで言った config/initializers を一番最初に読み込ませている。

Dir[File.expand_path("config/initializers/*", __dir__)].sort.each {|f| require f }

ActiveRecord

以下を食わせている。前半は establish_connection 用。後半は timestamp 周り。

path = File.expand_path("config/database.yml", __dir__)
specs = YAML.safe_load(ERB.new(IO.read(path)).result, aliases: true)
ActiveRecord::Base.configurations = specs.stringify_keys
ActiveRecord::Base.establish_connection(env.to_sym)

Time.zone_default = Time.find_zone!("Asia/Tokyo")
ActiveRecord::Base.time_zone_aware_attributes = true
ActiveRecord::Base.default_timezone = :utc

前半は sinatra-activerecord gem で良いんだけど、メンテが滞ってるのを見て から使わなくなってしまった。薄いのでシュッと剥がせたし。最近は次に名乗り出てくれたメンテナが元気なので大丈夫かもしれない。

timestamp は Rails だと Railtie が良い感じにやってくれているが、生で ActiveRecord を使うときは自分で指定する必要がある。 https://github.com/rails/rails/blob/v6.0.3/activerecord/lib/active_record/railtie.rb#L69-L74

あとは models/ 以下に配置して、 ApplicationRecord も欲しいので

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end
require_relative "application_record"
class User < ApplicationRecord
  ...
end

のような感じにしてある。

ActiveRecord::RecordNotFound

ActiveRecord::RecordNotFound を 500 じゃなく 404 で返したいので以下を書く。

error ActiveRecord::RecordNotFound do |e|
  not_found
  # 404 HTML を返したかったらこう
  # send_file "public/404.html", status: 404
end

I18n

Time を出力するときに I18n.l を使うと楽なので YAML を読み込ませておく。

# config/initializers/i18n.rb
I18n.load_path << Dir[File.expand_path("config/locales/*.yml")]

RSS/Atom

builder gem を使え!

ログイン

current_userauthenticate_user! さえあれば事足りることが多いので、 omniauth だけを使っている。

# application_helper.rb
def current_user
  return nil unless session[:user_id]
  @current_user ||= User.find_by(id: session[:user_id])
end
# app.rb
get "/auth/github/callback" do
  ...

  redirect env["omniauth.origin"] || "/"
end

private

  def authenticate_user!
    return if current_user

    if request.safe?
      redirect "/auth/github?origin=#{CGI.escape(request.fullpath)}"
    else
      redirect "/auth/github"
    end
  end

テスト

request spec

rspec-request_describer gem にお世話になっています。send_request を書いておけば rack-test で使える。

routing がカオスになってきたらリクエストを投げた後に last_request.env["sinatra.route"] を確認することで、routing spec の真似事ができる。

RSpec.describe "/", type: :request do
  let(:app) { App }
  let(:send_request) { send(http_method, path, request_body, env).status }

  describe "GET /" do
    it {
      is_expected.to eq 200
      expect(last_request.env["sinatra.route"]).to eq "GET /"
    }
  }
}

fixture

https://github.com/rspec/rspec-rails/blob/v4.0.1/lib/rspec/rails/file_fixture_support.rb をパクってきて spec/support/ 以下に置いてます。

module FileFixtureSupport
  extend ActiveSupport::Concern
  include ActiveSupport::Testing::FileFixtures
  included do
    self.file_fixture_path = File.join(File.expand_path("..", __dir__), "fixtures", "files")
  end
end

RSpec.configure do |config|
  config.include FileFixtureSupport
end

database_rewinder

頑張れば use_transactional_tests 使えるかもしれないけど、読み解くのサボっているので愛用しています。

まとめ

これぐらいの準備で割と Rails と似た書き味で書けるし、だいたい Sinatra::Contrib にあるから安心って感じですね。Padrino に標準装備されているものが多いのですごく悩ましい……。padrino-helpers に嫌な思い出があるだけで Padrino は悪くないので使えば良いのかもしれない。

デプロイしたのが先週なので、運用が始まったらもっと色々考えることが増えていきそうだと思う。例えばログ。(今は標準出力 && Rack::CommonLogger のままで一旦いいやって思って手を付けていない)

*1:GitHub を数時間泳いだところ、https://github.com/cesarfigueroa/acme がめちゃくちゃ良いので参考にしたい

MySQL の ORDER BY や GROUP BY に position を渡せるのを知って驚いたけど deprecated だった

ほぼタイトルママなんだけど

SELECT DATE(created_at), COUNT(1)
  FROM users
  GROUP BY 1;

ってクエリを見つけて、 GROUP BY 1 ってなんぞ……?という話。

MySQL のドキュメント には

[GROUP BY {col_name | expr | position}, ... [WITH ROLLUP]]

 

[ORDER BY {col_name | expr | position}

 

Columns selected for output can be referred to in ORDER BY and GROUP BY clauses using column names, column aliases, or column positions. Column positions are integers and begin with 1:

とあり、ORDER BY, GROUP BY は position を受け付ける。

これは SELECT 句に書いた列番号で指定できるってヤツなんだけど、なので最初の例で言うと GROUP BY DATE(created_at) と書く代わりに GROUP BY 1 と書けて便利。

でも

Use of column positions is deprecated because the syntax has been removed from the SQL standard.

とも書いてあって、はいナルホド〜

ので SQL99 で消えたのかな?知らんけど。

遠慮無く「やめましょう!」ってレビューできました。

Athena だと逆に推奨している

Top 10 Performance Tuning Tips for Amazon Athena | AWS Big Data Blog

  1. Optimize GROUP BY

...

sql SELECT state, gender, count(*) FROM census GROUP BY state, gender;

One other optimization is to use numbers instead of strings, if possible, within the GROUP BY clause. Numbers require less memory to store and are faster to compare than strings. The numbers represent the location of the grouped column name in the SELECT statement; for example:

sql SELECT state, gender, count(*) FROM census GROUP BY 1, 2;

RDBMS の実装に合わせて使い分けるということになるんだろうなぁ。

株式会社はてなに入社しました

株式会社はてなに入社しました

株式会社はてなに入社しました - hitode909の日記

マネージャとしても 2 年目になりました。 今年もよろしくお願いします。

gorogoro.rb を読んだ

gorogoro.rb とは

大江戸Ruby会議08でぺんさんが最後に再生していた頭のおかしい (褒め言葉) Quine。

コードはこちら

https://gist.github.com/tompng/45d93b3386b5986a94b9c3c8beecba69

実装を見ていく

アスキーアートプログラミング構文

まず見慣れた (毎年 Ruby 会議に参加しているうちになぜか見慣れてしまった) アスキーアートプログラミング構文が使われている。

eval((c=%{
  ...
}).split*'')

詳しくは あなたの知らない超絶技巧プログラミングの世界 の第 2 章を参照だけど、%{} の中がプログラムを好きな形に整形したもの。スペースや改行を取り除いた上で、セミコロンで改行してあげると人間が読める程度に復元できる。

Quine

c=(w*26+';eval((c=%{'+c+"}).split*'')").split$/

の辺りが Quine。

eval c= を文字列として再構成して、自身を埋め込んでいる。

メインループ

0.upto(90) {|tm|
  ...
  puts(...)
  sleep(0.05)
}

sleep を入れながら 91 回 puts している。つまりこのプログラム全体は 91 枚のパラパラ漫画。

tm (タイマーかな) を固定すると任意のコマを描画できる。

キャンバス

m = 96
...
cn = (1..m).map { [0] * m }
...
puts(
  (0..47).map {|i|
    m.times.map {|j|
      l, j = [cn[2*i+1][j], cn[2*i][j]]
      %['".:Y,L#])[3*l+j]
    } * ''
  } * $/
)

96 * 96 の 2 次元配列を変数 cn (多分キャンバス) に入れてあり、puts している。

(0..47).map {|i| ... } は最後に $/ つまり "\n"* (Array#join) しているので m.times.map {|j| ... } が空文字で join しているので

各画素は

%['".:Y,L#])[3*l+j]

で、3*l+j を出力してみると

000000000000000000000034100000011100000000000000000000000000000000000000000000000000000000000000
000000000000000000000041000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000440000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000004100000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000044000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000044000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000033343000000000000000000000000000000000000000000000000000000000000000000000000000
000000000003010000000011330000000000000000000000000000000000000000000000000000000000000000000000
000000000000668888888866014300000000000000000000000000000000000000000000000000000000000000000000
000000001062200008888888660143000000000000000000000000000000000000000000000000000000000000000000
000000000066660008888888806014300000000000000000000000000000000000000000000000000000000000000000
000000400888888600288882008004400000000000000000000000000000000000000000000000000000000000000000
000000008888888800000000008004400000000000000000000000000000000000000000000000000000000000000000
000000302888888200688660688004400000000000000000000000000000000000000000000000000000000000000000
000000100888800008888888882034400000000000000000000000000000000000000000000000000000000000000000
000000010028860008888888820344100000000000000000000000000000000000000000000000000000000000000000
000000001300286602288822003441000000000000000000000000000000000000000000000000000000000000000000
000000000013300002200033444100000000000000000000000000000000000000000000000000000000000000000000
000000000000111444444441100000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000001430000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000140000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000044000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000004400000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000440000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000044000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000001430000003333333000000000000000000000000000000000000000000000000000000
000000000000000000000000000143034111111111443300000000000000000000000000000000000000000000000000
000000000000000000000000000034100666686666001443000000000000000000000000000000000000000000000000
000000000000000000000000000310062222288888860014300000000000000000000000000000000000000000000000
000000000000000000000000003106200000088888888000430000000000000000000000000000000000000000000000
000000000000000000000000004006888860028888880080040000000000000000000000000000000000000000000000
000000000000000000000000000088888888000222200080040000000000000000000000000000000000000000000000
000000000000000000000000000088888882000660000880040000000000000000000000334444444433000000000000
000000000000000000000000004028888220068888868880040000000000000000000344441100000011143000000000
000000000000000000000000001302888000688888888200400000000000000000004441006688888866001430000000
000000000000000000000000000030028600088888882034144330000000000000044400622222288888866013000003
000000000000000000000000000001300222066222003410000114433300000000444002060000088888880600334441
000000000000000000000000000000001033333333110000000000001114443333440068888860028888820060110000
000000000000000000000000000000000000000000000000000000000000000111440088888880000020000680000000
000000000000000000000000000000000000000000000000000000000000000000440088888880006666006880000000
000000000000000000000000000000000000000000000000000000000000000000143028882200688888888800100000
000000000000000000000000000000000000000000000000000000000000000000014302886000888888888000000000
000000000000000000000000000000000000000000000000000000000000000000000130028600088888820010000000
000000000000000000000000000000000000000000000000000000000000000000000001330022000020001000000000
000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

のようになっていて、例えば 0 に置換することで各画素を出力している。目を細めてみるとこれだけでも絵が見えますねw

':. を使って 1 画素を更に上下に分割していたり (そのために行の倍のサイズのキャンバスがある)、#: を使い分けることで色の濃さ(質感)を表現していたりする。

カメラ

x = 1.62 * t + 2
y = -1.44 * t - 2
z = 10 - 7 * t - 4.1 * [tm - 6, 0].max / 9

の辺りがカメラ。

x = 0, y = 0, z = 3 とかに固定すると、その場で画像がクルクル回る。

3D 描画

rn が数式を 3D 描画しているところ?で、ピタゴラスの定理的なものが見えるねフンフンナルホドとか言ってみるけどまだ理解していない。

rn = ->(x, y, z, th) {
  xx = n + m * x / z
  yy = n + m * y / z
  rr = 144 / z
  r1 = 2**2i
  r2 = E**th.i
  tr = ->(x, y, z) {
    x, y = ((x + y.i) * r1).rect
    x, z = ((x + z.i) * r2).rect
    [*((x + y.i) / r1).rect, z]
  }
  ([0, xx - rr].max.ceil..[m - 1, xx + rr].min).each {|ix|
    ([0, yy - rr].max.ceil..[m - 1, yy + rr].min).each {|iy|
      vx, vy, vz = tr[ix / n - 1, iy / n - 1, -2]
      cx, cy, cz = tr[-x, -y, z]
      vv = vx * vx + vy * vy + vz * vz * 3
      vc = vx * cx + vy * cy + vz * cz * 3
      h = 0.3
      d = vc * vc - vv * (cx**2 + cy**2 + cz**2 * 3 - 1 - h * h * 3)
      d < 0 && next

      x2 = cx - vx * s = (h / vz).abs + cz / vz
      y2 = cy - vy * s
      x2**2 + y2**2 > 1 && next

      ...
    }
  }
}

各オブジェクト

lambda が一つのオブジェクトになっていて、例えば雷は tn

tn = ->(i,j) {
  a = 1 - j/52
  b = j>40 ? 0 : 1
  j = n - j/2.0
  (j/2 - a*16) < i && i < (j - b*16)
}

これを描画するとこうなる。

                                                ,#######################
                                                #######################
                                               ,######################
                                               ######################
                                              ,#####################
                                              #####################
                                             ,####################
                                             ####################
                                            ,###################
                                            ###################
                                           ,##################
                                           ##################
                                          ,#################
                                          #################
                                         ,################
                                         ################
                                        ,###############
                                        ###############
                                       ,##############
                                       ##############
                                      ,#############,,,,,,,,,,,,,,,,
                                      #############################
                                     ,############################
                                     ############################
                                    ,###########################
                                    ###########################
                                                   ,##########
                                                   ##########
                                                  ,#########
                                                  #########
                                                 ,########
                                                 ########
                                                ,#######
                                                #######
                                               ,######
                                               ######
                                              ,#####
                                              #####
                                             ,####
                                             ####
                                            ,###
                                            ###
                                           ,##
                                           ##
                                          ,#
                                          #
                                         ,

gt が雷門。

gt = ->(x, y) {
  y -= 0.2
  i = n + x * 168
  j = 24 + y * 84

  # 屋根
  next(x * (-y)**0.5 * 16 % 1 < 0.5 ? 1 : 2) if y < -0.4 && y > -0.9 - x**6 && y > -0.4 / x**2
  # 梁
  next(x.abs * 2.6 % 1 > 0.6 || y > -0.35 || x.abs < 0.1 ? 2 : 0) if y >= -0.4 && y < -0.32 - x**4 / 10
  # 提灯の中の「雷門」って文字列
  next(2) if (0 <= j && j < n && 0 < i && (c[j][i] || w) != w)
  # 提灯
  next(1) if 2 * x**4 + y**4 < 0.01
  # 風神雷神
  x = (x.abs - 0.5).abs
  y < -0.4 || y > 0.55 || x > 0.2 ? 0 : x > 0.12 || y > 0.47 || (y - 0.2) % 0.5 < 0.1 ? y > 0.45 && x > 0.12 ? 2 : 1 : 2 * x * x + y * y < 0.01 + y % 0.05 / 5 ? 1 : 0
}

雷門は特に提灯の中の「雷門」文字部分の実装が意味分かんなくて、

next(2) if (0 <= j && j < n && 0 < i && (c[j][i] || w) != w)

なんと c つまりこのプログラム自身の文字列を参照している。

AA になっているので提灯の中に「雷門」が出現するのだよね。プログラムを左寄せにすると提灯の中も左に寄る。

     ::Y#L::##::##''""'''""''""''""'''""''""''""'''""''""''""''"""''""''""''"""::##::##::##Y
    L::##::##::Y##::##::##L::##::##::###::##::##:::##::##::##Y::##::##::L##::##::##Y:L##::##'
   #::##::Y##::##::Y#L::##::##L::##::##:::##::##:::##::##::L##::##::L##::##::##Y::##::##Y::##'
  "::##L::##::##L::##::###::##::Y##::##:::##::##:::##::##:::##::##Y::##::##Y::##::L##::##::L##'
 "::Y#L::##::Y##::##L::##::Y##::##L::##::###::##:::##::###::##::L##::##Y::##::L##::##Y::##::L#Y
"::Y##::##L::##::Y##::Y##::##L::##::Y##::###::##:::##::###::##Y::##:::##::###::##Y::##::L##::##Y
::Y##::Y##::##L::##L::##:::##::Y##::###::###::##:::##:::##::###::###::##Y::##:::##::L##::L##::##
:Y##::Y##::Y##::###::###::###::##L::##L::##:::##:::##:::##:::##:::##::L##::L##::L##::L##::##Y::#
Y##::Y##::Y##::Y##::Y##:::##:::##:::##:::##:::##:::##:::##Y::##Y::##Y::##Y::##Y::##Y::##Y::##Y::
##::Y##::Y##:::##L::##L::###::###::Y##:::##:::##:::##Y::###::###::L##::L##:::##Y::##Y::##Y::L##:
L::Y##::Y##L::##L::###::Y##:::##L::###::###:::##:::###::###:::##:::##Y::###::L##:::##Y::##Y::L##
::###::Y##L::###::Y##:::##L::###:::##L::###:::##:::###::L##:::###::L##:::##Y::###::L##Y::###::L#
:##L::Y##L::###::Y##L::###:::##L::###:::###:::##:::###:::##Y::###:::###::L##:::###::L##Y::L##:::
##L::Y##:::###::Y##L::Y##L::###:::###:::###::Y##:::###:::###:::##Y::L##Y::###:::###::L##Y::L##Y:
L::Y##L::Y##L::Y##L::Y##L::Y##L::Y##L::Y##:::###:::###:::###:::###:::###:::###:::###:::###:::##Y
::Y##L::Y##L::Y##L:::###:::###:::###:::###:::###:::###Y::L###:::###:::###:::###:::###:::L##Y::L#
Y##L:::###L::Y##L::Y###:::###:::Y###:::###:::###::::###:::###:::###Y::L###:::###:::L##Y::L##Y:::
########               ##########         #############        ###########               #######
,,######"""""""""""""""##########"""""""""#############""""""""###########"""""""""""""""######,
   :::::    ,,,,,,,,,,,LLLLLL,,,,,,,,,,,,,LLLLLLLLLLLLL,,,,,,,,,,,,,LLLLL,,,,,,,,,,,,    :::::
   ::::::::::::::::::::::::::      '':::YYYY::::::::::::::::''      ::::::::::::::::::::::::::
   :::::::::::::::::::::::::: ####:::::::::::::::::::::::::::::'    ::::::::::::::::::::::::::
   :::::::::::::::::::::::::: ##":::::::::::::::::::::::::::::::'   ::::::::::::::::::::::::::
   :::::...............:::::: ####YYYYYYYYYY:::::::::::::::::::::   :::::................:::::
   :::::               :::::: ####LL:::::::::::::::::::::::::::::   :::::                :::::
   :::::               :::::: ####YYYYYYYYYYYYYYYYYYYYYY::::::::::  :::::                :::::
   :::::   '':::::''   :::::: ###########LLLLL::::::::::::::::::::  :::::    '::::::''   :::::
   :::::   ':::::::'   :::::: ###LLLL:::::::::::::::::::::::::::::  :::::   '':::::::'   :::::
   :::::  .:::::::::.  :::::: ##LLLLL:::::::::::::::::::::::::::::  :::::  .::::::::::.  :::::
   ::::: ':::::::::::' :::::: #YYYYYYY::::::::::::::::::::::::::::  :::::  ':::::::::::  :::::
   :::::  '::::::::::  :::::: ####LLLLL:::::::::::::::::::::::::::  :::::   ::::::::::'  :::::
   :::::  ..::::::::.  :::::: ###Y::::::::::::::::::::::::::::::::  :::::  ..::::::::..  :::::
   :::::   :::::::::   :::::: ########LLLLL:::::::::::::::::::::::  :::::    ::::::::'   :::::
   :::::   .........   :::::: ########LLLLLLLLLLLLLLLLLLLLLLLLLLLL,,LLLLL,,,,,........   :::::
   :::::               :::::: ##LLLLLLL:::::::::::::::::::::::::::  :::::                :::::
   :::::               :::::: ####LLLLLLLLLLLL:::::::::::::::::::   :::::                :::::
   :::::''''''''''''''':::::: ####LLLLLLLLLLLLLLLLL::::::::::::::   :::::'''''''''''''''':::::
   :::::::::::::::::::::::::: #######:::::::::::::::::::::::::::.   ::::::::::::::::::::::::::
   :::::::::::::::::::::::::: ##########LLL::::::::::::::::::::.    ::::::::::::::::::::::::::
   :::::::::::::::::::::::::: ,,,, .::::::::::::::::::::::::..      ::::::::::::::::::::::::::
   :::::               ::::::           .................           :::::                :::::
   :::::               ::::::                                       :::::                :::::
   :::::               ::::::                                       :::::                :::::
   :::::               ::::::                                       :::::                :::::
   :::::               ::::::                                       :::::                :::::
   #####'''''''''''''''######                                       #####''''''''''''''''#####
   #####:::::::::::::::######                                       #####::::::::::::::::#####
   #####:::::::::::::::######                                       #####::::::::::::::::#####

他のオブジェクトも全部数式で表現されている辺りも十二分に凄いんだけど、その凄さが霞んでしまうぐらいビックリした。

読み終わった感想

いやー、意味分かんなかった。

頑張って読んだらそれなりに理解はできるんだけど、こんなオシャレにカメラを動かせるセンスはまったく無いなー。