Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
441de42
[Automated] Update the native jar versions
chathushkaayash Mar 16, 2025
9c23d55
Add connection closure timeout configuration to WebSocket service
chathushkaayash Mar 17, 2025
3e71ae4
Add default connection closure timeout constant
chathushkaayash Mar 17, 2025
06cf25d
Merge branch 'main' into connection-closure-timeout
chathushkaayash Mar 25, 2025
23b90ea
Merge branch 'main' into connection-closure-timeout
chathushkaayash Mar 25, 2025
0d44e7e
Merge branch 'main' into connection-closure-timeout
chathushkaayash Mar 28, 2025
d88e8f3
[Automated] Update the native jar versions
chathushkaayash Mar 28, 2025
9d29c75
Update changelog
chathushkaayash Mar 28, 2025
5b6c942
Refactor connection closure timeout handling in WebSocket connector
chathushkaayash Mar 31, 2025
cc266f4
Merge branch 'main' into connection-closure-timeout
daneshk Mar 31, 2025
fb232b6
Update the spec
chathushkaayash Mar 31, 2025
597f4ac
Merge branch 'main' into connection-closure-timeout
chathushkaayash Apr 1, 2025
c86d100
Update sendCloseFrame method to use connectionClosureTimeout
chathushkaayash Apr 2, 2025
27442b9
Add test for connection closure timeout when returning close frames
chathushkaayash Apr 2, 2025
e8cfcf8
[Automated] Update the native jar versions
chathushkaayash Apr 2, 2025
6db92c9
Bump version to 2.15.0
chathushkaayash Apr 2, 2025
8912ad0
Update the behavior of handling negative values in connectionClosureT…
chathushkaayash Apr 2, 2025
d0a8285
Add compiler plugin validation for connection closure timeout
chathushkaayash Apr 2, 2025
ad49678
Remove unreachable exception in findTimeoutInSeconds
chathushkaayash Apr 3, 2025
1af2fff
Add test for handling negative timeout values in close method of the …
chathushkaayash Apr 3, 2025
bf91804
Remove unnecessary variable assignment
chathushkaayash Apr 3, 2025
81062ca
Remove unnecessary parenthesis
chathushkaayash Apr 3, 2025
ccbaef3
Refactor isAnnotationPresent method
chathushkaayash Apr 3, 2025
7be39da
Handle ArithmeticException in findTimeoutInSeconds method
chathushkaayash Apr 3, 2025
eead202
Enhance WebSocketUpgradeServiceValidatorTask
chathushkaayash Apr 4, 2025
2b1ff8e
Refactor getAnnotationValue method
chathushkaayash Apr 4, 2025
2e2dfb1
Improve timeout handling in WebSocket close operations and add tests …
chathushkaayash Apr 4, 2025
648bb8f
Refactor findTimeoutInSeconds method to handle exceptions
chathushkaayash Apr 5, 2025
b7f3a73
Refactor WebSocketUpgradeServiceValidatorTask to use SyntaxKind for f…
chathushkaayash Apr 5, 2025
47f0ef3
Refactor getAnnotationValue function to use equal symbol
chathushkaayash Apr 7, 2025
a908f5c
Add note on connectionClosureTimeout validation in WSServiceConfig do…
chathushkaayash Apr 7, 2025
89e8d2b
Refactor isAnnotationFieldPresent to use syntax kind checking
chathushkaayash Apr 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions ballerina/Ballerina.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,19 @@ path = "../native/build/libs/websocket-native-2.14.0-SNAPSHOT.jar"
groupId = "io.ballerina.stdlib"
artifactId = "http-native"
version = "2.14.0"
path = "./lib/http-native-2.14.0-20250305-195600-c559baa.jar"
path = "./lib/http-native-2.14.0-20250311-152000-c0b9408.jar"

[[platform.java21.dependency]]
groupId = "io.ballerina.stdlib"
artifactId = "mime-native"
version = "2.12.0"
path = "./lib/mime-native-2.12.0-20250305-174800-8528404.jar"
path = "./lib/mime-native-2.12.0-20250311-141300-98bc7d6.jar"

[[platform.java21.dependency]]
groupId = "io.ballerina.stdlib"
artifactId = "constraint-native"
version = "1.7.0"
path = "./lib/constraint-native-1.7.0-20250305-165400-52275bd.jar"
path = "./lib/constraint-native-1.7.0-20250311-133400-acfb095.jar"

[[platform.java21.dependency]]
groupId = "io.netty"
Expand Down
2 changes: 1 addition & 1 deletion ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

[ballerina]
dependencies-toml-version = "2"
distribution-version = "2201.12.0-20250228-201300-8d411a0f"
distribution-version = "2201.12.0-20250311-124800-b9003fed"

[[package]]
org = "ballerina"
Expand Down
12 changes: 9 additions & 3 deletions ballerina/annotation.bal
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,19 @@
#
# + subProtocols - Negotiable sub protocol by the service
# + idleTimeout - Idle timeout for the client connection. Upon timeout, `onIdleTimeout` resource (if defined)
# in the server service will be triggered. Note that this overrides the `timeout` config
# in the `websocket:Listener`, which is applicable only for the initial HTTP upgrade request
# in the server service will be triggered. Note that this overrides the `timeout` config
# in the `websocket:Listener`, which is applicable only for the initial HTTP upgrade request
# + maxFrameSize - The maximum payload size of a WebSocket frame in bytes.
# If this is not set or is negative or zero, the default frame size, which is 65536 will be used
# If this is not set or is negative or zero, the default frame size, which is 65536 will be used
# + auth - Listener authentication configurations
# + validation - Enable/disable constraint validation
# + dispatcherKey - The key which is going to be used for dispatching to custom remote functions.
# + dispatcherStreamId - The identifier used to distinguish between requests and their corresponding responses in a multiplexing scenario.
# + connectionClosureTimeout - Time to wait (in seconds) for the close frame to be received from the remote endpoint before closing the
# connection. If the timeout exceeds, then the connection is terminated even though a close frame
# is not received from the remote endpoint. If the value < 0 (e.g., -1), then the connection waits
# until a close frame is received. If the WebSocket frame is received from the remote endpoint
# within the waiting period, the connection is terminated immediately.
public type WSServiceConfig record {|
string[] subProtocols = [];
decimal idleTimeout = 0;
Expand All @@ -38,6 +43,7 @@ public type WSServiceConfig record {|
boolean validation = true;
string dispatcherKey?;
string dispatcherStreamId?;
decimal connectionClosureTimeout = 60;
|};

# The annotation which is used to configure a WebSocket service.
Expand Down
50 changes: 50 additions & 0 deletions ballerina/tests/connection_closure_timeout.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import ballerina/lang.runtime;
import ballerina/test;

listener Listener connectionClosureTimeoutListener = new (22100);

@ServiceConfig {
connectionClosureTimeout: 10
}
service / on connectionClosureTimeoutListener {
resource function get .() returns Service|UpgradeError {
return new ConnectionClosureTimeoutService();
}
}

service class ConnectionClosureTimeoutService {
*Service;

remote function onMessage(Caller caller, string data) returns error? {
_ = start caller->close();
runtime:sleep(1);
test:assertTrue(caller.isOpen());
runtime:sleep(10);
test:assertTrue(!caller.isOpen());
}
}

@test:Config {
groups: ["connectionClosureTimeout"]
}
public function testConnectionClosureTimeoutFromServer() returns Error? {
Client wsClient = check new ("ws://localhost:22100/");
check wsClient->writeMessage("Hi");
runtime:sleep(20);
}
4 changes: 2 additions & 2 deletions ballerina/websocket_caller.bal
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public isolated client class Caller {
# within the waiting period, the connection is terminated immediately
# + return - A `websocket:Error` if an error occurs when sending
remote isolated function close(int? statusCode = 1000, string? reason = (),
decimal timeout = 60) returns Error? {
decimal? timeout = ()) returns Error? {
int code = 1000;
if (statusCode is int) {
if (statusCode <= 999 || statusCode >= 1004 && statusCode <= 1006 || statusCode >= 1012 &&
Expand All @@ -101,7 +101,7 @@ public isolated client class Caller {
return self.externClose(code, reason is () ? "" : reason, timeout);
}

isolated function externClose(int statusCode, string reason, decimal timeoutInSecs) returns Error? = @java:Method {
isolated function externClose(int statusCode, string reason, decimal? timeoutInSecs = ()) returns Error? = @java:Method {
'class: "io.ballerina.stdlib.websocket.actions.websocketconnector.Close"
} external;

Expand Down
2 changes: 1 addition & 1 deletion ballerina/websocket_sync_client.bal
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ public isolated client class Client {
}
}

isolated function externClose(int statusCode, string reason, decimal timeoutInSecs)
isolated function externClose(int statusCode, string reason, decimal? timeoutInSecs)
returns Error? = @java:Method {
'class: "io.ballerina.stdlib.websocket.actions.websocketconnector.Close"
} external;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public class WebSocketConstants {
public static final String WEBSOCKET_ANNOTATION_CONFIGURATION = "ServiceConfig";
public static final BString ANNOTATION_ATTR_SUB_PROTOCOLS = StringUtils.fromString("subProtocols");
public static final BString ANNOTATION_ATTR_IDLE_TIMEOUT = StringUtils.fromString("idleTimeout");
public static final BString ANNOTATION_ATTR_CONNECTION_CLOSURE_TIMEOUT =
StringUtils.fromString("connectionClosureTimeout");
public static final BString ANNOTATION_ATTR_READ_IDLE_TIMEOUT = StringUtils.fromString("readTimeout");
public static final BString ANNOTATION_ATTR_TIMEOUT = StringUtils.fromString("timeout");
public static final BString ANNOTATION_ATTR_MAX_FRAME_SIZE = StringUtils.fromString("maxFrameSize");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.ballerina.stdlib.websocket.observability.WebSocketObservabilityConstants;
import io.ballerina.stdlib.websocket.observability.WebSocketObservabilityUtil;
import io.ballerina.stdlib.websocket.server.WebSocketConnectionInfo;
import io.ballerina.stdlib.websocket.server.WebSocketServerService;
import io.netty.channel.ChannelFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -45,20 +46,21 @@ public class Close {
private static final Logger log = LoggerFactory.getLogger(Close.class);

public static Object externClose(Environment env, BObject wsConnection, long statusCode, BString reason,
BDecimal timeoutInSecs) {
Object bTimeoutInSecs) {
return env.yieldAndRun(() -> {
CompletableFuture<Object> balFuture = new CompletableFuture<>();
WebSocketConnectionInfo connectionInfo = (WebSocketConnectionInfo) wsConnection
.getNativeData(WebSocketConstants.NATIVE_DATA_WEBSOCKET_CONNECTION_INFO);
WebSocketObservabilityUtil.observeResourceInvocation(env, connectionInfo,
WebSocketConstants.RESOURCE_NAME_CLOSE);
int timeoutInSecs = getConnectionClosureTimeout(bTimeoutInSecs, connectionInfo);
try {
CountDownLatch countDownLatch = new CountDownLatch(1);
List<BError> errors = new ArrayList<>(1);
ChannelFuture closeFuture = initiateConnectionClosure(errors, (int) statusCode, reason.getValue(),
connectionInfo, countDownLatch);
connectionInfo.getWebSocketConnection().readNextFrame();
waitForTimeout(errors, (int) timeoutInSecs.floatValue(), countDownLatch, connectionInfo);
waitForTimeout(errors, timeoutInSecs, countDownLatch, connectionInfo);
closeFuture.channel().close().addListener(future -> {
WebSocketUtil.setListenerOpenField(connectionInfo);
if (errors.isEmpty()) {
Expand All @@ -81,6 +83,16 @@ public static Object externClose(Environment env, BObject wsConnection, long sta
});
}

private static int getConnectionClosureTimeout(Object bTimeoutInSecs, WebSocketConnectionInfo connectionInfo) {
int connectionClosureTimeout = 60;
if (bTimeoutInSecs instanceof BDecimal) {
return (int) ((BDecimal) bTimeoutInSecs).floatValue();
} else if (connectionInfo.getService() instanceof WebSocketServerService webSocketServerService) {
return webSocketServerService.getConnectionClosureTimeout();
}
return connectionClosureTimeout;
}

private static ChannelFuture initiateConnectionClosure(List<BError> errors, int statusCode,
String reason, WebSocketConnectionInfo connectionInfo, CountDownLatch latch) throws IllegalAccessException {
WebSocketConnection webSocketConnection = connectionInfo.getWebSocketConnection();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class WebSocketServerService extends WebSocketService {
private int idleTimeoutInSeconds = 0;
private boolean enableValidation = true;
private String dispatchingKey = null;
private int connectionClosureTimeout;

public WebSocketServerService(BObject service, Runtime runtime, String basePath) {
super(service, runtime);
Expand All @@ -56,6 +57,8 @@ private void populateConfigs(String basePath) {
negotiableSubProtocols = WebSocketUtil.findNegotiableSubProtocols(configAnnotation);
idleTimeoutInSeconds = WebSocketUtil.findTimeoutInSeconds(configAnnotation,
WebSocketConstants.ANNOTATION_ATTR_IDLE_TIMEOUT, 0);
connectionClosureTimeout = WebSocketUtil.findTimeoutInSeconds(configAnnotation,
WebSocketConstants.ANNOTATION_ATTR_CONNECTION_CLOSURE_TIMEOUT, 60);
maxFrameSize = WebSocketUtil.findMaxFrameSize(configAnnotation);
enableValidation = configAnnotation.getBooleanValue(ANNOTATION_ATTR_VALIDATION_ENABLED);
if (configAnnotation.getStringValue(ANNOTATION_ATTR_DISPATCHER_KEY) != null) {
Expand Down Expand Up @@ -101,4 +104,8 @@ public String getDispatchingKey() {
public String getBasePath() {
return basePath;
}

public int getConnectionClosureTimeout() {
return connectionClosureTimeout;
}
}