はじめに
Oracle Saga Frameworkは、Oracle Databaseでマイクロサービス・アプリケーション用のsagaを実装および管理するフレームワークです。
Oracle Databaseをプラットフォームとして、Sagaベースのアプリケーションを構築することができます。
私自身マイクロサービスはまだまだ勉強中の身で試行錯誤ですが、中間層なしでできるPL/SQLを使ってどんなものか試してみました。
その1:設定編で、設定まで実施したので、この記事では実際にSagaを実行してみます。
1.Sagaを実行(成功のケース)
旅行代理店サービスで航空券を航空会社United、ホテルの部屋タイプをtwinを指定して予約します。Unitedの座席の残数は10席、ホテルのTwinの部屋の残数は1部屋ある状態です。
-- @AirlinePDB
SQL> connect admin/WElcome12345##@AirlinePDB
-- flightsを検索する
SQL> SELECT * FROM flights;
FLIGHT SEATS
---------- ----------
United 10
Delta 5
-- @HotelPDB
SQL> connect admin/WElcome12345##@HotelPDB
-- roomsを検索する
SQL> SELECT * FROM rooms;
ROOMTYPE CNT
-------------------- ----------
twin 1
single 3
イニシエータであるTravelAgencyPDBでDBMS_SAGA.BEGIN_SAGAを使ってSagaを開始し、開始したSagaにSEND_REQUESTで航空券予約サービスの参加者(Airline)を登録します。以下のコードで実行します。
-- @TravelAgencyPDB
connect admin/WElcome12345##@TravelAgencyPDB
DECLARE
saga_id RAW(16);
request JSON;
begin
saga_id := DBMS_SAGA.BEGIN_SAGA(
initiator_name=>'TravelAgency', timeout => 900);
request := JSON('{"flight":"United","room":"twin"}');
DBMS_SAGA.SEND_REQUEST(saga_id, 'Airline', request);
END;
/
実行状況をチェックするための表を作成していたのでそちらを確認してみます。
-- @AirlinePDB
SQL> connect admin/WElcome12345##@AirlinePDB
Connected.
SQL> SELECT * FROM flighttest;
SENDER MESSAGE
-------------------- ----------------------------------------------------------------------------------------------------
TRAVELAGENCY {"flight":"United","room":"twin"}
-- @HotelPDB
SQL> connect admin/WElcome12345##@HotelPDB
SQL> SELECT * FROM roomtest;
SENDER MESSAGE
-------------------- ----------------------------------------------------------------------------------------------------
TRAVELAGENCY {"flightresult":"success","flight":"United","room":"twin"}
-- @TravelAvencyPDB
SQL> connect admin/WElcome12345##@TravelAgencyPDB
Connected.
SQL> SELECT * FROM travelagencytest;
SENDER MESSAGE
-------------------- ----------------------------------------------------------------------------------------------------
AIRLINE {"flightresult":"success","flight":"United","room":"twin"}
HOTEL {"roomresult":"success","flightresult":"success","flight":"United","room":"twin"}
TravelAgencyから送信されたメッセージがAirlineで受信されています。
Airlineで処理され、"flightresult":"success"を加えたメッセージがTravelAgencyで受信されています。HotelPDBはTravelAgencyからそのメッセージを受信し、処理した結果"roomresult":"success"を加えたメッセージがTravelAgencyで受信されています。
どちらも予約も結果はsuccessなので、航空券とホテルの管理表を確認してみます。
-- @AirlinePDB
SQL> connect admin/WElcome12345##@AirlinePDB
Connected.
SQL> SELECT * FROM flights;
FLIGHT SEATS
---------- ----------
United 9
Delta 5
-- @HotelPDB
SQL> connect admin/WElcome12345##@HotelPDB
SQL> SELECT * FROM rooms;
ROOMTYPE CNT
-------------------- ----------
twin 0
single 3
各サービスで席と部屋数が1ずつ減っていることが確認できました。ホテルのtwinタイプの部屋数は0になったため、これ以上の予約はできません。
2.Sagaを実行(補償トランザクションのケース)
すべてのチェック表をtruncateコマンドでクリアしてから、もう1度同じSagaを実行してみます。
-- @TravelAgencyPDB
connect admin/WElcome12345##@TravelAgencyPDB
DECLARE
saga_id RAW(16);
request JSON;
begin
saga_id := DBMS_SAGA.BEGIN_SAGA(
initiator_name=>'TravelAgency', timeout => 900);
request := JSON('{"flight":"United","room":"twin"}');
DBMS_SAGA.SEND_REQUEST(saga_id, 'Airline', request);
END;
/
成功のケースと同じように実行状況を確認します。
-- @AirlinePDB
SQL> connect admin/WElcome12345##@AirlinePDB
Connected.
SQL> SELECT * FROM flighttest;
SENDER MESSAGE
-------------------- ----------------------------------------------------------------------------------------------------
TRAVELAGENCY {"flight":"United","room":"twin"}
-- @HotelPDB
SQL> connect admin/WElcome12345##@HotelPDB
SQL> SELECT * FROM roomtest;
SENDER MESSAGE
-------------------- ----------------------------------------------------------------------------------------------------
TRAVELAGENCY {"flightresult":"success","flight":"United","room":"twin"}
-- @TravelAvencyPDB
SQL> connect admin/WElcome12345##@TravelAgencyPDB
Connected.
SQL> SELECT * FROM travelagencytest;
SENDER MESSAGE
-------------------- ----------------------------------------------------------------------------------------------------
AIRLINE {"flightresult":"success","flight":"United","room":"twin"}
HOTEL {"roomresult":"no-room"}
成功のケースと同じようにAirlineのほうは"flightresult":"success"ですが、TravelAgencyがHotelから受信したメッセージは"roomresult":"no-room"となっています。ホテルのtwinタイプの部屋数は0だったからです。
続いて、航空券と部屋の管理表を確認してみます。
-- @AirlinePDB
SQL> connect admin/WElcome12345##@AirlinePDB
Connected.
SQL> SELECT * FROM flights;
FLIGHT SEATS
---------- ----------
United 9
Delta 5
-- @HotelPDB
SQL> connect admin/WElcome12345##@HotelPDB
SQL> SELECT * FROM rooms;
ROOMTYPE CNT
-------------------- ----------
twin 0
single 3
どちらの表も更新が行われていません。flights表の更新はsucesssでしたが、room表の更新ができなかったことにより、flights表の更新をロールバックする補償トランザクションが自動的に実行されたということになります。
これは、更新対象の列(seats)をロックフリー予約で指定したことで実現されています。ロックフリー予約を使用するとその列に対する補償トランザクションが自動的に実行されます。補償トランザクションのためのコードの記述が不要なのでSagaのコードがシンプルになります。
もちろん、アプリケーションによっては、ロックフリー予約ではなく独自の補償トランザクションの処理が必要な場合があります。そのときはコールバックパッケージでBEFORE_ROLLBACKメソッド、AFTER_ROLLBACKメソッドを用いて、明示的に補償トランザクションのための処理をコードに記述することができます。
3.Sagaの情報確認
その1:設定編で確認したSaga参加者を表示するCDB_SAGA_PARTITIONSのほかにもOracle DatabaseにはOracle Saga Frameworkのためのディクショナリが用意されています。いくつか確認してみます。
ここではCDB_xxxで確認していますが、DBA_xxx、USER_xxxでも確認可能です。
アクティブなSaga
CDB_SAGASはデータベース内のすべてのアクティブなSagaを表示します。
connect / as sysdba
SQL> SELECT * FROM CDB_SAGAS ORDER BY TO_TIMESTAMP_TZ(start_time);
ID INITIATOR IS_ COORDINATOR OWNER PARTICIPANT STATUS VERSION DURATION START_TIME COMPLETION_TIME CON_ID
----------------------------------- --------------- --- --------------- ---------- --------------- -------------- ---------- ---------- ----------------------------------------------- --------------- ----------
00FDB4DDC5852EA7E0633C00640ABCC6 TRAVELAGENCY NO TACOORDINATOR ADMIN AIRLINE Joined 1 900 21-JUL-23 10.27.06.455251 AM +00:00 5
他にSagaの状況に応じて以下のディクショナリで確認ができます。
- CDB_SAGA_DETAILS : データベース内のすべてのSagaの詳細
- CDB_SAGA_PENDING : データベース内のすべての保留中のSaga
- CDB_SAGA_FINALIZATION : データベース内のすべてのSagaの保留中のファイナライズアクション
- CDB_INCOMPLETE_SAGAS : データベース内のすべての未完了のSaga
Sagaの履歴
CDB_HIST_SAGASはデータベース内で完了したすべてのSagaの履歴を表示します。SAGA_HIST_RETENTION初期化パラメータで指定された期間(デフォルト30日)保持されます。
connect / as sysdba
SQL> SELECT id,participant,status,TO_TIMESTAMP_TZ(start_time) startdate,con_id FROM CDB_HIST_SAGAS ORDER BY startdate;
ID PARTICIPANT STATUS STARTDATE CON_ID
----------------------------------- --------------- -------------- --------------------------------------------------------------------------- ----------
00FE414767EB564DE0633C00640AEEF4 TRAVELAGENCY Committed 21-JUL-23 11.06.22.188277000 AM +00:00 4
00FE414767EB564DE0633C00640AEEF4 HOTEL Committed 21-JUL-23 11.06.22.188277000 AM +00:00 6
00FE414767EB564DE0633C00640AEEF4 AIRLINE Committed 21-JUL-23 11.06.22.188277000 AM +00:00 5
011C5DFBDF9E266EE0633C00640A6F78 TRAVELAGENCY Rolledback 22-JUL-23 11.01.52.791887000 PM +00:00 4
011C5DFBDF9E266EE0633C00640A6F78 AIRLINE Rolledback 22-JUL-23 11.01.52.791887000 PM +00:00 5
011C5DFBDF9E266EE0633C00640A6F78 HOTEL Rolledback 22-JUL-23 11.01.52.791887000 PM +00:00 6
Sagaのエラー
CDB_SAGA_ERRORSはデータベースのすべてのSagaによって生成されたエラーを表示します。以下はTravelAgencyのチェック表のmessage列のサイズを小さくして明示的にエラーを発生させた場合の表示です。
connect / as sysdba
SQL> SELECT * FROM CDB_SAGA_ERRORS ORDER BY TO_DATE(error_time,'yy-mm-dd hh24:mi:ss');
SAGA_ID PARTICIPANT SQL_ERROR SQL_COD ERROR_TIME CON_ID
-------------------------------- --------------- ---------------------------------------------------------------------------------------------------- ------- ------------------- ----------
011DC834B36D8F16E0633C00640A90B6 ORA-12899: value too large for column "ADMIN"."TRAVELAGENCYTEST"."MESSAGE" (actual: 58, maximum: 20) -1289 2023-07-23:00:43:11 4
4.おわりに
Oracle Database 23cで提供されたOracle Saga FramworkをPL/SQLで試してみました。データベースにSagaの実装が統合されるので、Sagaの運用管理が容易になり、データベースの整合性、可用性や拡張性をマイクロサービスの基盤として生かすことができます。PL/SQLだけでなく、OSagaインタフェースを使うことでJavaで実装可能ですので、触ってみていただければと思います。