ましめも

技術系メモ

お手軽Akka Schedulerとハマりポイント

定期実行処理を簡単に行えるAkka Schedulerというものがある。Akkaはplay framework上から簡単に使うことができるので、バッチ処理等を行うのにとても便利。
Schedulerには次のようなメソッドが定義されている*1

  • def schedule(initialDelay: FiniteDuration, interval: FiniteDuration)(f: => Unit)
    • initialDelay後, intervalの間隔で関数fを実行する
  • def schedule(initialDelay: FiniteDuration, interval: FiniteDuration, receiver: ActorRef, message: Any)
    • initialDelay後, intervalの間隔で receiver(Actor)に message を送る

「ちょっと定期実行したいだけだし&Actor定義するのめんどくさいし前者を使おう」と思って使ったら1つ問題点に気づいた。

例外発生時、定期実行されなくなってしまう

この方法の問題点は、scheduleに渡した関数内で例外が発生すると定期実行されなくなってしまうということだ*2。この方法で確実に定期実行するなら決して例外が発生しないようにしないといけない。

    var i = 0
    Akka.system.scheduler.schedule(5 seconds, 1 seconds) {
      i = i + 1
      Logger.info(s"$i 回目 実行します")
      if (i > 5) {
        throw new IllegalArgumentException("ごめん")
      }
      Logger.info("実行に成功しました!")
    }

実行結果(6回で実行が止まってしまう)

[info] application - 1 回目 実行します
[info] application - 実行に成功しました!
[info] application - 2 回目 実行します
[info] application - 実行に成功しました!
[info] application - 3 回目 実行します
[info] application - 実行に成功しました!
[info] application - 4 回目 実行します
[info] application - 実行に成功しました!
[info] application - 5 回目 実行します
[info] application - 実行に成功しました!
[info] application - 6 回目 実行します
java.lang.IllegalArgumentException: ごめん
	at Global$$anonfun$autoCleanUpFileStorage$1.apply$mcV$sp(Global.scala:35)
...
※以降何も出力されない

Actorを使った場合は例外が発生しても止まらない

一方でActorを定義しActorに定期的にメッセージを送るように設定した場合は、処理中に例外が発生しても止まることはない*3

  var j = 0
  class BatchActor extends Actor {
    def receive: Actor.Receive = {
      case e: String =>
        j = j + 1
        Logger.info(s"$j 回目 実行します")
        if (j > 5) {
          throw new IllegalArgumentException("ごめん")
        }
        Logger.info("実行に成功しました!")
    }
  }
// 1秒毎にBatchActorにメッセージ"はい"を送る
Akka.system.scheduler.schedule(5 seconds, 1 seconds, Akka.system.actorOf(Props[BatchActor]), "はい")

実行結果(6回目以降もずっと呼ばれ続けている)

