Skip to content

Offload blocking database and service calls from fx thread#466

Merged
DropSnorz merged 2 commits into
masterfrom
fix/blocking-calls
May 6, 2026
Merged

Offload blocking database and service calls from fx thread#466
DropSnorz merged 2 commits into
masterfrom
fix/blocking-calls

Conversation

@DropSnorz
Copy link
Copy Markdown
Owner

No description provided.

@DropSnorz DropSnorz self-assigned this May 4, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Walkthrough

This PR systematically refactors multiple controllers to use JavaFX ObjectProperty bindings for state management, replacing direct field references with property-driven reactive patterns. Concurrently, several blocking operations are made asynchronous using CompletableFuture, and plugin state precedence logic is adjusted to prioritize disabled status over incomplete scans.

Changes

Property-Driven Controller Refactoring

Layer / File(s) Summary
Property Foundation
src/main/java/com/owlplug/plugin/controllers/DirectoryInfoController.java, PluginInfoController.java, ComponentInfoController.java, SymlinkInfoController.java, src/main/java/com/owlplug/project/controllers/ProjectInfoController.java
Each controller introduces an ObjectProperty<T> field (e.g., pluginDirectoryProperty, pluginProperty, componentProperty, symlinkProperty, projectProperty) with corresponding public accessor methods and necessary JavaFX property imports.
Listener & Refresh Setup
DirectoryInfoController.java, PluginInfoController.java, ComponentInfoController.java, SymlinkInfoController.java, ProjectInfoController.java
Each controller's initialize() method registers a listener on its property to trigger refresh() on value changes. New refresh() methods consolidate UI update and data-loading logic previously scattered across setters.
Business Logic Migration
DirectoryInfoController.java, PluginInfoController.java, ComponentInfoController.java, SymlinkInfoController.java, ProjectInfoController.java
Replaces direct field access (e.g., this.plugin, this.symlink) with property getter calls (e.g., pluginProperty.get(), symlinkProperty.get()). Old setter methods (setPlugin(), setSymlink(), etc.) are removed in favor of property-based bindings.
Integration & Wiring
src/main/java/com/owlplug/plugin/controllers/NodeInfoController.java, src/main/java/com/owlplug/project/controllers/ProjectsController.java
Updates call sites to use the new property accessors with .set() instead of direct setters (e.g., pluginInfoController.pluginProperty().set(...) replaces setPlugin(...)). NodeInfoController applies this pattern across all four node type controllers.

Asynchronous Operations & State Logic

Layer / File(s) Summary
Async Operation Wrapping
src/main/java/com/owlplug/plugin/controllers/PluginTableController.java, PluginsController.java, PluginInfoController.java, src/main/java/com/owlplug/plugin/ui/RecoveredPluginView.java
Plugin enabling, plugin discovery loading, footprint persistence, and state updates are wrapped in CompletableFuture.runAsync() or CompletableFuture.supplyAsync() with results applied on the FX thread via FX.run(...). Adds java.util.concurrent.CompletableFuture imports across affected files.
State Precedence & Event Handling
src/main/java/com/owlplug/plugin/services/PluginService.java, PluginInfoController.java
PluginService reorders state checks so plugin.isDisabled() is evaluated before plugin.isScanComplete(), changing returned state when both apply. PluginInfoController adds an @EventListener handler for PluginRefreshEvent to trigger UI refresh.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • DropSnorz/OwlPlug#454: The asynchronous refactoring of PluginsController.displayPlugins() to load plugins via CompletableFuture.supplyAsync directly addresses the blocking pluginRepository.findAll() issue.

Possibly related PRs

  • DropSnorz/OwlPlug#280: Both modify PluginInfoController, PluginService, and PluginTableController with overlapping state and plugin management changes.
  • DropSnorz/OwlPlug#449: Both refactor PluginInfoController; PR #449 adds event publishing while this PR introduces property binding and async operations.
  • DropSnorz/OwlPlug#359: Both modify DirectoryInfoController; this PR refactors it to property-driven patterns while PR #359 changes post-removal task scheduling.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No description was provided by the author, making it impossible to assess relevance to the changeset. Add a pull request description explaining the motivation for offloading blocking calls from the FX thread, the property-driven refactor approach, and any testing performed.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main objective of the pull request—offloading blocking database and service calls from the FX thread, which is demonstrated throughout the changeset with CompletableFuture usage and property-driven refactors.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix/blocking-calls

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

@DropSnorz DropSnorz added this to the 1.33.0 milestone May 4, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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.

Inline comments:
In
`@owlplug-client/src/main/java/com/owlplug/plugin/controllers/DirectoryInfoController.java`:
- Around line 180-188: The repository queries in setPluginDirectory are running
on the JavaFX thread: replace the direct calls to
fileStatRepository.findByPath(path) and
fileStatRepository.findByParentPathOrderByLengthDesc(path) with an asynchronous
task (e.g. CompletableFuture.supplyAsync) and then marshal UI updates back onto
the FX thread (using FX.run() or Platform.runLater). Fetch both
Optional<FileStat> and List<FileStat> off the FX thread, then inside the FX
callback update directoryMetricsTab, directoryFilesTab, directoryFilesTableView,
pieChart and call createStatChartBuckets and pieChart.layout() so all DB work is
off the UI thread and only UI mutations happen on FX.

In
`@owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginInfoController.java`:
- Around line 143-145: The event handlers (e.g., the lambda passed to
enableButton.setOnAction that calls pluginService.enablePlugin(plugin)) close
over the mutable controller field plugin which can change before the async task
runs; capture the currently selected Plugin/PluginFootprint into a local final
variable (e.g., selectedPlugin or pluginToEnable) immediately inside the event
handler and use that local inside CompletableFuture.runAsync(...) so the
background work acts on the snapshot; apply the same fix to the other handlers
mentioned (lines 149-153) that similarly reference the field.

In
`@owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginsController.java`:
- Around line 209-213: displayPlugins can race: add a monotonically increasing
request id (e.g., an AtomicLong field like latestLoadId) that displayPlugins
increments and captures into a local variable before calling
CompletableFuture.supplyAsync(pluginRepository::findAll); in the thenAccept
handler compare the captured id with the current latestLoadId and only call
FX.run(() -> { treeViewController.setPlugins(...);
tableController.setPlugins(...); }) if they match, so stale async results (from
pluginRepository.findAll) do not overwrite newer loads.

In
`@owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginTableController.java`:
- Around line 233-235: The async call in enableItem.setOnAction uses
CompletableFuture.runAsync(() -> pluginService.enablePlugin(plugin)) but never
observes failures; attach a completion handler to log errors and update the UI
so failures do not fail silently. Replace the bare runAsync call with a chained
handler on the returned CompletableFuture (e.g., whenComplete or exceptionally)
that logs the thrown exception (using your logger) and calls Platform.runLater
to refresh the plugin table or show an error dialog; ensure you still refresh
the UI on success (call the existing refresh method) and reference
enableItem.setOnAction, pluginService.enablePlugin(plugin), and
CompletableFuture.runAsync in your changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: df649f35-525b-46c1-9ab2-e491a26fee48

📥 Commits

Reviewing files that changed from the base of the PR and between bac75b6 and cd85f23.

📒 Files selected for processing (6)
  • owlplug-client/src/main/java/com/owlplug/plugin/controllers/DirectoryInfoController.java
  • owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginInfoController.java
  • owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginTableController.java
  • owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginsController.java
  • owlplug-client/src/main/java/com/owlplug/plugin/services/PluginService.java
  • owlplug-client/src/main/java/com/owlplug/plugin/ui/RecoveredPluginView.java

@DropSnorz
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
owlplug-client/src/main/java/com/owlplug/project/controllers/ProjectInfoController.java (1)

194-209: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard refresh() against null project values to prevent UI crashes.

refresh() assumes projectProperty.get() is always non-null. If the property is cleared (or refresh is called before selection), this will NPE on Line 197+.

💡 Suggested fix
 public void refresh() {
   DawProject project = projectProperty.get();
+  if (project == null) {
+    projectInfoPane.setVisible(false);
+    projectOpenButton.setDisable(true);
+    pluginTable.setItems(FXCollections.observableArrayList());
+    return;
+  }
   projectInfoPane.setVisible(true);
   projectNameLabel.setText(project.getName());
   projectAppLabel.setText(project.getApplication().getName());
   projectAppImageView.setImage(this.getApplicationDefaults().getDAWApplicationIcon(project.getApplication()));
   projectOpenButton.setDisable(false);
🤖 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
`@owlplug-client/src/main/java/com/owlplug/project/controllers/ProjectInfoController.java`
around lines 194 - 209, The refresh() method assumes projectProperty.get() is
non-null; add a null check at the start of refresh() to safely handle
cleared/no-selection cases: if projectProperty.get() is null, hide or clear the
UI (e.g., projectInfoPane.setVisible(false)), disable/clear controls
(projectOpenButton.setDisable(true), clear labels like projectNameLabel,
projectAppLabel, appFullNameLabel, projectCreatedLabel,
projectLastModifiedLabel, projectPluginsFoundLabel, projectFormatVersionLabel,
projectPathLabel) and set
pluginTable.setItems(FXCollections.emptyObservableList()), then return;
otherwise proceed with the existing code that reads project fields. Ensure you
reference projectProperty, refresh(), projectInfoPane, projectOpenButton, all
label fields and pluginTable when implementing the guard.
owlplug-client/src/main/java/com/owlplug/plugin/controllers/DirectoryInfoController.java (1)

174-201: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a null guard in refresh() (consistent with PluginInfoController.refresh()).

pluginDirectoryProperty.get() is dereferenced on line 176 without a null check. If the property is ever reset to null, the listener triggers and this NPEs. The pattern in PluginInfoController.refresh() (early return on null) is worth mirroring here for safety.

🛡️ Proposed guard
   public void refresh() {
     PluginDirectory pluginDirectory = pluginDirectoryProperty.get();
+    if (pluginDirectory == null) {
+      return;
+    }
     directoryPathTextField.setText(pluginDirectory.getPath());
🤖 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
`@owlplug-client/src/main/java/com/owlplug/plugin/controllers/DirectoryInfoController.java`
around lines 174 - 201, In DirectoryInfoController.refresh(), guard against
pluginDirectoryProperty being null before dereferencing: fetch PluginDirectory
via pluginDirectoryProperty.get(), and if it is null return early (same pattern
used in PluginInfoController.refresh()); update subsequent usage of
pluginDirectory (getPath/getName/getPluginList) to assume non-null after this
check so the method doesn't NPE when the property is reset.
owlplug-client/src/main/java/com/owlplug/plugin/controllers/ComponentInfoController.java (1)

89-103: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a null guard in refresh() (consistent with PluginInfoController.refresh()).

componentProperty.get() is dereferenced on line 91 without a null check. If the property is reset to null, the listener fires and the body NPEs. Mirror the early-return pattern from PluginInfoController.refresh() here.

🛡️ Proposed guard
   public void refresh() {
     PluginComponent component = componentProperty.get();
+    if (component == null) {
+      return;
+    }
     pluginFormatIcon.setImage(this.getApplicationDefaults().getPluginFormatIcon(component.getPlugin().getFormat()));
🤖 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
`@owlplug-client/src/main/java/com/owlplug/plugin/controllers/ComponentInfoController.java`
around lines 89 - 103, The refresh() method dereferences componentProperty.get()
without a null guard which can NPE when the property is reset; add the same
early-return null check used in PluginInfoController.refresh(): get the
PluginComponent via componentProperty.get(), if it is null return immediately
(or clear/reset the UI as PluginInfoController does) before accessing component,
so all subsequent uses (pluginFormatIcon, pluginFormatLabel, pluginTitleLabel,
etc.) are protected.
owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginInfoController.java (1)

207-218: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Move tryResolveAndSaveImageUrl() off the FX thread using async pattern.

setPluginImage() runs on the FX thread (invoked from refresh() which is called via FX.run() at line 274). At line 215, pluginService.tryResolveAndSaveImageUrl(plugin) performs blocking operations: resolveImageUrl() queries the remote package repository, and pluginFootprintRepository.save() writes to the database. Line 217 immediately calls getScreenshotUrl(), relying on the side-effect from line 215 to have completed synchronously, blocking the UI thread.

Use CompletableFuture.runAsync() to perform the resolve/save off-thread, then reapply the image via FX.run() once complete. This matches the async pattern already used elsewhere in this controller (lines 150, 159).

🤖 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
`@owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginInfoController.java`
around lines 207 - 218, setPluginImage() is performing blocking resolve/save
work on the JavaFX thread by calling
pluginService.tryResolveAndSaveImageUrl(plugin) and then immediately re-reading
plugin.getFootprint().getScreenshotUrl(); move the blocking call off the FX
thread using CompletableFuture.runAsync(() ->
pluginService.tryResolveAndSaveImageUrl(plugin)) and, when that completes,
re-read the footprint screenshot URL and update the UI inside FX.run(() -> { ...
}) so setPluginImage() only triggers async work and UI updates occur on the FX
thread; ensure you still handle null/empty checks on plugin.getScreenshotUrl()
and plugin.getFootprint().getScreenshotUrl() and that the final
getScreenshotUrl() used to set the image is obtained after the async completion.
🤖 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.

Inline comments:
In
`@owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginInfoController.java`:
- Around line 148-161: The async fire-and-forget calls in
enableButton.setOnAction (which calls pluginService.enablePlugin) and the
nativeDiscoveryToggleButton listener (which calls pluginService.save on
plugin.getFootprint()) must handle exceptions and surface them to the UI: change
CompletableFuture.runAsync(...) to attach whenComplete/exceptionally handlers
that route errors to the existing notification/dialog plumbing and log them; for
the nativeDiscoveryToggleButton listener also revert the toggle state on failure
via Platform.runLater using pluginProperty.get() to restore the previous value
so the visible state matches the persisted state.

In
`@owlplug-client/src/main/java/com/owlplug/plugin/controllers/SymlinkInfoController.java`:
- Around line 108-114: The refresh() method dereferences symlinkProperty.get()
without a null check; mirror PluginInfoController behavior by early-returning if
symlinkProperty.get() == null to avoid NPEs. In refresh(), fetch Symlink symlink
= symlinkProperty.get(); if symlink is null return immediately; otherwise
continue to set directoryPathLabel,
pluginDirectoryListView.getItems().setAll(symlink.getPluginList()),
targetPathLabel (using Optional.ofNullable...), and
openTargetButton.setVisible(...) so behavior remains identical when a property
is unset.

---

Outside diff comments:
In
`@owlplug-client/src/main/java/com/owlplug/plugin/controllers/ComponentInfoController.java`:
- Around line 89-103: The refresh() method dereferences componentProperty.get()
without a null guard which can NPE when the property is reset; add the same
early-return null check used in PluginInfoController.refresh(): get the
PluginComponent via componentProperty.get(), if it is null return immediately
(or clear/reset the UI as PluginInfoController does) before accessing component,
so all subsequent uses (pluginFormatIcon, pluginFormatLabel, pluginTitleLabel,
etc.) are protected.

In
`@owlplug-client/src/main/java/com/owlplug/plugin/controllers/DirectoryInfoController.java`:
- Around line 174-201: In DirectoryInfoController.refresh(), guard against
pluginDirectoryProperty being null before dereferencing: fetch PluginDirectory
via pluginDirectoryProperty.get(), and if it is null return early (same pattern
used in PluginInfoController.refresh()); update subsequent usage of
pluginDirectory (getPath/getName/getPluginList) to assume non-null after this
check so the method doesn't NPE when the property is reset.

In
`@owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginInfoController.java`:
- Around line 207-218: setPluginImage() is performing blocking resolve/save work
on the JavaFX thread by calling pluginService.tryResolveAndSaveImageUrl(plugin)
and then immediately re-reading plugin.getFootprint().getScreenshotUrl(); move
the blocking call off the FX thread using CompletableFuture.runAsync(() ->
pluginService.tryResolveAndSaveImageUrl(plugin)) and, when that completes,
re-read the footprint screenshot URL and update the UI inside FX.run(() -> { ...
}) so setPluginImage() only triggers async work and UI updates occur on the FX
thread; ensure you still handle null/empty checks on plugin.getScreenshotUrl()
and plugin.getFootprint().getScreenshotUrl() and that the final
getScreenshotUrl() used to set the image is obtained after the async completion.

In
`@owlplug-client/src/main/java/com/owlplug/project/controllers/ProjectInfoController.java`:
- Around line 194-209: The refresh() method assumes projectProperty.get() is
non-null; add a null check at the start of refresh() to safely handle
cleared/no-selection cases: if projectProperty.get() is null, hide or clear the
UI (e.g., projectInfoPane.setVisible(false)), disable/clear controls
(projectOpenButton.setDisable(true), clear labels like projectNameLabel,
projectAppLabel, appFullNameLabel, projectCreatedLabel,
projectLastModifiedLabel, projectPluginsFoundLabel, projectFormatVersionLabel,
projectPathLabel) and set
pluginTable.setItems(FXCollections.emptyObservableList()), then return;
otherwise proceed with the existing code that reads project fields. Ensure you
reference projectProperty, refresh(), projectInfoPane, projectOpenButton, all
label fields and pluginTable when implementing the guard.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5a922c88-ace1-4038-9068-7aa5fbd507ac

📥 Commits

Reviewing files that changed from the base of the PR and between cd85f23 and 5ebb20a.

📒 Files selected for processing (7)
  • owlplug-client/src/main/java/com/owlplug/plugin/controllers/ComponentInfoController.java
  • owlplug-client/src/main/java/com/owlplug/plugin/controllers/DirectoryInfoController.java
  • owlplug-client/src/main/java/com/owlplug/plugin/controllers/NodeInfoController.java
  • owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginInfoController.java
  • owlplug-client/src/main/java/com/owlplug/plugin/controllers/SymlinkInfoController.java
  • owlplug-client/src/main/java/com/owlplug/project/controllers/ProjectInfoController.java
  • owlplug-client/src/main/java/com/owlplug/project/controllers/ProjectsController.java

@DropSnorz DropSnorz merged commit ef4dbe4 into master May 6, 2026
4 checks passed
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