Skip to content

Support running generated programs from multiple threads - #909

Open
yutaro-sakamoto wants to merge 11 commits into
opensourcecobol:developfrom
yutaro-sakamoto:multithread-upstream
Open

yutaro-sakamoto wants to merge 11 commits into
opensourcecobol:developfrom
yutaro-sakamoto:multithread-upstream

Conversation

@yutaro-sakamoto

Copy link
Copy Markdown
Contributor

概要

変換後のJavaプログラムとlibcobjを、TomcatやSpring Bootのようなマルチスレッド環境で使えるようにします。これまでは「1つのJVM = 1つのCOBOL実行単位」を前提としていたため、ランタイムの状態がstaticフィールドにあり、複数のスレッドから同時にCOBOLプログラムを実行すると状態が混線し、STOP RUNはSystem.exitでサーバごと終了していました。

変更点

  • 実行単位の状態をスレッドごとに保持: モジュールスタック、例外コード、実行位置、CALLのキャッシュとコールスタック、組込関数・INSPECT・STRING・DIVIDE REMAINDER・SORTの作業領域、オープン中のファイル、EXTERNAL項目、ポインタ、ESQLの接続とカーソルなどをThreadLocal化しました。プロセス共通の設定はcob_initで1回だけ読み込みます(CobolUtil.ensureInitialized())。CobolRunUnit.end()でスレッドの実行単位を終了し、状態を解放できます。
  • STOP RUN: System.exitせず、実行単位のルートのプログラムまで巻き戻して正常に復帰します。プロセスを終了するのは生成されたmain()だけです。
  • ファイルロック: 同一JVM内のスレッド間でも、別プロセスと同じ共有規則でロックが働きます(JvmFileLockRegistry)。OSのロックは台帳のチャネル上、ファイル終端のはるか先の範囲に保持するので、保持者のCLOSEでロックが途切れず、Windowsの必須ロックで自分のI/Oが妨げられることもありません。Linuxではファイルのどのチャネルを閉じてもJVMのロックが外れるため、ロック保持中はチャネルを閉じずに預かり、次のOPENで再利用します。INDEXEDファイルはロック所有者を実行単位ごとに記録します。
  • ESQL: バックエンドをスレッドごとに生成します。実行単位の終了時、DISCONNECTされていない接続はロールバックしてクローズします。
  • 1つのDISPLAY文の出力を原子的に書き出します。
  • FUNCTION CURRENT-DATEが使う共有のCalendarを、cob_init時点の不変のスナップショットに置き換えました(値は従来と同じです)。
  • テスト: tests/multithread、tests/esql-multithread、JUnitテスト、Spring Bootの実機スモークテスト(500並列リクエスト)を追加しました。
  • ドキュメント: doc/multithreading.md(日英)ほか。

互換性

  • 以前のバージョンのcobjで変換したプログラムは、変換し直す必要があります。生成コードが参照していた公開staticフィールド(CobolRuntimeException.codeなど)を、スレッドごとの状態にアクセスするメソッドに置き換えたためです。
  • Javaから呼び出した場合、STOP RUNはJVMを終了せず、返り値を返して復帰します。

