Pager.c を読んでみる (1)

SQLiteの内部を見てみたときのメモというエントリ。SQLiteは、コメントが充実していて凄いと思う。手始めに、ロールバックやアトミックな書き込みを提供しているということなので、Pager.cから始めてみる。

**                               OPEN <------+------+
**                                  |             |            |
**                                  V                |            |
**               +---------> READER------+         |
**               |                          |                               |
**               |                          V                              |
**               |<-------WRITER_LOCKED------> ERROR
**               |                          |                               ^  
**               |                          V                               |
**               |<------WRITER_CACHEMOD----- ->|
**               |                          |                                |
**               |                         V                                |
**               |<-------WRITER_DBMOD--------->|
**               |                          |                                |
**               |                         V                                |
**               +<------WRITER_FINISHED-------->+
**
**
** List of state transitions and the C [function] that performs each:
** 
**   OPEN              -> READER              [sqlite3PagerSharedLock]
**   READER            -> OPEN                [pager_unlock]
**
**   READER            -> WRITER_LOCKED       [sqlite3PagerBegin]
**   WRITER_LOCKED     -> WRITER_CACHEMOD     [pager_open_journal]
**   WRITER_CACHEMOD   -> WRITER_DBMOD        [syncJournal]
**   WRITER_DBMOD      -> WRITER_FINISHED     [sqlite3PagerCommitPhaseOne]
**   WRITER_***        -> READER              [pager_end_transaction]
**
**   WRITER_***        -> ERROR               [pager_error]
**   ERROR             -> OPEN                [pager_unlock]

Pager.eStateの状態遷移表が上側。下は、各状態遷移に関して、アクションとしての関数が右に記述されている。

OPEN

この時点では、read/writeいずれのトランザクションも実行されておらず、pagerに対するロックも何もしていない。よって、データベースサイズなどいった情報も信用できない。

READER

この状態は、ロールバックモードにおけるデータベースへの読み込み全ての要求に適合する(良く解らん、とりあえず、共有ロックをとってこのフェーズに飛んできてる。)。排他ロックがこのページにかかってなければ、ユーザレベルの読み込みトランザクションは、 開始する。lock_modeがnormalである場合で、リードトランザクションの場合、この状態に入る。そして、リードトランザクションが終了すると、OPENの状態へ遷移する。ただし、lock_modeがexclusiveの場合、リードトランザクションが終わったとしてもREADER状態のままとなる。唯一の例外は、ERROR状態経由でREADER->OPENになる。

  • リードトランザクションは許可されている(ライトトランザクションは許可されない)
  • データベースファイルに対して、少なくとも共有ロックをもつ。
  • dbSize変数は、信用してよい。dbOrigSizeとdbFileSizeは、信用してはならない。
  • データベースがWALデータベースなら、WALの接続がオープンされている。
  • リードトランザクションが開かれてない場合でさえ、ファイルシステムはhot-journalがないことが保証される(今のところ、なんのこっちゃ?hot-journal?)
WRITER_LOCKED

READERからWRITER_LOCKEDに遷移するのは、データベース上で最初にライトトランザクションが呼ばれた場合。この状態においては、ライトトランザクションが実行開始するために必要なロックを全て取得するが、キャッシュやデータベースに対する実際の更新アクションは行っていない。

この状態に移動したときには、RESERVEDorEXCLUSIVE lockが取得される。ただし、journalファイルには何も書かれてないし、開いてもいない。この状態において、トランザクションロールバックかコミットすれば、要求されていたデータベースへのロックは解除される。

WALモードでは、ログファイルをロックするために、WalBeginWriteTransaction()が呼び出される。もし、locking_mode=exclusiveで実行されていると、データベースファイルに対してEXCLUSIVE lockを取るように試みる。

  • ライトトランザクションは、アクティブ
  • ロールバックモードで接続が開かれている場合には、データベースファイルに対して、RESERVEDか、より強いロックが取得済み
  • WALモードで接続が開かれている場合、WALライトトランザクションが開いている。sqlite3WalBeginWriteTransaction()が、成功済み
  • dbSize, dbOrigSize, dbFileSizeは、全て妥当。
  • pager cacheの内容は変更していない。
  • journal fileは、オープンしているかどうかは分からない。
  • journalには何も書かれていない。
WRITER_CACHEMOD:

上位レイヤでページが最初に更新されたときに、ベージの状態は、WRITER_LOCKEDから、WRITER_CACHEMODへ遷移する。ロールバックモードである場合は、journalフィイルはオープンされ、最初にヘッダを書きこむ。ディスク上のデータベースファイルは、更新されてない。

  • ライトトランザクションは、アクティブ。
  • データベースファイルに対して、RESERVEDか、より強いロックが取得済み。
  • journalファイルは、開いており、最初のヘッダは書き込みされているが、ディスクへの同期はされていない
  • ページキャッシュの内容は、更新されている。
