Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
eecc892
fix: SCOPE_EXIT_CLEANUP type guards and GlobalDestruction CME
fglock Apr 10, 2026
df00d08
docs: update DBIx::Class plan with Phase 9 re-baseline after DESTROY/…
fglock Apr 10, 2026
3b9bb81
fix: prevent premature DESTROY by tracking named container local bind…
fglock Apr 10, 2026
a598143
fix: bundle Devel::GlobalDestruction with plain Exporter
fglock Apr 11, 2026
e0b7db7
feat: bundle DBI::Const::GetInfoType and related modules
fglock Apr 11, 2026
5f75cf4
docs: add Phase 10 full-suite re-baseline to DBIx::Class plan
fglock Apr 11, 2026
46eb4cd
docs: refine Phase 10 categorization with detailed failure analysis
fglock Apr 11, 2026
d34d2bc
fix: suppress MortalList flush during setFromList materialization
fglock Apr 11, 2026
61af173
docs: trim Phase 11 to focus on actionable next steps
fglock Apr 11, 2026
fe7d072
docs: update DBIx::Class plan with Step 11.2 failure analysis and rev…
fglock Apr 11, 2026
4f1ed14
fix: cascade hash/array cleanup for blessed objects without DESTROY
fglock Apr 11, 2026
088898a
docs: update DBIx::Class plan with Step 11.4 investigation and fix de…
fglock Apr 11, 2026
439bbcb
docs: update DBIx::Class plan with Step 11.4 findings and next steps
fglock Apr 11, 2026
43ea1d3
docs: Phase 12 plan — all DBIx::Class tests must pass
fglock Apr 11, 2026
a4c18ce
fix: DBI numeric formatting, DBI_DRIVER env, HandleError callback
fglock Apr 11, 2026
87de4f4
docs: add resource management warning to AGENTS.md
fglock Apr 11, 2026
82fb4b7
fix: suppress mortal flush on do-block scope exit
fglock Apr 11, 2026
56f1b38
fix: fire DESTROY for blessed objects when die unwinds through eval
fglock Apr 11, 2026
a5a1214
feat: DESTROY-on-die for blessed my-variables (Phase 13)
fglock Apr 11, 2026
4c375f0
fix: flush deferred DESTROY in void-context sub calls
fglock Apr 11, 2026
b77dc88
fix: DBI handle lifecycle — localBindingExists, finish, circular refs
fglock Apr 11, 2026
4a74280
fix: mortal mark/flush system for DESTROY timing and method chain tem…
fglock Apr 12, 2026
96c34a2
fix: release captures for ephemeral grep/map/sort/all/any block closures
fglock Apr 12, 2026
4e0d8e7
fix: defer refCount decrements in local array/hash restore
fglock Apr 12, 2026
e0a94c7
fix: clean up my-variable refCounts when method calls die in callCached
fglock Apr 12, 2026
05e4a32
fix: birth-track anonymous arrayrefs to prevent blessed object leaks
fglock Apr 12, 2026
2cf0f15
test: add regression tests for anonymous container DESTROY behavior
fglock Apr 12, 2026
30fb463
fix: prevent splice on @_ from prematurely destroying caller's objects
fglock Apr 12, 2026
db846e6
fix: prevent premature weak ref clearing for stash-installed closures
fglock Apr 12, 2026
ef424f7
fix: increase test JVM heap to 1g for code_too_large.t
fglock Apr 12, 2026
412ac26
docs: update DBIx::Class fix plan with Phase 15 issues
fglock Apr 12, 2026
0680977
fix: correct caller() frame counting when CORE::GLOBAL::caller is ove…
fglock Apr 12, 2026
f08d437
fix: caller() returns correct package for use/import through interpreter
fglock Apr 12, 2026
dddab71
feat: add wait operator to bytecode interpreter backend
fglock Apr 12, 2026
d52b345
docs: restructure DBIx::Class fix plan with three work directions
fglock Apr 12, 2026
d32de1d
fix: deferred capture cleanup enables DESTROY for captured blessed refs
fglock Apr 12, 2026
13a260e
fix: begin_work nested-txn check, $INC cleanup, selectcol_arrayref
fglock Apr 12, 2026
aed90fe
docs: update DBIx::Class fix plan with detailed analysis of remaining…
fglock Apr 12, 2026
e02e0f9
fix: implement DESTROY rescue detection for Schema self-save pattern
fglock Apr 12, 2026
bca73bd
fix: scope exit cleanup now follows Perl 5 reverse declaration order …
fglock Apr 13, 2026
aaae9ce
docs: compact DBIC design doc, add detailed GC fix plan
fglock Apr 13, 2026
4eb7632
fix: deferred weak-ref clearing for DESTROY-rescued objects
fglock Apr 13, 2026
7df81dc
fix: eliminate all DBIC GC failures + DBI RootClass support
fglock Apr 13, 2026
c2fdddb
docs: update DBIC design doc with final results (99.77%, 0 GC failures)
fglock Apr 13, 2026
beebccd
fix: next::method always uses C3 linearization (matches Perl 5)
fglock Apr 13, 2026
d6dd158
fix: clear weak refs on stash delete + fix B::REFCNT inflation
fglock Apr 13, 2026
17bd9e4
docs: update DBIC design doc with Fix 7 results (99.90%, 12 remaining)
fglock Apr 13, 2026
1d2128c
fix: DBI BYTE_STRING for DB strings + utf8::decode conditional UTF-8 …
fglock Apr 13, 2026
4b05e45
fix: release s///eg replacement closure captures to unblock DESTROY
fglock Apr 13, 2026
abcbb5b
fix: release interpreter closure captures at frame exit for DESTROY
fglock Apr 13, 2026
c65974e
fix: DBI UTF-8 round-trip and filehandle dup of closed handles
fglock Apr 13, 2026
cd5bbb5
docs: update DBIx::Class design doc with Fix 9 results
fglock Apr 13, 2026
d7a435d
fix: handle JVM VerifyError with interpreter fallback
fglock Apr 14, 2026
f6627da
fix: prevent premature DESTROY of hash element aliases during excepti…
fglock Apr 14, 2026
ad72557
fix: local $hashref->{key} now restores correctly after hash reassign…
fglock Apr 14, 2026
a13d6a3
fix: populate @DB::args in non-debug mode for caller() from package DB
fglock Apr 14, 2026
dbe9b87
docs: update design doc with F2-F5 completion and broad DBIC test res…
fglock Apr 14, 2026
2b2f7c7
fix: DESTROY rescue refCount tracking and glob stash hash access
fglock Apr 14, 2026
ff7ce8b
fix: document DESTROY rescue semantics and refCount inflation impact
fglock Apr 14, 2026
96be4a6
fix: DESTROY rescue protection for phantom chain in t/52leaks.t
fglock Apr 14, 2026
73739dd
fix: clear weak refs at refCount 0 even when localBindingExists block…
fglock Apr 14, 2026
10b7595
fix: add createAnonymousReference() for Storable/deserializer anonymo…
fglock Apr 18, 2026
3f13826
fix: cascade scope-exit cleanup when weak refs exist (not just blesse…
fglock Apr 18, 2026
5179414
docs: update dbix_class.md with Fix 10e/10f and remaining 52leaks ana…
fglock Apr 18, 2026
4da6ede
fix: base.pm considers package loaded if @ISA or $VERSION set (not ju…
fglock Apr 18, 2026
ed20075
docs: document Fix 10g (base.pm loaded-check), hri.t fix, and non-bug…
fglock Apr 18, 2026
29af4a7
fix: flock() allows multiple shared locks on same file from same JVM
fglock Apr 18, 2026
c8c38ca
fix: fork() doesn't emit "1..0 # SKIP" after tests have run
fglock Apr 18, 2026
afbc081
fix: DBI stores mutable scalars for user-writable attributes
fglock Apr 18, 2026
d254c09
fix: stringify overload self-reference falls back to default ref form
fglock Apr 18, 2026
a2efcd9
fix: preserve @_ snapshot so @DB::args survives shift() in callees
fglock Apr 18, 2026
c4a1571
docs: compress dbix_class.md with session's fixes 10h-10l and current…
fglock Apr 18, 2026
ced1821
fix: eq/ne throw "no method found" when overload fallback not permitted
fglock Apr 18, 2026
e5a006b
docs: update dbix_class.md with Fix 10m (eq/ne fallback) and DESTROY …
fglock Apr 18, 2026
ab299f0
docs(dbic): document why t/52leaks and txn_scope_guard#18 are blocked
fglock Apr 19, 2026
5830a34
docs(design): refcount alignment plan for Perl-compatible DESTROY/weaken
fglock Apr 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@

---

## ⚠️ Resource Management: Avoid Fork Exhaustion ⚠️

**Do NOT spawn excessive parallel processes.** Running too many background shells, subagents, or parallel builds at once can exhaust the system's process table (fork bomb), forcing a reboot and losing work.

- **Limit parallel operations**: Run at most 2-3 concurrent processes at a time
- **Avoid unnecessary background shells**: Use foreground execution when you don't need parallelism
- **Wait for processes to finish** before starting new ones when possible
- **Never run `make` in parallel with other heavy processes** (builds already use multiple threads internally)
- **Clean up**: Kill background shells when they're no longer needed

---

## Project Rules

### Progress Tracking for Multi-Phase Work
Expand Down Expand Up @@ -60,6 +72,10 @@ Example format at the end of a design doc:
- Keep docs updated as implementation progresses
- Reference related docs and skills at the end

### Sandbox Tests

- `dev/sandbox/destroy_weaken/` — Tests for DESTROY and weaken behavior (cascading cleanup, scope exit timing, blessed-without-DESTROY, etc.). Run with `./jperl` or `perl` for comparison.

### Partially Implemented Features

| Feature | Status |
Expand Down
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,10 @@ tasks.withType(JavaCompile).configureEach {
options.compilerArgs << '-Xlint:deprecation'
}

// Test execution configuration with native access
// Test execution configuration with native access and adequate heap
tasks.withType(Test).configureEach {
jvmArgs += '--enable-native-access=ALL-UNNAMED'
maxHeapSize = '1g'
}

// Enable native access for all Java execution tasks
Expand Down
3,416 changes: 324 additions & 3,092 deletions dev/design/destroy_weaken_plan.md

Large diffs are not rendered by default.

441 changes: 441 additions & 0 deletions dev/design/refcount_alignment_plan.md

Large diffs are not rendered by default.

1,019 changes: 160 additions & 859 deletions dev/modules/dbix_class.md

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions dev/patches/cpan/DBIx-Class-0.082844/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# DBIx::Class 0.082844 Patches for PerlOnJava

## Problem

DBIx::Class uses `TxnScopeGuard` which relies on `DESTROY` for automatic
transaction rollback when a scope guard goes out of scope without being
committed. On PerlOnJava (JVM), `DESTROY` does not fire deterministically,
so:

1. Failed bulk inserts leave `transaction_depth` permanently elevated
2. Subsequent transactions silently nest instead of creating new top-level transactions
3. `BEGIN`/`COMMIT` disappear from SQL traces
4. Failed populates don't roll back (partial data left in DB)

## Fix

Wrap `txn_scope_guard`-protected code in `eval { ... } or do { rollback; die }`
to ensure explicit rollback on error, instead of relying on guard DESTROY.

## Files Patched

### Storage/DBI.pm — `_insert_bulk` method (line ~2415)
- Wraps bulk insert + query_start/query_end + guard->commit in eval block
- On error: sets guard inactivated, calls txn_rollback, re-throws

### ResultSet.pm — `populate` method
- **List context path** (line ~2239): wraps map-insert loop + guard->commit in eval
- **Void context with rels path** (line ~2437): wraps _insert_bulk + children rels + guard->commit in eval

## Applying Patches

Patches must be applied to BOTH locations:
1. Installed modules: `~/.perlonjava/lib/DBIx/Class/Storage/DBI.pm` and `ResultSet.pm`
2. CPAN build dir: `~/.cpan/build/DBIx-Class-0.082844-*/lib/DBIx/Class/Storage/DBI.pm` and `ResultSet.pm`

```bash
# From the PerlOnJava project root:
cd ~/.perlonjava/lib
patch -p0 < path/to/dev/patches/cpan/DBIx-Class-0.082844/Storage-DBI.pm.patch

# Also patch the active CPAN build dir (find the latest one):
BUILDDIR=$(ls -td ~/.cpan/build/DBIx-Class-0.082844-*/lib | head -1)
cd "$BUILDDIR/.."
patch -p0 < path/to/dev/patches/cpan/DBIx-Class-0.082844/Storage-DBI.pm.patch
```

## Tests Fixed

- t/100populate.t: tests 37-42 (void ctx trace BEGIN/COMMIT), 53 (populate is atomic),
59 (literal+bind normalization), 104-107 (multicol-PK has_many trace)
- Result: 108/108 real tests pass (was 98/108), only GC tests 109-112 remain

## Date

2026-04-11
230 changes: 230 additions & 0 deletions dev/sandbox/destroy_weaken/destroy_no_destroy_method.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
use strict;
use warnings;
use Test::More;
use Scalar::Util qw(weaken isweak);

# =============================================================================
# destroy_no_destroy_method.t — Cascading cleanup for blessed objects
# without a DESTROY method
#
# When a blessed hash goes out of scope and its class does NOT define
# DESTROY, Perl must still decrement refcounts on the hash's values.
# This is critical for patterns like DBIx::Class where intermediate
# Moo objects (e.g. BlockRunner) hold strong refs to tracked objects
# but don't define DESTROY themselves.
#
# Root cause: DestroyDispatch.callDestroy skips scopeExitCleanupHash
# for blessed objects whose class has no DESTROY method, leaking the
# refcounts of the hash's values.
# =============================================================================

# --- Blessed holder WITHOUT DESTROY should still release contents ---
{
my @log;
{
package NDM_Tracked;
sub new { bless {}, shift }
sub DESTROY { push @log, "tracked" }
}
{
package NDM_HolderNoDestroy;
sub new { bless { target => $_[1] }, $_[0] }
# No DESTROY defined
}
my $weak;
{
my $tracked = NDM_Tracked->new;
$weak = $tracked;
weaken($weak);
my $holder = NDM_HolderNoDestroy->new($tracked);
}
is_deeply(\@log, ["tracked"],
"blessed holder without DESTROY still triggers DESTROY on contents");
ok(!defined $weak,
"tracked object is collected when holder without DESTROY goes out of scope");
}

# --- Contrast: blessed holder WITH DESTROY properly releases contents ---
{
my @log;
{
package NDM_TrackedB;
sub new { bless {}, shift }
sub DESTROY { push @log, "tracked" }
}
{
package NDM_HolderWithDestroy;
sub new { bless { target => $_[1] }, $_[0] }
sub DESTROY { push @log, "holder" }
}
my $weak;
{
my $tracked = NDM_TrackedB->new;
$weak = $tracked;
weaken($weak);
my $holder = NDM_HolderWithDestroy->new($tracked);
}
is_deeply(\@log, ["holder", "tracked"],
"blessed holder with DESTROY cascades to contents");
ok(!defined $weak,
"tracked object is collected when holder with DESTROY goes out of scope");
}

# --- Contrast: unblessed hashref properly releases contents ---
{
my @log;
{
package NDM_TrackedC;
sub new { bless {}, shift }
sub DESTROY { push @log, "tracked" }
}
my $weak;
{
my $tracked = NDM_TrackedC->new;
$weak = $tracked;
weaken($weak);
my $holder = { target => $tracked };
}
is_deeply(\@log, ["tracked"],
"unblessed hashref releases tracked contents");
ok(!defined $weak,
"tracked object is collected when unblessed holder goes out of scope");
}

# --- Nested: blessed-no-DESTROY holds blessed-no-DESTROY holds tracked ---
{
my @log;
{
package NDM_TrackedD;
sub new { bless {}, shift }
sub DESTROY { push @log, "tracked" }
}
{
package NDM_OuterNoDestroy;
sub new { bless { inner => $_[1] }, $_[0] }
}
{
package NDM_InnerNoDestroy;
sub new { bless { target => $_[1] }, $_[0] }
}
my $weak;
{
my $tracked = NDM_TrackedD->new;
$weak = $tracked;
weaken($weak);
my $inner = NDM_InnerNoDestroy->new($tracked);
my $outer = NDM_OuterNoDestroy->new($inner);
}
ok(!defined $weak,
"nested blessed-no-DESTROY chain still releases tracked object");
}

# --- Weak backref pattern (Schema/Storage cycle) ---
#
# Schema (blessed, has DESTROY) ──strong──> Storage
# Storage (blessed, has DESTROY) ──weak────> Schema
# BlockRunner (blessed, NO DESTROY) ──strong──> Storage
#
# When BlockRunner goes out of scope, Storage refcount must decrement.
# Later when Schema goes out of scope, cascading DESTROY must bring
# Storage refcount to 0.
{
my @log;
{
package NDM_Storage;
use Scalar::Util qw(weaken);
sub new {
my ($class, $schema) = @_;
my $self = bless {}, $class;
$self->{schema} = $schema;
weaken($self->{schema});
return $self;
}
sub DESTROY { push @log, "storage" }
}
{
package NDM_Schema;
sub new { bless {}, $_[0] }
sub DESTROY { push @log, "schema" }
}
{
package NDM_BlockRunner;
sub new { bless { storage => $_[1] }, $_[0] }
# No DESTROY — like DBIx::Class::Storage::BlockRunner
}

my $weak_storage;
{
my $schema = NDM_Schema->new;
my $storage = NDM_Storage->new($schema);
$schema->{storage} = $storage;

$weak_storage = $storage;
weaken($weak_storage);

# Simulate dbh_do: create a BlockRunner that holds storage
my $runner = NDM_BlockRunner->new($storage);
undef $storage;

# Runner goes out of scope here — must release storage ref
undef $runner;
# Now only $schema->{storage} should hold storage
}
# After block: schema out of scope -> DESTROY schema -> cascade -> DESTROY storage
ok(!defined $weak_storage,
"Schema/Storage/BlockRunner pattern: storage collected after all go out of scope");
my @sorted = sort @log;
ok(grep({ $_ eq "schema" } @sorted) && grep({ $_ eq "storage" } @sorted),
"both schema and storage DESTROY fired");
}

# --- Explicit undef of blessed-no-DESTROY should release contents ---
{
my @log;
{
package NDM_TrackedE;
sub new { bless {}, shift }
sub DESTROY { push @log, "tracked" }
}
{
package NDM_HolderNoDestroyE;
sub new { bless { target => $_[1] }, $_[0] }
}
my $weak;
my $tracked = NDM_TrackedE->new;
$weak = $tracked;
weaken($weak);
my $holder = NDM_HolderNoDestroyE->new($tracked);
undef $tracked; # only holder keeps it alive
ok(defined $weak, "tracked still alive via holder");
undef $holder; # should cascade-release tracked
ok(!defined $weak,
"explicit undef of blessed-no-DESTROY holder releases tracked object");
is_deeply(\@log, ["tracked"], "DESTROY fired on tracked after holder undef");
}

# --- Array-based blessed object without DESTROY ---
{
my @log;
{
package NDM_TrackedF;
sub new { bless {}, shift }
sub DESTROY { push @log, "tracked" }
}
{
package NDM_ArrayHolder;
sub new { bless [ $_[1] ], $_[0] }
# No DESTROY
}
my $weak;
{
my $tracked = NDM_TrackedF->new;
$weak = $tracked;
weaken($weak);
my $holder = NDM_ArrayHolder->new($tracked);
}
ok(!defined $weak,
"array-based blessed-no-DESTROY releases tracked object");
}

done_testing;
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,24 @@ private static RuntimeList executeCode(RuntimeCode runtimeCode, EmitterContext c

try {
if (isMainProgram) {
// Flush deferred mortal decrements from file-scoped lexical cleanup.
// The main script's apply() runs scopeExitCleanup for all my-variables
// (deferring refCount decrements), but the MortalList is not flushed
// inside the subroutine (flush=false for blockIsSubroutine). Process
// those decrements now so objects reach refCount=0 and DESTROY fires
// BEFORE END blocks run — matching Perl 5's destruct sequence where
// file-scoped lexicals are destroyed before END block dispatch.
MortalList.flush();

// Process captured variables whose scope has exited but whose
// refCount was deferred because captureCount > 0. The interpreter
// captures ALL visible lexicals for eval STRING support, inflating
// captureCount on variables that closures don't actually use.
// Now that all scopes have exited, it's safe to decrement.
// This must happen before END blocks so that DBIC's LeakTracer
// (which runs in an END block) sees objects properly DESTROY'd.
MortalList.flushDeferredCaptures();

CallerStack.push("main", ctx.compilerOptions.fileName, 0);
try {
runEndBlocks();
Expand All @@ -414,6 +432,8 @@ private static RuntimeList executeCode(RuntimeCode runtimeCode, EmitterContext c
result = e.returnValue != null ? e.returnValue.getList() : new RuntimeList();
} catch (Throwable t) {
if (isMainProgram) {
MortalList.flush(); // Flush file-scoped lexical cleanup before END
MortalList.flushDeferredCaptures(); // Process captured vars (see above)
CallerStack.push("main", ctx.compilerOptions.fileName, 0);
try {
runEndBlocks(false); // Don't reset $? on exception path
Expand Down
Loading