読者です 読者をやめる 読者になる 読者になる

眠すぎて明日が見えない

我が人生、眠さに勝るもの無し

alerty-plugin-slackを書いてみた

cronの失敗を検知する方法どうしようかなと思っていたら、sonotsさんが作られたalertyというツールを見つけてこれだ!となり、slackのpluginがなかったので作ってみた。

※現状githubの自分のRepositoryにしかあげてないです

alertyとは

sonotsさんが作られた、cronの失敗の結果を通知するためのツールです。 pluginを追加することによって通知先を容易に変更することが出来ます。

詳しくは下記のsonotsさんの記事をご参照ください。

blog.livedoor.jp

alerty-slack-plugin とは

名前の通りalertyでの通知をslackに流すためのpluginです。

github.com

使うときは下記のようなymlを書く必要があります。

# test.yml

log_path: STDOUT
log_level: debug
plugins:
  - type: slack
    webhook_url: https://hooks.slack.com/services/XX/XX/XX
    subject: "FAILURE: [${hostname}] ${command}"
    icon_emoji: ":innocent:"
    http_options:
        open_timeout: 10

注意しないといけないことは、

  • typeは type: slack を指定する
  • webhook_urlは incoming-webhooksから取得できるURLを設定する

以上の二点ぐらいかと思います。

例のymlで実行すると次のような結果になります。

実行コマンド

$ alerty -c test.yml -- hogehoge

結果

f:id:Maco_Tasu:20160713165730p:plain

まとめ

alertyのpluginの追加がとても簡単なので、今後も必要に応じてシュッとpluginを追加していきたいです。 作ったばかりなので、色々改善しないといけない点があるかもしれません。使ってみたという方いらっしゃいましたらフィードバック等いただけますと幸いです :bow:

小さなチーム、大きな仕事【完全版】

books

誕生日のお祝いでカロリさんから頂いた「小さなチーム、大きな仕事【完全版】」を読んだ。

  • 小さいほうが総じて動きやすい
  • 素直に接しよう
  • 進んでやることみつけて手を動かす人が大事
  • 人をむやみに採用してもコストになるだけ。必要な人を考える

以上の点が印象的だった。

本に書かれた内容自体は概ね共感できるし、実体験から書かれたいい本だった。この本と出会うきっかけをくれたカロリさんに感謝です🙏

Railsでサーバー時刻を任意の時間にする

programming

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で複合PKを使う際に`Undefined method 'to_sym'`がでて辛かったメモ 

Railsidcreated_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することができた。

ActiveRecordのfind_or_create_byが投げるクエリを検証した

RubyのORMのActiveRecordにfind_or_create_byというメソッドがある。このメソッドはデータがあったらselectした結果が返ってきて、ない場合はinsertをしてその結果を返してくれるという夢のような機能を実現してくれているらしい。 このデータがあったらselect、ない場合はinsertのような処理は扱うときによく考えないとロック周りでDBが詰まってつらい思いすることがある。そこでこのメソッドはどんなクエリを投げるのか検証してみた。

検証方法

  • ActiveRecord::Baseを系統したGamePlayCountモデルに対してrails cで操作を行う。
  • GamePlayCountは対象のgame_idを遊ぶとcountを++していくテーブルだと仮定する。

テーブル構造はこんな感じ。

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に対して下記の操作を行う。

  • find_or_create_by単体で使う
  • find_or_create_byとlockを組み合わせて使う
  • 検証する際は一つのケースに対してtransaction内、外両方のパターンを試してみる

検証結果はrails cでActiveRecord::Base.logger = Logger.new(STDOUT)を実行してログに流れるSQLを順番に抽出したものを貼っています。早速検証スタートだドン!

find_or_create_by単体で使う

検証1

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しようとすると勝手にトランザクションがはられるようです。

検証2

明示的にトランザクションを指定して試します。

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になりそうですね。

lockの検証

find_or_create_byとlockの組み合わせを実行する前にlock単体でどういったクエリを投げるか確認します。

検証3

GamePlayCount.lock(:true)
SELECT `game_play_counts`.* FROM `game_play_counts` FOR UPDATE

テーブルに対してFOR UPDATEかけた...トランザクション外だから影響ないけどテーブルロック一歩手間ですね。

検証4

次にトランザクション内で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を組み合わせて使う

find_or_create_byとlockを呼ぶとどうなるのか。大変気になるので早速検証します。

検証5

まずは明示的にトランザクション囲まないケースから試します。

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

トランザクション外なので同様にロックは取れません。

検証6

明示的にトランザクション囲んだケースを試します。

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で気になるメソッドがあったら随時クエリをのぞいていけたならと思います。

何でエンジニアをするのか

other

今年が終わりそうなので、自分がエンジニアをやる理由を振り返ってみる

  • 新しい技術に触れる瞬間が楽しい
  • コード書いている時間が楽しい
  • 一つのアーキテクトを作り上げるのが楽しい
  • 作り上げたものがユーザに届くのが嬉しい
  • 生じる問題をエンジニアリングで解決するのが楽しい
  • 結果自分のスキルになって成長を感じるのが嬉しい
  • etc..

以上です。

多段SSHする際の.ssh/configの設定

programming

多段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で混乱してはまるなーってなった。(実際ハマった)