WRITER_DBMOD:

WRITER_CACHEMODからWRITER_DBMODに遷移するのは、データベースファイルの内容を修正するとき。WAL接続の場合は、この状態には遷移しない。なぜなら、WAL接続では、ログに書きこむがデータベースファイルを更新しないため。

  • ライトトランザクションは、アクティブ
  • 排他ロックか、それより強りロックを保持。
  • journalファイルはオープンしており、最初のヘッダは書き込みと同期ができている。
  • ページキャッシュの内容は修正されていて、可能であればディスクに書きこまれている。
WRITER_FINISHED:

WAL接続をしている場合、この状態にはならない。ロールバックモードのpagerは、全てのトランザクションがデータベースファイルに書き込めたときに、WRITER_DBMODからWRITER_FINISHEDに変化する。この状態において、トランザクションは単純にjournalファイルをファイナライズすることでコミットされるかもしれない。一度、WRITER_FINISHEDの状態に遷移したら、それ以上の更新はできない。この時点で、上位のレイヤは、コミットするか、ロールバックするかしなければならない。

  • ライトトランザクションは、アクティブ。
  • 排他ロックか、それより強いロックが保持されている。
  • journalやデータベースへの全ての書き込みと同期が終了している。エラーが生じていなければ、やらなければいけないのはひとつだけであり、トランザクションをコミットするためにjournalをファイナライズすること(ファイナライズがいまいちわからない)。エラーが所持たら、トランザクションロールバックする。
ERROR:

エラー状態には、IO/disk-fullエラーが発生した時点で生じる。メモリ上のpagerの状態とdisk上のpagerの状態が一致していることが保証できなくなるということが、問題の本質。一時的なpagerファイルはERROR状態に遷移するかもしれないが、メモリにしか存在しないpagerでは、発生しない。例えば、ロールバックしている間にI/Oエラーが生じるとすると、page-cacheの内容は、ディスクと一致しない状態のままになるかもしれない。この時点で、READER状態に戻るのは危険である(通常、ロールバックの後で生じる遷移)。この状態において、リードトランザクションに対しては、データベース破損を通知する。ライトトランザクションに昇格したならば、データベースファイルをはっ損するかもしれない(だから、READER状態に戻るのはダメ)。このような障害を避けるため、pagerは、次のようなエラーが生じた場合、READERではなく、ERROR状態に遷移する。

一度、エラー状態に入ったら、そのpagerに対するread/write要求は全てエラーを返却する。結果的に、全ての未処理のトランザクションは中止され、そのpagerはOPEN状態に戻ることができる(この時、page-cacheの内容や別のメモリ状態といった情報を破棄する)。リードとアランザクションがそのpagerを次に開くときには、全ての情報(hot-journal rollbackも含む)がディスクから再ロードされる。この時点で、システムはエラーからリカバリされる。
pagerは、以下のような場合に、ERROR状態に遷移する。

  • (1).ロールバック中にエラーが発生。これは、sqlite3PagerRollback()内で発生する。
  • (2).コミットに続く、journalファイルのファイナライズ中にエラーが発生。sqlite3PagerCommitPhaseTwo()で発生する。
  • (3).メモリ領域を確保するために、pagerStress()内で、journalファイルやデータベースファイルへ書き込み中にエラーが発生する。

別な場合として、b-treeレイヤでエラーが返却される場合がある。b-treeレイヤでは、その時にロールバックを試みる。エラーが収まらないなら、(1)を通して、ERROR状態に入る。条件(3)は、トランザクション内でリードのみのトランザクションによって引き起こされるので必要。この場合、エラーコードがユーザに返却されるなら、b-treeレイヤではロールバックを試みない。リードのみの要求においては、内部的にpagerとディスクが同期されていないままの状態にならないから(あるときだけ、読み込めないというエラーがあるのだろうか?具体的なところがちょっとよくわからない。)。

  • Pager.errCode変数は、SQLITE_OK以外の何かをセットする。
  • pagerに対して、ひとつ以上の完了していない参照がある(全ての参照が破棄された後、OPEN状態へ遷移する。)。
  • ここでいう、pagerには、メモリのみのpagerは含まない。
Notes:
  • 接続がWALモードで開かれているなら、WRITER_DBMODやWRITER_FINISHEDには、遷移しない。
  • 通常、排他モードで開かれた接続は、PAGER_OPENには遷移しないが、例外はふたつ。排他モードがオンになった直後、(かつ、他のリード・ライトのトランザクションが実行される前)、か、ERROR状態から抜けるとき。
  • See also: assert_pager_state().
