Skip to content

Commit INDEXED files opened OUTPUT at COMMIT and CLOSE instead of on every WRITE - #892

Open
yutaro-sakamoto wants to merge 10 commits into
opensourcecobol:developfrom
yutaro-sakamoto:indexed-output-defer-commit-upstream
Open

Commit INDEXED files opened OUTPUT at COMMIT and CLOSE instead of on every WRITE#892
yutaro-sakamoto wants to merge 10 commits into
opensourcecobol:developfrom
yutaro-sakamoto:indexed-output-defer-commit-upstream

Conversation

@yutaro-sakamoto

Copy link
Copy Markdown
Contributor

Note

CIを通すために、PR #862(Windows向けのcobj.exe修正)をマージしたうえで変更を積んでいます。#862がdevelopに入った後、このブランチはrebaseします。

概要

OPEN OUTPUTした索引編成ファイルに対して、WRITEのたびに行っていたJDBCトランザクションのコミット(=SQLiteのfsync)をやめました。レコードはCOBOLのCOMMIT文とCLOSE時にコミットされます。80バイトのレコードを100,000件書き込む処理が 382.7秒 → 0.46秒 になります。

OPEN OUTPUTした索引編成ファイルはfile_lockテーブルによりファイル全体が排他ロックされ(他プロセスのOPENはファイルステータス61で拒否)、CLOSEまでに実行されるのはWRITEだけです。したがって中間状態が他プロセスから観測されることはなく、コミットを遅延しても共有の挙動は変わりません。

環境変数 COB_FILE_IDX_COMMIT_INTERVAL

ランタイム側でも定期的にコミットさせたい場合に指定します。

意味
INF(大文字小文字不問) 途中コミットを行わない。デフォルト(環境変数未指定時も同じ)
正の整数 N 成功したWRITEがN件たまるごとにもコミットする
0 1と同じ(WRITEのたびにコミット)
上記以外 標準エラー出力に警告を出してデフォルトを使用

デフォルトをINFにしたのは、COBOLにはCOMMIT文があり、コミットの時点はプログラムがCOMMIT文とCLOSEで決めるのが本来の姿だと考えたためです。整数を指定すると、CLOSE前にプロセスがクラッシュ・強制終了した場合に失われるレコードをその件数までに抑えられます(コミット1回あたり約4msのfsyncコストと引き換え)。doc/environment_variables.mddoc/environment_variables_JP.mdにも記載しています。

動作の要点

  • 重複キーの検出は従来通りWRITEのたびに行われます。 同一コネクション上のクエリからは未コミットの行も見えるため、ファイルステータス21・22は即座に報告されます。
  • 副キーを持つファイルでは各WRITEをセーブポイントで囲みます。 単純にconnection.rollback()すると、遅延中の成功済みWRITEまで巻き戻ってしまうためです。副キーを持たないファイルのWRITEは単一のINSERTで、部分的な変更が残る失敗が構造的に存在しないためセーブポイントを省略しています。
  • INPUTI-OEXTENDで開いたファイルの挙動は変わりません。

