俺のためのタスク管理サービスを作った

みなさんさようなら,またひとつサービス作ったよ.

俺のためのタスク管理サービス.

Fascia

めずらしく痛くないというのが一番の特徴じゃないですかね.

動機

ToDoリストをどこかで管理したい

これに尽きる.

だいたい普段生活していると,メモできなかったり,PCが近くにない状況に限って「あ,あれやらなきゃじゃん」というのを思い出したり,思いついたりする. これをいかにして管理していこうかと悩んで,いろいろ使ってはみたものの,どれも定着しなかった.

iPhoneにアプリを入れて,出先でも登録できるようにしても,なぜか定着してない.

なんで定着しないんだろう?

github管理下のToDoとそれ以外のものが並列に出てくるから

俺の脳内では,「あ,Whalebirdにあの機能積んでおこう」という思いつきも,「あ,仕事であれやっといたほうがいい」という思いつきも,「キッチンペーパー買ってこないと」という思いつきも,すべてが並列に出てくる. とくに何かについて考えていない時に,こういうものが順不同に適当に出てくる.

これらのToDoリストを,例えばtrelloで管理するとどうなるか.

  • Whalebirdのタスク

    trelloにメモ -> githubにissue立てる -> PRが出てくる -> PRとissueがcloseされる -> trelloのタスクを自分でdoneにする

  • 仕事のタスク

    trelloにメモ -> redmineにissue立てる -> PRが出てくる -> PRとissueがcloseされる -> trelloのタスクを自分でdoneにする

  • キッチンペーパー

    trelloにメモ -> 買い物に行く -> trelloのタスクを自分でdoneにする

フロー違いすぎ,手順多すぎ,めんどくさすぎ,無理.

一度,全部github管理にしようかとも思ったんですよ. でもキッチンペーパーの買い物とかが入る,家用のリポジトリ作るの?

コード0行だよ?

というわけで自作することにした.

使い方

いや,特に説明するようなことはないです.

githubログインにしておくと,project作るときに,githubリポジトリ関連にするかどうかの選択ができるんですが,そのくらいじゃないですかね.

あとはtrelloと同じ感じで使えます.

リポジトリ関連にしておくと,ちょっと複雑なことができます.

  • github側に勝手に同期される
  • webhookが設定されるので,github側の変更も同期される

コンセプト

俺のためにって書いたとおり,自分で使いたいものを作ったので,俺が便利だと思うように動くようにしてあります.

そして,個人のToDoを管理したかったので,チームで使うような用途は想定していません. なので,プロジェクトを他人と共有したり,チームを作成したり,何かをパブリックにしたりする機能はないです.

そのため,githubのissue管理として運営される類似のサービス(waffle.ioやzenhub)のように,チーム開発やスクラム向けの機能は提供しません.

ただ,githubのissue管理機能については,もっと便利にしていきたいので,

  • issueやPull Requestのコメント関連機能

あたりは近いうちに実装するかもしれません.

開発

リポジトリはこちら.

github.com

サーバサイド

サーバは全部goで書きました. あまり機能的には多くないアプリですが,それなりに行数増えてしまった.

golangを書いていると,設計をいろいろ悩んだりするので,とてもよかった. 特にpackageという分離は,classみたいに手軽に使えないよねっていうのが,今の感想.

それと,エラーハンドリングについてはかなり悩まされました. できれば,本当に不要なパターンを除いて,ほとんどのメソッドerror 型の戻り値をつけておいた方が良い.

フロントエンド

ほとんどReact Reduxです. ログインとか,そのへんだけはjs使わなかったので,サーバサイドでhtmlレンダリングしている.

SPAと決めて,覚悟しちゃうとこれはかなり使いやすいなー. Reactのサンプルでは,よくToDoリストのサンプルが載せられていて,「これ簡単にいけるかなー」って思ったけど,全然その後が大変だった. サンプルはみんな載せてくれるけど,本当に大変なのはあの先である.

あと,こういう形でjsレンダリングしたかったのは,のちのちアプリを作ることを考えて,サーバを全部APIにしておきたかったというのもある.

俺のために

あとはiOSアプリにすれば,俺の欲望は満たされることになる. しかし先は長そうだなこれ…….

Shoryuken,それと例外検知gemを作った話

みなさんさようなら. Railsの非同期処理って何使ってますか?

Sidekiq?Resque?,え?DelayedJob?

個人的にはSidekiqが結構好きで,よく使っていたのですが,最近Redisのメモリが足らなくなってきて…… たまにエンキューに失敗します.

もちろんインスタンスタイプを上げればいいんですが,それなりの値段するじゃないですか.

Shoryukenを使ってみる

