Hatena::ブログ(Diary)

じゅんいち☆かとうの技術日誌 このページをアンテナに追加 RSSフィード


はてなブログに引っ越しました。

2011-12-08

[] JIRAで謎い黒魔術!!!

http://atnd.org/events/22899

[twitter:@yusukey]に絡まれたので、しょうがなく我が家秘伝のJIRA黒魔術をざっくり公開します。。。

JIRAはワークフローを柔軟に扱えることが一つの魅力です。

たとえば、ストーリチケットを作成するときに、ストーリポイントは必須だよね。ストーリチケットだからね。でも、ストーリポイントを必須項目にするとストーリポイントが決まっていない状況では起票できないし、必須でなくしてしまうと未入力のままの可能性がある。

思いついたときに起票して、ストーリポイントも入力漏れを防ぐにはどうしたらよいか。

ワークフローをいじる

簡単です。チケットが開始状態になるまでは未入力だけど、開始するときには必須ってワークフローを書けばいいのです。

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

f:id:j5ik2o:20111208191312p:image

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

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スクリプトと入力してテストすることができます。

f:id:j5ik2o:20111208054619p:image:w360

f:id:j5ik2o:20111208054618p:image:w360

f:id:j5ik2o:20111208054617p:image:w360

f:id:j5ik2o:20111208055356p:image:w360

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

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

f:id:j5ik2o:20111208054859p:image:w360

動作確認してみる

試しにストーリポイントが未入力なチケットを開始状態にしてみる。

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

f:id:j5ik2o:20111208083855p:image:w360

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

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

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

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

f:id:j5ik2o:20111208062036p:image:w360

二つ目のブロックされる課題リンクがあるはちょっと複雑です。

JIRA Workflow Toolboxを手動でインストール。atlassian-jira/WEB-INF/lib にこのプラグインJARファイルをインストールするタイプのものです。

これもResolve Issueの条件に追加すればよいです。

f:id:j5ik2o:20111208083417p:image:w360

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

f:id:j5ik2o:20111208083158p:image:w360

f:id:j5ik2o:20111208061333p:image:w360

明日は[twitter:@tsuyoshikawa]です!

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

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

2011-12-06

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

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負荷は具体的な数値はとってませんが、パラレルの方はちゃんと全部使っている感じ。

通常のコレクション

f:id:j5ik2o:20111206085518p:image

パラレルコレクション

f:id:j5ik2o:20111206085516p:image

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

あわせて読みたい

