かとじゅんの技術日誌

技術の話をするところ

リトライハンドラを書いてみた(Scala版)

Scalaでもリトライハンドラ*1が欲しくなったので、このブログを参考に作ってみた。
Ruby の retry-handler が激しく便利そうなので Java で実装してみた - YoshioriのBlog

試行錯誤してたら、id:xuwei がforkしてくれていた!→ https://gist.github.com/2996927
参考になりました。
allCatchのほうがよさげだったので、結局以下のような感じになった。

例えばこんな使い方ができます。ConnectExceptionもしくはIOExceptionが発生した場合は3秒間隔で3回までリトライができます(第三引数には関数も渡せます)。リトライ上限回数をオーバーした場合はRetryExceptionがスローされます。retryが成功した場合はちゃんと戻り値を取得できる。これは便利です!

import RetryUtil._

try {
  val xml = retry(3, 3000, classOf[ConnectException], classOf[IOException] ) {
    val url = new URL("http://localhost/")
    println("start")
    val conn = url.openConnection
    val result = XML.load(conn.getInputStream)
    println("end")
    result
  }
} catch {
  case e: RetryException =>
    e.throwables.foreach(_.printStackTrace())
}

*1:Scalaの場合はこの名称が適切かどうかわからんが…。

WEB+DB PRESS Vol.67 で記事を書きました

WEB+DB PRESS Vol.67

WEB+DB PRESS Vol.67

今回もJavaのスレッドに関連する記事です。前回はメモリモデルに関連す話題でしたが、今回J2SE 5.0から使えるConcurrency Utilitiesについて解説してみました。店頭で見つけたら手にとって見てくださし。
そして次回の締め切りが、、迫り来る!

WEB+DB PRESS Vol.66 で記事を書きました

12月23日発売のWEB+DB PRESS Vol.66で連載4回目の記事を書きました。

WEB+DB PRESS Vol.66

WEB+DB PRESS Vol.66

タイトルは「再考するJava【第4回】並行処理におけるスレッドセーフの心得」です。
マルチスレッドは難しく広範囲な話題なので書くのが非常に難しいのですが、今回はスレッドセーフに絞って書いてみました。*1スレッドというとスレッドの起動や停止、スレッドプールとかに目がいきがちなのですが、それよりもスレッドセーフを実現するために何を気をつけるべきか、そういう根本的な概念に目を向けることも重要だと思いこの記事を書きました。
この記事を糸口にしていただき、並行処理に関する良書や文献などを読む機会が増えるとよいと思います。

ということで、よろしくお願いします。

*1:というかスレッドセーフといっても主にメモリモデルから視点で書いています。本当は契約プログラミングの視点を加えたかったのですが、紙幅が足りませんでした...あしからず

JIRAで謎い黒魔術!!!

http://atnd.org/events/22899
@に絡まれたので、しょうがなく我が家秘伝のJIRA黒魔術をざっくり公開します。。。

JIRAはワークフローを柔軟に扱えることが一つの魅力です。
たとえば、ストーリチケットを作成するときに、ストーリポイントは必須だよね。ストーリチケットだからね。でも、ストーリポイントを必須項目にするとストーリポイントが決まっていない状況では起票できないし、必須でなくしてしまうと未入力のままの可能性がある。
思いついたときに起票して、ストーリポイントも入力漏れを防ぐにはどうしたらよいか。

ワークフローをいじる

簡単です。チケットが開始状態になるまでは未入力だけど、開始するときには必須ってワークフローを書けばいいのです。
ワークフローを超ざっくり説明すると、、、ワークフローには、オープン、開始、処理中、解決、クローズとかの"状態"があります。それと各状態から状態に移動するための"遷移"と概念があります。この時点でだいぶ難しいw 絵にしてみた。こんな感じ。

その"遷移"を可能にする"条件"や遷移時に"検証"を行うことができます。"条件"はその名の通りその条件が有効でない時は、その先の状態に遷移できません。"検証"はその先の状態に遷移する際に呼び出され、検証に失敗した場合は警告表示と次の状態に遷移しません。

Script Runnerプラグインを使う