補足

  • CIを通すため、Fix Windows-specific cobj.exe bugs (#828) #862 の変更をマージしています。
  • developにはINDEXEDファイルのUNLOCKがまだ実装されていないため、indexed-record-lock.atはUNLOCKを使わず、別のレコードの読み込みやCLOSEでロックを解放する流れにしています。
  • tests/Makefile.inは、この環境でautomakeが動かないため、Makefile.amの変更を同じ形式で反映しています(差分は該当行のみです)。

Summary

This PR makes the generated Java programs and libcobj usable from several threads of one JVM, such as the request threads of Tomcat or Spring Boot. Until now the runtime assumed one COBOL run unit per JVM: its state lived in static fields, so programs running concurrently corrupted each other's state, and STOP RUN called System.exit, stopping the whole server.

Changes

  • Per-thread run-unit state: the module stack, exception codes, source location, CALL cache and call stack, the work areas of intrinsic functions, INSPECT, STRING, DIVIDE REMAINDER and SORT, open files, EXTERNAL items, pointers, and ESQL connections and cursors are kept in ThreadLocals. Process-wide configuration is read once in cob_init (CobolUtil.ensureInitialized()). CobolRunUnit.end() ends the run unit of a thread and releases its state.
  • STOP RUN no longer calls System.exit: it unwinds to the root program of the run unit and returns normally. Only the generated main() exits the process.
  • File locks: threads of one JVM share files under the same rules as separate processes (JvmFileLockRegistry). The OS lock is held on a registry-owned channel over a range far beyond EOF, so a holder's CLOSE does not drop it, and the mandatory locks of Windows do not block the holders' own I/O. On Linux, closing any channel of a file drops all the JVM's locks on it, so the registry parks those channels while the lock is held and reuses them for the next OPEN. INDEXED files record the lock owner per run unit.
  • ESQL: the backend is created per thread. When the run unit ends, connections that were not disconnected are rolled back and closed.
  • The output of one DISPLAY statement is written atomically.
  • The shared Calendar behind FUNCTION CURRENT-DATE is replaced with an immutable snapshot taken in cob_init; the returned values are unchanged.
  • Tests: new tests/multithread and tests/esql-multithread suites, JUnit tests, and a Spring Boot smoke test job that sends 500 concurrent requests.
  • Documentation: doc/multithreading.md (English and Japanese) and related documents.

Compatibility

  • Programs compiled by an earlier cobj must be recompiled: the generated code now calls accessor methods for the per-thread state instead of the public static fields it used to read (such as CobolRuntimeException.code).
  • When a program is entered from Java, STOP RUN returns its return code instead of exiting the JVM.

Notes

  • Fix Windows-specific cobj.exe bugs (#828) #862 is merged into this branch so that CI passes.
  • develop does not implement UNLOCK for INDEXED files yet, so indexed-record-lock.at releases record locks by reading another record or closing the file instead of using UNLOCK.
  • tests/Makefile.in mirrors the Makefile.am changes in the same format, because automake does not run in this environment; the diff is limited to the affected lines.

yutaro-sakamoto and others added 11 commits July 26, 2026 18:52
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
Generated programs and libcobj assumed one COBOL run unit per JVM: the
runtime kept the module stack, exception codes, CALL cache, work areas,
open files, EXTERNAL items and ESQL sessions in static fields, and STOP RUN
called System.exit. Running programs from several threads of one JVM, as
Tomcat or Spring Boot request threads do, therefore corrupted their state
or stopped the server.

- Keep the run-unit state of libcobj per thread (ThreadLocal); read the
  process-wide configuration once in cob_init (CobolUtil.ensureInitialized)
  and release a thread's state with CobolRunUnit.end().
- STOP RUN unwinds to the root program of the run unit and returns;
  only the generated main() exits the process.
- Threads of one JVM share sequential, line sequential and relative files
  under the same rules as separate processes (JvmFileLockRegistry: the
  lock sits on a registry channel beyond EOF, and channels are never
  closed while the JVM holds the lock). INDEXED files record a lock owner
  per run unit, so record locks work between threads.
- The ESQL backend is created per thread; connections left open are
  rolled back and closed when the run unit ends.
- One DISPLAY statement is written atomically.
- Replace the static Calendar behind FUNCTION CURRENT-DATE with an
  immutable snapshot taken in cob_init, and stop localtime() from
  rewriting the shared COB_DATE value.
- Add the multithread and esql-multithread test suites, JUnit tests and a
  Spring Boot smoke test job, and document the model in
  doc/multithreading.md.

Programs compiled by an earlier cobj must be recompiled: the generated
code now calls accessor methods instead of the removed public static
fields.
The Gradle wrapper of libcobj on develop (8.2.1) cannot build Spring Boot
3.4, which failed the smoke test job while configuring the project. Set up
Gradle 8.14.3 for the sample in CI and document the requirement.
setup-gradle extracts the Gradle distribution with unzip, which the
ubuntu:24.04 container does not have.
@yutaro-sakamoto
yutaro-sakamoto marked this pull request as ready for review September 30, 2026 11:04
@yutaro-sakamoto

Copy link
Copy Markdown
Contributor Author

@h-himo @n-isaka @tsh-hashimoto @kio-watanabe
フォーク側の私のリポジトリで開発したマルチスレッド対応のPRをこちらのリポジトリにも出しました。
元のPRはこちらのリポジトリに比べて多くの修正が入っていたバージョンがもとになっているので、このPRではその差分に由来する不具合はあるかもしれません。

また明日以降、このリポジトリに新しくPRを出したり、問い合わせに対応したりできるかは、私の立場上不透明です。

@yutaro-sakamoto

Copy link
Copy Markdown
Contributor Author

@h-himo @n-isaka @tsh-hashimoto @kio-watanabe
このリポジトリの私のアカウント下のフォークの名前をopensourcecobol4jからHagane-COBOLに変更しました。
https://github.com/yutaro-sakamoto/Hagane-COBOL

This branch has not been deployed

No deployments
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