Skip to content

Fix stale main table search results#16137

Closed
gabrielSossai wants to merge 2 commits into
JabRef:mainfrom
gabrielSossai:fix-for-issue-15710
Closed

Fix stale main table search results#16137
gabrielSossai wants to merge 2 commits into
JabRef:mainfrom
gabrielSossai:fix-for-issue-15710

Conversation

@gabrielSossai

Copy link
Copy Markdown
### Related issues and pull requests

Closes #15710

### PR Description

This PR prevents stale main table search results from an earlier query from overwriting the results of a newer query when searches complete out of order. It tracks the latest scheduled search update and only applies results from the most recent one, so the main table reflects the current search state.

Analogies: This pull request is like honey because it keeps the search result flow smooth, like chocolate because it improves a small but noticeable user experience detail, and like the moon because it helps the visible state of the table match the current phase of the search.

jabref-contrib-policy:4.2:reviewed:ok

### Steps to test

1. Open JabRef with a library containing entries with distinct titles.
2. Search for a title that matches one entry in the main table.
3. Quickly change the query to another title or clear/change the search.
4. Confirm that the main table only shows matches for the latest query and does not keep stale matches from the previous search.

Automated verification performed:

```powershell
.\gradlew.bat --no-configuration-cache :jabgui:test --tests org.jabref.gui.maintable.MainTableDataModelTest
.\gradlew.bat --no-configuration-cache :jabgui:checkstyleMain :jabgui:checkstyleTest
git diff --check
```

AI usage

OpenAI Codex (model GPT-5) was used to help inspect the issue, analyze the codebase, discuss implementation alternatives, write and revise the test/fix, check project contribution instructions, and prepare this PR draft. I reviewed, understood, and take ownership of the submitted changes.

AI CHECKLIST.md walkthrough

Code checklist

1. Code self-review

Nullability and control flow

  • No == null / != null checks — JSpecify annotations (@NullMarked, @Nullable, @NonNull) used instead.
  • No Objects.requireNonNull(...) — nullability expressed via JSpecify annotations.
  • [/] New classes annotated with @NullMarked (org.jspecify.annotations.NullMarked).
  • Optional consumed with ifPresent / ifPresentOrElse / map / orElseThrow — never orElse(unusedValue) nor an isPresent() + get() block.
  • [/] StringUtil.isBlank(...) used instead of s == null || s.isBlank().

Exceptions

  • No catch (Exception e) — only specific exceptions are caught.
  • No throw new RuntimeException(...) / IllegalStateException(...) — these tear down the whole application.
  • [/] Logged exceptions are passed as the last logger argument (LOGGER.info("...", e)), not concatenated into the message string.

Style and idioms

  • New BibEntry objects built with withers (withField, not setField).
  • Modern Java used: List.of() / Map.of() / Set.of(), Path.of(), SequencedCollection / SequencedSet, text blocks.
  • [/] Regexes use a precompiled Pattern.compile(...) constant, not String.matches(...).
  • Background work uses org.jabref.logic.util.BackgroundTask, not new Thread().
  • No commented-out code, no trivial comments restating the code, no AI-disclosure comments in source.
  • [/] Markdown Javadoc (///) uses Markdown syntax, not JavaDoc inline tags: `code` instead of {@code}, [ClassName] instead of {@link}.

User-facing text

  • [/] All user-facing text localized (Localization.lang in Java, % prefix in FXML).
  • [/] Sentence case (not Title Case); no trailing !; labels do not end with :.
  • [/] Variance expressed with placeholders ("...: %0"), not string concatenation.

Security

  • [/] User-controlled data (request params, entry fields, file contents) is HTML-escaped before being written into any text/html response — including exception/error messages, not just the success body (XSS).

Tests

  • [/] Behavior changes in org.jabref.model / org.jabref.logic have added or updated tests.
  • Tests assert object contents (assertEquals), use plain JUnit asserts (not AssertJ), have no @DisplayName, do not catch exceptions (let them propagate so JUnit reports setup/teardown failures directly), and use @TempDir instead of manual temp directories.

2. Verification commands

  • ./gradlew :jablib:check (or ./gradlew check for all modules).
  • ./gradlew checkstyleMain checkstyleTest checkstyleJmh.
  • ./gradlew modernizer.
  • ./gradlew --no-configuration-cache :rewriteDryRun reports no changes (run ./gradlew rewriteRun to fix).
  • ./gradlew javadoc.
  • npx markdownlint-cli2 "docs/**/*.md" "*.md" (only if Markdown changed).
  • [/] Only if formatting is still off after rewriteRun: docker run -v $(pwd):/github/workspace ghcr.io/leventebajczi/intellij-format:master "*.java" "" ".idea/codeStyles/Project.xml".

3. Documentation

  • CHANGELOG.md entry added if the change is visible to the user (end-user wording, no extra blank lines). Use TODO as the issue/PR reference placeholder when no issue is known and the PR is not yet created — never a fake number.
  • Searched jabref/issues and jabref-koppor/issues for a related issue; linked only on a confident match, otherwise kept TODO (no closes/fixes for merely-similar issues).
  • [/] Requirement added to docs/requirements/<area>.md if the change is a new feature or significant bug fix (skip for refactors, minor fixes, and internal changes).
  • [/] Developer documentation under docs/ updated if behavior or architecture changed.

4. Pull request

  • PR body built from .github/PULL_REQUEST_TEMPLATE.md, every section filled.
  • All checklist items kept and marked [x], [ ], or [/].
  • All HTML comments removed from the PR body.
  • PR created with gh pr create --body-file <file> (not --body).
  • [/] If CHANGELOG.md used a TODO placeholder, it was replaced with the real PR-number link after PR creation, then committed and pushed.

Checklist

  • I own the copyright of the code submitted and I license it under the MIT license
  • If AI tools were used, I disclosed them in the "AI usage" section and reviewed, understood, and take full ownership of all AI-generated code
  • 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

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Hey @gabrielSossai! 👋

Thank you for contributing to JabRef!

We have automated checks in place, based on which you will soon get feedback if any of them are failing. We also use Qodo for review assistance. It will update your pull request description with a review help and offer suggestions to improve the pull request.

After all automated checks pass, a maintainer will also review your contribution. Once that happens, you can go through their comments in the "Files changed" tab and act on them, or reply to the conversation if you have further inputs. You can read about the whole pull request process in our contribution guide.

Please ensure that your pull request is in line with our AI Usage Policy and make necessary disclosures.

@github-actions github-actions Bot added first contrib status: changes-required Pull requests that are not yet complete labels Jul 2, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Prevent stale main table search results from out-of-order searches

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Prevent older, slower searches from overwriting newer main table results.
• Apply search matches only when they correspond to the latest scheduled query.
• Add regression tests covering out-of-order search completion and group filtering behavior.
Diagram

graph TD
  A["Search query changed"] --> B["MainTableDataModel"] --> C["Schedule BackgroundTask"] --> D["SearchContext.search"] --> E["onSuccess: verify latest id"] --> F["Update matches + refilter"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Cancel previous in-flight search tasks
  • ➕ Avoids wasted work for searches that will be discarded
  • ➕ Reduces contention/CPU spikes on rapid typing
  • ➖ Requires TaskExecutor/BackgroundTask cancellation support end-to-end
  • ➖ Risk of partial state updates if cancellation is not cooperative
2. Reactive switchMap-style pipeline (latest-wins)
  • ➕ Encodes latest-result-wins semantics declaratively
  • ➕ Naturally composes with throttling/debouncing
  • ➖ Would introduce/extend reactive dependencies and refactor existing binding code
  • ➖ More invasive change than needed for a targeted bug fix
3. Serialize searches via single-thread executor/queue
  • ➕ Eliminates out-of-order completion by construction
  • ➖ Increases latency for the latest query when earlier searches are slow
  • ➖ Still does unnecessary work for intermediate queries

Recommendation: The AtomicLong versioning approach in this PR is the best trade-off: minimal surface-area change, clear correctness property (latest-wins), and no reliance on task cancellation semantics. Consider task cancellation only if performance issues from discarded searches become measurable.

Files changed (3) +178 / -8

Bug fix (1) +12 / -7
MainTableDataModel.javaGuard async search updates with a monotonically increasing update id +12/-7

Guard async search updates with a monotonically increasing update id

• Introduces an AtomicLong searchUpdateId and increments it for each search update request. Applies search results only if the completed task matches the latest id, preventing older searches from overwriting newer UI state.

jabgui/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java

Tests (1) +165 / -1
MainTableDataModelTest.javaAdd regression tests for out-of-order search completion and group selection +165/-1

Add regression tests for out-of-order search completion and group selection

• Adds a test harness TaskExecutor to queue BackgroundTasks and execute them out of order to reproduce the stale-results bug. Adds tests ensuring newer search results win and that group selection updates match/visibility as expected.

jabgui/src/test/java/org/jabref/gui/maintable/MainTableDataModelTest.java

Documentation (1) +1 / -0
CHANGELOG.mdDocument fix for stale search results in main table +1/-0

Document fix for stale search results in main table

• Adds a changelog entry describing that outdated search results no longer remain visible when searches are started in quick succession.

CHANGELOG.md

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

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Silent latest-search failure 🐞 Bug ☼ Reliability
Description
updateSearchMatches does not set an onFailure handler, and under UiTaskExecutor a failure is
only surfaced if an exception consumer is configured. With the new searchUpdateId guard, if the
latest search task fails, older completed searches are suppressed and the main table can remain
stuck showing stale results with no recovery path.
Code

jabgui/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java[R107-116]

+        long currentSearchUpdateId = searchUpdateId.incrementAndGet();
+        BackgroundTask.wrap(() -> query.map(searchQuery -> searchContext.search(searchQuery)))
+                      .onSuccess(results -> {
+                          if (currentSearchUpdateId != searchUpdateId.get()) {
+                              return;
+                          }
+
+                          results.ifPresentOrElse(this::setSearchMatches, this::clearSearchMatches);
+                          FilteredListProxy.refilterListReflection(entriesFiltered);
+                      }).executeWith(taskExecutor);
Evidence
The updated method uses a latest-only guard and provides only an onSuccess handler. In the
production executor, failures only invoke callbacks when an exception handler is set, so a failing
latest task can prevent any subsequent UI update and may not be surfaced.

jabgui/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java[106-116]
jabgui/src/main/java/org/jabref/gui/util/UiTaskExecutor.java[205-209]
jablib/src/main/java/org/jabref/logic/util/BackgroundTask.java[204-209]

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

### Issue description
The latest-only application logic (`searchUpdateId`) suppresses older results, but `updateSearchMatches` has no `onFailure` handling. If the newest search throws, the UI may keep showing results for a previous query indefinitely.

### Issue Context
In `UiTaskExecutor.getJavaFXTask`, `setOnFailed` is only registered if `BackgroundTask.getOnException()` is non-null; without `onFailure(...)`, failures may not be reported or acted upon.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java[106-116]
- jabgui/src/main/java/org/jabref/gui/util/UiTaskExecutor.java[196-209]

### Suggested fix
- Add `.onFailure(e -> { if (currentSearchUpdateId != searchUpdateId.get()) return; LOGGER.warn("Search failed", e); clearSearchMatches(); FilteredListProxy.refilterListReflection(entriesFiltered); })`.
- Decide the desired UI behavior on failure (clear results vs. keep previous, but then explicitly reflect the failure state); the key is to avoid silently leaving stale results for a different query.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. UI-thread mass search update 🐞 Bug ➹ Performance
Description
updateSearchMatches now applies setSearchMatches/clearSearchMatches inside
BackgroundTask.onSuccess, which runs on the JavaFX thread, and these methods iterate all entries
and mutate multiple observable properties. On large libraries and frequent search updates (typing),
this can cause noticeable UI freezes/jank.
Code

jabgui/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java[R107-116]

+        long currentSearchUpdateId = searchUpdateId.incrementAndGet();
+        BackgroundTask.wrap(() -> query.map(searchQuery -> searchContext.search(searchQuery)))
+                      .onSuccess(results -> {
+                          if (currentSearchUpdateId != searchUpdateId.get()) {
+                              return;
+                          }
+
+                          results.ifPresentOrElse(this::setSearchMatches, this::clearSearchMatches);
+                          FilteredListProxy.refilterListReflection(entriesFiltered);
+                      }).executeWith(taskExecutor);
Evidence
The PR moved match application into onSuccess (FX thread), and the applied methods are O(n) across
all entries and set multiple observable properties; this combination can block the UI thread.

jabgui/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java[106-116]
jabgui/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java[132-147]
jablib/src/main/java/org/jabref/logic/util/BackgroundTask.java[183-188]

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

### Issue description
`setSearchMatches` / `clearSearchMatches` iterate over all `entriesViewModel` and update multiple JavaFX properties. Because they are now invoked from `onSuccess`, this work runs on the JavaFX thread and can block rendering/interaction for large libraries.

### Issue Context
- `BackgroundTask.onSuccess` is documented to always run on the JavaFX thread.
- The heavy loops are in `setSearchMatches` / `clearSearchMatches`.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java[106-147]

### Suggested approach
- Keep `searchContext.search(...)` in the background task.
- In the background thread, compute a compact result (e.g., `Set<BibEntryId> matchedIds` and `Set<BibEntryId> fulltextIds`) and optionally a list of only entries whose state would change.
- In `onSuccess`, apply only the necessary property changes (delta update) and consider chunking updates if still large (e.g., batched `Platform.runLater` updates) to keep frames responsive.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +107 to +116
long currentSearchUpdateId = searchUpdateId.incrementAndGet();
BackgroundTask.wrap(() -> query.map(searchQuery -> searchContext.search(searchQuery)))
.onSuccess(results -> {
if (currentSearchUpdateId != searchUpdateId.get()) {
return;
}

results.ifPresentOrElse(this::setSearchMatches, this::clearSearchMatches);
FilteredListProxy.refilterListReflection(entriesFiltered);
}).executeWith(taskExecutor);

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.

Remediation recommended

1. Ui-thread mass search update 🐞 Bug ➹ Performance

updateSearchMatches now applies setSearchMatches/clearSearchMatches inside
BackgroundTask.onSuccess, which runs on the JavaFX thread, and these methods iterate all entries
and mutate multiple observable properties. On large libraries and frequent search updates (typing),
this can cause noticeable UI freezes/jank.
Agent Prompt
### Issue description
`setSearchMatches` / `clearSearchMatches` iterate over all `entriesViewModel` and update multiple JavaFX properties. Because they are now invoked from `onSuccess`, this work runs on the JavaFX thread and can block rendering/interaction for large libraries.

### Issue Context
- `BackgroundTask.onSuccess` is documented to always run on the JavaFX thread.
- The heavy loops are in `setSearchMatches` / `clearSearchMatches`.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java[106-147]

### Suggested approach
- Keep `searchContext.search(...)` in the background task.
- In the background thread, compute a compact result (e.g., `Set<BibEntryId> matchedIds` and `Set<BibEntryId> fulltextIds`) and optionally a list of only entries whose state would change.
- In `onSuccess`, apply only the necessary property changes (delta update) and consider chunking updates if still large (e.g., batched `Platform.runLater` updates) to keep frames responsive.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +107 to +116
long currentSearchUpdateId = searchUpdateId.incrementAndGet();
BackgroundTask.wrap(() -> query.map(searchQuery -> searchContext.search(searchQuery)))
.onSuccess(results -> {
if (currentSearchUpdateId != searchUpdateId.get()) {
return;
}

results.ifPresentOrElse(this::setSearchMatches, this::clearSearchMatches);
FilteredListProxy.refilterListReflection(entriesFiltered);
}).executeWith(taskExecutor);

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. Silent latest-search failure 🐞 Bug ☼ Reliability

updateSearchMatches does not set an onFailure handler, and under UiTaskExecutor a failure is
only surfaced if an exception consumer is configured. With the new searchUpdateId guard, if the
latest search task fails, older completed searches are suppressed and the main table can remain
stuck showing stale results with no recovery path.
Agent Prompt
### Issue description
The latest-only application logic (`searchUpdateId`) suppresses older results, but `updateSearchMatches` has no `onFailure` handling. If the newest search throws, the UI may keep showing results for a previous query indefinitely.

### Issue Context
In `UiTaskExecutor.getJavaFXTask`, `setOnFailed` is only registered if `BackgroundTask.getOnException()` is non-null; without `onFailure(...)`, failures may not be reported or acted upon.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java[106-116]
- jabgui/src/main/java/org/jabref/gui/util/UiTaskExecutor.java[196-209]

### Suggested fix
- Add `.onFailure(e -> { if (currentSearchUpdateId != searchUpdateId.get()) return; LOGGER.warn("Search failed", e); clearSearchMatches(); FilteredListProxy.refilterListReflection(entriesFiltered); })`.
- Decide the desired UI behavior on failure (clear results vs. keep previous, but then explicitly reflect the failure state); the key is to avoid silently leaving stale results for a different query.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@jabref-machine

Copy link
Copy Markdown
Collaborator

Your code currently does not meet JabRef's code guidelines. We use OpenRewrite to ensure "modern" Java coding practices. You can see which checks are failing by locating the box "Some checks were not successful" on the pull request page. To see the test output, locate "Source Code Tests / OpenRewrite (pull_request)" and click on it.

The issues found can be automatically fixed. Please execute the gradle task rewriteRun from the rewrite group of the Gradle Tool window in IntelliJ, then check the results, reformat the code, commit, and push.

@koppor

koppor commented Jul 2, 2026

Copy link
Copy Markdown
Member

Don't fool us.

@koppor koppor closed this Jul 2, 2026
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

This pull requests was closed without merging. You have been unassigned from the respective issue #15710. In case you closed the PR for yourself, you can re-open it. Please also check After submission of a pull request in CONTRIBUTING.md.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants