Skip to content

Commit 1ba69b7

Browse files
Merge pull request #105 from webfirmframework/dev-12.x.x
wffweb-12.0.10 release changes
2 parents 620ce26 + 0b0bd2e commit 1ba69b7

File tree

17 files changed

+1303
-837
lines changed

17 files changed

+1303
-837
lines changed

wffweb/pom.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<modelVersion>4.0.0</modelVersion>
55
<groupId>com.webfirmframework</groupId>
66
<artifactId>wffweb</artifactId>
7-
<version>12.0.9</version>
7+
<version>12.0.10</version>
88

99
<properties>
1010
<maven.build.timestamp.format>yyyyMMddHHmmss</maven.build.timestamp.format>
@@ -49,7 +49,7 @@
4949
<plugin>
5050
<groupId>org.sonatype.central</groupId>
5151
<artifactId>central-publishing-maven-plugin</artifactId>
52-
<version>0.9.0</version>
52+
<version>0.10.0</version>
5353
<extensions>true</extensions>
5454
<configuration>
5555
<publishingServerId>central</publishingServerId>
@@ -88,7 +88,7 @@
8888
<plugin>
8989
<groupId>org.apache.maven.plugins</groupId>
9090
<artifactId>maven-source-plugin</artifactId>
91-
<version>3.3.1</version>
91+
<version>3.4.0</version>
9292
<executions>
9393
<execution>
9494
<id>attach-sources</id>

wffweb/src/main/java/com/webfirmframework/wffweb/internal/tag/html/listener/ChildTagRemoveListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@ public static record Event(AbstractHtml parentTag, AbstractHtml removedChildTag,
3131

3232
public void childrenRemoved(Event event);
3333

34-
public void allChildrenRemoved(Event event);
34+
public boolean allChildrenRemoved(Event event);
3535

3636
}

wffweb/src/main/java/com/webfirmframework/wffweb/server/page/BrowserPage.java

