レベル: 中級 David Currie (david_currie@uk.ibm.com), Staff Software Engineer, IBM UK Ltd
2005年 5月 03日 J2EE(Java™ 2 Enterprise Edition)のConnector Architecture、JCAの最新版で行われた機能強化と変更について、David Currieが3回のシリーズを続けます。この記事では、新しいJCA作業管理契約(JCA work-management contract)を紹介します。この作業管理契約によって、リソース・アダプターは作業のスケジュールや処理に対して、アプリケーション・サーバーの機能を利用できるようになります。またJCAでの機能強化のもう1つ、トランザクション・インフロー(transaction-inflow)がサポートされたことによって、企業情報システムはその作業を、自身のトランザクションの下で行うことができます。
J2EE Connector Architectureの最新版であるJCA 1.5には、数多くの重要な機能改善が含まれており、また大きな追加も幾つか行われています。こうした変更を解説した3回シリーズの2回目である今回は、第1回で紹介したライフサイクル管理を基礎に、JCAでの新しい作業管理契約について見て行きます。この契約を利用すると、リソース・アダプターは、作業を遅延して実行したり、周期的に実行したりするためのタイマーを作ることができ、しかもその処理を、アプリケーション・サーバーのスレッドを利用して同期的、あるいは非同期的に行うことができるのです。今回の記事では、トランザクション・インフローのサポートによって、リソース・アダプターがサーバーにインポートするトランザクションの中で処理がどう行われるのか、またリソース・アダプターは、どのようにしてトランザクションの完了を制御するのか、等に関して解説します。
この機能を既存のリソース・アダプターで利用することや、新しいJCA 1.5リソース・アダプターを書くことを考えている人達にとっては、この記事は必読です。また、リソース・アダプターを利用したアプリケーションを書く人にとっても、背後で起きていることを知るために興味深い記事となるはずです。
仕事を行わせる
このシリーズの第1回では、アプリケーション・サーバー内でリソース・アダプター自身にライフサイクルを与えるための機構として、ResourceAdapterインターフェースを紹介しました。皆さんは、BootstrapContextと呼ばれるオブジェクトがstartメソッドに渡されたことを覚えているでしょうか。前回はこのオブジェクトを無視しましたが、リスト1に示すBootstrapContextインターフェースにある3つのメソッドは、作業管理やトランザクション・インフロー契約にとって、重要な鍵なのです。
リスト1. 3つのユーティリティー・オブジェクトを取得するために使用するBootstrapContextインターフェース
public interface BootstrapContext {
WorkManager getWorkManager();
XATerminator getXATerminator();
Timer createTimer() throws UnavailableException;
}
|
WorkManagerは、アプリケーション・スレッド上で同期的、あるいは非同期的に作業を実行させるために、リソース・アダプターに対して作業のスケジューリングを行わせます。この作業は、リソース・アダプターがインポートするトランザクション内で行われます。この場合、XATerminatorが作業を完了させます。Timerは、作業を遅延して、あるいは周期的に実行させます。この記事では、こうしたクラスそれぞれを詳細に説明し、またその使い方についても説明します。
WorkManagerインターフェースは、作業処理用に、3セットのメソッド(doWorkとstartWork、そしてscheduleWork)を提供しています。これをリスト2に示します。
リスト2. 作業アイテムを実行依頼するためのWorkManagerインターフェース
public interface WorkManager {
void doWork(Work work) throws WorkException;
void doWork(Work work, long startTimeout, ExecutionContext execContext,
WorkListener workListener) throws WorkException;
long startWork(Work work) throws WorkException;
long startWork(Work work, long startTimeout, ExecutionContext execContext,
WorkListener workListener) throws WorkException;
void scheduleWork(Work work) throws WorkException;
void scheduleWork(Work work, long startTimeout,
ExecutionContext execContext, WorkListener workListener)
throws WorkException;
}
|
これらのメソッドはそれぞれ、最初のパラメーターとして、Workインターフェースを実装したオブジェクトのインスタンスをとります。これをリスト3に示します。
リスト3. リソース・アダプターが実装するWorkインターフェース
public interface Work extends Runnable {
void release();
}
|
WorkインターフェースはRunnableを継承します。作業は、Javaスレッドを直接プログラムする場合のように、runメソッドの中で実行されるように実装する必要があります。releaseメソッドをどこで使うかについては、すぐ後で説明します。
WorkManagerのdoWorkメソッドは、作業が完了するまでブロックすることによって、作業を同期的に実行させます。これは、格別便利には思えないかも知れません。これは、runメソッドを直接呼び出す場合に実際に起こることではないでしょうか。いや、少し違うのです。まずこれは、『今は作業を行うのに適当ではない』とアプリケーション・サーバーに言わせるのです。例えば、ResourceAdapter
startメソッドのスコープの間にdoWorkを呼ぶと、doWorkはWorkRejectedExceptionを投げることによって『必要ならば非同期処理用に作業をスケジュールし、すぐにこのメソッドから戻るように』と示すことがわかるでしょう。
第2に、アプリケーション・サーバーが特別に忙しい場合には、doWorkはこの作業の開始を遅らせるかも知れません。2番目の、startTimeoutパラメーターを使って、リソース・アダプターがどのくらい長く作業の開始を待てるかを規定することができます。もし、アプリケーション・サーバーがその時間内に作業を開始しなかった場合には、再度、WorkRejectedExceptionが投げられます。WorkManagerインターフェースは定数IMMEDIATEとINDEFINITEを定義し、リソース・アダプターに『全く待てない』、あるいは、『永遠に待ち続ける』と言わせるのです。
第3に、次のセクションで説明するように、作業を、現在のスレッドに関連付けられたコンテキストではなく、リソース・アダプターがインポートしたトランザクションのコンテキストで実行させることもできます。これが、3番目のパラメーターである、オプションのExecutionContextの目的です。
最後に、doWorkメソッドを使うと、アプリケーション・サーバーはリソース・アダプターが実行する作業に対して、より制御を効かせることができます。例えば、アプリケーション・サーバーがシャットダウンしようとする時に、リソース・アダプターが長く複雑なオペレーションを行っている最中の場合には、サーバーはリソース・アダプターが終わるまで待ったり、強制終了させたりする必要がありません。サーバーはWorkオブジェクトのreleaseメソッドを呼ぶことによって、リソース・アダプターに対して信号を送るのです。そうするとリソース・アダプターは、できるだけ早くプロセスを完了させようとします。リスト4は、releaseメソッドの使い方の一例を示しています。
リスト4. Workオブジェクトの一例
public class ExampleWork implements Work {
private volatile boolean _released;
void run() {
for (int i = 0; i < 100; i++) {
if (_released) break;
// Do something
}
}
void release() {
_released = true;
}
}
|
リスト4で、volatileキーワードの使い方に注意してください。runメソッドが処理される一方で、releaseメソッドが別スレッドで呼ばれます。volatile修飾子によって、runメソッドは更新されたフィールドを見るように保証されるのです。
doWorkメソッドは、ちょっと変わった名前のWorkCompletedExceptionによってもフェールします。この例外は、runメソッドにスレッドが割り当てられたもののコンテキスト設定がフェールした場合、あるいはランタイム例外を投げることでメソッドが終了した場合に投げられます。こうしたフェール・パスのどれが起きたのかを示すためにエラー・コードが提供されており、問題の原因は、リンク付き例外として提供されます。
なぜ待つのか?
doWorkメソッドを利用することによって、呼び出し側のスレッドをブロックする一方で、作業を行えることを見てきました。しかし、待ちたくない場合には、つまり、同期的に作業を行いたくない場合には、どうすれば良いのでしょう。J2SE(Java 2 Platform, Standard Edition)環境では、マルチスレッドを使ってこれを実現するでしょう。ところがJ2EEアプリケーションでは、アプリケーション・サーバーにJava 2セキュリティー・マネージャーを使わせ、アプリケーションがそのスレッドから外れないようにさせます。アプリケーション・サーバーの機能の中には、EJBやサーブレットのプールによって並行性を提供する、という機能もあるので、そうした動作も妥当なことと言えます。アプリケーション・サーバーはまた、様々な形式のコンテキストを、新しいスレッドができると失われるようなスレッドに関連付けようとします。最後に、先に議論した通り、サーバーの制御外のスレッドがあると、順当なシャットダウンが難しくなります。
こうした制約は多くの場合、セキュリティー・ポリシーを使うことによって、ケースバイケースでオーバーライドできるのですが、WorkManagerでのstartWorkメソッドとscheduleWorkメソッドを使うと、アプリケーション・サーバーが制御を保持したまま、リソース・アダプターが作業を非同期的に処理することができるのです。startWorkメソッドは、作業の実行が開始されるまで待ちますが、その完了までは待ちません。従ってこのメソッドは、『作業が実行されることは知る必要があるものの、終了は知る必要が無い』呼び出し側で使うことができます。対照的にscheduleWorkメソッドは、処理用の作業が受け付けられ次第、返ります。この場合は、その作業が実際に実行されるという保証はありません。
リスト2の中にある、WorkManagerメソッド4番目のパラメーター、WorkListenerが最も便利なのは、こうした非同期メソッドで利用した場合でしょう。リスト5は、このインターフェースを示しています。
リスト5. イベント通知を受け取るためのWorkListenerインターフェース
public interface WorkListener {
void workAccepted(WorkEvent e);
void workRejected(WorkEvent e);
void workStarted(WorkEvent e);
void workCompleted(WorkEvent e);
}
|
リソース・アダプターはオプションとして、作業のアイテムが各状態、つまり受け付け(accepted)、開始(started)、完了(completed)、あるいはフェールの場合には拒否(rejected)、を通過したことの通知を受けるリスナーを渡すことができます。このリスナーを使って、作業のオリジネーターに対して、作業が完了したら通知を返したり、あるいは、フェールした場合には作業アイテムを再スケジュールさせたりすることができます。全メソッドのデフォルト実装を提供するWorkAdapterクラスも含まれており、サブクラスは、対象とするもののみをオーバーライドすれば良いようになっています。
WorkListenerの各メソッドには、WorkEventオブジェクトというパラメーターをとります。これをリスト6に示します。
リスト6. WorkEventクラスにある、他のメソッド
public class WorkEvent extends EventObject {
...
public int getType() { ... }
public Work getWork() { ... }
public long getStartDuration() { ... }
public WorkException getException() { ... }
}
|
WorkEventクラスには、通常のイベント・メソッドの他に、イベント・タイプ(accepted、rejected、started、あるいはcompleted)と対象作業アイテムに対する、アクセサー(accessors)を提供しています。そのため、複数の作業実行依頼に対して、単一の(マルチスレッドの)リスナーを使うことができます。また、作業の処理を開始するために要した時間を返すメソッドもあり、workRejectedやworkCompletedの場合であれば、発生したWorkRejectedExceptionあるいはWorkCompletedExceptionを返すメソッドもあります。
図1は、Workオブジェクトが通過する状態を示しています。
図1. Workオブジェクトに対する状態図
図1の下の方には3つのタイプの実行依頼メソッドがあり、縦の破線は、そのメソッドが返るべき(ライフサイクル上の)点を示しています。
明日に延ばせることを、なぜ今するのか?
doWork、startWork、scheduleWorkの各メソッドはどれも、WorkManagerに対して作業を即時に実行依頼します。WorkManagerが実行依頼を受け付けるまでに遅延が発生する可能性がありますが、呼び出し側は、(開始タイムアウトを使って)最大遅延を制御することしかできません。作業を、今すぐではなく、後で処理したいとしたらどうでしょう。scheduleWorkメソッドでは、別スレッド上で作業をスケジュールことはできますが、将来のいつかにスケジュールすることはできません。
ここに、BootstrapContextインターフェースの3番目のメソッドが登場するのです。createTimerメソッドを利用すると、リソース・アダプターはjava.util.Timerクラスのインスタンスを取得することができます。リスト7に示すこのクラスは、バージョン1.3の時から、標準Javaライブラリーの一部となっています。
リスト7. Timerクラスでのメソッド
public class Timer {
public void schedule(TimerTask task, long delay) { ... }
public void schedule(TimerTask task, Date time) { ... }
public void schedule(TimerTask task, Date firstTime, long period) { ... }
public void schedule(TimerTask task, long delay, long period) { ... }
public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) { ... }
public void scheduleAtFixedRate(TimerTask task, long delay, long period) { ... }
public void cancel() { ... }
}
|
java.util.Timerクラス上の、最初の2つのメソッドを使って、規定の遅延後にタスクが起こるようにしたり、指定した日時に起こるようにしたりすることができます。残る4つのスケジュール・メソッドには、さらにperiodパラメーターがあります。これらを使うと、最初の実行後、定期的に起こるべきイベントを、periodで周期を規定することによってスケジュールすることができます。scheduleメソッドとscheduleAtFixedRateメソッドは、操作のタイミングが保証されていないため、両者は異なります。例えばガーベジ・コレクションのような操作によって、タスクの実行が遅れる場合があります。もしタスクが遅延されると、scheduleメソッドは相変わらず、次のタスクを実行するまで一周期を丸々待ちます。ところがscheduleAtFixedRateメソッドは、その前のタスクを実行するはずであった時から、固定周期の時間後に次のタスクを実行するのです。従って、タスク間の時間間隔が重要な場合には、scheduleを使います。絶対時間や、累積時間が重要であれば、scheduleAtFixedRateを使います。
それぞれのscheduleメソッドは、最初のパラメーターとしてTimerTaskクラスを継承したオブジェクトを取ります。Workインターフェースの場合と同様、このクラスはRunnableを継承し、Timerは適当な時にrunメソッドを呼び出します。TimerTaskクラスにはcancelメソッドがあり、後に続くタスク呼び出しは、これを使ってキャンセルすることができます。あるいは、Timer上のcancelメソッドを呼び、現在スケジュールされている全タスクをキャンセルすることもできます。scheduledExecutionTimeメソッドを使うと、現在の実際の時間と、呼ばれるべきであった時間とをrunメソッドに比較させることができます。
TimerTaskのrunメソッドは新しいスレッドで呼び出されますが、このスレッドはJVMによって割り当てられており、アプリケーション・サーバーの制御外です。リソース・アダプターが何らかの重要な処理をするような場合には、この時点でリソース・アダプターはWorkManagerを使って、アプリケーション・サーバーのスレッドに切り換える必要があります。リスト8は、この良い習慣を使って、現在の時間以降、毎分、作業が実行されるようにスケジュールする例を示しています。
リスト8. TimerとWorkManagerを組み合わせて使う
final WorkManager workManager = context.getWorkManager();
final Timer timer = context.createTimer();
timer.scheduleAtFixedRate(new TimerTask() {
public void run() {
workManager.scheduleWork(new ExampleWork());
}
}, 0, 60 * 1000);
|
JCAのWorkManagerとTimerに代わるもの
先のセクションで触れたように、JCAでTimerを使うには、問題があります。つまり、TimerTaskが実行されるスレッドは、アプリケーション・サーバーの制御外なのです。Timerはインターフェースではなくクラスであり、スレッドは実際のところ、そのクラスのコンストラクターの中で生まれるので、アプリケーション・サーバーは、この振る舞いを修正することができません。アプリケーション・サーバーが割り込めるようにするための選択肢としては、IBMとBEAが共同開発したTimer and Work Manager for Application Servers 仕様(参考文献)からのインターフェースを使用する方法です。
この仕様のインターフェースを使う場合には、JNDI(Java Naming and Directory Interface)名前空間からTimerManagerの実装を入手します。TimerListenerの実装は、Timer(java.util.Timerではなく、commonj.timers.Timer)に代わってTimerManagerによってスケジュールされます。TimerListenerは、スケジュールされた時間に呼び出され、Timerは、例えばスケジュールした操作をキャンセルするといった操作を実行するために使用します。
名前から分かるように、この仕様は、JCA作業管理に対する代替手段にもなります。この仕様でのインターフェースはJCAのものと非常に似ていますが、最初のcommmonj.work.WorkManagerはJNDIから取得するところは異なります。この契約が提供している、他の振る舞いには、スケジュールされた作業(1つまたは複数)が完了するまでブロックする機能や、シリアル化可能な作業をリモートJVM上で実行できる機能などがあります。commonjTimerの場合と同様、commonjWorkManagerも、リソース・アダプターに対してしか使えないわけではありません。サーバー側J2EEコンポーネントであれば、どんなものでも、この機能を使うことができます。
リスト9は、リスト8の例を、commonjクラスを使うように書き直したらどうなるかを示しています。
リスト9. commonjインターフェースを使う
final InitialContext context = new InitialContext();
final WorkManager workManager = (WorkManager) context.lookup("java:comp/env/wm/MyWorkManager");
final TimerManager timerManager = (TimerManager)
context.lookup("java:comp/env/timer/MyTimer");
timerManager.scheduleAtFixedRate(new TimerListener() {
public void timerExpired(final Timer timer) {
try {
workManager.schedule(new ExampleCommonjWork());
} catch (final WorkException exception) {
}
}
}, 0, 60 * 1000);
|
ExampleCommonjWorkクラスは、リスト4のExampleWorkクラスと同じですが、commonjWorkインターフェースが要求するisDaemonメソッドを追加実装している点が異なります。このメソッドは、作業のライフが長い(long-lived)場合にはtrueを返すはずです。
トランザクションをインポートし、完了する
JCA 1.5までは、アプリケーション・サーバーは常にトランザクションのコーディネーターとして動作していました。アプリケーション・サーバーはトランザクションの開始に責任を持ち、また、各リソース・マネージャーがそのJCAコネクション・マネージャーを介してXAResourceをリストした後は、全リソースを一緒にコミットしたり、ロールバックしたりすることによってトランザクションの完了を調整することに責任を持ちました。JCA 1.5のトランザクション・インフロー契約では、EIS(enterprise information system)が、(トランザクションを開始、完了することによって)トランザクション・コーディネーターとして動作します。そうするとEISは、リソース・アダプターを介してアプリケーション・サーバーにトランザクションをインポートし、そのトランザクションの下で、サーバー内で作業を実行します。例えばEISは、コンテナー・マネージャーのトランザクション属性、Supportsを持ったMDB(message-driven bean)を呼び出すかも知れません。そうするとMDBメソッドが行う作業は、他のEJBへのコールを含めて、そのトランザクションの一部となります。
リソース・アダプターは、皆さんがリスト2でWorkManagerに渡されるのを見た、あの3番目のExecutionContextパラメーターを介して、トランザクションをインポートします。リスト10で分かる通り、このクラスは、トランザクションID(XID)とトランザクション・タイムアウトを設定するためのメソッドを持っています。
リスト10. WorkManagerに渡されるExecutionContextクラス
public class ExecutionContext {
public ExecutionContext() { ... }
public void setXid(Xid xid) { ... }
public Xid getXid() { ... }
public void setTransactionTimeout(long timeout)
throws NotSupportedException { ... }
public long getTransactionTimeout() { ... }
}
|
XIDはトランザクションを固有識別するものであり、フォーマット識別子、トランザクション修飾子、分岐修飾子という3つの部分から成っています。EISがトランザクション用のXIDを持っていない場合には、XA仕様(参考文献)に従ってXIDを構築する必要があります。そうするとアプリケーション・サーバーは、作業オブジェクトのrunメソッドを呼ぶ前に、このトランザクションを実行スレッドに関連付けます。
アプリケーション・サーバーにトランザクションをインポートした後は、リソース・アダプターは、そのトランザクションに関わるイベントをサーバーに通知する責任を持ちます。特に、トランザクションの完了をアプリケーション・サーバーに通知する必要があります。リソース・アダプターはこれを、リスト11に示すXATerminatorインターフェースを使って行います。XATerminatorインターフェースの実装は、BootstrapContextから得ることができます。
リスト11. トランザクション完了に使われるXATerminatorクラス
public interface XATerminator {
void commit(Xid xid, boolean onePhase) throws XAException;
void forget(Xid xid) throws XAException;
int prepare(Xid xid) throws XAException;
Xid[] recover(int flag) throws XAException;
void rollback(Xid xid) throws XAException;
}
|
XATerminatorインターフェースのメソッドは、XAResourceのメソッドと一致した場合にのみ、リソース・アダプターがアプリケーション・サーバーを呼び出します。トランザクションに1つ以上のリソースが関係する場合には、リソース・アダプターは通常、XATerminator上のprepareを呼び、ExecutionContextに渡したのと同じXidを渡します。全てのリソースがXA_OK(または、XA_RDONLY)を返した場合には、リソース・アダプターはcommitを呼ぶところに進みます。それ以外の場合には、rollbackを呼びます。
まとめ
この記事では、WorkManagerインターフェースを使って、アプリケーション・サーバーの制御下のスレッド上で、同期的あるいは非同期的に、作業処理をスケジュールする方法を説明しました、Timerインスタンスを使って、作業を後で実行する方法や、周期的に実行する方法を学び、JCAが提供するオブジェクトを使う代わりにcommonjを使う方法も見てきました。また、アプリケーション・サーバーにインポートしたトランザクションの中で、リソース・アダプターがどのように作業を実行するか、そしてXATerminatorインターフェースを使ってトランザクション完了をどう制御するかについても学びました。このシリーズ最後である、次回の第3回では、むしろメッセージ駆動beanサポートとして知られている、JCA 1.5のメッセージ・インフロー契約(message-inflow contract)について解説します。
参考文献
著者について  | 
|  | David Currieは最近、イギリスのHursley にある、IBM Software Services for WebSphere組織に加わりました。それまではWebSphere Application Serverの開発に携わっていました。当初はトランザクションの領域で、また、ごく最近では、メッセージングをサポートするJCAリソース・アダプターの設計や実装を行ってきています。連絡先はdavid_currie@uk.ibm.comです。 |
記事の評価
|