Gauche:ObjectPrevalence
Rui (2005/07/10 02:55:37 PDT): Object Prevalenceはシンプルなオブジェクト永続化メカニズムの一つです。その実装の一つであるdb.prevalenceは、汎用的なデータ構造をそのまま永続化できる透過的なメカニズムを提供します。
(2005/08/17 04:49:34 PDT) 整理してからと考えていましたが延び延びになってしまったので、とりあえず公開することにします。http://t-code.org/prevalence.scm 。
コメント
- 今作ってるプログラムで使ってみたいのですが、ライセンスはGauche互換でいいのでしょうか。 -- ひらっち
- Rui(2006/11/23 17:16:44 PST): はい。Gaucheと同じ宣伝条項なしBSDライセンスでお願いします。上記のファイルは置き換えておきました。ただしまだマジメな用途には使えないと思います。いまの実装では書き出したオブジェクトが決してGCされません(Gaucheにweak hash tableが実装されるまでこの制限は直らないでしょう)。いずれにせよ巨大なデータセットを扱うためのものではないので遊びで使う分には問題ないと思いますが。質問などあれば訊いてください。
- ありがとうございます。今のところは実用的な目的ではありません。とりあえずGaucheでこんなことができるよ、というデモの目的が大きいです。公開するかはまだ微妙。 -- ひらっち
Object Prevalence
まず、Object Prevalenceについて説明したほうがよいでしょう。Object Prevalenceは次のアイデアを基本としています。
- すべての永続オブジェクトはメモリ上に存在する
- 永続オブジェクトはシリアライズ可能でなければならない
- すべての永続オブジェクトが、定期的にファイルに書き出される(保存したものを「スナップショット」という)
- スナップショットを取得した後の永続オブジェクトに対する変更は、トランザクションログに保存される
永続化したオブジェクトをファイルから取り出すには、まずスナップショットを読み込み、その後トランザクションログを再実行します。これにより、すべてのオブジェクトを以前と同じ状態で再度手にすることができます。
db.prevalence
db.pervalenceでは、保存対象となる永続オブジェクトは次のルールに従い決定されます。
- 「ルートオブジェクト」は永続オブジェクトである
- 永続オブジェクトの永続スロットから参照されている永続化可能オブジェクトは、永続オブジェクトである
上記のルールから導かれる結果として、ルートオブジェクトから到達可能なオブジェクトはすべて保存され、後でまたルートオブジェクトから芋づる式に辿ってアクセスすることができます。永続オブジェクト間には循環を含むグラフ構造が含まれていてもかまいません。
永続化可能オブジェクトは、文字列、数、シンボルなどのread/write不変なオブジェクトか、あるいは<prevalence-base>のサブクラスのオブジェクトでなければなりません。
ルートオブジェクトから到達できないオブジェクトは、たとえそれが<prevalence-base>を継承していたとしても、保存されません。ルートから到達不可能になった永続オブジェクトは、それに対する変更はトランザクションログに記録されますが、次のスナップショットの取得時にオブジェクトは保存されません。オブジェクトへの参照が存在しなくなりGCされるか、あるいはプロセスをシャットダウンしたときに、そのオブジェクトは完全に失われます。
API
[class] <prevalence-system>
Prevalenceシステムを表すクラスです。
[class] <prevalence-meta>
永続化可能なオブジェクトのメタクラスとなるクラスです。このクラスをメタクラスとして用いると、スロット定義の:allocationキーワード引数に:persistentというキーワードを与えることができるようになります。:persistentキーワードが与えられると、そのスロットは永続スロットになり、オブジェクトがルートオブジェクトから到達可能な場合に下記の特別な扱いを受けるようになります。
- そのスロットへの代入がトランザクションログに記録されます。
- 代入されたオブジェクトが<prevalence-base>のサブクラスである場合、そのオブジェクトの属するシステムとして自分自身の属するシステムを追加します。(あるシステムのルートから到達可能であるという性質が感染ると考えるとわかりやすいでしょう。)
永続オブジェクトは同時に複数のシステムに所属することができます。複数のシステムに所属している場合、変更はそのすべてのシステムのログに記録され、スナップショットも同様にそのすべてのシステムに対して記録されます。
[class] <prevalence-base>
永続化可能なオブジェクトの親クラスとなるクラスです。このクラスのサブクラスは<prevalence-meta>をメタクラスとして持ちます。
[class] <persistent-hash-table>
ハッシュテーブルをラップするクラスです。<prevalence-base>のサブクラスであり、ハッシュのキーや値は自動的に永続化されます。hash-table-get、hash-table-set、hash-table-updateなどの手続きは、<persistent-hash-table>を認識するジェネリック関数にオーバーライドされます。
[procedure] (make-prevalence-system path &optional rootobj)
Prevalenceシステムを作成する手続きです。pathにすでにスナップショットファイルやトランザクションログファイルが存在する場合、そこに保存されているオブジェクトがロードされます。存在しない場合は、rootobjをルートオブジェクとするシステムが新たに作成されます。rootobjを与えなかった場合のデフォルトのルートオブジェクトは、<persistent-hash-table>のインスタンスです。
[procedure] system-close
システムのトランザクションログファイルをクローズします。クローズされたシステムに属するオブジェクトを変更しようとするとエラーが発生します。
[macro] with-transaction system body ...
ログへのアトミックな書き込みを保証するマクロです。bodyを実行しているあいだにsystemに対して行われた変更は、bodyから抜ける時点で一度にログに書き込まれます。ログへの書き込みが中途半端に終わった場合、リストア後の状態は''body'が実行される前か実行された後のどちらかであり、変更途中の状態が再現されることはありません。実際には、リストア時に書き込み中のトランザクションが見つかった場合に、その直前にログファイルを切り詰めた上で再度リストアを実行することで、記録のアトミックなリストアを保証しています。
RDBMSに見られるロールバックを提供しているわけではないことに注意してください。ロールバックが必要ならば、エラーの際に自分で変更を元に戻してください。
[macro] with-system (var system) body' ...
current-systemパラメータの値をsystemに変更し、bodyを実行、その後system-closeを呼ぶマクロです。エラーが発生した場合にもsystem-closeは呼び出されます。
[procedure] set-root-object! system obj
objをルートオブジェクトにします。
[procedure] get-root-object system
システムののルートオブジェクトを取得します。
[procedure] snapshot system
現在のシステムのスナップショットを書き出します。スナップショットの取得中、永続オブジェクトに変更を加えてはいけません。
例
名前とフレンドリストを持つ<person>クラスを永続クラスとして生成し、それを保存して再度取り出す例です。永続オブジェクトの保存や読み込みをプログラマが意識しなくてよいのがわかると思います。
(use prevalence) ;; 永続クラス (define-class <person> (<prevalence-base>) ((name :allocation :persistent :init-keyword :name) (friends :allocation :persistent :init-keyword :friends :init-value '()))) (define-method write-object ((obj <person>) out) (format out "#<<person> ~a ~S>" (ref obj 'name) (ref obj 'friends))) ;; 永続クラスのインスタンスを生成する。ここでは互いを参照する ;; 2つのインスタンスを作る (let* ((rui (make <person> :name "rui")) (sys (make-prevalence-system "/tmp/db" rui))) (set! (ref rui 'friends) (list (make <person> :name "jun" :friends (list rui)))) (system-close sys)) ;; 保存したオブジェクトを読み出す (let ((sys (make-prevalence-system "/tmp/db"))) (write/ss (get-root-object sys)) (newline)) ;; 出力は次のようになる。 ;; #0=#<<person> rui (#<<person> jun (#0#)>)>
その他の実装
- Java - Prevayler
- Common Lisp - CL-PREVALENCE
- .NET - Bamboo.Prevalence
- Ruby - Madeleine
- Python - Pypersyst
上記の実装に存在する「トランザクションオブジェクト」は、db.prevalenceには存在しません。トランザクションオブジェクトは、永続オブジェクトに対する変更操作を表すコマンドオブジェクトですが、db.prevalenceでは変更するものではなく変更そのものが記録されるため、明示的なトランザクションオブジェクトの生成が必要ないからです(これはBamboo.Prevalenceから借りたアイデアです)。
トランザクションオブジェクトは、永続オブジェクトへのアクセスをシリアライズするためにも使われます。しかし、永続オブジェクトへのアクセスがすべて競合するというわけではないはずで、これは別のロックを使って排他制御すればよい問題だと考えています。
課題
以下のものが必要でしょうか。
- レプリケーション
- クラスの世代管理
- BLOB (実体のファイルが別のどこかに保存されるようなオブジェクト。db.prevalenceでは本質的に巨大なデータを扱うことができないので、このようなものがあると便利かもしれない。CL-Prevalenceにはある)
レプリケーションはシステムを停止せずにスナップショットを取得するために必要です。トランザクションログを出力するメインのプロセスとそれを読み込むサブのプロセスという構成にし、スナップショットはサブのプロセスだけで出力することにより、無停止でのスナップショットをとることができます。(スナップショットを取るまえにforkして、子プロセスがスナップショットを書き出すというのを思いつきましたが、それだと親プロセスの書き出すトランザクションログがスナップショットの続きにならない(コンテキストを共有しない)ので無理です。)
BLOBではいつファイルを消すのかが問題になるでしょう。明示的な削除操作は避けたいところ。スナップショットの作成のようにすべての永続オブジェクトの更新を止めてBLOBをスキャン、参照されていないファイルを消すというのがいいでしょうか。
雑記
自分のアプリケーションのために上記のものを作ってます。アプリケーション本体はビジュアルなインタフェースを持たないサーバプロセスで、データ(せいぜい10万オブジェクトとか)を全部db.prevalenceで管理して、管理用Webアプリケーション(Kahuaで書く)はTCPでサーバプロセスに接続、REPLから永続オブジェクトにアクセスするということを考えてます。