hashtagleメモ

先日hashtagle という、Twitterハッシュタグを検索するサービスを公開したという記事を書いた。そこでは書かなかった、公開するまでに起こったこと、気付いたことに関して書き留めておく。

構成

  • Heroku の上に Sinatra で構築。
  • ハッシュタグのソースは Twitter のStreaming API の statuses/sample メソッドのみ。
  • ストレージは持たずオンメモリで動作させる。

日本語のツイートの取得

Perl には Lingua::LanguageGuesser という言語判定器なるモジュールがあるけれど、Ruby にはそのようなモジュールは見当たらなかった。
APIのレスポンスを見ると lang という項目があり、ツイートをしたユーザの言語設定が設定されている。日本語であれば ja となっているのでそれを利用した。言語設定が日本語というだけでは確実にツイートの中身が日本語とは判断できないが、そこは許容。

LRU

ハッシュタグのデータはすべて Hash で保持している。メモリは無限ではないので、ある一定以上の件数を超えないようにしている。新しいハッシュを登録しようとしたときに、Hash の件数が最大値だったら、当然どれか一つを捨てなければいけない。捨てる基準は 登録/更新された日時が古いハッシュタグ。いわゆる LRU ?
そのLRUの実装のイマイチ感が強い。その変遷を振り返ってみる。

初代

登録/更新日時の昇順にソートして先頭にきたハッシュタグを捨てる。

# ここにハッシュタグを溜め込む
hashtag = {}
# 一杯になったら一つ削除
delete_key = hashtag.to_a.sort_by {|h| ... }.shift[0]
hashtag.delete(delete_key)
二代目

ソートさせる必要はまったくないので、最小値だけをとるようにした。

# 本番 で "minvalue = v unless min_value" って書きたくないのでとりあえずの値を入れておく
delete_key = nil
min_value  = nil
hashtag.each do |k, v|
  delete_key = k
  min_value  = v
  break
end
# 本番
hashtag.each do |k, v|
  if min_value > v
    min_value = v
    delete_key = k
  end
end
hashtag.delete(delete_key)
三代目 (いまここ)

新しい Hash を追加するごとに毎回ハッシュを全スキャンするのはいやなので、LRU用のデータを持ってこまめにそれを更新してくことにした。

# パクパク
class RingBuffer < Array
  def initialize(size)
    @max = size
    super(0)
  end

  def push(object)
    out = size == @max ? shift : nil
    super
    out
  end
end
# lru 管理バッファ
lru_list = RingBuffer.new(ハッシュタグ保持数)
# ハッシュタグ更新時
lru_list.delete(key)
lru_list.push(key)
#ハッシュタグ登録時
delete_key = lru_list.push(key)
hashtag.delete(delete_key)

あ〜、コレ書いていて気付いたけど、Array.delete って配列を全スキャンするんだよな。同じ値は 1 つしかないってことはわかってるんで、一つ削除したらそこで終わるようにした方がいいかも。更新頻度が高い key は配列の終わりの方に固まるので、前から検索するよりも、後ろから検索した方がよさそう。

再接続

レスポンスのステータスが 200 より大きい値だったばあい、しばらく間を置いてから再接続処理をするようにとドキュメントには書いてある。間を置くために単純に sleep すると、他のイベントがブロックされてしまった。色々試してみたところ、EventMachine::Timer を使えばうまくいった。

EventMachine::Timer.new(秒) do
  再接続処理
end

再接続後のAPIレスポンス

Streaming API はふとした拍子に接続が切れることがある。そうなった場合再接続をする。と、ここまではいい。問題はそれから。
再接続後、API から返ってくるデータが再接続前と異なる。再接続前は純粋に JSON + \r\nが返ってくる。再接続後はというと、JSON のサイズ + \r\n + JSON + \r\n + \r\n が返ってくる。
\r\n ごとに分割した文字列を JSON のパーサーに渡しているので、JSON のサイズ(数字文字列)を渡したら当然エラーが発生。その対応として、例外をキャッチするようにした。ただ、再接続後のデータはすべてそうなるので、一度再接続すると例外の発生回数がすごいことに。
なので、JSONのパース時にパースする文字列の妥当性を確認する処理を追加。確認内容は文字列の先頭が '{'、末尾が '}' になっているかどうか。

gems

Heroku で準備されている gem のパッケージ以外で使いたいパッケージがある場合は、プロジェクトのルートディレクトリ(?)の下に .gems というフィアルを作り、その中に使用するパッケージを書くことで使えるようになる。

em-http-request --version '>= 0.2.7'
twitter --version '>= 0.9.4'
sequel

.gems を更新すると.gems に書かれているパッケージすべてのインストールが実行されるので、こまめに更新してたりすると面倒くさい。まあ、パッケージはそうそう更新するものでもないけど。

PostgreSQL

Heroku で使える DB は PostgreSQL。ローカルの開発環境では SQLite を使っていた。Heroku には DB をインポートする機能があり、そこでは SQLite 形式もインポートできるということで、お手軽な SQLite を使っていた。
でも、インポートする先は PostgreSQL。インポート後に動かしてみたら、「type "blob" does not exist」や「type "datetime" does not exist」なんてエラーが発生。PostgreSQL じゃない DB で開発するときは型に注意が必要。
開発環境と本番環境で違う RDBMS を使う方がどうかしてるとわれれば同意せざるをえないけど。