誕生日のお祝いでカロリさんから頂いた「小さなチーム、大きな仕事【完全版】」を読んだ。
- 小さいほうが総じて動きやすい
- 素直に接しよう
- 進んでやることみつけて手を動かす人が大事
- 人をむやみに採用してもコストになるだけ。必要な人を考える
以上の点が印象的だった。
本に書かれた内容自体は概ね共感できるし、実体験から書かれたいい本だった。この本と出会うきっかけをくれたカロリさんに感謝です🙏
誕生日のお祝いでカロリさんから頂いた「小さなチーム、大きな仕事【完全版】」を読んだ。
以上の点が印象的だった。
本に書かれた内容自体は概ね共感できるし、実体験から書かれたいい本だった。この本と出会うきっかけをくれたカロリさんに感謝です🙏
Railsとかに限った話じゃなくて、開発中のサーバー時刻を任意の時間に変更したいといったことはよくあると思う。
そうかの有名なアニメ、「時をかける少女」でいうタイムリープをしたいということである。
タイムリープする方法は
以上のような方法があげられるがサーバー自体の時刻を変更するなんて恐ろしすぎるし、かつめんどくさいのでアプリケーションの内部時刻を変更するアプローチで、Railsのサーバー時刻を変更できるようにしてみた
時間を操作する際に、よく使うモジュールでTimecopというものがある。このTimecopのread meでrailsで使う場合の方法も書いてある
# in config/environments/test.rb config.after_initialize do # Set Time.now to September 1, 2008 10:05:00 AM (at this instant), but allow it to move forward t = Time.local(2008, 9, 1, 10, 5, 0) Timecop.travel(t) end read meより転載
と書かれていた。
例ではtest.rbで使うようにかいてあったけど、これをdevelopment.rbに書いてあげれば、developmentで起動しているときのみサーバー時間をいじれるはず。そこで以下のように書いてみた
# in config/environments/development.rb config.after_initialize do if File.exist?("tmp/localtime") then localtime = File.open("tmp/localtime").read if !localtime.blank? then t = Time.parse(localtime) Timecop.travel(t) end end end
tmp/localtime
というファイルをつくってその中にyyyy-mm-dd hh:mm:dd
形式で時間を書いて、その時間を読み取ってタイムリープするといったコードです。(あんまり設定ファイルにごちゃごちゃ書くのもあれなんで切り出したほうがよいかも)
このlocaltimeの設定はrakeタスクで行うようにしていて、
# in lib/tasks/server_time.rake namespace :server_time do task :mock => :environment do |t| set_time = ENV['RAILS_TIME'] File.open("tmp/localtime","w") do |file| file.puts(set_time) end end task :reset => :environment do File.unlink('tmp/localtime') end end
こんな感じに実装しました使い方はシンプルで
bundle exec rake server_time:mock RAILS_TIME='2016-01-01 00:00:00'
で設定して、bundle exec rake server_time:reset
でリセットできます。
Railsでid
とcreated_at
を複合PKにしたくてschema.rbに直接下記のような定義を書いていた。
# 例 create_table "tests", id: false, force: :cascade do |t| t.integer "id", limit: 8 t.datetime "created_at", null: false t.datetime "updated_at", null: false end execute "ALTER TABLE `tests` ADD PRIMARY KEY (`id`, `created_at`);" execute "ALTER TABLE `tests` MODIFY `id` BIGINT AUTO_INCREMENT;"
直接いじっているのは、いろんな経緯があってのことなのでここでは説明しません。
bundle exec rake db:reset
を実行して期待通りに複合PKがはられたテーブルになっていたのでよっしゃいけるぞ!とおもってモデルで
def self.update test = Test.find(...) test.touch test.save!` end
以上のようなコードを書いて実行してみるとUndefined method 'to_sym'
のようなエラーがでて???ってなってた。
いろいろ試行錯誤してみて全く意味がわからなかったのでいろいろ調べてみると、rails(というかActiveRecord)が備えているそのままの機能では複合PKは扱えないそうで、それが原因かーとおもって更に調べると次の記事にたどりついた。
記事の内容通りに複合PKを扱うgemのcomposite_primary_keys
を使って、self.primary_keys
を定義したところ無事saveすることができた。
RubyのORMのActiveRecordにfind_or_create_byというメソッドがある。このメソッドはデータがあったらselectした結果が返ってきて、ない場合はinsertをしてその結果を返してくれるという夢のような機能を実現してくれているらしい。
このデータがあったらselect、ない場合はinsert
のような処理は扱うときによく考えないとロック周りでDBが詰まってつらい思いすることがある。そこでこのメソッドはどんなクエリを投げるのか検証してみた。
テーブル構造はこんな感じ。
CREATE TABLE `game_play_counts` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `game_id` int(10) unsigned NOT NULL DEFAULT '0', `count` int(10) unsigned NOT NULL DEFAULT '0', `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `game_id` (`game_id`), )
今回はGamePlayCountに対して下記の操作を行う。
検証結果はrails cでActiveRecord::Base.logger = Logger.new(STDOUT)
を実行してログに流れるSQLを順番に抽出したものを貼っています。早速検証スタートだドン!
find_or_create_byをそのまま呼んでみます。
GamePlayCount.find_or_create_by(:game_id => 8)
SELECT `game_play_counts`.* FROM `game_play_counts` WHERE `game_play_counts`.`game_id` = 8 LIMIT 1 BEGIN INSERT INTO `game_play_counts` (`game_id`, ...) VALUES ('8' ...) COMMIT
一度トランザクションの外でSELECTして、トランザクション内でINSERTする結果となりました。こちらで明示的にトランザクションを指定したわけでないけど、ActiveRecordではINSERTしようとすると勝手にトランザクションがはられるようです。
明示的にトランザクションを指定して試します。
ActiveRecord::Base.transaction do GamePlayCount.find_or_create_by(:game_id => 9) end
BEGIN SELECT `game_play_counts`.* FROM `game_play_counts` WHERE `game_play_counts`.`game_id` = 9 LIMIT 1 INSERT INTO `game_play_counts` (`game_id`, ...) VALUES ('9', ...) COMMIT
こちらはトランザクション内でSELECT, INSERTを行うクエリが発行されました。この場合、同時にアクセスが来た場合に片方がduplicateになりそうですね。
find_or_create_byとlockの組み合わせを実行する前にlock単体でどういったクエリを投げるか確認します。
GamePlayCount.lock(:true)
SELECT `game_play_counts`.* FROM `game_play_counts` FOR UPDATE
テーブルに対してFOR UPDATEかけた...トランザクション外だから影響ないけどテーブルロック一歩手間ですね。
次にトランザクション内でlockを呼んでみます。もうこれテーブルロックでしょ
ActiveRecord::Base.transaction do GamePlayCount.lock(:true) end
BEGIN COMMIT SELECT `game_play_counts`.* FROM `game_play_counts` FOR UPDATE
トランザクション内でFOR UPDATEかけると思ったけれど、意外なことに外で実行されました。これはこれで安全設計かもだけどちょっと違和感が...。ちなみにlockの後にfind指定で実行したらきちんとトランザクション内で実行されました。
find_or_create_byとlockを呼ぶとどうなるのか。大変気になるので早速検証します。
まずは明示的にトランザクション囲まないケースから試します。
GamePlayCount.lock(true).find_or_create_by(:game_id => 10)
SELECT `game_play_counts`.* FROM `game_play_counts` WHERE `game_play_counts`.`game_id` = 10 LIMIT 1 FOR UPDATE BEGIN INSERT INTO `game_play_counts` (`game_id`, ...) VALUES ('10', ...) COMMIT
トランザクション外でFOR UPDATEが実行されて、後にトランザクション内でINSERTが走りました。気持ちだけFOR UPDATEつけといたよ感あるけどロックは取れません。
SELECT `game_play_counts`.* FROM `game_play_counts` WHERE `game_play_counts`.`game_id` = 10 LIMIT 1 FOR UPDATE
トランザクション外なので同様にロックは取れません。
明示的にトランザクション囲んだケースを試します。
ActiveRecord::Base.transcation do GamePlayCount.lock(true).find_or_create_by(:game_id => 11) end
BEGIN SELECT `game_play_counts`.* FROM `game_play_counts` WHERE `game_play_counts`.`game_id` = 11 LIMIT 1 FOR UPDATE INSERT INTO `game_play_counts` (`game_id`, ...) VALUES ('11', ...) COMMIT
トランザクション内でFOR UPDATEしてINSERTもする挙動となりました。最初にFOR UPDATEの空打ちをしているので、ギャップロックが気になります。高い並列度でよばれたら詰まることがありそうですね。
BEGIN SELECT `game_play_counts`.* FROM `game_play_counts` WHERE `game_play_counts`.`game_id` = 11 LIMIT 1 FOR UPDATE COMMIT
対象のデータがある場合はトランザクション内でFOR UPDATEをしていて、ロックは取れていました。
今回は興味本位でfind_or_create_byを使うとどういったクエリになるか確認しました。find_or_create_byもduplicateのリスクがあるし、lockと組み合わせると初回アクセス時にギャップロックしてしまうしで、使い方によっては良くないことがあるなという感じです。エラー無く処理したいということならichirin2501先生のスライドにあるINSERT ... ON DUPLICATE KEY UPDATE
を使うのも良さそうです。INSERT ... ON DUPLICATE KEY UPDATE
は呼ばれる度にAUTO INCREMENT値がどんどん増えていってしまうので、BIGINTを使うなりしてOut of rangeになるリスクを避ける必要があるのでご注意ください。
またActiveRecordで気になるメソッドがあったら随時クエリをのぞいていけたならと思います。
今年が終わりそうなので、自分がエンジニアをやる理由を振り返ってみる
以上です。
多段SSHの設定いつも忘れるのでブログる。
まず設定の例から書くと
Host fumidai HostName hoge User macotasu Port 10022 Host target-server HostName fuga User macotasu Proxycommand ssh -CW %h:%p fumidai
こんな感じでsshの設定をかいて多段sshにすることができた。
よく理解していなくてハマったのがProxycommandの挙動である。 Proxycommandを使うと、Proxy先のhostからsshするようになる。 なのでtarget-serverへのsshの設定はfumidaiから見た設定を書く必要がある。 この基本的なところを理解していないと、複雑な環境設定がしているところへの 多段SSHで混乱してはまるなーってなった。(実際ハマった)
2年8ヶ月勤めていた面白法人カヤックを退職して、アニメイトラボで働き始めました。
10月~11月に転職活動をしていて、いくつかの魅力的な企業からオファーを頂いていたのですが、アニメイトラボは設立間もない会社で地盤整えていくフェーズだったのが楽しそうだったり、以前から一緒に働いてみたいと思っていた方が在籍していたりなどのご縁もあって、入社することを決めました。
オフィスは千駄ヶ谷にあって、会社の近くに銭湯があるのが良い感じです。
前職でお世話になった方々と「次が決まったら教えてね」というお話もあったので、ご挨拶を兼ねてブログに書きました。
今後とも皆様よろしくお願いいたします。