Skip to content

Commit

Permalink
WebSockets Next: clarify connector API docs/javadoc
Browse files Browse the repository at this point in the history
- also add tests for programmatic lookup of connectors
- related to quarkusio#44465
  • Loading branch information
mkouba committed Nov 13, 2024
1 parent 323ba29 commit c59a6dc
Show file tree
Hide file tree
Showing 9 changed files with 271 additions and 12 deletions.
62 changes: 57 additions & 5 deletions docs/src/main/asciidoc/websockets-next-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ and pull requests should be submitted there:
https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc
////
[id="websockets-next-reference-guide"]
= WebSockets Next extension reference guide
= WebSockets Next reference guide
:extension-status: preview
include::_attributes.adoc[]
:numbered:
Expand Down Expand Up @@ -78,7 +78,7 @@ implementation("io.quarkus:quarkus-websockets-next")

== Endpoints

Both the server and client APIs allow you to define _endpoints_ that are used to consume and send messages.
Both the <<server-api>> and <<client-api>> define _endpoints_ that are used to consume and send messages.
The endpoints are implemented as CDI beans and support injection.
Endpoints declare <<callback-methods,_callback methods_>> annotated with `@OnTextMessage`, `@OnBinaryMessage`, `@OnPong`, `@OnOpen`, `@OnClose` and `@OnError`.
These methods are used to handle various WebSocket events.
Expand Down Expand Up @@ -559,6 +559,7 @@ This means that if an endpoint receives events `A` and `B` (in this particular o
However, in some situations it is preferable to process events concurrently, i.e. with no ordering guarantees but also with no concurrency limits.
For this cases, the `InboundProcessingMode#CONCURRENT` should be used.

[[server-api]]
== Server API

=== HTTP server configuration
Expand Down Expand Up @@ -900,14 +901,15 @@ public class CustomTenantResolver implements TenantResolver {
----
For more information on Hibernate multitenancy, refer to the https://quarkus.io/guides/hibernate-orm#multitenancy[hibernate documentation].

[[client-api]]
== Client API

[[client-connectors]]
=== Client connectors

The `io.quarkus.websockets.next.WebSocketConnector<CLIENT>` is used to configure and create new connections for client endpoints.
A CDI bean that implements this interface is provided and can be injected in other beans.
The actual type argument is used to determine the client endpoint.
A connector can be used to configure and open a new client connection backed by a client endpoint that is used to consume and send messages.
Quarkus provides a CDI bean with bean type `io.quarkus.websockets.next.WebSocketConnector<CLIENT>` and default qualifer that can be injected in other beans.
The actual type argument of an injection point is used to determine the client endpoint.
The type is validated during build - if it does not represent a client endpoint the build fails.

Let’s consider the following client endpoint:
Expand Down Expand Up @@ -955,6 +957,31 @@ public class MyBean {

NOTE: If an application attempts to inject a connector for a missing endpoint, an error is thrown.

Connectors are not thread-safe and should not be used concurrently.
Connectors should also not be reused.
If you need to create multiple connections in a row you'll need to obtain a new connetor instance programmatically using `Instance#get()`:

[source, java]
----
import jakarta.enterprise.inject.Instance;
@Singleton
public class MyBean {
@Inject
Instance<WebSocketConnector<MyEndpoint>> connector;
void connect() {
var connection1 = connector.get().baseUri(uri)
.addHeader("Foo", "alpha")
.connectAndAwait();
var connection2 = connector.get().baseUri(uri)
.addHeader("Foo", "bravo")
.connectAndAwait();
}
}
----

==== Basic connector

In the case where the application developer does not need the combination of the client endpoint and the connector, a _basic connector_ can be used.
Expand Down Expand Up @@ -991,6 +1018,31 @@ The basic connector is closer to a low-level API and is reserved for advanced us
However, unlike others low-level WebSocket clients, it is still a CDI bean and can be injected in other beans.
It also provides a way to configure the execution model of the callbacks, ensuring optimal integration with the rest of Quarkus.

Connectors are not thread-safe and should not be used concurrently.
Connectors should also not be reused.
If you need to create multiple connections in a row you'll need to obtain a new connetor instance programmatically using `Instance#get()`:

[source, java]
----
import jakarta.enterprise.inject.Instance;
@Singleton
public class MyBean {
@Inject
Instance<BasicWebSocketConnector> connector;
void connect() {
var connection1 = connector.get().baseUri(uri)
.addHeader("Foo", "alpha")
.connectAndAwait();
var connection2 = connector.get().baseUri(uri)
.addHeader("Foo", "bravo")
.connectAndAwait();
}
}
----

[[ws-client-connection]]
=== WebSocket client connection

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package io.quarkus.websockets.next.test.client.programmatic;

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.net.URI;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.websockets.next.HandshakeRequest;
import io.quarkus.websockets.next.OnClose;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.WebSocketClient;
import io.quarkus.websockets.next.WebSocketClientConnection;
import io.quarkus.websockets.next.WebSocketConnector;

public class ClientEndpointProgrammaticTest {

@RegisterExtension
public static final QuarkusUnitTest test = new QuarkusUnitTest()
.withApplicationRoot(root -> {
root.addClasses(ServerEndpoint.class, ClientEndpoint.class);
});

@Inject
Instance<WebSocketConnector<ClientEndpoint>> connector;

@TestHTTPResource("/")
URI uri;

@Test
void testClient() throws InterruptedException {
WebSocketClientConnection connection1 = connector
.get()
.baseUri(uri)
.addHeader("Foo", "Lu")
.connectAndAwait();
connection1.sendTextAndAwait("Hi!");

WebSocketClientConnection connection2 = connector
.get()
.baseUri(uri)
.addHeader("Foo", "Ma")
.connectAndAwait();
connection2.sendTextAndAwait("Hi!");

assertTrue(ClientEndpoint.MESSAGE_LATCH.await(5, TimeUnit.SECONDS));
assertTrue(ClientEndpoint.MESSAGES.contains("Lu:Hello Lu!"));
assertTrue(ClientEndpoint.MESSAGES.contains("Lu:Hi!"));
assertTrue(ClientEndpoint.MESSAGES.contains("Ma:Hello Ma!"));
assertTrue(ClientEndpoint.MESSAGES.contains("Ma:Hi!"), ClientEndpoint.MESSAGES.toString());

connection1.closeAndAwait();
connection2.closeAndAwait();
assertTrue(ClientEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
assertTrue(ServerEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
}

@WebSocket(path = "/endpoint")
public static class ServerEndpoint {

static final CountDownLatch CLOSED_LATCH = new CountDownLatch(2);

@OnOpen
String open(HandshakeRequest handshakeRequest) {
return "Hello " + handshakeRequest.header("Foo") + "!";
}

@OnTextMessage
String echo(String message) {
return message;
}

@OnClose
void close() {
CLOSED_LATCH.countDown();
}

}

@WebSocketClient(path = "/endpoint")
public static class ClientEndpoint {

static final CountDownLatch MESSAGE_LATCH = new CountDownLatch(4);

static final List<String> MESSAGES = new CopyOnWriteArrayList<>();

static final CountDownLatch CLOSED_LATCH = new CountDownLatch(2);

@OnTextMessage
void onMessage(String message, HandshakeRequest handshakeRequest) {
MESSAGES.add(handshakeRequest.header("Foo") + ":" + message);
MESSAGE_LATCH.countDown();
}

@OnClose
void close() {
CLOSED_LATCH.countDown();
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.quarkus.websockets.next.test.client.programmatic;

import static org.junit.jupiter.api.Assertions.fail;

import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.arc.Unremovable;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.websockets.next.WebSocketClientException;
import io.quarkus.websockets.next.WebSocketConnector;

public class InvalidConnectorProgrammaticInjectionPointTest {

@RegisterExtension
public static final QuarkusUnitTest test = new QuarkusUnitTest()
.withApplicationRoot(root -> {
root.addClasses(Service.class);
})
.setExpectedException(WebSocketClientException.class, true);

@Test
void testInvalidInjectionPoint() {
fail();
}

@Unremovable
@Singleton
public static class Service {

@Inject
Instance<WebSocketConnector<String>> invalid;

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,40 @@
import java.util.function.BiConsumer;
import java.util.function.Consumer;

import jakarta.enterprise.inject.Default;
import jakarta.enterprise.inject.Instance;

import io.quarkus.arc.Arc;
import io.smallrye.common.annotation.CheckReturnValue;
import io.smallrye.common.annotation.Experimental;
import io.smallrye.mutiny.Uni;
import io.vertx.core.buffer.Buffer;

/**
* This basic connector can be used to configure and open new client connections. Unlike with {@link WebSocketConnector} a
* client endpoint class is not needed.
* A basic connector can be used to configure and open a new client connection. Unlike with {@link WebSocketConnector} a
* client endpoint is not used to consume and send messages.
* <p>
* Quarkus provides a CDI bean with bean type {@code BasicWebSocketConnector} and qualifier {@link Default}.
* <p>
* This construct is not thread-safe and should not be used concurrently.
* <p>
* Connectors should not be reused. If you need to create multiple connections in a row you'll need to obtain a new connetor
* instance programmatically using {@link Instance#get()}:
* <code><pre>
* import jakarta.enterprise.inject.Instance;
*
* &#64;Inject
* Instance&#60;BasicWebSocketConnector&#62; connector;
*
* void connect() {
* var connection1 = connector.get().baseUri(uri)
* .addHeader("Foo", "alpha")
* .connectAndAwait();
* var connection2 = connector.get().baseUri(uri)
* .addHeader("Foo", "bravo")
* .connectAndAwait();
* }
* </pre></code>
*
* @see WebSocketClientConnection
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
import java.util.Optional;
import java.util.stream.Stream;

import jakarta.enterprise.inject.Default;

import io.smallrye.common.annotation.Experimental;

/**
* Provides convenient access to all open client connections.
* <p>
* Quarkus provides a built-in CDI bean with the {@link jakarta.inject.Singleton} scope that implements this interface.
* Quarkus provides a CDI bean with bean type {@link OpenClientConnections} and qualifier {@link Default}.
*/
@Experimental("This API is experimental and may change in the future")
public interface OpenClientConnections extends Iterable<WebSocketClientConnection> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
import java.util.Optional;
import java.util.stream.Stream;

import jakarta.enterprise.inject.Default;

import io.smallrye.common.annotation.Experimental;

/**
* Provides convenient access to all open connections.
* <p>
* Quarkus provides a built-in CDI bean with the {@link jakarta.inject.Singleton} scope that implements this interface.
* Quarkus provides a CDI bean with bean type {@link OpenConnections} and qualifier {@link Default}.
*/
@Experimental("This API is experimental and may change in the future")
public interface OpenConnections extends Iterable<WebSocketConnection> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/**
* This interface represents a client connection to a WebSocket endpoint.
* <p>
* Quarkus provides a built-in CDI bean that implements this interface and can be injected in a {@link WebSocketClient}
* Quarkus provides a CDI bean that implements this interface and can be injected in a {@link WebSocketClient}
* endpoint and used to interact with the connected server.
*/
@Experimental("This API is experimental and may change in the future")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
/**
* This interface represents a connection from a client to a specific {@link WebSocket} endpoint on the server.
* <p>
* Quarkus provides a built-in CDI bean that implements this interface and can be injected in a {@link WebSocket}
* Quarkus provides a CDI bean that implements this interface and can be injected in a {@link WebSocket}
* endpoint and used to interact with the connected client, or all clients connected to the endpoint respectively
* (broadcasting).
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,40 @@
import java.net.URI;
import java.net.URLEncoder;

import jakarta.enterprise.inject.Default;
import jakarta.enterprise.inject.Instance;

import io.smallrye.common.annotation.CheckReturnValue;
import io.smallrye.common.annotation.Experimental;
import io.smallrye.mutiny.Uni;

/**
* This connector can be used to configure and open new client connections using a client endpoint class.
* A connector can be used to configure and open a new client connection backed by a client endpoint that is used to
* consume and send messages.
* <p>
* Quarkus provides a CDI bean with bean type {@code WebSocketConnector<CLIENT>} and qualifier {@link Default}. The actual type
* argument of an injection point is used to determine the client endpoint. The type is validated during build
* and if it does not represent a client endpoint then the build fails.
* <p>
* This construct is not thread-safe and should not be used concurrently.
* <p>
* Connectors should not be reused. If you need to create multiple connections in a row you'll need to obtain a new connetor
* instance programmatically using {@link Instance#get()}:
* <code><pre>
* import jakarta.enterprise.inject.Instance;
*
* &#64;Inject
* Instance&#60;WebSocketConnector&#60;MyEndpoint&#62;&#62; connector;
*
* void connect() {
* var connection1 = connector.get().baseUri(uri)
* .addHeader("Foo", "alpha")
* .connectAndAwait();
* var connection2 = connector.get().baseUri(uri)
* .addHeader("Foo", "bravo")
* .connectAndAwait();
* }
* </pre></code>
*
* @param <CLIENT> The client endpoint class
* @see WebSocketClient
Expand Down

0 comments on commit c59a6dc

Please sign in to comment.