Skip to content

Fix threading issues in citations relations tab#15233

Merged
Siedlerchr merged 6 commits into
mainfrom
mvStores
Mar 4, 2026
Merged

Fix threading issues in citations relations tab#15233
Siedlerchr merged 6 commits into
mainfrom
mvStores

Conversation

@Siedlerchr

@Siedlerchr Siedlerchr commented Feb 27, 2026

Copy link
Copy Markdown
Member

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

  1. open chocoloate or any other bib

  2. open citations tab in entry editor

  3. quickly navigate up and down with keyboard

  4. Observer no exceptions from mvstore

Checklist

  • I own the copyright of the code submitted and I license it under the MIT license
  • I manually tested my changes in running JabRef (always required)
  • [/] I added JUnit tests for changes (if applicable)
  • [/] I added screenshots in the PR description (if change is visible to the user)
  • [/] I added a screenshot in the PR description showing a library with a single entry with me as author and as title the issue number
  • I described the change in CHANGELOG.md in a way that can be understood by the average user (if change is visible to the user)
  • I checked the user documentation for up to dateness and submitted a pull request to our user documentation repository

@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Implement cooperative cancellation in citations relations tab

🐞 Bug fix ✨ Enhancement

Grey Divider

Walkthroughs

Description
• 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
Diagram
flowchart 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"]
Loading

Grey Divider

File Changes

1. jabgui/src/main/java/org/jabref/gui/JabRefGUI.java ✨ Enhancement +2/-1

Add dialogService to SearchCitationsRelationsService

• Add dialogService parameter to SearchCitationsRelationsService constructor
• Pass NotificationService for error handling in citation relations initialization

jabgui/src/main/java/org/jabref/gui/JabRefGUI.java


2. jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java 🐞 Bug fix +22/-14

Implement cooperative cancellation with null checks

• Replace cancel() with cancel(false) for cooperative cancellation throughout
• Add null checks before cancelling tasks in jumpToEntry and importEntries
• Pass isCancelled lambda to search methods for mid-operation cancellation checks
• Update JavaDoc reference to PreviewWithSourceTab class
• Remove this. qualifier from task assignments for consistency

jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java


3. jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java 🐞 Bug fix +1/-1

Use cooperative cancellation in ViewModel

• Change searchTask.cancel(true) to cancel(false) for cooperative cancellation
• Prevent interruption of running search tasks

jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java


View more (5)
4. jabgui/src/main/java/org/jabref/gui/util/UiTaskExecutor.java ✨ Enhancement +1/-1

Respect mayInterruptIfRunning in JavaFX Task

• Pass mayInterruptIfRunning property value to JavaFX Task cancel method
• Respect BackgroundTask's interruption preference in UI layer

jabgui/src/main/java/org/jabref/gui/util/UiTaskExecutor.java


5. jablib/src/main/java/org/jabref/logic/ai/util/MVStoreBase.java 🐞 Bug fix +14/-8

Add null safety checks to MVStore operations

• Add null check for path.getParent() before creating directories
• Add null checks in commit() and close() methods
• Improve robustness of MVStore lifecycle management

jablib/src/main/java/org/jabref/logic/ai/util/MVStoreBase.java


6. jablib/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java ✨ Enhancement +20/-2

Add cancellation token support to search methods

• Add BooleanSupplier isCancelled parameter to searchCites and searchCitedBy methods
• Create overloaded methods with default isCancelled implementation
• Check cancellation status after fetching and return empty list if cancelled
• Add NotificationService parameter to constructor

jablib/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java


7. jablib/src/main/java/org/jabref/logic/citation/repository/BibEntryCitationsAndReferencesRepositoryShell.java ✨ Enhancement +17/-24

Refactor to extend MVStoreBase for lifecycle management

• Extend MVStoreBase instead of implementing AutoCloseable directly
• Remove manual MVStore initialization code
• Add NotificationService parameter to constructor
• Add commit() calls after adding citations and references
• Implement abstract error message methods from MVStoreBase
• Update test constructor to call super with dummy path

jablib/src/main/java/org/jabref/logic/citation/repository/BibEntryCitationsAndReferencesRepositoryShell.java


8. jablib/src/main/java/org/jabref/logic/util/BackgroundTask.java ✨ Enhancement +11/-1

Add cooperative cancellation support to BackgroundTask

• Add mayInterruptIfRunning BooleanProperty to control interruption behavior
• Overload cancel() method with boolean parameter for cooperative cancellation
• Add mayInterruptIfRunningProperty() getter for UI layer access
• Update logging to include interruption flag

jablib/src/main/java/org/jabref/logic/util/BackgroundTask.java


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Racy task cancel supplier 🐞 Bug ⛯ Reliability
Description
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.
Code

jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java[R830-838]

+                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)
               );
Evidence
The background callable evaluates citingTask::isCancelled / citedByTask::isCancelled when it
runs, meaning it depends on the then-current value of those static fields. Those fields are
explicitly set to null on entry rebind, and tasks are executed asynchronously via
executor.submit(task), so there is a real race window where the field can be null/reassigned
before the callable starts (or while other queued tasks start), resulting in NPE or checking the
wrong task’s cancelled state. The service then changes behavior (returns List.of() and skips
storing) based on that supplier.

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]
jabgui/src/main/java/org/jabref/gui/util/UiTaskExecutor.java[100-125]
jablib/src/main/java/org/jabref/logic/citation/SearchCitationsRelationsService.java[87-99]
jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java[97-106]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


2. Empty path in MVStore test ctor🐞 Bug ⛯ Reliability
Description
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.
Code

jablib/src/main/java/org/jabref/logic/citation/repository/BibEntryCitationsAndReferencesRepositoryShell.java[R53-56]

+        super(Path.of(""), _ -> { }); // Dummy Path for testing
       this.citationsDao = citationsDao;
       this.referencesDao = referencesDao;
-        this.mvStore = null;
   }
Evidence
The test constructor explicitly uses Path.of(""). MVStoreBase always passes
mvStorePath.toString() to MVStore’s fileName(...) unless mvStorePath is null. There is no
guard to treat blank/empty filenames as in-memory stores, so the test path flows into MVStore open
logic.

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]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@Siedlerchr Siedlerchr added the status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers label Feb 27, 2026
Comment on lines +830 to 838
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)
);

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.

Action required

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

Comment on lines 53 to 56
super(Path.of(""), _ -> { }); // Dummy Path for testing
this.citationsDao = citationsDao;
this.referencesDao = referencesDao;
this.mvStore = null;
}

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.

Action required

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

fixed

@testlens-app

This comment has been minimized.

@testlens-app

This comment has been minimized.

@testlens-app

testlens-app Bot commented Feb 27, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 3fcf91d
▶️ Tests: 11252 executed
⚪️ Checks: 69/69 completed


Learn more about TestLens at testlens.app.

@subhramit subhramit left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Understood at a high-level, may need a second look

@subhramit subhramit added status: awaiting-second-review For non-trivial changes and removed status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers labels Feb 27, 2026
@Siedlerchr Siedlerchr added this pull request to the merge queue Mar 4, 2026
@github-actions github-actions Bot added the status: to-be-merged PRs which are accepted and should go into the merge-queue. label Mar 4, 2026
Merged via the queue into main with commit a2502f2 Mar 4, 2026
70 checks passed
@Siedlerchr Siedlerchr deleted the mvStores branch March 4, 2026 14:18
Siedlerchr added a commit that referenced this pull request Mar 5, 2026
…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)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: awaiting-second-review For non-trivial changes status: to-be-merged PRs which are accepted and should go into the merge-queue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants