Skip to content

Releases: hooji/DurableJavaThreads

v1.5.0

Choose a tag to compare

@hooji hooji released this 17 Apr 21:58
91a92e5

What's New

Portable snapshots

Snapshots now carry richer JVM metadata and can optionally bundle the user's class file bytes for fully self-contained restore.

  • SnapshotEnvironment gains six new fields populated from system properties at freeze time: javaRuntimeVersion (e.g. 21.0.2+13-LTS), javaVendor (Temurin / Oracle / Corretto / GraalVM), javaVmName (HotSpot / OpenJ9 / GraalVM), javaSpecificationVersion, osArch, and archDataModel. A restore-side tool can tell an operator exactly which JDK to provision instead of just "Java 21".
  • New opt-in Durable.setEmbedClassBytecodes(boolean) toggle. When enabled, every user class reachable from the snapshot is bundled inside SnapshotEnvironment.ClassEntry.bytecode. On restore, EmbeddedClassInstaller writes a temp jar and calls Instrumentation.appendToSystemClassLoaderSearch so missing classes become loadable. Default is off (smaller snapshots for the common same-classpath case); turn on for portable checkpoints.

Backwards compatible: older snapshots deserialize with the new fields null via Java's standard missing-field handling (SUID unchanged).

Internal refactors (no behavior change)

  • JdiHelper split into three focused files: PortEnumerator (per-OS listening-port enumeration), PortProber (parallel JDI probes + nonce verification with hard per-probe timeouts), SelfConnection (singleton self-attach lifecycle). Makes the self-attach subsystem easier to navigate and independently extractable.
  • New internal/handlers/ package: every JDK-type quirk (collections, StringBuilder, boxed primitives, BigDecimal/UUID/java.time.*, enums, java.lang.Object sentinel, opaque fail-fast, regular-object Objenesis path) now lives in a single TypeHandler file that pairs capture and restore. JdiHeapWalker dropped from ~1240 lines to ~160; HeapRestorer from ~450 to ~210. Ordering-sensitive dispatch documented in TypeHandlerRegistry.

API additions

  • Durable.setEmbedClassBytecodes(boolean) / Durable.isEmbedClassBytecodes()
  • SnapshotEnvironment.javaRuntimeVersion(), javaVendor(), javaVmName(), javaSpecificationVersion(), osArch(), archDataModel()
  • SnapshotEnvironment.ClassEntry.bytecode()

Tests

232 unit tests (+13), 46 E2E tests (+1). Every JDK-type handler retains its prior correctness; new tests cover flag toggling, snapshot round-trip with embedded bytes, and environment field round-trip.

v1.4.1

Choose a tag to compare

@hooji hooji released this 31 Mar 19:53
483f4e4

What's New

EnumSet & EnumMap Support

  • Full freeze/restore support for EnumSet (both RegularEnumSet and JumboEnumSet) and EnumMap
  • EnumSet elements captured via bitmask reading; EnumMap entries via keyUniverse/vals arrays

Improved Type Safety

  • Better UncapturableTypeException messages for threading types (ThreadLocal, locks, latches, barriers, atomics, executors/thread pools)
  • All unsupported JDK types fail fast with actionable advice

Code Improvements

  • DurableTransformer: replaced fragile per-class exclusion list with package-level prefix + whitelist
  • JdiValueConverter: removed unused restoredHeap/heapRestorer parameters
  • NullRef: singleton with readResolve() reduces allocation during heap walking
  • ObjenesisHolder: shared singleton eliminates per-call instantiation
  • waitForThreadAtMethod polling interval reduced from 10ms to 1ms for faster restore

Documentation

  • All 7 architecture docs updated to reflect current codebase
  • Stale references to removed dead code cleaned up
  • Audit items marked as resolved

Tests

  • 219 unit tests, 44 E2E integration tests (4 new unit + 1 new E2E for EnumSet/EnumMap)

v1.4.0

Choose a tag to compare

@hooji hooji released this 30 Mar 17:59
fe50b87

v1.4.0 — Architecture Documentation & Enterprise Hardening

New Documentation

  • Architecture Document (docs/ARCHITECTURE.md) — comprehensive technical
    description of the library internals: bytecode instrumentation pipeline,
    freeze/restore data flow, JDWP port discovery, thread safety model,
    serialization format, and error handling strategy

  • Code Audit (docs/CODE_AUDIT.md) — 19 findings across 4 severity levels
    covering potential bugs, robustness improvements, simplification opportunities,
    and enterprise hardening items

Bug Fixes & Hardening

  • GC protection retry loop — JdiLocalSetter.disableCollection() now retries
    up to 5 times when ObjectCollectedException is thrown during the narrow window
    between JDI resolve and object pinning. Previously, a single retry was attempted
    and if it also failed, the restore silently proceeded with an unpinned reference.
    Now throws a clear error after exhausting all attempts. This hardens restore
    against extreme GC pressure (e.g., ZGC with small regions under heavy allocation).

Tests

All 258 tests pass (215 unit + 43 E2E).

v1.3.5

Choose a tag to compare

@hooji hooji released this 29 Mar 01:06
ad5a250

v1.3.5 — Major Refactoring, Audit Fixes, and Robustness Improvements

Architecture Refactoring

ThreadRestorer decomposition (Stages 1–4):

  • Extract SnapshotValidator — bytecode/structure hash validation (Stage 1)
  • Extract ReflectionHelpers — method lookup, descriptor matching, bottom frame invocation (Stage 2a–2e)
  • Extract JdiValueConverter — stateless snapshot ObjectRef → JDI Value conversion (Stage 3)
  • Extract JdiLocalSetter — JDI local variable setting with frame matching, GC pinning, class preloading (Stage 4a–4c)
  • ThreadRestorer reduced from ~1150 lines to ~375 lines

PrologueInjector decomposition (v1.3.1):

  • Extract OperandStackSimulator — JVM operand stack simulation for type tracking
  • Extract PrologueEmitter — replay prologue bytecode emission
  • Extract PrologueTypes — shared data types
  • PrologueInjector reduced from ~1300 lines to ~280 lines

Bug Fixes

  • FrameSnapshot equals/hashCode/toString — lambdaBridgeInterface field was missing from all three methods, causing two snapshots differing only in lambda bridge interface to compare as equal
  • Snapshot handler deadlock — moved handler.accept() after threadRef.resume() so the handler no longer runs while the frozen thread is JDI-suspended; eliminates deadlock if handler acquires locks held by the frozen thread
  • FreezeFlag thread ID reuse — replaced Set<Long> (thread IDs) with Set<Thread> using IdentityHashMap; eliminates false positives from JVM thread ID recycling
  • Broken slot reflection — captureLocals used getMethod("slot") on a package-private method, which always threw NoSuchMethodException and silently defaulted every variable's slot to 0; removed the dead reflection entirely (restore matches by name, not slot)

Dead Code Removal

  • Removed 4 dead catch(Exception ignored) blocks in ThreadFreezer that were confirmed to never fire by running all 258 tests without them
  • Removed dead if/else branch in HeapRestorer.populateArray (both branches identical)
  • Removed unused isInfrastructureFrame static import from ThreadRestorer

Enterprise Improvements

  • Configurable timeouts via system properties:
    • durable.freeze.timeout.ms (default 30s) — caller thread wait for freeze worker
    • durable.jdi.wait.timeout.ms (default 30s) — JDI worker wait for replay thread
    • durable.restore.timeout.seconds (default 300s) — go-latch wait (existing)

CI

  • Removed ubuntu JDK 10 from matrix (intermittent Zulu failures unrelated to our code)
  • Bumped actions/checkout, actions/setup-java, actions/cache from v4 to v5 (Node.js 24)

Tests

All 258 tests pass (215 unit + 43 E2E) across JDK 8–25 on Linux, macOS, and Windows.

v1.3.1

Choose a tag to compare

@hooji hooji released this 27 Mar 08:58
ae6bad6

What's New in v1.3.1

JDK 10 compatibility fixes

  • JDWP port detection: getPid() now uses ProcessHandle.current().pid() on JDK 9+ instead of ManagementFactory.getRuntimeMXBean() which fails to initialize on JDK 10-14 in some environments. isJdwpOnCommandLine() now returns true (assume JDWP present) instead of false when ManagementFactory fails, so the port scanner runs instead of falling back to the wrong default port.
  • JDI stderr noise: Filtered "Ignoring cmd" diagnostic messages from JDK 10's JDWP transport layer in all E2E tests.

CI matrix

98 test combinations: JDK 8–25 on Linux and Windows, JDK 11–25 (minus 12/14) on macOS. Build + stress tests on all.

Test results

  • 131 unit tests: all pass
  • 40 E2E integration tests: all pass

v1.3.0

Choose a tag to compare