今回は、チケット作成直後の状態(オープン)から開始状態に遷移するStart Progressという"遷移"の"検証"に独自の条件を追加するのがよいです。
このような拡張を追加する際にはJIRAのプラグインを自作するというのもあるのですが、今回はもっと手軽にワークフローを制御できるScript Runnerを紹介します。

いきなりコードから説明。
ストーリチケットはIDが7番です。*1
ストーリポイントってカスタムフィールドなんでそれもカスタムフィールドの編集画面からパラメータを取得して、フィールドを取得します。フィールドのオブジェクトまでとれれば値の取得は簡単です。このルールの場合は、null以外と0以上を両方をチェックしています*2。"条件"も"検証"も戻り値としてはbooleanを返すだけでよいです。

import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.ComponentManager
import com.atlassian.jira.issue.fields.CustomField

CustomFieldManager customFieldManager = ComponentManager.getInstance().getCustomFieldManager()           
if (issue.getIssueTypeObject().getId() == "7"){
  CustomField customField = customFieldManager.getCustomFieldObject(10003)
  BigDecimal sp = customField.getValue(issue)
  return (sp != null && sp.floatValue() > 0)
}
true

また、ストーリチケット以外でかつサブタスクがないチケットは、開始する前に初期見積を必ず入れておきたい場合は以下のようなバリデータを書きます。意外と簡単に掛けてしまいます。これで初期見積も入力漏れのまま開始されることはないわけです。

if (issue.getIssueTypeObject().getId() != "7" && issue.getSubTaskObjects().size() == 0){
  Long orgEst = issue.getOriginalEstimate() 
  return (orgEst != null && orgEst.intValue() > 0)
}
true

JIRAでデバッグできる

どうやってデバッグするの?Eclipseとか用意するの?って疑問あると思うんですが、以下のような画面を辿っていくとConditionTesterという画面があるのでそこでチケットIDとGroovyスクリプトと入力してテストすることができます。



スクリプトをワークフローに追加する

テストが完了したスクリプトはワークフローの編集画面で"条件"もしくは"検証"に追加することができます。登録の際にエラーメッセージを登録できます。

動作確認してみる

試しにストーリポイントが未入力なチケットを開始状態にしてみる。
なんということでしょう。ストーリポイントが未入力または0の場合はバリデーションエラーが起きました。思った通りのバリデータを書けるではありませんか。これでいろいろ幸せでになります はぁと!

その他のワークフロー関係のおすすめ設定

インストールしたデフォルトだと次のような状態になっています。

  • 一つ目はサブタスクが解決になっていないのに親タスクを閉じれてしまう
  • 二つ目は課題リンクでブロックされるチケットがあるのに閉じれてしまう

最初のサブタスクの方は、In ProgressなどからResolvedに遷移するResolve Issueの条件に追加すれば、サブタスクが解決しないと親タスクも解決できないにようになります。これは簡単ですね。

二つ目のブロックされる課題リンクがあるはちょっと複雑です。
JIRA Workflow Toolboxを手動でインストール。atlassian-jira/WEB-INF/lib にこのプラグインのJARファイルをインストールするタイプのものです。
これもResolve Issueの条件に追加すればよいです。

条件はこんな感じ。ブロックする課題が解決もしくはクローズになっていないとチケットが解決にできないように設定します。

明日は@です!

*1:番号は課題タイプの編集画面のパラメータを見れば確認できます

*2:http://docs.atlassian.com/jira/ このあたりにJavadocがあるので見てみてください

パラレルコレクションの性能測定

Scala Advent Calendar jp 2011 6日目 いきます。

STMの話にしようと思ったのですが、いろいろまだ調査中なんでまた後日ということで、今回はパラレルコレクションでいきます。すでにあちこちのブログで扱っているネタなので目新しさはないですが...

パラレルコレクションは2.9から使える新機能です。
早速 使い方。通常のコレクションの要素を2倍する処理は次のように記述します。

List(1,2,3).map(_ * 2)

一方、パラレルコレクションではparメソッドを使います。

List(1,2,3).par.map(_ * 2)

scala.collection.immutable.List#parはParSeq[A]型の戻り値を返します。ParSeq#mapを呼び出すだけでmapを並行に処理できるわけです*1。本来並行処理を実装する場合は、スレッドの起動や待機、スレッドプールとタスクの管理など複雑な制御が伴いますが、パラレルコレクションの場合はparメソッドを呼ぶだけ並行処理を記述できます。
実際のテストプログラム。Benchのmainメソッドがエントリポイントです。引数に応じて通常のコレクションでの処理と、パラレルコレクションでの処理を呼び分けます。