#define PAGER_OPEN                  0
#define PAGER_READER                1
#define PAGER_WRITER_LOCKED         2
#define PAGER_WRITER_CACHEMOD       3
#define PAGER_WRITER_DBMOD          4
#define PAGER_WRITER_FINISHED       5
#define PAGER_ERROR                 6

Pager.eLock変数には、次のロック状態のいずれかがセットされる。NO_LOCK、SHARED_LOCK、RESERVED_LOCK、EXCLUSIVE_LOCKである。この変数のライフサイクルは、pagerLockDb()の呼び出しで始まり、pagerUnlockDb()で終わる。VFSのxLock()やxUnlock()がSQLITE_BUSY以外のエラーを返却するなら(例えば、SQLITE_IOERR)、命令が成功しているかどうかは分からない。このような状況においては、pagerLockDB()/pagerUnlockDb()は、保守的なアプローチをとる。eLock変数は、unlockしたときにはいつも更新される。VFS callが成功した場合のみ、ファイルをlockし更新する。こうすることで、eLock変数は、実際に保持しているロック場合でもロックしていないという値をセットするかもしれない、ただし、実際にはロックしてないのに、ロックしているという値をセットすることはない(このことがどう響くのかが、ちょっとよく分からない。実際にはロック持っているのに、ロックしていないという場合には、衝突を起こさないけど、ロックしていないのに、ロックを持っているといったら、おかしな更新がかかってしまう可能性があるからか。)これは、常に安全である。xUnlockが失敗、または、失敗しているように見えるならば、少しだけ、xLock()が増えるかもしれない。でも、状況がまずくなることはない。(実際は、ロックをとってないのに、取っていることにしてしまうと、データの書き換えで衝突が生じる。)

pagerがERRORからOPENへ遷移するときに、データベースファイルがunlockされたときは、例外となる。この時点で、ロールバックに必要なファイルシステム上のhot-journalファイルがあるかもれいない。この時点で、xUnlock()の呼び出しが失敗して、pagerがEXCLUSIVE lockをもたせたままだと、後々呼び出されるxCheckReservedLock()において、混乱が生じる(実際は、誰もロックしていないのに、ロックしていることになって、何らかの処理が進まないのかな?関数名もチェックだし。)。xCheckReservedLock()は、もしこのプロセス、又は、他のプロセスによって、RESERVEDロックが掛けられている場合trueを返却する。xCheckReservedLockは、pagerがEXCLUSIVEロックを持っているので、trueを返却する(でも、実際は、xUnlockでのエラーのことは分からない)。このようなことが生じると、hot-journalは別のプロセスでアクティブなトランザクションによって作成されたjournalに対して、次のようなミスを発生する。ロールバックせずに、データベースから読み込みを行ってしまう。なるほど、そういうことか。ロックで失敗しても、ロールバックが必要なのかな、と思ったがよくわからない。

うまくやるには、ERROR状態でxUnlockが失敗した場合、Pager.eLockにUNKNOWN_LOCKを設定する。この値は、実際にEXCLUSIVEロックをとれたときにだけ、変更する。UNKNOWN_LOCKの時には、OPEN->SHAREDの時に、hot-journalに対するチェックを省略する。そのかわり、hot-journalは存在するとし、ロールバックを行う前にEXCLUSIVEロックを取得する。詳細は、PagerSharedLock()を見ること。

コメントだけでもすごい分量だ、若干疲れた。

typedef struct PagerSavepoint PagerSavepoint;
struct PagerSavepoint {
  i64 iOffset;                 /* Starting offset in main journal */
  i64 iHdrOffset;              /* See above */
  Bitvec *pInSavepoint;        /* Set of pages in this savepoint */
  Pgno nOrig;                  /* Original number of pages in file */
  Pgno iSubRec;                /* Index of first record in sub-journal */
#ifndef SQLITE_OMIT_WAL
  u32 aWalData[WAL_SAVEPOINT_NDATA];        /* WAL savepoint context */
#endif
};

この構造体は、各savepointやトランザクションで作成され、Pager.aSavepoint[]に格納される。savepointが作られると、iHdrOffsetは0。メインとなるjournalに、journal-headerが書き込まれると、iHdrOffsetには、journal-headerを書きこむ直前のオフセットがセットされる。この情報は、savepointロールバックに必要となる(pagerPlaybackSavepoint()を参照)。

Pager.c::L455。ここまで。ちょっと、この後、pagerPlaybackSavepoint()に興味が出てきたのでそのルートを追いかけてみる。個人的に、ロールバックが実際にどのように行われるのかをちょっと知りたい。