まっしろけっけ

めもてきなやーつ

Sidekiq について基本と1年半運用してのあれこれ

はじめに

実際に運用していた時に非同期にしていた主な処理は下記のようなものがあります。

  • iOS Android の push 通知の送信処理
  • ログの作成
  • 様々な外部 API の呼び出し
  • 非同期で更新しても問題ないデータの更新

Sidekiq is なに

sidekiqは非同期処理を実現する gem
他にも Ruby で非同期処理を実現できる有名な gem には
resque や delayed_job 等がある。

sidekiq.org

Enterprise版等もありますが、
今回はOSS版を使用している前提でのお話しです。

他の非同期処理が可能な gem との簡単な比較

FAQ · mperham/sidekiq Wiki · GitHub
この内容は結構真実を語っていることを最近知った

Sidekiq
  • Redis
  • マルチスレッド
  • リトライ処理あり
  • おしゃれなダッシュボード
Resque
  • Redis
  • ジョブごとにフォーク
  • リトライ処理なし
  • Sidekiqに比べると簡素なダッシュボード
Delayed Job
  • DB(専用テーブル作成)
  • リトライ処理あり
  • delay method が便利
  • 基本的にDBを使うので導入簡単

実装例

Redis は既にセットアップ済みとする

sidekiq を追加

$ vi Gemfile
+ gem 'sidekiq'

$ bundle install

config を追加

$ vi config/sidekiq.yml
:verbose: true
:pidfile: ./tmp/pids/sidekiq.pid
:logfile: ./log/sidekiq.log
:concurrency: 10 # worker process 数
:queues: # 処理するキュー名
  - default
  - user

env による設定の変更方法

production:
  :concurrency: 25
staging:
  :concurrency: 15

下記のように記述することで priority が設定できる

:queues:
  - [user, 2]
  - [default, 1]

worker file を作成

# UserWorker を作成
$ bundle exec rails g sidekiq:worker User
      create  app/workers/user_worker.rb
      create  test/workers/user_worker_test.rb

user_worker を編集
※今回は例としてわかりやすい処理にしています。

$ vim app/workers/user_worker.rb
- def perform(*args)
- # Do something
+ sidekiq_options queue: :user # キュー名指定がない場合は default になる
+ def perform(id, name)
+   user = User.find(id)
+   user.update(name: name)

キューを積む

