Skip to content

#776 Make sure all closeables opened in another thread are closed on successful path.#777

Merged
yruslan merged 2 commits into
mainfrom
feature/776-add-more-closeable-safeguards
Jul 10, 2026
Merged

#776 Make sure all closeables opened in another thread are closed on successful path.#777
yruslan merged 2 commits into
mainfrom
feature/776-add-more-closeable-safeguards

Conversation

@yruslan

@yruslan yruslan commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Closes #776

Overview

Makes sure all closeables opened in another thread are closed on successful path.

Release Notes

  • Improved handling of interrupted database connection attempts by stopping retries immediately.
  • Improved cleanup of timed-out operations and registered resources.
  • Prevented resources from being closed more than once.
  • Strengthened JDBC connection shutdown handling.

Related

Summary by CodeRabbit

  • Documentation

    • Corrected grammar in JDBC connection documentation.
  • Tests

    • Added coverage for avoiding duplicate resource closure.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The changes improve thread-owned resource cleanup, coordinate JDBC connection closure with the registry, wait after interrupting timed-out threads, and stop JDBC retries when interrupted. A Scaladoc grammar correction and a registry cleanup test are also included.

Changes

Thread resource cleanup

Layer / File(s) Summary
Registry-managed close lifecycle
pramen/core/src/main/scala/za/co/absa/pramen/core/runner/task/ThreadClosableRegistry.scala, pramen/core/src/main/scala/za/co/absa/pramen/core/utils/hive/QueryExecutorJdbc.scala, pramen/core/src/test/.../ThreadClosableRegistrySuite.scala
ThreadClosableRegistry adds unregister-before-close handling and snapshot-based LIFO cleanup; QueryExecutorJdbc.close() uses it, and tests cover already-closed resources.
Timeout interruption and cleanup wait
pramen/core/src/main/scala/za/co/absa/pramen/core/utils/ThreadUtils.scala
runWithTimeout waits at least 1000 ms after interrupting an alive thread, captures its state, and logs registered closeable counts before timeout handling.

JDBC retry interruption

Layer / File(s) Summary
Connection retry and interruption handling
pramen/core/src/main/scala/za/co/absa/pramen/core/reader/JdbcUrlSelectorImpl.scala, pramen/core/src/main/scala/za/co/absa/pramen/core/reader/JdbcUrlSelector.scala
InterruptedException is rethrown without retry or backoff, ordinary failures retain retry behavior, and the JDBC connection Scaladoc grammar is corrected.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ThreadUtils
  participant WorkerThread
  participant ThreadClosableRegistry
  participant QueryExecutorJdbc
  ThreadUtils->>WorkerThread: interrupt after timeout
  ThreadUtils->>WorkerThread: wait for closeWaitMillis
  ThreadUtils->>ThreadClosableRegistry: cleanupThread(threadId)
  ThreadClosableRegistry->>QueryExecutorJdbc: close registered connection
  QueryExecutorJdbc->>ThreadClosableRegistry: closeCloseable(conn)
  ThreadClosableRegistry-->>QueryExecutorJdbc: unregister and close
Loading

Possibly related PRs

  • AbsaOSS/pramen#723 — Related timeout JDBC auto-close wiring uses the same registry and call sites.
  • AbsaOSS/pramen#773 — Related thread-timeout cleanup and interruption handling changes.
  • AbsaOSS/pramen#641 — Related JDBC connection creation behavior in getNewConnection.

Poem

I’m a bunny with a cleanup plan,
Closing each connection as fast as I can.
Interrupts now stop retries in flight,
Threads wait calmly through the night.
LIFO hops and locks grow light! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement success-path cleanup for closeables in separate threads, which satisfies issue #776.
Out of Scope Changes check ✅ Passed The changes are focused on closeable cleanup and thread handling, with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title matches the main change: closing closeables from another thread on the successful path.
Description check ✅ Passed The description follows the template with Overview, Release Notes, and the linked issue, though Related could be filled out more explicitly.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/776-add-more-closeable-safeguards

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
pramen/core/src/main/scala/za/co/absa/pramen/core/reader/JdbcUrlSelectorImpl.scala (1)