Scala 並列コレクション メモ(Hishidama's Scala parallel collections Memo)

scala2.9のparallel collection の benchmark をしてみた - scalaとか・・・

Scalaの並列コレクションで実際に並列化されているメソッドを調べてみた - chimerastのエレガント指向プログラミング日記

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

2011-02-13

[][] タスクを並行で実行するために必要なこと 〜基礎編〜

スレッドセーフの話も語ればキリがないのですが、そろそろ「タスクを並行に実行する」話題にいってみましょう。この手の記事は結構あるし、書籍の内容をまるまるというわけにいかないので、独断と偏見でポイントを絞って軽く解説する感じで書いてみます。とりあえず、ThreadクラスとExecutorクラスあたりから。

この書籍のP129あたりです。

並行処理のアプリケーションには、「良いスループット」と「良い応答性」の両方が必要と書かれています。スループットは、単位時間あたりの処理能力のことです。処理の効率みたいなものといえると思います。応答性については、処理待ちがなく次のタスクを待ち受けることができるのが応答性がよいといえるでしょう。

さて、P130には以下のようなウェブサーバのサンプルが掲載されていますが(というか、Socketとか出てきて面食らっている人も多いかもしれませんが)、簡単に説明するとnew ServerSocket(80)でローカルホスト上で80ポートで通信(TCP)を待受れるようになります。socket.accept()ではクライアントが接続するまでブロックします。接続があるとリターンしクライアントを表現するSocketオブジェクトであるconnectionが取得できます。具体的なコードは示されていませんが、handleRequestメソッドはそのconncetionを使ってクライントを通信を行います。

この処理は逐次処理なので、acceptメソッドでブロック中はhandleRequestメソッドは処理できませんし、その逆もそうです。つまり、複数のクライアントを同時に処理することができません。

class SingleThreadWebServer {

	public static void main(String[] args) throws IOException {
		ServerSocket socket = new ServerSocket(80);
		while( true ) {
			Socket connection = socket.accept();
			handleRequest(connection);
		}
	}

	private static void handleRequest(Socket connection) {
		// ...
	}
}

並行処理の基本はThreadクラス

続いて、P131で紹介されているThreadクラスを使ったサンプル。Runnableのrunメソッドが並行に実行されるので、複数のクライアントを処理することができます。

class ThreadPerTaskWebServer {

	public static void main(String[] args) throws IOException {
		ServerSocket socket = new ServerSocket(80);
		while( true ) {
			final Socket connection = socket.accept();
			Runnable task = new Runnable() {
				public void run() {
					handleRequest(connection);
				}
			}
			new Thread(task).start();
		}
	}
	
	private static void handleRequest(Socket connection) {
		// ...
	}
}

利点としては、mainメソッドのメインループがタスク(クライアントとの通信)を処理しないので、次の接続のための待ち受けが可能になります。その分、応答性がよくなります。また、タスクを並行に実行できるので、同時に複数のクライアントとの通信が可能になります。

欠点としては、スレッドを作成したり破棄したりすることはCPUやメモリを使うためコストがかかります。また、アクティブなスレッドはメモリも消費します。すでにタスクを処理しているスレッドCPUがビジー状態であれば、新たなスレッドを作成しても、オーバーヘッドが増えて返って処理能力が低下します。スレッドは無限には生成できず、上限が決まっています。その上限に至った場合はOutOfMemoryErorrが発生します。

ということで、たくさん作ればいいという問題ではありません。実行環境に最適なスレッド数というものがあります。

スレッドを再利用できるExecutorフレームワーク

実行効率を考慮した上で、実行環境に最適なスレッド数を実現するには、「スレッドの再利用」が不可欠です。java.util.concurrentパッケージにはExecutorフレームワークがあり、それを可能にします。

それでは、つべこべ言わずに、Executor版のソースコードをみてみましょう。「Threadクラスは、すっこんでろ!」ってことで、主役はExecutorさんです。

class TaskExecutionWebServer {
	private static final int NTHREADS = 100;
	private static final Executor exec = Executors.newFixedThreadPool(NTHREADS);

	public static void main(String[] args) throws IOException {
		ServerSocket socket = newServerSocket(80);
		while (true) {
			final Socket connection = socket.accept();
			Runnable task = new Runnable() {
				public void run() {
					handleRequest(connection);
				}
			};
			exec.execute(task); // タスクを実行
		}
	}

	private static void handleRequest(Socket connection) {
		// ...
	}
}

最初に目が行くのは、

	private static final Executor exec = Executors.newFixedThreadPool(NTHREADS);

だと思います。

ここでは、ExecutorsのnewFixedThreadPoolというファクトリメソッドでExecutorを生成しています。

Executorオブジェクトであるexecは、NTHREADS個(100個)のスレッドを持つスレッドプールを持つオブジェクトです。100個のスレッドがすべて処理中である場合は、追加されたタスクスレッドが使用可能になるまでキュー上で待機します。何かリクエストがある毎にスレッドを作成しないため実行効率もよく、NTHREADS値がCPUの処理能力に対して適切であれば、前述の問題も起きません。

詳しくは以下のJavadocですが、ちょっと訳がかなり厳しい感じのようですw

public static ExecutorService newFixedThreadPool(int nThreads)

共有アンバウンド形式のキューなしで動作する、固定数のスレッドを再利用するスレッドプールを作成します。任意のポイントで、最大 nThreads のスレッドがアクティブな処理タスクになります。すべてのスレッドがアクティブな場合に、追加のタスクが送信されると、それらのタスクスレッドが使用可能になるまでキューで待機します。実行中に発生した障害のために、いずれかのスレッドがシャットダウン前に終了した場合は、必要に応じて新規スレッドが引き継いで後続のタスクを実行します。明示的なシャットダウンが行われるまでは、スレッドはプール内に存在します。

Executors (Java Platform SE 6)

この書籍の訳者の岩谷さんのブログの、Javadocの日本語訳の方がわかりやすいです。

java.util.concurrent.Executors

共有アンバウンド形式のキューなしで動作する、

は、

サイズ制限のない共有キューを使って仕事をする

ということらしい。。これでは全く意味が違うのではw 正確さを求める方は、やはり原文を参照ということで。

次にここ。RunnableのインスタンスをExecutorクラスのexecuteメソッドに渡しています。Javadocではこのように説明があります。

将来のどの時点かで、指定されたコマンドを実行します。

ということなので、タスクが実行されるかどうかはExecutorの実装に依存します。今回はタスクの実行(execute)というよりは送信とか登録に近いです。実行は前述したようにスレッドプールの状況によって決定されます。

exec.execute(task); // タスクを実行(今から将来のどの時点かで実行)

Executors.newFixedThreadPoolメソッドは、ExecutorではなくExecutorServiceを返します。ExecutorはExecutorServiceのスーパーインターフェイスなので代入可能なのですが、タスクを中断させたり、タスクの完了したかどうかを調べることができません。ExecutorServiceの詳しい使い方は別のエントリで紹介しますが、殆どの場合でExecutorのexecuteメソッドではなく、ExecutorServiceのsubmitメソッドを使って「タスクを送信する」ことになると思います。

ExecutorService (Java Platform SE 6)

他のファクトリメソッドもある

Executorsクラスには、他にも使えるファクトリメソッドがあります。これらのファクトリメソッドで、Executor(ExecutorService)の戦略をストラテジパターンのように変更することができます。

newSingleThreadExecutorメソッド名前の通り、シングルスレッドで実行されるExecutorを生成します。executeを複数回実行してもタスクは逐次的に実行されます。
newCachedThreadPoolメソッド複数のスレッドで並行処理しますが、終了しているスレッドがあれば再利用されます。60秒使用されないスレッドは破棄されます。短時間で大量に発生するタスクを処理するのに向いています。
newScheduledThreadPoolメソッドタスクの遅延実行と周期実行のスケジューリングをサポートする固定サイズのスレッドプールを生成します。
newSingleThreadScheduledExecutorメソッドこちらもスケジューリングをサポートしますが、スレッドプールではなく1つのスレッドで実行するExecutorが生成されます。

このようなスレッドの再利用機構を利用すれば、タスクの発生毎にスレッドを作成/破棄するコストを抑え、CPUの処理能力を超えるタスクを実行することもなくなります。CPUの処理能力よりタスクが少ない場合はThreadクラスはお手軽でよいのですが、CPUの処理能力よりタスクが多い、もしくは多くなる可能性がある場合は、スレッドプールを利用することをお勧めします。

次はExecutorServiceを使って、ScalaとかErlangのようなアクターを簡易的に実装してみますか。

あわせて読みたい

shameful hiwa(Java並行処理プログラミングの訳者の方のブログ)

マルチスレッドプログラミング-TECHSCORE-

java.util.concurrentパッケージを用いたマルチスレッドプログラミングについて

虎の穴 J2SE 5.0 入門 Concurrency Utilities No.1

2010-12-29

[][] コードで学ぶドメイン駆動設計入門 〜リポジトリ編〜

コードで学ぶドメイン駆動設計入門 〜エンティティとバリューオブジェクト編〜 - じゅんいち☆かとうの技術日誌

コードで学ぶドメイン駆動設計入門 〜振る舞いとサービス編〜 - じゅんいち☆かとうの技術日誌

コードで学ぶドメイン駆動設計入門 〜ファクトリ編〜 - じゅんいち☆かとうの技術日誌

引き続き連投エントリ。私も来年で39歳になります。そして息子が7歳。いいおやじですが、脳は衰えないと言われています。鍛えれば鍛えたほど進化できると信じます。

ということで、リポジトリ編に入ります。

リポジトリ

リポジトリは、ライフサイクルの途中から最後にフォーカスし、オブジェクトの永続化と永続化されたそのオブジェクトを検索する手段を提供するオブジェクトです。このように説明すると、DAOに近い印象を持つかもしれませんが、DAOはRDBMSやSQLなどのインフラストラクチャ層の関心事を含んでいるので、ここでは一旦忘れます。

ファクトリで生成されたエンティティやバリューオブジェクトは、DBやファイルなどに一時的に保管されることが多いと思います。そのような時にリポジトリを使います。

たとえば、アドレス帳などに記載する住所をAddressのエンティティとして考えた場合に、そのAddressの永続化を担当するAddressRepositoryがあるものとします。そのAddressオブジェクトを一旦ファイルに保存するようなコードは以下です。

ちょっと蛇足ですが、ここで、「あれ、バリューオブジェクトに対応するリポジトリはないのか?」と思ったかもしれませんが、エンティティがバリューオブジェクトの参照を保持するので不要なのです。そもそも、バリューオブジェクトは識別子を持たないため、リポジトリに格納したところで、バリューオブジェクトを識別=見分けることができないのです。属性が完全に一致するかで検索すればいいのでは?と思った場合は要注意です。先のエントリでも述べたように属性は流転していくので、たとえ属性で完全に一致してもそれが本当に目的のオブジェクトかどうか知る術がないのです。この原則はリポジトリだけではなく、コレクションにバリューオブジェクトを格納した場合も検索できないというデメリットはあるので注意したところです。

話を元に戻すと、アドレスをファイルに保存した後はVMからGCされても、ファイルの中にエンティティは存在しています。メモリからファイルに移動した形です。これはドメインモデルとしての、物理的な構造は変化しただけで論理的な構造は変化していません。このようにして、エンティティは、VMのライフサイクルを越える独自のライフサイクルを扱っているわけです。

以下がアドレスをリポジトリに保存しているコードです。

Address address = addressFactory.newAddress(1L, "〇〇会社","東京都中央区銀座1-1-1");
addressRepositoryInFile.store(address); // エンティティを永続化する。すでにファイル上にaddressが存在すれば更新、なければ新規に追加します。
// この後、addressがGCされてもファイル上にaddressは存在する

また、GCされた後にもう一度保存したアドレス(address)を検索したい場合は、以下のようにエンティティの識別子で呼び戻せばよいのです。

Address address = addressRepositoryInFile.findByIdentity(1L);
// この後はVM上にGCされるまで存在する。

リポジトリを使ってエンティティの参照を取得した後GCされても、リポジトリ上にエンティティが存在しているならば、まだライフサイクルは終了していません。逆に、エンティティのライフサイクルが終了する、つまり寿命を全うするとは、どういう時でしょうか。

addressRepositoryInFile.delete(address);
address = null;

リポジトリから削除され、VM上からもGCされた時です。これでエンティティのライフサイクルは終了です。

しかし、エンティティの識別子については、注意が必要です。上記の例の、識別子1Lのエンティティが寿命を全うしたので、同じ1Lを使って違うエンティティを作ることは可能でしょうか。可能なのですが、それは本当にしてもよいことでしょうか?

先のエントリでも言及した通り、「夏目漱石は亡くなっても、夏目漱石というアイデンティティ」です。1Lは永久欠番にしないと身元(アイデンティティ)が保証できません。永久欠番にしないで再利用してしまうと、時代と共に夏目漱石というアイデンティティは異なるものが存在する(時間軸に対して同一の識別子で違うエンティティの実体が存在した)ということになってしまうのです。

このような事態を回避するならば、識別子の空間を十分に広い空間*1を使い、再利用を禁止するのが一番有効な手立てです。*2

実際の業務システムではDXOのコストが問題に

実際の業務システムでは、RDBMSを扱うリポジトリを実装して同様のことを実現すればよいでしょう。その場合は以下のエントリのようにリポジトリ内部からインフラストラクチャ層のDAOで、ドメイン層のエンティティ、バリューオブジェクトと、インフラストラクチャ層におけるRDBMSのテーブルを表現したエンティティの相互変換がどうしても発生します。

ウェブアプリケーションの構造について - じゅんいち☆かとうの技術日誌

I/Oの流れは、このようなイメージになります。異なるドメイン同士のデータを変換するオブジェクトをDXO(Domain eXchange Object)と呼ばれます。

[Domain Entity, VO] <--DXO--> [Domain Repository] <--DXO--> [RDBMS Dao, Entity]

このような手間の問題があるため、DDDで一番大変なのはリポジトリを実装することではないかと最近思っています。面倒なところなのですが、ここをきちんとやっておかないと、レイヤーが曖昧になりカオスへの道にどんどん入っていく原因を作ってしまいます。今後の改善策としては、DSL(Scalaなども視野に含めた視点でのDSL)で解決したり、コード生成でカバーしたりできるのではないかと考えています。*3

schema-generatorのリポジトリ

それでは、schema-generatorのリポジトリを紹介します。

DataSourceRepositoryInProperties(DataSourceのためのリポジトリ)では、設定ファイル上にあるデータソースを識別子を使って読み込み、DataSourceエンティティで返す役割を持っています。ちょっと長いですがw

/**
 * {@link DataSource}のためのリポジトリの実装クラス。
 * <p>
 * エンティティである{@link DataSource}の永続化を担うリポジトリ。
 * この実装では、プロパティファイルから{@link DataSource}を読み込みます。
 * </p>
 */
public class DataSourceRepositoryInProperties extends
		AbstractRepositoryInProperties implements DataSourceRepository {
	
	private final String fileName;
	
	/**
	 * インスタンスを生成する。
	 * 
	 * @param fileName
	 *            プロパティファイル名
	 */
	public DataSourceRepositoryInProperties(String fileName) {
		Validate.notNull(fileName);
		this.fileName = fileName;
	}
	
	/**
	 * {@inheritDoc}
	 * 
	 * @throws FileNotFoundRuntimeException ファイルが見つからない場合
	 * @throws IORuntimeException 
	 * プロパティファイルを読み込み時にエラーが発生した場合、 
	 * もしくはストリームをクローズできなかった場合。
	 */
	@Override
	public Collection<DataSource> findAll() {
		Properties properties = loadProperties(fileName);
		Map<String, DataSource> dataSources = parseProperties(properties);
		return dataSources.values();
	}
	
	@Override
	public DataSource findById(String identity) {
		Validate.notNull(identity);
		Properties properties = loadProperties(fileName);
		Map<String, DataSource> dataSources = parseProperties(properties);
		DataSource dataSource = dataSources.get(identity);
		return dataSource;
	}
	
	/**
	 * {@link DataSource}を管理する{@link Map}から指定した識別子の{@link DataSource}を取得する。
	 * 
	 * <p>{@link DataSource}を管理する{@link Map}に指定した識別子の{@link DataSource}がなければ作成して返す。</p>
	 * 
	 * @param dataSourceMap {@link DataSource}を管理する{@link Map}
	 * @param identity 識別子
	 * @return {@link DataSource}
	 */
	private DataSource getDataSoruceFromMap(
			Map<String, DataSource> dataSourceMap, String identity) {
		DataSource dataSource = null;
		if (dataSourceMap.containsKey(identity) == false) {
			dataSource = new DataSource(identity);
			dataSourceMap.put(identity, dataSource);
		} else {
			dataSource = dataSourceMap.get(identity);
		}
		return dataSource;
	}
	
	/**
	 * {@link Properties}を解析し、識別子をキーにデータソース情報を値に持つ{@link Map}に格納する。
	 * 
	 * @param properties {@link Properties}
	 * @return 識別子をキーにデータソース情報を値に持つ{@link Map}
	 */
	private Map<String, DataSource> parseProperties(Properties properties) {
		Validate.notNull(properties);
		Map<String, DataSource> dataSourceMap = Maps.newHashMap();
		for (Entry<Object, Object> propertiesEntry : properties.entrySet()) {
			String key = (String) propertiesEntry.getKey();
			if (key.startsWith("dataSources.") == false) {
				continue;
			}
			String value = (String) propertiesEntry.getValue();
			String[] split = key.split("\\.");
			String identity = split[1];
			DataSource dataSource =
					getDataSoruceFromMap(dataSourceMap, identity);
			String propertyName = split[2];
			setProperty(dataSource, propertyName, value);
		}
		return dataSourceMap;
	}
	
	/**
	 * {@link DataSource}のプロパティに値を設定する。
	 * 
	 * @param dataSource {@link DataSource}
	 * @param propertyName プロパティ名
	 * @param value
	 */
	private void setProperty(DataSource dataSource, String propertyName,
			String value) {
		if (propertyName.equals("driverClassName")) {
			dataSource.setDriverClassName(value);
		} else if (propertyName.equals("url")) {
			dataSource.setUrl(value);
		} else if (propertyName.equals("userName")) {
			dataSource.setUserName(value);
		} else if (propertyName.equals("password")) {
			dataSource.setPassword(value);
		}
	}
	
	@Override
	public void store(DataSource dataSource) {
		// 読み込みのみなので実装しない
		throw new UnsupportedOperationException("store");
	}
	
}

リポジトリは永続化を担当するので、エンティティを保存する機能とエンティティを検索する機能がメインなのですが、schema-generatorでは、設定ファイルからエンティティを読み込むだけなので、検索する機能のみを実装しています。

ここで注目したいのは、この二つのメソッドです。シグニチャに登場するのはドメイン層のエンティティです。インフラストラクチャ層のエンティティやテーブルクラスではありません。「DAOとは違う」とはこういうことです。仮に、リポジトリのメソッドのシグニチャに、インフラストラクチャ層のエンティティやテーブルクラスを利用した場合(つまりレイヤーを混同した場合)は、DBMSやSQLの言葉でドメイン層が汚染されることを意味します。そうなるとドメイン層にドメイン層以外の概念が存在することになり、コードを検討したり不具合の原因を調査することが困難になるでしょう。DAOはリポジトリの中だけに留めるようにするとよいでしょう。

public Collection<DataSource> findAll()
public DataSource findById(String identity)

汎用的なリポジトリのインターフェイス

今度は、schema-generatorの話題から離れて、現在考えているリポジトリのインターフェイスを以下に紹介します。findByIdがresolve、findAllがasEntitiesListに対応します。*4

public interface Repository<T extends Entity<T>> {
    /**
     * 識別子に該当するエンティティをリポジトリから取得する。
     *
     * @param identifier 識別子
     * @return エンティティ
     * @throws IllegalArgumentException
     * @throws EntityNotFoundRuntimeException エンティティが見つからなかった場合
     * @throws RepositoryRuntimeException     リポジトリにアクセスできない場合
     */
    T resolve(EntityIdentifier<T> identifier);

    /**
     * このリポジトリに格納されているすべてのエンティティをListで取得する。
     *
     * @return すべてのエンティティのList
     * @throws RepositoryRuntimeException リポジトリにアクセスできない場合
     */
    List<T> asEntitiesList();

    /**
     * このリポジトリに格納されているすべてのエンティティをSetで取得する。
     *
     * @return すべてのエンティティのSet
     * @throws RepositoryRuntimeException リポジトリにアクセスできない場合
     */
    Set<T> asEntitiesSet();

    /**
     * 指定した識別子のエンティティが存在するかを返す。
     *
     * @param identifier 識別子
     * @return 存在する場合はtrue
     * @throws RepositoryRuntimeException リポジトリにアクセスできない場合
     */
    boolean contains(EntityIdentifier<T> identifier);

    /**
     * 指定したのエンティティが存在するかを返す。
     *
     * @param entity エンティティ
     * @return 存在する場合はtrue
     * @throws RepositoryRuntimeException リポジトリにアクセスできない場合
     */
    boolean contains(T entity);

    /**
     * エンティティを保存する。
     *
     * @param entity 保存する対象のエンティティ
     * @throws RepositoryRuntimeException リポジトリにアクセスできない場合
     */
    void store(T entity);

    /**
     * 指定した識別子のエンティティを削除する。
     *
     * @param identifier 識別子
     * @throws EntityNotFoundRuntimeException 指定された識別子を持つエンティティが見つからなかった場合
     * @throws RepositoryRuntimeException     リポジトリにアクセスできない場合
     */
    void delete(EntityIdentifier<T> identifier);

    /**
     * 指定したエンティティを削除する。
     *
     * @param entity エンティティ
     * @throws EntityNotFoundRuntimeException 指定された識別子を持つエンティティが見つからなかった場合
     * @throws RepositoryRuntimeException     リポジトリにアクセスできない場合
     */
    void delete(T entity);
}

このインターフェイスを実装した実装クラスとして、オンメモリリポジトリを紹介します。

エンティティの識別子をキーに、エンティティを値に取るHashMapを内部で持っている単純なリポジトリです。

リポジトリは基本的に同一インスタンスで状態がどんどん変化していく、可変オブジェクトです。そして、エンティティ自体も可変オブジェクトです。先のエントリでも述べたように、可変オブジェクトは共有した際にあずかり知らぬところで意図しない更新が問題となります。その際の解決方法のひとつとしてcloneを実装することも紹介しました。

このオンメモリリポジトリでも、同様に外部から取り入れるエンティティはcloneしてから取り込みます。そして、外部に返すエンティティもcloneしたものを返します。また、オンメモリリポジトリは実体がメモリであるため、cloneすることができます。*5そうすることで、可変オブジェクトも不変条件を維持することができます。

/**
 * オンメモリ実装のリポジトリ。
 *
 * @param <T> エンティティの型
 */
public class OnMemoryRepository<T extends Entity<T>> implements Repository<T> {

    private final Map<EntityIdentifier<T>, T> entities = new HashMap<EntityIdentifier<T>, T>();

    @SuppressWarnings("unchecked")
    public OnMemoryRepository<T> clone() {
        try {
            return (OnMemoryRepository<T>) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new Error("clone not supported");
        }
    }

    @Override
    public T resolve(EntityIdentifier<T> identifier) {
        Validate.notNull(identifier);
        return entities.get(identifier).clone();
    }

    @Override
    public List<T> asEntitiesList() {
        List<T> result = new ArrayList<T>(entities.size());
        for (T entity : entities.values()) {
            result.add(entity.clone());
        }
        return result;
    }

    @Override
    public Set<T> asEntitiesSet() {
        Set<T> result = new HashSet<T>(entities.size());
        for (T entity : entities.values()) {
            result.add(entity.clone());
        }
        return result;
    }

    @Override
    public boolean contains(EntityIdentifier<T> identifier) {
        Validate.notNull(identifier);
        return entities.containsKey(identifier);
    }

    @Override
    public boolean contains(T entity) {
        Validate.notNull(entity);
        return contains(entity.getIdentifier());
    }

    @Override
    public void store(T entity) {
        Validate.notNull(entity);
        entities.put(entity.getIdentifier(), entity.clone());
    }

    @Override
    public void delete(EntityIdentifier<T> identifier) {
        Validate.notNull(identifier);
        entities.remove(identifier);
    }

    @Override
    public void delete(T entity) {
        Validate.notNull(entity);
        delete(entity.getIdentifier());
    }
}

次はアグリゲートにいきますが、もしかしたらScalaが入るかもw

追記:

オブジェクト間の依存関係を爆発させないための工夫としては、エンティティの識別子だけを保持するやり方があります。

// 従業員にタスクを割り当てるマネージャ。
public class Mangaer extends AbstractEntity<Manager> { 
    // private Collection<Employee> employees; とせずにエンティティの識別子だけを保持する
    private final Collection<EntityIdentifier<Employee>> employeeIds;
    private final Repository<Employee> employeeRepoistory;

    // コンストラクタにリポジトリとエンティティのIdの集合を渡す
    public Mangaer(Repository<Employee> employeeRepoistory,
                   Collection<EntityIdentifier<Employee>> employeeIds){
        this.employeeRepoistory = employeeRepoistory;
        this.employeeIds = new ArrayList<EntityIdentifier<Employee>>(employeeIds);
    }

    // 全員にタスクを割り当てる
    public void dispatch(Task task){
        for(EntityIdentifier<Employee> employeeId : employeeIds){
             final Employee employee = employeeRepoistory.resolve(employeeId);
             employee.processTask(task); // なんか仕事させる
        }  
    }

}

あわせて読みたい

コードで学ぶドメイン駆動設計入門 〜エンティティとバリューオブジェクト編〜 - じゅんいち☆かとうの技術日誌

コードで学ぶドメイン駆動設計入門 〜振る舞いとサービス編〜 - じゅんいち☆かとうの技術日誌

コードで学ぶドメイン駆動設計入門 〜ファクトリ編〜 - じゅんいち☆かとうの技術日誌

コードで学ぶドメイン駆動設計入門 〜アグリゲート編〜 - じゅんいち☆かとうの技術日誌

*1:128ビットの値表現が可能なUUIDであれば十分だと思います。2の128乗=340澗(かん)個のアドレス表現が可能だからです。澗(かん)と言われてもよくわからないと思うのですが、これはIPv6と同じアドレス空間です。

*2java.util.UUID#randomUUID()は、論理的にIDの衝突は起こり得るのですが、ほぼゼロに近い確率なので現実的にIDとして利用可能です。DDDの原書にもIDはファクトリで生成するとよいと触れられていますが、AbstractEntityFactory#createメソッドなどで識別子の自動付与を行うとよいでしょう。

*3:Jiemamyプロジェクトには、object-manipulatorというDXO用のライブラリがあります。

*4:こちらもJiemamyプロジェクトのdddbaseから影響を受けています。

*5:ファイルやDBではcloneすることが現実的ではありませんので実装しません

2010-01-29

[] 無茶振りタスクから身を守る方法

最近、デスマの話を聞いても驚かなくなった。仕事は選べなくなったとよく聞く。

その会話の中でよく耳にするのは、「無茶振りプロジェクト」や「無茶振りタスク」の話題だ。

こんなシーンが想像できる。

とある顧客:この不具合さー、明日まで対応できる?

とあるエンジニア:え、明日ですか、、、(明日といっても後数時間じゃん)

とある顧客:明日までに対応しますって、お客さんと約束したので、対応してもらわないと困ります。

とあるエンジニア:(困るっていわれても、、、)あ、分かりました。対応します(無茶振りだなー。できるかなー)

その後、必死になって不具合原因を調査する。コードが超スパゲッティで原因調査に時間が掛かりそうだということがわかった。でも、今更断れないということで、徹夜して頑張ることに。。

そして翌日。

とある顧客:昨日の不具合の改修完了しました?

とあるエンジニア:いやー、ちょっと終わっていません。。いろいろ難しくて。。

とある顧客:ありえないでしょう。やるっていったでしょう。。

まぁ、無茶振りなのは分かるw

だが、これは”とあるエンジニア”に問題がある(と考えたほうが価値的だ)。その理由は当然 受発注で顧客スタンスを尊重すべきだという話もあるが、相手をいくら批判しても相手に変わってもらわないといけない。つまり自分のコントロール下でない外部要因となる。それなら、まず自分のコントロール下である自分の行動を最適化したほうがいい。


まず考えられるのは、最初に無条件にタスクを安請け合いしている点。

次は、作業開始後に、作業が完了できないリスクが発生しているのに、顧客に警告を出さずに納期当日を迎えている点だ。

タスクを実現するためのIF文を考える

最初にタスクが無茶振りで問題だと思いながらも、顧客に何も伝えずにタスクを受けている。これは問題。

「このタスクは明日までに作業を完了させることはできません。」とはっきりいうことだ。

これだけだと、単なる あなたの感想でしかない。全くロジックになっていない。

「このタスクは明日までに作業を完了させることはできません。なぜならば○○○だからです。」とはっきりいう。

たとえば、このケースの場合は、「このタスクは納期どおりに作業することはできません。なぜならばソースを解析するためのリソースが2名足りないだからです。」などが考えられる。*1


このロジックポリシーを採用している場合の難点があるとしたら、「こいつはいつも否定的なことばかりいいやがって」という感情論に対するサポートができていないということだ。

そこに目を向けるなら、対偶命題を使おう。「AならばBである」に対する対偶命題は「BでないならAでない」だ。

「(もし)ソースを解析するためのリソースが2名追加してもらえば、明日までに作業を完了させることができます」という具合だ。


ここで間違ってほしくないのだが、この対偶命題で「いや、それでもできないよ」となる場合は、そもそもの最初の命題がおかしいと考えなければならない。納期通りに作業を完了できない根拠がまだほかにあるということだ。要は○○○の部分の根拠が考慮漏れで、MECEではなかったのだ。それで対偶命題を作っても論理破綻するのは当然だ。


いずれにしても、「ソースを解析するためのリソースが2名追加してもらえば」の部分は、タスクを完了させるための前提条件。プログラムでいうならば、IF文に相当する部分だ。この条件だけは、足りない場合はIF文を複数考えるか、AND条件を考えればよい。どんな無茶なタスクでも前提のIF文があるはずだ。IF文が成り立つなら絶対実行できるだろうし、成り立たないなら絶対に実行できない。どうしても無茶振りタスクを実行させたいなら、顧客にそのIF文をクリアするようにしてもらえばよいのだ。これは契約条件のようなものだ。


屏風の虎退治ができるようになろう

一休さんの屏風の虎退治という説話を思い出してほしい。「”屏風の虎”を退治せよ」という無茶振りタスクに対して、「”屏風の虎”を屏風から出してもらえば、虎を退治する」という命題で挑んだわけだ。*2これは、無茶振りタスクの証明には十分でわかりやすい。私の考えでは、無茶振りタスクには、それを実現するためのIF文を作って顧客に説明するのが一番納得感が得られてわかりやすいと思っている。そんなの屁理屈といってしまうのは簡単だろうだが、業務においては「わかりやすい」つまりシンプルにするということは何事にも代え難い。問題をシンプルにすることで仕事が早くなればよいのだ。無用に感情論に走るよりは何倍も価値的だ。目的と手段を履き違えてはならいと思う。


現場では、事前調査の時間が全くなく、タスクの実行を余儀なくされる場合も多い。その場合は次のリスクに対するスタンスが大事なる。

リスクの報告と契約変更を忘れない

2点目。まぁ、これは前のエントリでも書いたが「仮説思考」で考えて、リスクが発生したら早期に修正することだ。その際には顧客などの利害関係者にも報告しなければならない。報告については日々の作業の状況を伝えていたほうがいい。報告までのインターバルが長いと、現状を無視した会話になりがちだ。顧客にこちらの状況を理解してもらいたければ、できるだけ日々の作業状況を伝えることだ。アジャイルでもこのスタンスは重要視されているはずだ。まぁ、言うのは簡単だが、意外に難しい。日々流れているタスクが多いと、意識しておかないと、重大なリスクも自分勝手に判断して見逃してしまう可能性があるから注意が必要だ。


それと、リスクが発生した場合の対処だが、リスクが発生した事でタスクの前提条件が崩れたので、顧客などの利害関係者を含めて、タスクの見直しや再定義が必要になる。これを怠ると、認識の相違がでてトラブルの元となるので気をつけたい。つまるところ、タスクの依頼主と担当者の間には、”契約”が成立していると考えたほうがいい。法的にいっても口頭で約束しても契約は成り立つ。リスクが発生して契約が守れなくなったらすぐにアラートを上げて、契約の変更を申請しよう。


上記の例では、とある顧客と、とあるエンジニアとしたが、タスクの依頼主と担当者に置き換えて表すことができるので、社外だけではなく社内にも目を向けてみると成り立つところが多い。上記に書いたことは、顧客と向き合うPMやリーダだけでなく、プログラマが実践しても十分に効果を出すことができる。

このご時世、無茶振りタスクがいつ降ってくるかわからない、ぜひ心掛けたいところだ。

*1:演繹的には○○○のところの根拠がMECEで数が多く納得感があれば相手は反論できないはずだ

*2:屏風から虎を出してしまったら退治できるわけ?という疑問当然あるが、、、そこはおいておいてーw

2009-02-11

[][][] 継続は力なり

とはよく言ったものですが、本業との兼ね合いで、地豆はおろか、自分のプロダクトにコミットできていません>< 継続は力なりは大事ですね。

地豆に関しても、このような自分のライフスタイルに会っていて、地豆の優先度に会うタスクを見出せないでいる。

地豆に貢献できていないのは残念だけど、S2Chronos, S2Configは徐々に使ってくれる開発者の方も増えてきているので継続的にコミットしなきゃなと。

つうことで、S2ChronosをDIコンテナ非依存にするとか、複数サーバでもアトミックにタスクを実行できるようにするとか、いろいろやりたいことはありますが、、、そのような機能追加は一旦置いておいて、もう少し使い勝手とかドキュメント(Javadoc,チュートリアルの充実)とか、そういう基本的なところに力を入れることにします。

id:happy_ryoには「S2Configはこれかなり使えるじゃないですか!しーさーこんで登壇してください」とかいわれたけどリリース間に合わんので自重させてください><

2008-10-02

[][] maven-antrun-pluginでs2jdbc-genのタスクを実行させる件

s2jdbc-genはantのタスクが用意されていて,DoltengでSAStruts/S2JDBCのプロジェクトを作成するとs2jdbc-gen-build.xmlが作成されます.しかし,pom.xmlでmaven-eclipse-plugin形式のプロジェクトに変更してしまうと,クラスパスの問題でそのままではs2jdbc-gen-build.xmlが実行できなくなります.

まぁ,ちまちまM2_REPOのクラスパスをs2jdbc-gen-build.xmlに1個1個定義してあげればなんとかなりますが,pom.xmlのdependencyのversionなどが変更になった場合都度修正するのが面倒なんで,maven-antrun-pluginを使いますです.以下のようなplugin設定を施せばスキーマからエンティティやサービスクラスを生成することができます.antにmaven.runtime.classpathを渡してあげるのがみそですね.ただ,compileフェーズで毎回実行されてしまうので,独自のフェーズを定義してあげたほうがよいかもしれません.そのあたり私もMaven2を理解しきっていないので何かよい方法があれば教えてくださいw

そもそもMaven2のプラグインとして実装したほうがよいかもしれませんが,とりあえずお手軽に実行できる手段として作ってみました.

	<build>
	...
		<plugins>
		...
			<plugin>
				<artifactId>maven-antrun-plugin</artifactId>
				<dependencies>
					<dependency>
						<groupId>org.apache.ant</groupId>
						<artifactId>ant</artifactId>
						<version>1.7.0</version>
					</dependency>
				</dependencies>
				<executions>
					<execution>
						<id>gen-entity</id>
						<phase>compile</phase>
						<configuration>
							<tasks>
								<property name="classpathdir"
									value="target/classes" />
								<property name="rootpackagename"
									value="hogehoge" />
								<property name="entitypackagename"
									value="entity" />
								<property name="javafiledestdir"
									value="src/main/java" />
								<property name="testjavafiledestdir"
									value="src/test/java" />
								<property name="javafileencoding"
									value="UTF-8" />
								<property name="env" value="ut" />
								<property name="jdbcmanagername"
									value="jdbcManager" />

								<taskdef
									resource="s2jdbc-gen-task.properties" classpathref="maven.runtime.classpath">
								</taskdef>
								<gen-entity
									rootpackagename="${rootpackagename}"
									entitypackagename="${entitypackagename}"
									javafiledestdir="${javafiledestdir}"
									javafileencoding="${javafileencoding}" env="${env}"
									jdbcmanagername="${jdbcmanagername}" classpathref="maven.runtime.classpath">
								</gen-entity>
								<javac srcdir="${javafiledestdir}"
									destdir="${classpathdir}" encoding="${javafileencoding}"
									fork="true" debug="on" classpathref="maven.runtime.classpath">
									<include
										name="${rootpackagename}/${entitypackagename}/**" />
								</javac>
								<gen-test classpathdir="${classpathdir}"
									rootpackagename="${rootpackagename}"
									entitypackagename="${entitypackagename}"
									javafiledestdir="${testjavafiledestdir}"
									javafileencoding="${javafileencoding}" env="${env}"
									jdbcmanagername="${jdbcmanagername}" classpathref="maven.runtime.classpath" />
								 <gen-condition
									classpathdir="${classpathdir}"
									rootpackagename="${rootpackagename}"
									entitypackagename="${entitypackagename}"
									javafiledestdir="${javafiledestdir}"
									javafileencoding="${javafileencoding}" env="${env}"
									jdbcmanagername="${jdbcmanagername}" classpathref="classpath" />
								<gen-names
									classpathdir="${classpathdir}"
									rootpackagename="${rootpackagename}"
									entitypackagename="${entitypackagename}"
									javafiledestdir="${javafiledestdir}"
									javafileencoding="${javafileencoding}" env="${env}"
									jdbcmanagername="${jdbcmanagername}" classpathref="maven.runtime.classpath" />
								<gen-service
									classpathdir="${classpathdir}"
									rootpackagename="${rootpackagename}"
									entitypackagename="${entitypackagename}"
									javafiledestdir="${javafiledestdir}"
									javafileencoding="${javafileencoding}" env="${env}"
									jdbcmanagername="${jdbcmanagername}" classpathref="maven.runtime.classpath" />
							</tasks>
						</configuration>
						<goals>
							<goal>run</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		...
		</plugins>
	...
	</build>
	<dependencies>
	...
		<dependency>
			<groupId>org.seasar.container</groupId>
			<artifactId>s2jdbc-gen</artifactId>
			<version>0.9.1</version>
		</dependency>
	...
	<dependencies>

ウェブアプリプロジェクトの場合はclasspathdirをsrc/main/webapp/WEB-INF/classesなど適切な場所にしてください.

2008-09-17

[][] タスククラスに複数のタスクグループを定義してみる

お次はタスクグループをタスククラス内に複数定義する方法です.

以下の例ではタスククラス内に二つのタスクグループを作っています.タスクグループを指定するにはdoから始まるタスクメソッドにTaskGroupアノテーションを使ってグループ名を記述します.タスクグループが開始する前に呼ばれるstartメソッドをgroupAであればstartGroupAという名前で記述します.タスクグループが終了した後に呼ばれるendメソッドをgroupAであればendGroupAという名前で定義します.startGroupAでは,グループが開始された時最初に実行するタスクメソッドを記述してください.タスクグループの定義はこれで完了.

後は,タスクの開始メソッド,startメソッドでNextTaskアノテーションを使い,最初に実行するタスクグループ名を指定します.そうするとタスク開始時に期待どおりに指定したタスクグループのタスクから実行されます.タスクグループAが終了したらタスクグループBを実行したい場合は,タスクグループの終了メソッド,endGroupAメソッドにNextTaskアノテーションで次に実行すべきタスクグループを指定してください.

start,end系のメソッド戻り値で次に遷移するタスクグループ名ないしタスク名が指定できない仕様になっています.次のバージョンは戻り値で指定できるようにする予定です.

@Task
@NonDelayTrigger
public class TaskGroupTask {

	private Logger log = Logger.getLogger(TaskGroupTask.class);

	@NextTask("groupA")
	public void start() {
		log.info("start");
	}

	//----- タスクグループA -----

	@NextTask("taskA")
	public void startGroupA() {
		log.info("startGroupA");
	}

	@TaskGroup("groupA")
	@NextTask("taskB")
	public void doTaskA() {
		log.info("doTaskA");
	}

	@TaskGroup("groupA")
	public void doTaskB() {
		log.info("doTaskB");
	}

	@NextTask("groupB")
	public void endGroupA() {
		log.info("endGroupA");
	}


	//----- タスクグループB -----

	@NextTask("taskC")
	public void startGroupB() {
		log.info("startGroupB");
	}

	@TaskGroup("groupB")
	@NextTask("taskD")
	public void doTaskC() {
		log.info("doTaskC");
	}

	@TaskGroup("groupB")
	public void doTaskD() {
		log.info("doTaskD");
	}

	public void endGroupB() {
		log.info("endGroupB");
	}

	public void end() {
		log.info("end");
	}

}

2008-09-16

[][] S2Chronosでバッチ処理をつくってみよう

1.0.0をリリースした記念にS2Chronosの使い方を簡単に紹介していく記事を書いていきたいと思います.

セットアップ

セットアップ手順は

http://s2chronos.sandbox.seasar.org/ja/install.html

をご覧ください.

サンプルのダウンロードは

http://s2chronos.sandbox.seasar.org/ja/download.html

からお願いします.

コンソール版のサンプルはs2chronos-example-1.0.0.zip

Teeda版のサンプルはs2chronos-teeda-example-1.0.0.zip

SAStruts版のサンプルはs2chronos-sastruts-example-1.0.0.zip

です.

コンソールアプリではスケジューラの起動命令を記述する必要があります.三行で書けますw

	public static void main(String[] args) {
		SingletonS2ContainerFactory.init();
		SingletonS2Container.getComponent(Scheduler.class).process();
		SingletonS2ContainerFactory.destroy();
	}

アプリケーションサーバにS2Chronosを内包しウェブアプリとバッチ処理を同居させることができます.ただし,S2ContainerServletより後に起動するようにload-on-startupを調整してください.

    <servlet>
        <servlet-name>chronosServlet</servlet-name>
        <servlet-class>org.seasar.chronos.extension.servlet.S2ChronosServlet</servlet-class>
        <load-on-startup>3</load-on-startup>
    </servlet>

すでに持っているDIできる資産を好きなだけバッチ処理にDIしてください.

バッチ処理はタスククラスを書くことから

ルートパッケージ名+taskに,クラスサフィックスがTaskで終わるクラス名で以下のようなタスククラスを定義しましょう.すると,アプリケーションを起動したときにS2Chronosがタスククラスを見つけて,スケジューラに自動的にスケジュール登録します.このサンプルは,1分ごとにdoExecuteメソッドを呼び出し,ログを出力します.

@Task
@CronTrigger(expression = "0 */1 * * * ?")
public class SampleTask {
    
    private static Logger log = Logger.getLogger(SampleTask .class);
    
    // タスク処理
    public void doExecute() {
        log.info(this.getClass().getSimpleName() + ":doExecute");
    }

}

ちなみに,@Taskがついていない,トリガーが指定されていないタスクスケジューリングされません.(Taskアノテーションがついていても,@Task(autoSchedule=false)の場合はスケジューリングされません)

実行時間はトリガーで指定する

トリガーはタスクを起動する時間を指定する役割を持っています.標準でいくつかのトリガーが用意されていますが,やはり最も利用するのがクローントリガーです.上記の例ではCROND形式の起動条件を指定して1分ごとにタスクが起動するようにしています.

単純なバッチ処理であれば覚えることはこれだけ.次回はスケジューラのスレッドの話をさせてもらいます.

[][] 初期化と破棄処理について

スレッドプール周りの話に行く前に,初期化と破棄処理のお話をしておかないとまずいw.

initializeとdestroyという名のメソッドを定義してください.そうするとS2Chronosのスケジューラによって登録された後に,スケジューラがタスクを順番に検索します.その検索時に最初に一度だけinitializeメソッドが実行されます.

そして,破棄メソッドですが,タスククラスのdoExecuteメソッドが終了したら,スケジューラ内部でのタスクの状態管理が実行済みに遷移し,その後一定間隔でスケジューラのキューから削除されます.その削除前に一度だけdestroyメソッドが呼ばれます.(GC的な破棄という意味ではないので誤解なきよう)ただし,destroyメソッドタスククラスがisReScheduleTaskをtrueで返すか,タスククラスが返すトリガーがisReScheduleTaskをtrueで返す場合は,destroyメソッドは呼ばれません.isReScheduleTaskがtrueの場合は永遠に終わりの来ないタスクという意味合いになります.注意してください.標準では@CronTrigger, CCronTriggerはisReScheduleTaskをtrueで返しますので,destroyメソッドを定義していても呼ばれません.

このあたりも,Teedaをマネて作っているので規約ベースなんですが,今後はT2のようにアノテーションで好きな名前を指定できるようにしたいと思いますw

@Task
@NonDelayTrigger
public class SampleTask {

	private static Logger log = Logger.getLogger(SampleTask .class);

	public void initialize(){
		log.info(this.getClass().getSimpleName() + ":initialize");
	}

	public void doExecute(){
		log.info(this.getClass().getSimpleName() + ":doExecute");
	}

	public void destroy(){
		log.info(this.getClass().getSimpleName() + ":destroy");
	}
}

[][] 開始処理と終了処理について

退屈な説明ですが,基本的なことなんでご容赦をw

初期化処理,破棄処理に加えてタスクを実行前と実行後に呼ばれるメソッドを定義できます.startとendというメソッド名で定義してください.下記の例では,呼び出し順序が,initialize→ start → doExecute → end → destroyとなります.

isReScheduleTaskがtrueを返すタスクは,タスクが実行されるごとにstartとendが実行されます.

@Task
@NonDelayTrigger
public class SampleTask {

	private static Logger log = Logger.getLogger(SampleTask .class);

	public void initialize(){
		log.info(this.getClass().getSimpleName() + ":initialize");
	}

	public void start(){
		log.info(this.getClass().getSimpleName() + ":start");
	}

	public void doExecute(){
		log.info(this.getClass().getSimpleName() + ":doExecute");
	}

	public void end(){
		log.info(this.getClass().getSimpleName() + ":end");
	}

	public void destroy(){
		log.info(this.getClass().getSimpleName() + ":destroy");
	}
}

[][] タスクタスクグループの関係

詳しくは知りませんが,JP1にはジョブとジョブグループが存在します.

ジョブ,つまりバッチ処理の最小単位ですが,ジョブグループはジョブを複数内包することができます.これをそのままクラス表現にすると,コンポジットパターンでJobGroupはJobを複数保持できるクラスということになるかと思います.

このモデルは意図的にS2Chronosでは採用していません.非常に見通しが悪くなるためです.デザイン的には優れていてもソースコードの見通しが悪くなるものはなるべく採用しないというスタンスです.

では,S2Chronosではジョブグループに相当するタスクグループはどう表現するかってことですが,,,

まず,タスククラスでは,複数のタスクメソッドを管理できます.doExecute以外のdoから始まるメソッドを複数定義できます.以下を参照してください.

@Task
@NonDelayTrigger
public class SampleTask {

	private static Logger log = Logger.getLogger(SampleTask .class);

	public void initialize(){
		log.info(this.getClass().getSimpleName() + ":initialize");
	}

	@NextTask("taskA")
	public void start(){
		log.info(this.getClass().getSimpleName() + ":start");
	}

	@NextTask("taskB")
	public void doTaskA(){
		log.info(this.getClass().getSimpleName() + ":doTaskA");
	}

	public void doTaskB(){
		log.info(this.getClass().getSimpleName() + ":doTaskB");
	}

	public void end(){
		log.info(this.getClass().getSimpleName() + ":end");
	}

	public void destroy(){
		log.info(this.getClass().getSimpleName() + ":destroy");
	}
}

この場合,doから始まるタスクメソッドが複数あるのでどこから始めればよいかわかりません.なので,startメソッドにNextTaskアノテーションを使って指定する必要があります.doを省いたメソッド名をキャメル形式で指定してください.また,doTaskAの後に実行するタスクメソッドもdoTaskA自体にNextTaskアノテーションで定義してください.

この方法では,コンパイル時に遷移先のタスクを決定しますが,以下のようにすれば実行時にも指定できます.

@Task
@NonDelayTrigger
public class SampleTask {

	private static Logger log = Logger.getLogger(SampleTask .class);

	public void initialize(){
		log.info(this.getClass().getSimpleName() + ":initialize");
	}

	@NextTask("taskA")
	public void start(){
		log.info(this.getClass().getSimpleName() + ":start");
	}


	public String doTaskA(){
		log.info(this.getClass().getSimpleName() + ":doTaskA");
		return "taskB";
	}

	public void doTaskB(){
		log.info(this.getClass().getSimpleName() + ":doTaskB");
	}

	public void end(){
		log.info(this.getClass().getSimpleName() + ":end");
	}

	public void destroy(){
		log.info(this.getClass().getSimpleName() + ":destroy");
	}
}

戻り値で次に遷移するタスクメソッドを指定すれば実行に遷移先を決定できます.

タスクメソッドはdoTaskAが完了すれば,doTaskBへと同期的に呼び出されて実行されます.通常の関数呼び出しと変わりません.このタスクメソッドを非同期に呼び出したい場合は以下のようにします.

@Task
@NonDelayTrigger
public class SampleTask {

	private static Logger log = Logger.getLogger(SampleTask .class);

	public void initialize(){
		log.info(this.getClass().getSimpleName() + ":initialize");
	}

	@NextTask("taskA")
	public void start(){
		log.info(this.getClass().getSimpleName() + ":start");
	}

	@NextTask("taskA")
	@JoinTask(JoinType.NoWait)
	public void doTaskA(){
		log.info(this.getClass().getSimpleName() + ":doTaskA");
	}

	public void doTaskB(){
		log.info(this.getClass().getSimpleName() + ":doTaskB");
	}

	public void end(){
		log.info(this.getClass().getSimpleName() + ":end");
	}

	public void destroy(){
		log.info(this.getClass().getSimpleName() + ":destroy");
	}
}

このようにJoinTaskアノテーションでNoWaitを指定するとdoTaskAの終了を待たずに次のdoTaskBに遷移します.ちなみに,先ほどの戻り値にStringを使って次のタスクメソッドに遷移する場合は同期呼び出し,つまりJoinTaskアノテーションでWaitが指定されたのと同じ扱いになります.

同期でも非同期でもすべてのタスクメソッドが終了するとendメソッドが呼ばれます.

このように,ひとつのタスククラスで複数のバッチ処理をメソッド単位で定義できます.1つのバッチ処理でも,複数のバッチ処理でもタスククラスで柔軟に定義できるようにしています.S2Chronosではこれをタスクグループと定義しています.また,タスククラスには複数のタスクグループを定義できます.これは別の機会に紹介します.

同期呼び出しについては通常の関数呼び出しと変わらないわけなので,NextTaskアノテーションを使わずとも記述できますが,タスクメソッドの遷移はコードを書かないというポリシーで設計されています.

[][] スレッドプールの使い方

バッチ処理を効率よく実行するには,マルチスレッド化はかかせません.

ただし,ハードウェアリソースを無視してスレッドを作りすぎるのはあまり賢い方法ではありませんので,ここはスレッドプールを使いましょう.ということで,S2Chronosの内部では,Java5から使えるconcurrentパッケージのスレッドプールを利用しています.

開発者は特別に意識しなくともバッチ処理をスレッドプール上で実行させることが可能となります.

次のようなコードを書くとスレッドプールを明示的に指定できます.threadPoolTypeとthreadPoolSizeプロパティで作成されるスレッドプールが決まります.ThreadPoolTypeは,Java5のconcurrentパッケージに準拠しており,FIXED/CACHED/SINGLE/SCHEDULEDが使えます.

@Task
@NonDelayTrigger
public class BasicTask {
    
    private static Logger log = Logger.getLogger(BasicTask.class);
    
    // スレッドプールタイプを返します.
    public ThreadPoolType getThreadPoolType(){
    	return ThreadPoolType.FIXED;
    }
    
    // スレッドプールのサイズを返します.
    public Integer getThreadPoolSize(){
    	return 10;
    }

    // タスク処理
    public void doExecute() {
        log.info(this.getClass().getSimpleName() + ":doExecute");
    }

}

また,以下のようにThreadPoolを使ってもOKです.

@Task
@NonDelayTrigger
public class BasicTask {
    
    private static Logger log = Logger.getLogger(BasicTask.class);
    
    private TaskThreadPool threadPool = new ThreadPool();
    
    public void initialize(){
    	threadPool.setThreadPoolType(ThreadPoolType.FIXED);
        threadPool.setThreadPoolSize(10);
    }
    
    // スレッドプールを返します.
    public TaskThreadPool getThreadPool(){
    	return threadPool;
    }

    // タスク処理
    public void doExecute() {
        log.info(this.getClass().getSimpleName() + ":doExecute");
    }

}

ただ,この状況だとタスクメソッドは同時に実行されるのは1個だけなのでスレッドプールは使い切りませんw

たとえば,以下だと

@Task
@NonDelayTrigger
public class BasicTask {
    
    private static Logger log = Logger.getLogger(BasicTask.class);
    
    private TaskThreadPool threadPool = new ThreadPool();
    
    public void initialize(){
    	threadPool.setThreadPoolType(ThreadPoolType.FIXED);
        threadPool.setThreadPoolSize(10);
    }
    
    // スレッドプールを返します.
    public TaskThreadPool getThreadPool(){
    	return threadPool;
    }

    // タスク処理
    @CloneTask(20)
    public void doExecute() {
        log.info(this.getClass().getSimpleName() + ":doExecute");
    }

}

CloneTaskアノテーションでdoExecuteを同時に20個並列実行しようとします.スレッドプールは10個ですので,まず最初に10個が実行され空きが発生しだい残りのタスクメソッドが実行されます.また,CloneTaskアノテーション以外にJoinTaskでNoWaitを指定したタスクがある場合も,指定されたスレッドプール内で並行処理を実行します.

このスレッドプールを複数のタスク間で共有したい場合も以下のようにすれば可能です.

TaskThreadPoolをMapで管理するThreadPoolCacheUtilを用意してください.(標準で用意したほうがいいかな...検討しますw)そこで管理されるスレッドプール情報を複数のタスククラスのthreadPoolプロパティで返せば,スレッドプールを共有するタスククラスを実装できます.

@Task
@NonDelayTrigger
public class BasicTask {
    
    private static Logger log = Logger.getLogger(BasicTask.class);

    // スレッドプールを返します.
    public TaskThreadPool  getThreadPool(){
    	return ThreadPoolCacheUtil.get("basicGroup");
    }

    // タスク処理
    @CloneTask(20)
    public void doExecute() {
        log.info(this.getClass().getSimpleName() + ":doExecute");
    }

}

2008-09-08

[] S2Chronos 1.0.0をリリースしました

遅くなりましてすみません.やっとこさリリースしました.

initializeメソッドとdestroyメソッドが名ばかりだったのですが,今回のリリースでちゃっと動作するようにしました.

これまでのinitializeとdestoryは,start, endに置き換わっていますのでご注意ください.これまでのタスククラスのソースがある場合は単純にstartとendに置き換えてください.

不明点などあれば何なりとSeasar user のMLか,ブログにコメントいただければ反応するようにしたいと思います.

よろしくお願いいたします.

*New Feature
[CHRONOS-20] TaskTriggerにshutdownTaskプロパティを追加しました
[CHRONOS-22] Taskクラスをpublicフィールドに対応させました
[CHRONOS-21] Taskクラス内でスローしたExceptionをexceptionプロパティにDIするようにしました
[CHRONOS-27] トリガーの有効期間を指定できるScopedTimeTriggerを新規に追加しました
[CHRONOS-28] トリガーを複数格納できるTriggerChainトリガーを新規に実装しました
[CHRONOS-29] S2ChronosのHOT deployを無効化できるオプションを実装しました
[CHRONOS-30] タスクが初期されるときと破棄されるときに1度だけ呼び出されるメソッドを新規に実装しました

*Improvement
[CHRONOS-24] initializeメソッドとdestroyメソッドをstartメソッドとendメソッドに改名しました      
[CHRONOS-25] startメソッドがない場合もしくはstartメソッドにNextTaskアノテーションがない場合にでも,doExecuteがあれば実行されるようにしました      
[CHRONOS-26] タスククラスのisReScheduleをisReScheduleTaskに改名し,TaskTriggerにもisReScheduleTaskを保持するようにしました

[] Seasar Conference 2008 Autumn ありがとうございました

御礼

裏番組がひがさんのセッションというのに、私のセッションに足を運んでいただいた方に感謝感謝です。本当にありがたいことです。また、S2ChronosはDI時代の新しいバッチ処理フレームワークの立ち位置というのはあるなぁーと手応えを感じました。これからも今後も皆様のお役に立てるように頑張ります。

私も他のセッションに参加

id:shot6さんのT2(T2Framework)と、長谷川さんのSpringの話、id:dewaさんのSAStrutsの開発tipsとLTをみてきました。

T2の規約よりアノテーションを重視するコンセプトはよいなと思いました。形式知と暗黙知のバランスを取るってのはこれから求められることだろうと。

規約を多用しすぎるフレームワークって、主要な規約を暗記するのが大変。ストーンオーシャンにでてくるジェイル・ハウス・ロックというスタンドって、新しいことは3つまでしか覚えれないという能力なんですがw、それに近いぐらい 現場のみんなも新しく規約を覚えるのは大変かもしれないなぁーと最近は思ったりしちょりますw

T2もSAStrutsも目指しているのは形式知と暗黙知のちょうどよいバランスを取ることなんではないかなと個人的に思いました。

id:dewaさんのセッションも、SAStrutsの使った場合のアプリケーションの設計戦略について大変参考になりました。

バッチ処理系のフレームワーク

長谷川さんのセッションで、Spring Batch、LT枠でTERASOLNAのBatchフレームワークの話が出てました。Spring Batchは提供されている機能がリッチすぎて戸惑うな。ミニマルなAPIセットだけでも切り出せてもらえると使いやすいんじゃないかと思うけど。。。

TERALSOLNAのほうがまだ使いやすそう。だけど、XMLやクラスは必要最低限のものしか書きたくないんですよねー。一番の特徴として入力系と出力のタスクを分割することでI/O待ちを減らせるのはよいですね。これはS2Chonosでも対応したいな。S2JMSを使ったキューを用意して、キューイングされたときに有効になるトリガーを用意したいと思います。

飲み会にて

id:makotanさんとid:dewaさんの間に座りました。Buriについて参考になる情報をいただきました。fmfm。次の案件では実戦投入したいと思います。id:dewaさんの新たな挑戦のお話を聞く。自分も前向きに頑張ろう。id:dewaさんのお隣にid:taediumさん。S2JDBC-Genをリリースされて何よりです。

さて、これからS2Chronosの1.0.0のリリース作業を開始したいと思います。

どうぞよろしくお願いいたします。