package parallel
import scala.collection.immutable.NumericRange

// プログラム本体
object Bench {

  // nまでの階乗を計算するメソッド
  def fac(n: BigInt) =
    NumericRange(BigInt(1), n, BigInt(1)).
      foldLeft(BigInt(1)) { (cur, next) =>
        cur * next
      }

  def main(args: Array[String]): Unit = {
    import parallel.BenchUtil._
    args match {
      case Array("N") =>
        bench(50, "normal") {
          (1 to 2000).map { x => fac(x) }
        }
      case Array("P") =>
        bench(50, "parallel") {
          (1 to 2000).par.map { x => fac(x) }
        }
    }
  }

}

BenchUtil#benchメソッドは計測した結果をソートして、前後20%ずつ削除した値だけを利用して平均値や標準偏差、最大最小値を計算。

package parallel
import scala.compat.Platform

// ベンチマーク用ユーティリティクラス
object BenchUtil {

  private def avg(xs: List[BigDecimal]): BigDecimal =
    xs.sum / xs.size

  private def std(xs: List[BigDecimal]): BigDecimal = {
    val a = avg(xs)
    Math.sqrt((xs.foldLeft(BigDecimal(0))((s, c) => s + (c - a) * (c + a)) / xs.size).toDouble)
  }

  private def median(xs: List[BigDecimal]) = xs.toSet.toList.sortWith(_ < _) match {
    case n :: Nil => n
    case xs if xs.size % 2 != 0 => xs(xs.size / 2)
    case xs if xs.size % 2 == 0 => {
      val a = xs(xs.size / 2 - 1)
      val b = xs(xs.size / 2)
      (a + b) / 2
    }
    case _ => throw new RuntimeException
  }

  private def mode(xs: List[BigDecimal]): BigDecimal =
    xs.foldLeft(Map[BigDecimal, Int]().withDefaultValue(0)) { (map, key) => map + (key -> (map(key) + 1)) } maxBy (_._2) _1

  def bench(n: Int, msg:String)(f: => Unit) {
    val times = for (i <- List.range(1, n + 1, 1)) yield {
      Platform.collectGarbage
      val start = System.nanoTime
      f
      val stop = System.nanoTime
      BigDecimal(stop - start) / 1000 / 1000
    }
    val truncate = n / 5
    val result = times.sortWith(_ < _).view(truncate, n - truncate).toList
    if (result.size > 0) {
      println("%s, threadId = %d, n = %d, avg = %11.2f, std = %11.2f, median = %11.2f, mode = %11.2f, min = %11.2f, max = %11.2f".
        format(msg, Thread.currentThread.getId, result.size, avg(result), std(result), median(result), mode(result), result.min, result.max))
    }
  }
}

結果は次のとおり。私の環境ではパラレルコレクションを使った処理の方が3倍ぐらい高速になりました。もっとコアが多いマシンでテストしたいところだけど、個人ではこれが限界。

動作環境: MacBook Pro 15インチ Corei7 2GHz(物理コアは4つ。仮想コアとして8つ) 
単位はmsec
normal  , threadId = 1, n = 30, avg = 3177.07, std = 10.39, median = 3179.97, mode = 3184.39, min = 3155.66, max = 3191.19
parallel, threadId = 1, n = 30, avg = 1110.68, std = 27.62, median = 1113.52, mode = 1081.60, min = 1065.08, max = 1157.97

CPU負荷は具体的な数値はとってませんが、パラレルの方はちゃんと全部使っている感じ。
通常のコレクション

パラレルコレクション

使い込んでみないと具体的にどういうところで使えるかわかりませんが、動画のエンコードとか面白そうかなと思ったりしています。

あわせて読みたい
Scala 並列コレクション メモ(Hishidama's Scala parallel collections Memo)
scala2.9のparallel collection の benchmark をしてみた - scalaとか・・・
Scalaの並列コレクションで実際に並列化されているメソッドを調べてみた - chimerastのエレガント指向プログラミング日記

*1:当然mapに渡す関数に副作用がないことが前提