Lines changed: 81 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,13 @@ public abstract class BrowserPage implements Serializable {
190190

191191
private static final int INITIAL_WS_DEFAULT_HEARTBEAT_INTERVAL = 25_000;
192192

193-
private static final int INITIAL_WS_DEFAULT_HEARTBEAT_TIMEOUT = INITIAL_WS_DEFAULT_HEARTBEAT_INTERVAL;
193+
private static final int INITIAL_WS_DEFAULT_HEARTBEAT_TIMEOUT = 10_000;
194194

195195
private static volatile int wsDefaultHeartbeatInterval = INITIAL_WS_DEFAULT_HEARTBEAT_INTERVAL;
196196

197197
private static volatile int wsDefaultReconnectInterval = 2_000;
198198

199-
private static volatile int wsDefaultHeartbeatTimeout = 10_000;
199+
private static volatile int wsDefaultHeartbeatTimeout = INITIAL_WS_DEFAULT_HEARTBEAT_TIMEOUT;
200200

201201
private final LongAdder pushQueueSize = new LongAdder();
202202

@@ -247,6 +247,8 @@ public abstract class BrowserPage implements Serializable {
247247

248248
private final AtomicInteger clientSidePayloadIdGenerator = new AtomicInteger();
249249

250+
private final AtomicBoolean hasHitBufferOutOfMemory = new AtomicBoolean();
251+
250252
private static final long DEFAULT_IO_BUFFER_TIMEOUT = TimeUnit.MILLISECONDS
251253
.toNanos(INITIAL_WS_DEFAULT_HEARTBEAT_INTERVAL);
252254

@@ -606,9 +608,9 @@ public final WebSocketPushListener getWsListener() {
606608
return wsListenerHolder != null ? wsListenerHolder.webSocketPushListener : null;
607609
}
608610

609-
final void push(final NameValue... nameValues) {
611+
final boolean push(final NameValue... nameValues) {
610612
final ByteBuffer payload = buildPayload(WffBinaryMessageUtil.VERSION_1.getWffBinaryMessageBytes(nameValues));
611-
push(new ClientTasksWrapper(payload));
613+
return push(new ClientTasksWrapper(payload));
612614
}
613615

614616
/**
@@ -631,8 +633,10 @@ final ClientTasksWrapper pushAndGetWrapper(final Queue<Collection<NameValue>> mu
631633
}
632634

633635
final ClientTasksWrapper clientTasks = new ClientTasksWrapper(tasks);
634-
push(clientTasks);
635-
return clientTasks;
636+
if (push(clientTasks)) {
637+
return clientTasks;
638+
}
639+
return null;
636640
}
637641

638642
private void pushLockless(final ClientTasksWrapper clientTasks) {
@@ -652,23 +656,40 @@ private void pushLockless(final ClientTasksWrapper clientTasks) {
652656
}
653657
}
654658

655-
private void push(final ClientTasksWrapper clientTasks) {
659+
private boolean push(final ClientTasksWrapper clientTasks) {
656660
if (outputBufferLimitLock != null) {
657661
final WebSocketPushListenerHolder wsListenerCurrent = wsListener;
658662
if (wsListenerCurrent == null || (!wsListenerCurrent.serverSideActionPerformed.get()
659663
&& !wsListenerCurrent.clientSideJSExecuted.get())) {
660664
try {
665+
// if wsListenerCurrent is null there is no advantage of waiting
666+
final boolean acquired = isQuickTryAcquireApplicableWithNoBOMCheck(wsListenerCurrent)
667+
? outputBufferLimitLock.tryAcquire(clientTasks.getCurrentSize())
668+
: outputBufferLimitLock.tryAcquire(clientTasks.getCurrentSize(),
669+
settings.outputBufferTimeout, TimeUnit.NANOSECONDS);
661670
// onPayloadLoss check should be second
662-
if (outputBufferLimitLock.tryAcquire(clientTasks.getCurrentSize(), settings.outputBufferTimeout,
663-
TimeUnit.NANOSECONDS) || onPayloadLoss == null) {
671+
if (acquired || onPayloadLoss == null) {
664672
pushLockless(clientTasks);
673+
return true;
665674
} else {
666-
if (LOGGER.isLoggable(Level.SEVERE)) {
667-
LOGGER.severe(
668-
"""
669-
Buffer timeout reached while preparing server event for client so further changes will not be pushed to client.
670-
Increase Settings.outputBufferLimit or Settings.outputBufferTimeout to solve this issue.
671-
NB: Settings.outputBufferTimeout should be <= maxIdleTimeout by BrowserPageContent.enableAutoClean method.""");
675+
if (LOGGER.isLoggable(Level.SEVERE) || LOGGER.isLoggable(Level.FINEST)) {
676+
if (wsListenerCurrent != null) {
677+
if (LOGGER.isLoggable(Level.SEVERE)) {
678+
LOGGER.severe(
679+
"""
680+
Buffer timeout reached while preparing server event for client so further changes will not be pushed to client.
681+
Increase Settings.outputBufferLimit or Settings.outputBufferTimeout to solve this issue.
682+
NB: Settings.outputBufferTimeout should be <= maxIdleTimeout by BrowserPageContent.enableAutoClean method.""");
683+
}
684+
} else {
685+
if (LOGGER.isLoggable(Level.FINEST)) {
686+
LOGGER.finest(
687+
"""
688+
Buffer timeout reached while preparing server event for client so further changes will not be pushed to client and there is no active websocket connection available at the moment.
689+
Increase Settings.outputBufferLimit or Settings.outputBufferTimeout to solve this issue.
690+
NB: Settings.outputBufferTimeout should be <= maxIdleTimeout by BrowserPageContent.enableAutoClean method.""");
691+
}
692+
}
672693
}
673694
if (wsListenerCurrent != null && !wsListenerCurrent.clientSideJSExecuted.get()
674695
&& onPayloadLoss.javaScript != null && !onPayloadLoss.javaScript.isBlank()) {
@@ -691,6 +712,7 @@ private void push(final ClientTasksWrapper clientTasks) {
691712
wffBMBytesQueue.offerFirst(new ClientTasksWrapper(clientAction));
692713
}
693714
wsListenerCurrent.clientSideJSExecuted.set(true);
715+
hasHitBufferOutOfMemory.set(true);
694716
}
695717

696718
}
@@ -702,7 +724,9 @@ private void push(final ClientTasksWrapper clientTasks) {
702724
}
703725
} else {
704726
pushLockless(clientTasks);
727+
return true;
705728
}
729+
return false;
706730
}
707731

708732
private void pushWffBMBytesQueue() {
@@ -783,7 +807,7 @@ private void pushWffBMBytesQueue() {
783807

784808
if (pushWffBMBytesQueueLock.hasQueuedThreads()) {
785809
final Thread waitingThread = waitingThreadRef.get();
786-
if (waitingThread != null && !waitingThread.equals(taskThread)
810+
if (waitingThread != null && waitingThread != taskThread
787811
&& waitingThread.getPriority() >= taskThread.getPriority()) {
788812
break;
789813
}
@@ -911,22 +935,34 @@ final void webSocketMessagedWithoutLosslessCheck(final byte[] message) {
911935

912936
if (inputBufferLimitLock != null) {
913937
try {
938+
final WebSocketPushListenerHolder wsListenerCurrent = wsListener;
939+
final boolean acquired = isQuickTryAcquireApplicable(wsListenerCurrent)
940+
? inputBufferLimitLock.tryAcquire(message.length)
941+
: inputBufferLimitLock.tryAcquire(message.length, settings.inputBufferTimeout,
942+
TimeUnit.NANOSECONDS);
914943
// onPayloadLoss check should be second
915-
if (inputBufferLimitLock.tryAcquire(message.length, settings.inputBufferTimeout, TimeUnit.NANOSECONDS)
916-
|| onPayloadLoss == null) {
944+
if (acquired || onPayloadLoss == null) {
917945
taskFromClientQ.offer(message);
918946
} else {
919-
if (LOGGER.isLoggable(Level.SEVERE)) {
920-
LOGGER.severe(
921-
"""
922-
Buffer timeout reached while processing event from client so further client events will not be received at server side.
923-
Increase Settings.inputBufferLimit or Settings.inputBufferTimeout to solve this issue.
924-
NB: Settings.inputBufferTimeout should be <= maxIdleTimeout by BrowserPageContent.enableAutoClean method.""");
947+
if (LOGGER.isLoggable(Level.SEVERE) || LOGGER.isLoggable(Level.FINEST)) {
948+
final String msg = """
949+
Buffer timeout reached while processing event from client so further client events will not be received at server side.
950+
Increase Settings.inputBufferLimit or Settings.inputBufferTimeout to solve this issue.
951+
NB: Settings.inputBufferTimeout should be <= maxIdleTimeout by BrowserPageContent.enableAutoClean method.""";
952+
if (wsListenerCurrent != null) {
953+
if (LOGGER.isLoggable(Level.SEVERE)) {
954+
LOGGER.severe(msg);
955+
}
956+
} else {
957+
if (LOGGER.isLoggable(Level.FINEST)) {
958+
LOGGER.finest(msg);
959+
}
960+
}
925961
}
926-
final WebSocketPushListenerHolder wsListenerCurrent = wsListener;
927962
if (wsListenerCurrent != null && onPayloadLoss.serverSideAction != null) {
928963
onPayloadLoss.serverSideAction.perform();
929964
wsListenerCurrent.serverSideActionPerformed.set(true);
965+
hasHitBufferOutOfMemory.set(true);
930966
}
931967
}
932968
} catch (final InterruptedException e) {
@@ -968,6 +1004,7 @@ final boolean checkLosslessCommunication(final byte[] message) {
9681004
}
9691005
onPayloadLoss.serverSideAction.perform();
9701006
wsListenerCurrent.serverSideActionPerformed.set(true);
1007+
hasHitBufferOutOfMemory.set(true);
9711008
return false;
9721009
}
9731010
}
@@ -3481,4 +3518,23 @@ private int getClientSidePayloadId() {
34813518
return id;
34823519
}
34833520

3521+
private boolean isQuickTryAcquireApplicable(final WebSocketPushListenerHolder wsListenerCurrent) {
3522+
return wsListenerCurrent == null || hasHitBufferOutOfMemory.get() || (wsHeartbeatInterval > 0
3523+
&& wsHeartbeatTimeout > 0
3524+
&& (System.currentTimeMillis() - lastClientAccessedTime) > (wsHeartbeatInterval + wsHeartbeatTimeout));
3525+
}
3526+
3527+
private boolean isQuickTryAcquireApplicableWithNoBOMCheck(final WebSocketPushListenerHolder wsListenerCurrent) {
3528+
return wsListenerCurrent == null || (wsHeartbeatInterval > 0 && wsHeartbeatTimeout > 0
3529+
&& (System.currentTimeMillis() - lastClientAccessedTime) > (wsHeartbeatInterval + wsHeartbeatTimeout));
3530+
}
3531+
3532+
/**
3533+
* @return true if the browser page has hit out of memory.
3534+
* @since 12.0.10
3535+
*/
3536+
public final boolean hasHitBufferOutOfMemory() {
3537+
return hasHitBufferOutOfMemory.get();
3538+
}
3539+
34843540
}

wffweb/src/main/java/com/webfirmframework/wffweb/server/page/BrowserPageContext.java

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import java.util.Queue;
2727
import java.util.Set;
2828
import java.util.concurrent.ConcurrentHashMap;
29-
import java.util.concurrent.ConcurrentLinkedQueue;
3029
import java.util.concurrent.Executor;
3130
import java.util.concurrent.Executors;
3231
import java.util.concurrent.ScheduledExecutorService;
@@ -86,13 +85,6 @@ public enum BrowserPageContext {
8685

8786
private transient final ReferenceQueue<BrowserPage> browserPageRQ;
8887

89-
private transient final Queue<BrowserPageGCTask> browserPageGCTasksQ;
90-
91-
// NB: this caching is required trigger to enqueue to ReferenceQueue when
92-
// BrowserPageGCTask object is GCed
93-
// also remove it from this cache when polled from browserPageRQ
94-
private transient final Set<BrowserPageGCTask> browserPageGCTasksCache;
95-
9688
/**
9789
* Note: Only for internal use.
9890
*
@@ -120,8 +112,6 @@ private BrowserPageSessionWrapper(final String httpSessionId) {
120112
instanceIdBPForWS = new ConcurrentHashMap<>();
121113
instanceIdHttpSessionId = new ConcurrentHashMap<>();
122114
browserPageRQ = new ReferenceQueue<>();
123-
browserPageGCTasksQ = new ConcurrentLinkedQueue<>();
124-
browserPageGCTasksCache = ConcurrentHashMap.newKeySet(2);
125115

126116
initConfig();
127117
}
@@ -170,10 +160,8 @@ public BrowserPageSession addBrowserPage(final String httpSessionId, final Brows
170160
}
171161

172162
if (browserPage.getExternalDrivePath() != null) {
173-
// NB: this caching is required trigger to enqueue to ReferenceQueue when
174-
// BrowserPageGCTask object is GCed
175-
// also remove it from this cache when polled from browserPageRQ
176-
browserPageGCTasksCache.add(new BrowserPageGCTask(browserPage, browserPageRQ));
163+
// the object is registered in Cleaner object in the constructor.
164+
new BrowserPageGCTask(browserPage, browserPageRQ);
177165
}
178166

179167
runAutoClean();
@@ -579,20 +567,18 @@ public void clean(final long maxIdleTimeout) {
579567
});
580568
}
581569

582-
Reference<? extends BrowserPage> gcTask;
583-
while ((gcTask = browserPageRQ.poll()) != null) {
584-
gcTask.clear();
585-
browserPageGCTasksQ.offer((BrowserPageGCTask) gcTask);
586-
}
587-
588570
runGCTasksForBrowserPage();
589571
}
590572

591573
private void runGCTasksForBrowserPage() {
592-
BrowserPageGCTask gcTask;
593-
while ((gcTask = browserPageGCTasksQ.poll()) != null) {
594-
browserPageGCTasksCache.remove(gcTask);
595-
gcTask.run();
574+
// Never call gcTask.clear(), it may lead to potential race condition (We faced
575+
// similar bug.).
576+
// Read javadoc for more details.
577+
Reference<? extends BrowserPage> gcTask;
578+
while ((gcTask = browserPageRQ.poll()) != null) {
579+
if (gcTask instanceof final BrowserPageGCTask browserPageGCTask) {
580+
browserPageGCTask.clean();
581+
}
596582
}
597583
}
598584

@@ -687,11 +673,7 @@ public boolean isAutoCleanEnabled() {
687673
* @since 12.0.1
688674
*/
689675
public void runAutoClean() {
690-
Reference<? extends BrowserPage> gcTask;
691-
while ((gcTask = browserPageRQ.poll()) != null) {
692-
gcTask.clear();
693-
browserPageGCTasksQ.offer((BrowserPageGCTask) gcTask);
694-
}
676+
runGCTasksForBrowserPage();
695677
final MinIntervalExecutor autoCleanTaskExecutor = this.autoCleanTaskExecutor;
696678
if (autoCleanTaskExecutor != null) {
697679
autoCleanTaskExecutor.runAsync();
@@ -954,9 +936,10 @@ public boolean exists(final BrowserPage browserPage) throws NullValueException {
954936
* Checks the existence of valid {@code browserPage} in this context.
955937
*
956938
* <br>
957-
* Note: this operation is not atomic. The validity is time dependent, even if
939+
* Note: this operation is not atomic. The validity is time-dependent, even if
958940
* the method returns true {@code browserPage} could be invalid in the next
959-
* moment. However, if it returns false it is trust worthy.
941+
* moment. However, if it returns false it is trustworthy. Even if the browser
942+
* page has hit buffer out of memory then also it will return false.
960943
*
961944
* @param browserPage
962945
* @return true if the given browserPage exists in the BrowserPageContext and
@@ -972,7 +955,9 @@ public boolean existsAndValid(final BrowserPage browserPage) throws NullValueExc
972955
if (browserPage == null) {
973956
throw new NullValueException("browserPage instance cannot be null");
974957
}
975-
958+
if (browserPage.hasHitBufferOutOfMemory()) {
959+
return false;
960+
}
976961
final MinIntervalExecutor autoCleanTaskExecutor = this.autoCleanTaskExecutor;
977962
if (autoCleanTaskExecutor != null) {
978963
if ((System.currentTimeMillis() - browserPage.getLastClientAccessedTime()) >= autoCleanTaskExecutor

wffweb/src/main/java/com/webfirmframework/wffweb/server/page/BrowserPageGCTask.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,43 @@
1515
*/
1616
package com.webfirmframework.wffweb.server.page;
1717

18+
import java.lang.ref.Cleaner;
1819
import java.lang.ref.ReferenceQueue;
1920
import java.lang.ref.WeakReference;
2021

22+
import com.webfirmframework.wffweb.settings.WffConfiguration;
2123
import com.webfirmframework.wffweb.util.FileUtil;
2224

2325
/**
2426
* @author WFF
2527
* @since 3.0.18
2628
*
2729
*/
28-
class BrowserPageGCTask extends WeakReference<BrowserPage> {
30+
class BrowserPageGCTask extends WeakReference<BrowserPage> implements Runnable {
2931

3032
private final String externalDrivePath;
3133

3234
private final String subDirName;
3335

36+
private final Cleaner.Cleanable cleanable;
37+
3438
BrowserPageGCTask(final BrowserPage referent, final ReferenceQueue<? super BrowserPage> q) {
3539
super(referent, q);
3640
externalDrivePath = referent.getExternalDrivePath();
3741
subDirName = referent.getInstanceId();
42+
cleanable = WffConfiguration.secondaryCleaner().register(referent, this);
3843
}
3944

40-
void run() {
45+
@Override
46+
public void run() {
4147
FileUtil.removeDirRecursively(externalDrivePath, subDirName);
4248
}
4349

50+
/**
51+
* @since 12.0.10
52+
*/
53+
void clean() {
54+
cleanable.clean();
55+
}
56+
4457
}

0 commit comments

Comments
 (0)