まっしろけっけ

めもてきなやーつ

STF を使ってブラウザ上から Android をいじってみた

STF is 何 ?

STF | Smartphone Test Farm

CyberAgent 社がオープンソースとして公開しているブラウザ上から Android を操作できるようになるやつ。
ブラウザ上から apk を install させたりもたしかできたはず
Android は様々な端末があり、
端末ごとに動作が違ったりで検証が大変 + 多数のプロダクトで Android アプリを開発していると端末の貸し借りが発生したりするので
端末の管理が大変という問題とか開発拠点が離れている場合などもあるのでそこら辺を解消できる。

なぜか使った記憶がありとても便利だった。(元社員だっただけです)

なぜやってみたか

@misyobun さんがやってたのを見て
そういえば弊社CTOのあんちぽさんに「導入しといて」って雑に振られたので
面白そうだし「わかりました」って言ったのを思い出したから
(2週間前くらいだっただろうか…)

さっそく導入

homebrew があると便利なので入れとくといいかと

  • rethinkdb
  • graphicsmagick
  • zeromq
  • protobuf
  • yasm
  • pkg-config

上記をまとめて install

brew install rethinkdb graphicsmagick zeromq protobuf yasm pkg-config

node.js を install

$ brew install node
$ node -v
v5.0.0

stf を install

$ npm install -g stf
...
# node-gyp の install で error が発生した…

調べると node-gyp は最新の node に対応してないとかなんとか
なので node v0.12.7 を入れた

node の uninstall は下記で行う

$ brew uninstall node

もう一度 stf を install

$ npm install -g stf
...
# pkg-config が見つけられないてきな error が発生した…

# PKG_CONFIG_PATH を指定してやる (path は各々の環境に合わせてください)
$ export PKG_CONFIG_PATH=/usr/local/Cellar/zeromq/4.1.3/lib/pkgconfig/
$ npm install -g stf

install 関連は終わり

stf を立ち上げる

まずは rethinkdb から

$ rethinkdb
Recursively removing directory /hoge/rethinkdb_data/tmp
Initializing directory /hoge/rethinkdb_data
Running rethinkdb 2.1.5-2 (CLANG 7.0.0 (clang-700.0.72))...
Running on Darwin 14.5.0 x86_64
Loading data from directory /Users/mac/rethinkdb_data
...
...

stf を立ち上げる
端末はUSBでつないでおく

$ stf local
# ログがいっぱいでる

http://localhost:7100/ にアクセスすると下記のような画面が表示される
f:id:shiro-16:20151103180449p:plain

画像で「停止する」の部分が「準備中」になっていていくら待っても準備が完了しない…

log を見てみるとなにか失敗している

INF/device:resources:service 44364 [hoge] Installing STFService
FTL/device 44364 [hoge] Setup had an error TimeoutError: operation timed out
    at afterTimeout (/usr/local/lib/node_modules/stf/node_modules/adbkit/node_modules/bluebird/js/main/timers.js:11:15)
    at timeoutTimeout (/usr/local/lib/node_modules/stf/node_modules/adbkit/node_modules/bluebird/js/main/timers.js:53:9)
    at Timer.listOnTimeout (timers.js:119:15)

STFService.apk の install が失敗してしまっている様子?
とりあえず手動で STFService.apk を install する
(adb コマンド is 何 って人はググってくれ!)

apk のソースは ここ にあるのでこれを build してもいいと思う

$ adb install /usr/local/lib/node_modules/stf/vendor/STFService/STFService.apk

これで stf を再起動してみたが同じエラーが出てたので USB を一度抜き挿ししたら端末を操作できるようになった。
結果は下記をみてください


最後に

結構簡単にできた。
RethinkDB触ったことないのでちょっといじってみるのは良さそう
実際にこれを会社で導入する場合 server 用意してそれに USB ハブを挿しまくるのだろう。
そこら辺はなんかいい感じに相談してみよう。

やはり便利だ。

Sidekiq の queue を眺める際に使用するコマンドを雑にまとめる

はじめに

前回書いたこの記事を書く際に実際に積まれている queue をあれこれ見ていたのですが、
その際に使ったコマンドを忘れそうだったのでまとめておく。
めんどくさくてまとめなかったわけでは…

shiro-16.hatenablog.com

redis-cli

まずは基本的なこと redis-cli を使って redis のデータを見ることが出来る

Redis server に接続して対話的に実行
$ redis-cli
redis 127.0.0.1:6379> KEYS *
引数を指定して実行
$ redis-cli KEYS '*'

perform_async

※ perform_async で user 3 つ default 2 つ queue を積んだ状態

どんな key が生成されるか
$ $ redis-cli KEYS '*'
1) "queue:default"
2) "queues"
3) "queue:user"
key の member を全て取得
# SMEMBERS key
$ redis-cli SMEMBERS 'queues'
1) "user"
2) "default"
queue の件数を取得
$ redis-cli LLEN "queue:user"
(integer) 3
queue を 1 件取得
# LINDEX index
$ redis-cli LINDEX "queue:user" 0
"{\"class\":\"UserWorker\",\"args\":[1],\"retry\":true,\"queue\":\"user\",\"jid\":\"adddb1f368e7a622225ef315\",\"created_at\":1445260869.8274186,\"enqueued_at\":1445260869.8274736}"
queue を複数件取得
# LRANGE start end
$ redis-cli LRANGE "queue:user" 1 2
1) "{\"class\":\"UserWorker\",\"args\":[1],\"retry\":true,\"queue\":\"user\",\"jid\":\"b009bbcaa2b0dca70201b448\",\"created_at\":1445260841.7604742,\"enqueued_at\":1445260841.760538}"
2) "{\"class\":\"UserWorker\",\"args\":[1],\"retry\":true,\"queue\":\"user\",\"jid\":\"c9d40578849234b9f8b8d429\",\"created_at\":1445260828.8738196,\"enqueued_at\":1445260828.8746564}"

perform_in

※ perform_in で 3 つ queue を積んだ状態

どんな key が生成されるか
$ redis-cli KEYS '*'
1) "schedule"
schedule の member を見る

start と end を指定する

# ZRANGE key start end
$ redis-cli ZRANGE schedule 0 0
1) "{\"class\":\"UserWorker\",\"args\":[1],\"retry\":true,\"queue\":\"default\",\"jid\":\"2b6f8042a496b71128881c12\",\"created_at\":1445259670.319731}"

全て取得する

$ redis-cli ZRANGEBYSCORE schedule -inf +inf
1) "{\"class\":\"UserWorker\",\"args\":[1],\"retry\":true,\"queue\":\"default\",\"jid\":\"2b6f8042a496b71128881c12\",\"created_at\":1445259670.319731}"
2) "{\"class\":\"UserWorker\",\"args\":[1],\"retry\":true,\"queue\":\"default\",\"jid\":\"edc66e38f51e4b405ef2b984\",\"created_at\":1445260137.1156876}"
3) "{\"class\":\"UserWorker\",\"args\":[1],\"retry\":true,\"queue\":\"default\",\"jid\":\"8eb4b0bf44dee20a4c797424\",\"created_at\":1445260138.2186956}"

削除する

# ZREM key member
$ redis-cli ZREM schedule "{\"class\":\"UserWorker\",\"args\":[1],\"retry\":true,\"queue\":\"default\",\"jid\":\"2b6f8042a496b71128881c12\",\"created_at\":1445259670.319731}"

全部消したい!

普通はやらないけど全部のデータを消したい時に使う

$ redis-cli FLUSHALL
OK

最後に

ここら辺が使えれば Sidekiq の Redis 周りのことを調べるには困らないはず!

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!