@hooji hooji released this 26 Mar 10:43
191efc5

What's New in v1.3.0

Lambda frame support

Lambdas on the call stack are now fully supported for freeze/restore. Previously, any $$Lambda frame threw LambdaFrameException. Now the library transparently handles them.

Supported patterns:

  • Lambda as Runnable / Callable thread target
  • Lambda with captured variables from enclosing scope
  • Lambda passed as a callback to user methods
  • Method references (this::method) as functional interfaces

How it works: The library skips $$Lambda dispatch frames during freeze, detects the functional interface via JDI, and creates a java.lang.reflect.Proxy bridge at restore time. The proxy delegates to the compiler-generated synthetic method (e.g., Foo.lambda$doWork$0), whose replay prologue takes over. Captured variables — which become parameters of the synthetic method — are restored by JDI in the single-pass restore.

Not supported: Stream pipelines (stream().filter().map().collect()) and deeply-chained functional APIs.

invokedynamic instrumentation fix

Fixed VerifyError in methods that create lambdas (e.g., new Thread(() -> work(), "name")). The operand stack simulator now tracks NEW as an uninitialized reference type. Invokes whose sub-stack contains uninitialized refs (between NEW and <init>) get no-op stubs instead of invalid bytecode merge paths.

Also removed legacy invokedynamic workarounds — stubs now uniformly jump to BEFORE_INVOKE for all invoke types.

Test results

  • 131 unit tests: all pass
  • 40 E2E integration tests: all pass (4 new lambda tests)

v1.2.0

Choose a tag to compare

@hooji hooji released this 26 Mar 07:46
2d1a53d

What's New in v1.2.0

Single-pass restore architecture

The restore operation has been rewritten to use a single-pass JDI local-variable setting strategy. Previously, restore used a two-phase approach (Phase 1: rebuild call stack + set parameters, Phase 2: per-frame localsReady() blocking). The new architecture eliminates Phase 2 entirely.

How it works:

  • ALL frames (deepest and non-deepest) now jump to BEFORE_INVOKE in their original code sections during replay
  • The deepest frame's invoke is the freeze() call itself — during restore, freeze() detects restore mode via a ThreadLocal flag and blocks on a go-latch
  • With every frame in its original code section, all local variables are naturally in scope
  • JDI sets ALL locals in ALL frames in a single pass
  • When RestoredThread.resume() releases the go-latch, freeze() returns normally and user code continues

Benefits:

  • Simpler, more reliable restore — no per-frame Phase 2 synchronization
  • Eliminated slot-reuse conflicts — emitLocalVariables() reverted to params-only-wide scope
  • Removed dead code: the localsReady() / resumePoint() blocking machinery is no longer used by the restore path

API improvements

  • Durable.restore() simple overloads now restore, resume, and join automatically
  • Advanced overload returns RestoredThread handle with explicit resume() control
  • All JDI work completes before restore() returns — no background threads

Bug fixes

  • Fixed VerifyError from slot-reuse in emitLocalDefaults() — now tracks actual store instructions for type inference instead of defaulting untracked slots to Object
  • Excluded com/intellij/ from instrumentation (IDE runtime classes)

Test results

  • 130 unit tests: all pass (including 3 new slot-reuse verification tests)
  • 35 E2E integration tests: all pass

v1.1.0

Choose a tag to compare

@hooji hooji released this 22 Mar 22:40
77b27b1

What's New in v1.1.0

Immutable JDK type support

Freeze/restore now correctly handles immutable JDK types whose final fields can't be set via Objenesis. Each type is captured via JDI field reads and reconstructed via parse/valueOf on restore.

Supported types: BigDecimal, BigInteger, UUID, LocalDate, LocalTime, LocalDateTime, Instant, Duration, ZonedDateTime, OffsetDateTime, Period, Year, YearMonth, MonthDay, ZoneOffset, ZoneId, URI

Enum support

Enum constants are captured by name and restored via Enum.valueOf(), preserving:

  • == identity (critical for switch statements and comparisons)
  • ordinal() values and custom enum fields
  • Use as HashMap/HashSet keys
  • Works for both user-defined enums and JDK enums like TimeUnit
  • Correctly handles Java 8 enum constants with method overrides (anonymous subclasses)

Fail-fast validation (UncapturableTypeException)

freeze() now throws UncapturableTypeException immediately when it encounters a type that cannot be restored correctly:

Type Advice
Optional Use T directly with null instead
Unmodifiable collections Use mutable collections
Thread, ClassLoader Remove from captured state
I/O / network types Close or remove before freezing
Pattern Store pattern string, recompile after restore

JDI reliability improvements

  • Double-suspend — All JDI suspend/resume operations use suspend count = 2 for resilience against spurious JDI resume events
  • Event draining — Pending JDI events from prior freeze/restore cycles are drained before each suspend to prevent races
  • Exponential backoff — Retry with up to 10 attempts (50ms → 800ms backoff) on InvalidStackFrameException
  • Pre-load classes — All classes referenced by snapshot locals are force-loaded before frame manipulation, preventing ClassNotLoadedException during restore
  • JDWP bypass for Java 8 — When JDI's classloader lookup fails for bootstrap-loaded classes on Java 8, the library sets slot values directly via JDWP reflection, bypassing the broken client-side type check
  • Automatic class loading — Classes referenced by local variables are automatically force-loaded via Class.forName() through JDI when a ClassNotLoadedException is encountered during restore

Bug fixes

  • Fixed boxed primitives (Integer, Long, etc.) inside collections losing their values during restore
  • Fixed latch deadlock when freeze worker thread completed before caller reached wait
  • Fixed JDI connection races during concurrent freeze attempts
  • Fixed field mutations being duplicated during skip-pass replay
  • Fixed resume stub using dummy receiver instead of heap-restored object

CI coverage expansion

Test matrix now covers 7 JDK versions × 3 platforms = 21 combinations:

  • JDK versions: 8, 9, 11, 17 (LTS), 21 (LTS), 24, 25
  • Platforms: Ubuntu, macOS (ARM), Windows
  • 127 unit tests + 35 integration tests per combination

v1.0.8

Choose a tag to compare

@hooji hooji released this 22 Mar 20:15
7cdfae4

What's New

Fix: Java 8 restore failures (enum == identity, ClassNotLoadedException)

Fixed enum restore failing on Java 8 with InvalidStackFrameException during JDI Phase 2 local variable setting.

Root cause: On Java 8, java.util.concurrent.TimeUnit (and potentially other classes) weren't loaded in the restore JVM. When setValue() threw ClassNotLoadedException, the fallback forceLoadClass() used invokeMethod to run Class.forName() — this implicitly resumes the thread, invalidating all cached StackFrame references. The subsequent retry setValue() then crashed with InvalidStackFrameException, causing the restore to fail silently (no enum output, no exception propagated).

Fixes:

  1. Pre-load all snapshot classes — Before any frame manipulation, iterate through all snapshot locals, extract class names from type descriptors, and force-load any unloaded classes via JDI. This eliminates ClassNotLoadedException during setValue(), avoiding the frame-invalidating forceLoadClass call entirely.

  2. Double-suspend in restore path — Apply the same double-suspend mitigation from v1.0.7's freeze fix to the restore path's Phase 1 and Phase 2 suspend()/resume() calls.

Testing

  • 127 unit tests, 35 integration tests across Java 8/9/11/25 × Ubuntu/macOS/Windows

v1.0.7

Choose a tag to compare

@hooji hooji released this 22 Mar 19:38
81b0bd6

What's New

Fix: JDI thread suspension race in multi-cycle freeze (CI fix)

Fixed InvalidStackFrameException: Thread has been resumed failures that occurred across all platforms (Ubuntu, Windows, macOS) during multi-cycle freeze/restore tests.

Root cause: When the JDI connection is reused across freeze cycles (by design — vm.dispose() is not called to avoid spurious stderr output), pending events from prior cycles can race with threadRef.suspend() and resume the thread before frame data is read.

Fix (three-pronged):

  1. Drain pending JDI events before each suspend attempt — clears stale resume-triggering events accumulated from prior freeze/restore cycles
  2. Double-suspend the thread (suspend count = 2) so a single spurious resume event doesn't drop the count to 0 and allow the thread to run
  3. Exponential backoff with up to 10 retries (50ms → 800ms) instead of fixed 50ms × 5 attempts

Also included (from v1.0.6)

  • Freeze/restore of immutable JDK types (BigDecimal, UUID, java.time.*, URI)
  • Enum capture by name with == identity preservation
  • Fail-fast UncapturableTypeException for types that can't be restored correctly
  • Automatic class loading via JDI during cross-JVM restore
  • Fixed boxed primitive restoration and latch deadlock

Testing

  • 127 unit tests, 35 integration tests across Java 8/9/11/25 × Ubuntu/macOS/Windows