Skip to content

Commit 0e5c8ed

Browse files
authored
feat: add session lock request, acquire, release events (#24498)
Enable listening to session lock request/acquire/release
1 parent 09e7b23 commit 0e5c8ed

5 files changed

Lines changed: 531 additions & 1 deletion

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.server;
17+
18+
import java.util.concurrent.TimeUnit;
19+
import java.util.concurrent.locks.ReentrantLock;
20+
21+
/**
22+
* {@link ReentrantLock} used for Vaadin session locking that notifies the
23+
* owning {@link VaadinService}'s {@link SessionLockListener}s around the
24+
* outermost lock acquisition and release.
25+
* <p>
26+
* Both the request-handling lock path
27+
* ({@link VaadinService#lockSession(WrappedSession)}) and
28+
* {@link VaadinSession#lock()} / {@link VaadinSession#unlock()} (used by
29+
* {@link com.vaadin.flow.component.UI#access(Command)}) operate on the same
30+
* lock instance, so instrumenting the lock captures every acquisition exactly
31+
* once at the true acquire/release moment.
32+
* <p>
33+
* Only the outermost acquisition is reported: reentrant re-locks
34+
* ({@link #getHoldCount()} {@code > 0}) acquire without waiting and are not
35+
* signalled. The reference to the service is {@code transient}; after session
36+
* passivation/activation it behaves as a plain {@link ReentrantLock} until a
37+
* fresh instance is created.
38+
* <p>
39+
* Every acquisition path is instrumented so events stay balanced regardless of
40+
* how the lock was taken: {@link #lock()} and {@link #lockInterruptibly()}
41+
* block and so report {@code lockRequested} before the attempt to capture the
42+
* real wait time, while the non-blocking {@link #tryLock()} /
43+
* {@link #tryLock(long, TimeUnit)} report {@code lockRequested} and
44+
* {@code lockAcquired} together only on a successful outermost acquisition
45+
* (their wait time is effectively zero). This matters because
46+
* {@link VaadinService#ensureAccessQueuePurged(VaadinSession)} acquires the
47+
* lock with {@code tryLock} and then {@link #unlock() unlocks}; instrumenting
48+
* only {@code lock()} would leave that path firing {@code lockReleased} with no
49+
* matching acquire.
50+
*/
51+
class InstrumentedReentrantLock extends ReentrantLock {
52+
53+
private final transient VaadinService service;
54+
55+
InstrumentedReentrantLock(VaadinService service) {
56+
this.service = service;
57+
}
58+
59+
@Override
60+
public void lock() {
61+
boolean outermost = getHoldCount() == 0;
62+
if (outermost && service != null) {
63+
service.fireSessionLockRequested();
64+
}
65+
super.lock();
66+
if (outermost && service != null) {
67+
service.fireSessionLockAcquired();
68+
}
69+
}
70+
71+
@Override
72+
public void lockInterruptibly() throws InterruptedException {
73+
boolean outermost = getHoldCount() == 0;
74+
// Report after a confirmed acquisition: lockInterruptibly may abort
75+
// with InterruptedException without taking the lock.
76+
super.lockInterruptibly();
77+
if (outermost && service != null) {
78+
service.fireSessionLockRequested();
79+
service.fireSessionLockAcquired();
80+
}
81+
}
82+
83+
@Override
84+
public boolean tryLock() {
85+
boolean outermost = getHoldCount() == 0;
86+
boolean acquired = super.tryLock();
87+
if (outermost && acquired && service != null) {
88+
service.fireSessionLockRequested();
89+
service.fireSessionLockAcquired();
90+
}
91+
return acquired;
92+
}
93+
94+
@Override
95+
public boolean tryLock(long timeout, TimeUnit unit)
96+
throws InterruptedException {
97+
boolean outermost = getHoldCount() == 0;
98+
boolean acquired = super.tryLock(timeout, unit);
99+
if (outermost && acquired && service != null) {
100+
service.fireSessionLockRequested();
101+
service.fireSessionLockAcquired();
102+
}
103+
return acquired;
104+
}
105+
106+
@Override
107+
public void unlock() {
108+
boolean ultimateRelease = getHoldCount() == 1;
109+
super.unlock();
110+
if (ultimateRelease && service != null) {
111+
service.fireSessionLockReleased();
112+
}
113+
}
114+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.server;
17+
18+
import java.util.EventObject;
19+
20+
/**
21+
* Event fired to {@link SessionLockListener}s around acquisition and release of
22+
* a Vaadin session lock.
23+
* <p>
24+
* The same lock instance protects a session whether it is acquired by the
25+
* framework while handling a request or via {@link VaadinSession#lock()} (for
26+
* example from {@link com.vaadin.flow.component.UI#access(Command)}). A
27+
* listener receives {@link SessionLockListener#lockRequested},
28+
* {@link SessionLockListener#lockAcquired} and
29+
* {@link SessionLockListener#lockReleased} on the same thread for a given
30+
* outermost lock-hold, so timing state can be kept in a thread local.
31+
*
32+
* @see SessionLockListener
33+
*/
34+
public class SessionLockEvent extends EventObject {
35+
36+
/**
37+
* Creates a new session lock event.
38+
*
39+
* @param service
40+
* the Vaadin service whose session lock is being acquired or
41+
* released, not {@code null}
42+
*/
43+
public SessionLockEvent(VaadinService service) {
44+
super(service);
45+
}
46+
47+
/**
48+
* Gets the Vaadin service from which this event originates.
49+
*
50+
* @return the Vaadin service instance
51+
*/
52+
@Override
53+
public VaadinService getSource() {
54+
return (VaadinService) super.getSource();
55+
}
56+
57+
/**
58+
* Gets the Vaadin service from which this event originates.
59+
*
60+
* @return the Vaadin service instance
61+
*/
62+
public VaadinService getService() {
63+
return getSource();
64+
}
65+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.server;
17+
18+
import java.io.Serializable;
19+
20+
/**
21+
* Listener that is notified when a Vaadin session lock is acquired and
22+
* released, enabling observation of session-lock contention (for example to
23+
* publish lock wait and hold-time metrics).
24+
* <p>
25+
* All Vaadin server-side work for a single session is serialized behind one
26+
* lock, so the time a thread blocks acquiring it and the time it holds it are
27+
* key performance signals. Callbacks are delivered for the <em>outermost</em>
28+
* acquisition only (reentrant re-locks are not reported), and the three
29+
* callbacks for a single hold are delivered on the same thread in the order
30+
* {@link #lockRequested}, {@link #lockAcquired}, {@link #lockReleased}, so a
31+
* listener can record the start time in a {@link ThreadLocal} on
32+
* {@code lockRequested}, derive the wait time on {@code lockAcquired}, and the
33+
* hold time on {@code lockReleased}.
34+
* <p>
35+
* When several listeners are registered, {@link #lockRequested} and
36+
* {@link #lockAcquired} are delivered in registration order while
37+
* {@link #lockReleased} is delivered in reverse registration order, so the
38+
* callbacks nest like {@code try}/{@code finally} blocks: a listener registered
39+
* later sees its {@code lockReleased} run before that of a listener registered
40+
* earlier.
41+
* <p>
42+
* Implementations must be fast and non-blocking: callbacks run on the
43+
* request/access thread directly around the lock operation. Exceptions thrown
44+
* from a callback are logged and suppressed so they cannot disrupt session
45+
* locking.
46+
* <p>
47+
* Register via
48+
* {@link VaadinService#addSessionLockListener(SessionLockListener)}, typically
49+
* from a {@link VaadinServiceInitListener}. Listeners registered after a
50+
* session's lock has already been created are still honoured.
51+
*
52+
* @see VaadinService#addSessionLockListener(SessionLockListener)
53+
* @see SessionLockEvent
54+
*/
55+
public interface SessionLockListener extends Serializable {
56+
57+
/**
58+
* Invoked on the locking thread immediately before it attempts to acquire
59+
* the session lock for an outermost (non-reentrant) acquisition. The thread
60+
* may block between this callback and {@link #lockAcquired}; the elapsed
61+
* time is the lock wait time. For a non-blocking acquisition
62+
* ({@code tryLock}) this is delivered together with {@link #lockAcquired}
63+
* only when the lock was actually taken, so the reported wait time is
64+
* effectively zero.
65+
*
66+
* @param event
67+
* the session lock event
68+
*/
69+
default void lockRequested(SessionLockEvent event) {
70+
}
71+
72+
/**
73+
* Invoked on the locking thread immediately after it has acquired the
74+
* session lock for an outermost acquisition.
75+
*
76+
* @param event
77+
* the session lock event
78+
*/
79+
default void lockAcquired(SessionLockEvent event) {
80+
}
81+
82+
/**
83+
* Invoked on the locking thread immediately after the outermost lock hold
84+
* has been released (the lock's hold count reached zero). The time since
85+
* {@link #lockAcquired} is the lock hold time.
86+
*
87+
* @param event
88+
* the session lock event
89+
*/
90+
default void lockReleased(SessionLockEvent event) {
91+
}
92+
}

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

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.Collections;
3333
import java.util.HashMap;
3434
import java.util.List;
35+
import java.util.ListIterator;
3536
import java.util.Locale;
3637
import java.util.Map;
3738
import java.util.Map.Entry;
@@ -159,6 +160,7 @@ public abstract class VaadinService implements Serializable {
159160
private final List<SessionInitListener> sessionInitListeners = new CopyOnWriteArrayList<>();
160161
private final List<UIInitListener> uiInitListeners = new CopyOnWriteArrayList<>();
161162
private final List<SessionDestroyListener> sessionDestroyListeners = new CopyOnWriteArrayList<>();
163+
private final List<SessionLockListener> sessionLockListeners = new CopyOnWriteArrayList<>();
162164

163165
private SystemMessagesProvider systemMessagesProvider = DefaultSystemMessagesProvider
164166
.get();
@@ -813,6 +815,79 @@ public Registration addUIInitListener(UIInitListener listener) {
813815
return Registration.addAndRemove(uiInitListeners, listener);
814816
}
815817

818+
/**
819+
* Adds a listener that gets notified when a session lock for this service
820+
* is acquired and released, enabling observation of session-lock contention
821+
* (for example to publish lock wait and hold-time metrics).
822+
* <p>
823+
* Register typically from a {@link VaadinServiceInitListener}. Listeners
824+
* are notified for the outermost lock acquisition only; see
825+
* {@link SessionLockListener} for the callback contract.
826+
*
827+
* @param listener
828+
* the session lock listener
829+
* @return a handle that can be used for removing the listener
830+
* @see SessionLockListener
831+
*/
832+
public Registration addSessionLockListener(SessionLockListener listener) {
833+
return Registration.addAndRemove(sessionLockListeners, listener);
834+
}
835+
836+
boolean hasSessionLockListeners() {
837+
return !sessionLockListeners.isEmpty();
838+
}
839+
840+
void fireSessionLockRequested() {
841+
if (sessionLockListeners.isEmpty()) {
842+
return;
843+
}
844+
SessionLockEvent event = new SessionLockEvent(this);
845+
for (SessionLockListener listener : sessionLockListeners) {
846+
try {
847+
listener.lockRequested(event);
848+
} catch (RuntimeException e) {
849+
getLogger().error("Error in SessionLockListener.lockRequested",
850+
e);
851+
}
852+
}
853+
}
854+
855+
void fireSessionLockAcquired() {
856+
if (sessionLockListeners.isEmpty()) {
857+
return;
858+
}
859+
SessionLockEvent event = new SessionLockEvent(this);
860+
for (SessionLockListener listener : sessionLockListeners) {
861+
try {
862+
listener.lockAcquired(event);
863+
} catch (RuntimeException e) {
864+
getLogger().error("Error in SessionLockListener.lockAcquired",
865+
e);
866+
}
867+
}
868+
}
869+
870+
void fireSessionLockReleased() {
871+
if (sessionLockListeners.isEmpty()) {
872+
return;
873+
}
874+
SessionLockEvent event = new SessionLockEvent(this);
875+
// Released is fired in reverse registration order so that listeners
876+
// are nested: a listener's lockReleased runs before the lockReleased
877+
// of the listeners that were notified before it on lockAcquired.
878+
ListIterator<SessionLockListener> listeners = sessionLockListeners
879+
.listIterator(sessionLockListeners.size());
880+
while (listeners.hasPrevious()) {
881+
SessionLockListener listener = listeners.previous();
882+
try {
883+
listener.lockReleased(event);
884+
} catch (RuntimeException e) {
885+
getLogger().error("Error in SessionLockListener.lockReleased",
886+
e);
887+
}
888+
}
889+
}
890+
816891
/**
817892
* Adds a listener that gets notified when a Vaadin service session that has
818893
* been initialized for this service is destroyed.
@@ -1027,7 +1102,7 @@ protected Lock lockSession(WrappedSession wrappedSession) {
10271102
synchronized (VaadinService.class) {
10281103
lock = getSessionLock(wrappedSession);
10291104
if (lock == null) {
1030-
lock = new ReentrantLock();
1105+
lock = new InstrumentedReentrantLock(this);
10311106
setSessionLock(wrappedSession, lock);
10321107
}
10331108
}

0 commit comments

Comments
 (0)