Skip to content

Keep external change detection active while saving - #16613

Open
Siedlerchr wants to merge 2 commits into
mainfrom
agent/monitor-save-events
Open

Keep external change detection active while saving#16613
Siedlerchr wants to merge 2 commits into
mainfrom
agent/monitor-save-events

Conversation

@Siedlerchr

@Siedlerchr Siedlerchr commented Aug 17, 2026

Copy link
Copy Markdown
Member

Summary

JabRef now keeps its external-change watcher registered while a library is saved. Filesystem events received during the save are deferred, then the saved library is scanned once afterwards so persistent external changes can still be offered for conflict resolution.

Analogies

The monitor is like honey: it stays connected rather than being removed while the jar is opened. It waits like chocolate cooling before judging the final shape of a save. Once the save finishes, it checks the resulting file like the moon reflecting what is actually on disk.

This draft requires contributor review before adding the policy compliance tag required for a ready-for-review pull request.

Steps to test

  1. Open the same library in two JabRef instances.
  2. Save the library in the first instance and verify that the second instance offers the external-changes notification and resolver.
  3. While a save is in progress, modify the library from another process. After the save completes, verify that JabRef scans the resulting file and offers persistent external changes for review.

Related issues and pull requests

No confident matching issue identified.

AI usage

Codex (GPT-5) assisted with implementation and tests from the contributor-requested design. AIL4: AI produced the implementation from a human-provided basic idea. The contributor must review, understand, and take full ownership before marking this PR ready for review.

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). Link the issue if one exists; link the PR only when no issue exists. Use TODO as the placeholder when neither is known yet — 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 (no issue confidently identified yet — an existing issue link always stays), it was replaced with the real PR-number link after PR creation, then committed and pushed. If an issue is identified or created later, the link is switched to the issue.

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

@Siedlerchr
Siedlerchr marked this pull request as ready for review August 20, 2026 18:51
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Keep external change detection active during library saves

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

Grey Divider

AI Description

• Keep the file watcher registered during saves and defer change handling until save completes.
• Trigger a single post-save rescan so concurrent external edits can still be resolved.
• Add regression tests and document the UX requirement and changelog entry.
Diagram

graph TD
  A["SaveDatabaseAction"] -->|"suspend/resume"| B["LibraryTab"] -->|"suspend/resume"| C["DatabaseChangeMonitor"]
  G["FileUpdateMonitor"] -->|"fileUpdated()"| C -->|"enqueue scan"| F["TaskExecutor"] --> E["ChangeScanner"] --> I["Library file"]
  H["External editor"] -->|"writes"| I
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Unregister watcher during save + explicit post-save scan
  • ➕ Preserves the previous semantic of ignoring save-time events
  • ➕ Avoids needing a suspended flag in the listener
  • ➖ Still risks missing external changes that occur between unregister/register timing
  • ➖ More churn in watcher registration; harder to reason about correctness under races
2. Debounce fileUpdated events with a timer/window
  • ➕ Collapses bursts of events (including those from JabRef’s own write) automatically
  • ➕ May reduce repeated scans on noisy filesystems
  • ➖ Adds timing heuristics and tuning complexity
  • ➖ Can delay user notification and still needs a post-save correctness story
3. Detect and ignore “self-write” events via write token/mtime correlation
  • ➕ Keeps detection active without broad suspension
  • ➕ Can avoid scans caused by JabRef’s own write
  • ➖ Platform/filesystem-dependent and fragile (mtime granularity, atomic replaces)
  • ➖ More complex than a single post-save rescan

Recommendation: The PR’s approach (keep watcher registered, suppress handling during save, then run exactly one post-save scan) is the best tradeoff for correctness and simplicity. It avoids reacting to transient/incomplete save state while still catching external writes that persist after the save finishes, and it’s covered by focused regression tests.

Files changed (7) +131 / -21

Bug fix (3) +38 / -16
LibraryTab.javaRename and route change-monitor controls to suspend/resume detection +4/-4

Rename and route change-monitor controls to suspend/resume detection

• Renames suspend/resume methods to reflect the new semantics (suspending detection rather than unregistering the watcher) and delegates directly to the monitor’s new APIs.

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

DatabaseChangeMonitor.javaAdd suspend/resume change detection with single rescan on resume +32/-10

Add suspend/resume change detection with single rescan on resume

• Keeps the file listener registered but gates reaction to events via a 'changeDetectionSuspended' flag. On resume, forces one scan of the on-disk file to detect any persistent external changes after the save completes.

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java

SaveDatabaseAction.javaBracket saves with change-detection suspend/resume +2/-2

Bracket saves with change-detection suspend/resume

• Replaces change-monitor unregister/reregister calls with change-detection suspension around the save, ensuring the watcher remains active and a post-save rescan occurs.

jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java

Tests (2) +85 / -5
DatabaseChangeMonitorTest.javaAdd tests for suspended detection and rescan behavior +80/-0

Add tests for suspended detection and rescan behavior

• Adds coverage ensuring file updates are ignored while suspended, the watcher is not removed, and exactly one scan is scheduled when resuming detection.

jabgui/src/test/java/org/jabref/gui/collab/DatabaseChangeMonitorTest.java

SaveDatabaseActionTest.javaUpdate save test to new change-detection method names +5/-5

Update save test to new change-detection method names

• Renames and updates the save bracketing test to assert 'suspendChangeDetection()'/'resumeChangeDetection()' ordering around a successful save.

jabgui/src/test/java/org/jabref/gui/exporter/SaveDatabaseActionTest.java

Documentation (2) +8 / -0
CHANGELOG.mdDocument fix for missed external-change detection during saves +1/-0

Document fix for missed external-change detection during saves

• Adds a user-facing changelog entry describing that external changes made during a save can now be detected and reviewed for resolution.

CHANGELOG.md

ux.mdAdd UX requirement for post-save external change detection +7/-0

Add UX requirement for post-save external change detection

• Introduces requirement 'req~ux.external-library-changes.after-save~1' specifying that JabRef must keep observing file changes during save, defer detection, and inspect the resulting file after saving.

docs/requirements/ux.md

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

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. SaveAs scan path race 🐞 Bug ≡ Correctness
Description
DatabaseChangeMonitor.resumeChangeDetection() schedules a scan that consults
BibDatabaseContext.getDatabasePath() at execution time, but SaveDatabaseAction.saveAs(...)
updates the context path only after save(file, mode) returns. This can scan the old/empty path
during Save As, causing missed detection on the newly saved file or spurious notifications from
scanning the previous file.
Code

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[R134-138]

+    public void resumeChangeDetection() {
+        synchronized (database) {
+            changeDetectionSuspended = false;
+            scanForChanges();
+        }
Evidence
The PR changed resume semantics so it now schedules a scan on resume. Save As sets the new path only
after the save call returns, so the scan can execute while the context still points to the old/empty
path because ChangeScanner reads the path from the context during task execution.

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[134-139]
jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java[154-166]
jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java[225-252]
jabgui/src/main/java/org/jabref/gui/collab/ChangeScanner.java[36-46]

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

## Issue description
`resumeChangeDetection()` triggers `scanForChanges()` which eventually reads `database.getDatabasePath()` inside the background task. During `SaveDatabaseAction.saveAs(...)`, the new database path is only set *after* `save(file, mode)` (which resumes change detection in its `finally`). This creates a race where the post-save scan may run against the old (or empty) path.

## Issue Context
- `SaveDatabaseAction.saveAs(...)` calls `save(file, mode)` and only sets `context.setDatabasePath(file)` after a successful return.
- `save(Path, ...)` always calls `libraryTab.resumeChangeDetection()` in `finally`.
- `resumeChangeDetection()` immediately schedules a scan, and `ChangeScanner.scanForChanges()` uses `database.getDatabasePath()`.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java[154-166]
- jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java[212-252]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[134-154]
- jabgui/src/main/java/org/jabref/gui/collab/ChangeScanner.java[36-59]

## Suggested approach
1. Add an API that allows scanning the just-saved path explicitly (e.g., `DatabaseChangeMonitor.scanForChanges(Path fileToCompare)` or `resumeChangeDetection(Path savedPath)`), using `ChangeScanner.getDatabaseChanges(savedPath)` instead of relying on `database.getDatabasePath()`.
2. In `SaveDatabaseAction.save(Path targetPath, ...)`, call the new resume-and-scan method with `targetPath` in the `finally`.
3. Keep the existing no-arg resume for non-save callers (if any), but ensure Save As uses the path-aware version to remove the race.

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



Remediation recommended

2. scanForChanges comment not /// 📘 Rule violation ⚙ Maintainability
Description
A new multi-line comment in scanForChanges() uses // instead of the required Markdown Javadoc
/// style. This diverges from the mandated documentation convention and increases style
inconsistency in changed code.
Code

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[R143-144]

+        // File watchers can report JabRef's own write or an external change while saving. Scanning once after the
+        // save finishes avoids reacting to an incomplete file and still detects an external write that occurred then.
Evidence
PR Compliance ID 11 requires multi-line comments to use Markdown Javadoc (///). The PR adds a
two-line // comment in scanForChanges() (lines 143-144), which does not follow that requirement.

AGENTS.md: Use Markdown Javadoc (///) for Multi-line Comments and Markdown Syntax Within
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[141-145]

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

## Issue description
New multi-line documentation-style comments must use Markdown Javadoc (`///`) and Markdown syntax.

## Issue Context
`scanForChanges()` introduces a two-line explanatory comment using `//`, which violates the project’s required multi-line comment convention.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[141-145]

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


3. Watcher thread can block 🐞 Bug ☼ Reliability
Description
Because change detection is now suspended (not unregistered) during saves, filesystem callbacks
still invoke DatabaseChangeMonitor.fileUpdated() on the WatchService thread and block on
synchronized (database) while the save holds the same lock. This can stall
DefaultFileUpdateMonitor’s single watcher loop, delaying or losing other filesystem events
(including for other monitored files).
Code

jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java[225]

+        libraryTab.suspendChangeDetection();
Evidence
With the PR’s new suspend/resume strategy, the listener stays registered during saves. The
WatchService thread executes fileUpdated() inline, and fileUpdated() synchronizes on the same
context that saveDatabase locks for the duration of the write, so the watcher thread can be
blocked until saving completes.

jabgui/src/main/java/org/jabref/gui/util/DefaultFileUpdateMonitor.java[39-67]
jabgui/src/main/java/org/jabref/gui/util/DefaultFileUpdateMonitor.java[82-84]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[119-125]
jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java[260-289]

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 WatchService thread calls `FileUpdateListener.fileUpdated()` inline. With this PR, the listener remains registered during saves and `fileUpdated()` synchronizes on the `BibDatabaseContext` object; the save path holds the same lock for the duration of writing. This blocks the WatchService thread, preventing timely processing of further filesystem events.

## Issue Context
- `DefaultFileUpdateMonitor.notifyAboutChange` directly calls listeners on the watcher thread.
- `DatabaseChangeMonitor.fileUpdated()` synchronizes on `database` before checking the suspended flag.
- `SaveDatabaseAction.saveDatabase(...)` synchronizes on the same `BibDatabaseContext` while writing.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/util/DefaultFileUpdateMonitor.java[39-67]
- jabgui/src/main/java/org/jabref/gui/util/DefaultFileUpdateMonitor.java[82-84]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[119-132]
- jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java[255-290]

## Suggested approach
1. Make `changeDetectionSuspended` readable without taking the database monitor (e.g., `volatile` or `AtomicBoolean`).
2. In `fileUpdated()`, check the suspended flag *before* attempting any locking; if suspended, return immediately (or set a lightweight `pendingUpdate` flag) without blocking the WatchService thread.
3. If you still need mutual exclusion with saving, move any required locking into the background task (or use a separate lock dedicated to change-detection state) so the watcher thread never blocks on long-running saves.

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


4. Duplicate post-save scans 🐞 Bug ➹ Performance
Description
resumeChangeDetection() always schedules a scan and queued filesystem events that were blocked
during the save can run after resume and schedule additional scans, leading to redundant parsing and
potentially duplicated external-change notifications. This is a performance regression on large
libraries and can create noisy UX (multiple identical notifications).
Code

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[R134-138]

+    public void resumeChangeDetection() {
+        synchronized (database) {
+            changeDetectionSuspended = false;
+            scanForChanges();
+        }
Evidence
The code now schedules a scan on every resume and also schedules scans from file watcher callbacks
whenever not suspended. Since the save path holds the same lock used in fileUpdated(), file events
can be delayed until after resumeChangeDetection() has already scheduled its scan, resulting in
multiple scans for a single save.

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[119-125]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[134-154]
jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java[225-252]
jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java[260-289]

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

## Issue description
After a save, `resumeChangeDetection()` schedules a scan unconditionally. Because watcher callbacks may be blocked during the save and delivered after resume, `fileUpdated()` can schedule an additional scan immediately after the resume-triggered scan is queued.

## Issue Context
- `resumeChangeDetection()` calls `scanForChanges()` every time.
- `fileUpdated()` calls `scanForChanges()` whenever not suspended.
- Save holds the same lock `fileUpdated()` uses, so events can be delayed until after resume.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[119-154]
- jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java[225-252]
- jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java[260-289]

## Suggested approach
1. Add a small coalescing mechanism in `DatabaseChangeMonitor`, e.g.:
  - `AtomicBoolean scanScheduled`
  - `AtomicBoolean pendingUpdateWhileSuspended`
2. In `fileUpdated()`:
  - if suspended: set `pendingUpdateWhileSuspended=true` and return
  - else: `scheduleScanIfNotScheduled()`
3. In `resumeChangeDetection()`:
  - clear suspended
  - only schedule a scan if `pendingUpdateWhileSuspended` was true (or if the save path explicitly requests one), and clear the pending flag
4. Clear `scanScheduled` in both `onSuccess` and `onFailure` callbacks so subsequent real updates can schedule scans.

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


Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +143 to +144
// File watchers can report JabRef's own write or an external change while saving. Scanning once after the
// save finishes avoids reacting to an incomplete file and still detects an external write that occurred then.

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. scanforchanges comment not /// 📘 Rule violation ⚙ Maintainability

A new multi-line comment in scanForChanges() uses // instead of the required Markdown Javadoc
/// style. This diverges from the mandated documentation convention and increases style
inconsistency in changed code.
Agent Prompt
## Issue description
New multi-line documentation-style comments must use Markdown Javadoc (`///`) and Markdown syntax.

## Issue Context
`scanForChanges()` introduces a two-line explanatory comment using `//`, which violates the project’s required multi-line comment convention.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[141-145]

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

Comment on lines +134 to +138
public void resumeChangeDetection() {
synchronized (database) {
changeDetectionSuspended = false;
scanForChanges();
}

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. Saveas scan path race 🐞 Bug ≡ Correctness

DatabaseChangeMonitor.resumeChangeDetection() schedules a scan that consults
BibDatabaseContext.getDatabasePath() at execution time, but SaveDatabaseAction.saveAs(...)
updates the context path only after save(file, mode) returns. This can scan the old/empty path
during Save As, causing missed detection on the newly saved file or spurious notifications from
scanning the previous file.
Agent Prompt
## Issue description
`resumeChangeDetection()` triggers `scanForChanges()` which eventually reads `database.getDatabasePath()` inside the background task. During `SaveDatabaseAction.saveAs(...)`, the new database path is only set *after* `save(file, mode)` (which resumes change detection in its `finally`). This creates a race where the post-save scan may run against the old (or empty) path.

## Issue Context
- `SaveDatabaseAction.saveAs(...)` calls `save(file, mode)` and only sets `context.setDatabasePath(file)` after a successful return.
- `save(Path, ...)` always calls `libraryTab.resumeChangeDetection()` in `finally`.
- `resumeChangeDetection()` immediately schedules a scan, and `ChangeScanner.scanForChanges()` uses `database.getDatabasePath()`.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java[154-166]
- jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java[212-252]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[134-154]
- jabgui/src/main/java/org/jabref/gui/collab/ChangeScanner.java[36-59]

## Suggested approach
1. Add an API that allows scanning the just-saved path explicitly (e.g., `DatabaseChangeMonitor.scanForChanges(Path fileToCompare)` or `resumeChangeDetection(Path savedPath)`), using `ChangeScanner.getDatabaseChanges(savedPath)` instead of relying on `database.getDatabasePath()`.
2. In `SaveDatabaseAction.save(Path targetPath, ...)`, call the new resume-and-scan method with `targetPath` in the `finally`.
3. Keep the existing no-arg resume for non-save callers (if any), but ensure Save As uses the path-aware version to remove the race.

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

}

libraryTab.suspendChangeMonitor();
libraryTab.suspendChangeDetection();

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

3. Watcher thread can block 🐞 Bug ☼ Reliability

Because change detection is now suspended (not unregistered) during saves, filesystem callbacks
still invoke DatabaseChangeMonitor.fileUpdated() on the WatchService thread and block on
synchronized (database) while the save holds the same lock. This can stall
DefaultFileUpdateMonitor’s single watcher loop, delaying or losing other filesystem events
(including for other monitored files).
Agent Prompt
## Issue description
The WatchService thread calls `FileUpdateListener.fileUpdated()` inline. With this PR, the listener remains registered during saves and `fileUpdated()` synchronizes on the `BibDatabaseContext` object; the save path holds the same lock for the duration of writing. This blocks the WatchService thread, preventing timely processing of further filesystem events.

## Issue Context
- `DefaultFileUpdateMonitor.notifyAboutChange` directly calls listeners on the watcher thread.
- `DatabaseChangeMonitor.fileUpdated()` synchronizes on `database` before checking the suspended flag.
- `SaveDatabaseAction.saveDatabase(...)` synchronizes on the same `BibDatabaseContext` while writing.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/util/DefaultFileUpdateMonitor.java[39-67]
- jabgui/src/main/java/org/jabref/gui/util/DefaultFileUpdateMonitor.java[82-84]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[119-132]
- jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java[255-290]

## Suggested approach
1. Make `changeDetectionSuspended` readable without taking the database monitor (e.g., `volatile` or `AtomicBoolean`).
2. In `fileUpdated()`, check the suspended flag *before* attempting any locking; if suspended, return immediately (or set a lightweight `pendingUpdate` flag) without blocking the WatchService thread.
3. If you still need mutual exclusion with saving, move any required locking into the background task (or use a separate lock dedicated to change-detection state) so the watcher thread never blocks on long-running saves.

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

Comment on lines +134 to +138
public void resumeChangeDetection() {
synchronized (database) {
changeDetectionSuspended = false;
scanForChanges();
}

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

4. Duplicate post-save scans 🐞 Bug ➹ Performance

resumeChangeDetection() always schedules a scan and queued filesystem events that were blocked
during the save can run after resume and schedule additional scans, leading to redundant parsing and
potentially duplicated external-change notifications. This is a performance regression on large
libraries and can create noisy UX (multiple identical notifications).
Agent Prompt
## Issue description
After a save, `resumeChangeDetection()` schedules a scan unconditionally. Because watcher callbacks may be blocked during the save and delivered after resume, `fileUpdated()` can schedule an additional scan immediately after the resume-triggered scan is queued.

## Issue Context
- `resumeChangeDetection()` calls `scanForChanges()` every time.
- `fileUpdated()` calls `scanForChanges()` whenever not suspended.
- Save holds the same lock `fileUpdated()` uses, so events can be delayed until after resume.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[119-154]
- jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java[225-252]
- jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java[260-289]

## Suggested approach
1. Add a small coalescing mechanism in `DatabaseChangeMonitor`, e.g.:
   - `AtomicBoolean scanScheduled`
   - `AtomicBoolean pendingUpdateWhileSuspended`
2. In `fileUpdated()`:
   - if suspended: set `pendingUpdateWhileSuspended=true` and return
   - else: `scheduleScanIfNotScheduled()`
3. In `resumeChangeDetection()`:
   - clear suspended
   - only schedule a scan if `pendingUpdateWhileSuspended` was true (or if the save path explicitly requests one), and clear the pending flag
4. Clear `scanScheduled` in both `onSuccess` and `onFailure` callbacks so subsequent real updates can schedule scans.

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

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