Fix threading issues in citations relations tab#15233
Conversation
Use cooperative cancellation
Review Summary by QodoImplement cooperative cancellation in citations relations tab
WalkthroughsDescription• Implement cooperative cancellation in citation relations tab - Pass cancellation token to search methods - Use cancel(false) for non-interruptible cancellation - Add mayInterruptIfRunning property to BackgroundTask • Refactor MVStore initialization into MVStoreBase - Extract common MVStore setup logic - Add null checks for safe resource cleanup - Improve error handling with NotificationService • Add cancellation checks in search operations - Check isCancelled after fetching citations/references - Return empty list if cancelled mid-operation Diagramflowchart LR
A["CitationRelationsTab"] -->|passes cancellation token| B["SearchCitationsRelationsService"]
B -->|checks isCancelled| C["Citation/Reference Fetcher"]
C -->|returns early if cancelled| D["Empty List"]
A -->|calls cancel with flag| E["BackgroundTask"]
E -->|sets mayInterruptIfRunning| F["UiTaskExecutor"]
G["BibEntryCitationsAndReferencesRepositoryShell"] -->|extends| H["MVStoreBase"]
H -->|manages MVStore lifecycle| I["Safe Resource Cleanup"]
File Changes1. jabgui/src/main/java/org/jabref/gui/JabRefGUI.java
|
Code Review by Qodo
1. Racy task cancel supplier
|
| citingTask = BackgroundTask.wrap( | ||
| () -> this.searchCitationsRelationsService.searchCites(entry, bypassCache, citingTask::isCancelled) | ||
| ); | ||
| yield citingTask; | ||
| } | ||
| case CitationFetcher.SearchType.CITED_BY -> { | ||
| this.citedByTask = BackgroundTask.wrap( | ||
| () -> this.searchCitationsRelationsService.searchCitedBy(entry, bypassCache) | ||
| citedByTask = BackgroundTask.wrap( | ||
| () -> this.searchCitationsRelationsService.searchCitedBy(entry, bypassCache, citedByTask::isCancelled) | ||
| ); |
There was a problem hiding this comment.
1. Racy task cancel supplier 🐞 Bug ⛯ Reliability
createBackgroundTask passes citingTask::isCancelled / citedByTask::isCancelled by reading a mutable static field at execution time; if the field is nulled/replaced while the JavaFX task is queued or running, the method-reference evaluation can NPE or bind cancellation checks to the wrong task, causing incorrect early-return behavior and/or stale repository writes.
Agent Prompt
### Issue description
`CitationRelationsTab#createBackgroundTask` uses `citingTask::isCancelled` / `citedByTask::isCancelled` inside the task callable. Because these are **static mutable fields** that are later set to `null`/overwritten, the callable can evaluate the method reference against `null` or the wrong task when it starts (tasks are queued on an executor), leading to NPEs or incorrect cancellation decisions.
### Issue Context
We need the `BooleanSupplier` passed to `SearchCitationsRelationsService` to reliably represent *the task that is currently executing*, regardless of what the UI thread does with `citingTask`/`citedByTask` fields.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java[825-841]
- jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java[702-713]
### Implementation sketch
- Replace `BackgroundTask.wrap(() -> service.searchCites(..., citingTask::isCancelled))` with a task instance whose `call()` method uses `this::isCancelled`.
- Assign the created task to `citingTask`/`citedByTask` *after* creation.
- Ensure cancellation code paths don’t create a window where `citingTask` is set to null while queued tasks still depend on it.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| super(Path.of(""), _ -> { }); // Dummy Path for testing | ||
| this.citationsDao = citationsDao; | ||
| this.referencesDao = referencesDao; | ||
| this.mvStore = null; | ||
| } |
There was a problem hiding this comment.
2. Empty path in mvstore test ctor 🐞 Bug ⛯ Reliability
The @VisibleForTesting constructor now calls super(Path.of(""), ...), which MVStoreBase forwards
to .fileName(mvStorePath.toString()); for an empty path this is an empty string, which may cause
MVStore to attempt to open an invalid/ambiguous filename or fail during tests.
Agent Prompt
### Issue description
`BibEntryCitationsAndReferencesRepositoryShell`’s `@VisibleForTesting` constructor calls `super(Path.of(""), ...)`. `MVStoreBase` forwards `path.toString()` to `MVStore.Builder.fileName(...)`, so an empty string is passed to MVStore, which can lead to failures or unintended filesystem behavior during tests.
### Issue Context
The intent of the `@VisibleForTesting` constructor is to avoid real disk IO and use mocks; it should not need to open a file-backed MVStore at all.
### Fix Focus Areas
- jablib/src/main/java/org/jabref/logic/citation/repository/BibEntryCitationsAndReferencesRepositoryShell.java[48-56]
- jablib/src/main/java/org/jabref/logic/ai/util/MVStoreBase.java[33-37]
### Implementation options
1) **Preferred:** Add a `protected MVStoreBase(NotificationService)` (or similar) that opens an in-memory store (`fileName(null)`), and use that from the testing constructor.
2) Add a guard in `MVStoreBase`:
- compute `String fileName = mvStorePath == null ? null : mvStorePath.toString();`
- if `fileName != null && fileName.isBlank()` then set it to `null`
- pass `fileName` to `.fileName(...)`.
Either approach makes test behavior deterministic and avoids passing an empty filename to MVStore.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
✅ All tests passed ✅🏷️ Commit: 3fcf91d Learn more about TestLens at testlens.app. |
subhramit
left a comment
There was a problem hiding this comment.
Understood at a high-level, may need a second look
…rg.openrewrite.recipe-rewrite-recipe-bom-3.25.0 * upstream/main: (35 commits) Chore: add dependency-management.md (#15278) Chore(deps): Bump dev.langchain4j:langchain4j-bom in /versions (#15277) New Crowdin updates (#15274) Chore(deps): Bump actions/upload-artifact from 6 to 7 (#15271) Chore(deps): Bump actions/download-artifact from 7 to 8 (#15270) Chore(deps): Bump docker/login-action from 3 to 4 (#15268) Fix threading issues in citations relations tab (#15233) Fix: Citavi XML importer now preserves citation keys (#14658) (#15257) Preserve no break spaces in Latex to Unicode conversion (#15174) Fix: open javafx.scene.control.skin to controlsfx (#15260) Reduce complexity in dependencies setup (restore) (#15194) New translations jabref_en.properties (French) (#15256) Fix: exception dialog shows up when moving sidepanel down/up (#15248) Implement reset for Name Display Preferences (#15136) Chore(deps): Bump net.bytebuddy:byte-buddy in /versions (#15252) Chore(deps): Bump io.zonky.test.postgres:embedded-postgres-binaries-bom (#15253) Chore(deps): Bump io.zonky.test:embedded-postgres in /versions (#15254) Chore(deps): Bump net.ltgt.errorprone from 5.0.0 to 5.1.0 in /jablib (#15251) New Crowdin updates (#15247) Refined the "Select files to import" page in "Search for unlinked local files" dialog (#15110) ...
Use cooperative cancellation
When navigating quickly up and down the entries in citations tabs, there were mvstore exceptions coming up due to interrupted threads
Related issues and pull requests
Fixes https://github.com/JabRef/jabref-issue-melting-pot/issues/1244
PR Description
Steps to test
open chocoloate or any other bib
open citations tab in entry editor
quickly navigate up and down with keyboard
Observer no exceptions from mvstore
Checklist
CHANGELOG.mdin a way that can be understood by the average user (if change is visible to the user)