Shoryukenというのがあってですね,だいたいSidekiqと同じ要領で使える非同期処理用のgemなんですが,バックエンドがAWS SQSなんですよ. だから,これRedisの運用とか真面目に考えなくてもいけるんじゃね?とりあえず全部メッセージに突っ込んでも,重くならないし!

そんなこんなでShoryukenを使ってみました. というか,既に運用しているasumibotに入れて,運用してます.

github.com

Shoryukenの細かい動作についてはこのへんにまとめました.

qiita.com

使用感:

  • SQSを使うのでメモリのことを気にしなくても良く,エンキュー数が増えてきても安心
  • 料金的に$0.5/1M という非常に安い価格設定なので,Redisを増設するより安い
  • (そもそもキューってRedisに入れるほどの情報でもないし……
  • マルチスレッドで,だいたいSidekiqと同じ要領で使える
  • ローカルでのSQS再現にはFakeSQSを使えばいいので,開発環境でのAWSSQS課金についての心配は不要
  • 今のところ例外が起こってもどこにも通知されない
  • リトライの機構についてはSQSを頼ることになるので,若干不便
  • 速度的には,SQSのポーリグ間隔(によってSQSの料金が変わる)に依存するので,Sidekiqほど即時に処理してくれるわけではない

例外が通知されないのは困る

上記の中でも特に例外のところが困っていて,RailsのアプリだとExceptionNotification等で例外を検知していると思うのですが,そこで検知してくれない.

Sidekiqなんかだと,ExceptionNotification側に既に用意されていて,割と簡単に用意できたりします.

github.com

これがないのが困っていて,みんなどうしているのかなー?

ExceptionNotification::Shoryuken

と思っていても始まらないので,gemを作りました.

github.com

だいたい,ExceptionNotificationのSidekiqをパクった感じです.

普通にインストールしたら,config/initializers/exception_notification.rb あたりに,

require 'exception_notification/shoryuken'

ExceptionNotification.configure do |config|
  # setup some notification
end

とか書いておけば,勝手に検知して,その他の例外と同じように設定してある通知先(メールとかSlackとか)に通知してくれます.

Shoryukenってまだ直しどころがある

実はShoryuken本体の方にもPullRequestを投げたりしているのですが,Shoryukenってまだ未熟な感じがします. 使ってると足らないところを結構発見するので,直し甲斐があるというか,PullRequestが積まれるというか.

ソース的にはSidekiqを参考にしている色が強く出ていて,PullRequestやIssueの議論でもSidekiqの話がたまに例に出されます.

そういう話をして,Sidekiqのソースを見に行くと「完成度たけーなおい」と思うしかない.

Shoryukenはそれに比べると,機能的にも少ないし,完成度もまだまだな感じがします. それは盛り上がり方とか,作者のやる気にも寄るんでしょうけど…….

使う人が増えてくれれば嬉しいんですが,みんな本当に非同期処理って何使ってるんだろう?

SQSの機能が,Sidekiq+Redisほどブラックボックスとして扱えないので,SQSの知識が必要になってしまうという障壁はあるんですが. それでも,それなりの大きさのRedis運用をまともに考えるよりは,かなり手抜きができて使いやすいと思うんですけどね.

ReactComponentにおけるkeyは結構大事

ReactComponentでhtmlをレンダリングしていて,コンソールにwarningが出ていることがある. 無視しても大丈夫なものもあるのかもしれないが,エラーが出ることもあるので,気になって調べてみた.

こんな感じのエラー.

Warning: setState(...): Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `ListView`. See https://fb.me/react-warning-keys for more information.

mapしているところで発生している

いろいろと自分のコードを消したりしてみると,どうやらrender内でmapしている部分で発生している.具体的にはこんなコード.

{list.map(function(list, i) {
  return (
    <div className="list">
      <div ...></div>
    </div>
  )
}, this)}

この部分をごっそり消すと,見事warningは両方共でなくなる.

mapに問題があるのか? とか,forEachで表示したほうがいいのか? とか,やってみたものの特に変化はない.

keyを指定したらwarningが消えた

unique "key"と書いてあるのが気になって,keyについて調べてみた. そこで,試しにkeyを入れてみたら,見事にwarningが消えた.

{list.map(function(list, i) {
  return (
    <div key={i} className="list">
      <div ...></div>
    </div>
  )
}, this)}

keyの役割

具体的なkeyの役割についてはこちらの記事が詳しい.

qiita.com

つまりは,mapするようなレンダリングをやると,繰り返し内で描画されるオブジェクトの区別がつかなくなり,差分レンダリングができなくなってしまう. そのために,繰り返しで同じオブジェクトをいっぱい生成するにしても,React側で区別がつくkeyを設定して欲しいと.

意外にもかなり重要な役割を担っていたくせに,今まで全然知らなかった.

key大事.

でもなぜ,setStateのwarningが消えたのかは謎

keyを設定しろって言ってるwarningが消えるのは理解できるが,setStaterender内にあると言っていたwarningが,この修正だけでなぜ消えるのかは,不明です. 誰か知ってたら教えてください.

巨大レコードのページネーション用gemを作った

Railsにおいて,kaminariwill_paginateのようなページネーションライブラリは非常に強力だ. というか,あまりページネーションというものを意識せずとも使えてしまうので,便利だ.

ページネーション時のカウントクエリが重い

便利ではあるのだが,すべての場合において無敵なわけではない. 複雑なJOIN を繰り返すことによりインデックスが効かなくなった状態で,レコード数が大量にあるような場合.そんなときに,これらのページネーションを使っていると,重くなる場合がある.

ページネーションライブラリは,ページの下部にページネーションのビューを表示している. そして,ここにはラストページの番号が書かれている.

f:id:h3poteto:20151219205701p:plain

この数字は,ページネーションライブラリ内部でカウントクエリを発行して,条件に合致するレコード数をカウントしている.

複雑で重いクエリの場合,このカウントクエリが非常に重くなってしまう場合がある.

大量のレコードがある場合のページネーション

gemにした

このような場合のページネーションを上手いこと解決してくれるgemを作った.

github.com

方針

そもそもそのページ数,正確である必要は?

カウントクエリが重くなるような状態では,たいていの場合はレコード数が多い.例えば4000ページくらいあったとしよう.果たして1ページ目を訪れる人のどのくらいが,最終ページにいくだろうか? そして最終ページにいくとき,そのページ数が正確である必要性はあるのだろうか?

これは俺の個人的意見なのだが,ページ数もレコード件数も,そこまで正確である必要はないと思っている.だいたい有効桁数3桁くらいまで出ていれば,その先はなんであろうと変わらない. そもそも天下のgoogleだって,検索件数を正確には出していないじゃないですか.

f:id:h3poteto:20151219205638p:plain

どう実現するか

  1. 本当に一番最初に1ページ目を表示したときに,ページ数をCOUNT クエリ発行してカウントする
  2. その値を(クエリをKeyとして)Redisに保存しておく
  3. ラストページの値は,最初にカウントした値を元に,多めに見積もって適当にceil して出しておく
  4. アクセスされたとき,もしレコード件数が0件だったら,もう一度COUNT クエリを発行して正確なラストページの値を算出し,ラストのレコードを表示する
  5. そのとき,Redisに保存されている値と違う値になっていたら,Redisの値を更新する

使い方

READMEに書いてあることとほとんど同じですが.

Redisの設定

config/initalizers/redis.rb みたいなファイルを作ってください.

GuessPaging::RedisClient.setup do |config|
  config.redis_host = '127.0.0.1'
  config.redis_port = 6379
end

Controllers

class RecordsController < ApplicationController
  def search
    @guess = GuessPaging::Paginate.new(
      query: Record.where(category_id: params[:category_id].to_i),
      per_page: 10,
      essential: 3)
    @guess.guess(
      page_params: params[:page]
    )
  end
end

per_page はわかると思います.ちなみにデフォルトで10が設定されているので,省略可能なオプションです. essential は,ページ数を適当に丸めて表示するときの有効桁数です.この例で言うと,ラストが4561ページだったら,4570と表示されることになります.この値も,デフォルト値として3が設定されているので,省略可能なオプションになります.

Views

<% @guess.records.each do |r| %>
  <%= r.hogehoge %>
<% end %>
<%= paging(@guess) %>

元のActiveRecord オブジェクトはrecordsというインスタンス変数に格納されています.ここから取り出せば,今までと同じように使えます.

Assets

app/assets/stylesheets/application.css に以下の行を追加します.

//= require 'guess_paging'

これでこんな感じのビューデザインになります.

f:id:h3poteto:20151219205735p:plain

Helpers

  • ページネーションのビューを表示する

    <%= paging(@guess) %>

  • 現在のページ番号

    @guess.current_page

  • ラストページの番号(概数)

    @guess.max_page

  • レコード件数(概算)

    @guess.count

速くなるよ

あまり複雑なDBを即席で作れなかったので,適当に10万件くらいの軽いレコードなので,そこまで大きな差は出ていませんが.

  • kaminari f:id:h3poteto:20151219205819p:plain f:id:h3poteto:20151219205825p:plain

  • guess_paging f:id:h3poteto:20151219205833p:plain f:id:h3poteto:20151219205841p:plain

できていないこと

  • localeによるビューの変更 kaminari のようにlocaleの設定でページネーションビューの表示文字を変更することについては,対応していません.

  • デザイン変更 これは使う側で適当なcssを作ってもらえばもちろん上書きできます.デフォルトだと,//= require 'guess_paging' したものがそのまま適応されます.

思うところ

今回のgemはActiveRecord の拡張として作ってはいません.どうもページネーションの機能がActiveRecord に拡張されて入るというのは,違う気がしていてい,ページネーションはページネーションのオブジェクトを生成するように作りました. ただ,これにもちょっとだけ不便なところがあって,ActiveRecord の拡張として作ると,呼び出された時に初めてDBアクセスしてARのオブジェクトが生成されます. なので,コントローラでクエリを書いておくと,ビューで呼び出すときに初めてDBアクセスされますよね.そのため,kaminari のようなページネーションでは,ページネーション用のカウントクエリはビューで呼び出した際に初めてクエリが呼ばれます.

それに対して,guess_paging は,ARで書いたクエリを一度GuessPaging に渡して,そこでオブジェクト化しているので,guessメソッドを読んだ時点でARのオブジェクト生成が走り,必要なクエリが呼ばれます. なるほど,ARの拡張として作るというのにはそういう意味があるんですね.

もうひとつ考えたこととしては,おそらくこのgemってレコード件数が少ないページにはあんまり使い勝手が良くないんですよね. だって,勝手にページ数保持するじゃないですか,正確じゃないでしょ. これを10ページとかでやられたら,むしろ邪魔なわけですよ.

そうなると kaminariwill_paginate と同居するってことは十分に考えられて,その際にARの拡張にしているとメソッド名被りそうだな……と. であるならオブジェクトからして別の方が取り回しが楽な予感がしました.

golangのpanicは例外ではないのか?

goには例外がないと言われている.確かに,よく見かける,exception とかはないんだけど,あれ? panic があるじゃん?

では panic は例外ではないのか?

我々は、処理構造を制御するためのtry-catch-finally形式の例外処理機構によって、コードが入り組んでしまうと考えています。しかも、ファイルを開けないといった、ごく一般的なエラーをさも特別なエラーであるかのように扱わせる傾向があります。

Go言語では、異なるアプローチを取りました。Go言語では戻り値として複数の値が返せるので、一般的なエラーハンドリングの場合、戻り値といっしょにエラー情報を返すことができます。エラー型は標準化されており、Goの他の機能と相まってエラーハンドリングがすっきりしたものとなります。これは、他の言語と大きく異なる点です。

それとは別に、Go言語にはエラーシグナルの発行機構と、本当に例外的な状況から回復する機構があります。エラーの発生によって、関数のコールスタックの巻き戻りが開始する中でのみ、このエラー回復メカニズムは実行可能です。このメカニズムは、大きな障害をハンドリングするのにも充分な上に、処理構造に特別な制御を行う必要もありません。また上手に使えば、エラーハンドリングのコードが読みやすくもなります。

http://golang.jp/go_faq

つまり,try-catch-finally形式の例外ではない.

冒頭でも述べたように、Goは一般的な例外処理への使用を推奨しないものの、例外処理用の専用構文を備えている。一般的な例外にはあくまで戻り値を使う。

https://ja.wikipedia.org/wiki/Go_%28%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E%29#.E4.BE.8B.E5.A4.96.E5.87.A6.E7.90.86

なるほど.ということは,「例外が存在しない」わけではなくて「一般的なtry-catchの中で発生させているような使い勝手の良いexceptionがない」というべきなのか.

厳密に言えば,panic defer recoverpanic を拾えるわけだ.ただし,ブロックのようなものを作れないので,あくまで関数全体に対して,defer recover することにはなるのだが. ただ,その機構を いつものexception みたいに使うなよ っていう意味を込めての「例外がない」なのか.

とはいえ,実現したいことは理解できる.

一般的な,try-catch内で発生させる exception については,「それ本当に例外なのか?」というのと「読みにくい」に尽きる. そこに関しては納得できる.

ただ,例外を使わずこまめに error を返していくと,こういう意見にも納得できるわけで…….

if err != nil によるエラーハンドリングを繰り返し繰り返し書かなくてはならないことがあります。 Go Blogにはif err != nil { return err }のパターンはあまり出現しない(once per page or two)と書かれていますが、 プログラムのタイプによっては(例えばいろいろな外部リソースや外部ライブラリをつなぐようなコード)かなりの頻度で if err != nil を書かざるを得ないことがあるような気がします。

http://www.yunabe.jp/docs/golang_pitfall.html#section-4

さらに,この error を書くパターンだと,スタックトレースを見るために発生源の情報を error に追記する必要があり…….

これでエラーがどこで発生したのかが分かるようになります。ただこういうことしてると実は例外で良かったんじゃないかという気分にもなりますが。

http://www.yunabe.jp/docs/golang_pitfall.html#return-nil-err--error

なので,結局割といっぱいこのパターンを書かなきゃいけないとは思うんですよね.

ただ,最初の思惑どおり,やっぱり例外として既に別処理に回されたりしていることはないので,上から順番に処理されていくという意味で読みやすくはあります.