[info] application - 1 回目 実行します
[info] application - 実行に成功しました!
[info] application - 2 回目 実行します
[info] application - 実行に成功しました!
[info] application - 3 回目 実行します
[info] application - 実行に成功しました!
[info] application - 4 回目 実行します
[info] application - 実行に成功しました!
[info] application - 5 回目 実行します
[info] application - 実行に成功しました!
[info] application - 6 回目 実行します
[ERROR] [02/11/2014 20:26:07.746] [application-akka.actor.default-dispatcher-5] [akka://application/user/$a] ごめん
java.lang.IllegalArgumentException: ごめん
	at Global$BatchActor$$anonfun$receive$1.applyOrElse(Global.scala:37)
..

[info] application - 7 回目 実行します
[ERROR] [02/11/2014 20:26:08.741] [application-akka.actor.default-dispatcher-5] [akka://application/user/$a] ごめん
java.lang.IllegalArgumentException: ごめん
	at Global$BatchActor$$anonfun$receive$1.applyOrElse(Global.scala:37)
..
[info] application - 8 回目 実行します
[ERROR] [02/11/2014 20:26:09.741] [application-akka.actor.default-dispatcher-2] [akka://application/user/$a] ごめん
..

この仕様を知らずに前者の関数を渡すschedule関数を使っている&例外処理をちゃんとしてないと、「あれれー?気づいたら定期実行されなくなってるぞー」ということになるので注意。

*1:http://doc.akka.io/docs/akka/2.1.0/scala/scheduler.html

*2:https://github.com/akka/akka/blob/v2.2.0/akka-actor/src/main/scala/akka/actor/Scheduler.scala#L527 例外が発生した場合は次の実行のためのスケジューリングがされない

*3:https://github.com/akka/akka/blob/v2.2.0/akka-actor/src/main/scala/akka/actor/Scheduler.scala#L62 メッセージの送信自体で例外が発生した場合やActorが終了している場合は止まる

playのRequest#remoteAddressで取れるのはIPアドレスではない

ちゃんと細かくドキュメントを読まず http://www.playframework.com/documentation/2.1.0/HTTPServer の書くままに設定してると痛い目にあう

突然カンマ区切りのIPアドレスがやってくる

FugaLogging.write(id, "投稿しました", request.remoteAddress)

こんな感じでログを残していたのだが突然IPアドレスがカンマ区切りで記録されていることが…

2014-02-10 23:36:41,933 XXXXXX    A.B.C.D
2014-02-10 23:36:47,432 XXXXXX    A.B.C.D
2014-02-10 23:37:47,142 XXXXXX    A.B.C.D, E.F.G.H
2014-02-10 23:38:15,870 XXXXXX    A.B.C.D
2014-02-10 23:38:20,336 XXXXXX    A.B.C.D

うっ…

Request#remoteAddressはX-Forwarded-Forを返す

http://www.playframework.com/documentation/2.1.0/HTTPServerの下に記載されている通り次の場合はRequest#remoteAddressの返り値は接続元のIPアドレスではなくX-Forwarded-Forとなる。これは、nginxやApache等のリバースプロキシ存在下を考慮した仕様。

  • 127.0.0.1からのアクセス
  • application.conf等にtrustxforwarded=trueが書かれている

ドキュメントの通り次のようにnginxでproxy_set_headerを設定しているとX-Forwarded-Forには "接続元のIPアドレス" もしくは "接続元のIPアドレス, nginxにアクセスした時点で付加されていたX-Forwarded-For" という形になる。

  proxy_set_header   X-Real-IP $remote_addr;
  proxy_set_header   X-Scheme $scheme;
  proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;

アクセス元のIPアドレスだけを取る方法

今回のケースでは迷惑行為防止としてログに記録するのが目的なのでX-Forwarded-Forが全て記録されているのは好都合だったが、本当に「アクセス元のIPアドレス」がほしい場合があるかもしれない。

そういう場合は次のいずれかの方法をとれば良い。

  • Request#remoteAddress の結果を分解して一番右のIPアドレスを得る (おすすめ)
    • メリット: 確実
    • デメリット: ちょっとだけ面倒
    • (僕の頭でplayが勝手にX-Forwarded-Forの一番右のIPアドレスを取り出すのかと思い込んでいた…)
  • 上記のドキュメント通りのnginxの設定だった場合、X-Real-IPヘッダを取り出し使う
    • メリット: 簡単
    • デメリット: playには直接アクセスできないことが条件(X-Real-IPヘッダが信頼できる状況)
      • パフォーマンスの関係上、クライアントが直接playにリクエストを送りたい場合がある(一部の通信はnginxを通して, 一部の通信は直接playに…とか)と、X-Real-IPヘッダが信頼できるのかどうか判定できない。
  • proxy_set_header X-Forwarded-For $remote_addr としてしまう
    • メリット: 簡単
    • デメリット: X-Forwarded-Forの意味からズレてる


Request#remoteAddress が playへの直接アクセスのときとリバースプロキシ通したときとで意味が変わるのはややこしいなあ…

play frameworkでPUT, DELETE, PATCH等のリクエストを<form>から受け取る

RESTfulなパスにしたいとき、<form>タグでmethod=PUT, DELETE等を送れない問題*1にぶち当たることがある。
全部POSTで送ればいいのだが、どうしてもPUTやDELETEを使いたいRESTful脳な人のための解決方法。

解決方法

Railsはこの願いを叶えるために_methodというパラメータを渡した場合はそのメソッドとしてリクエストを解釈するという仕様がある。(formヘルパーは、勝手に<input type="hidden" name="_method" value="put">というhiddenパラメータをつけてくれる *2 )play frameworkでもこれとほぼ同様の手段で解決できるよう、@helper.formの代わりになるヘルパーと、HTTPリクエストの処理の仕方を変えるようにした。

app/views/helper/formExtended.scala.html

GET or POSTでない場合のみGETパラメータに"_method=METHOD"をつけるヘルパー。@helper.formの代わりに@helper.formExtendedとしてあげれば良い。

@(action: Call, args: (Symbol, String)*)(body: => Html)

@availableFormMethod(method: String) = @{
  method.toUpperCase match {
    case "GET" | "POST" | "" => true
    case _ => false
  }
}
@appendMethod(action: Call) = @{
  val (url, method) = (action.url, action.method)
  if(availableFormMethod(method)) {
    url
  } else {
    url + (if(url.contains('?')) "&" else "?") + "_method=" + method
  }
}

<form action="@appendMethod(action)" method="@if(availableFormMethod(action.method)) {@action.method} else {POST}" @toHtmlArgs(args.toMap)>
  @body
</form>

(大抵のケースだとCSRF対策でtokenを送っていると思うので、送るロジックをついでにここに書いちゃえばいい)

app/Global.scala

あとはGlobal.scalaでリクエストのメソッドを書き換えてあげれば完成

object Global extends GlobalSettings with Results {
....
  override def onRouteRequest(request: RequestHeader): Option[Handler] = {
    val requestRewrited = request.method.toUpperCase match {
      case "POST" =>
        request.copy(method = request.getQueryString("_method").getOrElse("POST"))
      case _ => request
    }
    super.onRouteRequest(requestRewrited)
  }
}


(追記) @formExtended というヘルパーを作るより、単純にCallを書き換えてくれるやつ作ったほうが良いかも

*1:HTML4だとGET, POSTしか定義されていない。2014/02/02時点のChrome, Firefoxもこれら以外のmethodで送れないように思える…。要検証。

*2:http://guides.rubyonrails.org/form_helpers.html

play framework 2.2でテスト用configを設定する

FakeApplicationで指定のconfigを読み込ませたい場合(例えばconf/test.conf)は次のようにすれば読み込むことができる。

  "テストケース" should {
    "ふがほげ" in {
      running(new FakeApplication with DevSettings {
        def devSettings: Map[String, String] = Map(
          "config.file" -> "conf/test.conf"
        )
      }) {
        // テストコード
      }
    }
  }

JSONをHTML/CSSで表現する part2

再チャレンジしていいかんじにコピペできる版を作った。

コピペ例

{
  "a": "fuga",
  "b": [
    1,
    2,
    true
  ]
}

実現方法

見えない文字列を挿入することによりインデントを実現…。結局泥臭い方法になっちゃったなあ。

JSONをHTML/CSSで表現する

突然JSONをHTML/CSSでいい感じに表現したくなったのでやってみた。

<div class="json">
    <div class="js-value js-object">
      <div class="element">
        <div class="js-key">key1</div>
        <div class="js-value js-string">value1</div>
      </div>
      <div class="element">
        <div class="js-key">key2</div>
        <div class="js-value js-object">
          <div class="element">
            <div class="js-key">key3</div>
            <div class="js-value js-number">12345</div>
          </div>
          <div class="element">
            <div class="js-key">key4</div>
            <div class="js-value js-array">
              <div class="element">
                <div class="js-value js-string">A</div>
              </div>
              <div class="element">
                <div class="js-value js-string">B</div>
              </div>
              <div class="element">
                <div class="js-value js-number">3</div>
              </div>
              <div class="element">
                <div class="js-value js-object">
                  <div class="element">
                    <div class="js-key">fuga</div>
                    <div class="js-value js-number">12345</div>
                  </div>
                  <div class="element">
                    <div class="js-key">hoge</div>
                    <div class="js-value js-number">12345</div>
                  </div>
                </div>
              </div>
              <div class="element">
                <div class="js-value js-string">4</div>
              </div>
            </div>
          </div>
          <div class="element">
            <div class="js-key">key6</div>
            <div class="js-value js-string">めっちゃ長い文字列ABCDEFGめっちゃ長い文字列ABCDEFGめっちゃ長い文字列ABCDEFGめっちゃ長い文字列ABCDEFG</div>
          </div>
        </div>
      </div>
    </div>
  </div>

表示結果

f:id:mashijp:20131017234559p:plain

解説

箇条書きで簡潔に

  • 1つの要素は<div class="element">で表現している
  • キーは<div class="js-key">, 値は<div class="js-value 型の名前">で表現。型の名前は以下の通り
    • js-string ... 文字列型
    • js-number ... 数値型
    • js-boolean ... 真偽型
    • js-array ... 配列
    • js-object ... オブジェクト
    • js-nullを忘れてた…(あとでやる)

ここがすごいの

CSSのafter, before要素を使ってるので「"」や「,」「{」「[」をつけなくてもいい

誰が得するの?

...

感想

作ったあとに気づいたけど[や{等をCSSで表示するようにしてるのでコピペしたときに[や{がつかない!!!!!!!!CSSのcontentってコピペできないのか…。
↓コピペ結果例

key1 value1 
key2 
key3 12345 
key4 
A 
B 
3 
fuga 12345 
hoge 12345
 
4
 
key6 めっちゃ長い文字列ABCDEFGめっちゃ長い文字列ABCDEFGめっちゃ長い文字列ABCDEFGめっちゃ長い文字列ABCDEFG

まじで糞なものを作ってしまった感…

UnicornでUnix domain socketを使う場合は絶対パスで指定しなければならない

listen 8080 # TCP
listen "tmp/unicorn.sock" # Unix Domain Socketのつもり

と指定するとそんなListenの方法ないよー、と怒られた

I, [2013-06-26T09:22:21.178723 #11457]  INFO -- : listening on addr=0.0.0.0:8080 fd=8
F, [2013-06-26T09:22:21.179525 #11457] FATAL -- : error adding listener addr=tmp/unicorn.sock
/u/apps/..../gems/unicorn-4.6.2/lib/unicorn/socket_helper.rb:149:in `bind_listen': Don't know how to bind: tmp/unicorn.sock (ArgumentError)

Unicornソースコード
https://github.com/schneems/unicorn/blob/master/lib/unicorn/socket_helper.rb

    def bind_listen(address = '0.0.0.0:8080', opt = {})
      return address unless String === address

      sock = if address[0] == ?/
        if File.exist?(address)
          if File.socket?(address)
            begin
              UNIXSocket.new(address).close

1文字目がスラッシュからはじまってるかどうかで判定してるんですね...そうですか...

つまり、絶対パスじゃないと無理