$ bundle exec rails c
irb> UserWorker.perform_async(1, "test")
=> "f212bd8bae56c79467494ec8"
irb> User.find(1) # この状態ではまだ record が更新されていない
  User Load (0.3ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User id: 1, name: "1", created_at: "2015-10-11 13:13:39", updated_at: "2015-10-11 13:13:50">

Sidekiq を起動する

$ bundle exec sidekiq -C config/sidekiq.yml -d
# ちょっと時間をおいて
$ bundle exec rails c
irb> User.find(1) # name が更新されていることが確認できる
  User Load (0.3ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User id: 1, name: "test", created_at: "2015-10-11 13:13:39", updated_at: "2015-10-11 13:17:14">

これで簡単な実装例はおしまいです。

Sidekiq の enqueue の解説

Sidekiq が Redis にどのように queue を追加するかを解説する。
Sidekiq は使う method によって使う Redis のデータ型が違う

perform_asyncの場合

「セット型」と「リスト型」を使う。

1.セット型で queues という key で各ワーカーのキュー名を member として登録する
(実装例の場合は member に user を指定する)

2.リストの先頭に queue:キュー名 をキーにして string を追加する。
(実装例の場合は queue:user というキー名になる)

3.string は { class: worker class 名, arg: perform_asyncの引数配列, retry: retry機能が有効かのフラグ, queue: キュー名, jid: SecureRandom.hex(12) で生成された job id, created_at: 作成日, enqueued_at: キューに追加した時間 } この Hash を json にしたもの

以下が実装例での string の例

# Hash
{ class: "UserWorker", args: [1, "test"], retry: true, queue: "user", jid: 6284ea2d3637756fc121b8f7, created_at: 1444571406.7752862, enqueued_at: 1444571406.7754705 }

# Json化
"{\"class\":\"UserWorker\",\"args\":[1,\"test\"],\"retry\":true,\"queue\":\"user\",\"jid\":\"6284ea2d3637756fc121b8f7\",\"created_at\":1444571406.7752862,\"enqueued_at\":1444571406.7754705}"

perform_in の場合

「ソート済みセット型」を使う

1. ソート済みセット型で schedule という key で score に 実行時間を指定して member を登録する

2. member は { class: worker class 名, arg: perform_asyncの引数配列, retry: retry機能が有効かのフラグ, queue: キュー名, jid: SecureRandom.hex(12) で生成された job id, created_at: 作成日 } この Hash を json にしたもの

以下が実装例での member の例

# Hash
{ class: "UserWorker", args: [1, "test"], retry: true, queue: "user", jid: 6284ea2d3637756fc121b8f7, created_at: 1444571406.7752862 }

# Json化
"{\"class\":\"UserWorker\",\"args\":[1,\"test\"],\"retry\":true,\"queue\":\"user\",\"jid\":\"6284ea2d3637756fc121b8f7\",\"created_at\":1444571406.7752862}"

Redis について

Sidekiq を使う上で欠かせない Redis ですが、今回の記事で Redis のことまで深く説明すると
だいぶ長い記事になるので別記事として後日公開します。

工夫した点

ここからが本題の知見と言ってもいいかなという部分になります。

1. Redis の構成

当時開発していた Rails のアプリケーションでは、
session, sidekiq, cache に Redis を使用していました。
それぞれ別々の Redis にデータを保存していました。

別々にした理由は下記

  • Redis はオンメモリ型のKVSなので容量的な制約が大きい(実メモリの約半分程度)
  • 複数の用途で使い容量が設定によっては消えて欲しくないものが消える危険
  • 1つの Redis で運用をし後々分割しようと思った際にデータを分けるのが面倒
構成

レプリケーションを組み,Redis Sentinelで自動フェイルオーバーするように設定を行う。
アプリケーション側は下記の記事で書きましたが, gem の redis-sentinel を導入

shiro-16.hatenablog.com

redis-sentinel を導入すると Rails は Redis Sentinel から現在の master 情報を取得し接続、
障害により接続に失敗した場合に 「Redis Sentinel から master 情報を取得し接続」を繰り返し
master が切り替わったタイミングで正常に接続ができるようになるという感じ

2. 引数は出来るだけ少なく

perform_async 等の引数は出来るだけ少なくしましょうという話

例として Twitter でファボられた時に push を送る処理を実装
(実際には1ユーザに複数のtokenが紐づく可能性がある)

# ダメな例
# token: device_token or registration_id
# message: push 内容
def perform(token, message)
   FavoritePush.send(token, message)
end

# 良い例
# id: favorite情報を保存している record の id
def perform(id)
  favorite = Favorite.find(id)
  message = FavoritePush.message(favorite)
  FavoritePush.send(favorite.tweet.user.token, message)
end
何が良いのか?

・Redis の容量の圧迫を軽減出来る
・キューを積んでから実行されるまでに record が更新された場合にも対処出来る(今回の場合 token が更新されて push が送信出来ない問題を回避出来る)
・console 等からキューを積む場合に id を渡すだけなのでシンプル
SQLの発行回数が増えるのですが primary key や index を使用した検索になることが多いと思われるので問題ないかと思われる。
DBが重くなるのであれば Slave を増やせばいいので Redis の容量問題に比べれば微々たるものだし、primary key や index を使用しても重いならそれは設計が(ry
・自分が運用していた時は引数は1〜3つでした。(例外が一部ありましたが)

※ ただし引数が多くなる場合も、勿論あります。

3. 判定処理等を worker 側に持たせる

今回は2の例に push を送信するかどうかを判定する処理を追加する

# ダメな例
def perform(token, message)
  FavoritePush.send(token, message)
end
# キューを積む際に
# PushWorker.perform_async(favorite.tweet.user.token, FavoritePush.message(favorite)) if favorite.tweet.user.send_push?


# 良い例
def perform(id)
  favorite = Favorite.find(id)
  return unless favorite.tweet.user.send_push?
  message = FavoritePush.message(favorite)
  FavoritePush.send(favorite.tweet.user.token, message)
end
何が良いのか?

今回の場合はシンプルな判定だが、もっと複雑な判定を行う場面は多い
そうなった場合に特に有効

・キューを積む際に判定するより同期処理側の処理速度は速くなる
・今回の場合は message を生成する処理も worker 側で行うので同期処理側の処理速度は速くなる
・console 等からキューを積む場合に判定内容を気にしなくて良くなる

※ Redis に積むキューの数自体は増えてしまうので Redis の使用量は増えてしまいます

4. retry 機能に注意する

Sidekiq には retry 機能があるのでそのことを念頭に置いて実装を行う必要がある。
push 送信の処理に送信済みかフラグを持たせる

def perform(id)
  favorite = Favorite.find(id)
  return unless favorite.tweet.user.send_push?
  message = FavoritePush.message(favorite)
  FavoritePush.send(favorite.tweet.user.token, message)
  favorite.update(send_push: true)
end

retry 機能があるのでもし favorite.update でエラーが起こった場合
retry されて再度実行されます。
なので favorite.update が正常に終了するまで同じ内容の push を送信し続けることになってしまいます。

例外をうまく扱うか update を別 worker として実装し push 送信後にキューを積む等の工夫が必要

※ retry を個別に off にすることも可能

5. record が削除されている場合も考慮する

Sidekiq だけの話ではないですが、
非同期処理なので処理が実行される際には既に対象の record が削除されている可能性も考えなければなりません。
4.の retry の話を念頭に置いて ActiveRecord::RecordNotFound 等の例外を上手く処理する必要があります。

6. batch 処理

「3.判定処理等を worker 側に持たせる」と同じような内容になるのですが、
batch 等であるテーブルの各 record に対して複雑な処理をする場合、
処理は全て worker 側で行ってしまおうということです。

例)全ユーザを対象に何かしらの計算を処理を行う batch があるとする

# ダメな例
User.find_each do |user|
  # 複雑な処理

  UserWorker.perform_async(user.id, result)
end
class UserWorker
  def perform(id, result)
    user = User.find(id)
    user.update(result: result)
  end
end

# いい例
User.select(:id).find_each do |user|
  UserWorker.perform_async(user.id)
end

class UserWorker
  def perform(id)
    user = User.find(id)
    # 複雑な処理

    user.update(result: result)
  end
end
良い点

・時間がかかる複雑な処理をマルチスレッドで行うことで処理速度が上がる

deploy

deployに関しては下記のページを参考にするのが良さそう
Deployment · mperham/sidekiq Wiki · GitHub

その他

Best Practices · mperham/sidekiq Wiki · GitHub
ここら辺も合わせて読むと良いかと

最後に

なぜこのような記事にまとめようと思ったのかというと
前々からまとめておこうとは思ったのですが、
最近 Sidekiq を使用していないので忘れてしまいそうだったことと、
最近 Sidekiq 感覚で Delayed Job を使用したら少々痛い目を見たという
辛い経験があったので非同期処理についてまとめておこうといった感じです。

また、ここに書いてあることが全て正しいわけではなく
自分の開発していたアプリケーションにはこのパターンが良かったというだけで、
これに沿ったからといって問題が全て解決するというわけではないと思います。

Sidekiq と Redis の特徴を理解して上手く付き合っていくのが良いかと思います。

自分は Sidekiq 好きなので贔屓目の記事になっているかもしれません。

表参道.rb #5 にてLTしてきた

表参道.rb is

omotesandorb.connpass.com

おしゃれな感じのする地域Rubyコミュニティ
人気らしく毎回すぐに枠が埋まっているらしい、
今回は会場が会社から徒歩で行けたことと先月の途中から勤務時間が変更になって
勉強会に参加しやすくなった(会社的には勉強会行くって言えば早く退社したり実は出来るのだが)ので
行くならLTしよ〜という軽いノリでLT枠で参加した。

今回は第五回目だが自分は初参加でした。

LT

発表者が十数名いて色々なテーマでLTされていたので面白かった。
英語力…ということ出来事がちょうどあった日に英語に関するgemの話を聞けたり
jsの話があったり,.rbなのにErlang + Elixirの話しかしなかったりw
もちろんRubyの話もあったし、frozen literalとかはこれはhsbtさんが言ってたあれか!という感じだった。

自分の発表内容

自分はStyleStatsの宣伝紹介をしてきました。
資料は下記

www.slideshare.net

  • StyleStatsの説明
  • Ruby StyleStatsの説明
  • Ruby StyleStatsで使用したgemの説明
  • v0.2.0の説明
  • v1.0.0への展望

という感じでLTを行いました。

実はLTした際は一部のtestが不十分だったので、まだv0.2.0を公開できていなかった。
なのでその旨を説明したページが1,2ページあった。

懇親会など

sansan株式会社さんが手の込んだお食事を大量に提供してくれていて
結構お腹いっぱいになってしまった。感謝感謝です。

色々な方とお話し出来て面白かった。
この業界の狭さをまたまた感じることとなったのでした。

最後に

表参道.rb 運営者の方々
sansan株式会社様
お話しをさせて頂いた方々
LTを聞いて頂いた方々
ありがとうございました!!1

Rails Girls Tokyo 5thでコーチとスポンサーLTしてきたよっていう話

Rails Girls is 何

railsgirls.com

ここら辺に書いてあります。
何故やってるかは下記あたりが参考になるかと

Rails Girls: Not Only for Girls - RubyKaigi 2014
日本RailsGirls活動紹介 // Speaker Deck


Girlsという名前が付いてますが、女性同伴なら男性もおkらしいですよ。
今回1名いましたね。(スタップT着てない男性いるな…?とは思っていたのですが自分の周りのことでいっぱいいっぱいで後で参加者だと知った)

何故コーチをやったのか?

いくつか理由があります。
ここに書いた以外にも細かい理由ならもっとあると思う。(単純に面白そうとか)

誰かに教えるといこと

誰かに何かを教えるということで、間違っていることを教えないようにとか
教えようとしている内容をもう一度自分で勉強してみたり
自分にとっても色々と学びがあるというのは経験上知っていたので自分が成長する為にという点
前職の際に Ruby を教えた経験が一度あるがなんか上手く教えることが出来なかったな…というモヤモヤもあった。

4th の開催の際の出来事

Rails Girls を知ったのは約1年前の 4th 開催の時で、
その際もコーチに応募しようと思っていたのですが忘れていて結局応募しなかった。
当時同じチームで働いていたフロントのエンジニアの女性が 4th に参加してすごく楽しかったと言っていたし、
その後の業務で Rails 側の簡単な修正とかもやってくれるようになって
Rails Girls の恩恵を少なからず受けていたのでそういう風な人が増えればいいなと思っていた。
また、その女性は社内の研修っぽい案件で web server が必要になり Rails 使って作っていて
そのレビューとか頼まれてしていた。

4th 開催時の自分の周りの環境

周りに Rails のプロジェクトが少なかったこともあり、
普及したいと思っていた自分にとって何かしらヒントが得られればと当時は思っていた。
今は単純に Rails 使う人増えればいいなと思っている。

多様性の話

上記で紹介したスライドでも言及されていますが、
女性だから◯◯じゃないといけない、男性だから◯◯じゃないといけないとか
そういうのは正直アホくさって思ってる人間なので女性のエンジニアが増えるのは良いことだと思った。
女性エンジニアが増えることにより、色々な考えが生まれ、それがサービス等に反映されていくことだと思っている。

Rails Girls Tokyo 5th

1日目は install day ということで RubyRails を install して終わり
2日目で ワークショップとして Rails でアプリケーションを作成する。

だいたい5,6人のチームに分かれそれぞれのチームにコーチが3人ずつ着くといった感じだった。
自分のチームは@yotii23,@kunitooと自分がコーチ
事前の打ち合わせでチームの進行や説明役を自分がやることになっていた。
(鳥居さんは去年のRubyKaigiの発表聞いていたしschooの配信も見てたり一方的に知っていたので一緒のチームで進行的なやつをやるのはプレッシャーだったw)

当日の様子を知りたい方は Twitter でタグでも漁れば出てくるとおもいます。

感想

Rails Girls がどうやって作られているかの一端でも知ることが出来た。
・実際に教えてみると、想定していなかった質問(sqlite3のデータみたいとか)が飛んできてその場で調べたりした。
・プログラミングが初めてという方に説明することの難しさ、
自分が頭で理解していることを言語化し他人に説明する難しさをとてもを感じた。
・チーム内の進行や説明をする役を自分からやるって言って結果的には良かった。緊張もしたが上記のような説明の難しさを感じることが出来たし、鳥居さんからアドバイス貰ったり、手助けをして貰ったり人に教える際の勉強になった。
・なにより Girls に楽しかったと言ってもらえたし喜んで貰えたのが嬉しかった。
・楽しかった。
・かわいいTシャツ貰った。

・この業界は狭いなと感じることが多々あった

言いたいことがもっとある気がするけど、とりあえず終わりです。
オーガナイザーの方々、コーチ、スタッフの方々、参加者の皆さん貴重な体験をありがとうございました。

スポンサーLT

スポンサーLT やることになった経緯

@hsbtこと弊社の柴田さんが slack にて「当日RubyConf Taiwan 行ってるから誰か代わりにLTやってきて」と発言

その時はまだコーチをやることが決まってなかったのでとりあえずコーチやることになったら考えよ程度
その週末くらいにコーチをやることが決まったので

@shiro16「@hsbt: まだ Rails Girls の LT って決まりましたか?」
@hsbt「まだです」
@shiro16 「じゃやります」

みたいな感じのやりとりがあり入社2ヶ月の新人がスポンサーLTすることに決まった。

LT 資料

画像がほとんどなので意味わからないかもしれないけどとりあえず置いておきます。
会社の説明、サービス紹介、CTO,チーフエンジニア紹介、面白制度や新卒研修等を紹介した。(一部削ってます)

www.slideshare.net

最後に

弊社ではエンジニアをはじめ、いろんな人を募集していますので下記から何卒pepabo.com

RubyでStyleStatsをつくった

StyleStats is 何?

StyleStatsの説明は下記をご覧いただくのが一番早いかと思います。

html5experts.jp

簡単に言うと@t32kさんが作ったCSSを解析するツールです。
先週のYAPC::Asia Tokyo 2015の2日目でLTをなさっていたっぽいので
知っている方もいるかと思います。

経緯

今回StyleStatsをRubyで作ることになった経緯は下記です。

こんなことをつぶやいていたので、おっ!と思って反応してみた

そしたらお願いされたので30秒くらい考えて面白そうだと思ったので
頑張ってみる旨を伝えた。
8月中に公開できればいいかなという感じで考えていた。

t32kさんとは前職で一時期一緒のチームで働いていたり等の関わりがありました。

作る際に注意した点

  • CLIツールなので出来るだけ他のGemに依存しないようにしたい。
  • できる限り本家のStyleStatsと同じオプション等を指定して使えるようにしたい。

特に注意した点は上記の2つ

gem依存問題に関して

今回実装する際の大きなポイントが

  • CSSを解析する必要があるのでCSSをパースする
  • URLを渡された際にはHTMLをパースしてSTYLEタグとLINKタグからstylesheetを取得する

上記の2点がありそれを行うgemを使うか自前で実装するかする必要があった。
自前で実装するにしては結構な時間が必要になるなと思ったのでgemを使うことに決めた。

CSSのパースに関しては premailer/css_parser · GitHub こちらのgemくらいしか
要件を満たしてくれそうなやつがなかったので選択。

HTMLのパースに関してはNokogiriが有名ですがNokogiriはインストールする際に
libxmlのエラーが発生するというよく見かける問題があるので、
そこで諦められるのは嫌だったので他のgemを探したら YorickPeterse/oga · GitHub こちらのgemが良さそうだったので選択した。
(最初はNokogiriを使っていた)

作ってみての感想

出来たのがこちら
最初はgem名もstylestatsで作っていたが、本家とこのgemを入れた際に面倒くさいなと思って
Rubyっぽくstyle_statsにした。

optionのほとんどが未実装の状態だがt32kさんに確認してもらったら
どんどんリリースしちゃっていいよって感じだったのでリリースすることにした。

github.com

  • 作ってみて気づいたがStyleStatsは思っていた以上に高機能で良くできていた。
  • Rubyだったので文字の色は赤くしてみた。
  • まだoptionはformatしか指定出来ない。
  • optionは今後のversion upで対応していく。
  • CSSにそんなに詳しくなかったが、CSSのParserを自作出来るのでは?と思える程度には理解出来た。気がする
  • 作ったけど、クソコードな部分が多いので、v1.0.0までに全部書きかわる可能性があるかもしれない
  • 楽しかった!

最後に

このような機会を与えて頂き有り難く思います。
実は勉強会かtwitterでStyleStatsを知った時に下記のような感想を持っていました。
なのでとても楽しく開発できたし、少しても関われることができありがたいという思いです。

golangのフレームワークrevelを使用して掲示板っぽいものを作ってみる

はじめに

今回は golang の revel framework を使用して掲示板っぽいやつを作ってみる。
掲示板っぽいと言っても基本的には APIJson を返すことにする。
しかし html を返す場合もやることはほぼ変わらない。

今回作成する API は一般的な掲示板でいうスレッドは存在せず
レスのみを扱う。(今後スレッド対応やログイン対応等を行っていくかもしれない)
endpoint は下記のようにする

http method path 説明
GET /comments 一覧
GET /comments/:id 詳細
POST /comments 登録
DELETE /comments/:id 削除

revel is 何 ?

revel は高機能な重量級の framework で Ruby で言うと sinatra というより
Rails に近い framework になります。

環境

$ go version
go version go1.4.2
$ mysql --version
mysql  Ver 14.14 Distrib 5.6.13

revel を install する

下記で install できる

$ go get github.com/revel/revel # revel framework を取得
$ go get github.com/revel/cmd/revel # revel command を取得

プロジェクトを作成してみる

とりあえずハロワが表示されるとこまで進める。

$ revel new github.com/shiro16/golang-bbs # rails new みたいなやつ
~
~ revel! http://revel.github.io
~
Your application is ready:
   ${GOPATH}/src/github.com/shiro16/golang-bbs

You can run it with:
   revel run github.com/shiro16/golang-bbs
$ cd ${GOPATH}/src/github.com/shiro16/golang-bbs
$ revel run # applicaton の起動

これで「http://localhost:9000/」にアクセスすると
「It works!」が表示される。

API の endpoint を定義する

routing を追加する

routing は「config/routes」で管理されている
下記のように編集する。
今回の Api の path は「/api/v1」をベースにする

GET     /                                       App.Index
+ GET     /api/v1/comments                        ApiV1Comments.Index
+ GET     /api/v1/comments/:id                    ApiV1Comments.Show
+ POST    /api/v1/comments                        ApiV1Comments.Create
+ DELETE  /api/v1/comments/:id                    ApiV1Comments.Delete
controller を作成する

今回は api で共通して使うであろう Error 処理等をまとめて記述する
ベースとなる controller も作成しておく

// app/controllers/api/v1/v1.go
package controllers

import (
        "github.com/revel/revel"
        "github.com/shiro16/golang-bbs/app/utils"
        "net/http"
)

// 埋め込みによって revel.Controller をラップした ApiV1Controller を定義する
type ApiV1Controller struct {
        *revel.Controller
}

// エラーの際に返す Json 用の構造体
type ErrorResponse struct {
        Code    int    `json:"code"`
        Message string `json:"message"`
}

// 正常な際に返す Json 用の構造体(今回は1種類で統一する)
type Response struct {
        Results interface{} `json:"results"`
}

// 引数として渡されて interface にリクエストの Json の値を格納する
func (c *ApiV1Controller) BindParams(s interface{}) error {
        return utils.JsonDecode(c.Request.Body, s)
}

// Bad Request Error を返すやつ
func (c *ApiV1Controller) HandleBadRequestError(s string) revel.Result {
        c.Response.Status = http.StatusBadRequest
        r := ErrorResponse{c.Response.Status, s}
        return c.RenderJson(r)
}

// Not Found Error を返すやつ
func (c *ApiV1Controller) HandleNotFoundError(s string) revel.Result {
        c.Response.Status = http.StatusNotFound
        r := ErrorResponse{c.Response.Status, s}
        return c.RenderJson(r)
}

// Internal Server Error を返すやつ
func (c *ApiV1Controller) HandleInternalServerError(s string) revel.Result {
        c.Response.Status = http.StatusInternalServerError
        r := ErrorResponse{c.Response.Status, s}
        return c.RenderJson(r)
}

これでベースとなる処理が完了したので comments controller を作成していく

// app/controllers/api/v1/comments.go
package controllers

import (
        "github.com/revel/revel"
        "github.com/shiro16/golang-bbs/app/controllers"
)

type ApiV1Comments struct {
        ApiV1Controller
}

func (c ApiV1Comments) Index() revel.Result {
        r := Response{"index"}
        return c.RenderJson(r)
}

func (c ApiV1Comments) Show(id int) revel.Result {
        r := Response{"show"}
        return c.RenderJson(r)
}

func (c ApiV1Comments) Create() revel.Result {
        r := Response{"create"}
        return c.RenderJson(r)
}

func (c ApiV1Comments) Delete(id int) revel.Result {
        r := Response{"delete"}
        return c.RenderJson(r)
}

これで controller の作成は完了。
DB へ接続してデータを取得する等は次で行います。
実際に起動してみると下記のようになるかと。

$ revel run github.com/shiro16/golang-bbs

# 別窓等で
$ curl http://localhost:9000/api/v1/comments
{
  "results": "index"
}
$ curl http://localhost:9000/api/v1/comments/1
{
  "results": "show"
}
$ curl -X POST http://localhost:9000/api/v1/comments
{
  "results": "create"
}
$ curl -X DELETE http://localhost:9000/api/v1/comments/1
{
  "results": "delete"
}

DB 周りの処理を作成する

現状 ORM が revel には無い為自分で好きなものを使う。
revel の samples では gorp が使われているが
今回は gorm を使用する。

validation には validator を使用する。

DB の接続情報を追加

golang_bbs_development」という名前の DB 名にしています。

// conf/app.conf
[dev]
mode.dev = true
results.pretty = true
watch = true
watcher.mode = "normal"
log.trace.output = off
log.info.output  = stderr
log.warn.output  = stderr
log.error.output = stderr
db.info = "root@/golang_bbs_development?charset=utf8&parseTime=True
model を作成する

gorm を使用するので取得する
validator も取得しておく

$ go get github.com/jinzhu/gorm
$ go get gopkg.in/validator.v2

comment model を作成

// app/models/comment.go 
package models

import (
        "time"
)

type Comment struct {
        ID        uint64     `gorm:"primary_key" json:"id"`
        Nickname  string     `sql:"size:64" json:"nickname" validate:"max=64"`
        Body      string     `sql:"size:255" json:"body" validate:"min=1,max=255"`
        CreatedAt time.Time  `json:"created_at"`
        UpdatedAt time.Time  `json:"updated_at"`
        DeletedAt *time.Time `json:"deleted_at"`
}

DB への接続などの初期化処理を作成

// app/controllers/gorm.go
package controllers

import (
        _ "github.com/go-sql-driver/mysql"
        "github.com/jinzhu/gorm"
        "github.com/revel/revel"
        "github.com/shiro16/golang-bbs/app/models"
        "log"
)

var DB *gorm.DB

func InitDB() {
        db, err := gorm.Open("mysql", dbInfoString())
        if err != nil {
                log.Panicf("Failed to connect to database: %v\n", err)
        }

        db.DB()
        db.AutoMigrate(&models.Comment{}) # ここで table の作成を行っている
        DB = &db
}

func dbInfoString() string {
        s, b := revel.Config.String("db.info")
        if !b {
                log.Panicf("database info not found")
        }

        return s
}

上記を呼び出す処理を追記

// app/init.go
- import "github.com/revel/revel"
+ import(
+         "github.com/revel/revel"
+         "github.com/shiro16/golang-bbs/app/controllers"
+ )

func init() {
....
+ revel.OnAppStart(controllers.InitDB) // 28行目くらいに
}

DB 周りの処理の作成が完了し、
残すは作成した model を controller で実際に使用する処理を残すのみ

controller で model を使用する

comments controller を編集していく

package controllers

import (
	"github.com/revel/revel"
+ 	"github.com/shiro16/golang-bbs/app/controllers"
+ 	"github.com/shiro16/golang-bbs/app/models"
+ 	"gopkg.in/validator.v2"
)

type ApiV1Comments struct {
	ApiV1Controller
}

func (c ApiV1Comments) Index() revel.Result {
+ 	comments := []models.Comment{}

+ 	if err := controllers.DB.Order("id desc").Find(&comments).Error; err != nil {
+ 		return c.HandleInternalServerError("Record Find Failure")
+ 	}

+ 	r := Response{comments}
- 	r := Response{"index"}
	return c.RenderJson(r)
}

func (c ApiV1Comments) Show(id int) revel.Result {
+ 	comment := &models.Comment{}

+ 	if err := controllers.DB.First(&comment, id).Error; err != nil {
+ 		return c.HandleNotFoundError(err.Error())
+ 	}

+ 	r := Response{comment}
- 	r := Response{"show"}
	return c.RenderJson(r)
}

func (c ApiV1Comments) Create() revel.Result {
+ 	comment := &models.Comment{}

+ 	if err := c.BindParams(comment); err != nil {
+ 		return c.HandleBadRequestError(err.Error())
+ 	}

+ 	if err := validator.Validate(comment); err != nil {
+ 		return c.HandleBadRequestError(err.Error())
+ 	}

+ 	if err := controllers.DB.Create(comment).Error; err != nil {
+ 		return c.HandleInternalServerError("Record Create Failure")
+ 	}

+ 	r := Response{comment}
- 	r := Response{"create"}
	return c.RenderJson(r)
}

func (c ApiV1Comments) Delete(id int) revel.Result {
+ 	comment := models.Comment{}

+ 	if err := controllers.DB.First(&comment, id).Error; err != nil {
+ 		return c.HandleNotFoundError(err.Error())
+ 	}

+ 	if err := controllers.DB.Delete(&comment).Error; err != nil {
+ 		return c.HandleInternalServerError("Record Delete Failure")
+ 	}

+ 	r := Response{"success"}
- 	r := Response{"delete"}
	return c.RenderJson(r)
}

これで各 endpoint にアクセスしてみる

$ revel run github.com/shiro16/golang-bbs
# 別窓等で
$ curl -H "Content-type: application/json" -X POST -d '{"nickname":"shiro16", "body":"test comment"}' http://localhost:9000/api/v1/comments
{
  "results": {
    "id": 1,
    "nickname": "shiro16",
    "body": "test comment",
    "created_at": "2015-08-13T21:20:46.681910871+09:00",
    "updated_at": "2015-08-13T21:20:46.681910871+09:00",
    "deleted_at": null
  }
}
# validation がちゃんと機能しているかチェック
$ curl -H "Content-type: application/json" -X POST -d '{"nickname":"shiro16", "body":""}' http://localhost:9000/api/v1/comments
{
  "code": 400,
  "message": "Body: less than min"
}
$ curl http://localhost:9000/api/v1/comments
{
  "results": [
    {
      "id": 1,
      "nickname": "shiro16",
      "body": "test comment",
      "created_at": "2015-08-13T12:20:47Z",
      "updated_at": "2015-08-13T12:20:47Z",
      "deleted_at": null
    }
  ]
}
$ curl http://localhost:9000/api/v1/comments/1
{
  "results": {
    "id": 1,
    "nickname": "shiro16",
    "body": "test comment",
    "created_at": "2015-08-13T12:20:47Z",
    "updated_at": "2015-08-13T12:20:47Z",
    "deleted_at": null
  }
}
$ curl http://localhost:9000/api/v1/comments/2 
{
  "code": 404,
  "message": "record not found"
}
$ curl -X DELETE http://localhost:9000/api/v1/comments/1
{
  "results": "success"
}

まとめ

こんな感じで雑な部分もありますが revel を使って基本的な処理の作成が完了しました。
validation に関しては model にてチェックを行った方がいいかと思いますが、
今回は時間の関係で controller にて行っています。
今回作成したものはこちらで公開しています。
API 以外の処理も追加していますので参考にしてください。

今回の説明以外の詳しい内容は下記を参考にするといいと思います。
とくに sample を配布しているので「go get github.com/revel/samples」で取得して
見てみるといいかと思います。
Welcome to Revel, the Web Framework for Go!

GMOペパボに入社しました

はじめに

タイトルの通りGMOペパボに入社しました。
7月1日から働き始めてるので実際には約10日程たってます。
なんで転職したかとか周りの人にあまり説明+お知らせをしていないので
それ用の記事と現在の自分の考えをメモっておくように記事を書いてます。

転職しようと思った訳

前職もweb系のエンジニアとしてRuby書いていたわけですが、
今年(2015年)の初め頃から、
社内でRubyRailsを使用したサービス開発で学びや技術的な刺激を受ける機会が激減したと感じ始めた。
激減した理由としては当時1年半くらいRuby書いていて、
社内でRubyをメインで使用して開発しているエンジニアとしては経験年数が長くなり知識が増えたことが
原因だと思っている。

その為、技術的な刺激を求め社外のRuby系の勉強会などに参加したりしていたが
普段自分が何気なく書いているコードに関しても、もっと良い方法があるんじゃないか?
とか色々考えるようになり、転職をしようかと考えた。

ただ、すぐに転職活動を行わなかったのはまだ当時の会社でやりたいことがあったこと
担当していたサービスを任せられるエンジニアを育成すること
その他色々な理由で2015年後半に転職活動をしようと思っていたのです。

2015年後半…?

「え、おまえ後半入って速攻で転職してるやん」ってなると思うのですが
理由は色々あるのです。
タイミング的なあれだったり、ゴールデンウィークからの社会復帰に失敗したからとか
大人の事情とか、そういうものだと思ってください。
悪いことはしてません

なんでペパボ?

転職する一番の理由がRubyの凄いエンジニアのいる会社で働いて自分の技術力をもっと磨きたいなので
とりあえず頭に浮かんだ会社が2社ありその片方がペパボでした。

pepabo.com

とりあえず話聞きたいと思い上記の制度を利用しお話を聞きに行くことに
@kentaro,@hsbtのお二人を指名させていただきました。
現状抱えている課題や今後人が増えていく上で起こりえるであろう課題等、本当に細かいことまでお答えしていただき
当時はそれなりに人数の多い会社で働いていたのでRubyでの開発以外のそういう部分の課題の解決にも携われたら面白いだろうなと思い受けてみたら、ありがたいことに採用して頂いたという経緯です。
頭に浮かんだもう一社についてはお祈…

実際どうなの?

1,2週間働いただけなのでまだまだわからないこと多めですが、
3日目くらいからSlackで@udzuraさんと技術的な会話が出来たり、
技術的な刺激を受ける機会は多いなと感じている。
やらなければいけないことや技術的な課題も多々あるのでやり甲斐も凄く感じていて
楽しくなりそうな気配はしている。

さいごに

こんな経緯で今回GMOペパボで働くことになりました。
今後に関して当分はRubyでサービスの開発を行っていくとのがメインになるかと思いますが、
解決したい課題も多々あるので、
インフラや基盤の方でサービスを作るエンジニアを支えるという立場を経験するのも面白そうだなという思いがあります。

ペパボに興味がある方はペパランチョンで話を聞きに来るも良し下記から申し込むのも良しかと思います。

pepabo.com


このエントリーはもし誰かに怒られたら消します

PHPでmemcachedに保存されたセッション情報をRubyで扱う

経緯

PHPで作成されたシステムで発行されたsession idを元に
Rubyからそのセッションに格納されている情報を知りたいかもしれないという状況が
あるかもしれない。
無事社会復帰を果たした会社で話題になったので調べてみた。
前々職の際にPHPmemcachedに関して結構調査してたりしたので大分行けそうな気はしていた。
途中経過をメモ

環境

php.iniにて下記のように設定

session.save_handler = memcached
session.save_path = "localhost:11211"

memcachedlocalhostのport11211で動かしています。

PHPにてsessionに何かを保存する

今回は適当なデータを格納するスクリプトを作成
session.phpというファイル名で保存

#!/usr/bin/php
<?php
session_start();

echo $_SESSION['count'] = rand(1, 100);
$_SESSION['testkey'] = 'hoge';

echo session_id();
echo "\n"
?>

実行してsession idを確認してみる

$ php session.php
66
f1sk3iprn07l2vu6a5e09dqd86 # これがデータを格納したsession id

memcachedにどのようなデータが格納されているか確認する

memcached-toolっていうコマンドを使ってmemcachedをいじれたりする

$ memcached-tool localhost:11211 dump
Dumping memcache contents
  Number of buckets: 1
  Number of items  : 1
Dumping bucket 3 - 1 total items
add memc.sess.key.f1sk3iprn07l2vu6a5e09dqd86 0 1435838226 30
count|i:66;testkey|s:4:"hoge";

先ほどのphpスクリプトを実行して表示されたsession idの前に
「memc.sess.key.」が付いている。
ここをみるとPHPが勝手に付ける仕様のようだ。

セッションに格納していたデータは「count|i:66;testkey|s:4:"hoge";」このようにシリアライズ
されて保存されている。
phpの標準のserialize関数ではなくmemcachedの独自のシリアライズっぽい?

Rubyで取得してみる

memcachedのデータを扱うgemはそのままevan/memcached · GitHubを使ってみたのですが、ここでエラーが出たので断念。
phpで独自のシリアライズ保存されたデータだからそりゃそうなのだが…

ということでmperham/dalli · GitHubこちらを試してみた。

$ gem install dalli
$ irb
irb(main)> require 'dalli'
irb(main)> dalli_client = Dalli::Client.new('localhost:11211')
irb(main)> dalli_client.get "memc.sess.key.f1sk3iprn07l2vu6a5e09dqd86"
=> "count|i:66;testkey|s:4:\"hoge\";"

これでシリアライズされたデータが取得出来た。

まとめ

PHPで生成されたsession idを元にsessionに格納されたデータ(シリアライズされたデータ)を
取得することに成功した。

とりあえず今日はここまで
TODOは以下

しかし久しぶりにPHP書いたな