併せて修正した関連箇所

  • exitFileIOが実際にクローズするようにしました。 従来は「WARNING - Implicit CLOSE」を表示するだけで実際にはクローズしておらず、CLOSEを忘れたままSTOP RUNに到達すると遅延レコードが失われるうえ、file_lockの行が残って以後のOPENがすべてファイルステータス61で拒否される状態でした。
  • unlock_を実装しました。 従来は「Unlocking INDEXED file is not implemented」を出力するだけでした。現在は自プロセスが保持するレコードロックを解放するので、UNLOCK文・COMMIT文・ROLLBACK文が索引編成ファイルでも機能します。ただしfile_lockの行はオープン中である登録を兼ねているため、CLOSEまで解放しません(先に消すと他プロセスのOPEN OUTPUTがテーブルを作り直せてしまいます)。
  • ROLLBACK文をrollback_フックとして実装しました。 OPEN OUTPUTのファイルでは未コミットのWRITEを実際に取り消します(順序チェックの基準キーも消すので、同じキーから書き直せます)。それ以外のモードでは各文がその場でコミット済みで取り消せる変更がないため、opensource COBOL 1.xのcob_rollbackと同じくレコードロックの解放だけを行います。
  • SQLエラー時のファイルステータスを見直しました。 従来はSQLExceptionを一律51(レコードロック)にしていましたが、ディスクフルは34、一意制約違反は22、ロック競合は51、その他は30に対応付けます。またSQLiteはディスクフル等では文単位ではなくトランザクション全体を巻き戻すため、1件のWRITEエラーの裏でバッファ中のレコードがすべて失われることがあります。この場合CLOSE30を返し、成功を装いません。
  • ファイルキャッシュはオープン中のファイルだけを保持するようにし、INSERT文のPreparedStatementはテーブルごとに1つ生成して使い回すようにしました。

性能

80バイトのレコードをN件連続WRITEするプログラムのwall time(JVM起動時間 約0.15秒を含む)。環境: WSL2 (Linux 5.15) + ext4、中央値/3回。

N 変更前 変更後(デフォルト=INF) 高速化
1,000 3.86s 0.153s 25x
10,000 36.8s 0.205s 179x
100,000 382.7s 0.461s 830x

100,000件書き込んだファイルをcobj-idx unloadし、100,000行すべてが正しいキー順で格納されていることを確認しています。

テスト

tests/misc.src/indexed-output-commit-interval.at を追加しました(8グループ)。

  1. OPEN OUTPUTでの重複主キー(22)・重複ユニーク副キー(22)・キー順序違反(21)がWRITE時点で報告され、エラー後も書き込み済みレコードが失われないこと
  2. ACCESS SEQUENTIALでの21と22の優先順位(001,003の順に書いた後の002や001は、重複ではなく順序違反の21。22になるのは直前のキーと等しい場合だけ)がindexed_writeの実装と一致すること
  3. COB_FILE_IDX_COMMIT_INTERVAL0/1/3/10/1000000/INF/infと変えても読み戻した結果が完全に一致すること、不正値は警告を出してデフォルトにフォールバックすること
  4. CLOSEせずにSTOP RUNしても暗黙クローズで全件コミットされること
  5. OPEN EXTENDでのWRITEのファイルステータスと追記結果
  6. COMMIT文が遅延中のレコードをコミットすること
  7. ROLLBACK文がOPEN OUTPUTの未コミット分を取り消し、同じキーを書き直せること
  8. コミット済みのレコード(I-OのREWRITECOMMIT文の後)はROLLBACKで取り消されないこと

Summary (English)

An indexed file opened with OUTPUT no longer commits the backing JDBC transaction — and therefore fsyncs the SQLite database — after every WRITE. The records are committed by the COBOL COMMIT statement and at CLOSE. Writing 100,000 80-byte records takes 0.46s instead of 382.7s.

Such a file holds an exclusive lock through the file_lock table (any other OPEN gets file status 61) and only WRITE statements run until CLOSE, so no other process can observe the intermediate states and deferring the commits does not change the sharing behaviour.

Environment variable COB_FILE_IDX_COMMIT_INTERVAL

Value Meaning
INF (case-insensitive) no intermediate commits — the default, also used when the variable is unset
positive integer N additionally commit every N successful WRITEs
0 same as 1 (commit on every WRITE)
anything else reported on standard error, the default is used

INF is the default because COBOL has a COMMIT statement, so the commit points belong to the program. Setting an integer bounds what a crash before CLOSE can lose at that number of records, in exchange for one fsync per interval. Documented in doc/environment_variables.md and doc/environment_variables_JP.md.

Key points

  • Duplicate-key detection still happens on every WRITE: uncommitted rows are visible to queries on the same connection, so file status 21 and 22 are reported immediately.
  • Each WRITE of a file with alternate keys runs inside a savepoint, so a failed WRITE rolls back only its own changes instead of the whole buffer. A file with no alternate key needs none, because its WRITE is a single atomic INSERT.
  • Files opened INPUT, I-O or EXTEND behave as before.

Related fixes

  • exitFileIO now really closes the files it warns about. It only printed "WARNING - Implicit CLOSE" before, so a program reaching STOP RUN without CLOSE lost the deferred records and left a stale file_lock row that made every later OPEN fail with status 61.
  • unlock_ is implemented instead of printing "Unlocking INDEXED file is not implemented", so UNLOCK, COMMIT and ROLLBACK work on indexed files. The file_lock row is deliberately kept until CLOSE, since it doubles as the registration that the file is open.
  • ROLLBACK is dispatched through a rollback_ hook. For a file open OUTPUT it discards the writes that are still uncommitted (and forgets the last key, so the same keys can be written again); in the other modes every statement is already committed, so it releases the record locks only, exactly like cob_rollback in opensource COBOL 1.x.
  • File statuses for SQL failures were revised. Every SQLException used to become 51 (record locked); disk full is now 34, a constraint violation 22, lock contention 51 and anything else 30. SQLite also rolls back the whole transaction rather than the failing statement for conditions such as disk full, so a single failed WRITE can take the buffered records with it — CLOSE returns 30 in that case rather than reporting success.
  • The file cache keeps only open files, and the INSERT statements are prepared once per table instead of on every WRITE.

Benchmark

Wall time of a program that sequentially WRITEs N 80-byte records (including ~0.15s of JVM startup). WSL2 (Linux 5.15) on ext4, median of 3 runs.

N before after (default INF) speedup
1,000 3.86s 0.153s 25x
10,000 36.8s 0.205s 179x
100,000 382.7s 0.461s 830x

The 100,000-record file was unloaded with cobj-idx unload to confirm that all 100,000 records are present in key order.

Tests

tests/misc.src/indexed-output-commit-interval.at adds eight groups covering the WRITE statuses in OUTPUT and EXTEND mode, the precedence between status 21 and 22 for ACCESS SEQUENTIAL, the invariance of the written records across every COB_FILE_IDX_COMMIT_INTERVAL value, the implicit close at STOP RUN, and the COMMIT and ROLLBACK statements.

codegen.c kept the inside_check/inside_stack bookkeeping from the
original C code generator, but only the decrements survived the
rewrite to Java code generation: nothing ever increments inside_check.
On non-GCC builds (the MSVC-built cobj.exe) the counter goes negative
as soon as a runtime check is attached to a reference (-debug with a
variable subscript, reference modification or SEARCH ALL), and
inside_stack[inside_check - 1] then writes out of bounds, crashing the
compiler. GCC builds never compile these blocks, which is why Linux
was unaffected. Remove the machinery entirely: the generated Java
must not depend on the compiler that built cobj.

Un-skip the Windows tests that were failing due to this crash.

Fixes opensourcecobol#829, opensourcecobol#831, opensourcecobol#832, opensourcecobol#833, opensourcecobol#834 (sub-issues of opensourcecobol#828)
With -java-package the jar arguments were assembled as
package_dir + file_path_delimitor + class, mixing the '/' separators
from the package path with '\' on Windows (e.g. com/abc\prog.class),
which produces malformed entry names in the archive. The jar tool
accepts '/' everywhere, so join the paths with '/' unconditionally.

The class file cleanup after archiving had the opposite problem:
cmd.exe's del does not accept '/' separators. Build the remove paths
with the native separator, let cd handle the output directory, and
drop a leftover '#aaa' token that del would treat as an argument.

Un-skip the Windows jar test.

