This is an automated email from the ASF dual-hosted git repository.
tabish pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/qpid-protonj2.git
The following commit(s) were added to refs/heads/main by this push:
new b14a1b39 PROTON-2767 Add some new documentation in the client docs
module
b14a1b39 is described below
commit b14a1b390c8d66b542597b48f366a32c9077a845
Author: Timothy Bish <[email protected]>
AuthorDate: Tue Sep 26 15:59:33 2023 -0400
PROTON-2767 Add some new documentation in the client docs module
Adds work on a getting started page and pages for large message handling and
client reconnection handling.
---
.../GettingStarted.md | 50 +++++++-
protonj2-client-docs/LargeMessages.md | 128 +++++++++++++++++++++
protonj2-client-docs/Reconnection.md | 95 +++++++++++++++
protonj2-client/README.md | 4 +-
4 files changed, 270 insertions(+), 7 deletions(-)
diff --git a/protonj2-client/README.md b/protonj2-client-docs/GettingStarted.md
similarity index 72%
copy from protonj2-client/README.md
copy to protonj2-client-docs/GettingStarted.md
index 9f610e82..543b2acd 100644
--- a/protonj2-client/README.md
+++ b/protonj2-client-docs/GettingStarted.md
@@ -1,4 +1,4 @@
-# Qpid protonj2 Client Library
+# Getting started with the Qpid protonj2 Client Library
This client provides an imperative API for AMQP messaging applications
@@ -39,7 +39,7 @@ Execute the tests and produce code coverage report:
## Creating a connection
-The entry point for creating new connections with the proton-dotnet client is
the Client
+The entry point for creating new connections with the protonj2 client is the
Client
type which provides a simple static factory method to create new instances.
Client container = Client.create();
@@ -50,34 +50,64 @@ to set the AMQP container Id that will be set on
connections created from a give
instance.
Once you have created a Client instance you can use that to create new
connections which
-will be of type Connection. The Client instance provides API for creating a
connection
-to a given host and port as well as providing connection options object that
carry a large
+will be of type ``Connection``. The Client instance provides API for creating
a connection
+to a given host and port as well as providing connection options objects that
carry a large
set of connection specific configuration elements to customize the behavior of
your connection.
The basic create API looks as follows:
Connection connection = container.connect(remoteAddress, remotePort, new
ConnectionOptions());
-From you connection instance you can then proceed to create sessions, senders
and receivers that
+From your connection instance you can then proceed to create sessions, senders
and receivers that
you can use in your application.
+### Creating a message
+
+The application code can create a message to be sent by using static factory
methods in the ``Message``
+type that can later be sent using the ``Sender`` type. These factory methods
accept a few types that
+map nicely into the standard AMQP message format body sections. A typical
message creation example
+is shown below.
+
+```
+ final Message message = Message.create("message content");
+```
+
+The above code creates a new message object that carries a string value in the
body of an AMQP message
+and it is carried inside an ``AmqpValue`` section. Other methods exist that
wrap other types in the
+appropriate section types, a list of those is given below:
+
++ **Map** The factory method creates a message with the Map value wrapped in
an ``AmqpValue`` section.
++ **List** The factory method creates a message with the List value wrapped in
an ``AmqpSequence`` section.
++ **byte[]** The factory method creates a message with the byte array wrapped
in an ``Data`` section.
++ **Object** All other objects are assumed to be types that should be wrapped
in an ``AmqpValue`` section.
+
+It is also possible to create an empty message and set a body and it will be
wrapped in AMQP section types
+following the same rules as listed above. Advanced users should spend time
reading the API documentation of
+the ``Message`` type to learn more.
+
### Sending a message
Once you have a connection you can create senders that can be used to send
messages to a remote
peer on a specified address. The connection instance provides methods for
creating senders and
is used as follows:
+```
Sender sender = connection.openSender("address");
+```
A message instance must be created before you can send it and the Message
interface provides
simple static factory methods for common message types you might want to send,
for this example
we will create a message that carries text in an AmqpValue body section:
+```
Message<String> message = Message<String>.create("Hello World");
+```
Once you have the message that you want to send the previously created sender
can be used as
follows:
+```
Tracker tracker = sender.send(message);
+```
The Send method of a sender will attempt to send the specified message and if
the connection
is open and the send can be performed it will return a Tracker instance to
provides API for
@@ -90,31 +120,41 @@ To receive a message sent to the remote peer a Receiver
instance must be created
on a given address for new messages to arrive. The connection instance
provides methods for
creating receivers and is used as follows:
+```
Receiver receiver = connection.openReceiver("address");
+```
After creating the receiver the application can then call one of the available
receive APIs to
await the arrival of a message from a remote sender.
+```
Delivery delivery = receiver.receive();
+```
By default receivers created from the client API have a credit window
configured and will
manage the outstanding credit with the remote for your application however if
you have
configured the client not to manage a credit window then your application will
need to
provide receiver credit before invoking the receive APIs.
+```
receiver.addCredit(1);
+```
Once a delivery arrives an Delivery instance is returned which provides API to
both access
the delivered message and to provide a disposition to the remote indicating if
the delivered
message is accepted or was rejected for some reason etc. The message is
obtained by calling
the message API as follows:
+```
Message<object> received = delivery.message();
+```
Once the message is examined and processed the application can accept delivery
by calling
the accept method from the delivery object as follows:
+```
delivery.accept();
+```
Other settlement options exist in the delivery API which provide the
application wil full
access to the AMQP specification delivery outcomes for the received message.
diff --git a/protonj2-client-docs/LargeMessages.md
b/protonj2-client-docs/LargeMessages.md
new file mode 100644
index 00000000..f2c0ba10
--- /dev/null
+++ b/protonj2-client-docs/LargeMessages.md
@@ -0,0 +1,128 @@
+# Sending and Receiving large messages with protonj2
+
+When sending and receiving messages whose size exceeds what might otherwise be
acceptable to having in memory all at once the protonj2 client has a flexible
API to make this process simpler. The stream sender and stream receiver APIs in
the protonj2 client offer the ability to read and write messages in manageable
chunks that prevent application memory being exhausted trying to house the
entire message. This API also provides a simple means of streaming files
directly vs having to write [...]
+
+## Stream senders and receivers
+
+The API for handling large message is broken out into stream senders and
stream receivers that behave a bit differently than the stanard senders and
receivers. Unlike the standard sender and receiver which operate on whole in
memory messages the streaming API makes message content available through
stream where bytes can be read or written in chunks without the need to have to
entire contents in memory at once. Also the underlying streaming
implementation performs tight flow control to [...]
+
+## Using the stream sender
+
+To send a large message using the stream sender API you need to create a
``StreamSender`` type which operates similar to the normal Sender API but
imposes some restrictions on usage compared to the normal Sender. Creating the
stream sender is shown below:
+
+```
+ final StreamSender sender = connection.openStreamSender(address)
+```
+
+This code opens a new stream sender for the given address and returns a
``StreamSender`` type which you can then use to obtain the streaming message
type which you will use to write the outgoing bytes. Unlike the standard
message type the streaming message is created from the sender and it tied
directly to that sender instance, and only one streaming message can be active
on a sender at any give time. To create an outbound stream message use the
following code:
+
+```
+ final StreamSenderMessage message = sender.beginMessage();
+```
+
+This requests that the sender initiate a new outbound streaming message and
will throw an exception if another stream sender message is still active. The
``StreamSenderMessage`` is a specialized ``Message`` type whose body is an
output stream type meaning that it behaves much like a normal message only the
application must get a reference to the body output stream to write the
outgoing bytes. The code below shows how this can be done in practice.
+
+```
+ message.durable(true);
+ message.annotation("x-opt-annotation", "value");
+ message.property("application-property", "value");
+
+ // Creates an OutputStream that writes a single Data Section whose expected
+ // size is configured in the stream options.
+ final OutputStreamOptions streamOptions = new
OutputStreamOptions().bodyLength(knownLength);
+ final OutputStream output = message.body(streamOptions);
+
+ while (<has data to send>) {
+ output.write(buffer, i, chunkSize);
+ }
+
+ output.close(); // This completes the message send.
+
+ message.tracker().awaitSettlement();
+```
+
+In the example above the application code has already obtained a stream sender
message and uses it much like a normal message, setting application properties
and annotations for the receiver to interpret and then begins writing from some
data source a stream of bytes that will be encoded into an AMQP ``Data``
section as the body of the message, the sender will ensure that the writes
occur in managable chunks and will not retain the previously written bytes in
memory. A write call the the [...]
+
+Once the application has written all the payload into the message it completes
the operation by closing the ``OutputStream`` and then it can await settlement
from the remote to indicate the message was received and processed successfully.
+
+### Sending a large file using the stream sender
+
+Sending a file using the ``StreamSenderMessage`` is an ideal use case for the
stream sender. The first thing the application would need to do is to validate
a file exists and open it (this is omitted here). Once a file has been opened
the following code can be used to stream to contents to the remote peer.
+
+```
+ final File inputFile = <app opens file>;
+
+ try (Connection connection = client.connect(serverHost, serverPort,
options);
+ StreamSender sender = connection.openStreamSender(address);
+ FileInputStream inputStream = new FileInputStream(inputFile)) {
+
+ final StreamSenderMessage message = sender.beginMessage();
+
+ // Application can inform the other side what the original file name
was.
+ message.property(fileNameKey, inputFile.getName());
+
+ try (OutputStream output = message.body()) {
+ inputStream.transferTo(output);
+ } catch (IOException e) {
+ message.abort();
+ }
+```
+
+In the example above the code makes use the JDK API from the ``InputStream``
class to transfer the contents of a file to the remote peer, the transfer API
will read the contents of the file in small chunks and write those into the
provided ``OutputStream`` which in this case is the stream from our
``StreamSenderMessage``.
+
+## Using the stream receiver
+
+To receive a large message using the stream receiver API you need to create a
``StreamReceiver`` type which operates similar to the normal Receiver API but
imposes some restrictions on usage compared to the normal Sender. Creating the
stream receiver is shown below:
+
+```
+ final StreamReceiver receiver = connection.openStreamReceiver(address)) {
+```
+
+This code opens a new stream receiver for the given address and returns a
``StreamReceiver`` type which you can then use to obtain the streaming message
type which you will use to read the incoming bytes. Just like the standard
message type the streaming message is received from the receiver instance but
and it is tied directly to that receiver as it read incoming bytes from the
remote peer, therefore only one streaming message can be active on a receiver
at any give time. To create an i [...]
+
+```
+ final StreamDelivery delivery = receiver.receive();
+ final StreamReceiverMessage message = delivery.message();
+```
+
+Once a new inbound streaming message has been received the application can
read the bytes by requesting the ``InputStream`` from the message body and
reading from it as one would any other input stream scenario.
+
+```
+ try (InputStream inputStream = message.body()) {
+ final byte[] chunk = new byte[10];
+ int readCount = 0;
+
+ while (inputStream.read(chunk) != -1) {
+ <Application logic to handle read bytes>
+ }
+ }
+```
+
+In the example code above the application reads from the message body input
stream and simply writes out small chunks of the body to system out, the read
calls might block while waiting for bytes to arrive from the remote but the
application remains unaffected in this case.
+
+### Receiving a large file using the stream receiver
+
+Just as stream sending example from previously sent a large file using the
``StreamSenderMessage`` an application can receive and write a large message
directly into a file with a few quick lines of code. An example follows which
shows how this can be done, the application is responsible for choosing a
proper location for the file and verifiying that it has write access.
+
+```
+ try (Connection connection = client.connect(serverHost, serverPort,
options);
+ StreamReceiver receiver = connection.openStreamReceiver(address)) {
+
+ StreamDelivery delivery = receiver.receive();
+ StreamReceiverMessage message = delivery.message();
+
+ // Application must choose a file name and check it can write to the
+ // target location before receiving the contents.
+ final String outputPath = ...
+ final String filename = ...
+
+ try (FileOutputStream outputStream = new FileOutputStream(new
File(outputPath, filename))) {
+ message.body().transferTo(outputStream);
+ }
+
+ delivery.accept();
+ }
+```
+
+Just as in the stream sender case the application can make use of the JDK
transfer API for ``InputStream`` instances to handle the bulk of the work
reading small blocks of bytes and writing them into the target file, in most
cases the application should add more error handling code not shown in the
example. Reading from the incoming byte stream can block waiting for data from
the remote which may need to be accounted for in some applications.
+
diff --git a/protonj2-client-docs/Reconnection.md
b/protonj2-client-docs/Reconnection.md
new file mode 100644
index 00000000..8b2df421
--- /dev/null
+++ b/protonj2-client-docs/Reconnection.md
@@ -0,0 +1,95 @@
+# Client Fault Tolerance Configuration
+
+The protonj2 client supports configuration to enable a connection to be handle
both reestablished after an interruption and handling not being able to
initially connect to a remote peer.
+
+## Enabling Reconnection
+
+By default the client does not attempt to reconnect to the configured remote
peer, this can be easily done though by toggling the appropriate configuration
option as follows:
+
+```
+ final ConnectionOptions connectionOpts = new ConnectionOptions();
+ connectionOpts.reconnectEnabled(true);
+```
+
+Once enabled the client will try indefinitely to connect and if disconnected
to reconnect to the remote peer that was specified in the connection call that
created the connection instance. Additional options exist to control how many
attempts to connect or reconnect are performed before the client gives up and
marks the connection as failed. An example of configuring reconnection attempt
and delays is below, see the full configuration document for all the available
options and a descrip [...]
+
+```
+ ConnectionOptions options = new ConnectionOptions();
+ options.reconnectOptions().reconnectEnabled(true);
+ options.reconnectOptions().maxReconnectAttempts(5);
+ options.reconnectOptions().maxInitialConnectionAttempts(5);
+ options.reconnectOptions().reconnectDelay(10);
+```
+
+Additional remote locations can be added to the reconnection options to allow
for reconnect to an alternative location should the host specified in the
connect API be unavailable, these hosts will always be tried after the host
specified in the connect API and will be tried in order until a new connection
is established.
+
+```
+ ConnectionOptions options = new ConnectionOptions();
+ options.reconnectOptions().reconnectEnabled(true);
+ options.reconnectOptions().addReconnectLocation("host2", 5672);
+ options.reconnectOptions().addReconnectLocation("host3", 5672);
+ options.reconnectOptions().addReconnectLocation("host4", 5672);
+```
+
+## Reconnection and Client behavior
+
+The client reconnect handling is transparent in most cases and application
code need not be adjusted to handle special case scenarios. In some very
special cases the application nay need to make some configuration changes
depending on how the client is used which mostly involves choosing timeouts for
actions such as send timeouts.
+
+A few select client operations and their behaviors during connection
interruption as documented below:
+
++ **In Flight Send** A message that was sent and a valid tracker instance was
returned will throw exception from any of the tracker methods that operate or
wait on send outcomes which indicate a failure as the client cannot be certain
if the send completed or failed.
++ **Send blocked on credit** A send that is blocked waiting on credit will
continue to wait during a connection interruption and only be failed if the
client reaches configured reconnect limits, or the configured send timeout is
reached.
++ **Active transactions** If the application begins a new transaction and the
client connection is interrupted before the transaction is committed the
transaction will be marked as invalid and any call to commit will throw an
exception, a call to roll back will succeed.
++ **Handling received messages** If the application received a delivery and
attempts to accept it (or apply any other outcome) the disposition operation
will fail indicating the disposition could not be applied.
+
+## Reconnection event notifications
+
+An application can configure event handlers that will be notified for various
events related to the reconnection handling of the protonj2 client. The events
available for subscription consist of following types:
+
++ **Connected** The client succeeded in establishing an initial connection to
a remote peer.
++ **Interrupted** The client connection to a remote peer was broken it will
now attempt to reconnect.
++ **Reconnected** The client succeeded in establishing an new connection to
remote peer after having been interrupted.
++ **Disconnected** The client failed to establish a new connection and the
configured attempt limit was reached (if set).
+
+To subscribe to one of the above events the application must set an event
handler in the connection options instance for the desired event.
+
+As an example the client can set a handler to called upon the first successful
connection to a remote peer and the event would carry the host and port where
the connection was established to in a ConnectionEvent object.
+
+```
+ final ConnectionOptions options = connectionOptions();
+
+ options.connectedHandler((connection, location) -> {
+ LOG.info("Client signaled that it was able to establish a connection");
+ });
+
+```
+
+Then to be notified when an active connection is interrupted a handler is set
in the connection which will be called with an disconnection event that carries
the host and port that the client was connected to and an exception that
provides any available details on the reason for disconnection.
+
+```
+ final ConnectionOptions options = connectionOptions();
+
+ options.interruptedHandler((connection, location) -> {
+ LOG.info("Client signaled that its connection was interrupted");
+ });
+```
+
+To be notified when a connection that was previously interrupted is
successfully able to reconnect to one of the configured remote peers the
reconnection event can be used which will be notified on reconnect and provided
a connection event object that carries the host and port that the client
reconnected to:
+
+```
+ final ConnectionOptions options = connectionOptions();
+
+ options.reconnectedHandler((connection, location) -> {
+ LOG.info("Client signaled that it was able to reconnect");
+ });
+```
+
+To be notified when the client has given up on reconnection due to exceeding
the configured reconnection attempt the application can set a handler on the
disconnected event which will be given a disconnection event object that
carries the host and port of the last location the client was successfully
connected to and an exception object that provides any available details on the
failure cause.
+
+```
+ final ConnectionOptions options = connectionOptions();
+
+ options.disconnectedHandler((connection, location) -> {
+ LOG.info("Client signaled that it was not able to reconnect and will
not attempt further retries.");
+ });
+```
diff --git a/protonj2-client/README.md b/protonj2-client/README.md
index 9f610e82..e8e6c590 100644
--- a/protonj2-client/README.md
+++ b/protonj2-client/README.md
@@ -39,7 +39,7 @@ Execute the tests and produce code coverage report:
## Creating a connection
-The entry point for creating new connections with the proton-dotnet client is
the Client
+The entry point for creating new connections with the protonj2 client is the
Client
type which provides a simple static factory method to create new instances.
Client container = Client.create();
@@ -57,7 +57,7 @@ The basic create API looks as follows:
Connection connection = container.connect(remoteAddress, remotePort, new
ConnectionOptions());
-From you connection instance you can then proceed to create sessions, senders
and receivers that
+From your connection instance you can then proceed to create sessions, senders
and receivers that
you can use in your application.
### Sending a message
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]