154-167: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

InterruptedException handling correctly bypasses retry and backoff.

The pattern match rethrows InterruptedException immediately, while preserving the existing retry/backoff flow for all other Throwable cases. Thread.sleep at line 162 is outside the Try block, so an interruption during backoff also propagates directly without retrying — exactly the desired behavior. The downstream caller in QueryExecutorJdbc.executeActionOnConnection (lines 119-147) catches and rethrows InterruptedException without swallowing it, so the propagation chain is intact.

One minor note: the inner case ex: InterruptedException shadows the outer ex from Failure(ex). This is valid Scala but could be slightly clearer with a distinct name (e.g., ie), though it's not a functional concern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@pramen/core/src/main/scala/za/co/absa/pramen/core/reader/JdbcUrlSelectorImpl.scala`
around lines 154 - 167, Rename the pattern-bound variable in the
InterruptedException branch of getNewConnection from ex to a distinct name such
as ie, and rethrow that renamed variable; leave the existing retry and backoff
handling for other Throwable cases unchanged.
pramen/core/src/main/scala/za/co/absa/pramen/core/runner/task/ThreadClosableRegistry.scala (1)

47-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Scaladoc is misleading and has grammar/typo issues for this new public method.

The doc block appears copied from registerCloseable: line 49 claims the resource is closed later "when cleanupThread is called", but closeCloseable closes it immediately; the @param says "to register" instead of "to close"; and line 48 reads "unregisters is as an transactional atomic operation". Also note the close happens outside the synchronized unregisterCloseable, so it is not truly atomic. The log message on line 57 has a typo ("closable").

📝 Suggested doc/message fixes
-  /**
-    * Closes a closeable resource and unregisters is as an transactional atomic operation.
-    * The resource will be automatically closed when [[cleanupThread]] is called for this thread.
-    *
-    * `@param` closeable The AutoCloseable resource to register
-    */
+  /**
+    * Unregisters a closeable resource from the registry and then closes it.
+    * Close exceptions are logged and swallowed; they are not rethrown.
+    *
+    * `@param` closeable The AutoCloseable resource to close
+    */
   def closeCloseable(closeable: AutoCloseable): Unit = {
     unregisterCloseable(closeable)

     try {
-      log.info(s"Closing closable of type ${closeable.getClass.getCanonicalName}...")
+      log.info(s"Closing closeable of type ${closeable.getClass.getCanonicalName}...")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@pramen/core/src/main/scala/za/co/absa/pramen/core/runner/task/ThreadClosableRegistry.scala`
around lines 47 - 62, Correct the Scaladoc for closeCloseable to describe
immediate closing and unregistration, fix the grammar and “an transactional”
typo, and change the `@param` text to say the resource is being closed. Avoid
claiming the operation is atomic, since close occurs outside unregisterCloseable
synchronization. Also fix the log message in closeCloseable to use “closeable”
instead of “closable”.
pramen/core/src/main/scala/za/co/absa/pramen/core/utils/ThreadUtils.scala (1)

64-83: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

The reworked sequencing is correct: capturing the stack trace before interrupt(), then join(closeWaitMillis) to let the thread self-clean, and reusing the precomputed isAlive for the later timeout branch all look right. Using Math.max(..., MIN_WAIT_AFTER_INTERRUPT_MS) also correctly prevents thread.join(0) from blocking indefinitely when cleanupTimeout is zero.

One operational note: with the default cleanupTimeout of 5 minutes, closeWaitMillis becomes 5 minutes, and if closeables remain the subsequent DetachedRunService.runWithTimeoutThenDetach(cleanupTimeout) can wait up to another cleanupTimeout. In the worst case the calling (runner) thread can block for roughly 2 × cleanupTimeout. Confirm this combined budget is acceptable for the task runner, or consider deriving the post-interrupt wait from a smaller bound.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pramen/core/src/main/scala/za/co/absa/pramen/core/utils/ThreadUtils.scala`
around lines 64 - 83, Review the timeout budget in the thread cleanup flow
around the thread-closing method: after the initial wait, closeWaitMillis may
consume the full cleanupTimeout before
DetachedRunService.runWithTimeoutThenDetach(cleanupTimeout) waits again. Confirm
that the potential 2×cleanupTimeout blocking period is acceptable; otherwise
derive closeWaitMillis from a smaller bounded post-interrupt timeout while
preserving the minimum wait safeguard.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@pramen/core/src/main/scala/za/co/absa/pramen/core/reader/JdbcUrlSelectorImpl.scala`:
- Around line 154-167: Rename the pattern-bound variable in the
InterruptedException branch of getNewConnection from ex to a distinct name such
as ie, and rethrow that renamed variable; leave the existing retry and backoff
handling for other Throwable cases unchanged.

In
`@pramen/core/src/main/scala/za/co/absa/pramen/core/runner/task/ThreadClosableRegistry.scala`:
- Around line 47-62: Correct the Scaladoc for closeCloseable to describe
immediate closing and unregistration, fix the grammar and “an transactional”
typo, and change the `@param` text to say the resource is being closed. Avoid
claiming the operation is atomic, since close occurs outside unregisterCloseable
synchronization. Also fix the log message in closeCloseable to use “closeable”
instead of “closable”.

In `@pramen/core/src/main/scala/za/co/absa/pramen/core/utils/ThreadUtils.scala`:
- Around line 64-83: Review the timeout budget in the thread cleanup flow around
the thread-closing method: after the initial wait, closeWaitMillis may consume
the full cleanupTimeout before
DetachedRunService.runWithTimeoutThenDetach(cleanupTimeout) waits again. Confirm
that the potential 2×cleanupTimeout blocking period is acceptable; otherwise
derive closeWaitMillis from a smaller bounded post-interrupt timeout while
preserving the minimum wait safeguard.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ccdc12c9-aacc-4580-81b8-164ffec37723

📥 Commits

Reviewing files that changed from the base of the PR and between 61e85d7 and 353f877.

📒 Files selected for processing (6)
  • pramen/core/src/main/scala/za/co/absa/pramen/core/reader/JdbcUrlSelector.scala
  • pramen/core/src/main/scala/za/co/absa/pramen/core/reader/JdbcUrlSelectorImpl.scala
  • pramen/core/src/main/scala/za/co/absa/pramen/core/runner/task/ThreadClosableRegistry.scala
  • pramen/core/src/main/scala/za/co/absa/pramen/core/utils/ThreadUtils.scala
  • pramen/core/src/main/scala/za/co/absa/pramen/core/utils/hive/QueryExecutorJdbc.scala
  • pramen/core/src/test/scala/za/co/absa/pramen/core/tests/runner/task/ThreadClosableRegistrySuite.scala

@github-actions

Copy link
Copy Markdown

Unit Test Coverage

Overall Project 76.83% -0.11% 🍏
Files changed 57.68%

Module Coverage
pramen:core Jacoco Report 77.81% -0.12%
Files
Module File Coverage
pramen:core Jacoco Report JdbcUrlSelector.scala 100% 🍏
ThreadUtils.scala 84.81% 🍏
QueryExecutorJdbc.scala 79.64% -0.42% 🍏
ThreadClosableRegistry.scala 75% -16.96%
JdbcUrlSelectorImpl.scala 70.54% -10.49%

@yruslan
yruslan merged commit 9f1da9e into main Jul 10, 2026
7 checks passed
@yruslan
yruslan deleted the feature/776-add-more-closeable-safeguards branch July 10, 2026 07:59
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.

When running Pramen tasks in a separate thread, make sure all closeables are closed

1 participant