Fixes opensourcecobol#830 (sub-issue of opensourcecobol#828)
Removing the inside_check block exposed a variableScope finding for
'code' in joutput_stmt; declare it in the handler block where it is
used.
Every program in a translation unit becomes its own .java/.class file
named after the program ID, so two programs whose IDs differ only in
letter case overwrite each other's files on a case-insensitive
filesystem and the result silently misbehaves. On Windows, detect the
collision before code generation and fail with an explicit error.

The two affected tests stay skipped on Windows because the behavior
they exercise is impossible there by design; a new Windows-only test
verifies the diagnostic instead.

Fixes opensourcecobol#835 (sub-issue of opensourcecobol#828)
On case-insensitive file systems programs whose PROGRAM-IDs differ
only in letter case would generate class files with the same name;
cobj now reports a compile error there, so document the restriction
in the usage section.
* Use "cd /d" on Windows: cmd.exe's plain "cd" does not switch
  drives, so with -o pointing at another drive the jar and del
  commands would run against the wrong directory
* Replace the unbounded strcpy into package_dir_native with snprintf
  and give package_name_to_path an explicit output bound, so an
  overlong -java-package argument can no longer overflow the buffers
@yutaro-sakamoto
yutaro-sakamoto force-pushed the indexed-output-defer-commit-upstream branch 2 times, most recently from 9e664a2 to b91cfac Compare August 19, 2026 08:08
…every WRITE

An indexed file opened with OUTPUT holds an exclusive file lock and only
WRITE statements run until CLOSE, so no other process can observe the
intermediate states. Committing the JDBC transaction - and therefore
fsyncing the SQLite database - after every WRITE is unnecessary there.
The records are now committed by the COBOL COMMIT statement and at
CLOSE. Writing 100,000 records takes 0.46s instead of 382.7s.

COB_FILE_IDX_COMMIT_INTERVAL makes the runtime commit on its own every N
successful WRITEs as well, which bounds what a crash before CLOSE can
lose at N records in exchange for one fsync per interval. It defaults to
INF (no intermediate commits) because COBOL has a COMMIT statement, so
the commit points belong to the program. 0 is treated as 1 (commit per
WRITE) and an invalid value falls back to the default with a warning.

Duplicate-key detection is unaffected: uncommitted rows are visible to
queries on the same connection, so WRITE still reports file status 21
and 22 immediately. Each WRITE of a file with alternate keys runs inside
a savepoint so that a failed WRITE rolls back only its own changes;
files with no alternate key need no savepoint because their WRITE is a
single atomic INSERT.

Supporting changes:

- exitFileIO now really closes the files it warns about. It only printed
  "WARNING - Implicit CLOSE" before, so a program reaching STOP RUN
  without CLOSE lost the deferred records and left a stale file_lock row
  that made every later OPEN fail with status 61.
- unlock_ releases this process's record locks instead of printing
  "Unlocking INDEXED file is not implemented", so UNLOCK, COMMIT and
  ROLLBACK work on indexed files. The file_lock row is deliberately kept
  until CLOSE, since it doubles as the open registration.
- ROLLBACK is dispatched through a rollback_ hook. For a file open
  OUTPUT it discards the writes that are still uncommitted; in the other
  modes every statement is already committed, so it releases the record
  locks only, as cob_rollback does in opensource COBOL 1.x.
- SQL failures on the WRITE path used to be reported as status 51
  (record locked) whatever went wrong. They now map to 34 for disk full,
  22 for a constraint violation, 51 for lock contention and 30
  otherwise. CLOSE reports 30 when a failed WRITE may have taken the
  buffered records with it, since SQLite rolls back the whole
  transaction for conditions such as disk full.
- The file cache keeps only open files, and the INSERT statements are
  prepared once per table instead of on every WRITE.
@yutaro-sakamoto
yutaro-sakamoto force-pushed the indexed-output-defer-commit-upstream branch from b91cfac to 960dce0 Compare August 19, 2026 09:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant