Skip to content

Commit c9303ca

Browse files
vaadin-botArtur-claude
authored
fix: clear repeatable read transaction before running access tasks (#24400) (CP: 25.1) (#24426)
This PR cherry-picks changes from the original PR #24400 to branch 25.1. --- #### Original PR description > ## Summary > > - Clear the session-scoped transaction **before** running pending access tasks during session unlock > - This ensures effects triggered by access tasks see fresh signal values instead of stale cached values from the repeatable read transaction > - Added a unit test to verify that clearing the transaction fallback allows fresh reads > > ## Details > > When a shared signal is updated concurrently with an ongoing UIDL request, effects associated with that UI would run with the repeatable read transaction used for the request. If the transaction cached the old value, effects would miss the update. > > The fix moves `sessionScopedTransaction = null` to execute before `runPendingAccessTasks()` instead of after, ensuring access tasks read fresh values. > > ## Test plan > > - [ ] Existing unit tests pass > - [ ] New `clearingFallback_allowsFreshReads` test verifies the fix behavior > - [ ] Manual test with the reproduction case from the issue > > Fixes #24399 > > Slack thread: https://vaadin.slack.com/archives/C6RAXJATF/p1779368249043829?thread_ts=1779368185.339629&cid=C6RAXJATF > > https://claude.ai/code/session_01QW3F8K4uxGyaUNa9xwoKTR > > --- > _Generated by [Claude Code](https://claude.ai/code/session_01QW3F8K4uxGyaUNa9xwoKTR)_ Co-authored-by: Artur Signell <artur@vaadin.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 76d6a85 commit c9303ca

3 files changed

Lines changed: 63 additions & 0 deletions

File tree

‎flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2337,6 +2337,9 @@ public void runPendingAccessTasks(VaadinSession session) {
23372337
if (!pendingAccess.isCancelled()) {
23382338
CurrentInstance.clearAll();
23392339
CurrentInstance.setCurrent(session);
2340+
// Clear session-scoped transaction so each task gets fresh
2341+
// reads from shared signals instead of stale cached values
2342+
session.clearSessionScopedTransaction();
23402343
pendingAccess.run();
23412344

23422345
try {

‎flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,17 @@ Transaction getOrCreateSessionScopedTransaction() {
721721
return sessionScopedTransaction;
722722
}
723723

724+
/**
725+
* Clears the session-scoped transaction so that the next signal operation
726+
* will create a fresh transaction with up-to-date values.
727+
* <p>
728+
* The session lock must be held when calling this method.
729+
*/
730+
void clearSessionScopedTransaction() {
731+
assert hasLock();
732+
sessionScopedTransaction = null;
733+
}
734+
724735
/**
725736
* Locks this session to protect its data from concurrent access. Accessing
726737
* the UI state from outside the normal request handling should always lock

‎flow-server/src/test/java/com/vaadin/flow/dom/ElementEffectTest.java‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import com.vaadin.flow.signals.local.ListSignal;
4848
import com.vaadin.flow.signals.local.ValueSignal;
4949
import com.vaadin.flow.signals.shared.SharedListSignal;
50+
import com.vaadin.flow.signals.shared.SharedValueSignal;
5051
import com.vaadin.tests.util.MockUI;
5152

5253
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -456,6 +457,54 @@ void effect_triggeredWithOtherUILocked_effectRunAsynchronously() {
456457
"Effect should run with correct UI context");
457458
}
458459

460+
@Test
461+
void effect_sharedSignalUpdatedOutsideSessionTransaction_reRunsWithFreshValue() {
462+
// Reproduces issue #24399: while a UIDL request holds the session
463+
// lock, the session's repeatable-read transaction has cached the
464+
// current signal value. A concurrent write to that signal — done
465+
// outside the session transaction, simulating another session or
466+
// a background message handler — must trigger the effect to
467+
// re-run with the fresh value, not the stale value cached in the
468+
// session transaction.
469+
CurrentInstance.clearAll();
470+
VaadinService.setCurrent(service);
471+
472+
var session = new MockVaadinSession(service);
473+
session.lock();
474+
VaadinSession.setCurrent(session);
475+
var ui = new MockUI(session);
476+
477+
SharedValueSignal<String> signal = new SharedValueSignal<>("initial");
478+
List<String> observed = new ArrayList<>();
479+
Signal.effect(ui, () -> observed.add(signal.get()));
480+
481+
// Initial run executes inline (UI.getCurrent() == ui) and reads
482+
// through the session-scoped write-through transaction, priming
483+
// its repeatable-read cache with "initial".
484+
assertEquals(List.of("initial"), observed,
485+
"Effect should run once initially");
486+
487+
// Clear UI thread-local so the effect dispatcher takes the
488+
// async path and routes the re-run through ui.access(), which
489+
// enqueues a FutureAccess while the session lock is held.
490+
UI.setCurrent(null);
491+
492+
// Update the signal outside the session's repeatable-read
493+
// transaction (runWithoutTransaction routes the write through
494+
// ROOT). The tree's submitted snapshot becomes "external"
495+
// while the session transaction still caches "initial".
496+
Signal.runWithoutTransaction(() -> signal.set("external"));
497+
498+
// Flush the effect dispatcher (queued in TestService's
499+
// FlushableExecutor) and then run pending access tasks —
500+
// simulating session unlock at the end of a UIDL request.
501+
service.flushExecutorAndAccessTasks(session);
502+
503+
assertEquals(List.of("initial", "external"), observed,
504+
"Effect re-run must observe the fresh signal value, not the "
505+
+ "stale value cached in the session transaction");
506+
}
507+
459508
@Test
460509
void effect_throwExceptionWhenRunningDirectly_delegatedToErrorHandler() {
461510
CurrentInstance.clearAll();

0 commit comments

Comments
 (0)