io_nats_client_impl - RichardHightower/jnats GitHub Wiki

io.nats.client.impl

MockNatsConnection

MockNatsConnection

The MockNatsConnection class is a subclass of NatsConnection and is packaged in the current module with package scope. It is designed to have the necessary access for its functionality.

MessageQueueBenchmark

MessageQueueBenchmark

The MessageQueueBenchmark class is a public class used for benchmarking message queues. It is primarily used by software engineers to measure the performance of different message queue implementations. The class is responsible for setting up and executing benchmark tests on message queues, allowing developers to compare the speed and efficiency of various queueing systems.

public static void main(String[] args) throws InterruptedException

public static void main(String[] args) throws InterruptedException {
    int msgCount = 10_000_000;
    NatsMessage[] msgs = new NatsMessage[msgCount];
    long start, end;
    System.out.printf("Running benchmarks with %s messages.\n", NumberFormat.getInstance().format(msgCount));
    System.out.println("Warmed up ...");
    byte[] warmBytes = "a".getBytes();
    MessageQueue warm = new MessageQueue(false);
    for (int j = 0; j < msgCount; j++) {
        msgs[j] = new ProtocolMessage(warmBytes);
        warm.push(msgs[j]);
    }
    System.out.println("Starting tests ...");
    MessageQueue push = new MessageQueue(false);
    start = System.nanoTime();
    for (int i = 0; i < msgCount; i++) {
        push.push(msgs[i]);
    }
    end = System.nanoTime();
    System.out.printf("\nTotal time to perform %s push operations was %s ms, %s ns/op\n", NumberFormat.getInstance().format(msgCount), NumberFormat.getInstance().format((end - start) / 1_000_000L), NumberFormat.getInstance().format(((double) (end - start)) / ((double) (msgCount))));
    System.out.printf("\tor %s op/s\n", NumberFormat.getInstance().format(1_000_000_000L * ((double) (msgCount)) / ((double) (end - start))));
    start = System.nanoTime();
    for (int i = 0; i < msgCount; i++) {
        push.popNow();
    }
    end = System.nanoTime();
    System.out.printf("\nTotal time to perform %s popnow operations was %s ms, %s ns/op\n", NumberFormat.getInstance().format(msgCount), NumberFormat.getInstance().format((end - start) / 1_000_000L), NumberFormat.getInstance().format(((double) (end - start)) / ((double) (msgCount))));
    System.out.printf("\tor %s op/s\n", NumberFormat.getInstance().format(1_000_000_000L * ((double) (msgCount)) / ((double) (end - start))));
    MessageQueue accumulateQueue = new MessageQueue(true);
    for (int j = 0; j < msgCount; j++) {
        msgs[j].next = null;
    }
    for (int i = 0; i < msgCount; i++) {
        accumulateQueue.push(msgs[i]);
    }
    start = System.nanoTime();
    while (accumulateQueue.length() > 0) {
        // works for single thread, but not multi
        accumulateQueue.accumulate(10_000, 100, Duration.ofMillis(500));
    }
    end = System.nanoTime();
    System.out.printf("\nTotal time to perform accumulate %s messages was %s ms, %s ns/op\n", NumberFormat.getInstance().format(msgCount), NumberFormat.getInstance().format((end - start) / 1_000_000L), NumberFormat.getInstance().format(((double) (end - start)) / ((double) (msgCount))));
    System.out.printf("\tor %s op/s\n", NumberFormat.getInstance().format(1_000_000_000L * ((double) (msgCount)) / ((double) (end - start))));
    for (int j = 0; j < msgCount; j++) {
        msgs[j].next = null;
    }
    final MessageQueue pushPopThreadQueue = new MessageQueue(false);
    final Duration timeout = Duration.ofMillis(10);
    final CompletableFuture<Void> go = new CompletableFuture<>();
    Thread pusher = new Thread(() -> {
        try {
            go.get();
            for (int i = 0; i < msgCount; i++) {
                pushPopThreadQueue.push(msgs[i]);
            }
        } catch (Exception exp) {
            exp.printStackTrace();
        }
    });
    pusher.start();
    Thread popper = new Thread(() -> {
        try {
            go.get();
            for (int i = 0; i < msgCount; i++) {
                pushPopThreadQueue.pop(timeout);
            }
        } catch (Exception exp) {
            exp.printStackTrace();
        }
    });
    popper.start();
    start = System.nanoTime();
    go.complete(null);
    pusher.join();
    popper.join();
    end = System.nanoTime();
    System.out.printf("\nTotal time to perform %s pushes in one thread and pop with timeout in another was %s ms, %s ns/op\n", NumberFormat.getInstance().format(msgCount), NumberFormat.getInstance().format((end - start) / 1_000_000L), NumberFormat.getInstance().format(((double) (end - start)) / ((double) (msgCount))));
    System.out.printf("\tor %s op/s\n", NumberFormat.getInstance().format(1_000_000_000L * ((double) (msgCount)) / ((double) (end - start))));
    final CompletableFuture<Void> go2 = new CompletableFuture<>();
    for (int j = 0; j < msgCount; j++) {
        msgs[j].next = null;
    }
    final MessageQueue pushPopNowThreadQueue = new MessageQueue(false);
    pusher = new Thread(() -> {
        try {
            go2.get();
            for (int i = 0; i < msgCount; i++) {
                pushPopNowThreadQueue.push(msgs[i]);
            }
        } catch (Exception exp) {
            exp.printStackTrace();
        }
    });
    pusher.start();
    popper = new Thread(() -> {
        try {
            go2.get();
            for (int i = 0; i < msgCount; i++) {
                pushPopNowThreadQueue.popNow();
            }
        } catch (Exception exp) {
            exp.printStackTrace();
        }
    });
    popper.start();
    start = System.nanoTime();
    go2.complete(null);
    pusher.join();
    popper.join();
    end = System.nanoTime();
    System.out.printf("\nTotal time to perform %s pushes in one thread and pop nows in another was %s ms, %s ns/op\n", NumberFormat.getInstance().format(msgCount), NumberFormat.getInstance().format((end - start) / 1_000_000L), NumberFormat.getInstance().format(((double) (end - start)) / ((double) (msgCount))));
    System.out.printf("\tor %s op/s\n", NumberFormat.getInstance().format(1_000_000_000L * ((double) (msgCount)) / ((double) (end - start))));
    final CompletableFuture<Void> go3 = new CompletableFuture<>();
    for (int j = 0; j < msgCount; j++) {
        msgs[j].next = null;
    }
    final MessageQueue pushAccumulateThreadQueue = new MessageQueue(true);
    pusher = new Thread(() -> {
        try {
            go3.get();
            for (int i = 0; i < msgCount; i++) {
                pushAccumulateThreadQueue.push(msgs[i]);
            }
        } catch (Exception exp) {
            exp.printStackTrace();
        }
    });
    pusher.start();
    popper = new Thread(() -> {
        try {
            go3.get();
            int remaining = msgCount;
            while (remaining > 0) {
                NatsMessage cursor = pushAccumulateThreadQueue.accumulate(10_000, 100, Duration.ofMillis(500));
                while (cursor != null) {
                    remaining--;
                    cursor = cursor.next;
                }
            }
        } catch (Exception exp) {
            exp.printStackTrace();
        }
    });
    popper.start();
    start = System.nanoTime();
    go3.complete(null);
    pusher.join();
    popper.join();
    end = System.nanoTime();
    System.out.printf("\nTotal time to perform %s pushes in one thread and accumlates in another was %s ms, %s ns/op\n", NumberFormat.getInstance().format(msgCount), NumberFormat.getInstance().format((end - start) / 1_000_000L), NumberFormat.getInstance().format(((double) (end - start)) / ((double) (msgCount))));
    System.out.printf("\tor %s op/s\n", NumberFormat.getInstance().format(1_000_000_000L * ((double) (msgCount)) / ((double) (end - start))));
}

The main method in the io.nats.client.impl.MessageQueueBenchmark class performs a benchmark test on the MessageQueue class.

  1. It initializes a variable msgCount with a value of 10 million and creates an array msgs of NatsMessage objects with the same size.
  2. It prints the number of messages being used in the benchmark test.
  3. It prints "Warmed up ..." to indicate that the warm-up phase is starting.
  4. It creates a MessageQueue called warm and initializes it with 10 million ProtocolMessage objects containing a byte array with the letter 'a'.
  5. It prints "Starting tests ..." to indicate that the actual benchmark tests are starting.
  6. It creates a new MessageQueue called push.
  7. It measures the time it takes to push all the msgs into the push queue and prints the total time and operations per second for the push operations.
  8. It measures the time it takes to pop all the messages from the push queue and prints the total time and operations per second for the pop operations.
  9. It creates a new MessageQueue called accumulateQueue.
  10. It pushes all the msgs into the accumulateQueue queue.
  11. It measures the time it takes to accumulate all the messages in the accumulateQueue queue and prints the total time and operations per second for the accumulate operations.
  12. It creates a new MessageQueue called pushPopThreadQueue.
  13. It initializes a CompletableFuture called go.
  14. It creates a thread called pusher that pushes all the msgs into the pushPopThreadQueue queue.
  15. It creates a thread called popper that pops messages from the pushPopThreadQueue queue with a timeout of 10 milliseconds.
  16. It measures the time it takes to perform the push and pop operations in separate threads and prints the total time and operations per second for these operations.
  17. It initializes a new CompletableFuture called go2.
  18. It creates a new MessageQueue called pushPopNowThreadQueue.
  19. It creates a new thread called pusher that pushes all the msgs into the pushPopNowThreadQueue queue.
  20. It creates a new thread called popper that pops messages from the pushPopNowThreadQueue queue without any timeout.
  21. It measures the time it takes to perform the push and popnow operations in separate threads and prints the total time and operations per second for these operations.
  22. It initializes a new CompletableFuture called go3.
  23. It creates a new MessageQueue called pushAccumulateThreadQueue.
  24. It creates a new thread called pusher that pushes all the msgs into the pushAccumulateThreadQueue queue.
  25. It creates a new thread called popper that accumulates messages from the pushAccumulateThreadQueue queue until all messages have been accumulated.
  26. It measures the time it takes to perform the push and accumulate operations in separate threads and prints the total time and operations per second for these operations.

The main method in the class io.nats.client.impl.MessageQueueBenchmark is used to benchmark the performance of various operations on a message queue.

The method starts by initializing an array of NatsMessage objects with a specified count of messages. It then runs a series of tests, measuring the time it takes to perform different operations on the message queue.

The method first warms up the message queue by pushing messages onto it. It then measures the time taken to perform a series of push operations on the queue. Next, it measures the time taken to perform a series of pop-now operations, which tries to pop a message from the queue with a timeout.

After that, the method initializes a new message queue and pushes messages onto it. It then measures the time taken to perform a series of accumulate operations, which continuously accumulates messages from the queue until it becomes empty.

The method then runs two threads concurrently - one pushing messages onto the queue and the other popping messages with a timeout. It measures the time taken for this scenario.

Finally, the method runs two threads - one pushing messages onto the queue and the other popping messages immediately. It measures the time taken for this scenario as well.

For each test scenario, the method outputs the total time taken for the operations, the time taken per operation, and the operations per second.

Overall, this method provides a benchmark for measuring the performance of various operations on a message queue implementation.

sequence diagram

MessageProtocolCreationBenchmark

MessageProtocolCreationBenchmark

The MessageProtocolCreationBenchmark class is a public class used for benchmarking the creation of message protocols.

public static void main(String[] args) throws InterruptedException

public static void main(String[] args) throws InterruptedException {
    int warmup = 1_000_000;
    int msgCount = 50_000_000;
    System.out.printf("### Running benchmarks with %s messages.\n", NumberFormat.getInstance().format(msgCount));
    for (int j = 0; j < warmup; j++) {
        new NatsMessage("subject", "replyTo", EMPTY_BODY);
    }
    long start = System.nanoTime();
    for (int j = 0; j < msgCount; j++) {
        new NatsMessage("subject", "replyTo", EMPTY_BODY);
    }
    long end = System.nanoTime();
    System.out.printf("\n### Total time to create %s non-utf8 messages for sending was %s ms\n\t%f ns/op\n\t%s op/sec\n", NumberFormat.getInstance().format(msgCount), NumberFormat.getInstance().format((end - start) / 1_000_000L), ((double) (end - start)) / ((double) (msgCount)), NumberFormat.getInstance().format(((double) (1_000_000_000L * msgCount)) / ((double) (end - start))));
    start = System.nanoTime();
    for (int j = 0; j < msgCount; j++) {
        new NatsMessage("subject", "replyTo", EMPTY_BODY);
    }
    end = System.nanoTime();
    System.out.printf("\n### Total time to create %s utf8 messages for sending was %s ms\n\t%f ns/op\n\t%s op/sec\n", NumberFormat.getInstance().format(msgCount), NumberFormat.getInstance().format((end - start) / 1_000_000L), ((double) (end - start)) / ((double) (msgCount)), NumberFormat.getInstance().format(((double) (1_000_000_000L * msgCount)) / ((double) (end - start))));
    start = System.nanoTime();
    for (int j = 0; j < msgCount; j++) {
        new ProtocolMessage(EMPTY_BODY);
    }
    end = System.nanoTime();
    System.out.printf("\n### Total time to create %s a protocol message was %s ms\n\t%f ns/op\n\t%s op/sec\n", NumberFormat.getInstance().format(msgCount), NumberFormat.getInstance().format((end - start) / 1_000_000L), ((double) (end - start)) / ((double) (msgCount)), NumberFormat.getInstance().format(((double) (1_000_000_000L * msgCount)) / ((double) (end - start))));
}

The main method in the class io.nats.client.impl.MessageProtocolCreationBenchmark performs the following steps:

  1. Define the variables warmup and msgCount. warmup is set to 1,000,000 and msgCount is set to 50,000,000 (representing the number of messages to be processed).
  2. Print a message indicating the number of messages that will be used for the benchmark.
  3. Perform a warm-up phase by creating a new NatsMessage object 1,000,000 times using the values "subject", "replyTo", and EMPTY_BODY. This is done to warm-up the JVM and optimize any code before running the actual benchmark.
  4. Record the start time using System.nanoTime().
  5. Create a new NatsMessage object msgCount times using the same values as in the warm-up phase. This measures the time taken to create msgCount non-utf8 messages for sending.
  6. Record the end time using System.nanoTime().
  7. Print the total time taken to create msgCount non-utf8 messages in milliseconds, the average time taken per operation in nanoseconds, and the number of operations per second.
  8. Repeat steps 5-7 with utf8 messages instead of non-utf8 messages.
  9. Repeat steps 5-7 with a ProtocolMessage object instead of NatsMessage to measure the time taken to create a protocol message.
  10. The benchmark is completed.

The main method in the MessageProtocolCreationBenchmark class is a benchmarking method. It measures the time taken to create different types of messages for sending.

Here's an overview of what the method does:

  1. Sets the value for warmup and msgCount variables, which specify the number of warmup iterations and the total number of messages, respectively.
  2. Prints the number of messages being benchmarked.
  3. Performs warmup iterations, where each iteration creates a new NatsMessage object with specific parameters.
  4. Starts timing.
  5. Performs msgCount iterations, where each iteration creates a new NatsMessage object with specific parameters.
  6. Stops timing and prints the total time taken to create the non-utf8 messages, the average time per operation, and the number of operations per second.
  7. Resets timing and repeats steps 4-6 for creating utf8 messages.
  8. Resets timing and repeats steps 4-6 for creating a protocol message.

Overall, this method helps measure the performance of creating different types of messages used in communication. The time taken for message creation, the average time per operation, and the number of operations per second are printed as benchmark results for each type of message.

sequence diagram

NatsPackageScopeWorkarounds

NatsPackageScopeWorkarounds

The NatsPackageScopeWorkarounds class is a public class that provides workarounds for package scope limitations in the Nats package.

ParseTesting

ParseTesting

ParseTesting is a public class that is used for testing parsing functionality. It contains various methods and properties to facilitate parsing operations.

public static void main(String[] args) throws InterruptedException

public static void main(String[] args) throws InterruptedException {
    int iterations = 1_000_000;
    System.out.println("###");
    System.out.printf("### Running parse tests with %s msgs.\n", NumberFormat.getInstance().format(iterations));
    System.out.println("###");
    runTest(iterations, "+OK");
    runTest(iterations, "PONG");
    runTest(iterations, "INFO " + infoJson);
    runTest(iterations, "MSG longer.subject.abitlikeaninbox 22 longer.replyto.abitlikeaninbox 234");
    runTest(iterations, "-ERR some error with spaces in it");
}

The main method in the ParseTesting class performs the following steps:

  1. Declare and initialize a variable iterations with the value 1_000_000.
  2. Print the header information using System.out.println.
  3. Use NumberFormat.getInstance().format(iterations) to format the value of iterations.
  4. Print a message indicating the number of iterations being tested.
  5. Call the runTest method with the specified iterations and the string "+OK".
  6. Call the runTest method with the specified iterations and the string "PONG".
  7. Call the runTest method with the specified iterations and the string "INFO " + infoJson, where infoJson is a variable that holds a JSON object.
  8. Call the runTest method with the specified iterations and the string "MSG longer.subject.abitlikeaninbox 22 longer.replyto.abitlikeaninbox 234".
  9. Call the runTest method with the specified iterations and the string "-ERR some error with spaces in it".

The runTest method is not defined in the given code snippet, so its functionality cannot be determined.

The main method in the ParseTesting class is used to run parse tests.

It first sets the number of iterations to 1 million and then prints out a header indicating the start of the parse tests, along with the number of messages involved in the test.

The runTest method is then called multiple times, passing different test strings as arguments. These test strings include various patterns such as "+OK", "PONG", "INFO" followed by infoJson, a longer subject and replyto message, and a message with an error.

Overall, this method executes parse tests with different message strings to test the parsing functionality of the software.

sequence diagram

public static String[] splitCharBuffer(CharBuffer buffer)

public static String[] splitCharBuffer(CharBuffer buffer) {
    ArrayList<String> list = new ArrayList<>();
    StringBuilder builder = new StringBuilder();
    while (buffer.hasRemaining()) {
        char c = buffer.get();
        if (c == ' ') {
            list.add(builder.toString());
            builder = new StringBuilder();
        } else {
            builder.append(c);
        }
    }
    if (builder.length() > 0) {
        list.add(builder.toString());
    }
    return list.toArray(new String[0]);
}

The splitCharBuffer method in the ParseTesting class, defined in the io.nats.client.impl package, is used to split a CharBuffer into an array of strings. Here is a step-by-step description of what the method does:

  1. Create an empty ArrayList to store the individual strings extracted from the CharBuffer.
  2. Create an empty StringBuilder as a temporary container for each individual string.
  3. Start a loop that continues as long as there are remaining characters in the CharBuffer.
  4. Retrieve the next character from the CharBuffer using the get() method, and assign it to the variable c.
  5. Check if the character c is a space (' ').
  6. If the character c is a space, convert the contents of the StringBuilder to a string using the toString() method, and add it to the ArrayList.
  7. Create a new empty StringBuilder to start collecting a new string.
  8. If the character c is not a space, append the character to the StringBuilder using the append() method.
  9. Repeat steps 4 to 8 until all characters in the CharBuffer have been processed.
  10. Check if the StringBuilder still contains characters that have not been added to the ArrayList.
  11. If so, convert the remaining characters in the StringBuilder to a string using the toString() method, and add it to the ArrayList.
  12. Convert the ArrayList to an array of strings using the toArray(T[] a) method with an empty array as the argument.
  13. Return the resulting array of strings.

This method essentially splits the characters in the CharBuffer by spaces, creating an array of strings where each element represents a word or a group of non-space characters.

The method splitCharBuffer in class ParseTesting is used to split a CharBuffer into an array of strings based on spaces (' ') as the delimiter.

Here is a brief description of what the method does:

  1. Creates an empty ArrayList to hold the individual strings.
  2. Initializes a StringBuilder to build each string.
  3. Iterates through each character in the CharBuffer using a while loop.
  4. Checks if the current character is a space (' ').
    • If it is a space, the contents of the StringBuilder are added to the ArrayList as a string, and the StringBuilder is cleared to build a new string.
    • If it is not a space, the character is appended to the StringBuilder.
  5. After the loop, the contents of the StringBuilder are added to the ArrayList if it is not empty.
  6. Converts the ArrayList into an array of strings using the toArray method.
  7. Returns the array of strings.

sequence diagram

public static String grabNextWithBuilder(CharBuffer buffer)

public static String grabNextWithBuilder(CharBuffer buffer) {
    StringBuilder builder = new StringBuilder();
    while (buffer.hasRemaining()) {
        char c = buffer.get();
        if (c == ' ') {
            return builder.toString();
        } else {
            builder.append(c);
        }
    }
    return builder.toString();
}

The method grabNextWithBuilder defined in the class io.nats.client.impl.ParseTesting takes a CharBuffer named buffer as input and returns a String.

Here is a step-by-step description of what the method does:

  1. Create a new StringBuilder called builder to store characters from the buffer.

  2. Enter a loop that continues as long as the buffer has remaining characters.

  3. Get the next character from the buffer and assign it to the variable c.

  4. Check if the character c is equal to a space (' '). If it is, then it means the method has encountered a space and it should stop reading characters from the buffer. In this case, the method returns the current contents of the builder as a String.

  5. If the character c is not equal to a space, then it means the method has not encountered a space yet and it should continue reading characters from the buffer. In this case, the character c is appended to the builder.

  6. After appending the character c to the builder, the loop continues to the next iteration to get the next character from the buffer.

  7. If the loop completes without encountering a space character, it means that the entire buffer has been consumed. In this case, the method returns the current contents of the builder as a String.

This method essentially reads characters from the buffer until it encounters a space character or reaches the end of the buffer. It then returns the read characters as a String.

The grabNextWithBuilder method defined in the ParseTesting class of the io.nats.client.impl package is a static method that takes a CharBuffer as its parameter.

The purpose of this method is to extract the next string from the given CharBuffer. It does this by iterating over each character in the buffer. If a space character (' ') is encountered, the method returns the accumulated characters in the StringBuilder as a string. If any other character is encountered, it is appended to the StringBuilder.

After processing all characters in the buffer, the method returns the accumulated characters in the StringBuilder as a string, regardless of whether a space was encountered or not.

sequence diagram

public static String grabNextWithSubsequence(CharBuffer buffer)

public static String grabNextWithSubsequence(CharBuffer buffer) {
    if (!buffer.hasRemaining()) {
        return null;
    }
    int start = buffer.position();
    while (buffer.hasRemaining()) {
        char c = buffer.get();
        if (c == ' ') {
            int end = buffer.position();
            buffer.position(start);
            //don't grab the space
            CharBuffer slice = buffer.subSequence(0, end - start - 1);
            buffer.position(end);
            return slice.toString();
        }
    }
    buffer.position(start);
    String retVal = buffer.toString();
    buffer.position(buffer.limit());
    return retVal;
}

The grabNextWithSubsequence method in the ParseTesting class is used to extract a substring from a given CharBuffer until the next occurrence of a space character (' '). Here is a step-by-step description of what the method does:

  1. Check if the input buffer has any remaining characters. If it doesn't, return null.

  2. Get the current position of the buffer and assign it to the start variable. This is the starting position from where the substring will be extracted.

  3. Enter a while loop that runs until there are no more characters remaining in the buffer.

  4. Get the next character in the buffer and assign it to the variable c.

  5. Check if the character c is equal to a space (' '). If it is, it means we have found the next occurrence of a space character.

  6. Get the current position of the buffer and assign it to the end variable. This is the ending position of the substring.

  7. Set the position of the buffer back to the start position so that we can create a subsequence from the original buffer starting at the start position.

  8. Create a new CharBuffer named slice using the subSequence method, which extracts a subsequence of characters from the buffer starting at the start position and ending at end - start - 1 position. This is done to exclude the space character.

  9. Set the position of the buffer to the end position so that we can continue iterating through the remaining characters.

  10. Convert the slice CharBuffer to a String using the toString method and assign it to the retVal variable.

  11. Set the position of the buffer to its limit, which effectively marks the end of the buffer.

  12. Return the retVal string, which is the extracted substring.

By following these steps, the grabNextWithSubsequence method efficiently extracts the next substring from the CharBuffer until the next space character is encountered.

The grabNextWithSubsequence method in the io.nats.client.impl.ParseTesting class is used to extract the next subsequence of characters from a given CharBuffer object.

Here's how it works:

  • It starts by checking if the given CharBuffer has any remaining characters. If not, it returns null.
  • If there are remaining characters, it captures the starting position of the buffer.
  • It then iterates over the buffer, character by character, until it encounters a space character.
  • Once a space is found, it captures the ending position of the subsequence and creates a new CharBuffer slice from the original buffer, excluding the space character.
  • Finally, it resets the position of the original buffer to the starting position, returns the string representation of the subsequence, and sets the position of the buffer to its limit.

In summary, this method extracts the next subsequence of characters from a CharBuffer until it reaches a space character, excluding the space itself.

sequence diagram

public static CharSequence grabNextAsSubsequence(CharBuffer buffer)

public static CharSequence grabNextAsSubsequence(CharBuffer buffer) {
    if (!buffer.hasRemaining()) {
        return null;
    }
    int start = buffer.position();
    while (buffer.hasRemaining()) {
        char c = buffer.get();
        if (c == ' ') {
            int end = buffer.position();
            buffer.position(start);
            //don't grab the space
            CharBuffer slice = buffer.subSequence(0, end - start - 1);
            buffer.position(end);
            return slice;
        }
    }
    buffer.position(start);
    CharSequence retVal = buffer.subSequence(0, buffer.remaining());
    buffer.position(buffer.limit());
    return retVal;
}

Method: grabNextAsSubsequence

Description: This method is used to grab the next character sequence from the input CharBuffer as a subsequence. It stops when it encounters a space character (' ') and returns the subsequence excluding the space. If the buffer is empty or contains only spaces, it returns null.

Parameters:

  • buffer: The input CharBuffer from which to grab the next subsequence.

Returns:

  • CharSequence : The grabbed subsequence excluding space, or null if the buffer is empty or contains only spaces.

Steps: The method performs the following steps:

  1. Check if the buffer is empty (buffer.hasRemaining()). If not, proceed to the next step. Otherwise, return null.

  2. Get the current position of the buffer (start = buffer.position()).

  3. Enter a while loop, iterating until the buffer has no more remaining characters.

  4. Get the next character from the buffer (c = buffer.get()).

  5. Check if the character is a space (' '). If true, go to the next step. Otherwise, continue with the next iteration of the loop.

  6. Get the current position of the buffer (end = buffer.position()).

  7. Set the position of the buffer back to the start position (buffer.position(start)).

  8. Create a new CharBuffer slice from the original buffer, starting from index 0 and ending at end - start - 1 (excluding the space). Assign it to slice variable.

  9. Set the position of the buffer to the end position (buffer.position(end)).

  10. Return the grabbed subsequence (slice).

  11. If the loop ends without encountering a space character, set the buffer position back to the start position (buffer.position(start)).

  12. Create a new CharBuffer subsequence from the original buffer, starting from index 0 and ending at the remaining characters in the buffer. Assign it to retVal variable.

  13. Set the buffer position to the buffer limit (buffer.position(buffer.limit())).

  14. Return the grabbed subsequence (retVal).

The grabNextAsSubsequence method in the ParseTesting class takes a CharBuffer as input and returns the next sequence of characters in the buffer until it encounters a space character (' ').

It starts by checking if the buffer has remaining characters. If it doesn't, it returns null. Otherwise, it records the starting position of the buffer.

The method then iterates over the remaining characters in the buffer. For each character, it checks if it is a space character. If it is, it records the ending position of the buffer and creates a subsequence (excluding the space character) from the starting position to the ending position. The buffer's position is then updated to the ending position, and the subsequence is returned.

If no space character is found, the method resets the buffer's position to the starting position, and creates a subsequence from the starting position to the end of the buffer. The buffer's position is then set to its limit, and the subsequence is returned.

In summary, this method retrieves the next sequence of characters from a CharBuffer until it encounters a space character, excluding the space character from the resulting subsequence.

sequence diagram

public static String grabNextWithCharArray(CharBuffer buffer)

public static String grabNextWithCharArray(CharBuffer buffer) {
    int remaining = buffer.remaining();
    if (remaining == 0) {
        return null;
    }
    int i = 0;
    while (remaining > 0) {
        char c = buffer.get();
        if (c == ' ') {
            return new String(buff, 0, i);
        } else {
            buff[i] = c;
            i++;
        }
        remaining--;
    }
    return new String(buff, 0, i);
}

Method Description

The grabNextWithCharArray method in the ParseTesting class is a public static method that takes a CharBuffer as its parameter. The purpose of this method is to extract the next string from the CharBuffer.

Parameters

  • buffer: A CharBuffer object that contains the characters to be processed.

Return Value

  • If the buffer is empty (i.e., remaining == 0), the method returns null.
  • If the next character in the buffer is a space character, the method returns a new String object containing the characters from buff[0] to buff[i-1] (where buff is an array of characters).
  • Otherwise, the method continues to read characters from the buffer until a space character is encountered or the end of the buffer is reached. It then returns a new String object containing the characters from buff[0] to buff[i-1].

Method Body Explanation

  1. The method starts by retrieving the remaining number of characters in the buffer using the remaining() method of the CharBuffer.
  2. If the remaining count is equal to 0, the method returns null, indicating that there are no more characters to process.
  3. If there are remaining characters, the method initializes a counter variable i as 0 to keep track of the current position in the buff array (initialized elsewhere in the code and not shown).
  4. The method enters a loop that will continue until there are no remaining characters in the buffer.
  5. In each iteration of the loop, the method retrieves the next character from the buffer using the get() method of the CharBuffer and assigns it to the variable c.
  6. If the character c is a space character, the method creates a new String object using the characters stored in the buff array from index 0 to i-1 (these characters represent the parsed string), and returns it.
  7. If the character c is not a space, it is stored in the buff array at the current position i, and the i counter is incremented by 1.
  8. The method then decrements the remaining count by 1.
  9. The loop continues until the remaining count reaches 0, indicating that all characters from the buffer have been processed.
  10. If no space character was encountered in the buffer, the method creates a new String object using the characters stored in the buff array from index 0 to i-1 and returns it.

Note: The code provided does not show the declaration and initialization of the buff array, which is assumed to be present in the class and accessible to this method.

The method grabNextWithCharArray is a public static method defined in the class io.nats.client.impl.ParseTesting. It takes a CharBuffer object as a parameter.

The purpose of this method is to extract the next word delimited by a space character (' ') from the CharBuffer and return it as a String. If there are no more characters left in the buffer, it returns null.

Internally, the method iterates over the characters in the CharBuffer using a while loop. It reads each character one by one and checks if it is a space character. If it is, it creates a new String object from the characters read so far using the new String(buff, 0, i) constructor and returns it.

If the character is not a space, it adds the character to an internal character array buff at the current position i. After each character is read, remaining is decremented and i is incremented. This process continues until either a space character is encountered or there are no more characters left in the buffer.

Finally, if the end of the buffer is reached without encountering a space character, it creates a new String object from the characters read so far using new String(buff, 0, i) and returns it.

Note: The buff array is not declared in the given code snippet, so the method assumes that it is already declared and accessible within the class scope.

sequence diagram

public static String protocolFor(char[] chars, int length)

public static String protocolFor(char[] chars, int length) {
    if (length == 3) {
        if (chars[0] == '+' && chars[1] == 'O' && chars[2] == 'K') {
            return OP_OK;
        } else if (chars[0] == 'M' && chars[1] == 'S' && chars[2] == 'G') {
            return OP_MSG;
        } else {
            return null;
        }
    } else if (length == 4) {
        // order for uniqueness
        if (chars[1] == 'I' && chars[0] == 'P' && chars[2] == 'N' && chars[3] == 'G') {
            return OP_PING;
        } else if (chars[0] == 'P' && chars[1] == 'O' && chars[2] == 'N' && chars[3] == 'G') {
            return OP_PONG;
        } else if (chars[0] == '-' && chars[1] == 'E' && chars[2] == 'R' && chars[3] == 'R') {
            return OP_ERR;
        } else if (chars[2] == 'F' && chars[0] == 'I' && chars[1] == 'N' && chars[3] == 'O') {
            return OP_INFO;
        } else {
            return null;
        }
    } else {
        return null;
    }
}

The protocolFor method in the io.nats.client.impl.ParseTesting class is responsible for determining the protocol type based on the input chars and length.

Step 1:

The method accepts two parameters - chars which is a character array and length which denotes the size of the array.

Step 2:

If the length of the array is 3, it checks if the first character is '+', the second character is 'O', and the third character is 'K'. If it matches, it returns the value OP_OK. Else, it checks if the first character is 'M', the second character is 'S', and the third character is 'G'. If it matches, it returns the value OP_MSG. If none of the conditions are met, it returns null.

Step 3:

If the length of the array is 4, it checks for the following conditions:

  • If the second character is 'I', the first character is 'P', the third character is 'N' and the fourth character is 'G', it returns the value OP_PING.
  • If the first character is 'P', the second character is 'O', the third character is 'N' and the fourth character is 'G', it returns the value OP_PONG.
  • If the first character is '-', the second character is 'E', the third character is 'R', and the fourth character is 'R', it returns the value OP_ERR.
  • If the third character is 'F', the first character is 'I', the second character is 'N', and the fourth character is 'O', it returns the value OP_INFO. If none of the conditions are met, it returns null.

Step 4:

If the length of the array is neither 3 nor 4, it returns null.

Overall, this method determines the protocol type based on the characters in the chars array and returns the corresponding protocol value.

The method protocolFor in the class ParseTesting is used to determine the protocol for the given array of characters (chars) and its length (length). It checks the length of the array and performs different checks based on the length to determine the protocol.

If the length is 3, it checks if the characters at indexes 0, 1, and 2 are equal to '+', 'O', and 'K' respectively. If they are, it returns the constant OP_OK. Otherwise, it checks if the characters represent the 'MSG' protocol and returns OP_MSG. If none of these conditions are met, it returns null.

If the length is 4, it performs similar checks for different protocols. For example, it checks if the characters at indexes 0, 1, 2, and 3 are equal to 'P', 'I', 'N', and 'G' respectively to determine the 'PING' protocol. It checks for other protocols like 'PONG', 'ERR', and 'INFO' in a similar manner. If none of these conditions are met, it returns null.

If the length is neither 3 nor 4, it returns null.

In summary, the method protocolFor is used to determine the protocol based on the given array of characters and its length. It returns the corresponding protocol constant or null if the protocol cannot be determined.

sequence diagram

public static String grabProtocol(CharBuffer buffer)

public static String grabProtocol(CharBuffer buffer) {
    int remaining = buffer.remaining();
    if (remaining == 0) {
        return null;
    }
    int i = 0;
    while (remaining > 0) {
        char c = buffer.get();
        if (c == ' ') {
            return protocolFor(buff, i);
        } else {
            buff[i] = c;
            i++;
        }
        remaining--;
    }
    return protocolFor(buff, i);
}

The grabProtocol method takes a CharBuffer as a parameter and returns a string representing the protocol.

  1. Check the number of remaining characters in the buffer using buffer.remaining(). If there are no remaining characters (i.e., remaining == 0), return null.

  2. Initialize a variable i with a value of 0. This variable will be used as an index to store characters in a temporary buffer.

  3. Enter a loop while there are remaining characters in the buffer (i.e., remaining > 0).

  4. Get the next character from the buffer using buffer.get() and assign it to a variable c.

  5. Check if the character c is a space character.

    a. If it is a space character, call a method protocolFor with the temporary buffer buff (not shown in the provided code) and the index i as arguments, and return the result of this method call as the protocol string.

    b. If it is not a space character, store the character c in the temporary buffer buff at index i.

  6. Decrement the remaining variable by 1, indicating that one character has been processed.

  7. After exiting the loop, call the protocolFor method again with the temporary buffer buff and the index i as arguments, and return the result as the protocol string.

Here is a modified version of the provided code with the missing buff variable declaration and the protocolFor method called with the correct arguments:

public static String grabProtocol(CharBuffer buffer) {
    int remaining = buffer.remaining();
    if (remaining == 0) {
        return null;
    }

    char[] buff = new char[remaining]; // Temporary buffer to store characters
    int i = 0;

    while (remaining > 0) {
        char c = buffer.get();

        if (c == ' ') {
            return protocolFor(buff, i);
        } else {
            buff[i] = c;
            i++;
        }
        remaining--;
    }

    return protocolFor(buff, i);
}

The grabProtocol method in the ParseTesting class, defined in the io.nats.client.impl package, takes a CharBuffer as input.

It first checks the number of remaining characters in the buffer. If it is zero, it returns null.

Otherwise, it initializes a counter variable i and enters a loop while there are remaining characters in the buffer. Inside the loop, it retrieves each character from the buffer and checks if it is a space (' '). If it is, it invokes the protocolFor method with the buff array and the current value of i as arguments, and returns the result.

If the current character is not a space, it stores it in the buff array at index i and increments i by 1.

Regardless of whether a space character is found or not, the method decrements the remaining variable by 1 after processing each character.

Finally, if the loop completes without encountering a space character, the method again invokes the protocolFor method with the buff array and the final value of i as arguments, and returns the result.

This method is expected to determine the protocol from the input CharBuffer by extracting the string until the first space (' ') character is encountered.

sequence diagram

public static void runTest(int iterations, String serverMessage)

public static void runTest(int iterations, String serverMessage) {
    Pattern space = Pattern.compile(" ");
    ByteBuffer protocolBuffer = ByteBuffer.allocate(32 * 1024);
    protocolBuffer.put(serverMessage.getBytes(StandardCharsets.UTF_8));
    protocolBuffer.flip();
    CharBuffer buffer = StandardCharsets.UTF_8.decode(protocolBuffer);
    String pl = buffer.toString();
    buffer.rewind();
    protocolBuffer.rewind();
    String[] newversion = splitCharBuffer(buffer);
    String[] oldversion = space.split(pl);
    buffer.rewind();
    ArrayList<String> opAware = new ArrayList<>();
    CharSequence s = null;
    while ((s = grabNext(buffer)) != null) {
        opAware.add(s.toString());
    }
    String[] opAwareArray = opAware.toArray(new String[0]);
    System.out.printf("### Parsing server string: %s\n", serverMessage);
    boolean newOk = Arrays.equals(newversion, oldversion);
    System.out.println("### Old and new versions are equal: " + newOk);
    if (!newOk) {
        System.exit(-1);
    }
    boolean protoOk = Arrays.equals(opAwareArray, oldversion);
    System.out.println("### Old and op-aware versions are equal: " + protoOk);
    if (!protoOk) {
        System.exit(-1);
    }
    long start = System.nanoTime();
    for (int i = 0; i < iterations; i++) {
        StandardCharsets.UTF_8.decode(protocolBuffer).toString();
        protocolBuffer.rewind();
    }
    long end = System.nanoTime();
    System.out.printf("### %s raw utf8 decode/sec.\n", NumberFormat.getInstance().format(1_000_000_000L * iterations / (end - start)));
    start = System.nanoTime();
    for (int i = 0; i < iterations; i++) {
        String protocolLine = StandardCharsets.UTF_8.decode(protocolBuffer).toString();
        space.split(protocolLine);
        protocolBuffer.rewind();
    }
    end = System.nanoTime();
    System.out.printf("### %s old parses/sec.\n", NumberFormat.getInstance().format(1_000_000_000L * iterations / (end - start)));
    start = System.nanoTime();
    for (int i = 0; i < iterations; i++) {
        CharBuffer charBuffer = StandardCharsets.UTF_8.decode(protocolBuffer);
        splitCharBuffer(charBuffer);
        protocolBuffer.rewind();
    }
    end = System.nanoTime();
    System.out.printf("### %s new parses/sec.\n", NumberFormat.getInstance().format(1_000_000_000L * iterations / (end - start)));
    start = System.nanoTime();
    for (int i = 0; i < iterations; i++) {
        CharBuffer charBuffer = StandardCharsets.UTF_8.decode(protocolBuffer);
        String op = grabProtocol(charBuffer);
        switch(op) {
            case OP_MSG:
                //subject
                grabNext(charBuffer);
                // sid
                grabNext(charBuffer);
                // replyto or length
                grabNext(charBuffer);
                // length or null
                grabNext(charBuffer);
                break;
            case OP_ERR:
                grabTheRest(charBuffer);
                break;
            case OP_OK:
            case OP_PING:
            case OP_PONG:
            case OP_INFO:
                grabTheRest(charBuffer);
                break;
            default:
                break;
        }
        protocolBuffer.rewind();
    }
    end = System.nanoTime();
    System.out.printf("### %s op-aware parses/sec.\n", NumberFormat.getInstance().format(1_000_000_000L * iterations / (end - start)));
    System.out.println();
}

The runTest method in the io.nats.client.impl.ParseTesting class is performing the following steps:

  1. It receives two parameters: iterations (the number of iterations to run the test) and serverMessage (the message to be parsed).
  2. It creates a Pattern object called space to match a space character.
  3. It creates a ByteBuffer called protocolBuffer with a size of 32 * 1024 bytes.
  4. It puts the bytes of the serverMessage string into the protocolBuffer using the UTF-8 encoding.
  5. It flips the protocolBuffer to prepare for reading.
  6. It decodes the bytes in the protocolBuffer using the UTF-8 charset to create a CharBuffer called buffer.
  7. It converts the buffer to a string pl.
  8. It rewinds the buffer and protocolBuffer to prepare for reading.
  9. It splits the buffer into an array of strings called newversion using a custom method splitCharBuffer.
  10. It splits the pl string into an array of strings called oldversion by matching spaces.
  11. It rewinds the buffer and creates an ArrayList called opAware.
  12. It enters a loop where it grabs the next sequence of characters from the buffer and adds it to the opAware list until there are no more sequences.
  13. It converts the opAware list to an array of strings called opAwareArray.
  14. It prints the serverMessage in a formatted string.
  15. It checks if newversion and oldversion arrays are equal and assigns the result to newOk.
  16. It prints whether the old and new versions are equal.
  17. If the old and new versions are not equal, it exits the program with a status of -1.
  18. It checks if opAwareArray and oldversion arrays are equal and assigns the result to protoOk.
  19. It prints whether the old and op-aware versions are equal.
  20. If the old and op-aware versions are not equal, it exits the program with a status of -1.
  21. It initializes a variable start with the current system time in nanoseconds.
  22. It enters a loop where it decodes the protocolBuffer using UTF-8 encoding and converts it to a string, and then rewinds the protocolBuffer, for each iteration.
  23. It calculates the elapsed time in nanoseconds by subtracting start from the current system time and assigns it to end.
  24. It prints the number of raw UTF-8 decodes per second.
  25. It initializes start with the current system time in nanoseconds.
  26. It enters a loop where it decodes the protocolBuffer, converts it to a string, splits the resulting string using spaces, and then rewinds the protocolBuffer, for each iteration.
  27. It calculates the elapsed time in nanoseconds by subtracting start from the current system time and assigns it to end.
  28. It prints the number of old parses per second.
  29. It initializes start with the current system time in nanoseconds.
  30. It enters a loop where it decodes the protocolBuffer and splitCharBuffer the resulting CharBuffer, and then rewinds the protocolBuffer, for each iteration.
  31. It calculates the elapsed time in nanoseconds by subtracting start from the current system time and assigns it to end.
  32. It prints the number of new parses per second.
  33. It initializes start with the current system time in nanoseconds.
  34. It enters a loop where it decodes the protocolBuffer, gets the next operation from the resulting CharBuffer, and performs different actions based on the operation, and then rewinds the protocolBuffer, for each iteration.
  35. It calculates the elapsed time in nanoseconds by subtracting start from the current system time and assigns it to end.
  36. It prints the number of op-aware parses per second.
  37. It prints an empty line.

The runTest method in the ParseTesting class is a performance test method used to compare different parsing approaches for a given server message.

This method takes in two parameters: iterations (representing the number of iterations to run the test) and serverMessage (representing the message received from the server).

The method starts by initializing the necessary variables, such as space (to split strings by space), protocolBuffer (a byte buffer to store the server message), and buffer (a character buffer decoded from the byte buffer using UTF-8). It also splits the buffer and pl (the decoded server message) into arrays of strings.

Next, it creates an ArrayList called opAware and iterates over the characters in the buffer, adding them to the opAware list as separate strings. It then converts the opAware list to an array.

The method then prints some debug information about the server message and compares the old and new versions of the parsed message. If they are not equal, the method exits with an error code.

It then performs several benchmarking tests on the parsing approaches. It measures the time it takes to decode the server message multiple times using the old and new parsing methods, as well as an "op-aware" parsing method. It also measures the time it takes to grab specific protocol elements based on their operation type.

Finally, it prints the results of the benchmarking tests.

The purpose of this method is to assess the performance of different parsing approaches and determine the most efficient one for decoding and processing server messages.

sequence diagram

StringListReader

StringListReader

The StringListReader class is an abstract class that extends the AbstractListReader. It provides functionality to read a list of strings.

@Override

void processItems(List items)

@Override
void processItems(List<JsonValue> items) {
    for (JsonValue v : items) {
        if (v.string != null) {
            strings.add(v.string);
        }
    }
}

Method: processItems(List<JsonValue> items)

This method is implemented in the io.nats.client.impl.StringListReader class and overrides the processItems method from its parent class.

Purpose

The purpose of this method is to process a list of JsonValue items and add any strings found in the items to the strings list.

Steps

  1. Iterate over each JsonValue in the items list
  2. For each JsonValue, check if the string field is not null
  3. If the string field is not null, add it to the strings list

The processItems method, defined in the StringListReader class in the io.nats.client.impl package, is an overridden method that takes a List of JsonValues as input and performs the following operations:

  • It iterates over each JsonValue v in the provided list.
  • It checks if the string property of v is not null.
  • If the string property is not null, it adds the value of v.string to the strings collection.

In summary, the processItems method collects non-null string values from a list of JsonValues and adds them to the strings collection.

ListRequestEngine

ListRequestEngine

The ListRequestEngine class is an extension of the ApiResponse class. It represents a request engine for listing items and provides functionalities for managing and retrieving lists of items.

byte[] internalNextJson(String fieldName, String filter)

byte[] internalNextJson(String fieldName, String filter) {
    if (hasMore()) {
        if (filter == null) {
            return noFilterJson();
        }
        return (OFFSET_JSON_START + nextOffset() + ",\"" + fieldName + "\":\"" + filter + "\"}").getBytes(StandardCharsets.US_ASCII);
    }
    return null;
}
```## NatsConsumer

**NatsConsumer**

The `NatsConsumer` class is an abstract class that implements the `Consumer` interface. This class is used for consuming messages from a NATS messaging system. It provides the necessary functionality for connecting to a NATS server, subscribing to specific topics, and processing messages received from the server. Users can extend this class to create their own custom consumer implementations for handling NATS messages.
### boolean hasReachedPendingLimits() 
```java
boolean hasReachedPendingLimits() {
    long ml = maxMessages.get();
    if (ml > 0 && getPendingMessageCount() >= ml) {
        return true;
    }
    long bl = maxBytes.get();
    return bl > 0 && getPendingByteCount() >= bl;
}

The hasReachedPendingLimits() method, defined in the NatsConsumer class within the io.nats.client.impl package, is responsible for determining whether the consumer has reached its pending limits.

Here's a step-by-step breakdown of what the method does:

  1. Retrieve the value of the maxMessages variable, which represents the maximum number of messages allowed to be pending. Store it in the ml variable.
  2. Check if ml is greater than 0 and if the number of pending messages, obtained from the getPendingMessageCount() method, is greater than or equal to ml.
  3. If the above condition is true, it means that the pending message count has reached or exceeded the maximum allowed limit. In this case, the method returns true, indicating that the pending limits have been reached.
  4. If the above condition is false, retrieve the value of the maxBytes variable, which represents the maximum number of bytes allowed to be pending. Store it in the bl variable.
  5. Check if bl is greater than 0 and if the number of pending bytes, obtained from the getPendingByteCount() method, is greater than or equal to bl.
  6. If the above condition is true, it means that the pending byte count has reached or exceeded the maximum allowed limit. In this case, the method returns true, indicating that the pending limits have been reached.
  7. If both conditions in step 3 and step 6 are false, it means that the pending limits have not been reached. In this case, the method returns false.

The hasReachedPendingLimits() method essentially checks if either the maximum number of pending messages or bytes has been reached and returns true, otherwise it returns false.

The hasReachedPendingLimits method in the NatsConsumer class is a boolean method that determines whether the consumer has reached its pending message or byte limits.

It first retrieves the maximum number of messages (ml) using the maxMessages field. If ml is greater than 0 and the number of pending messages is equal to or exceeds ml, the method returns true.

If the number of pending messages is below ml or ml is 0, the method proceeds to check the maximum number of bytes (bl) using the maxBytes field. If bl is greater than 0 and the number of pending bytes is equal to or exceeds bl, the method returns true.

If both the maximum number of messages and maximum number of bytes are below their respective limits, the method returns false.

sequence diagram

public CompletableFuture drain(Duration timeout) throws InterruptedException

public CompletableFuture<Boolean> drain(Duration timeout) throws InterruptedException {
    if (!this.isActive() || this.connection == null) {
        throw new IllegalStateException("Consumer is closed");
    }
    if (isDraining()) {
        return this.getDrainingFuture();
    }
    Instant start = Instant.now();
    final CompletableFuture<Boolean> tracker = new CompletableFuture<>();
    this.markDraining(tracker);
    this.sendUnsubForDrain();
    try {
        // Flush and wait up to the timeout
        this.connection.flush(timeout);
    } catch (TimeoutException e) {
        this.connection.processException(e);
    }
    this.markUnsubedForDrain();
    // Wait for the timeout or the pending count to go to 0, skipped if conn is
    // draining
    connection.getExecutor().submit(() -> {
        try {
            Instant now = Instant.now();
            while (timeout == null || timeout.equals(Duration.ZERO) || Duration.between(start, now).compareTo(timeout) < 0) {
                if (this.isDrained()) {
                    break;
                }
                // Sleep 1 milli
                Thread.sleep(1);
                now = Instant.now();
            }
            this.cleanUpAfterDrain();
        } catch (InterruptedException e) {
            this.connection.processException(e);
        } finally {
            tracker.complete(this.isDrained());
        }
    });
    return getDrainingFuture();
}

The drain method is defined in the class io.nats.client.impl.NatsConsumer and takes a Duration parameter called timeout. It returns a CompletableFuture<Boolean>.

Here is a step-by-step description of what the drain method does based on its body:

  1. Check if the consumer is active and if the connection is not null. If either condition is false, throw an IllegalStateException with the message "Consumer is closed".
  2. Check if the consumer is already draining. If it is, return the result of getDrainingFuture(), which is a CompletableFuture<Boolean>.
  3. Get the current timestamp and create a new CompletableFuture<Boolean> called tracker.
  4. Mark the consumer as draining using the markDraining method and pass in the tracker as an argument.
  5. Send an unsubscribe message for draining using the sendUnsubForDrain method.
  6. Try to flush the connection and wait up to the specified timeout. If a TimeoutException occurs, handle it by calling the processException method of the connection.
  7. Mark the consumer as unsubscribed for draining using the markUnsubedForDrain method.
  8. Submit a task to the connection's executor to run the following code:
    • Create a new timestamp called now.
    • Enter a loop that continues while the timeout is null, equal to Duration.ZERO, or the difference between start and now is less than timeout.
    • Inside the loop, check if the consumer is drained. If it is, break out of the loop.
    • Sleep for 1 millisecond.
    • Update the value of now to the current timestamp.
    • After the loop ends, call the cleanUpAfterDrain method.
    • Finally, complete the tracker with the boolean value indicating if the consumer is drained.
  9. Return the result of getDrainingFuture().

Please note that this description is based on the provided code snippet, and further details about the class and its dependencies may be necessary to fully understand the functionality of the drain method.

The drain method in the NatsConsumer class is used to gracefully stop consuming messages from a NATS server. When called, it performs the following steps:

  1. Checks if the consumer is active and if a connection to the NATS server is established. If not, it throws an IllegalStateException.
  2. Checks if the consumer is already in the process of draining (i.e., stopping). If it is, it returns a future that represents the current draining state.
  3. Marks the consumer as draining and creates a new CompletableFuture object to track its draining state.
  4. Sends an unsubscribe message to the NATS server to stop receiving new messages.
  5. Flushes the connection and waits for the specified timeout duration for pending messages to be sent.
  6. If the flush times out, it processes the timeout exception in the connection.
  7. Marks the consumer as unsubscribed for draining.
  8. Spawns a new thread that continuously checks if the draining has completed within the specified timeout.
  9. Within this thread, it checks if the draining has completed or if the timeout duration has passed. If so, it breaks out of the loop.
  10. After the loop exits, it performs necessary cleanup tasks after the draining process.
  11. Finally, it sets the draining future to the current draining state and returns it.

Overall, the drain method allows for controlled termination of message consumption from the NATS server, ensuring that any pending messages are sent before stopping and providing a way to track the draining state.

sequence diagram

StringAuthHandler

StringAuthHandler

The StringAuthHandler class is an implementation of the AuthHandler interface. It provides functionality for authenticating users based on a string representation of their credentials.

public byte[] sign(byte[] nonce)

public byte[] sign(byte[] nonce) {
    try {
        NKey nkey = NKey.fromSeed(this.nkey);
        byte[] sig = nkey.sign(nonce);
        nkey.clear();
        return sig;
    } catch (Exception exp) {
        throw new IllegalStateException("problem signing nonce", exp);
    }
}

The sign method in the StringAuthHandler class in the io.nats.client.impl package is responsible for generating a signature for a given nonce.

Here is a step-by-step description of what the method is doing:

  1. Convert the nkey string (presumably containing a seed) into an NKey object using the fromSeed method.
  2. Generate a signature for the given nonce by calling the sign method on the NKey object. The generated signature will be stored in a byte array.
  3. Clear the NKey object by calling the clear method. This is done to securely dispose of any sensitive information stored in the NKey object.
  4. Return the generated signature as a byte array.

If any exceptions occur during the process, an IllegalStateException is thrown with the message "problem signing nonce" and the corresponding exception as the cause.

The method sign in the StringAuthHandler class is used to sign a given nonce using an NKey.

To do this, it first creates an instance of the NKey class using the nkey property of the StringAuthHandler object. Then, it calls the sign method of the NKey instance, passing the nonce as a parameter. The returned value is the signature of the nonce.

After the signature is obtained, the clear method is called on the NKey instance to clear any sensitive information related to the key.

Finally, the method returns the signature as a byte array.

If any exception occurs during the signing process, an IllegalStateException is thrown with an error message indicating the problem encountered.

sequence diagram

NatsJetStreamImpl

NatsJetStreamImpl

The NatsJetStreamImpl class is an implementation of the NatsJetStreamConstants interface. It provides functionality related to the NATS JetStream feature.

// ----------------------------------------------------------------------------------------------------

// Management that is also needed by regular context // ---------------------------------------------------------------------------------------------------- ConsumerInfo _getConsumerInfo(String streamName, String consumerName) throws IOException, JetStreamApiException

// ----------------------------------------------------------------------------------------------------
// Management that is also needed by regular context
// ----------------------------------------------------------------------------------------------------
ConsumerInfo _getConsumerInfo(String streamName, String consumerName) throws IOException, JetStreamApiException {
    String subj = String.format(JSAPI_CONSUMER_INFO, streamName, consumerName);
    Message resp = makeRequestResponseRequired(subj, null, jso.getRequestTimeout());
    return new ConsumerInfo(resp).throwOnHasError();
}

The _getConsumerInfo method is defined in the NatsJetStreamImpl class in the io.nats.client.impl package. It takes two parameters: streamName and consumerName.

Below is a step-by-step description of what this method does based on its body:

  1. It creates a subject by formatting the streamName and consumerName using the JSAPI_CONSUMER_INFO format string. This subject is used to request information about a specific consumer in a specific JetStream stream.
String subj = String.format(JSAPI_CONSUMER_INFO, streamName, consumerName);
  1. It makes a request to the NATS server and expects a response. The makeRequestResponseRequired method, which is not shown in the code snippet, is responsible for sending the request and handling the response. The subj parameter is used as the subject for the request, and the null parameter indicates that the request does not include any additional payload data. The jso.getRequestTimeout() method is used to determine the timeout for the request.
Message resp = makeRequestResponseRequired(subj, null, jso.getRequestTimeout());
  1. It creates a ConsumerInfo object using the response message obtained from the previous step. The ConsumerInfo class is responsible for parsing and representing the consumer information received from the JetStream server.
return new ConsumerInfo(resp).throwOnHasError();
  1. It returns the ConsumerInfo object. Before returning, it checks if the ConsumerInfo object contains any error. If an error is detected, it throws a JetStreamApiException with details about the error.

That's the step-by-step description of the _getConsumerInfo method based on its body.

The _getConsumerInfo method in the class io.nats.client.impl.NatsJetStreamImpl is responsible for retrieving the information of a specific consumer within a given stream.

Given the streamName and consumerName as parameters, the method constructs a subject string using JSAPI_CONSUMER_INFO format and sends a request using the makeRequestResponseRequired method from the same class. It expects a response message from the NATS server.

Then, it creates a ConsumerInfo object from the response message and checks if any error occurred during the retrieval process. If an error exists, it throws a JetStreamApiException. Otherwise, it returns the retrieved ConsumerInfo.

This method is part of the management functionality required by the regular context of the NatsJetStreamImpl class.

sequence diagram

ConsumerInfo _createConsumer(String streamName, ConsumerConfiguration config) throws IOException, JetStreamApiException

ConsumerInfo _createConsumer(String streamName, ConsumerConfiguration config) throws IOException, JetStreamApiException {
    // ConsumerConfiguration validates that name and durable are the same if both are supplied.
    String consumerName = config.getName();
    if (consumerName != null && !consumerCreate290Available) {
        throw JsConsumerCreate290NotAvailable.instance();
    }
    String durable = config.getDurable();
    String subj;
    if (consumerCreate290Available) {
        if (consumerName == null) {
            // if both consumerName and durable are null, generate a name
            consumerName = durable == null ? generateConsumerName() : durable;
        }
        String fs = config.getFilterSubject();
        if (fs == null || fs.equals(">")) {
            subj = String.format(JSAPI_CONSUMER_CREATE_V290, streamName, consumerName);
        } else {
            subj = String.format(JSAPI_CONSUMER_CREATE_V290_W_FILTER, streamName, consumerName, fs);
        }
    } else if (durable == null) {
        subj = String.format(JSAPI_CONSUMER_CREATE, streamName);
    } else {
        subj = String.format(JSAPI_DURABLE_CREATE, streamName, durable);
    }
    ConsumerCreateRequest ccr = new ConsumerCreateRequest(streamName, config);
    Message resp = makeRequestResponseRequired(subj, ccr.serialize(), conn.getOptions().getConnectionTimeout());
    return new ConsumerInfo(resp).throwOnHasError();
}

Method: _createConsumer

This method is defined in the class io.nats.client.impl.NatsJetStreamImpl and is responsible for creating a JetStream consumer with the given stream name and consumer configuration.

Signature

ConsumerInfo _createConsumer(String streamName, ConsumerConfiguration config) throws IOException, JetStreamApiException

Steps

  1. Check if the consumerName in the config is not null and if consumerCreate290Available is false.

    • If true, throw an instance of JsConsumerCreate290NotAvailable, indicating that consumer creation is not available.
  2. Get the durable option from the config.

  3. Determine the subject for creating the consumer based on whether consumerCreate290Available is true or false.

  4. If consumerCreate290Available is true, check if consumerName is null.

    • If both consumerName and durable are null, generate a consumer name using the generateConsumerName method.
  5. Get the filterSubject from the config.

    • If filterSubject is null or equals to ">", format the subject using JSAPI_CONSUMER_CREATE_V290 with streamName and consumerName.
    • Otherwise, format the subject using JSAPI_CONSUMER_CREATE_V290_W_FILTER with streamName, consumerName, and filterSubject.
  6. If consumerCreate290Available is false and durable is null, format the subject using JSAPI_CONSUMER_CREATE and streamName.

  7. If none of the above conditions match, format the subject using JSAPI_DURABLE_CREATE with streamName and durable.

  8. Create a ConsumerCreateRequest object with streamName and config.

  9. Make a request with a response required using the subject and serialized ConsumerCreateRequest.

    • Use the connection timeout from the conn.getOptions().
  10. Create a new ConsumerInfo object with the response message and throw an exception if the response has an error.

  11. Return the ConsumerInfo object.

Note: The method throws IOException and JetStreamApiException.

The _createConsumer method is responsible for creating a consumer for a specific stream in the NATS JetStream implementation. It takes in the stream name and a ConsumerConfiguration object as parameters.

First, it validates that the consumer name is provided only if the consumerCreate290Available flag is false. If the flag is true and the consumer name is not provided, a name is generated.

Next, it determines the subject to use for creating the consumer based on the availability of the consumerCreate290Available flag and the presence of a filter subject.

Finally, it creates a ConsumerCreateRequest using the stream name and configuration, makes a request to create the consumer using the determined subject, and returns a ConsumerInfo object representing the created consumer. Any errors encountered during the creation process are thrown as JetStreamApiException.

sequence diagram

void _createConsumerUnsubscribeOnException(String stream, ConsumerConfiguration cc, NatsJetStreamSubscription sub) throws IOException, JetStreamApiException

void _createConsumerUnsubscribeOnException(String stream, ConsumerConfiguration cc, NatsJetStreamSubscription sub) throws IOException, JetStreamApiException {
    try {
        ConsumerInfo ci = _createConsumer(stream, cc);
        sub.setConsumerName(ci.getName());
    } catch (IOException | JetStreamApiException e) {
        // create consumer can fail, unsubscribe and then throw the exception to the user
        if (sub.getDispatcher() == null) {
            sub.unsubscribe();
        } else {
            sub.getDispatcher().unsubscribe(sub);
        }
        throw e;
    }
}

The _createConsumerUnsubscribeOnException method in the class NatsJetStreamImpl is responsible for creating a consumer for a specific stream using the given consumer configuration. If an exception occurs during the creation of the consumer, the method will automatically unsubscribe the subscription and then re-throw the exception to the user.

Here is a step-by-step description of what the method is doing:

  1. The method accepts three parameters: stream (the name of the stream), cc (the consumer configuration), and sub (the NatsJetStreamSubscription object).
  2. The method tries to create a consumer using the _createConsumer method, passing the stream and cc parameters. This method will return a ConsumerInfo object.
  3. If the consumer creation is successful, the method sets the consumer name in the sub object using the setName method.
  4. If an exception occurs during the consumer creation (either an IOException or a JetStreamApiException), the catch block is executed.
  5. Inside the catch block, the method checks if the sub object has a dispatcher (a MessageDispatcher object) attached to it.
  6. If the dispatcher is not present, the method calls the unsubscribe method on the sub object to unsubscribe it.
  7. If the dispatcher is present, the method calls the unsubscribe method on the dispatcher, passing the sub object as a parameter.
  8. Finally, the method re-throws the caught exception (e) to the user, ensuring that the calling code is aware of the failure that occurred during consumer creation.

By unsubscribing the subscription in case of an exception, the method ensures that no messages will be received by the consumer in an inconsistent state. Instead, the subscription is safely closed, and the exception is propagated to the user for appropriate error handling and reporting.

This method, _createConsumerUnsubscribeOnException, is used to create a consumer subscription for a given stream with the specified consumer configuration. It takes in three parameters: String stream, ConsumerConfiguration cc, and NatsJetStreamSubscription sub.

Inside the method, it first attempts to create the consumer using the _createConsumer method and assigns the consumer's name to the subscription. If any exception occurs during this process, the method catches it and performs the necessary cleanup.

If the subscription's dispatcher is null, it calls the unsubscribe() method to unsubscribe the subscription completely. Otherwise, it calls the unsubscribe(sub) method on the dispatcher.

Finally, regardless of whether an exception occurred or not, the method throws the caught exception to the user.

sequence diagram

StreamInfo _getStreamInfo(String streamName, StreamInfoOptions options) throws IOException, JetStreamApiException

StreamInfo _getStreamInfo(String streamName, StreamInfoOptions options) throws IOException, JetStreamApiException {
    String subj = String.format(JSAPI_STREAM_INFO, streamName);
    StreamInfoReader sir = new StreamInfoReader();
    while (sir.hasMore()) {
        Message resp = makeRequestResponseRequired(subj, sir.nextJson(options), jso.getRequestTimeout());
        sir.process(resp);
    }
    return cacheStreamInfo(streamName, sir.getStreamInfo());
}

Method Description - _getStreamInfo

This method is defined in the class io.nats.client.impl.NatsJetStreamImpl. It takes in two parameters - streamName (a String) and options (a StreamInfoOptions object).

Step 1: Formatting the Subject

The method formats the subject using the streamName parameter and a constant JSAPI_STREAM_INFO. The formatted subject is stored in the subj variable.

Step 2: StreamInfoReader Initialization and Reading

A StreamInfoReader object sir is initialized. The method then enters a loop to read the StreamInfo response using the sir.hasMore() method.

Step 3: Making a Request and Processing the Response

Inside the loop, the method makes a request by calling the makeRequestResponseRequired method with the subj and sir.nextJson(options) as parameters. The jso.getRequestTimeout() value is passed as an argument as well.

The response received is then processed by calling the sir.process(resp) method.

Step 4: Caching and Returning the StreamInfo

After exiting the loop, the method caches the StreamInfo by calling the cacheStreamInfo method with the streamName and sir.getStreamInfo() as parameters.

Finally, it returns the cached StreamInfo as the output of the method.

Note: The method also throws two exceptions - IOException and JetStreamApiException.

The _getStreamInfo method in the NatsJetStreamImpl class is used to retrieve information about a stream in the NATS JetStream messaging system.

  • The method takes two parameters: streamName (the name of the stream to get information about) and options (additional options for retrieving stream information).
  • It constructs a subject string for sending a request to the server using the JSAPI_STREAM_INFO format.
  • It creates a StreamInfoReader object to read the response messages from the server.
  • It then enters a loop to process all the response messages and extract the stream information using the sir object.
  • Finally, it caches the retrieved stream information using the cacheStreamInfo method and returns the cached information.

This method can throw an IOException if there is an error during the request-response process, and a JetStreamApiException if there is an error related to the JetStream API.

sequence diagram

List _getStreamNames(String subjectFilter) throws IOException, JetStreamApiException

List<String> _getStreamNames(String subjectFilter) throws IOException, JetStreamApiException {
    StreamNamesReader snr = new StreamNamesReader();
    while (snr.hasMore()) {
        Message resp = makeRequestResponseRequired(JSAPI_STREAM_NAMES, snr.nextJson(subjectFilter), jso.getRequestTimeout());
        snr.process(resp);
    }
    return snr.getStrings();
}

Method _getStreamNames Description

This method is defined in the NatsJetStreamImpl class in the io.nats.client.impl package. It retrieves a list of stream names based on a subject filter.

Input Parameters

  • subjectFilter (Type: String): It is the filter used to match the stream names.

Exceptions

  • IOException: This exception is thrown if there is an error while performing IO operations.
  • JetStreamApiException: This exception is thrown if there is an error related to JetStream API.

Method Steps:

  1. Initialize a StreamNamesReader object named snr.
  2. Enter a loop that continues while snr has more elements.
  3. Inside the loop, perform the following steps:
    • Create a Message object resp by making a request with the JSAPI_STREAM_NAMES identifier, the nextJson obtained from snr using the subjectFilter, and the request timeout from the jso object.
    • Process the response resp using the snr.process method.
  4. After the loop ends, return the strings obtained from the snr object using the getStrings method.

Please note that more implementation details might be present for some methods or variables used in this method, but they are not mentioned in the provided BODY.

The _getStreamNames method is a part of the NatsJetStreamImpl class in the io.nats.client.impl package. This method takes a subjectFilter as input and returns a list of stream names that match the filter.

The method starts by creating a StreamNamesReader object. It then enters a loop that continues as long as there are more stream names to process.

Within the loop, the method makes a request to retrieve the stream names by calling the makeRequestResponseRequired method. This method requires a response and throws an exception if it does not receive one. The request is made with the JSAPI_STREAM_NAMES parameter and the next JSON object from the StreamNamesReader which includes the subjectFilter and the request timeout from the jso object.

The response is then processed by calling the snr.process method, which updates the StreamNamesReader object with the names retrieved from the response.

Once there are no more stream names to process, the method returns the list of stream names by calling the snr.getStrings method.

This method may throw an IOException or JetStreamApiException, which should be handled by the calling code.

sequence diagram

// ----------------------------------------------------------------------------------------------------

// General Utils // ---------------------------------------------------------------------------------------------------- ConsumerInfo lookupConsumerInfo(String streamName, String consumerName) throws IOException, JetStreamApiException

// ----------------------------------------------------------------------------------------------------
// General Utils
// ----------------------------------------------------------------------------------------------------
ConsumerInfo lookupConsumerInfo(String streamName, String consumerName) throws IOException, JetStreamApiException {
    try {
        return _getConsumerInfo(streamName, consumerName);
    } catch (JetStreamApiException e) {
        // the right side of this condition...  ( starting here \/ ) is for backward compatibility with server versions that did not provide api error codes
        if (e.getApiErrorCode() == JS_CONSUMER_NOT_FOUND_ERR || (e.getErrorCode() == 404 && e.getErrorDescription().contains("consumer"))) {
            return null;
        }
        throw e;
    }
}

The lookupConsumerInfo method in the NatsJetStreamImpl class is used to retrieve information about a specific consumer from a given stream.

Here is a step-by-step description of the method's functionality:

  1. The method takes two parameters: streamName (the name of the stream) and consumerName (the name of the consumer).

  2. The method tries to retrieve the consumer information by calling the _getConsumerInfo method with the provided streamName and consumerName.

  3. If the _getConsumerInfo method throws a JetStreamApiException, the method checks the exception's apiErrorCode and errorCode.

  4. If the apiErrorCode matches the JS_CONSUMER_NOT_FOUND_ERR constant or the errorCode is 404 and the error description contains the string "consumer", this means that the consumer was not found. In this case, the method returns null.

  5. If the exception does not match the above conditions, it is re-thrown to be handled by the calling code.

Note: The method assumes that the _getConsumerInfo method has the necessary logic to retrieve the consumer information based on the provided stream and consumer names.

The lookupConsumerInfo method is used to retrieve information about a specific consumer in a JetStream stream. It takes in the streamName and consumerName as parameters and returns a ConsumerInfo object.

Internally, it calls the _getConsumerInfo method to retrieve the consumer information. If the consumer is not found, it handles backward compatibility by checking the error code and error description provided by the JetStream API. If the error code indicates a consumer not found error or the error description contains "consumer", the method returns null. Otherwise, it rethrows the JetStreamApiException for other types of errors.

sequence diagram

// ----------------------------------------------------------------------------------------------------

// Request Utils // ---------------------------------------------------------------------------------------------------- Message makeRequestResponseRequired(String subject, byte[] bytes, Duration timeout) throws IOException

// ----------------------------------------------------------------------------------------------------
// Request Utils
// ----------------------------------------------------------------------------------------------------
Message makeRequestResponseRequired(String subject, byte[] bytes, Duration timeout) throws IOException {
    try {
        return responseRequired(conn.request(prependPrefix(subject), bytes, timeout));
    } catch (InterruptedException e) {
        throw new IOException(e);
    }
}

Method: makeRequestResponseRequired

Description:

This method is used to send a request with a required response using the NATS messaging system. It takes in a subject, a byte array, and a timeout duration as inputs and returns a Message object.

Step by step description:

  1. Prepend the subject with the configured prefix using the prependPrefix method.
  2. Send the request using the NATS connection object (conn) by calling the request method with the prepended subject, the byte array, and the timeout duration.
  3. Call the responseRequired method to handle the response received from the request.
  4. If the request is interrupted and an InterruptedException is thrown, wrap it in an IOException and throw it.

The makeRequestResponseRequired method is a utility method that is used to send a request message to a NATS server and waits for a response.

It takes three parameters:

  • subject: The subject of the message.
  • bytes: The message payload as a byte array.
  • timeout: The duration for which the method will wait for a response before throwing an exception.

Internally, the method makes use of the conn.request method to send the request message to the NATS server. It prepends a prefix to the subject before sending the request.

If a response is received within the specified timeout duration, the method returns the received Message object. Otherwise, it throws an IOException.

Overall, makeRequestResponseRequired provides a convenient way to send request messages and handle the corresponding responses.

sequence diagram

Message makeInternalRequestResponseRequired(String subject, Headers headers, byte[] data, Duration timeout, CancelAction cancelAction) throws IOException

Message makeInternalRequestResponseRequired(String subject, Headers headers, byte[] data, Duration timeout, CancelAction cancelAction) throws IOException {
    try {
        return responseRequired(conn.requestInternal(subject, headers, data, timeout, cancelAction));
    } catch (InterruptedException e) {
        throw new IOException(e);
    }
}

The makeInternalRequestResponseRequired method is defined in the NatsJetStreamImpl class in the io.nats.client.impl package. The purpose of this method is to send an internal NATS request with the expectation of receiving a response.

Here is a step-by-step description of what this method is doing:

  1. The method takes the following parameters:

    • subject (String): The subject of the NATS request.
    • headers (Headers): The headers to be included in the request.
    • data (byte[]): The payload data of the request.
    • timeout (Duration): The maximum duration to wait for a response.
    • cancelAction (CancelAction): An action to be executed if the request is cancelled.
  2. The method tries to make the internal NATS request by calling the requestInternal method on the connection (conn) object. This method returns a Message object representing the response.

    • The requestInternal method is passed the subject, headers, data, timeout, and cancelAction parameters.
    • The method might throw an interrupted exception, so it is surrounded by a try-catch block.
  3. If the requestInternal method call is successful, the method calls the responseRequired method to process the received response.

    • The responseRequired method is passed the Message object representing the response.
    • The result of this method call is returned by the makeInternalRequestResponseRequired method.
  4. If an InterruptedException is caught during the execution of the try block, it is wrapped in an IOException and thrown.

In summary, the makeInternalRequestResponseRequired method sends an internal NATS request with the specified subject, headers, and payload data. It waits for a response up to the specified timeout duration, and then processes the received response before returning it.

The makeInternalRequestResponseRequired method is a part of the NatsJetStreamImpl class in the io.nats.client.impl package.

This method is responsible for making an internal request to the NATS server and requiring a response. It takes in the parameters subject (the subject of the message), headers (optional headers for the message), data (the payload of the message), timeout (the maximum time to wait for a response), and cancelAction (an action to execute if the request is cancelled).

Internally, this method calls the requestInternal method of the conn object (an instance of Connection) to make the request and waits for the response. It then returns the response as a Message object.

If the method is interrupted while waiting for the response, it throws an IOException.

sequence diagram

Message responseRequired(Message respMessage) throws IOException

Message responseRequired(Message respMessage) throws IOException {
    if (respMessage == null) {
        throw new IOException("Timeout or no response waiting for NATS JetStream server");
    }
    return respMessage;
}

The responseRequired method defined in the NatsJetStreamImpl class is responsible for handling the response message received from the NATS JetStream server. Here is a step-by-step description of what the method does based on its body:

  1. The method takes a parameter respMessage, which is the response message received from the server.
  2. If the respMessage is null, it means that the method has either timed out waiting for a response or there was no response from the NATS JetStream server.
  3. In this case, the method throws an IOException with the message "Timeout or no response waiting for NATS JetStream server" to indicate the error condition.
  4. If the respMessage is not null, it means that a response message was received from the server.
  5. In this case, the method simply returns the respMessage as-is.

Overall, the responseRequired method allows you to handle the response message received from the NATS JetStream server, throwing an exception if no response was received or returning the response message if it was received successfully.

The responseRequired method is a part of the NatsJetStreamImpl class and it takes a Message object as input parameter and returns a Message object.

It checks if the input respMessage is null and throws an IOException with a message indicating a timeout or no response from the NATS JetStream server.

If the respMessage is not null, it simply returns the respMessage.

This method is used to verify if a response is required from the NATS JetStream server and handles the logic for handling timeouts or missing responses.

sequence diagram

NatsJetStream

NatsJetStream

The NatsJetStream class is a subclass of NatsJetStreamImpl and implements the JetStream interface. It provides functionality for working with JetStream in NATS, a high-performance messaging system.

// ----------------------------------------------------------------------------------------------------

// Publish // ---------------------------------------------------------------------------------------------------- /** *

// ----------------------------------------------------------------------------------------------------
// Publish
// ----------------------------------------------------------------------------------------------------
/**
 * {@inheritDoc}
 */
@Override
public PublishAck publish(String subject, byte[] body) throws IOException, JetStreamApiException {
    return publishSyncInternal(subject, null, body, null);
}

The publish method defined in the NatsJetStream class is responsible for publishing a message to a subject in the NATS server using the NATS JetStream API.

Here is a step-by-step description of what the method is doing based on its body:

  1. The method signature indicates that it returns a PublishAck object and can potentially throw IOException or JetStreamApiException.

  2. The method overrides the publish method declared in a super class.

  3. The subject and body parameters are passed to the method. The subject parameter specifies the subject to which the message will be published, and the body parameter is the byte array representing the content of the message.

  4. The method calls the private method publishSyncInternal with the subject, null as the replyTo parameter, body, and null as the headers parameter.

  5. The publishSyncInternal method is responsible for executing the actual publish operation synchronously.

  6. The PublishAck object returned by publishSyncInternal is then returned by the publish method.

  7. If an IOException or JetStreamApiException occurs during the execution, it is thrown by the method.

This method provides a convenient way to publish a message to a subject in NATS JetStream without specifying a reply subject or any headers.

The publish method in the NatsJetStream class is responsible for publishing a message to a specified subject in the NATS messaging system.

It takes two parameters: subject (which represents the subject/topic to publish the message to) and body (which is the actual content of the message in the form of a byte array).

Internally, it calls the publishSyncInternal method to publish the message synchronously, passing along the subject and body parameters. It also provides null values for two optional parameters: options and messageId.

The publish method then returns a PublishAck object, which represents an acknowledgment of the message publishing process.

In case of any exceptions or errors during the publishing process, such as IO errors or JetStream API exceptions, the method throws IOException or JetStreamApiException respectively, allowing the caller to handle the errors appropriately.

sequence diagram

private PublishAck publishSyncInternal(String subject, Headers headers, byte[] data, PublishOptions options) throws IOException, JetStreamApiException

private PublishAck publishSyncInternal(String subject, Headers headers, byte[] data, PublishOptions options) throws IOException, JetStreamApiException {
    Headers merged = mergePublishOptions(headers, options);
    if (jso.isPublishNoAck()) {
        conn.publishInternal(subject, null, merged, data);
        return null;
    }
    Duration timeout = options == null ? jso.getRequestTimeout() : options.getStreamTimeout();
    Message resp = makeInternalRequestResponseRequired(subject, merged, data, timeout, CancelAction.COMPLETE);
    return processPublishResponse(resp, options);
}

The publishSyncInternal method is a function defined in the NatsJetStream class, located in the io.nats.client.impl package.

Here is a step-by-step description of what the method does:

  1. The method accepts four parameters: subject (the subject to publish the message to), headers (the headers for the message), data (the message data as a byte array), and options (additional options for publishing).
  2. The mergePublishOptions method is called to merge the headers and options parameters into a single Headers object named merged.
  3. The method then checks if the PublishOptions object has the publishNoAck flag set. If it does, it calls the publishInternal method on the conn object (which represents the NATS connection) to publish the message without waiting for an acknowledgement. The subject, null for reply subject, merged for headers, and data are passed as parameters to the publishInternal method. The method then returns null since no acknowledgement is expected.
  4. If the publishNoAck flag is not set, the method proceeds to get the request timeout value from the options object (or uses the default request timeout from jso if options is null) and assigns it to the timeout variable of type Duration.
  5. The makeInternalRequestResponseRequired method is called with the subject, merged headers, data, timeout, and CancelAction.COMPLETE parameters. This method is responsible for making the internal request and waiting for a response. It returns a Message object named resp which represents the response received.
  6. Finally, the processPublishResponse method is called, passing the resp object and the options object as parameters. This method is responsible for processing the publish response, including handling any errors, and returns a PublishAck object representing the acknowledgement of the publish operation. The PublishAck object is then returned by the publishSyncInternal method.

The method publishSyncInternal is responsible for publishing a message synchronously to NATS JetStream.

It takes in the following parameters:

  • subject (String): The subject/topic to publish the message on.
  • headers (Headers): The headers to be included with the message.
  • data (byte[]): The actual data/message to be published.
  • options (PublishOptions): Additional options for publishing.

First, the method merges the headers and options into a new header object called merged.

If the PublishOptions object's isPublishNoAck() method returns true, it means that the message should not expect an acknowledgement upon successful publishing. In this case, the publishInternal method is called on the conn object (which represents the NATS connection) to publish the message with the subject, null reply subject, merged headers, and data. The method then returns null.

If isPublishNoAck() returns false, it means that an acknowledgement is expected. The method then determines the timeout duration to wait for the response by checking the options object. If options is null, it uses the default request timeout from the jso (JetStreamOptions) object. Otherwise, it uses the StreamTimeout from the options object.

The makeInternalRequestResponseRequired method is called to make an internal request and wait for a response. It includes the subject, merged headers, data, timeout, and CancelAction.COMPLETE (which means the request is not cancellable). This method returns a Message object as the response.

Finally, the method processPublishResponse is called to process the resp (response) and return a PublishAck object, based on the provided options.

The method can throw IOException and JetStreamApiException, which indicates potential issues with the I/O operations or JetStream API calls.

sequence diagram

private CompletableFuture publishAsyncInternal(String subject, Headers headers, byte[] data, PublishOptions options, Duration knownTimeout)

private CompletableFuture<PublishAck> publishAsyncInternal(String subject, Headers headers, byte[] data, PublishOptions options, Duration knownTimeout) {
    Headers merged = mergePublishOptions(headers, options);
    if (jso.isPublishNoAck()) {
        conn.publishInternal(subject, null, merged, data);
        return null;
    }
    CompletableFuture<Message> future = conn.requestFutureInternal(subject, merged, data, knownTimeout, CancelAction.COMPLETE);
    return future.thenCompose(resp -> {
        try {
            responseRequired(resp);
            return CompletableFuture.completedFuture(processPublishResponse(resp, options));
        } catch (IOException | JetStreamApiException e) {
            throw new RuntimeException(e);
        }
    });
}

Method: publishAsyncInternal

This method is defined in the class io.nats.client.impl.NatsJetStream and has the following signature:

private CompletableFuture<PublishAck> publishAsyncInternal(String subject, Headers headers, byte[] data, PublishOptions options, Duration knownTimeout)

Description

The publishAsyncInternal method is responsible for asynchronously publishing a message to Nats JetStream.

Steps

  1. Merge the headers and options to create a new Headers object named merged.
  2. Check if the jso.isPublishNoAck() flag is set. If it is, perform the following steps:
    • Call the conn.publishInternal method to publish the message without acknowledgments. Pass the subject, null for the reply subject, merged headers, and the message data as arguments.
    • Return null indicating that no acknowledgement is expected.
  3. If the jso.isPublishNoAck() flag is not set, proceed to the next step.
  4. Call the conn.requestFutureInternal method to send a request message and get a CompletableFuture<Message> object named future. Pass the subject, merged headers, data, knownTimeout, and CancelAction.COMPLETE as arguments.
  5. Use the future.thenCompose method to chain further processing after the response is received. Perform the following steps:
    • Inside the thenCompose callback function, check for any exceptions that occurred during the response processing. If an exception is caught, throw a RuntimeException with the caught exception as its cause.
    • If no exceptions occurred, call the responseRequired method to validate the response.
    • Call the processPublishResponse method to process the publish response and obtain a PublishAck object. Pass the resp (response) and options as arguments.
    • Wrap the PublishAck object in a completed CompletableFuture using CompletableFuture.completedFuture.
  6. Return the CompletableFuture<PublishAck> object obtained from step 5.

Note: The method assumes that the necessary imports and declarations are present in the class.

The publishAsyncInternal method is used to asynchronously publish a message to a subject in NATS JetStream.

Here's a breakdown of what this method does:

  1. It takes several parameters: subject (the subject to publish to), headers (optional headers of the message), data (the message payload as a byte array), options (optional publish options), and knownTimeout (timeout duration).

  2. It merges the headers and options into a new Headers object called merged, using the mergePublishOptions method.

  3. If the publish option isPublishNoAck is true, it calls the publishInternal method on the connection object (conn) to publish the message without waiting for an acknowledgement. It then returns null.

  4. If the publish option isPublishNoAck is false, it creates a CompletableFuture called future by calling the requestFutureInternal method on the connection object (conn). This sends the message to the subject and expects a response from the server within the specified knownTimeout.

  5. It then uses the thenCompose method on the future to handle the response from the server. This is where the logic for processing the response and handling any errors is implemented.

  6. Inside the thenCompose block, it checks if a response is required (using the responseRequired method), and if so, it processes the response using the processPublishResponse method and returns a completed CompletableFuture with the result.

  7. If any IOException or JetStreamApiException is caught during the processing of the response, it throws a RuntimeException.

In summary, the publishAsyncInternal method asynchronously publishes a message to a subject in NATS JetStream and handles the response from the server based on the specified publish options and timeout.

sequence diagram

private PublishAck processPublishResponse(Message resp, PublishOptions options) throws IOException, JetStreamApiException

private PublishAck processPublishResponse(Message resp, PublishOptions options) throws IOException, JetStreamApiException {
    if (resp.isStatusMessage()) {
        throw new IOException("Error Publishing: " + resp.getStatus().getMessageWithCode());
    }
    PublishAck ack = new PublishAck(resp);
    String ackStream = ack.getStream();
    String pubStream = options == null ? null : options.getStream();
    // stream specified in options but different from ack should not happen but...
    if (pubStream != null && !pubStream.equals(ackStream)) {
        throw new IOException("Expected ack from stream " + pubStream + ", received from: " + ackStream);
    }
    return ack;
}

The processPublishResponse method in the NatsJetStream class is responsible for processing the response received after publishing a message. Here is a step-by-step description of what the method does:

  1. The method takes two parameters: a Message object representing the response received and a PublishOptions object containing the options used for publishing the message.
  2. The method first checks if the response is a status message. If it is, an IOException is thrown with an error message including the status message code and description.
  3. If the response is not a status message, the method proceeds to create a new PublishAck object using the response. This PublishAck object represents the acknowledgment received for the published message.
  4. The method then retrieves the stream name from the PublishAck object and assigns it to the variable ackStream.
  5. Next, the method checks if the PublishOptions object is null. If it is, the variable pubStream is set to null.
  6. If the PublishOptions object is not null, the method retrieves the stream name from the PublishOptions object and assigns it to the variable pubStream.
  7. The method then checks if the pubStream is not null and if it is different from the ackStream. If they are different, an IOException is thrown with an error message indicating the expected stream from the pubStream variable and the actual stream received from the ackStream variable.
  8. Finally, if none of the above conditions are met, the method returns the PublishAck object.

In summary, the processPublishResponse method handles the response received after publishing a message by checking for status messages, creating a PublishAck object, and performing stream comparison checks before returning the PublishAck object.

The processPublishResponse method in the NatsJetStream class is responsible for processing the response received after publishing a message.

The method takes two parameters - resp (representing the response message) and options (representing the options used for publishing). The method returns a PublishAck object.

If the response message indicates an error (status message), an IOException is thrown with the corresponding error message.

The method creates a PublishAck object using the response message, which represents the acknowledgment received for the published message.

It then checks if the stream specified in the options is different from the stream mentioned in the acknowledgment. If there is a mismatch, an IOException is thrown.

Finally, the method returns the PublishAck object.

Overall, this method handles error cases during publishing and ensures that the received acknowledgment matches the specified stream.

sequence diagram

private Headers mergePublishOptions(Headers headers, PublishOptions opts)

private Headers mergePublishOptions(Headers headers, PublishOptions opts) {
    // never touch the user's original headers
    Headers merged = headers == null ? null : new Headers(headers);
    if (opts != null) {
        merged = mergeNum(merged, EXPECTED_LAST_SEQ_HDR, opts.getExpectedLastSequence());
        merged = mergeNum(merged, EXPECTED_LAST_SUB_SEQ_HDR, opts.getExpectedLastSubjectSequence());
        merged = mergeString(merged, EXPECTED_LAST_MSG_ID_HDR, opts.getExpectedLastMsgId());
        merged = mergeString(merged, EXPECTED_STREAM_HDR, opts.getExpectedStream());
        merged = mergeString(merged, MSG_ID_HDR, opts.getMessageId());
    }
    return merged;
}

The mergePublishOptions method in the NatsJetStream class is used to merge the provided headers and PublishOptions object.

Here is a step-by-step description of what the method does:

  1. It takes two parameters: headers and opts of types Headers and PublishOptions respectively.

  2. It creates a new Headers object called merged by cloning the original headers provided as a parameter. If headers is null, merged will also be null.

  3. It checks if the opts object is not null.

  4. It calls the mergeNum method to merge the EXPECTED_LAST_SEQ_HDR (a constant value) header into merged with the value retrieved from opts.getExpectedLastSequence().

  5. It calls the mergeNum method to merge the EXPECTED_LAST_SUB_SEQ_HDR (a constant value) header into merged with the value retrieved from opts.getExpectedLastSubjectSequence().

  6. It calls the mergeString method to merge the EXPECTED_LAST_MSG_ID_HDR (a constant value) header into merged with the value retrieved from opts.getExpectedLastMsgId().

  7. It calls the mergeString method to merge the EXPECTED_STREAM_HDR (a constant value) header into merged with the value retrieved from opts.getExpectedStream().

  8. It calls the mergeString method to merge the MSG_ID_HDR (a constant value) header into merged with the value retrieved from opts.getMessageId().

  9. It returns the merged Headers object.

Note: The mergeNum and mergeString methods are not defined in the provided code snippet. They are assumed to be helper methods that merge the provided key-value pair into the Headers object.

The method mergePublishOptions in the NatsJetStream class merges the given headers with the opts (PublishOptions) to create a new set of merged headers.

The original headers are never modified to ensure they remain intact. If headers is null, the merged headers will be null as well.

The method then proceeds to merge specific elements of opts into the merged headers using helper methods like mergeNum and mergeString. These elements include expectedLastSequence, expectedLastSubjectSequence, expectedLastMsgId, expectedStream, and messageId.

Finally, the merged headers are returned as the result of the method.

sequence diagram

private Headers _mergeNum(Headers h, String key, String value)

private Headers _mergeNum(Headers h, String key, String value) {
    if (h == null) {
        h = new Headers();
    }
    return h.add(key, value);
}

NatsJetStream _mergeNum Method

The _mergeNum method is a private method defined in the io.nats.client.impl.NatsJetStream class. This method takes three parameters:

  1. Headers h: An instance of the Headers class, which represents the headers for a NatsJetStream request.
  2. String key: The key of the header to be added or modified.
  3. String value: The value of the header to be added or modified.

The purpose of the _mergeNum method is to merge a new header or modify an existing header in the Headers object. Here is a step-by-step description of what the method does:

  1. Check if the h parameter is null.
  2. If h is null, create a new instance of the Headers class and assign it to h.
  3. Add or modify a header in the Headers object identified by the key parameter and set its value to the value parameter.
  4. Return the modified Headers object.

The _mergeNum method ensures that a new header is added or an existing header is modified in the Headers object, which is then returned.

The _mergeNum method is a private method defined in the io.nats.client.impl.NatsJetStream class. This method takes three parameters: h, key, and value.

The purpose of this method is to merge the provided key-value pair into the given Headers object h. If the h object is null, a new Headers object is created. The method then adds the key-value pair to the Headers object using the add method.

Overall, the _mergeNum method ensures that the provided key-value pair is included in the Headers object, even if it is initially null, and returns the updated Headers object.

sequence diagram

JetStreamSubscription createSubscription(String subject, String queueName, NatsDispatcher dispatcher, MessageHandler userHandler, boolean isAutoAck, PushSubscribeOptions pushSubscribeOptions, PullSubscribeOptions pullSubscribeOptions) throws IOException, JetStreamApiException

JetStreamSubscription createSubscription(String subject, String queueName, NatsDispatcher dispatcher, MessageHandler userHandler, boolean isAutoAck, PushSubscribeOptions pushSubscribeOptions, PullSubscribeOptions pullSubscribeOptions) throws IOException, JetStreamApiException {
    // 1. Prepare for all the validation
    boolean isPullMode = pullSubscribeOptions != null;
    SubscribeOptions so;
    String stream;
    String qgroup;
    ConsumerConfiguration userCC;
    if (isPullMode) {
        // options must have already been checked to be non-null
        so = pullSubscribeOptions;
        stream = pullSubscribeOptions.getStream();
        userCC = so.getConsumerConfiguration();
        // just to make compiler happy both paths set variable
        qgroup = null;
        validateNotSupplied(userCC.getDeliverGroup(), JsSubPullCantHaveDeliverGroup);
        validateNotSupplied(userCC.getDeliverSubject(), JsSubPullCantHaveDeliverSubject);
    } else {
        so = pushSubscribeOptions == null ? DEFAULT_PUSH_OPTS : pushSubscribeOptions;
        // might be null, that's ok (see directBind)
        stream = so.getStream();
        userCC = so.getConsumerConfiguration();
        if (userCC.maxPullWaitingWasSet()) {
            throw JsSubPushCantHaveMaxPullWaiting.instance();
        }
        if (userCC.maxBatchWasSet()) {
            throw JsSubPushCantHaveMaxBatch.instance();
        }
        if (userCC.maxBytesWasSet()) {
            throw JsSubPushCantHaveMaxBytes.instance();
        }
        // figure out the queue name
        qgroup = validateMustMatchIfBothSupplied(userCC.getDeliverGroup(), queueName, JsSubQueueDeliverGroupMismatch);
        if (so.isOrdered() && qgroup != null) {
            throw JsSubOrderedNotAllowOnQueues.instance();
        }
        if (dispatcher != null && (so.getPendingMessageLimit() != Consumer.DEFAULT_MAX_MESSAGES || so.getPendingByteLimit() != Consumer.DEFAULT_MAX_BYTES)) {
            throw JsSubPushAsyncCantSetPending.instance();
        }
    }
    // 2A. Flow Control / heartbeat not always valid
    if (userCC.getIdleHeartbeat() != null && userCC.getIdleHeartbeat().toMillis() > 0) {
        if (isPullMode) {
            throw JsSubFcHbNotValidPull.instance();
        }
        if (qgroup != null) {
            throw JsSubFcHbNotValidQueue.instance();
        }
    }
    // 2B. Did they tell me what stream? No? look it up.
    final String fnlStream;
    if (stream == null) {
        fnlStream = lookupStreamBySubject(subject);
        if (fnlStream == null) {
            throw JsSubNoMatchingStreamForSubject.instance();
        }
    } else {
        fnlStream = stream;
    }
    ConsumerConfiguration serverCC = null;
    String consumerName = userCC.getDurable();
    if (consumerName == null) {
        consumerName = userCC.getName();
    }
    String inboxDeliver = userCC.getDeliverSubject();
    // 3. Does this consumer already exist?
    if (consumerName != null) {
        ConsumerInfo serverInfo = lookupConsumerInfo(fnlStream, consumerName);
        if (serverInfo != null) {
            // the consumer for that durable already exists
            serverCC = serverInfo.getConsumerConfiguration();
            // check to see if the user sent a different version than the server has
            // because modifications are not allowed during create subscription
            ConsumerConfigurationComparer userCCC = new ConsumerConfigurationComparer(userCC);
            List<String> changes = userCCC.getChanges(serverCC);
            if (changes.size() > 0) {
                throw JsSubExistingConsumerCannotBeModified.instance("Changed fields: " + changes);
            }
            // deliver subject must be null/empty for pull, defined for push
            if (isPullMode) {
                if (!nullOrEmpty(serverCC.getDeliverSubject())) {
                    throw JsSubConsumerAlreadyConfiguredAsPush.instance();
                }
            } else if (nullOrEmpty(serverCC.getDeliverSubject())) {
                throw JsSubConsumerAlreadyConfiguredAsPull.instance();
            }
            if (serverCC.getDeliverGroup() == null) {
                // lookedUp was null, means existing consumer is not a queue consumer
                if (qgroup == null) {
                    // ok fine, no queue requested and the existing consumer is also not a queue consumer
                    // we must check if the consumer is in use though
                    if (serverInfo.isPushBound()) {
                        throw JsSubConsumerAlreadyBound.instance();
                    }
                } else {
                    // else they requested a queue but this durable was not configured as queue
                    throw JsSubExistingConsumerNotQueue.instance();
                }
            } else if (qgroup == null) {
                throw JsSubExistingConsumerIsQueue.instance();
            } else if (!serverCC.getDeliverGroup().equals(qgroup)) {
                throw JsSubExistingQueueDoesNotMatchRequestedQueue.instance();
            }
            // durable already exists, make sure the filter subject matches
            if (nullOrEmpty(subject)) {
                // allowed if they had given both stream and durable
                subject = userCC.getFilterSubject();
            } else if (!isFilterMatch(subject, serverCC.getFilterSubject(), fnlStream)) {
                throw JsSubSubjectDoesNotMatchFilter.instance();
            }
            // use the deliver subject as the inbox. It may be null, that's ok, we'll fix that later
            inboxDeliver = serverCC.getDeliverSubject();
        } else if (so.isBind()) {
            throw JsSubConsumerNotFoundRequiredInBind.instance();
        }
    }
    // 4. If pull or no deliver subject (inbox) provided or found, make an inbox.
    final String fnlInboxDeliver;
    if (isPullMode) {
        fnlInboxDeliver = conn.createInbox() + ".*";
    } else if (inboxDeliver == null) {
        fnlInboxDeliver = conn.createInbox();
    } else {
        fnlInboxDeliver = inboxDeliver;
    }
    // 5. If consumer does not exist, create and settle on the config. Name will have to wait
    //    If the consumer exists, I know what the settled info is
    final String settledConsumerName;
    final ConsumerConfiguration settledServerCC;
    if (serverCC == null) {
        ConsumerConfiguration.Builder ccBuilder = ConsumerConfiguration.builder(userCC);
        // Pull mode doesn't maintain a deliver subject. It's actually an error if we send it.
        if (!isPullMode) {
            ccBuilder.deliverSubject(fnlInboxDeliver);
        }
        if (userCC.getFilterSubject() == null) {
            ccBuilder.filterSubject(subject);
        }
        ccBuilder.deliverGroup(qgroup);
        settledServerCC = ccBuilder.build();
        settledConsumerName = null;
    } else {
        settledServerCC = serverCC;
        settledConsumerName = consumerName;
    }
    // 6. create the subscription. lambda needs final or effectively final vars
    final MessageManager mm;
    final NatsSubscriptionFactory subFactory;
    if (isPullMode) {
        MessageManagerFactory mmFactory = so.isOrdered() ? _pullOrderedMessageManagerFactory : _pullMessageManagerFactory;
        mm = mmFactory.createMessageManager(conn, this, fnlStream, so, settledServerCC, false, dispatcher == null);
        subFactory = (sid, lSubject, lQgroup, lConn, lDispatcher) -> new NatsJetStreamPullSubscription(sid, lSubject, lConn, lDispatcher, this, fnlStream, settledConsumerName, mm);
    } else {
        MessageManagerFactory mmFactory = so.isOrdered() ? _pushOrderedMessageManagerFactory : _pushMessageManagerFactory;
        mm = mmFactory.createMessageManager(conn, this, fnlStream, so, settledServerCC, false, dispatcher == null);
        subFactory = (sid, lSubject, lQgroup, lConn, lDispatcher) -> {
            NatsJetStreamSubscription nsub = new NatsJetStreamSubscription(sid, lSubject, lQgroup, lConn, lDispatcher, this, fnlStream, settledConsumerName, mm);
            if (lDispatcher == null) {
                nsub.setPendingLimits(so.getPendingMessageLimit(), so.getPendingByteLimit());
            }
            return nsub;
        };
    }
    NatsJetStreamSubscription sub;
    if (dispatcher == null) {
        sub = (NatsJetStreamSubscription) conn.createSubscription(fnlInboxDeliver, qgroup, null, subFactory);
    } else {
        AsyncMessageHandler handler = new AsyncMessageHandler(mm, userHandler, isAutoAck, settledServerCC);
        sub = (NatsJetStreamSubscription) dispatcher.subscribeImplJetStream(fnlInboxDeliver, qgroup, handler, subFactory);
    }
    // 7. The consumer might need to be created, do it here
    if (settledConsumerName == null) {
        _createConsumerUnsubscribeOnException(fnlStream, settledServerCC, sub);
    }
    return sub;
}

Method Description: createSubscription

This method is defined in the io.nats.client.impl.NatsJetStream class. It is used to create a JetStream subscription. The method takes various parameters and returns a JetStreamSubscription.

Method Signature

JetStreamSubscription createSubscription(String subject, String queueName, NatsDispatcher dispatcher, MessageHandler userHandler, boolean isAutoAck, PushSubscribeOptions pushSubscribeOptions, PullSubscribeOptions pullSubscribeOptions) throws IOException, JetStreamApiException

Parameters

  • subject: The subject for the subscription.
  • queueName: The name of the queue (optional).
  • dispatcher: The message dispatcher for the subscription (optional).
  • userHandler: The user-defined message handler.
  • isAutoAck: A boolean flag indicating whether the messages should be auto-acknowledged.
  • pushSubscribeOptions: Options for push subscriptions (optional).
  • pullSubscribeOptions: Options for pull subscriptions (optional).

Steps

  1. Prepare for validation

    • Check if the subscription is in pull mode or push mode.
    • Set the appropriate options and configurations based on the mode.
    • Validate that certain options are not supplied in pull mode.
    • Validate that certain options are not supplied in push mode.
  2. Flow Control / Heartbeat validation

    • If an idle heartbeat is set and greater than zero:
      • Throw an exception if the subscription is in pull mode.
      • Throw an exception if a consumer group is specified.
  3. Stream lookup

    • If the stream is not provided:
      • Look up the stream based on the subject.
      • Throw an exception if no matching stream is found.
  4. Consumer existence check

    • If a consumer name is provided:
      • Look up the consumer information in the server.
      • If the consumer exists:
        • Get the server-side consumer configuration.
        • Compare the user configuration with the server configuration.
        • Throw an exception if there are any changes in the configurations.
        • Validate that the deliver subject is appropriate for the subscription mode.
        • Validate that the consumer is not already bound or configured as a push/pull consumer.
      • If the consumer does not exist and binding is required:
        • Throw an exception indicating that the consumer is not found.
  5. Inbox creation

    • If the subscription is in pull mode or no deliver subject is provided:
      • Create an inbox for message delivery.
    • Otherwise, use the provided deliver subject.
  6. Consumer configuration and creation

    • If the consumer does not exist:
      • Build the consumer configuration based on the user configuration.
      • Set the deliver subject if not in pull mode.
      • Set the filter subject if it is not already set.
      • Set the deliver group.
    • Otherwise, use the settled server-side configuration and consumer name.
    • Create the message manager based on the subscription mode and other parameters.
    • Set the subscription factory based on the mode and message manager.
    • Create the JetStream subscription based on the provided parameters and subscription factory.
    • Set the pending limits if the message dispatcher is not provided.
    • Create and associate the asynchronous message handler if the dispatcher is provided.
    • Call the appropriate subscription creation method based on the presence of the dispatcher.
  7. Consumer creation

    • If the consumer was not found, create the consumer on the server and handle unsubscribe on exception.
  8. Return the created subscription

Note: The method throws IOException and JetStreamApiException in case of any errors.

The createSubscription method in the NatsJetStream class is used to create a subscription for consuming messages from a specified subject in JetStream.

Here is a high-level overview of what the method does:

  1. It performs validation based on the subscription options provided, such as whether it's a push or pull subscription and if any conflicting options are present.
  2. It checks if the consumer configuration already exists, and if it does, verifies that the configuration matches the provided options.
  3. If the consumer does not exist or if it is a new consumer, it creates a settled configuration based on the provided options.
  4. It determines the deliver subject for the subscription, either by creating a new inbox or using the existing deliver subject.
  5. Based on whether it is a push or pull subscription, it creates the appropriate type of subscription with the necessary message manager implementation.
  6. If a dispatcher is provided, it subscribes to the deliver subject using the dispatcher's subscribeImplJetStream method. Otherwise, it directly creates a subscription using the connection's createSubscription method.
  7. If a new consumer was created, it calls the _createConsumerUnsubscribeOnException method to handle any errors and unsubscribe from the subscription in case of an exception.
  8. Finally, it returns the created subscription object.

Please note that this is a high-level description and the method may have more intricate details and mechanisms not covered in this summary.

private boolean isFilterMatch(String subscribeSubject, String filterSubject, String stream) throws IOException, JetStreamApiException

private boolean isFilterMatch(String subscribeSubject, String filterSubject, String stream) throws IOException, JetStreamApiException {
    // subscribeSubject guaranteed to not be null
    // filterSubject may be null or empty or have value
    if (subscribeSubject.equals(filterSubject)) {
        return true;
    }
    if (nullOrEmpty(filterSubject) || filterSubject.equals(">")) {
        // lookup stream subject returns null if there is not exactly one subject
        String streamSubject = lookupStreamSubject(stream);
        return subscribeSubject.equals(streamSubject);
    }
    return false;
}

Method Description: isFilterMatch

Overview

This method is used to determine if a filter subject matches with a given subscribe subject. It takes three parameters - subscribeSubject, filterSubject, and stream.

Step-by-step Description

  1. Check if subscribeSubject is equal to filterSubject:

    • If true, return true (indicating a match).
    • If false, proceed to the next step.
  2. Check if filterSubject is either null, empty, or equals to >.

    • If true, perform a lookup of the stream subject by calling the lookupStreamSubject method with the stream parameter.
      • If the lookup returns null (indicating there is not exactly one subject), return false (indicating there is no match).
      • If the lookup returns a non-null value, compare it with subscribeSubject.
        • If they are equal, return true (indicating a match).
        • If they are not equal, return false (indicating no match).
    • If false, return false (indicating no match).
  3. Return false as a default fallback (indicating no match).

The isFilterMatch method is a private method defined in the NatsJetStream class. It takes three parameters: subscribeSubject, filterSubject, and stream. The method checks whether the subscribeSubject is a match for the filterSubject based on certain conditions.

First, it checks if the subscribeSubject is equal to the filterSubject. If they are equal, it returns true.

Next, it checks if the filterSubject is null, empty, or has the value ">". If any of these conditions is true, it calls the lookupStreamSubject method to get the streamSubject and then checks if the subscribeSubject is equal to the streamSubject. If they are equal, it returns true.

If none of the above conditions are met, it returns false.

This method is used to determine whether a given subject should be filtered based on the subscription subject and the filter subject in the context of the NatsJetStream implementation.

sequence diagram

@Override

public JetStreamSubscription subscribe(String subject, String queue, PushSubscribeOptions options) throws IOException, JetStreamApiException

@Override
public JetStreamSubscription subscribe(String subject, String queue, PushSubscribeOptions options) throws IOException, JetStreamApiException {
    validateSubject(subject, isSubjectRequired(options));
    queue = emptyAsNull(validateQueueName(queue, false));
    return createSubscription(subject, queue, null, null, false, options, null);
}

The subscribe method in the NatsJetStream class is used to create a subscription to a subject in the NATS JetStream server.

Here is a step-by-step description of what the method does based on its code:

  1. Validates the subject parameter to ensure it is not null or empty, and checks if it is required based on the options parameter.
  2. Validates the queue parameter to ensure it is not empty and converts any empty strings to null if allowed.
  3. Calls the createSubscription method with the following parameters:
    • subject: The subject to subscribe to.
    • queue: The queue to receive messages from (can be null).
    • null (placeholder): Placeholder parameter for the durableName parameter that is not used in this method.
    • null (placeholder): Placeholder parameter for the deliverStartTime parameter that is not used in this method.
    • false: The manualAcks parameter that specifies whether to manually acknowledge messages.
    • options: The PushSubscribeOptions object that contains additional options for the subscription.
    • null (placeholder): Placeholder parameter for the flowControl parameter that is not used in this method.
  4. Returns the JetStreamSubscription object representing the subscription.

This method handles exceptions such as IOException and JetStreamApiException that can be thrown during the subscription process.

The subscribe method in the NatsJetStream class is used to create a subscription for consuming messages from a NATS JetStream.

This method takes three parameters: subject (the subject to subscribe to), queue (an optional queue group name for load balancing), and options (options for configuring the subscription).

Firstly, the method validates the subject and checks if it is required based on the options. If the queue parameter is empty, it is set to null. Then, the method calls the createSubscription method, passing the subject, queue, and other necessary parameters to create the subscription. Finally, the method returns a JetStreamSubscription object representing the created subscription.

If any exceptions occur during the execution of this method, such as an IOException or a JetStreamApiException, they are thrown.

sequence diagram

@Override

public JetStreamSubscription subscribe(String subject, Dispatcher dispatcher, MessageHandler handler, boolean autoAck) throws IOException, JetStreamApiException

@Override
public JetStreamSubscription subscribe(String subject, Dispatcher dispatcher, MessageHandler handler, boolean autoAck) throws IOException, JetStreamApiException {
    validateSubject(subject, true);
    validateNotNull(dispatcher, "Dispatcher");
    validateNotNull(handler, "Handler");
    return createSubscription(subject, null, (NatsDispatcher) dispatcher, handler, autoAck, null, null);
}

The subscribe method in the NatsJetStream class is used to create a JetStream subscription on a specified subject. It takes in the following parameters:

  1. subject (String): The subject to subscribe to.
  2. dispatcher (Dispatcher): An instance of a dispatcher responsible for delivering messages to the handler.
  3. handler (MessageHandler): An instance of the message handler responsible for processing incoming messages.
  4. autoAck (boolean): A flag indicating whether messages should be automatically acknowledged.

The method performs the following steps:

  1. Validate the subject to ensure it is not null or empty. If it fails validation, throw an IllegalArgumentException.
  2. Validate the dispatcher to ensure it is not null. If it fails validation, throw an IllegalArgumentException.
  3. Validate the message handler to ensure it is not null. If it fails validation, throw an IllegalArgumentException.
  4. Create a new subscription by invoking the createSubscription method and passing the subject, null, the dispatcher casted to NatsDispatcher, the handler, the autoAck flag, and two null values as arguments.
  5. Return the newly created subscription.

Note: The implementation of createSubscription is not provided in the given snippet, so the exact details of how the subscription is created are not available.

The subscribe method is used to create a subscription for consuming messages from a subject in the NATS JetStream messaging system.

It takes the following parameters:

  • subject: The subject to subscribe to.
  • dispatcher: A dispatcher object responsible for dispatching incoming messages to the appropriate handler.
  • handler: A message handler object that processes incoming messages.
  • autoAck: A boolean flag indicating whether the subscription should automatically acknowledge messages after they have been successfully processed.

The method first performs some validation checks on the subject, dispatcher, and handler parameters to ensure they are not null. Then, it calls the createSubscription method internally to create the actual subscription object and returns it.

Overall, the method provides a convenient way to create a subscription for consuming messages from a subject in the NATS JetStream system, by specifying the subject, dispatcher, handler, and auto-acknowledgment settings.

sequence diagram

@Override

public JetStreamSubscription subscribe(String subject, Dispatcher dispatcher, MessageHandler handler, boolean autoAck, PushSubscribeOptions options) throws IOException, JetStreamApiException

@Override
public JetStreamSubscription subscribe(String subject, Dispatcher dispatcher, MessageHandler handler, boolean autoAck, PushSubscribeOptions options) throws IOException, JetStreamApiException {
    validateSubject(subject, isSubjectRequired(options));
    validateNotNull(dispatcher, "Dispatcher");
    validateNotNull(handler, "Handler");
    return createSubscription(subject, null, (NatsDispatcher) dispatcher, handler, autoAck, options, null);
}

Method: subscribe

This method is defined in the io.nats.client.impl.NatsJetStream class. It takes in the following parameters:

  • subject (String): The subject to subscribe to.
  • dispatcher (Dispatcher): The dispatcher to use for message handling.
  • handler (MessageHandler): The handler for processing received messages.
  • autoAck (boolean): Flag indicating whether messages should be automatically acknowledged.
  • options (PushSubscribeOptions): Optional options for configuring the subscription.

The method performs the following steps:

  1. Validate the subject parameter by calling the validateSubject method, passing in the subject and the result of the isSubjectRequired method called with the options parameter. This ensures that the subject is valid and meets any configured requirements.

  2. Validate that the dispatcher parameter is not null by calling the validateNotNull method with the dispatcher and the string "Dispatcher" as arguments. This ensures that a valid dispatcher is provided for message handling.

  3. Validate that the handler parameter is not null by calling the validateNotNull method with the handler and the string "Handler" as arguments. This ensures that a valid message handler is provided for processing received messages.

  4. Call the createSubscription method, passing in the subject, null for the queueGroup parameter, the dispatcher cast to NatsDispatcher, the handler, the autoAck flag, the options, and null for the jsSub parameter. This method creates and returns a JetStreamSubscription object based on the provided parameters.

  5. Return the JetStreamSubscription object created in the previous step.

Note: This method can throw an IOException or a JetStreamApiException, which may occur during the validation or subscription creation process.

The subscribe() method in the NatsJetStream class is used to subscribe to a subject in NATS JetStream.

It takes in the following parameters:

  • subject (String): The subject to subscribe to.
  • dispatcher (Dispatcher): An object that dispatches incoming messages to the appropriate handler.
  • handler (MessageHandler): An object that handles incoming messages.
  • autoAck (boolean): A flag indicating whether the subscription should automatically acknowledge the messages.
  • options (PushSubscribeOptions): Additional options for the subscription.

The method first validates the subject and the dispatcher to ensure they are not null. It then creates a subscription using the createSubscription() method, passing in the subject, null for the interest, the dispatcher, the handler, autoAck flag, options, and null for the flow control options.

The method returns a JetStreamSubscription object representing the subscription.

sequence diagram

@Override

public JetStreamSubscription subscribe(String subject, String queue, Dispatcher dispatcher, MessageHandler handler, boolean autoAck, PushSubscribeOptions options) throws IOException, JetStreamApiException

@Override
public JetStreamSubscription subscribe(String subject, String queue, Dispatcher dispatcher, MessageHandler handler, boolean autoAck, PushSubscribeOptions options) throws IOException, JetStreamApiException {
    validateSubject(subject, isSubjectRequired(options));
    queue = emptyAsNull(validateQueueName(queue, false));
    validateNotNull(dispatcher, "Dispatcher");
    validateNotNull(handler, "Handler");
    return createSubscription(subject, queue, (NatsDispatcher) dispatcher, handler, autoAck, options, null);
}

The subscribe method in the NatsJetStream class is used to create a JetStream subscription for consuming messages from a subject. Here is a step-by-step description of what the method does based on its body:

  1. Validates the subject parameter to ensure it is not null and meets the requirements specified by the options parameter.
  2. Validates the queue parameter, which represents the name of the consumer group queue, to ensure it is not empty and returns null if it is empty.
  3. Validates the dispatcher parameter, which is responsible for dispatching messages to the handler, to ensure it is not null.
  4. Validates the handler parameter, which is responsible for handling incoming messages, to ensure it is not null.
  5. Calls the createSubscription method with the validated parameters to create a JetStream subscription.
  6. Returns the created JetStream subscription.

The subscribe method throws an IOException if there is an error during the subscription creation process and a JetStreamApiException if there is an error with the JetStream API.

The subscribe method in the io.nats.client.impl.NatsJetStream class is used to create a subscription to a JetStream in NATS.

It takes the following parameters:

  • subject: The subject to subscribe to.
  • queue: The optional queue group name for the subscription.
  • dispatcher: The dispatcher used for message dispatching.
  • handler: The handler for processing received messages.
  • autoAck: A flag indicating if the messages should be automatically acknowledged.
  • options: Additional options for the subscription.

The method begins by validating the subject and queue parameters. It ensures that the subject is not empty and checks if the queue name is provided or not.

Next, it checks that both the dispatcher and handler objects are not null, as they are required to process the received messages.

Finally, it calls the createSubscription method with the validated parameters to actually create the subscription and returns the resulting JetStreamSubscription object.

If any errors occur during the subscription creation, such as IO exceptions or JetStream API exceptions, they are caught and thrown.

sequence diagram

@Override

public JetStreamSubscription subscribe(String subject, PullSubscribeOptions options) throws IOException, JetStreamApiException

@Override
public JetStreamSubscription subscribe(String subject, PullSubscribeOptions options) throws IOException, JetStreamApiException {
    validateSubject(subject, isSubjectRequired(options));
    validateNotNull(options, "Pull Subscribe Options");
    return createSubscription(subject, null, null, null, false, null, options);
}

The subscribe method in the NatsJetStream class is responsible for creating a JetStream subscription. Here is a step-by-step breakdown of what the method does based on the provided body:

  1. The method overrides the subscribe method from the parent class.
  2. The method takes two parameters: subject (the subject to subscribe to) and options (the pull subscription options).
  3. Before proceeding, the method validates the subject by calling the validateSubject method, passing the subject and a check if the subject is required based on the options.
  4. The method then validates that the options are not null, using the validateNotNull method, passing the options and a descriptive error message.
  5. Finally, the method calls the createSubscription method, passing various parameters:
    • The subject parameter provided to the subscribe method.
    • null for the Queue parameter.
    • null for the Durable parameter.
    • null for the DeliverSubject parameter.
    • false for the NoAck parameter.
    • null for the AckWait parameter.
    • The options parameter provided to the subscribe method.
  6. The method returns the result of the createSubscription method, which is a JetStreamSubscription.

Overall, the subscribe method validates the subject and options parameters, and then calls another method to create and return a JetStream subscription based on the provided parameters.

The subscribe method in the NatsJetStream class, defined in io.nats.client.impl, is used to create a subscription to consume messages from a JetStream subject using pull-based consumption.

The method takes two parameters:

  • subject: The subject (topic) to subscribe to.
  • options: The pull subscription options, which customize the behavior of the subscription.

The method first performs some input validation, ensuring that the subject is valid and that the options are not null. If the validation passes, it then calls the createSubscription method internally to create and return a JetStreamSubscription object.

The returned JetStreamSubscription is the subscription that can be used to receive messages from the specified subject using pull-based consumption.

Possible exceptions that can be thrown by the method include IOException (in case of an I/O error) and JetStreamApiException (if there is an error related to JetStream API).

Note that the provided code only shows the method body, and the implementation of the createSubscription method is not provided.

sequence diagram

@Override

public JetStreamSubscription subscribe(String subject, Dispatcher dispatcher, MessageHandler handler, PullSubscribeOptions options) throws IOException, JetStreamApiException

@Override
public JetStreamSubscription subscribe(String subject, Dispatcher dispatcher, MessageHandler handler, PullSubscribeOptions options) throws IOException, JetStreamApiException {
    validateSubject(subject, isSubjectRequired(options));
    validateNotNull(dispatcher, "Dispatcher");
    validateNotNull(handler, "Handler");
    validateNotNull(options, "Pull Subscribe Options");
    return createSubscription(subject, null, (NatsDispatcher) dispatcher, handler, false, null, options);
}

The subscribe method is defined in the class io.nats.client.impl.NatsJetStream. It takes four parameters: subject, dispatcher, handler, and options.

  1. The method first calls the validateSubject method to ensure that the subject parameter is valid. It checks if the subject is required based on the options parameter by calling the isSubjectRequired method.
  2. It then calls the validateNotNull method for the dispatcher, handler, and options parameters to ensure that they are not null.
  3. Finally, it calls the createSubscription method to create a JetStream subscription. It passes the subject, null, the dispatcher casted to NatsDispatcher, the handler and the options parameters to the createSubscription method. The last two parameters in the createSubscription method call are false and null.

The method returns a JetStreamSubscription object obtained from the createSubscription method call.

The subscribe method in class NatsJetStream is used to create a subscription to a given subject in the NATS JetStream messaging system.

It takes in the following parameters:

  • subject (string): The subject to subscribe to.
  • dispatcher (Dispatcher): The dispatcher to use for delivering messages.
  • handler (MessageHandler): The handler to process incoming messages.
  • options (PullSubscribeOptions): The options for the subscription.

This method first performs some validations on the subject, dispatcher, handler, and options to ensure they are not null.

Then, it calls the createSubscription method to create the subscription. It passes the subject, dispatcher, handler, and options to the createSubscription method, along with some additional parameters.

Finally, it returns the created JetStreamSubscription object.

sequence diagram

@Override

public ConsumerContext consumerContext(String streamName, String consumerName) throws IOException, JetStreamApiException

@Override
public ConsumerContext consumerContext(String streamName, String consumerName) throws IOException, JetStreamApiException {
    Validator.validateStreamName(streamName, true);
    Validator.required(consumerName, "Consumer Name");
    return getNatsStreamContext(streamName).consumerContext(consumerName);
}

The consumerContext method in the NatsJetStream class is used to retrieve a ConsumerContext object based on the provided streamName and consumerName. Here is a step-by-step description of what this method does:

  1. The method is declared with the @Override annotation, indicating that it overrides a method in a superclass or interface.

  2. The validateStreamName method from the Validator class is called with the streamName as an argument to check if it is valid. The true boolean value indicates that an exception should be thrown if the stream name is invalid.

  3. The required method from the Validator class is called with the consumerName and a string value "Consumer Name" as arguments to check if the consumer name is not null or empty. If it is null or empty, an exception is thrown.

  4. The getNatsStreamContext method is called with the streamName as an argument to retrieve the corresponding NatsStreamContext object.

  5. The consumerContext method is called on the NatsStreamContext object returned in the previous step with the consumerName as an argument. This method retrieves the ConsumerContext object associated with the specified consumer name.

  6. The retrieved ConsumerContext object is returned as the result of the method.

Note: The method includes throws declarations for IOException and JetStreamApiException, indicating that it can throw these exceptions during its execution.

The consumerContext method is a part of the NatsJetStream class in the io.nats.client.impl package. It takes two parameters streamName and consumerName, both of type String.

The method first validates the streamName parameter using the validateStreamName method from the Validator class, ensuring that it is not empty or null.

Next, it checks that the consumerName parameter is not empty or null, using the required method from the Validator class.

Finally, it calls the consumerContext method on the NatsStreamContext object obtained from the getNatsStreamContext method. This method returns a ConsumerContext object, which is then returned by the consumerContext method.

Overall, this method is responsible for validating the input parameters and returning the consumer context for the specified stream and consumer.

sequence diagram

StreamInfoReader

StreamInfoReader

The StreamInfoReader class is responsible for reading information about a stream. It provides methods to retrieve various details such as the stream's name, duration, and format. This class acts as a utility for obtaining relevant information about a stream in a convenient manner.

void process(Message msg) throws JetStreamApiException

void process(Message msg) throws JetStreamApiException {
    engine = new ListRequestEngine(msg);
    StreamInfo si = new StreamInfo(msg);
    if (streamInfo == null) {
        streamInfo = si;
    } else {
        streamInfo.getStreamState().getSubjects().addAll(si.getStreamState().getSubjects());
    }
}

Method: process(Message msg)

This method is defined in the class io.nats.client.impl.StreamInfoReader. It is responsible for processing a message and updating the stream information.

Parameters

  • msg: The message to be processed.

Steps

  1. Create a new ListRequestEngine object named engine by passing the msg as a parameter.
  2. Create a new StreamInfo object named si by passing the msg as a parameter.
  3. Check if the variable streamInfo is null.
    • If it is null, assign the value of si to streamInfo.
    • If it is not null, add all the subjects from si.getStreamState().getSubjects() to streamInfo.getStreamState().getSubjects().

The process method in the io.nats.client.impl.StreamInfoReader class is responsible for processing a message received by the reader.

  1. First, a new ListRequestEngine is created using the received message.
  2. Then, a StreamInfo object is created based on the received message.
  3. If the streamInfo object is currently null, the streamInfo variable is set to the newly created si object.
  4. If the streamInfo object is not null, the subjects from the si object's stream state are added to the subjects of the existing streamInfo object.

In summary, the process method processes a message by creating a ListRequestEngine and a StreamInfo object, and updates the streamInfo object with the received message's stream state subjects.

sequence diagram

byte[] nextJson(StreamInfoOptions options)

byte[] nextJson(StreamInfoOptions options) {
    StringBuilder sb = beginJson();
    addField(sb, "offset", engine.nextOffset());
    if (options != null) {
        addField(sb, SUBJECTS_FILTER, options.getSubjectsFilter());
        addFldWhenTrue(sb, DELETED_DETAILS, options.isDeletedDetails());
    }
    return endJson(sb).toString().getBytes();
}

The nextJson method in the StreamInfoReader class is used to return the JSON representation of the next stream information entry based on the given options.

Here is a step-by-step description of what the method does:

  1. Initialize a StringBuilder named sb by calling the beginJson method.
  2. Add the "offset" field to the sb with the value obtained from calling the nextOffset method on the engine object.
  3. Check if the options parameter is not null.
  4. If options is not null:
    • Add the "subjects_filter" field to the sb with the value obtained from calling the getSubjectsFilter method on the options object.
    • Check if the isDeletedDetails method on the options object returns true.
    • If it does, add the "deleted_details" field to the sb.
  5. Call the endJson method on the sb to finalize the JSON representation.
  6. Convert the sb to a String and then get the corresponding byte representation as a byte[].
  7. Return the byte array.

The nextJson method in the StreamInfoReader class is responsible for generating a JSON representation of the next stream information.

Here is a brief description of what this method does:

  • Creates a StringBuilder to build the JSON string.
  • Adds the "offset" field to the JSON string by calling the nextOffset method of the underlying engine.
  • Checks if the options parameter is not null:
    • If not null, adds the "subjectsFilter" field to the JSON string by calling the getSubjectsFilter method of the options parameter.
    • Adds the "deletedDetails" field to the JSON string by calling the isDeletedDetails method of the options parameter.
  • Returns the JSON string representation as a byte array.

This method is used to get the next JSON representation of the stream information, including offset, subjects filter, and deleted details if specified by the options parameter.

sequence diagram

FileAuthHandler

FileAuthHandler

The FileAuthHandler class is an implementation of the AuthHandler interface. It provides functionality for handling authentication in a file-based system.

private char[] extract(CharBuffer data, int headers)

private char[] extract(CharBuffer data, int headers) {
    CharBuffer buff = CharBuffer.allocate(data.length());
    boolean skipLine = false;
    int headerCount = 0;
    int linePos = -1;
    while (data.length() > 0) {
        char c = data.get();
        linePos++;
        // End of line, either we got it, or we should keep reading the new line
        if (c == '\n' || c == '\r') {
            if (buff.position() > 0) {
                // we wrote something
                break;
            }
            skipLine = false;
            // so we can start right up
            linePos = -1;
            continue;
        }
        // skip to the new line
        if (skipLine) {
            continue;
        }
        // Ignore whitespace
        if (Character.isWhitespace(c)) {
            continue;
        }
        // If we are on a - skip that line, bump the header count
        if (c == '-' && linePos == 0) {
            skipLine = true;
            headerCount++;
            continue;
        }
        // Skip the line, or add to buff
        if (!skipLine && headerCount == headers) {
            buff.put(c);
        }
    }
    // check for naked value
    if (buff.position() == 0 && headers == 1) {
        data.position(0);
        while (data.length() > 0) {
            char c = data.get();
            if (c == '\n' || c == '\r' || Character.isWhitespace(c)) {
                if (buff.position() > 0) {
                    // we wrote something
                    break;
                }
                continue;
            }
            buff.put(c);
        }
        buff.flip();
    } else {
        buff.flip();
    }
    char[] retVal = new char[buff.length()];
    buff.get(retVal);
    buff.clear();
    for (int i = 0; i < buff.capacity(); i++) {
        buff.put('\0');
    }
    return retVal;
}
```### private char[] readKeyChars() throws IOException 
```java
private char[] readKeyChars() throws IOException {
    char[] keyChars = null;
    if (this.credsFile != null) {
        byte[] data = Files.readAllBytes(Paths.get(this.credsFile));
        ByteBuffer bb = ByteBuffer.wrap(data);
        CharBuffer chars = StandardCharsets.UTF_8.decode(bb);
        // we are 2nd so 3 headers
        keyChars = this.extract(chars, 3);
        // Clear things up as best we can
        chars.clear();
        for (int i = 0; i < chars.capacity(); i++) {
            chars.put('\0');
        }
        for (int i = 0; i < data.length; i++) {
            data[i] = 0;
        }
    } else {
        byte[] data = Files.readAllBytes(Paths.get(this.nkeyFile));
        ByteBuffer bb = ByteBuffer.wrap(data);
        CharBuffer chars = StandardCharsets.UTF_8.decode(bb);
        keyChars = this.extract(chars, 1);
        // Clear things up as best we can
        chars.clear();
        for (int i = 0; i < chars.capacity(); i++) {
            chars.put('\0');
        }
        for (int i = 0; i < data.length; i++) {
            data[i] = 0;
        }
    }
    return keyChars;
}
```### public byte[] sign(byte[] nonce) 
```java
public byte[] sign(byte[] nonce) {
    try {
        char[] keyChars = this.readKeyChars();
        NKey nkey = NKey.fromSeed(keyChars);
        byte[] sig = nkey.sign(nonce);
        nkey.clear();
        return sig;
    } catch (Exception exp) {
        throw new IllegalStateException("problem signing nonce", exp);
    }
}

The sign method in the class io.nats.client.impl.FileAuthHandler is used to sign a given byte array using a secret key stored in a file.

Here is a step-by-step description of what the method does:

  1. Reads the secret key stored in the file as characters and stores them in a char array called keyChars.

  2. Converts the keyChars array into an NKey object called nkey using the fromSeed method of the NKey class. The fromSeed method takes the characters of the secret key and creates a NKey object, which represents a cryptographic key derived from the seed.

  3. The nkey.sign(nonce) method is called to sign the given nonce byte array using the key stored in the nkey object. This method internally uses a cryptographic algorithm to generate a digital signature based on the key and the nonce.

  4. The nkey.clear() method is called to clear the internal state of the nkey object. This is done to remove any sensitive information from memory after the signing process.

  5. The generated signature is returned as a byte array.

  6. If any exception occurs during the signing process, an IllegalStateException is thrown with the message "problem signing nonce". The original exception is wrapped and included as the cause of the IllegalStateException.

The sign method in the FileAuthHandler class is responsible for signing the provided nonce bytes using a private key stored in a file.

First, the method reads the private key characters from the file using the readKeyChars method.

Then, it creates an NKey object from the private key characters.

Next, it uses the sign method of the NKey object to sign the nonce bytes.

After signing, it clears the private key from memory using the clear method of the NKey object.

Finally, it returns the signature as a byte array.

If any exception occurs during the signing process, it throws an IllegalStateException with an appropriate error message.

sequence diagram

NatsSubscription

NatsSubscription

The NatsSubscription class extends the NatsConsumer class and implements the Subscription interface. It represents a subscription to a NATS messaging system. This class provides functionality to subscribe to topics and receive messages from the NATS server. Additionally, it inherits properties and methods from the NatsConsumer class, allowing for efficient message consumption and processing.

void reSubscribe(String newDeliverSubject)

void reSubscribe(String newDeliverSubject) {
    connection.sendUnsub(this, 0);
    if (dispatcher == null) {
        connection.remove(this);
        sid = connection.reSubscribe(this, newDeliverSubject, queueName);
    } else {
        MessageHandler handler = dispatcher.getSubscriptionHandlers().get(sid);
        dispatcher.remove(this);
        sid = dispatcher.reSubscribe(this, newDeliverSubject, queueName, handler);
    }
    subject = newDeliverSubject;
}

The reSubscribe method in the NatsSubscription class is responsible for resubscribing the subscription to a new deliver subject.

Here is a step-by-step description of what the method does:

  1. The method starts by sending an unsubscribe request using the sendUnsub method of the connection associated with the subscription. This unsubscribes the subscription from the current subject it was subscribed to.

  2. After unsubscribing, the method checks if the subscription has a dispatcher assigned to it. The dispatcher is responsible for handling incoming messages for the subscription.

  3. If the subscription does not have a dispatcher, it means it is a single-subscription and not part of a subscription group. In this case, the subscription is removed from the connection using the remove method.

  4. The method then proceeds to re-subscribe the subscription to the new deliver subject using the reSubscribe method of the connection. This method takes the subscription, the new deliver subject, and the existing queue name (if any) as parameters. The reSubscribe method returns the new subscription ID (sid).

  5. If the subscription has a dispatcher assigned to it, it means it is part of a subscription group. In this case, the method retrieves the message handler associated with the subscription using the getSubscriptionHandlers method of the dispatcher.

  6. The method then removes the subscription from the dispatcher using the remove method.

  7. Finally, the method re-subscribes the subscription to the new deliver subject using the reSubscribe method of the dispatcher. This method takes the subscription, the new deliver subject, the existing queue name (if any), and the message handler as parameters. The reSubscribe method returns the new subscription ID (sid).

  8. The method updates the subscription's subject with the new deliver subject.

That concludes the step-by-step description of the reSubscribe method.

The reSubscribe method in the NatsSubscription class is used to resubscribe the subscription to a new deliver subject.

It first sends an unsubscribe message to the NATS server for the current subscription using the connection.sendUnsub method.

If the dispatcher object is null, it means that the subscription is standalone and not part of a dispatcher. In this case, it removes the subscription from the connection's list of active subscriptions using connection.remove, and then resubscribes the subscription to the new deliver subject by calling connection.reSubscribe method with the new deliver subject and the existing queue name.

If the dispatcher object is not null, it means that the subscription is part of a dispatcher. In this case, it looks up the message handler associated with the subscription using dispatcher.getSubscriptionHandlers().get(sid). Then it removes the subscription from the dispatcher's list of active subscriptions using dispatcher.remove, and finally resubscribes the subscription to the new deliver subject by calling dispatcher.reSubscribe with the new deliver subject, the existing queue name, and the retrieved message handler.

After the resubscription, the subject instance variable is updated with the new deliver subject.

sequence diagram

void invalidate()

void invalidate() {
    if (this.incoming != null) {
        this.incoming.pause();
    }
    this.dispatcher = null;
    this.incoming = null;
}

Method: invalidate()

This method is defined in the class io.nats.client.impl.NatsSubscription and is responsible for invalidating the subscription.

Step-by-Step Description:

  1. Check if the incoming object is not null:

    • If incoming is not null, proceed to the next step.
    • If incoming is null, skip to step 4.
  2. Call the pause() method on the incoming object:

    • incoming.pause() is called to pause the incoming messages on the subscription.
  3. Set both the dispatcher and incoming objects to null:

    • dispatcher is set to null so that no further dispatching of messages can occur.
    • incoming is set to null so that the subscription no longer holds a reference to incoming messages.
  4. The method execution is completed.

The invalidate method in the NatsSubscription class, defined in the io.nats.client.impl package, is used to mark the subscription as invalid.

When called, the method checks if the incoming field is not null. If it is not null, it pauses the incoming object, which is most likely some kind of message or data receiver for the subscription.

Next, it sets both the dispatcher and incoming fields to null thereby invalidating the subscription. This means that any further messages or data for this subscription will be ignored.

Overall, the invalidate method is responsible for pausing incoming messages and invalidating the subscription by setting relevant fields to null.

sequence diagram

protected NatsMessage nextMessageInternal(Duration timeout) throws InterruptedException

protected NatsMessage nextMessageInternal(Duration timeout) throws InterruptedException {
    if (this.dispatcher != null) {
        throw new IllegalStateException("Subscriptions that belong to a dispatcher cannot respond to nextMessage directly.");
    } else if (this.incoming == null) {
        throw new IllegalStateException("This subscription is inactive.");
    }
    NatsMessage msg = incoming.pop(timeout);
    if (this.incoming == null || !this.incoming.isRunning()) {
        // We were unsubscribed while waiting
        throw new IllegalStateException("This subscription became inactive.");
    }
    if (msg != null) {
        this.incrementDeliveredCount();
    }
    if (this.reachedUnsubLimit()) {
        this.connection.invalidate(this);
    }
    return msg;
}

The method nextMessageInternal is part of the class io.nats.client.impl.NatsSubscription and is used to retrieve the next message from the subscription's incoming queue. Here is a step-by-step description of what this method does:

  1. Check if the subscription has a dispatcher assigned to it. If it does, it means that the subscription belongs to a dispatcher and cannot respond to nextMessage directly. If this condition is true, an IllegalStateException is thrown.

  2. Check if the incoming queue is null. If it is, it means that this subscription is inactive and cannot retrieve the next message. If this condition is true, an IllegalStateException is thrown.

  3. Use the pop method of the incoming queue to retrieve the next message. The pop method blocks until a message is available or until the specified timeout duration has passed.

  4. Check if the incoming queue is null or not running anymore. If it is null or not running, it means that the subscription was unsubscribed while waiting for the next message. If this condition is true, an IllegalStateException is thrown.

  5. If a message was retrieved successfully (i.e., msg is not null), increment the delivered count of the subscription.

  6. Check if the subscription has reached the unsubscribe limit. If it has, it means that the subscription should be invalidated by the connection. The connection's invalidate method is called with the current subscription as a parameter.

  7. Finally, return the retrieved message (msg) to the caller.

Note: The timeout parameter is used to specify the maximum time to wait for a message. It is of type Duration, which represents a duration-based amount of time. If no message is available within the specified timeout, the pop method will return null.

The nextMessageInternal method in the NatsSubscription class is used to retrieve the next message that has been received by the subscription.

Here's a breakdown of what the method does:

  1. It first checks if the subscription belongs to a dispatcher. If it does, an IllegalStateException is thrown because subscriptions belonging to a dispatcher cannot respond to nextMessage directly.

  2. It then checks if the subscription is active. If it is not, an IllegalStateException is thrown indicating that the subscription is inactive.

  3. The method then waits for the next message to be received, using the specified timeout duration.

  4. After waiting for a message, it checks if the subscription is still active. If it has become inactive during the waiting period, an IllegalStateException is thrown.

  5. If a message is received, the method increments the delivered count of the subscription.

  6. It then checks if the subscription has reached the unsubscribe limit. If it has, the associated connection is invalidated.

  7. Finally, the method returns the received message.

Overall, the nextMessageInternal method ensures that the subscription is active, waits for the next message, handles subscription and connection state changes, and returns the received message.

sequence diagram

@Override

public void unsubscribe()

@Override
public void unsubscribe() {
    if (this.dispatcher != null) {
        throw new IllegalStateException("Subscriptions that belong to a dispatcher cannot respond to unsubscribe directly.");
    } else if (this.incoming == null) {
        throw new IllegalStateException("This subscription is inactive.");
    }
    if (isDraining()) {
        // No op while draining
        return;
    }
    this.connection.unsubscribe(this, -1);
}

The unsubscribe method in the class io.nats.client.impl.NatsSubscription is responsible for unsubscribing a subscription from a NATS server.

Here's a step-by-step description of what this method does based on its body:

  1. Check if the dispatcher object of the subscription is not null. If it is not null, it means that the subscription belongs to a dispatcher, and it is not allowed to respond to unsubscribe directly. In such a case, throw an IllegalStateException with the message "Subscriptions that belong to a dispatcher cannot respond to unsubscribe directly."

  2. If the incoming object of the subscription is null, it means that the subscription is inactive and cannot be unsubscribed. In this case, throw an IllegalStateException with the message "This subscription is inactive."

  3. Check if the subscription is currently in the process of draining messages. If it is, then there is no need to perform any action, so return without doing anything.

  4. If none of the above conditions are met, call the unsubscribe method of the connection object (which represents the NATS server connection) with the subscription object and -1 as parameters. This effectively unsubscribes the subscription from the NATS server.

That's the step-by-step description of what the unsubscribe method does based on its body.

The unsubscribe method in the NatsSubscription class is used to unsubscribe from a NATS subscription.

Here's a breakdown of what the method does:

  1. It first checks if the subscription is associated with a dispatcher. If it is, it throws an IllegalStateException with the message that subscriptions belonging to a dispatcher cannot directly respond to unsubscribe requests.

  2. It then checks if the subscription is inactive by checking if the incoming field is null. If it is, it throws an IllegalStateException with the message that the subscription is inactive.

  3. Next, it checks if the subscription is currently being drained. If it is, it simply returns and does nothing. This implies that the unsubscription request is disregarded while the subscription is being drained.

  4. If none of the above conditions are met, it proceeds to call the unsubscribe method on the underlying NATS connection, passing in the subscription instance and a timeout value of -1, indicating an immediate unsubscribe request.

sequence diagram

@Override

public Subscription unsubscribe(int after)

@Override
public Subscription unsubscribe(int after) {
    if (this.dispatcher != null) {
        throw new IllegalStateException("Subscriptions that belong to a dispatcher cannot respond to unsubscribe directly.");
    } else if (this.incoming == null) {
        throw new IllegalStateException("This subscription is inactive.");
    }
    if (isDraining()) {
        // No op while draining
        return this;
    }
    this.connection.unsubscribe(this, after);
    return this;
}

The unsubscribe method in the NatsSubscription class is used to unsubscribe from a NATS subscription.

Step-by-step description of the method:

  1. Check if the subscription has a dispatcher assigned to it:

    • If a dispatcher is assigned, it means that the subscription belongs to a dispatcher and cannot directly respond to an unsubscribe. In this case, an IllegalStateException is thrown with the error message "Subscriptions that belong to a dispatcher cannot respond to unsubscribe directly."
  2. Check if the subscription is inactive:

    • If the incoming field is null, it means that the subscription is inactive and cannot be unsubscribed. In this case, an IllegalStateException is thrown with the error message "This subscription is inactive."
  3. Check if the subscription is currently draining:

    • If the subscription is in the process of draining, meaning that it is in the process of shutting down, no additional unsubscribe operations can be performed. In this case, the method returns the current instance of the NatsSubscription object without performing any further action.
  4. If none of the above conditions are met, perform the unsubscribe operation:

    • The connection object associated with the subscription is used to perform the actual unsubscribe operation by invoking the unsubscribe method on it. The after parameter specified in the method call determines the number of server replies to wait for before considering the unsubscribe successful.
  5. Finally, return the current instance of the NatsSubscription object after performing the unsubscribe operation.

Please note that this description is based on the provided code snippet and may not capture all the possible scenarios or interactions with other parts of the codebase.

The unsubscribe method is used to unsubscribe from a NATS subscription. It takes an integer parameter after, which specifies the number of messages to allow before automatically unsubscribing.

If the subscription belongs to a dispatcher, it throws an IllegalStateException since subscriptions that belong to a dispatcher cannot directly respond to unsubscribe.

If the subscription is inactive (i.e., there is no incoming message), it also throws an IllegalStateException.

If the subscription is currently being drained (i.e., actively unsubscribing), the method returns itself (this) without performing any operation.

Otherwise, it calls the unsubscribe method on the connection associated with the subscription, passing in the subscription instance and the after parameter. Lastly, it returns itself (this).

sequence diagram

NatsServerPool

NatsServerPool

The NatsServerPool class is a public implementation of the ServerPool interface.

public void initialize(Options opts)

public void initialize(Options opts) {
    // 1. Hold on to options as we need them for settings
    options = opts;
    // 2. maxConnectAttempts accounts for the first connect attempt and also reconnect attempts
    maxConnectAttempts = options.getMaxReconnect() < 0 ? Integer.MAX_VALUE : options.getMaxReconnect() + 1;
    // 3. Add all the bootstrap to the server list and prepare list for next
    //    FYI bootstrap will always have at least the default url
    synchronized (listLock) {
        entryList = new ArrayList<>();
        for (NatsUri nuri : options.getNatsServerUris()) {
            // 1. If item is not found in the list being built, add to the list
            boolean notAlreadyInList = true;
            for (ServerPoolEntry entry : entryList) {
                if (nuri.equivalent(entry.nuri)) {
                    notAlreadyInList = false;
                    break;
                }
            }
            if (notAlreadyInList) {
                if (defaultScheme == null && !nuri.getScheme().equals(NatsConstants.NATS_PROTOCOL)) {
                    defaultScheme = nuri.getScheme();
                }
                entryList.add(new ServerPoolEntry(nuri, false));
            }
        }
        // 6. prepare list for next
        afterListChanged();
    }
}

The initialize method in the NatsServerPool class is responsible for setting up the server pool based on the provided options.

  1. Hold on to options as we need them for settings:

    • The options variable is assigned the value of the opts parameter, which holds the options for configuring the server pool.
  2. Calculate the maximum number of connect attempts:

    • The maxConnectAttempts variable is assigned a value based on the maxReconnect option from the options object.
    • If maxReconnect is less than 0, maxConnectAttempts is set to Integer.MAX_VALUE, indicating an infinite number of reconnect attempts.
    • Otherwise, maxConnectAttempts is set to maxReconnect + 1.
  3. Add all the bootstrap to the server list:

    • A synchronized block is used to ensure thread safety when modifying the entryList variable.
    • The entryList variable is initialized as a new ArrayList.
    • For each NatsUri in the natsServerUris option from the options object:
      1. Check if the item is not already in the list being built.
      2. If it is not already in the list, create a new ServerPoolEntry with the NatsUri and add it to the entryList.
      3. Additionally, if the defaultScheme is null and the current NatsUri has a scheme different from the NatsConstants.NATS_PROTOCOL, assign the scheme of the NatsUri to the defaultScheme.
  4. Prepare list for the next operation:

    • Call the afterListChanged method to handle any necessary updates or adjustments after the entryList has been modified.

This method initializes the server pool by storing the options, determining the maximum number of connect attempts, building the initial list of server entries based on the provided URIs, and performing any required preparations for subsequent operations.

The initialize method in the NatsServerPool class initializes the NATS server pool using the provided options. Here is a breakdown of what the method does:

  1. Stores the provided options internally for later use.
  2. Calculates the maximum number of allowed connection attempts based on the maximum reconnect value from the options.
  3. Populates the server list by adding all the bootstrap servers specified in the options. If a server is already in the list, it won't be added again.
  4. If a defaultScheme hasn't been set yet and the first server in the list has a non-default scheme, the defaultScheme is set to that scheme.
  5. Each server in the list is wrapped in a ServerPoolEntry object and added to the entryList.
  6. Once the server list has been updated, any necessary operations for handling the change in the list are performed.

sequence diagram

@Override

public boolean acceptDiscoveredUrls(List discoveredServers)

@Override
public boolean acceptDiscoveredUrls(List<String> discoveredServers) {
    // 1. If ignored discovered servers, don't do anything b/c never want
    //    anything but the explicit, which is already loaded.
    // 2. return false == no new servers discovered
    if (options.isIgnoreDiscoveredServers()) {
        return false;
    }
    synchronized (listLock) {
        // 2. Build a list for discovered
        //    - since we'll need the NatsUris later
        //    - and to have a list to use to prune removed gossiped servers
        List<NatsUri> discovered = new ArrayList<>();
        for (String d : discoveredServers) {
            try {
                discovered.add(new NatsUri(d, defaultScheme));
            } catch (URISyntaxException ignore) {
                // should never actually happen
            }
        }
        // 3. Start a new server list, loading in current order from the current list, and keeping
        //    - the last connected
        //    - all non-gossiped
        //    - any found in the new discovered list
        //      - for any new discovered, we also remove them from
        //        that list so step there are no dupes for step #4
        //      - This also maintains the Srv state of an already known discovered
        List<ServerPoolEntry> newEntryList = new ArrayList<>();
        for (ServerPoolEntry entry : entryList) {
            int ix = findEquivalent(discovered, entry.nuri);
            if (ix != -1 || entry.nuri.equals(lastConnected) || !entry.isGossiped) {
                newEntryList.add(entry);
                if (ix != -1) {
                    discovered.remove(ix);
                }
            }
        }
        // 4. Add all left over from the new discovered list
        boolean discoveryContainedUnknowns = false;
        if (discovered.size() > 0) {
            discoveryContainedUnknowns = true;
            for (NatsUri d : discovered) {
                newEntryList.add(new ServerPoolEntry(d, true));
            }
        }
        // 5. replace the list with the new one
        entryList = newEntryList;
        // 6. prepare list for next
        afterListChanged();
        // 7.
        return discoveryContainedUnknowns;
    }
}

The acceptDiscoveredUrls method in the NatsServerPool class is responsible for updating the server pool based on the newly discovered NATS server URLs.

Here is a step-by-step description of what the method does:

  1. It first checks if the option to ignore discovered servers is enabled. If it is, the method returns false, indicating that no new servers have been discovered.

  2. If the ignore option is not enabled, the method proceeds to update the server pool.

  3. Inside a synchronized block, the method creates a new list called discovered, which will be used to store the newly discovered server URIs as NatsUri objects (using the defaultScheme).

  4. For each URL in the discoveredServers list, the method attempts to create a new NatsUri object. If the URL is invalid and results in a URISyntaxException, the exception is ignored.

  5. After creating the discovered list, the method starts building a new list called newEntryList, which will replace the existing server pool list.

  6. For each entry in the existing server pool list (entryList), the method checks if the entry matches any of the discovered server URIs or the last connected URI, or if the entry is not gossiped.

  7. If a match is found or the entry is not gossiped, the entry is added to the newEntryList. If a match is found in the discovered list (discovered), that entry is removed from the discovered list to prevent duplicates in the next step.

  8. After iterating through all entries in the existing server pool list, the method checks if there are any leftover entries in the discovered list. If there are, it means that these entries were not matched with any existing entries and are considered newly discovered servers.

  9. For each remaining entry in the discovered list, a new ServerPoolEntry is created and added to the newEntryList.

  10. After adding all the necessary entries to the newEntryList, the method replaces the existing server pool list (entryList) with the newEntryList.

  11. The afterListChanged method is then called to perform any necessary operations after the server pool list has been updated.

  12. Finally, the method returns a boolean value, indicating whether any unknown servers were discovered during the process. This value is determined by checking if the size of the discovered list is greater than zero.

That's the step-by-step description of what the acceptDiscoveredUrls method does based on its body.

The acceptDiscoveredUrls method is used to update the list of servers in the NATS server pool based on new discovered URLs.

Here's a step-by-step breakdown of what the method does:

  1. If the option to ignore discovered servers is enabled, the method does nothing and returns false.

  2. If the ignore option is not enabled, the method proceeds and creates a synchronized block.

  3. Within the synchronized block, the method builds a new list of NatsUri objects based on the discovered server URLs provided as input.

  4. It then creates a new list called newEntryList to store the updated server pool entries. It iterates over the existing entryList and adds the entries that match the following conditions:

    • The entry has an equivalent NatsUri in the discovered list.
    • The entry is the last connected server.
    • The entry is a non-gossiped server.
  5. After adding the relevant entries from the existing list, the method checks if there are any remaining NatsUri objects in the discovered list. If there are, it iterates over them and adds new ServerPoolEntry objects to the newEntryList with the isGossiped flag set to true.

  6. The entryList is then replaced with the updated newEntryList.

  7. The afterListChanged() method is called to perform any necessary actions after the server list has been updated.

  8. Finally, the method returns true if the discovered list contained any unknown server URLs, indicating that new servers were added to the server pool. Otherwise, it returns false.

Overall, the acceptDiscoveredUrls method is responsible for accepting new discovered server URLs and updating the server pool accordingly.

sequence diagram

private void afterListChanged()

private void afterListChanged() {
    // 1. randomize if needed and allowed
    if (entryList.size() > 1 && !options.isNoRandomize()) {
        Collections.shuffle(entryList, ThreadLocalRandom.current());
    }
    // 2. calculate hasSecureServer and find the index of lastConnected
    hasSecureServer = false;
    int lastConnectedIx = -1;
    for (int ix = 0; ix < entryList.size(); ix++) {
        NatsUri nuri = entryList.get(ix).nuri;
        hasSecureServer |= nuri.isSecure();
        if (nuri.equals(lastConnected)) {
            lastConnectedIx = ix;
        }
    }
    // C. put the last connected server at the end of the list
    if (lastConnectedIx != -1) {
        entryList.add(entryList.remove(lastConnectedIx));
    }
}

The afterListChanged method in the NatsServerPool class performs the following steps:

  1. Check if the entryList has more than one element and if the noRandomize option is not set. If both conditions are met, randomize the order of the elements in the entryList using Collections.shuffle method and a ThreadLocalRandom generator.

  2. Calculate the hasSecureServer flag by iterating through each element in the entryList. Get the nuri (NatsUri) from the element and set hasSecureServer to true if any of the nuri is secure (isSecure() method returns true). Additionally, find the index of the lastConnected NatsUri in the entryList and store it in the lastConnectedIx variable.

  3. Reorder the entryList such that the element at the lastConnectedIx position is moved to the end of the list. This is done using the add and remove methods of entryList, ensuring that the last connected server is now at the end of the list.

The method afterListChanged is responsible for performing some operations after the list of server entries is changed. Here's a breakdown of what the method does:

  1. Randomize: If the entryList has more than one element and the options do not specify to not randomize, the method shuffles the elements in the entryList using Collections.shuffle() with the help of ThreadLocalRandom.current(). This step ensures that the order of the servers is randomized.

  2. Calculate hasSecureServer and find the index of lastConnected: The method iterates through each element in the entryList and checks if the server URL (NatsUri) is secure. It sets the hasSecureServer flag to true if any of the server URLs are secure. It also finds the index of the last connected server by comparing the server's URL with the lastConnected variable.

  3. Put the last connected server at the end of the list: If a last connected server was found (i.e., lastConnectedIx is not -1), the method removes the element at lastConnectedIx from the entryList and adds it back at the end of the list. This step ensures that the last connected server is always at the end of the list.

Overall, this method is responsible for randomizing the server list, checking for secure servers, and ensuring the last connected server is moved to the end of the list.

sequence diagram

@Override

public NatsUri peekNextServer()

@Override
public NatsUri peekNextServer() {
    synchronized (listLock) {
        return entryList.size() > 0 ? entryList.get(0).nuri : null;
    }
}

The peekNextServer method in the io.nats.client.impl.NatsServerPool class is used to retrieve the next server in a list of server entries. Below is a step-by-step description of what the method does:

  1. The method peekNextServer is implemented and overrides the same method from the parent class or interface.
  2. The method is declared to return an object of type NatsUri.
  3. The method is synchronized using the synchronized keyword, which means only one thread can access this code block at a time.
  4. Inside the synchronized block, the method checks if the entryList (a list of server entries) has a size greater than 0.
  5. If the entryList has at least one entry, the method returns the nuri (a field of the first entry in the entryList).
  6. If the entryList is empty, i.e., it doesn't have any entries, the method returns null.

The peekNextServer method in the NatsServerPool class returns the URI of the next server in the pool without removing it from the list. It achieves this by synchronizing access to the list of server entries and checking if the list is not empty. If it is not empty, it retrieves the first entry's URI from the list and returns it. Otherwise, it returns null.

sequence diagram

@Override

public NatsUri nextServer()

@Override
public NatsUri nextServer() {
    // 0. The list is already managed for qualified by connectFailed
    // 1. Get the first item in the list, update it's time, add back to the end of list
    synchronized (listLock) {
        if (entryList.size() > 0) {
            ServerPoolEntry entry = entryList.remove(0);
            entry.lastAttempt = System.currentTimeMillis();
            entryList.add(entry);
            return entry.nuri;
        }
        return null;
    }
}

The nextServer() method is used to retrieve the next available server from the list of server entries in the NatsServerPool class. Here is a step-by-step description of what the method is doing:

  1. It first checks if the size of the entry list is greater than 0 to ensure there are available servers.
  2. If there are available servers, it retrieves the first server entry from the list using the remove(0) method, which removes and returns the element at index 0.
  3. It then updates the last attempt time of the server entry using entry.lastAttempt = System.currentTimeMillis(), which sets the last attempt time to the current system time in milliseconds.
  4. After updating the last attempt time, it adds the server entry back to the end of the list using entryList.add(entry).
  5. Finally, it returns the NatsUri (URI of the server) from the retrieved server entry using return entry.nuri.

If there are no available servers (i.e., the entry list is empty), it returns null.

Note: The code uses synchronization with the listLock object to ensure thread safety when accessing and modifying the entry list.

The nextServer method in the NatsServerPool class is used to retrieve the next available NatsUri from the server pool. Here's what the method does:

  1. It checks if the entryList (which represents the list of available servers) is not empty.
  2. If the entryList is not empty, it removes the first server entry (ServerPoolEntry) from the list.
  3. It updates the lastAttempt timestamp of the removed server entry with the current system time in milliseconds.
  4. It adds the updated server entry back to the end of the entryList.
  5. Finally, it returns the NatsUri associated with the removed server entry.

If the entryList is empty, it returns null, indicating that no servers are currently available in the pool.

sequence diagram

@Override

public List resolveHostToIps(String host)

@Override
public List<String> resolveHostToIps(String host) {
    // 1. if options.isNoResolveHostnames(), return empty list
    if (options.isNoResolveHostnames()) {
        return null;
    }
    // 2. else, try to resolve the hostname, adding results to list
    List<String> results = new ArrayList<>();
    try {
        InetAddress[] addresses = InetAddress.getAllByName(host);
        for (InetAddress a : addresses) {
            results.add(a.getHostAddress());
        }
    } catch (UnknownHostException ignore) {
        // A user might have supplied a bad host, but the server shouldn't.
        // Either way, nothing much we can do.
    }
    // 3. no results, return null.
    if (results.size() == 0) {
        return null;
    }
    // 4. if results has more than 1 and allowed to randomize, shuffle the list
    if (results.size() > 1 && !options.isNoRandomize()) {
        Collections.shuffle(results, ThreadLocalRandom.current());
    }
    return results;
}

The resolveHostToIps method in the io.nats.client.impl.NatsServerPool class is used to resolve a hostname to a list of IP addresses. Here is a step-by-step breakdown of what the method does:

  1. Check if the options.isNoResolveHostnames() flag is set. If it is, return an empty list.
  2. If the flag is not set:
    • Create an empty list to store the IP addresses.
    • Attempt to resolve the hostname using InetAddress.getAllByName(host).
    • For each resolved InetAddress, add its host address to the results list.
  3. If no IP addresses were resolved, return null.
  4. If more than one IP address is resolved and randomization is allowed (i.e., !options.isNoRandomize()), shuffle the results list using Collections.shuffle() and the current thread's random number generator.
  5. Return the list of resolved IP addresses.

Note: If an exception occurs during the hostname resolution process (e.g., UnknownHostException), it is caught and ignored. This is to handle cases where a user might have provided an invalid hostname, but the server should not be affected.

The resolveHostToIps method is used to resolve the IP addresses associated with a given hostname.

Here is a step-by-step breakdown of what the method does:

  1. Check if the options.isNoResolveHostnames() flag is set. If so, return an empty list.
  2. If the flag is not set, attempt to resolve the hostname using InetAddress.getAllByName(host). This method returns an array of InetAddress objects, representing the resolved IP addresses.
  3. Iterate over each InetAddress object in the array and add its host address to the results list.
  4. If no IP addresses were resolved (i.e., the results list is empty), return null.
  5. If the results list has more than one IP address and the options.isNoRandomize() flag is not set, shuffle the results list using Collections.shuffle() and a ThreadLocalRandom object.
  6. Finally, return the results list, which contains the resolved IP addresses.

Overall, the resolveHostToIps method checks if hostname resolution is enabled, resolves the hostname to IP addresses, and applies optional randomization to the list of IP addresses before returning them.

sequence diagram

@Override

public void connectSucceeded(NatsUri nuri)

@Override
public void connectSucceeded(NatsUri nuri) {
    // 1. Work from the end because nextServer moved the one being tried to the end
    // 2. If we find the server in the list...
    //    2.1. remember it and
    //    2.2. reset failed attempts
    synchronized (listLock) {
        for (int x = entryList.size() - 1; x >= 0; x--) {
            ServerPoolEntry entry = entryList.get(x);
            if (entry.nuri.equals(nuri)) {
                lastConnected = nuri;
                entry.failedAttempts = 0;
                return;
            }
        }
    }
}

The connectSucceeded method in the NatsServerPool class is responsible for handling the successful connection to a server. Below is a step-by-step description of what this method does:

  1. Start by acquiring a lock on the listLock object to ensure thread safety.
  2. Iterate over the entryList, starting from the end and moving towards the beginning.
  3. For each ServerPoolEntry in the entryList, check if its nuri (NatsUri) is equal to the nuri passed as a parameter to the method.
  4. If a matching server is found:
    • Set the lastConnected field to the nuri value, indicating that this server was the last successfully connected server.
    • Reset the failedAttempts counter for this server entry, since the connection was successful.
    • Exit the method.
  5. Release the lock acquired in step 1.

It's worth noting that the lastConnected and failedAttempts fields are likely used for tracking the state of server connection attempts and are not directly related to the logic of the connectSucceeded method.

The connectSucceeded method in the NatsServerPool class handles the connection success event for a NATS server.

It starts by working from the end of the server list because the nextServer method moved the server being tried to the end.

Then, if it finds the server in the list, it performs the following steps:

  1. It stores the URI of the server that was successfully connected.
  2. It resets the failed attempts for that server to 0.

This method is synchronized to ensure thread safety when accessing and modifying the entryList.

sequence diagram

@Override

public void connectFailed(NatsUri nuri)

@Override
public void connectFailed(NatsUri nuri) {
    // 1. Work from the end because nextServer moved the one being tried to the end
    // 2. If we find the server in the list...
    //    2.1. increment failed attempts
    //    2.2. if failed attempts reaches max, remove it from the list
    synchronized (listLock) {
        for (int x = entryList.size() - 1; x >= 0; x--) {
            ServerPoolEntry entry = entryList.get(x);
            if (entry.nuri.equals(nuri)) {
                if (++entry.failedAttempts >= maxConnectAttempts) {
                    entryList.remove(x);
                }
                return;
            }
        }
    }
}

The connectFailed method in the NatsServerPool class is used to handle the case where a connection to a server has failed. Here is a step-by-step description of what the method does:

  1. The method starts by synchronizing on the listLock, ensuring that only one thread can execute this code block at a time.

  2. The method iterates over the entryList, which is a list of ServerPoolEntry objects representing the available NATS servers.

  3. The iteration starts from the end of the entryList and moves towards the beginning. This is done because the nextServer method (not shown in the provided code) moves the server being tried to the end of the list.

  4. For each ServerPoolEntry object in the entryList, the method checks if its nuri (NATS URI) is equal to the failed NATS URI (nuri) passed as a parameter to the connectFailed method.

  5. If the nuri of a ServerPoolEntry matches the failed NATS URI, then it means that this server connection has failed.

  6. In this case, the method increments the failedAttempts counter of the ServerPoolEntry object.

  7. If the incremented failedAttempts counter reaches the maximum allowed connect attempts (maxConnectAttempts), then the server is removed from the entryList by invoking the remove method on the entryList with the index (x) of the failed server.

  8. After removing the server, the method returns and completes its execution.

Note: The provided code does not handle the scenario where the failed server is not found in the entryList. If the failed server is not in the entryList, nothing happens, and the method returns without making any modifications.

The connectFailed method in the NatsServerPool class is used to handle the case when a connection to a NATS server fails.

In the method body, it iterates over the entries in the server pool list in reverse order, starting from the end. It checks if the current entry's NatsUri (representing the server's URI) matches the failed NatsUri. If a match is found, it increments the number of failed attempts for that server entry. If the number of failed attempts exceeds the maximum allowed connect attempts, the server entry is removed from the list.

The method is synchronized, ensuring that access to the server pool list is thread-safe.

sequence diagram

private int findEquivalent(List list, NatsUri toFind)

private int findEquivalent(List<NatsUri> list, NatsUri toFind) {
    for (int i = 0; i < list.size(); i++) {
        NatsUri nuri = list.get(i);
        if (nuri.equivalent(toFind)) {
            return i;
        }
    }
    return -1;
}

The findEquivalent method in the NatsServerPool class is a private method that takes in two parameters: a List of NatsUri objects called list and a NatsUri object called toFind. The purpose of this method is to find the index of a NatsUri object in the given list that is equivalent to the toFind object.

Here is a step-by-step description of the method:

  1. Initialize a for loop that iterates over the list from index 0 to list.size() - 1.
  2. On each iteration, get the NatsUri object at the current index (i) from the list and assign it to the variable nuri.
  3. Check if the nuri object is equivalent to the toFind object by invoking the equivalent method on nuri. This method likely compares the relevant properties of the two NatsUri objects and returns a boolean indicating if they are equivalent.
  4. If the nuri object is equivalent to the toFind object, return the current index (i) as the result.
  5. If no equivalent NatsUri object is found in the list, the loop completes without returning and the method proceeds to the next step.
  6. Return -1 to indicate that no equivalent object was found in the list.

In summary, the findEquivalent method searches for a NatsUri object in the given list that is equivalent to the toFind object and returns its index. If no equivalent object is found, it returns -1.

The findEquivalent method, which is defined in the NatsServerPool class in the io.nats.client.impl package, searches for an equivalent NatsUri object within a given list.

It iterates through the list using a for loop, comparing each NatsUri object in the list to the toFind parameter. If it finds an equivalent NatsUri object, it returns the index of that object in the list. If no equivalent object is found, it returns -1.

This method can be used to quickly find the index of a specific NatsUri object within a list of NatsUri objects.

NatsStreamContext

NatsStreamContext

The NatsStreamContext is a class that implements the StreamContext interface. It is a simplified experimental class that may undergo changes in future versions.

@Override

public IterableConsumer orderedConsume(OrderedConsumerConfig config, ConsumeOptions consumeOptions) throws IOException, JetStreamApiException

@Override
public IterableConsumer orderedConsume(OrderedConsumerConfig config, ConsumeOptions consumeOptions) throws IOException, JetStreamApiException {
    Validator.required(config, "Ordered Consumer Config");
    Validator.required(consumeOptions, "Consume Options");
    ConsumerConfiguration cc = getBackingConsumerConfiguration(config);
    PullSubscribeOptions pso = new OrderedPullSubscribeOptionsBuilder(streamName, cc).build();
    return new NatsIterableConsumer(new SimplifiedSubscriptionMaker(js, pso, cc.getFilterSubject()), consumeOptions, null);
}

The orderedConsume method is defined in the NatsStreamContext class from the package io.nats.client.impl.

This method requires two parameters: config of type OrderedConsumerConfig and consumeOptions of type ConsumeOptions. It also throws two exceptions: IOException and JetStreamApiException.

Here is a step-by-step description of what the orderedConsume method does based on its body:

  1. Validate that the config parameter is not null using the Validator.required method. If it is null, throw an exception with the message "Ordered Consumer Config is required."

  2. Validate that the consumeOptions parameter is not null using the Validator.required method. If it is null, throw an exception with the message "Consume Options is required."

  3. Retrieve the consumer configuration object for the given config using the getBackingConsumerConfiguration method.

  4. Create a PullSubscribeOptions object named pso by using the OrderedPullSubscribeOptionsBuilder with the streamName (which is assumed to be a class field) and the cc consumer configuration object.

  5. Create a new instance of the NatsIterableConsumer class, passing the following parameters:

    • An instance of the SimplifiedSubscriptionMaker class, initialized with the js object (which is assumed to be a class field), the pso pull subscribe options, and the filter subject from the consumer configuration.
    • The consumeOptions parameter.
    • null for the last parameter.
  6. Return the created instance of the NatsIterableConsumer class as an IterableConsumer.

Overall, this method validates the input parameters, creates a PullSubscribeOptions object, and then creates and returns an instance of the NatsIterableConsumer class with the provided options and configuration.

The orderedConsume method is used to create an iterable consumer for consuming messages in a specific order from a NATS stream.

The method takes in two parameters: config, which is an object of type OrderedConsumerConfig, and consumeOptions, which is an object of type ConsumeOptions.

First, the method checks if both config and consumeOptions are not null; if either of them is null, it throws an IOException or JetStreamApiException.

Next, it creates a ConsumerConfiguration object cc by calling the getBackingConsumerConfiguration method with the config parameter.

Then, it builds a PullSubscribeOptions object pso using the streamName and cc objects.

Finally, it returns a new instance of NatsIterableConsumer class, initialized with a SimplifiedSubscriptionMaker object created using the js, pso, and cc.getFilterSubject() objects, along with the consumeOptions parameter and a null value for an optional context parameter.

sequence diagram

@Override

public MessageConsumer orderedConsume(OrderedConsumerConfig config, MessageHandler handler, ConsumeOptions consumeOptions) throws IOException, JetStreamApiException

@Override
public MessageConsumer orderedConsume(OrderedConsumerConfig config, MessageHandler handler, ConsumeOptions consumeOptions) throws IOException, JetStreamApiException {
    Validator.required(config, "Ordered Consumer Config");
    Validator.required(handler, "Message Handler");
    Validator.required(consumeOptions, "Consume Options");
    ConsumerConfiguration cc = getBackingConsumerConfiguration(config);
    PullSubscribeOptions pso = new OrderedPullSubscribeOptionsBuilder(streamName, cc).build();
    return new NatsMessageConsumer(new SimplifiedSubscriptionMaker(js, pso, cc.getFilterSubject()), handler, consumeOptions, null);
}

The method orderedConsume is a method override in the class io.nats.client.impl.NatsStreamContext. It returns a MessageConsumer object. Here is a step-by-step description of what the method does based on its body:

  1. It takes three parameters: config, handler, and consumeOptions.
  2. It validates that all three parameters are not null using the Validator.required method. If any of the parameters are null, it will throw an IllegalArgumentException with a descriptive error message.
  3. It creates a ConsumerConfiguration object by calling the getBackingConsumerConfiguration method with the config parameter.
  4. It creates a PullSubscribeOptions object by calling the constructor of OrderedPullSubscribeOptionsBuilder with the streamName and cc parameters.
  5. It creates a new instance of NatsMessageConsumer by passing the following arguments:
    • A SimplifiedSubscriptionMaker object with the js, pso, and cc.getFilterSubject() parameters.
    • The handler parameter.
    • The consumeOptions parameter.
    • null for an optional executor parameter.
  6. It returns the created NatsMessageConsumer object as the result of the method.

That's the step-by-step description of what the orderedConsume method does based on its body.

The method orderedConsume in the NatsStreamContext class is used to create an ordered message consumer for a specified stream and configures it based on the provided parameters.

The method takes in an OrderedConsumerConfig object, a MessageHandler object, and ConsumeOptions object as input parameters. These parameters are used to configure the consumer.

Internally, the method checks that the provided parameters are not null using the Validator.required method.

It then creates a ConsumerConfiguration object based on the provided OrderedConsumerConfig using the getBackingConsumerConfiguration method.

Next, it creates a PullSubscribeOptions object using the OrderedPullSubscribeOptionsBuilder with the stream name and the previously created ConsumerConfiguration.

Finally, it creates a new NatsMessageConsumer object passing in a SimplifiedSubscriptionMaker with the provided parameters along with consumeOptions and null for the last two required parameters.

In summary, the orderedConsume method creates and configures an ordered message consumer for a specified stream using the provided parameters and returns an instance of NatsMessageConsumer.

sequence diagram

MessageManager

MessageManager

The MessageManager class is an abstract class that provides functionality for managing messages. It serves as a base class for other classes that specialize in handling different types of messages.

protected void trackJsMessage(Message msg)

protected void trackJsMessage(Message msg) {
    synchronized (stateChangeLock) {
        NatsJetStreamMetaData meta = msg.metaData();
        lastStreamSeq = meta.streamSequence();
        lastConsumerSeq++;
    }
}

The trackJsMessage method, defined in the io.nats.client.impl.MessageManager class, performs the following steps based on its body:

  1. Acquire the lock associated with the stateChangeLock object for synchronization purposes.
  2. Retrieve the metadata associated with the provided Message object, msg, by calling the metaData() method on it. The retrieved metadata is stored in the meta variable.
  3. Extract and assign the stream sequence number from the retrieved metadata to the lastStreamSeq variable.
  4. Increment the lastConsumerSeq variable by one.
  5. Release the lock associated with the stateChangeLock object.

The trackJsMessage method, defined in the MessageManager class of the io.nats.client.impl package, is used to track the state of a JavaScript message in the NATS messaging system.

In detail, this method takes a Message object as input and performs the following actions:

  1. It ensures that the execution of this method is thread-safe by using synchronization on the stateChangeLock variable.
  2. It extracts the metadata of the message using the metaData() method and assigns it to the meta variable of type NatsJetStreamMetaData.
  3. It updates the value of the lastStreamSeq variable to match the streamSequence() value from the message metadata. This represents the last sequence number received from the stream.
  4. It increments the value of the lastConsumerSeq variable by 1. This represents the last consumed sequence number by the consumer.

Overall, the trackJsMessage method helps maintain and update the state of JavaScript messages in a NATS system, specifically keeping track of the last received sequence number from the stream and the last consumed sequence number by the consumer.

sequence diagram

protected void configureIdleHeartbeat(Duration configIdleHeartbeat, long configMessageAlarmTime)

protected void configureIdleHeartbeat(Duration configIdleHeartbeat, long configMessageAlarmTime) {
    synchronized (stateChangeLock) {
        idleHeartbeatSetting = configIdleHeartbeat == null ? 0 : configIdleHeartbeat.toMillis();
        if (idleHeartbeatSetting <= 0) {
            alarmPeriodSetting = 0;
            hb = false;
        } else {
            if (configMessageAlarmTime < idleHeartbeatSetting) {
                alarmPeriodSetting = idleHeartbeatSetting * THRESHOLD;
            } else {
                alarmPeriodSetting = configMessageAlarmTime;
            }
            hb = true;
        }
    }
}

The method configureIdleHeartbeat in class io.nats.client.impl.MessageManager is responsible for configuring the idle heartbeat settings and the message alarm time. Here is a step-by-step description of what this method does:

  1. Start synchronized block: The method starts by entering a synchronized block using stateChangeLock as the lock object. This ensures that the state changes made by this method are thread-safe.

  2. Configure the idle heartbeat setting: The method starts by setting the idleHeartbeatSetting based on the provided configIdleHeartbeat. If configIdleHeartbeat is null, idleHeartbeatSetting is set to 0. Otherwise, it is set to the duration in milliseconds represented by configIdleHeartbeat.

  3. Check idle heartbeat setting: The method checks if the idleHeartbeatSetting is less than or equal to 0. If it is, it means that the idle heartbeat should be disabled. In this case, the alarmPeriodSetting is set to 0 and hb (heartbeat setting) is set to false.

  4. Configure alarm period setting: If the idleHeartbeatSetting is greater than 0, it means that the idle heartbeat should be enabled. In this case, the method checks if the provided configMessageAlarmTime is less than the idleHeartbeatSetting. If it is, the alarmPeriodSetting is set to the idleHeartbeatSetting multiplied by a threshold value (THRESHOLD). This threshold value determines the interval between message alarms.

  5. Configure heartbeat setting: Finally, the method sets the hb (heartbeat setting) to true indicating that the heartbeat should be enabled.

  6. End synchronized block: The synchronized block ends, allowing other threads to access the shared state.

Please note that this description is based on the provided code snippet and may not cover the full context or functionality of the class.

The configureIdleHeartbeat method is used to set the idle heartbeat configuration and message alarm time for a message manager.

The method takes two parameters: configIdleHeartbeat and configMessageAlarmTime.

Inside the method, the idle heartbeat setting is configured based on the value of configIdleHeartbeat. If configIdleHeartbeat is null or less than or equal to 0, the idle heartbeat setting is set to 0 and the alarm period setting is also set to 0, indicating that the heartbeat functionality is disabled.

If configIdleHeartbeat is greater than 0, the method checks if configMessageAlarmTime is less than configIdleHeartbeat. If it is, the alarm period setting is set to configIdleHeartbeat multiplied by a constant THRESHOLD. Otherwise, the alarm period setting is set to configMessageAlarmTime.

Finally, the hb flag is set to true if the idle heartbeat setting is greater than 0, indicating that the heartbeat functionality is enabled. The configuration is performed in a synchronized block to ensure thread safety.

sequence diagram

protected void initOrResetHeartbeatTimer()

protected void initOrResetHeartbeatTimer() {
    synchronized (stateChangeLock) {
        shutdownHeartbeatTimer();
        heartbeatTimer = new Timer();
        heartbeatTimerTask = new TimerTask() {

            @Override
            public void run() {
                long sinceLast = System.currentTimeMillis() - lastMsgReceived;
                if (sinceLast > alarmPeriodSetting) {
                    handleHeartbeatError();
                }
            }
        };
        heartbeatTimer.schedule(heartbeatTimerTask, alarmPeriodSetting, alarmPeriodSetting);
    }
}

The initOrResetHeartbeatTimer method in the MessageManager class is used to initialize or reset a heartbeat timer.

  1. First, the method acquires a lock on the stateChangeLock object to ensure thread safety.

  2. Then, it calls the shutdownHeartbeatTimer method to stop any existing heartbeat timer.

  3. Next, it creates a new instance of the Timer class and assigns it to the heartbeatTimer member variable.

  4. It also creates a new instance of the TimerTask anonymous inner class, which overrides the run method.

  5. Inside the run method, it calculates the time interval sinceLast by subtracting the current time in milliseconds from the lastMsgReceived variable.

  6. If the sinceLast interval exceeds the alarmPeriodSetting, it calls the handleHeartbeatError method to handle the heartbeat error.

  7. Finally, it schedules the heartbeatTimerTask to run repeatedly at a fixed rate specified by the alarmPeriodSetting.

Note: The method uses a synchronized block to ensure that only one thread can access and modify the timer and timer task objects at a time. This helps prevent race conditions and conflicts between multiple threads.

The initOrResetHeartbeatTimer method, defined in the io.nats.client.impl.MessageManager class, is responsible for initializing or resetting a heartbeat timer.

The method first acquires a lock to ensure thread safety. It then shuts down any existing heartbeat timer by invoking the shutdownHeartbeatTimer method.

A new Timer is created, along with a TimerTask that defines the task to be executed by the timer. In this case, the task checks the time elapsed since the last message was received. If the elapsed time exceeds a predefined alarm period, the handleHeartbeatError method is called.

Finally, the timer task is scheduled to run at regular intervals specified by the alarmPeriodSetting parameter.

sequence diagram

protected void shutdownHeartbeatTimer()

protected void shutdownHeartbeatTimer() {
    synchronized (stateChangeLock) {
        if (heartbeatTimer != null) {
            heartbeatTimer.cancel();
            heartbeatTimer = null;
        }
    }
}

The method shutdownHeartbeatTimer() defined in class io.nats.client.impl.MessageManager is responsible for stopping and shutting down the heartbeat timer. The step-by-step description is as follows:

  1. Acquire the lock on the stateChangeLock object to ensure thread safety.
  2. Check if the heartbeatTimer object is not null (indicating that the timer is currently running).
  3. If the heartbeatTimer is not null, cancel the timer by calling its cancel() method.
  4. Set the value of heartbeatTimer to null, indicating that the timer has been stopped and shut down.
  5. Release the lock on the stateChangeLock object.

The method essentially stops and shuts down the heartbeat timer by canceling it and setting the heartbeatTimer variable to null.

The shutdownHeartbeatTimer method defined in the io.nats.client.impl.MessageManager class is responsible for shutting down the heartbeat timer.

It does this by acquiring a lock on the stateChangeLock object to ensure thread safety. It then checks if the heartbeatTimer object is not null. If it is not null, it cancels the timer and sets it to null, effectively shutting it down.

This method ensures that the heartbeat timer is properly stopped, preventing any unwanted timers from running and consuming system resources.

sequence diagram

ConsumerListReader

ConsumerListReader

This ConsumerListReader class is an extension of the AbstractListReader class. It is responsible for reading and processing consumer lists.

@Override

protected void processItems(List items)

@Override
protected void processItems(List<JsonValue> items) {
    for (JsonValue v : items) {
        consumers.add(new ConsumerInfo(v));
    }
}

The processItems method, defined in the ConsumerListReader class within the io.nats.client.impl package, is responsible for populating a list of ConsumerInfo objects based on the input list of JsonValue items.

Here is a step-by-step description of the method:

  1. The method is marked with the @Override annotation, indicating that it overrides a method from a superclass or interface.

  2. The method is declared with a protected access modifier, making it accessible only within the same package and subclasses.

  3. The method takes a single parameter, items, of type List<JsonValue>. This parameter represents the input list of JsonValue items that need to be processed.

  4. Inside the method, a for-each loop is used to iterate over each JsonValue object in the items list.

  5. Within the loop, a new instance of ConsumerInfo is created using the current JsonValue object (v), and it is added to the consumers list. The add method is called on the consumers list, indicating that a new ConsumerInfo object is being added to the list.

  6. Once the for-each loop finishes iterating over all the items, the method execution is complete.

Please note that without further context or the definition of the ConsumerInfo class, it is not possible to provide a more detailed explanation of what happens inside the ConsumerInfo constructor or how the consumers list is used after the method execution.

The processItems method, which is defined in the ConsumerListReader class in the io.nats.client.impl package, is responsible for processing a list of JsonValue items.

In the method body, it iterates over each JsonValue item in the items list. For each item, it creates a new ConsumerInfo instance using the JsonValue item and adds it to the consumers list.

Overall, the purpose of this method is to parse a list of JsonValue items and convert them into ConsumerInfo objects, adding them to the consumers list for further processing or storage.

sequence diagram

NatsMessage

NatsMessage

The NatsMessage class is a public class that implements the Message interface. It represents a message in the NATS messaging system. This class provides functionality for creating and handling NATS messages.

protected void finishConstruct()

protected void finishConstruct() {
    int replyToLen = replyTo == null ? 0 : replyTo.length();
    if (headers != null && !headers.isEmpty()) {
        headerLen = headers.serializedLength();
    } else {
        headerLen = 0;
    }
    int headerAndDataLen = headerLen + dataLen;
    // initialize the builder with a reasonable length, preventing resize in 99.9% of the cases
    // 32 for misc + subject length doubled in case of utf8 mode + replyToLen + totLen (headerLen + dataLen)
    ByteArrayBuilder bab = new ByteArrayBuilder(32 + (subject.length() * 2) + replyToLen + headerAndDataLen);
    // protocol come first
    if (headerLen > 0) {
        bab.append(HPUB_SP_BYTES, 0, HPUB_SP_BYTES_LEN);
    } else {
        bab.append(PUB_SP_BYTES, 0, PUB_SP_BYTES_LEN);
    }
    // next comes the subject
    bab.append(subject.getBytes(UTF_8)).append(SP);
    // reply to if it's there
    if (replyToLen > 0) {
        bab.append(replyTo.getBytes(UTF_8)).append(SP);
    }
    // header length if there are headers
    if (headerLen > 0) {
        bab.append(Integer.toString(headerLen).getBytes(US_ASCII)).append(SP);
    }
    // payload length
    bab.append(Integer.toString(headerAndDataLen).getBytes(US_ASCII));
    protocolBab = bab;
    // One CRLF. This is just how controlLineLength is defined.
    controlLineLength = protocolBab.length() + 2;
    // The 2nd CRLFs
    sizeInBytes = controlLineLength + headerAndDataLen + 2;
}

The finishConstruct method is responsible for finalizing the construction of the NatsMessage object by setting the appropriate values for various variables based on the provided BODY.

Here is a step-by-step description of what the method does:

  1. It calculates the length of the replyTo string, assigning it to the replyToLen variable.
  2. It checks if the headers map is not null and not empty. If it is not null and not empty, it calculates the serialized length of the headers and assigns it to the headerLen variable. Otherwise, it sets headerLen to 0.
  3. It calculates the total length of the headers and data, assigning it to the headerAndDataLen variable.
  4. It initializes a ByteArrayBuilder object called bab with an initial capacity calculated based on the lengths of different strings and variables.
  5. It appends the appropriate protocol bytes (HPUB_SP_BYTES or PUB_SP_BYTES) depending on the value of headerLen.
  6. It appends the subject string converted to bytes using UTF-8 encoding, followed by a space character.
  7. It appends the replyTo string converted to bytes using UTF-8 encoding, followed by a space character, if replyToLen is greater than 0.
  8. It appends the length of the headers (headerLen) as a string converted to bytes using US-ASCII encoding, followed by a space character, if headerLen is greater than 0.
  9. It appends the total length of the headers and data (headerAndDataLen) as a string converted to bytes using US-ASCII encoding.
  10. It assigns the bab object to the protocolBab variable.
  11. It calculates the length of the protocolBab object plus 2 (for a CRLF), assigning it to the controlLineLength variable.
  12. It calculates the size of the message in bytes by adding controlLineLength, headerAndDataLen, and 2 (for an additional CRLF), assigning it to the sizeInBytes variable.

This completes the finalization process of the NatsMessage object based on the provided BODY.

The finishConstruct method in the NatsMessage class is responsible for finalizing the construction of a NATS message.

Here are the steps performed by the method:

  1. It calculates the length of the replyTo string.
  2. It checks if there are any headers present and calculates the length of the serialized headers.
  3. It calculates the total length of the headers and data.
  4. It initializes a ByteArrayBuilder with a length calculated based on the various components of the message.
  5. It appends the appropriate protocol bytes to the ByteArrayBuilder depending on whether headers are present or not.
  6. It appends the subject bytes to the ByteArrayBuilder, followed by a space.
  7. If a replyTo string is present, it appends the replyTo bytes to the ByteArrayBuilder, followed by a space.
  8. If headers are present, it appends the serialized length of the headers to the ByteArrayBuilder, followed by a space.
  9. It appends the payload length to the ByteArrayBuilder.
  10. It assigns the ByteArrayBuilder to the protocolBab field of the NatsMessage object.
  11. It calculates the controlLineLength by adding 2 to the length of the protocolBab.
  12. It calculates the sizeInBytes by adding the controlLineLength to the headerAndDataLen, both plus 2.

In summary, the finishConstruct method calculates and builds the protocol representation of a NATS message, including the subject, replyTo, headers, and payload length.

sequence diagram

int copyNotEmptyHeaders(int destPosition, byte[] dest)

int copyNotEmptyHeaders(int destPosition, byte[] dest) {
    if (headers != null && !headers.isEmpty()) {
        return headers.serializeToArray(destPosition, dest);
    }
    return 0;
}

The method copyNotEmptyHeaders is defined in the class io.nats.client.impl.NatsMessage and it takes two parameters: destPosition and dest, which represent the position in the destination array where the headers should be copied and the destination array itself, respectively.

The method first checks whether the variable headers is not null and not empty. The headers variable refers to a collection of headers associated with the message.

If headers is not null and not empty, the method calls the serializeToArray method on the headers object, passing in the destPosition and dest parameters. This method serializes the headers into a byte array and copies them into the dest array, starting at the specified position. The serializeToArray method returns the number of bytes copied.

If headers is null or empty, the method returns 0, indicating that no headers were copied.

In summary, the copyNotEmptyHeaders method copies non-empty headers associated with the message into a byte array at the specified position. If there are no headers or they are empty, the method returns 0.

The copyNotEmptyHeaders method defined in the NatsMessage class of the io.nats.client.impl package is used to copy any non-empty headers of a message into a destination byte array starting from a specified position.

Here is what the method does:

  1. If the headers field of the NatsMessage object is not null and is not empty:

    • It calls the serializeToArray method on the headers object and passes the destPosition and dest as parameters.
    • This method serializes the headers into a byte array starting from the specified position, and returns the number of bytes written.
    • The number of bytes written is then returned by the copyNotEmptyHeaders method.
  2. If the headers field is null or empty:

    • The method simply returns 0 indicating that no headers were copied.

Overall, the copyNotEmptyHeaders method is used to serialize non-empty headers of a NatsMessage object into a byte array at a specified position.

sequence diagram

private String dataToString()

private String dataToString() {
    if (data.length == 0) {
        return "<no data>";
    }
    String s = new String(data, UTF_8);
    int at = s.indexOf("io.nats.jetstream.api");
    if (at == -1) {
        return s.length() > 27 ? s.substring(0, 27) + "..." : s;
    }
    int at2 = s.indexOf('"', at);
    return s.substring(at, at2);
}

The dataToString method in the NatsMessage class is used to convert the data property of the instance to a String representation.

Here is a step-by-step description of what the method does based on its implementation:

  1. If the length of the data array is 0, the method returns the string "". This means that if there is no data present, the method will just return this string.

  2. Otherwise, it creates a new String s by decoding the data array using the UTF-8 character set.

  3. It then searches for the occurrence of the string "io.nats.jetstream.api" in s. If this string is not found, it proceeds to step 6.

  4. If the string is found, it searches for the position of the next double quote character (") starting from the position of the found string. This is done by calling the indexOf method on s with the starting position set as the found index. The result is stored in the at2 variable.

  5. It extracts a substring from s starting from the found string position (at) and ending at the previous double quote position (at2). This substring represents a portion of the original data that is enclosed within double quotes. This substring is then returned as the result of the method.

  6. If the string "io.nats.jetstream.api" is not found in s, it checks if the length of s is greater than 27. If it is, it extracts the first 27 characters of s and appends "..." to it. If it is not greater than 27, it simply returns s as is.

In summary, the dataToString method converts the data property to a string representation with specific formatting rules based on the content of the data.

The dataToString method in the NatsMessage class is used to convert the byte array data of a message to a string representation.

If the data array is empty, it returns "<no data>". Otherwise, it converts the byte array to a string using the UTF-8 encoding.

It then checks if the resulting string contains the substring "io.nats.jetstream.api". If it does not, it returns a substring of the string up to the first 27 characters (or the whole string if it is less than 27 characters).

If the string does contain the substring "io.nats.jetstream.api", it finds the index of the next double quote character (") after the substring and returns a substring from the index of "io.nats.jetstream.api" up to the index of the next double quote character.

sequence diagram

MemoryAuthHandler

MemoryAuthHandler

The MemoryAuthHandler class is a public class that implements the AuthHandler interface. It provides authentication handling functionality.

private char[] extract(CharBuffer data, int headers)

private char[] extract(CharBuffer data, int headers) {
    CharBuffer buff = CharBuffer.allocate(data.length());
    boolean skipLine = false;
    int headerCount = 0;
    int linePos = -1;
    while (data.length() > 0) {
        char c = data.get();
        linePos++;
        // End of line, either we got it, or we should keep reading the new line
        if (c == '\n' || c == '\r') {
            if (buff.position() > 0) {
                // we wrote something
                break;
            }
            skipLine = false;
            // so we can start right up
            linePos = -1;
            continue;
        }
        // skip to the new line
        if (skipLine) {
            continue;
        }
        // Ignore whitespace
        if (Character.isWhitespace(c)) {
            continue;
        }
        // If we are on a - skip that line, bump the header count
        if (c == '-' && linePos == 0) {
            skipLine = true;
            headerCount++;
            continue;
        }
        // Skip the line, or add to buff
        if (headerCount == headers) {
            buff.put(c);
        }
    }
    // check for naked value
    if (buff.position() == 0 && headers == 1) {
        data.position(0);
        while (data.length() > 0) {
            char c = data.get();
            if (c == '\n' || c == '\r' || Character.isWhitespace(c)) {
                if (buff.position() > 0) {
                    // we wrote something
                    break;
                }
                continue;
            }
            buff.put(c);
        }
        buff.flip();
    } else {
        buff.flip();
    }
    char[] retVal = new char[buff.length()];
    buff.get(retVal);
    buff.clear();
    for (int i = 0; i < buff.capacity(); i++) {
        buff.put('\0');
    }
    return retVal;
}
```## NatsFetchConsumer

**NatsFetchConsumer**

`NatsFetchConsumer` is a class that extends `NatsMessageConsumerBase` and implements the `FetchConsumer` interface. It is used for consuming messages from a NATS messaging system. This class provides the necessary functionalities to fetch messages from the NATS server and allows for easy integration and handling of message consumption in software applications.
### @Override
public Message nextMessage() throws InterruptedException, JetStreamStatusCheckedException 
```java
@Override
public Message nextMessage() throws InterruptedException, JetStreamStatusCheckedException {
    try {
        if (startNanos == -1) {
            startNanos = System.nanoTime();
        }
        long timeLeftMillis = (maxWaitNanos - (System.nanoTime() - startNanos)) / 1_000_000;
        // if the manager thinks it has received everything in the pull, it means
        // that all the messages are already in the internal queue and there is
        // no waiting necessary
        if (timeLeftMillis < 1 | pmm.pendingMessages < 1 || (pmm.trackingBytes && pmm.pendingBytes < 1)) {
            // null means don't wait
            return sub._nextUnmanagedNoWait(pullSubject);
        }
        return sub._nextUnmanaged(timeLeftMillis, pullSubject);
    } catch (JetStreamStatusException e) {
        throw new JetStreamStatusCheckedException(e);
    } catch (IllegalStateException i) {
        // this happens if the consumer is stopped, since it is
        // drained/unsubscribed, so don't pass it on if it's expected
        return null;
    }
}

Method: nextMessage()

This method is defined in the NatsFetchConsumer class located in the package io.nats.client.impl. It overrides the method nextMessage() and throws two exceptions InterruptedException and JetStreamStatusCheckedException.

Step-by-Step Description:

  1. Inside the method, there is a check to see if startNanos variable is -1. If it is, the current time in nanoseconds is assigned to startNanos.

  2. The variable timeLeftMillis is calculated by subtracting the difference between maxWaitNanos and the difference between the current time in nanoseconds and startNanos, and then dividing the result by 1,000,000.

  3. There is a conditional statement that checks if the value of timeLeftMillis is less than 1 or pmm.pendingMessages is less than 1 or (pmm.trackingBytes is true and pmm.pendingBytes is less than 1).

  4. If the above condition is true, the method sub._nextUnmanagedNoWait(pullSubject) is called with pullSubject as an argument and the result is returned. This means that there is no waiting required because all the messages are already in the internal queue.

  5. If the above condition is false, the method sub._nextUnmanaged(timeLeftMillis, pullSubject) is called with timeLeftMillis and pullSubject as arguments and the result is returned. This means that waiting is required for the next message to be available.

  6. If a JetStreamStatusException is caught during the execution, it is wrapped inside a JetStreamStatusCheckedException and re-thrown.

  7. If an IllegalStateException is caught during the execution, it means that the consumer has been stopped, so null is returned.

Note: This step-by-step description assumes that there are relevant definitions and imports available in the code that have not been provided.

The nextMessage method is an overridden method in the NatsFetchConsumer class that is used to retrieve the next message from a NATS server.

Here is a brief description of what the method does:

  1. It checks if the start nanoseconds value is set, and if not, it sets it to the current system nanoseconds value.
  2. It calculates the remaining time in milliseconds based on the maximum wait nanoseconds and the elapsed time since the start.
  3. It checks if there is no time left for waiting, or if there are no pending messages or pending bytes to receive. If any of these conditions are true, it returns the next unmanaged message without waiting.
  4. If there is still time left and there are pending messages or pending bytes, it calls the _nextUnmanaged method with the calculated time left in milliseconds and the pull subject.
  5. If any JetStreamStatusException occurs during the process, it throws a JetStreamStatusCheckedException.
  6. If an IllegalStateException occurs, which means the consumer is stopped, it returns null.

Overall, this method retrieves the next message from the NATS server, either immediately or after waiting for a certain period of time.

sequence diagram

NatsKeyValue

NatsKeyValue

NatsKeyValue is a public class that extends the NatsFeatureBase class and implements the KeyValue interface. It provides functionality related to key value storage and retrieval using the NATS messaging system.

KeyValueEntry _get(String key, long revision) throws IOException, JetStreamApiException

KeyValueEntry _get(String key, long revision) throws IOException, JetStreamApiException {
    MessageInfo mi = _getBySeq(revision);
    if (mi != null) {
        KeyValueEntry kve = new KeyValueEntry(mi);
        if (key.equals(kve.getKey())) {
            return kve;
        }
    }
    return null;
}

Description of the _get Method

The _get method is defined in the NatsKeyValue class within the io.nats.client.impl package. This method takes two parameters: key, which represents the key to be retrieved, and revision, which represents the revision number.

The purpose of this method is to retrieve a KeyValueEntry object from the key-value store based on the given key and revision number.

Here is a step-by-step description of what the _get method does:

  1. Initialize a local variable mi of type MessageInfo and assign it the result of the _getBySeq method call passing the revision parameter. The _getBySeq method returns a MessageInfo object corresponding to the given revision number.

  2. Check if the mi object is not null, indicating that a valid MessageInfo object has been retrieved.

  3. If the mi object is not null, create a new KeyValueEntry object named kve and pass the mi object as a parameter to its constructor. The KeyValueEntry class encapsulates the key-value pair information.

  4. Check if the given key is equal to the key value stored in the kve object using the equals method.

  5. If the given key matches the key stored in the kve object, return the kve object. This means that a valid KeyValueEntry object for the given key and revision has been found.

  6. If the key does not match or if the mi object is null, return null. This means that no KeyValueEntry object was found for the given key and revision.

Note: This method can potentially throw two exceptions: IOException and JetStreamApiException. However, the exceptions are not handled within the method itself and are instead declared in the method signature, indicating that the caller of this method is responsible for handling these exceptions.

The _get method in the NatsKeyValue class is used to retrieve a key-value entry based on a given key and revision.

It first calls the _getBySeq method to retrieve the message info for the specified revision. If the message info is not null, it creates a KeyValueEntry object using the retrieved message info. Then it checks if the key of the KeyValueEntry object matches the given key. If it does, the method returns the KeyValueEntry object.

If the key does not match or if the message info is null, the method returns null.

sequence diagram

@Override

public long create(String key, byte[] value) throws IOException, JetStreamApiException

@Override
public long create(String key, byte[] value) throws IOException, JetStreamApiException {
    validateNonWildcardKvKeyRequired(key);
    try {
        return update(key, value, 0);
    } catch (JetStreamApiException e) {
        if (e.getApiErrorCode() == JS_WRONG_LAST_SEQUENCE) {
            // must check if the last message for this subject is a delete or purge
            KeyValueEntry kve = _get(key);
            if (kve != null && kve.getOperation() != KeyValueOperation.PUT) {
                return update(key, value, kve.getRevision());
            }
        }
        throw e;
    }
}

The create method in the NatsKeyValue class is responsible for creating a key-value entry in the NATS Key-Value store.

Here is a step-by-step description of what the method does:

  1. The method begins by overriding the create method and defining its signature to accept a String key and a byte[] value. It also declares that it can throw IOException and JetStreamApiException.

  2. The method first calls the validateNonWildcardKvKeyRequired method and passes the key as an argument. This method is responsible for validating that the key is not a wildcard pattern, ensuring that it can be used in the Key-Value store. If the key is invalid, an exception will be thrown.

  3. If the key is valid, the method tries to update the key-value entry by calling the update method with the key, value, and 0 as arguments. The update method is responsible for creating or updating the entry.

  4. If the update method throws a JetStreamApiException with the error code JS_WRONG_LAST_SEQUENCE, it means that the last operation for this key was not a regular update. In this case, the method retrieves the current key-value entry for the key by calling the _get method.

  5. If a key-value entry exists for the key and the operation is not PUT (i.e., it is a delete or a purge operation), the method calls the update method again with the key, value, and the revision obtained from the key-value entry. This ensures that the new value will have the correct revision.

  6. If the JetStreamApiException does not have the error code JS_WRONG_LAST_SEQUENCE or there is no existing key-value entry for the key, the original JetStreamApiException is re-thrown.

  7. Finally, if the create method successfully creates or updates the key-value entry, it returns the revision of the entry.

The create method in the NatsKeyValue class allows you to create a new key-value entry in a NATS Key-Value Store.

The method takes a key and value as parameters. It first performs a validation check on the key to ensure that it doesn't contain any wildcard characters.

Then, it calls the update method with the key, value, and a revision of 0 to create the entry. If the update method throws a JetStreamApiException with an apiErrorCode of JS_WRONG_LAST_SEQUENCE, it means that the last message for the given key is a delete or purge operation. In this case, the method checks if the entry exists and its operation is not a PUT. If these conditions are met, it tries to update the entry again with the key, value, and the revision of the existing entry.

If any other type of JetStreamApiException occurs, it is rethrown to the caller.

In summary, the create method creates a new key-value entry in the NATS Key-Value Store, allowing for conditional updates if necessary.

sequence diagram

@Override

public long update(String key, byte[] value, long expectedRevision) throws IOException, JetStreamApiException

@Override
public long update(String key, byte[] value, long expectedRevision) throws IOException, JetStreamApiException {
    validateNonWildcardKvKeyRequired(key);
    Headers h = new Headers().add(EXPECTED_LAST_SUB_SEQ_HDR, Long.toString(expectedRevision));
    return _write(key, value, h).getSeqno();
}

The update method in the io.nats.client.impl.NatsKeyValue class is used to update the value associated with a given key in a key-value store.

Here is a step-by-step description of what the method does based on its body:

  1. It overrides the update method from the superclass, indicating that this implementation provides a specific behavior for updating a key-value pair.

  2. It takes four parameters as input:

    • key: The key for the value to be updated in the key-value store.
    • value: The new value to be associated with the key.
    • expectedRevision: The expected revision of the key-value pair being updated.
  3. It first validates that the key provided is not a wildcard key, as wildcard keys are not allowed for this operation.

  4. It creates a new Headers object, which is used to include client-defined metadata in the message headers. In this case, it adds a header with the key EXPECTED_LAST_SUB_SEQ_HDR and the value set as the string representation of expectedRevision.

  5. It calls the _write method, passing the key, value, and Headers object as parameters. This method is responsible for writing the updated value to the key-value store.

  6. It then retrieves the sequence number (seqno) of the message that was written using the _write method, which represents the position of the message in the server's history.

  7. Finally, it returns the seqno as the result of the update method.

In summary, the update method in the NatsKeyValue class validates the key, creates a header with the expected revision, writes the updated value to the key-value store, and returns the sequence number of the written message.

The update method in the NatsKeyValue class is used to update a value associated with a specific key in a key-value store.

It takes in three parameters:

  1. key - the key of the value to be updated
  2. value - the new value to be associated with the key
  3. expectedRevision - the expected revision of the value, which is used for concurrency control

First, the method validates that a non-wildcard key is required. It then creates a new Headers object and adds the EXPECTED_LAST_SUB_SEQ_HDR (expected revision) header with the value converted to a string.

Next, the _write method is called with the key, value, and headers. This method is responsible for actually writing the updated value to the key-value store.

Finally, the method returns the sequence number (getSeqno()) of the write operation, indicating the position of the updated value in the store.

sequence diagram

@Override

public NatsKeyValueWatchSubscription watch(String key, KeyValueWatcher watcher, KeyValueWatchOption... watchOptions) throws IOException, JetStreamApiException, InterruptedException

@Override
public NatsKeyValueWatchSubscription watch(String key, KeyValueWatcher watcher, KeyValueWatchOption... watchOptions) throws IOException, JetStreamApiException, InterruptedException {
    validateKvKeyWildcardAllowedRequired(key);
    validateNotNull(watcher, "Watcher is required");
    return new NatsKeyValueWatchSubscription(this, key, watcher, watchOptions);
}

The watch method in the NatsKeyValue class is used to create a watch subscription for a specific key in the Nats key-value store. Here is a step-by-step breakdown of what the method is doing:

  1. The method is marked with the @Override annotation, indicating that it overrides a method from a parent class or interface.

  2. The method signature includes the return type, which is NatsKeyValueWatchSubscription. This is the type of object that will be returned by the method.

  3. The method takes several parameters:

    • key is a string parameter representing the key for which the watch subscription is being created.
    • watcher is an implementation of the KeyValueWatcher interface. This object will be notified of changes to the watched key.
    • watchOptions is a variable number of KeyValueWatchOption objects. These options allow customization of the watch subscription.
  4. The method starts by calling the validateKvKeyWildcardAllowedRequired method with the key parameter. This method ensures that the key is valid and throws an exception if it contains a wildcard character that is not allowed.

  5. Next, the method calls the validateNotNull method with the watcher parameter. This method checks if the watcher object is not null and throws an exception if it is.

  6. Finally, the method creates a new NatsKeyValueWatchSubscription object, passing this (the current instance of NatsKeyValue), key, watcher, and watchOptions as arguments. This subscription object is then returned by the method.

Overall, the watch method validates the parameters, creates a watch subscription object, and returns it to the caller. This allows the caller to receive notifications about changes to the watched key.

The watch method in the io.nats.client.impl.NatsKeyValue class allows a software engineer to create a subscription for watching changes to a specific key in a key-value store.

This method takes in the following parameters:

  • key: The key to watch.
  • watcher: The KeyValueWatcher implementation that will handle the incoming changes.
  • watchOptions (optional): Additional options for the watch operation.

Before creating the subscription, the method performs some validation checks:

  • It verifies that the provided key is allowed and not a wildcard.
  • It ensures that the watcher parameter is not null.

Finally, the method returns a new instance of NatsKeyValueWatchSubscription that encapsulates the watch subscription for the given key, watcher, and options.

sequence diagram

@Override

public List keys() throws IOException, JetStreamApiException, InterruptedException

@Override
public List<String> keys() throws IOException, JetStreamApiException, InterruptedException {
    List<String> list = new ArrayList<>();
    visitSubject(readSubject(">"), DeliverPolicy.LastPerSubject, true, false, m -> {
        KeyValueOperation op = getOperation(m.getHeaders());
        if (op == KeyValueOperation.PUT) {
            list.add(new BucketAndKey(m).key);
        }
    });
    return list;
}

The keys() method defined in the io.nats.client.impl.NatsKeyValue class retrieves a list of keys.

Here is a step-by-step description of what the method does based on its body:

  1. The method overrides the keys() method from its parent class.
  2. It declares a new ArrayList named list to store the keys.
  3. It calls the visitSubject() method with the following parameters:
    • The result of the readSubject(">") method call, which retrieves the subject to read from.
    • The DeliverPolicy.LastPerSubject enum value, which specifies the delivery policy.
    • The true boolean value, which indicates that the subject is a generic bucket.
    • The false boolean value, which indicates that it should not fetch the values.
    • A lambda expression that is executed for each message returned by the visitSubject() method.
      • The lambda expression retrieves the operation specified in the message's headers.
      • If the operation is a PUT operation, it adds the key from the message to the list.
  4. The method returns the list containing the keys.

This method essentially visits a subject, retrieves messages, and extracts keys from messages that have a PUT operation specified in their headers.

The keys() method in the NatsKeyValue class is used to retrieve a list of keys stored in a KeyValue store using the NATS messaging system.

The method starts by creating an empty list to store the keys. It then visits the NATS subject corresponding to the readSubject(">") method, retrieves the last message for that subject, and processes it.

Inside the processing logic, it checks the operation type associated with the message headers using the getOperation() method. If the operation is a PUT operation, it extracts the key from the message and adds it to the list.

Finally, the method returns the list of keys.

sequence diagram

@Override

public List history(String key) throws IOException, JetStreamApiException, InterruptedException

@Override
public List<KeyValueEntry> history(String key) throws IOException, JetStreamApiException, InterruptedException {
    validateNonWildcardKvKeyRequired(key);
    List<KeyValueEntry> list = new ArrayList<>();
    visitSubject(readSubject(key), DeliverPolicy.All, false, true, m -> list.add(new KeyValueEntry(m)));
    return list;
}

The history method in the NatsKeyValue class is used to retrieve the history of values for a given key. It takes a key as a parameter and returns a List of KeyValueEntry objects.

Here is a step-by-step description of what the method is doing:

  1. First, it validates that a non-wildcard key is required by calling the validateNonWildcardKvKeyRequired method with the key parameter. This ensures that the key is not a wildcard value, such as "*", and throws an exception if it is.

  2. Next, a new empty ArrayList called list is created to store the KeyValueEntry objects.

  3. The readSubject method is called with the key parameter to determine the NATS subject to read the history from.

  4. The visitSubject method is called with the following parameters:

    • The NATS subject to visit, obtained from the readSubject method.
    • The DeliverPolicy.All flag, indicating that all messages should be delivered.
    • The false flag, indicating that a history call is being made (as opposed to a read or delete call).
    • The true flag, indicating that only the metadata of the messages should be retrieved.
    • A lambda expression that adds a new KeyValueEntry object to the list for each message visited.

    The visitSubject method is responsible for visiting the messages in the NATS subject and performing the specified action on each message. In this case, it adds a new KeyValueEntry object to the list for each message.

  5. Finally, the method returns the list containing the KeyValueEntry objects, which represents the history of values for the given key.

That's it! This is a high-level overview of what the history method in the NatsKeyValue class is doing based on its body.

The history method in the NatsKeyValue class allows the retrieval of the historical entries for a given key.

Here is a brief description of what this method does:

  • It first validates that a non-wildcard key is required, throwing an exception if a wildcard key is provided.
  • It initializes an empty list to store the historical entries.
  • It visits the subject related to the given key, uses the DeliverPolicy.All policy to ensure that all historical messages are retrieved.
  • It iterates over the messages received and adds them to the list as KeyValueEntry instances.
  • Finally, it returns the populated list of historical entries.

sequence diagram

@Override

public void purgeDeletes(KeyValuePurgeOptions options) throws IOException, JetStreamApiException, InterruptedException

@Override
public void purgeDeletes(KeyValuePurgeOptions options) throws IOException, JetStreamApiException, InterruptedException {
    long dmThresh = options == null ? KeyValuePurgeOptions.DEFAULT_THRESHOLD_MILLIS : options.getDeleteMarkersThresholdMillis();
    ZonedDateTime limit;
    if (dmThresh < 0) {
        // long enough in the future to clear all
        limit = DateTimeUtils.fromNow(600000);
    } else if (dmThresh == 0) {
        limit = DateTimeUtils.fromNow(KeyValuePurgeOptions.DEFAULT_THRESHOLD_MILLIS);
    } else {
        limit = DateTimeUtils.fromNow(-dmThresh);
    }
    List<String> keep0List = new ArrayList<>();
    List<String> keep1List = new ArrayList<>();
    visitSubject(streamSubject, DeliverPolicy.LastPerSubject, true, false, m -> {
        KeyValueEntry kve = new KeyValueEntry(m);
        if (kve.getOperation() != KeyValueOperation.PUT) {
            if (kve.getCreated().isAfter(limit)) {
                keep1List.add(new BucketAndKey(m).key);
            } else {
                keep0List.add(new BucketAndKey(m).key);
            }
        }
    });
    for (String key : keep0List) {
        jsm.purgeStream(streamName, PurgeOptions.subject(readSubject(key)));
    }
    for (String key : keep1List) {
        PurgeOptions po = PurgeOptions.builder().subject(readSubject(key)).keep(1).build();
        jsm.purgeStream(streamName, po);
    }
}

The purgeDeletes method in the NatsKeyValue class is used to remove delete markers from the key-value store based on the specified options.

Here is a step-by-step description of what the method is doing:

  1. The method starts by checking the provided KeyValuePurgeOptions parameter. If it is null, it assigns the default delete markers threshold value (KeyValuePurgeOptions.DEFAULT_THRESHOLD_MILLIS) to dmThresh variable. Otherwise, it assigns the value from the options object.

  2. It then calculates the limit variable based on the value of dmThresh. If dmThresh is less than 0, it sets limit to a future date-time that is far enough in the future to clear all delete markers. If dmThresh is 0, it sets limit to a date-time that is calculated by subtracting the default delete markers threshold from the current date-time. Otherwise, it sets limit to a date-time that is calculated by subtracting the absolute value of dmThresh from the current date-time.

  3. Two empty lists, keep0List and keep1List, are created to store the keys of the entries that need to be kept.

  4. The visitSubject method is called with the streamSubject parameter, DeliverPolicy.LastPerSubject, true, and false as arguments. This method visits all the messages in the NATS stream subject and executes the provided lambda function m -> {...} for each message.

  5. Inside the lambda function, a new KeyValueEntry object kve is created using the message m.

  6. If the operation of the kve object is not KeyValueOperation.PUT, it means it is a delete marker. In this case, it checks if the created timestamp of the delete marker is after the limit date-time. If it is, it adds the key of the delete marker to keep1List. Otherwise, it adds the key to keep0List.

  7. After all messages have been visited, the method iterates over the keep0List and calls the purgeStream method of the jsm object (JetStreamManager) to purge the stream with the stream name using the PurgeOptions.subject method with the readSubject method called on each key as the subject.

  8. Next, it iterates over the keep1List and creates a new PurgeOptions object po using the PurgeOptions.builder() method. It sets the subject of po using the readSubject method called on each key and sets the keep property of po to 1. Finally, it calls the purgeStream method of the jsm object with the stream name and po as arguments to purge the stream.

That's the step-by-step description of the purgeDeletes method in the NatsKeyValue class.

The purgeDeletes method is responsible for purging delete markers in the NatsKeyValue system based on the provided options.

Firstly, it retrieves the delete markers threshold from the options parameter. If no options are provided, it uses the default threshold from KeyValuePurgeOptions.

Next, it calculates the limit based on the threshold. If the threshold is less than 0, it sets the limit to a future time (600000ms from now) to clear all delete markers. If the threshold is 0, it sets the limit based on the default threshold. Otherwise, it sets the limit based on the negative threshold value.

Then, it iterates over the entries in the stream subject and processes them. For each entry, it converts it to a KeyValueEntry object and checks if the operation is not a PUT operation. If the entry's creation time is after the limit, it adds the entry's key to the keep1List. Otherwise, it adds the key to the keep0List.

After processing all entries, it performs two purging operations. Firstly, it purges the stream by iterating over the keys in the keep0List and using jsm.purgeStream() with a PurgeOptions object created from the corresponding key's read subject. Secondly, it purges the stream again by iterating over the keys in the keep1List and using jsm.purgeStream() with a PurgeOptions object created with the subject from the corresponding key and specifying to keep only one message.

Overall, this method is used to remove delete markers from the NatsKeyValue system based on the specified options and thresholds.

sequence diagram

PullOrderedMessageManager

PullOrderedMessageManager

The PullOrderedMessageManager class extends the PullMessageManager class and is responsible for managing the retrieval of ordered messages.

@Override

protected ManageResult manage(Message msg)

@Override
protected ManageResult manage(Message msg) {
    if (!msg.getSID().equals(targetSid.get())) {
        // wrong sid is throwaway from previous consumer that errored
        return STATUS_HANDLED;
    }
    if (msg.isJetStream()) {
        long receivedConsumerSeq = msg.metaData().consumerSequence();
        if (expectedExternalConsumerSeq != receivedConsumerSeq) {
            handleErrorCondition();
            return STATUS_HANDLED;
        }
        trackJsMessage(msg);
        expectedExternalConsumerSeq++;
        return MESSAGE;
    }
    return manageStatus(msg);
}

The manage() method in the PullOrderedMessageManager class is responsible for handling a message received by a consumer. Here is a step-by-step description of what the method does based on its body:

  1. The method starts with an @Override annotation, indicating that it overrides a method defined in a superclass or interface.

  2. The method has a parameter of type Message named msg. This parameter represents the message received by the consumer.

  3. The method first checks if the SID (Session ID) of the message is not equal to the targetSid value.

  4. If the SID does not match, it means that the message is from a previous consumer that encountered an error. In this case, the method returns STATUS_HANDLED, indicating that the message has been successfully handled and can be discarded.

  5. If the SID matches, the method continues to the next condition.

  6. The method checks if the message is a JetStream message by calling the isJetStream() method on the message object.

  7. If the message is a JetStream message, the method retrieves the consumer sequence number from the message's metadata by calling the consumerSequence() method. It compares this value to the expected external consumer sequence number (expectedExternalConsumerSeq).

  8. If the obtained consumer sequence number does not match the expected value, the method calls a handleErrorCondition() method to handle the error condition and returns STATUS_HANDLED.

  9. If the consumer sequence number matches, it means that the message is in the correct order. The method tracks the JetStream message by calling the trackJsMessage() method.

  10. The method increments the expectedExternalConsumerSeq by 1, indicating that the next expected message should have a higher consumer sequence number.

  11. Finally, if the message is not a JetStream message, the method calls another method called manageStatus(msg) to handle the message and return the appropriate status.

In summary, the manage() method in the PullOrderedMessageManager class handles messages received by a consumer by ensuring that the messages are in the correct order based on their consumer sequence numbers. If a message is from a previous consumer or if it has an incorrect sequence number, the method handles the error condition. Otherwise, it tracks the JetStream message and updates the expected sequence number for the next message.

The manage method in the PullOrderedMessageManager class is responsible for managing a received message.

Here's a breakdown of what it does:

  1. First, it checks if the message's session ID (SID) is not the same as the target session ID stored in the class. If they are not equal, it means that the message is from a previous consumer that encountered an error and can be discarded. In this case, it returns STATUS_HANDLED.

  2. If the message is marked as a JetStream message (indicated by the isJetStream flag), it performs the following steps:

    • Checks if the consumer sequence of the message matches the expected external consumer sequence. If they do not match, it indicates that there is a handling error and the handleErrorCondition method is called. Then, it returns STATUS_HANDLED.

    • Tracks the JetStream message by calling the trackJsMessage method.

    • Increments the expected external consumer sequence.

    • Finally, it returns MESSAGE.

  3. If the message is not a JetStream message, it calls the manageStatus method to handle the message status and returns its result.

Overall, the manage method ensures that the received message is correctly handled according to its type (JetStream or non-JetStream) and handles any error conditions that may occur.

sequence diagram

private void handleErrorCondition()

private void handleErrorCondition() {
    try {
        targetSid.set(null);
        // consumer always starts with consumer sequence 1
        expectedExternalConsumerSeq = 1;
        // 1. shutdown the manager, for instance stops heartbeat timers
        shutdown();
        // 2. re-subscribe. This means kill the sub then make a new one
        //    New sub needs a new deliverSubject
        String newDeliverSubject = sub.connection.createInbox();
        sub.reSubscribe(newDeliverSubject);
        targetSid.set(sub.getSID());
        // 3. make a new consumer using the same deliver subject but
        //    with a new starting point
        ConsumerConfiguration userCC = ConsumerConfiguration.builder(originalCc).name(ConsumerUtils.generateConsumerName()).deliverPolicy(DeliverPolicy.ByStartSequence).deliverSubject(newDeliverSubject).startSequence(Math.max(1, lastStreamSeq + 1)).startTime(// clear start time in case it was originally set
        null).build();
        js._createConsumerUnsubscribeOnException(stream, userCC, sub);
        // 4. restart the manager.
        startup(sub);
    } catch (Exception e) {
        IllegalStateException ise = new IllegalStateException("Ordered subscription fatal error.", e);
        js.conn.processException(ise);
        if (syncMode) {
            throw ise;
        }
    }
}

The handleErrorCondition method defined in the io.nats.client.impl.PullOrderedMessageManager class handles error conditions. Here is a step-by-step description of what the method does:

  1. The targetSid variable is set to null. This variable is used to store the subscription ID of the consumer.
  2. The expectedExternalConsumerSeq variable is set to 1. This variable keeps track of the expected external consumer sequence number.
  3. The shutdown method is called. This method shuts down the manager, which includes stopping heartbeat timers.
  4. The sub object, which represents a subscription, is re-subscribed. This means that the current subscription is killed and a new one is created. The new subscription needs a new deliver subject, which is obtained by calling sub.connection.createInbox().
  5. The targetSid variable is set to the subscription ID of the new subscription.
  6. A new consumer is created using the same deliver subject as the new subscription, but with a new starting point. The starting point is set to the maximum of 1 and the last stream sequence number plus 1.
  7. The js._createConsumerUnsubscribeOnException method is called to create a new consumer and unsubscribe the previous one in case of an exception. This method takes the stream name, the user-defined consumer configuration, and the subscription as arguments.
  8. The startup method is called to restart the manager. This method performs any necessary initialization steps for the manager.
  9. If any exception occurs in the try block, an IllegalStateException is created with an error message and the exception as arguments.
  10. The processException method of the connection associated with the js object is called to handle the exception.
  11. If the syncMode flag is true, the IllegalStateException is re-thrown. This allows the caller of the method to handle the exception synchronously.

This method handles errors in ordered subscriptions by shutting down the manager, re-subscribing with a new deliver subject and starting point, creating a new consumer, and restarting the manager. If any exception occurs during this process, it is processed and potentially re-thrown.

The handleErrorCondition method in the PullOrderedMessageManager class handles error conditions during an ordered subscription.

  1. First, it resets the target subscription ID and sets the expected external consumer sequence to 1.
  2. Then, it shuts down the manager, which includes stopping heartbeat timers.
  3. Next, it re-subscribes by creating a new deliver subject and subscribing to it.
  4. After that, it sets the new target subscription ID and creates a new consumer using the same deliver subject but with a new starting point.
  5. It then restarts the manager to resume normal operation.
  6. If any exception occurs during this process, it creates an IllegalStateException with an appropriate error message and propagates it to the parent process. If the method is called in a synchronous mode, it throws the exception.

Overall, this method handles error conditions during an ordered subscription by resetting and re-subscribing to recover from the error.

sequence diagram

PullMessageManager

PullMessageManager

The PullMessageManager class extends the MessageManager class and is responsible for managing and handling pull messages. It provides functionality to retrieve messages from a source and process them accordingly.

@Override

protected void startPullRequest(String pullSubject, PullRequestOptions pro, boolean raiseStatusWarnings, TrackPendingListener trackPendingListener)

@Override
protected void startPullRequest(String pullSubject, PullRequestOptions pro, boolean raiseStatusWarnings, TrackPendingListener trackPendingListener) {
    synchronized (stateChangeLock) {
        this.raiseStatusWarnings = raiseStatusWarnings;
        this.trackPendingListener = trackPendingListener;
        pendingMessages += pro.getBatchSize();
        pendingBytes += pro.getMaxBytes();
        trackingBytes = (pendingBytes > 0);
        configureIdleHeartbeat(pro.getIdleHeartbeat(), -1);
        if (hb) {
            initOrResetHeartbeatTimer();
        } else {
            shutdownHeartbeatTimer();
        }
    }
}

The startPullRequest method is defined in the PullMessageManager class within the io.nats.client.impl package. This method is an overridden implementation of a protected method.

The method takes the following arguments:

  • pullSubject: A string representing the subject used for pulling the messages.
  • pro: An object of type PullRequestOptions that contains various options for the pull request.
  • raiseStatusWarnings: A boolean value indicating whether status warnings should be raised.
  • trackPendingListener: An object implementing the TrackPendingListener interface, used for tracking pending messages.

The method performs the following steps:

  1. Acquires a lock on the stateChangeLock object to synchronize access to shared variables.
  2. Sets the raiseStatusWarnings variable of the class to the value passed as the argument.
  3. Sets the trackPendingListener variable of the class to the value passed as the argument.
  4. Increments the pendingMessages variable of the class by the value of pro.getBatchSize().
  5. Increments the pendingBytes variable of the class by the value of pro.getMaxBytes().
  6. Determines whether the pendingBytes variable is greater than zero and sets the trackingBytes variable accordingly.
  7. Configures the idle heartbeat using the pro.getIdleHeartbeat() value and sets -1 as the maximum allowed number of heartbeats.
  8. Checks the value of the hb variable and executes the following:
    • If hb is true, calls the initOrResetHeartbeatTimer() method to initialize or reset the heartbeat timer.
    • If hb is false, calls the shutdownHeartbeatTimer() method to shutdown/cancel the heartbeat timer.

Once the method completes, the lock on the stateChangeLock object is released.

Note: The purpose and implementation details of the initOrResetHeartbeatTimer() and shutdownHeartbeatTimer() methods are not mentioned in the provided code snippet.

The method startPullRequest is used to initiate a pull request for consuming messages from a NATS server.

The method takes in the following parameters:

  • pullSubject: The subject on which to pull messages.
  • pro: An object of type PullRequestOptions that specifies various options for the pull request, such as batch size, maximum bytes, and idle heartbeat.
  • raiseStatusWarnings: A boolean flag indicating whether to raise status warnings.
  • trackPendingListener: An object of type TrackPendingListener that is used to track pending messages.

Inside the method, a synchronized block is used to ensure thread safety. The method sets the raiseStatusWarnings flag and assigns the trackPendingListener object. It also updates the count of pending messages and pending bytes based on the options specified in pro.

The method then configures the idle heartbeat for the pull request, based on the IdleHeartbeat value in pro. If the idle heartbeat is enabled (hb is true), it initializes or resets the heartbeat timer. Otherwise, it shuts down the heartbeat timer.

sequence diagram

private void trackPending(int m, long b)

private void trackPending(int m, long b) {
    synchronized (stateChangeLock) {
        pendingMessages -= m;
        boolean zero = pendingMessages < 1;
        if (trackingBytes) {
            pendingBytes -= b;
            zero |= pendingBytes < 1;
        }
        if (zero) {
            pendingMessages = 0;
            pendingBytes = 0;
            trackingBytes = false;
            if (hb) {
                shutdownHeartbeatTimer();
            }
        }
        if (trackPendingListener != null) {
            trackPendingListener.track(pendingMessages, pendingBytes, trackingBytes);
        }
    }
}

The trackPending method, defined in the PullMessageManager class within the io.nats.client.impl package, performs the following steps based on its input:

  1. Acquire a lock on the stateChangeLock object to ensure thread safety.
  2. Decrement the pendingMessages variable by the value of m.
  3. Check if pendingMessages is less than 1 and assign the result to the zero variable.
  4. If the trackingBytes flag is enabled, decrement the pendingBytes variable by the value of b and update the zero variable to true if pendingBytes is less than 1.
  5. If the zero variable is true, indicating that both pendingMessages and pendingBytes are now zero, perform the following steps:
    • Set pendingMessages and pendingBytes to 0.
    • Set trackingBytes to false.
    • If the hb flag is enabled, call the shutdownHeartbeatTimer method.
  6. If the trackPendingListener object is not null, call its track method passing the current values of pendingMessages, pendingBytes, and trackingBytes.
  7. Release the lock on the stateChangeLock object.

Note: It is assumed that the pendingMessages, pendingBytes, trackingBytes, hb, trackPendingListener, and stateChangeLock variables are instance variables of the PullMessageManager class.

The trackPending method in the PullMessageManager class is responsible for tracking the number of pending messages and bytes. This method takes two parameters: m, which represents the number of messages to be tracked, and b, which represents the number of bytes to be tracked.

Within this method, the pending message count and byte count are updated based on the provided parameters. If the tracking of bytes is enabled (trackingBytes is set to true), the pending byte count is also updated. If either the pending message count or byte count becomes zero or less, the tracking is stopped and the counters and tracking status are reset.

Additionally, if a trackPendingListener is registered, the track method of the listener is invoked to provide the current values of the pending message count, pending byte count, and tracking status.

Overall, this method is used to keep track of the number of pending messages and bytes in the PullMessageManager class and communicate any changes to a listener if present.

sequence diagram

@Override

protected Boolean beforeQueueProcessorImpl(NatsMessage msg)

@Override
protected Boolean beforeQueueProcessorImpl(NatsMessage msg) {
    // record message time. Used for heartbeat tracking
    messageReceived();
    Status status = msg.getStatus();
    // normal js message
    if (status == null) {
        trackPending(1, msg.consumeByteCount());
        return true;
    }
    // heartbeat just needed to be recorded
    if (status.isHeartbeat()) {
        return false;
    }
    Headers h = msg.getHeaders();
    if (h != null) {
        String s = h.getFirst(NATS_PENDING_MESSAGES);
        if (s != null) {
            try {
                int m = Integer.parseInt(s);
                long b = Long.parseLong(h.getFirst(NATS_PENDING_BYTES));
                trackPending(m, b);
            }// shouldn't happen but don't fail
             catch (NumberFormatException ignore) {
            }
        }
    }
    return true;
}

The beforeQueueProcessorImpl method, defined in the PullMessageManager class in the io.nats.client.impl package, performs the following actions based on its body:

  1. It overrides the beforeQueueProcessorImpl method and takes a NatsMessage object as a parameter.

  2. It calls the messageReceived method to record the time at which the message is received. This time is used for heartbeat tracking.

  3. It gets the status of the message using the getStatus method from the NatsMessage object.

  4. If the status is null, it means it is a normal JavaScript message. It tracks the pending message count and the byte count using the trackPending method and returns true.

  5. If the status is not null and is a heartbeat, it returns false without performing any further actions. Heartbeats are only recorded and not processed.

  6. It gets the headers from the NatsMessage object using the getHeaders method.

  7. If the headers exist, it tries to retrieve a specific header value. It gets the value associated with the NATS_PENDING_MESSAGES key using the getFirst method of the Headers object.

  8. If the value is not null, it converts the value to an integer and retrieves the corresponding value for NATS_PENDING_BYTES from the headers.

  9. It then invokes the trackPending method with the pending message count and byte count.

  10. If any exceptions occur during parsing the header values to integers, they are caught and ignored.

  11. Finally, it returns true.

This method is used to process incoming messages, track pending messages and bytes, and handle heartbeats in the NATS messaging system.

The beforeQueueProcessorImpl method is a protected method in the PullMessageManager class that is overridden with the @Override annotation. This method accepts a NatsMessage object as a parameter.

The purpose of this method is to process the NatsMessage before adding it to the message queue. It performs the following tasks:

  1. It records the time the message is received, which is used for heartbeat tracking.
  2. It checks the status of the message. If the status is null, it means it is a normal JavaScript message, and it proceeds to track the pending message count and consume byte count before returning true.
  3. If the status is a heartbeat, it simply returns false without doing any processing.
  4. It retrieves the headers from the message and checks if there is a header with the name "NATS_PENDING_MESSAGES". If such a header exists, it parses the value as an integer and retrieves the corresponding "NATS_PENDING_BYTES" header. It then tracks the pending message count and bytes based on these values.
  5. Finally, it returns true, indicating that the message has been processed successfully and can be added to the message queue.

Note that if any exceptions occur during parsing of the pending message count or bytes, they are caught and ignored to prevent the method from failing.

sequence diagram

@Override

protected ManageResult manage(Message msg)

@Override
protected ManageResult manage(Message msg) {
    // normal js message
    if (msg.getStatus() == null) {
        trackJsMessage(msg);
        return MESSAGE;
    }
    return manageStatus(msg);
}

The manage method in the PullMessageManager class in the io.nats.client.impl package performs certain actions based on the provided Message object.

Here is a step-by-step description of what the manage method does:

  1. The method overrides the manage method defined in the superclass.
  2. The method takes a Message object as a parameter.
  3. It first checks if the status of the message is null.
  4. If the status is null, it means that it is a normal JavaScript message.
  5. In this case, the method calls the trackJsMessage method, which likely performs some internal tracking or processing of the message.
  6. After tracking the message, the method returns a ManageResult of type MESSAGE.
  7. If the status is not null, it means that the message has a non-null status.
  8. In this case, the method calls the manageStatus method, which is not shown in the provided code snippet.
  9. The manageStatus method likely handles the message according to its status and returns a ManageResult object based on that handling.
  10. The method returns the ManageResult object returned by the manageStatus method.

It is important to note that without the implementation of the trackJsMessage and manageStatus methods, it is unclear what specific actions are being taken for normal JavaScript messages and messages with a status. The provided code snippet only provides the high-level logic of the manage method.

The manage method in the PullMessageManager class, defined in the io.nats.client.impl package, is an overridden method that takes in a Message object as a parameter and returns a ManageResult object.

The method first checks if the status of the Message is null. If it is, it means that the message is a normal JavaScript message. In this case, the method calls the trackJsMessage function to handle the message and then returns a ManageResult with the value MESSAGE.

If the status of the Message is not null, the method calls the manageStatus function to handle the message, and the result of this function call is returned as the ManageResult.

Overall, the manage method in the PullMessageManager class is responsible for managing messages received by the client, distinguishing between normal JavaScript messages and messages with a status, and handling them accordingly.

sequence diagram

protected ManageResult manageStatus(Message msg)

protected ManageResult manageStatus(Message msg) {
    Status status = msg.getStatus();
    switch(status.getCode()) {
        case NOT_FOUND_CODE:
        case REQUEST_TIMEOUT_CODE:
            if (raiseStatusWarnings) {
                conn.executeCallback((c, el) -> el.pullStatusWarning(c, sub, status));
            }
            return STATUS_TERMINUS;
        case CONFLICT_CODE:
            // sometimes just a warning
            String statMsg = status.getMessage();
            if (statMsg.startsWith("Exceeded Max")) {
                if (raiseStatusWarnings) {
                    conn.executeCallback((c, el) -> el.pullStatusWarning(c, sub, status));
                }
                return STATUS_HANDLED;
            }
            if (statMsg.equals(BATCH_COMPLETED) || statMsg.equals(MESSAGE_SIZE_EXCEEDS_MAX_BYTES)) {
                return STATUS_TERMINUS;
            }
            break;
    }
    // fall through, all others are errors
    conn.executeCallback((c, el) -> el.pullStatusError(c, sub, status));
    return STATUS_ERROR;
}

The manageStatus method, defined in the PullMessageManager class in the io.nats.client.impl package, takes a Message object as input and returns a ManageResult object.

Here is a step-by-step description of what the method does based on the provided code:

  1. Get the Status object from the msg parameter.
  2. Use a switch statement to check the code of the Status object.
  3. If the code is equal to NOT_FOUND_CODE or REQUEST_TIMEOUT_CODE:
    • Check if raiseStatusWarnings is true.
    • If true, execute a callback function on the conn object, passing the conn, el, and sub variables, and calling the pullStatusWarning method on el with the c, sub, and status parameters.
    • Return STATUS_TERMINUS.
  4. If the code is equal to CONFLICT_CODE:
    • Get the message from the Status object.
    • If the message starts with "Exceeded Max":
      • Check if raiseStatusWarnings is true.
      • If true, execute a callback function on the conn object, passing the conn, el, and sub variables, and calling the pullStatusWarning method on el with the c, sub, and status parameters.
      • Return STATUS_HANDLED.
    • If the message is equal to BATCH_COMPLETED or MESSAGE_SIZE_EXCEEDS_MAX_BYTES:
      • Return STATUS_TERMINUS.
  5. If none of the above cases match, execute a callback function on the conn object, passing the conn, el, and sub variables, and calling the pullStatusError method on el with the c, sub, and status parameters.
  6. Return STATUS_ERROR.

The manageStatus method in the PullMessageManager class is responsible for handling the status of a message received from a NATS server.

Here is a breakdown of what the method does:

  1. It takes a Message object as input.
  2. It gets the Status object from the Message.
  3. It checks the code of the status using a switch statement.
  4. If the code is NOT_FOUND_CODE or REQUEST_TIMEOUT_CODE, it checks if raiseStatusWarnings is true. If it is, it executes a callback function pullStatusWarning and returns STATUS_TERMINUS.
  5. If the code is CONFLICT_CODE, it checks the message content. If it starts with "Exceeded Max", it checks if raiseStatusWarnings is true. If it is, it executes a callback function pullStatusWarning and returns STATUS_HANDLED.
  6. If the message content is "BATCH_COMPLETED" or "MESSAGE_SIZE_EXCEEDS_MAX_BYTES", it returns STATUS_TERMINUS.
  7. If none of the above cases match, it executes a callback function pullStatusError and returns STATUS_ERROR.

Overall, the method handles different status codes and performs specific actions based on the code and the content of the status message.

Headers

Headers

An object that represents a map of keys (headers) to a list of values. This class does not accept null or invalid keys. It ignores null values, accepts empty strings as a value, and rejects invalid values. It is important to note that this class is not thread-safe.

public Headers add(String key, String... values)

public Headers add(String key, String... values) {
    if (values != null) {
        _add(key, Arrays.asList(values));
    }
    return this;
}

The add method in class io.nats.client.impl.Headers is used to add a key-value pair to the existing headers. It accepts a key parameter which represents the header name, and a values parameter which represents the values to be associated with the header.

Here is the step-by-step description of what the add method does based on its body:

  1. Check if the values parameter is not null. If it is null, then there is nothing to add, so the method just returns the current instance of Headers.

  2. If the values parameter is not null, it means that there are values to be added to the header.

  3. Call the _add method, which is a private method defined in the same class, to actually add the key-value pair to the headers. The _add method takes the key and values parameters as arguments.

  4. Convert the values array to a List using the Arrays.asList method. This is done to make it easier to work with the values.

  5. Pass the converted list of values to the _add method along with the key.

  6. After the _add method has added the key-value pair to the headers, the add method returns the current instance of Headers. This allows for method chaining, where multiple calls to add can be made in a single statement.

The add method defined in the io.nats.client.impl.Headers class is used to add key-value pairs to the headers. It takes a key and one or more values as parameters. If the values parameter is not null, the method calls the internal _add method to add the key-value pair to the headers. The method then returns the updated headers object.

sequence diagram

// the add delegate

private void _add(String key, Collection values)

// the add delegate
private void _add(String key, Collection<String> values) {
    if (values != null) {
        Checker checked = new Checker(key, values);
        if (checked.hasValues()) {
            // get values by key or compute empty if absent
            // update the data length with the additional len
            // update the lengthMap for the key to the old length plus the new length
            List<String> currentSet = valuesMap.computeIfAbsent(key, k -> new ArrayList<>());
            currentSet.addAll(checked.list);
            dataLength += checked.len;
            int oldLen = lengthMap.getOrDefault(key, 0);
            lengthMap.put(key, oldLen + checked.len);
            // since the data changed, clear this so it's rebuilt
            serialized = null;
        }
    }
}

Method Description: _add(String key, Collection values)

This method is defined in the Headers class of the io.nats.client.impl package. It is a private method that takes two parameters: a String key and a Collection of Strings values.

Step 1: Check if values are not null

The method first checks if the passed values parameter is not null.

Step 2: Perform necessary operations if values are not null

If values is not null, the method initializes a Checker object with the key and values. The Checker is responsible for validating and processing the values.

Step 3: Validate and process the values

The method calls the hasValues() method of the Checker object to determine if there are valid values to process. If checked.hasValues() returns true, it means that there are valid values.

Step 4: Update the data length and lengthMap

The method retrieves the current set of values associated with the given key from the valuesMap using the computeIfAbsent() method. If there is no existing set of values, a new ArrayList is created and associated with the key.

The method then adds all the validated values from the Checker object to the current set of values.

The dataLength is updated by adding the length of the validated values.

The oldLen is retrieved from the lengthMap using the getOrDefault() method. If there is no existing length for the key, a default value of 0 is used.

The lengthMap is then updated by adding the new length of the values to the existing length.

Step 5: Clear serialized data

Finally, since the data has changed, the method sets the serialized variable to null, indicating that the serialized data needs to be rebuilt.

It is important to note that this method is private, indicating that it is not intended to be accessed or called directly by other classes. It is called internally within the Headers class.

The method _add in the class io.nats.client.impl.Headers is used to add key-value pairs to the headers. It takes in a key and a collection of values as parameters.

First, it checks if the collection of values is not null. Then, it creates a Checker object to validate the key and values. If the Checker determines that there are valid values, the method performs the following steps:

  1. It retrieves the current set of values associated with the key from the valuesMap. If there are no values, it creates a new empty ArrayList.

  2. It adds all the validated values from the Checker object to the current set of values.

  3. It updates the dataLength variable by adding the length of the values.

  4. It gets the old length associated with the key from the lengthMap and adds the length of the values to it.

  5. It sets the serialized variable to null, indicating that the data has changed and needs to be rebuilt.

In summary, this method allows you to add key-value pairs to the headers, keeping track of the length of the data and updating internal data structures accordingly.

sequence diagram

public Headers put(String key, String... values)

public Headers put(String key, String... values) {
    if (values != null) {
        _put(key, Arrays.asList(values));
    }
    return this;
}

The put method in the Headers class, defined in the package io.nats.client.impl, is used to add or update a header entry with the provided key and values.

Here is a step-by-step description of what this method does:

  1. Start by checking if the values parameter is not null.

  2. If values is not null, proceed to the next step. Otherwise, skip the following steps and return the current instance of Headers.

  3. Call the _put method of the Headers class, passing the key and values as arguments.

  4. The _put method is responsible for adding or updating the header entry. It takes the key and values as parameters.

  5. Convert the values array to a List using the Arrays.asList method.

  6. Add or update the entry in the underlying data structure of the Headers class, using the key and the List of values.

  7. After updating the header entry, return the current instance of Headers.

Following this step-by-step process, the put method allows you to add or update the header entry with the provided key and values in the Headers class.

The put method in the Headers class is used to add or update the values associated with a particular header key. The method takes a key and a variable number of values as input.

First, the method checks if the values array is not null. If it is not null, the method calls the _put method, which internally updates the header values map with the given key and values.

Finally, the method returns the updated Headers object, allowing for method chaining. This allows multiple put calls to be chained together to add or update multiple headers in a single line of code.

sequence diagram

public Headers put(Map<String, List> map)

public Headers put(Map<String, List<String>> map) {
    for (String key : map.keySet()) {
        put(key, map.get(key));
    }
    return this;
}

Method Description: put(Map<String, List> map)

The put method in the Headers class is used to add multiple key-value pairs to the existing headers.

Step-by-step Description:

  1. The put method takes a parameter map of type Map<String, List<String>> which represents the key-value pairs to be added to the headers.

  2. It loops over each key in the map using a for-each loop.

  3. For each key, it calls the put method (overloaded) of the Headers class and passes the key along with the corresponding value from the map.

  4. After adding all the key-value pairs from the map to the headers, the put method returns the updated Headers object.

Example Usage:

Map<String, List<String>> map = new HashMap<>();
map.put("Content-Type", Arrays.asList("application/json"));
map.put("Authorization", Arrays.asList("Bearer token"));

Headers headers = new Headers();
headers.put(map);

The put method in the Headers class is used to add or update headers in the headers map of a NATS message. The method takes a Map object as input, where the keys are strings representing the header names, and the values are lists of strings representing the header values.

The method iterates over the keys in the input map and calls the put method (presumably another overloaded method in the Headers class) to add or update each header in the headers map. Finally, it returns the Headers object itself, allowing method chaining.

Essentially, this method provides a convenient way to add multiple headers to the headers map of a NATS message at once.

sequence diagram

// the put delegate that all puts call

private void _put(String key, Collection values)

// the put delegate that all puts call
private void _put(String key, Collection<String> values) {
    if (key == null || key.length() == 0) {
        throw new IllegalArgumentException("Key cannot be null or empty.");
    }
    if (values != null) {
        Checker checked = new Checker(key, values);
        if (checked.hasValues()) {
            // update the data length removing the old length adding the new length
            // put for the key
            dataLength = dataLength - lengthMap.getOrDefault(key, 0) + checked.len;
            valuesMap.put(key, checked.list);
            lengthMap.put(key, checked.len);
            // since the data changed, clear this so it's rebuilt
            serialized = null;
        }
    }
}

The _put method in the Headers class is responsible for adding or updating key-value pairs in the valuesMap and lengthMap fields.

Here is a step-by-step description of what the _put method does:

  1. Check if the key parameter is null or empty.

    • If it is null or empty, throw an IllegalArgumentException with the message "Key cannot be null or empty."
  2. Check if the values parameter is not null.

    • If it is not null, proceed with the following steps.
  3. Create a new instance of the Checker class, passing the key and values parameters.

    • The Checker class is responsible for validating and processing the collection of values.
  4. Check if the Checker object has values to be processed.

    • If it has values, proceed with the following steps.
  5. Calculate the new data length:

    • Subtract the old length associated with the key (from the lengthMap) if it exists.
    • Add the new length associated with the key (from the Checker object).
  6. Update the valuesMap, lengthMap, and serialized fields:

    • Set the key in the valuesMap to the list of values from the Checker object.
    • Set the key in the lengthMap to the new length from the Checker object.
    • Set the serialized field to null, indicating that it needs to be rebuilt.

That's the step-by-step description of what the _put method in the Headers class does based on its body.

The _put method in the io.nats.client.impl.Headers class is responsible for adding or updating a key-value pair in the headers of a NATS message.

The method takes a key and a collection of values as parameters. It first checks if the key is null or empty, and throws an exception if it is.

If the values collection is not null, the method validates and checks the values using a Checker object. If the values are valid, the method updates the data length of the headers by subtracting the old length associated with the given key (if any) and adding the new length.

The method then updates the valuesMap, which stores the key-value pairs, with the new values and updates the lengthMap, which stores the lengths of the values associated with each key. Finally, it clears the serialized field, so that the headers are rebuilt the next time they are accessed.

sequence diagram

public void remove(String... keys)

public void remove(String... keys) {
    for (String key : keys) {
        _remove(key);
    }
    // since the data changed, clear this so it's rebuilt
    serialized = null;
}

Method: remove(keys)

This method is defined in the class io.nats.client.impl.Headers and is used to remove one or more keys from the headers.

Step 1:

Iterate through each key in the keys array.

Step 2:

Call the private method _remove(key) to remove the key from the headers.

Step 3:

Reset the value of the serialized variable to null. This is done to indicate that the data has changed and needs to be rebuilt.

Once the method is executed, the specified keys will be removed from the headers and the serialized variable will be reset to null.

The remove method in the io.nats.client.impl.Headers class is used to remove one or more keys from the header data. It accepts one or more key strings as parameter, iterates over each key, and calls the private method _remove to remove the corresponding header. After removing the header(s), it also clears the serialized variable, which indicates that the header data has changed and needs to be rebuilt.

sequence diagram

public void remove(Collection keys)

public void remove(Collection<String> keys) {
    for (String key : keys) {
        _remove(key);
    }
    // since the data changed, clear this so it's rebuilt
    serialized = null;
}

Method: remove

Description:

The remove method removes the specified keys from the Headers object. It takes a collection of keys as input and iterates through each key to remove it from the headers.

Steps:

  1. Start by looping through each key in the keys collection.
  2. For each key, call the _remove method with the key as the parameter. This method will remove the key from the headers.
  3. Once all keys have been removed, the serialized field is set to null. This field holds the serialized representation of the headers and by setting it to null, it ensures that the serialized version will be rebuilt the next time it is accessed.

Note: This method assumes that the _remove method is implemented properly to remove the specified key from the headers.

Method Description

The remove method in the io.nats.client.impl.Headers class is used to remove multiple keys from the headers collection.

Parameters

  • keys (Collection): A collection of strings representing the keys to be removed from the headers collection.

Functionality

  • The method iterates through each key in the keys collection and removes it from the headers collection by calling the _remove method internally.
  • After removing the keys, the serialized variable, which stores the serialized version of the headers collection, is set to null. This ensures that the serialized version gets rebuilt if needed.

private void _remove(String key)

private void _remove(String key) {
    // if the values had a key, then the data length had a length
    if (valuesMap.remove(key) != null) {
        dataLength -= lengthMap.remove(key);
    }
}

Method _remove

The _remove method is defined in the Headers class, located in the io.nats.client.impl package.

Purpose

The purpose of the _remove method is to remove a specified key from the Headers object by removing its corresponding entry from the valuesMap and reducing the dataLength by the length of the removed value.

Parameters

  • key : The key of the entry to be removed from the Headers object.

Code

private void _remove(String key) {
    if (valuesMap.remove(key) != null) {
        dataLength -= lengthMap.remove(key);
    }
}

Steps

  1. The method begins by attempting to remove the entry identified by the key parameter from the valuesMap.
  2. The remove method returns the previous value associated with the specified key, or null if there was no mapping for the key.
  3. If the return value is not null, indicating that an entry was successfully removed from the valuesMap, the method proceeds to the next step.
  4. The length of the removed value is obtained from the lengthMap using the same key that was used to remove the entry from the valuesMap.
  5. The length of the removed value is subtracted from the dataLength variable to update its value.
  6. The method execution completes.

Related Classes/Objects

  • Headers: The class that contains the _remove method.
  • valuesMap: A data structure that stores key-value pairs representing the headers.
  • dataLength: A counter that keeps track of the total length of the data stored in the Headers object.
  • lengthMap: A mapping of keys to their corresponding lengths in the Headers object.

The _remove method, defined in the io.nats.client.impl.Headers class, is used to remove a key-value pair from the headers of a NATS message.

The method takes a key as a parameter and performs the following steps:

  1. It checks if the valuesMap contains the specified key. If the key exists, it removes the corresponding value from the valuesMap.
  2. Additionally, it removes the corresponding length value from the lengthMap associated with the key.
  3. If the removal was successful, it subtracts the length value from the dataLength variable.

In summary, this method removes a key-value pair from the headers and updates the data length accordingly.

sequence diagram

public void clear()

public void clear() {
    valuesMap.clear();
    lengthMap.clear();
    dataLength = 0;
    serialized = null;
}

The clear() method, defined in the Headers class of the io.nats.client.impl package, is used to clear the state of the headers object. This method performs the following steps:

  1. Clear the valuesMap: This removes all key-value pairs stored in the valuesMap, which is a map used to store the header name and its corresponding value.

  2. Clear the lengthMap: This clears the lengthMap, which is a map used to store the length of the serialized version of each header.

  3. Reset the dataLength variable: This sets the dataLength variable to 0, indicating that there is no data stored in the headers after clearing.

  4. Set the serialized variable to null: The serialized variable is a byte array that holds the serialized representation of the headers. Setting it to null indicates that there is no serialized data present after clearing the headers.

By executing these steps, the clear() method ensures that all data stored in the headers object is removed, effectively clearing its state and preparing it for further usage.

The clear method in the Headers class, defined in the io.nats.client.impl package, is used to clear the existing data in the headers object.

This method performs the following actions:

  1. Clears the valuesMap and lengthMap data structures, removing all key-value pairs.
  2. Resets the dataLength variable to 0.
  3. Sets the serialized field to null.

After calling the clear method, the headers object will have no data or state stored.

sequence diagram

public boolean containsKeyIgnoreCase(String key)

public boolean containsKeyIgnoreCase(String key) {
    for (String k : valuesMap.keySet()) {
        if (k.equalsIgnoreCase(key)) {
            return true;
        }
    }
    return false;
}

The containsKeyIgnoreCase method defined in class io.nats.client.impl.Headers is used to check if the given key exists in the valuesMap ignoring the case sensitivity.

Here is a step-by-step description of how the method works:

  1. Initialize the for loop: Iterate over each key, k, in the valuesMap using a for-each loop.

  2. Check for a case-insensitive match: Compare the key, k, with the input key ignoring any differences in case.

  3. Case-insensitive match found: If k matches key ignoring case, return true to indicate that the key exists in the valuesMap.

  4. Repeat for each key: Continue the for loop, checking the next key in the valuesMap.

  5. No match found: If the loop completes without finding a case-insensitive match for key in any of the keys in valuesMap, return false to indicate that the key does not exist in the valuesMap.

That's how the containsKeyIgnoreCase method functions to determine if a specified key is present in the valuesMap of the Headers class, regardless of case sensitivity.

The containsKeyIgnoreCase method, defined in the Headers class in the io.nats.client.impl package, determines whether the valuesMap contains a key that matches the provided key (case-insensitive).

It iterates through all the keys in the valuesMap and compares each key with the provided key using a case-insensitive comparison. If a match is found, the method returns true. Otherwise, if no match is found, the method returns false.

sequence diagram

public Set keySetIgnoreCase()

public Set<String> keySetIgnoreCase() {
    HashSet<String> set = new HashSet<>();
    for (String k : valuesMap.keySet()) {
        set.add(k.toLowerCase());
    }
    return Collections.unmodifiableSet(set);
}

Method Description: keySetIgnoreCase

This method keySetIgnoreCase is a member of the Headers class in the io.nats.client.impl package. It returns a Set of case-insensitive keys present in the valuesMap of the Headers object. The returned set is a read-only view and cannot be modified.

Method Signature

public Set<String> keySetIgnoreCase()

Method Body

public Set<String> keySetIgnoreCase() {
    HashSet<String> set = new HashSet<>();
    for (String k : valuesMap.keySet()) {
        set.add(k.toLowerCase());
    }
    return Collections.unmodifiableSet(set);
}

Steps Overview:

  1. Create an empty HashSet named set to store the case-insensitive keys.
  2. Iterate over all the keys present in the valuesMap using a for-each loop.
  3. Inside the loop, convert each key to lowercase using the toLowerCase() method and add it to the set.
  4. Once all keys have been processed, return the set as an unmodifiable set using the Collections.unmodifiableSet() method.

Detailed Steps:

  1. Create an empty HashSet named set to store the case-insensitive keys.
HashSet<String> set = new HashSet<>();
  1. Iterate over all the keys present in the valuesMap using a for-each loop.
for (String k : valuesMap.keySet()) {
  1. Inside the loop, convert each key to lowercase using the toLowerCase() method and add it to the set.
    set.add(k.toLowerCase());
  1. Once all keys have been processed, return the set as an unmodifiable set using the Collections.unmodifiableSet() method.
}
return Collections.unmodifiableSet(set);

The keySetIgnoreCase method in the io.nats.client.impl.Headers class is used to retrieve a set of keys from the Headers object, ignoring the case of the keys.

It creates a new HashSet object to store the keys and then iterates over the existing keys in the valuesMap of the Headers object. For each key, it converts the key to lowercase and adds it to the set.

Finally, it returns an unmodifiable set of keys, ensuring that the original set cannot be modified.

sequence diagram

@Deprecated

public ByteArrayBuilder appendSerialized(ByteArrayBuilder bab)

@Deprecated
public ByteArrayBuilder appendSerialized(ByteArrayBuilder bab) {
    bab.append(HEADER_VERSION_BYTES_PLUS_CRLF);
    for (String key : valuesMap.keySet()) {
        for (String value : valuesMap.get(key)) {
            bab.append(key);
            bab.append(COLON_BYTES);
            bab.append(value);
            bab.append(CRLF_BYTES);
        }
    }
    bab.append(CRLF_BYTES);
    return bab;
}

The appendSerialized method in the Headers class, located in the io.nats.client.impl package, is used to append the serialized header values to a ByteArrayBuilder object. Here is a step-by-step description of what this method does based on its body:

  1. Declare the method as @Deprecated, indicating that it is no longer recommended for use.

  2. The method takes a ByteArrayBuilder object, bab, as a parameter.

  3. Append the characters represented by HEADER_VERSION_BYTES_PLUS_CRLF to the bab object. It is assumed that this constant variable contains the serialized representation of the version of the headers, followed by a carriage return and line feed.

  4. Iterate over the keys present in the valuesMap map. The valuesMap map is assumed to be a mapping of header keys to a list of values.

  5. For each key in the valuesMap map, iterate over the values associated with that key.

  6. Append the key to the bab object using the append method of ByteArrayBuilder.

  7. Append the characters represented by COLON_BYTES to the bab object. It is assumed that this constant variable contains the serialized representation of the colon character.

  8. Append the value to the bab object using the append method of ByteArrayBuilder.

  9. Append the characters represented by CRLF_BYTES to the bab object. It is assumed that this constant variable contains the serialized representation of a carriage return and line feed.

  10. Repeat steps 6-9 for each value associated with the key.

  11. Repeat steps 5-10 for each key in the valuesMap map.

  12. Append the characters represented by CRLF_BYTES to the bab object again.

  13. Return the bab object.

Note: The specific details of the constant variables (HEADER_VERSION_BYTES_PLUS_CRLF, COLON_BYTES, and CRLF_BYTES) and how the valuesMap map is populated are not given in the provided code. These details may affect the behavior and purpose of the appendSerialized method.

The method appendSerialized in the class Headers is used to append serialized header values to a ByteArrayBuilder object.

It first appends a specific header version to the ByteArrayBuilder. Then, it iterates over each key in the valuesMap and for each key, it iterates over the corresponding list of values. For each value, it appends the key, a colon, the value, and a new line character to the ByteArrayBuilder.

After appending all the key-value pairs, it adds a new line character to the ByteArrayBuilder and returns it.

sequence diagram

public int serializeToArray(int destPosition, byte[] dest)

public int serializeToArray(int destPosition, byte[] dest) {
    System.arraycopy(HEADER_VERSION_BYTES_PLUS_CRLF, 0, dest, destPosition, HVCRLF_BYTES);
    destPosition += HVCRLF_BYTES;
    for (Map.Entry<String, List<String>> entry : valuesMap.entrySet()) {
        List<String> values = entry.getValue();
        for (String value : values) {
            byte[] bytes = entry.getKey().getBytes(US_ASCII);
            System.arraycopy(bytes, 0, dest, destPosition, bytes.length);
            destPosition += bytes.length;
            dest[destPosition++] = COLON;
            bytes = value.getBytes(US_ASCII);
            System.arraycopy(bytes, 0, dest, destPosition, bytes.length);
            destPosition += bytes.length;
            dest[destPosition++] = CR;
            dest[destPosition++] = LF;
        }
    }
    dest[destPosition++] = CR;
    dest[destPosition] = LF;
    return serializedLength();
}

The serializeToArray method in the Headers class is responsible for serializing the header values into a byte array.

Here is a step-by-step description of what the method is doing:

  1. It starts by copying the HEADER_VERSION_BYTES_PLUS_CRLF byte array to the dest byte array starting from the destPosition. The HEADER_VERSION_BYTES_PLUS_CRLF consists of the header version followed by a carriage return (CR) and line feed (LF) characters.

  2. The destPosition is incremented by HVCRLF_BYTES, which is the length of HEADER_VERSION_BYTES_PLUS_CRLF.

  3. The method then iterates over the key-value pairs in the valuesMap.

  4. For each key-value pair, it gets the list of values associated with the key.

  5. It loops over each value in the list.

  6. It converts the key and value to bytes using the US_ASCII charset.

  7. It then copies the key bytes to the dest byte array starting from the destPosition.

  8. The destPosition is incremented by the length of the key bytes.

  9. It adds a colon character to the dest byte array at destPosition.

  10. It then copies the value bytes to the dest byte array starting from the destPosition.

  11. The destPosition is incremented by the length of the value bytes.

  12. It adds a carriage return (CR) and line feed (LF) characters to the dest byte array at destPosition.

  13. The destPosition is incremented by 2 to account for the CR and LF characters.

  14. This process is repeated for all the values in the list associated with the key.

  15. After iterating over all the key-value pairs, it adds a carriage return (CR) and line feed (LF) characters to the dest byte array at destPosition.

  16. The destPosition is incremented by 2.

  17. Finally, it returns the length of the serialized header values.

The serializeToArray method essentially serializes the header values into a byte array by appending the key-value pairs along with the necessary characters to separate them.

The serializeToArray method in the io.nats.client.impl.Headers class is used to serialize the headers into a byte array.

It takes two parameters: destPosition, which specifies the starting position in the dest byte array where the serialized headers will be copied to, and dest, which is the destination byte array.

The method first copies the header version bytes followed by carriage return and line feed characters to the dest byte array at the specified destPosition. Then, it iterates over each entry in the valuesMap (a map of header names to lists of header values), and for each entry, it iterates over the list of header values. For each value, it copies the bytes of the header name and value followed by a colon character, and appends the carriage return and line feed characters to the dest byte array. After processing all the headers, it appends the final carriage return and line feed characters to the dest byte array. Finally, it returns the total length of the serialized headers.

Overall, this method serializes the headers and appends them to the dest byte array, returning the length of the serialized headers.

sequence diagram

private void checkKey(String key)

private void checkKey(String key) {
    // key cannot be null or empty and contain only printable characters except colon
    if (key == null || key.length() == 0) {
        throw new IllegalArgumentException(KEY_CANNOT_BE_EMPTY_OR_NULL);
    }
    int len = key.length();
    for (int idx = 0; idx < len; idx++) {
        char c = key.charAt(idx);
        if (c < 33 || c > 126 || c == ':') {
            throw new IllegalArgumentException(KEY_INVALID_CHARACTER + "'" + c + "'");
        }
    }
}

Method: checkKey()

This method is defined in the class io.nats.client.impl.Headers and is used to validate whether a given key is valid or not. The method takes a String parameter key as input.

Steps:

  1. Check if the provided key is null or empty:

    if (key == null || key.length() == 0) {
        throw new IllegalArgumentException(KEY_CANNOT_BE_EMPTY_OR_NULL);
    }
    • If the key is null or empty, an IllegalArgumentException is thrown with the message "Key cannot be empty or null."
  2. Calculate the length of the key:

    int len = key.length();
  3. Iterate through each character of the key:

    for (int idx = 0; idx < len; idx++) {
        char c = key.charAt(idx);
        // Perform validation checks on each character
    }
  4. Perform validation checks on each character:

    • Check if the character is less than 33 (ASCII value) or greater than 126 (ASCII value) or equal to ':'.
        if (c < 33 || c > 126 || c == ':') {
            throw new IllegalArgumentException(KEY_INVALID_CHARACTER + "'" + c + "'");
        }
    • If the character does not meet the above conditions, an IllegalArgumentException is thrown with the message "Invalid character found in key: ''", where '' represents the specific character that failed the validation.

Note: The purpose of this method is to ensure that the provided key is not null, not empty, and contains only printable characters except for the colon (':').

The checkKey method in the Headers class of the io.nats.client.impl package is used to validate a given key string.

It checks if the key is null or empty, and throws an IllegalArgumentException with an appropriate error message if it is.

Then, it iterates over each character in the key string and checks if it is a printable character (ASCII value between 33 and 126) and not a colon. If any character fails this check, it throws an IllegalArgumentException with an error message indicating the invalid character.

In summary, this method ensures that a key string meets certain criteria (not null or empty, contains only printable characters except colon) and throws an exception if any invalid conditions are found.

sequence diagram

private void checkValue(String val)

private void checkValue(String val) {
    // Generally more permissive than HTTP.  Allow only printable
    // characters and include tab (0x9) to cover what's allowed
    // in quoted strings and comments.
    val.chars().forEach(c -> {
        if ((c < 32 && c != 9) || c > 126) {
            throw new IllegalArgumentException(VALUE_INVALID_CHARACTERS + c);
        }
    });
}

The checkValue method in the Headers class, located in the io.nats.client.impl package, performs a validation check on a given val parameter. This method ensures that the characters in the provided val are within the allowable range.

Step by step description of the checkValue method:

  1. The method starts with a private visibility modifier, indicating that it can only be accessed within the Headers class.

  2. The method takes a single parameter, val, which is a String representing the value to be checked.

  3. The method uses the chars() method of the val string to convert it into an IntStream of Unicode code points.

  4. For each character in the IntStream, the method performs the following validation check:

    a. If the character's code point is less than 32 (excluding the tab character with a code point of 9) or greater than 126, an IllegalArgumentException is thrown. This exception includes the value of the invalid character.

  5. The method does not return any value, as it is a void method.

In summary, the checkValue method ensures that the characters in the provided val are printable and within the allowable range, throwing an exception if any invalid character is found.

The checkValue method, defined in the Headers class of the io.nats.client.impl package, is used to validate a value before it is set or added to a header.

The method iterates over each character in the val string and checks if it falls within the range of printable characters. It allows printable characters, including tabs (0x9), which are commonly used in quoted strings and comments.

If a character in the val string is found to be outside the permissible range (less than 32 and not equal to 9, or greater than 126), an IllegalArgumentException is thrown with a specific message indicating the invalid character.

sequence diagram

SimplifiedSubscriptionMaker

SimplifiedSubscriptionMaker

The SimplifiedSubscriptionMaker class is a public class used for creating simplified subscriptions. It provides functionality and methods for managing subscriptions in a simplified manner.

public NatsJetStreamPullSubscription makeSubscription(MessageHandler messageHandler) throws IOException, JetStreamApiException

public NatsJetStreamPullSubscription makeSubscription(MessageHandler messageHandler) throws IOException, JetStreamApiException {
    if (messageHandler == null) {
        return (NatsJetStreamPullSubscription) js.subscribe(subscribeSubject, pso);
    }
    dispatcher = js.conn.createDispatcher();
    return (NatsJetStreamPullSubscription) js.subscribe(subscribeSubject, dispatcher, messageHandler, pso);
}

Steps for the makeSubscription method in io.nats.client.impl.SimplifiedSubscriptionMaker:

  1. Check if the messageHandler parameter is null.
  2. If messageHandler is null, call the subscribe method of the js object with subscribeSubject and pso as arguments. This will create and return a NatsJetStreamPullSubscription object.
  3. If messageHandler is not null, create a new dispatcher by calling the createDispatcher method of the js.conn object.
  4. Call the subscribe method of the js object with subscribeSubject, dispatcher, messageHandler, and pso as arguments. This will create and return a NatsJetStreamPullSubscription object.
  5. Return the NatsJetStreamPullSubscription object obtained from the previous step.

Note: This method can throw IOException and JetStreamApiException, so make sure to handle these exceptions appropriately.

The makeSubscription method in the SimplifiedSubscriptionMaker class is used to create a subscription to a NATS JetStream pull-based consumer group.

The method takes a MessageHandler object as a parameter, which is responsible for processing incoming messages. If the messageHandler is null, the method creates and returns a NatsJetStreamPullSubscription by subscribing directly to the subscribeSubject with the specified pso (PullSubOptions).

If a messageHandler is provided, the method creates a NATS Dispatcher using js.conn.createDispatcher() and then subscribes to the subscribeSubject using the dispatcher, messageHandler, and the pso. Finally, it returns a NatsJetStreamPullSubscription.

Overall, the makeSubscription method allows for the creation of a pull-based subscription to a NATS JetStream consumer group, either with or without a custom MessageHandler.

sequence diagram

StatusMessage

StatusMessage

StatusMessage is a public class that extends the IncomingMessage class. It represents a message that contains a status update. This class provides functionality to create, manipulate, and access status messages in a software application.

ConsumerNamesReader

ConsumerNamesReader

ConsumerNamesReader is a class that extends the StringListReader class. It is responsible for reading and processing a list of consumer names. This class provides methods and functionality to retrieve and manipulate the consumer names from a specified source.

NatsWatchSubscription

NatsWatchSubscription

The NatsWatchSubscription<T> class is a public class that implements the AutoCloseable interface. It represents a subscription to a NATS streaming server. This class allows the user to watch and receive messages from the server. It is generic and can be parameterized with a specific type T for the messages being received. The NatsWatchSubscription<T> class also provides a method to close the subscription and release any resources associated with it.

protected void finishInit(NatsFeatureBase fb, String subscribeSubject, DeliverPolicy deliverPolicy, boolean headersOnly, WatchMessageHandler handler) throws IOException, JetStreamApiException

protected void finishInit(NatsFeatureBase fb, String subscribeSubject, DeliverPolicy deliverPolicy, boolean headersOnly, WatchMessageHandler<T> handler) throws IOException, JetStreamApiException {
    if (deliverPolicy == DeliverPolicy.New || fb._getLast(subscribeSubject) == null) {
        handler.sendEndOfData();
    }
    PushSubscribeOptions pso = PushSubscribeOptions.builder().stream(fb.getStreamName()).ordered(true).configuration(ConsumerConfiguration.builder().ackPolicy(AckPolicy.None).deliverPolicy(deliverPolicy).headersOnly(headersOnly).filterSubject(subscribeSubject).build()).build();
    dispatcher = (NatsDispatcher) ((NatsJetStream) js).conn.createDispatcher();
    sub = js.subscribe(subscribeSubject, dispatcher, handler, false, pso);
    if (!handler.endOfDataSent) {
        long pending = sub.getConsumerInfo().getCalculatedPending();
        if (pending == 0) {
            handler.sendEndOfData();
        }
    }
}

The finishInit method in the NatsWatchSubscription class is responsible for initializing and completing the setup of a particular subscription. Here is a step-by-step breakdown of what the method does:

  1. Check if the deliverPolicy is set to "New" or if the last message received for the subscribeSubject is null.

    • If true, call the sendEndOfData method on the provided handler object. This is used to indicate that there is no more data to be received for the subscription.
  2. Create a PushSubscribeOptions object with the following configurations:

    • Set the stream name using fb.getStreamName().
    • Enable ordered delivery.
    • Create a ConsumerConfiguration object with the following settings:
      • Set the acknowledgement policy to "None".
      • Set the deliver policy to the provided deliverPolicy.
      • Set the headersOnly flag.
      • Set the filter subject to the subscribeSubject.
    • Build and configure the PushSubscribeOptions object.
  3. Create a dispatcher object by casting the js (NatsJetStream) connection's dispatcher to NatsDispatcher.

  4. Subscribe to the subscribeSubject using the js (NatsJetStream) object with the provided dispatcher, handler, and pso (PushSubscribeOptions).

    • Set the false argument to indicate that the consumer is not durable.
  5. Check if the endOfDataSent flag on the handler object is not set.

    • If true, get the calculated number of pending messages using sub.getConsumerInfo().getCalculatedPending().
    • Check if the pending count is equal to 0.
      • If true, call the sendEndOfData method on the handler object.

This method initializes and sets up the subscription for receiving messages based on the provided parameters, and also handles cases where there is no data or pending messages to be received.

The finishInit method in the NatsWatchSubscription class is responsible for finalizing the initialization of a NatsWatchSubscription object.

This method takes several parameters such as fb (an instance of NatsFeatureBase), subscribeSubject (the subject to subscribe to), deliverPolicy (the delivery policy), headersOnly (indicating if only headers should be received), and handler (an instance of WatchMessageHandler).

The method first checks if the deliverPolicy is set to DeliverPolicy.New or if there are no previously received messages for the subscribed subject. If either of these conditions is true, it calls the sendEndOfData method on the handler to indicate the end of data.

Next, it creates a PushSubscribeOptions object with the appropriate configuration options for the subscription. It then creates a NatsDispatcher using the connection from the NatsJetStream object and assigns it to the dispatcher variable.

The method then subscribes to the subscribeSubject using the js (NatsJetStream) object, the dispatcher, the handler, and the PushSubscribeOptions object.

Finally, it checks if the sendEndOfData method has already been called on the handler. If not, it retrieves the number of pending messages from the subscription and if there are no pending messages, it calls the sendEndOfData method on the handler.

In summary, the finishInit method initializes a NatsWatchSubscription object by setting up the subscription to a specified subject, configuring the delivery options, and ensuring that the appropriate end of data indication is sent to the message handler.

public void unsubscribe()

public void unsubscribe() {
    if (dispatcher != null) {
        dispatcher.unsubscribe(sub);
        if (dispatcher.getSubscriptionHandlers().size() == 0) {
            dispatcher.connection.closeDispatcher(dispatcher);
            dispatcher = null;
        }
    }
}

Method Description - unsubscribe()

This method is defined in the NatsWatchSubscription class of the io.nats.client.impl package. It is used to unsubscribe from a subscription.

The method has the following steps:

  1. Check if the dispatcher variable is not null.
  2. If dispatcher is not null, call the unsubscribe() method on the dispatcher object, passing the sub (subscription) parameter.
  3. Check if the dispatcher object's getSubscriptionHandlers() method returns a size of 0.
  4. If the size is 0, it means that there are no more handlers subscribed to the dispatcher. In this case, call the closeDispatcher() method on the connection object, passing the dispatcher as a parameter, and set dispatcher variable to null.

Note: The dispatcher and connection objects are assumed to be defined in the NatsWatchSubscription class.

The unsubscribe method, defined in the NatsWatchSubscription class of the io.nats.client.impl package, is responsible for unsubscribing from a NATS topic subscription.

The method first checks if the dispatcher (a NatsDispatcher object) is not null. If it is not null, it calls the unsubscribe method of the dispatcher object, passing the sub (a SubscriptionImpl object) as an argument. This effectively unsubscribes from the topic.

Next, it checks if there are no more subscription handlers registered with the dispatcher. If there are no subscription handlers, it closes the connection for the dispatcher by calling the closeDispatcher method of the connection object and sets the dispatcher to null.

In summary, the unsubscribe method allows for the graceful unsubscription from a NATS topic, cleaning up the necessary resources in case there are no more subscription handlers left.

sequence diagram

MessageQueue

MessageQueue

The MessageQueue class is a software component that provides a mechanism for storing and organizing messages. It allows messages to be added to the queue in a sequential order and retrieved from the queue based on a specific criteria. This class serves as a central hub for message management, ensuring efficient and reliable message delivery within a software system.

boolean push(NatsMessage msg, boolean internal)

boolean push(NatsMessage msg, boolean internal) {
    this.filterLock.lock();
    try {
        // If we aren't running, then we need to obey the filter lock
        // to avoid ordering problems
        if (!internal && this.discardWhenFull) {
            return this.queue.offer(msg);
        }
        if (!this.offer(msg)) {
            throw new IllegalStateException("Output queue is full " + queue.size());
        }
        this.sizeInBytes.getAndAdd(msg.getSizeInBytes());
        this.length.incrementAndGet();
        return true;
    } finally {
        this.filterLock.unlock();
    }
}

The push method in the MessageQueue class is used to add a message to the queue. It takes two parameters: msg, which is the NatsMessage to be pushed, and internal, a boolean flag indicating whether the push operation is internal.

Here is a step-by-step description of what the push method is doing based on its body:

  1. The method acquires a lock (filterLock) to ensure thread safety while modifying the queue.

  2. If the push operation is not internal and the discardWhenFull flag is set to true, the method tries to add the msg to the queue using the offer method of the underlying queue implementation (this.queue). If the queue is full, the method immediately returns false, indicating that the message was not added.

  3. If the push operation is internal or the discardWhenFull flag is set to false, the msg is added to the queue using the offer method. If the offer method returns false, indicating that the queue is full, an IllegalStateException is thrown with a message indicating the size of the queue.

  4. The sizeInBytes field of the MessageQueue is updated by adding msg.getSizeInBytes() to its current value using the getAndAdd method of the AtomicLong class. This field represents the total size of all messages in the queue in bytes.

  5. The length field of the MessageQueue is incremented by one using the incrementAndGet method of the AtomicInteger class. This field represents the current length of the queue, i.e., the number of messages in the queue.

  6. The method returns true to indicate that the message was successfully added to the queue.

  7. Finally, the lock (filterLock) is released in a finally block to ensure that it is always unlocked, even in case of exceptions.

The push method defined in the io.nats.client.impl.MessageQueue class is responsible for adding a message to the message queue.

The method takes two parameters: msg, which represents the message to be pushed onto the queue, and internal, a boolean flag indicating whether the push operation is internal or not.

Within the method, a lock is acquired to ensure thread safety. If the push operation is not internal and the queue is set to discard messages when full, the method will attempt to offer the message to the queue using the offer method. The offer method returns true if successful, indicating that the message was added to the queue.

If the push operation is internal or the queue is not set to discard messages when full, and the push operation using the offer method fails, an IllegalStateException is thrown with a message indicating the queue is full.

After successfully pushing the message to the queue, the size and length of the queue are updated. The size of the message in bytes is incremented using msg.getSizeInBytes(), and the length of the queue is incremented by one. Finally, the method returns true to indicate that the push operation was successful.

Regardless of the outcome, the lock is released using the unlock method in a finally block to ensure that the lock is always released, even if an exception is thrown during the push operation.

sequence diagram

void poisonTheQueue()

void poisonTheQueue() {
    try {
        this.queue.add(this.poisonPill);
    } catch (IllegalStateException ie) {
        // queue was full, so we don't really need poison pill
        // ok to ignore this
    }
}

Method: poisonTheQueue

This method is defined in the io.nats.client.impl.MessageQueue class. It adds a poison pill to the queue.

Signature

void poisonTheQueue()

Description

  1. The method creates a try-catch block to handle any IllegalStateException that may be thrown during the execution.
  2. Inside the try block, the method adds this.poisonPill to the queue using the add method.
  3. If the queue was full and the add operation throws an IllegalStateException, it means that adding the poison pill was not possible.
  4. In the catch block, an IllegalStateException is caught and ignored. No action is taken in this case.
  5. The method does not return a value.

Note: The purpose of adding a poison pill to the queue is usually to indicate that the queue should be terminated or stopped. This allows consumers of the queue to recognize the poison pill and take appropriate actions.

The poisonTheQueue method in the MessageQueue class is used to add a poison pill to the queue.

A poison pill is a special message or object that is used to signal the termination of a processing queue. In this method, the poisonPill object is added to the queue using the add method. If the queue is full and cannot accept any more elements, an IllegalStateException is thrown. However, this exception is caught and ignored, as the poison pill is not necessary if the queue is already full.

The purpose of adding a poison pill to the queue is to ensure that any consumers waiting for messages from the queue are eventually notified of its termination. When a consumer encounters the poison pill, it knows that there are no more messages to process and can gracefully exit the processing loop.

sequence diagram

boolean offer(NatsMessage msg)

boolean offer(NatsMessage msg) {
    try {
        return this.queue.offer(msg, 5, TimeUnit.SECONDS);
    } catch (InterruptedException ie) {
        return false;
    }
}

The offer method, defined in the MessageQueue class within the io.nats.client.impl package, takes a NatsMessage object as an argument and returns a boolean value. This method is responsible for adding the specified message to the message queue.

Here is a step-by-step description of what the offer method does:

  1. The method takes the NatsMessage object, msg, as a parameter.

  2. Inside the method, a try-catch block is used to handle any potential InterruptedException that may occur.

  3. The offer method of the underlying queue is called, passing in the msg object as well as a timeout value of 5 seconds and the TimeUnit.SECONDS enum.

  4. If the offer method is able to successfully add the msg object to the queue within the specified timeout period, it will return true.

  5. If the operation is interrupted, the catch block will catch the InterruptedException and handle it accordingly.

  6. In case of an InterruptedException, the method will return false.

In summary, the offer method attempts to add the specified NatsMessage object to the message queue with a timeout of 5 seconds. If the operation is successful, it returns true, otherwise it returns false.

The offer method of the io.nats.client.impl.MessageQueue class is used to add a NatsMessage object to the internal queue.

The method attempts to add the NatsMessage object to the queue using the offer method of the underlying Queue implementation. The offer method has a timeout of 5 seconds, meaning that if the queue is full, the method will wait for up to 5 seconds for space to become available before returning false.

If the operation is interrupted by an InterruptedException, the method catches the exception and returns false.

Overall, the offer method provides a way to add messages to the MessageQueue, with a timeout for handling cases where the queue is full.

sequence diagram

NatsMessage poll(Duration timeout) throws InterruptedException

NatsMessage poll(Duration timeout) throws InterruptedException {
    NatsMessage msg = null;
    if (timeout == null || this.isDraining()) {
        // try immediately
        msg = this.queue.poll();
    } else {
        long nanos = timeout.toNanos();
        if (nanos != 0) {
            msg = this.queue.poll(nanos, TimeUnit.NANOSECONDS);
        } else {
            // A value of 0 means wait forever
            // We will loop and wait for a LONG time
            // if told to suspend/drain the poison pill will break this loop
            while (this.isRunning()) {
                msg = this.queue.poll(100, TimeUnit.DAYS);
                if (msg != null)
                    break;
            }
        }
    }
    if (msg == poisonPill) {
        return null;
    }
    return msg;
}

Method: poll

The poll method of the MessageQueue class is responsible for retrieving a message from the internal message queue.

Parameters

  • timeout (optional): A Duration object specifying the maximum time to wait for a message before returning. If not specified or if the queue is in the draining state, the method tries to immediately retrieve a message from the queue.

Return Value

  • NatsMessage: The retrieved message from the queue. If the retrieved message is the poison pill (a special marker used to signal the termination of the queue), the method returns null.

Method Logic

  1. If timeout is not specified or if the queue is in the draining state:

    • Assign the result of calling the poll method on the queue to msg.
      • If there are no messages in the queue, msg will be null.
  2. If timeout is specified and the queue is not in the draining state:

    • Convert timeout into the number of nanoseconds and assign it to nanos.
    • If nanos is not equal to 0:
      • Assign the result of calling the poll method on the queue with the nanos and TimeUnit.NANOSECONDS parameters to msg.
      • If there are no messages in the queue within the specified timeout, msg will be null.
    • If nanos is equal to 0, indicating an infinite wait time:
      • Enter a loop while the queue is in the running state:
        • Assign the result of calling the poll method on the queue with a timeout of 100 days to msg.
        • If a message is retrieved (msg is not null), exit the loop.
        • This loop continues until a message is retrieved or the queue is no longer running.
  3. If msg is equal to the poison pill:

    • Return null.
  4. Return msg, which is the retrieved message from the queue.

The poll method in the MessageQueue class is used to retrieve a NatsMessage from the queue. It takes a Duration parameter representing the timeout for waiting to receive a message.

If the timeout is null or the queue is in a draining state, the method immediately tries to retrieve a message using this.queue.poll().

If the timeout is not null and the queue is not in a draining state, the method waits for the specified duration to receive a message using this.queue.poll(nanos, TimeUnit.NANOSECONDS).

If the duration is zero, meaning wait forever, the method will loop and wait for a message to be available. It uses this.queue.poll(100, TimeUnit.DAYS) to wait indefinitely for a message, and breaks the loop if a message is received or if the queue is no longer running.

If the retrieved message is the poison pill (a special value used to indicate the end of the queue), null is returned. Otherwise, the retrieved NatsMessage is returned.

NatsMessage pop(Duration timeout) throws InterruptedException

NatsMessage pop(Duration timeout) throws InterruptedException {
    if (!this.isRunning()) {
        return null;
    }
    NatsMessage msg = this.poll(timeout);
    if (msg == null) {
        return null;
    }
    this.sizeInBytes.getAndAdd(-msg.getSizeInBytes());
    this.length.decrementAndGet();
    return msg;
}

The pop method in the MessageQueue class is used to retrieve a NatsMessage from the queue. Here is a step-by-step description of what the method does:

  1. Check if the message queue is running:

    • If the message queue is not running, return null.
  2. Call the poll method with a specified timeout duration:

    • The poll method waits for a specified duration to retrieve a message from the queue.
    • If a message is retrieved within the timeout, assign it to the msg variable.
    • If no message is retrieved within the timeout, return null.
  3. Update the size and length of the queue:

    • If a message is retrieved (msg is not null), subtract its size in bytes from the total size of the queue using the sizeInBytes.getAndAdd(-msg.getSizeInBytes()) method.
    • Decrement the length of the queue using the length.decrementAndGet() method.
  4. Return the retrieved message:

    • Return the retrieved msg object.

Note: If the pop method is interrupted while waiting for a message (due to InterruptedException), it will propagate the exception.

The pop method in the MessageQueue class, io.nats.client.impl.MessageQueue.pop, retrieves the next message from the queue and removes it. It takes a Duration parameter timeout to specify how long to wait for a message to become available in case the queue is empty. If the MessageQueue is not currently running, the method returns null. Otherwise, it calls the poll method with the specified timeout to get the next message. If there is no message available within the specified timeout, it returns null. If a message is retrieved successfully, the method updates the total size of the queue by subtracting the size of the retrieved message from sizeInBytes, decrements the length of the queue by one, and finally returns the retrieved message.

sequence diagram

// Waits up to the timeout to try to accumulate multiple messages

// Use the next field to read the entire set accumulated. // maxSize and maxMessages are both checked and if either is exceeded // the method returns. // // A timeout of 0 will wait forever (or until the queue is stopped/drained) // // Only works in single reader mode, because we want to maintain order. // accumulate reads off the concurrent queue one at a time, so if multiple // readers are present, you could get out of order message delivery. NatsMessage accumulate(long maxSize, long maxMessages, Duration timeout) throws InterruptedException

// Waits up to the timeout to try to accumulate multiple messages
// Use the next field to read the entire set accumulated.
// maxSize and maxMessages are both checked and if either is exceeded
// the method returns.
//
// A timeout of 0 will wait forever (or until the queue is stopped/drained)
//
// Only works in single reader mode, because we want to maintain order.
// accumulate reads off the concurrent queue one at a time, so if multiple
// readers are present, you could get out of order message delivery.
NatsMessage accumulate(long maxSize, long maxMessages, Duration timeout) throws InterruptedException {
    if (!this.singleThreadedReader) {
        throw new IllegalStateException("Accumulate is only supported in single reader mode.");
    }
    if (!this.isRunning()) {
        return null;
    }
    NatsMessage msg = this.poll(timeout);
    if (msg == null) {
        return null;
    }
    long size = msg.getSizeInBytes();
    if (maxMessages <= 1 || size >= maxSize) {
        this.sizeInBytes.addAndGet(-size);
        this.length.decrementAndGet();
        return msg;
    }
    long count = 1;
    NatsMessage cursor = msg;
    while (cursor != null) {
        NatsMessage next = this.queue.peek();
        if (next != null && next != this.poisonPill) {
            long s = next.getSizeInBytes();
            if (maxSize < 0 || (size + s) < maxSize) {
                // keep going
                size += s;
                count++;
                cursor.next = this.queue.poll();
                cursor = cursor.next;
                if (count == maxMessages) {
                    break;
                }
            } else {
                // One more is too far
                break;
            }
        } else {
            // Didn't meet max condition
            break;
        }
    }
    this.sizeInBytes.addAndGet(-size);
    this.length.addAndGet(-count);
    return msg;
}

The accumulate method in the MessageQueue class is used to accumulate multiple messages from the queue. It waits for a specified timeout duration to accumulate messages, up to a maximum size and maximum number of messages.

Here is a step-by-step description of what the method does:

  1. Check if the queue is in single reader mode. If not, throw an IllegalStateException indicating that accumulate is only supported in single reader mode.
  2. Check if the queue is running. If not, return null.
  3. Use the poll method to retrieve the next message from the queue, waiting for the specified timeout duration. If no message is received, return null.
  4. Get the size of the retrieved message.
  5. Check if the maxMessages is less than or equal to 1 or if the size of the retrieved message is greater than or equal to the maxSize. If true, decrement the size and length counters, and return the retrieved message.
  6. Initialize variables size to the size of the retrieved message and count to 1.
  7. Start a loop to accumulate more messages.
  8. Peek at the next message in the queue without removing it.
  9. If a next message exists and it is not the poisonPill: a. Get the size of the next message. b. Check if the maxSize is less than 0 or if the accumulated size plus the next message size is less than maxSize. If true, continue accumulating.
    • Increase the accumulated size and count.
    • Set the next reference of the current message to the next message and assign the next message to the cursor variable.
    • Check if the accumulated count is equal to maxMessages. If true, exit the loop. c. If the accumulated size is greater than or equal to maxSize, exit the loop because accumulating one more message would exceed the maximum size. d. If the next message does not meet the max condition, exit the loop.
  10. Decrement the size and length counters by the accumulated size and count.
  11. Return the retrieved message.

Note: The accumulate method must be called in a single reader mode to maintain the order of message delivery. If multiple readers are present, there is a possibility of out-of-order message delivery.

The accumulate method in the MessageQueue class is used to accumulate multiple messages from the queue up to a specific maximum size or number of messages.

Here's how it works:

  1. This method can only be used when the queue is in single reader mode, ensuring that message order is maintained.

  2. The method waits for a specified timeout duration to try to accumulate multiple messages. If the timeout is set to 0, it will wait indefinitely until the queue is stopped or drained.

  3. The method checks the maxSize and maxMessages parameters and if either limit is exceeded, the method returns.

  4. It retrieves the first message from the queue using the poll method with the specified timeout duration.

  5. If no message is available, it returns null.

  6. If the retrieved message's size is equal to or greater than the maxSize parameter or maxMessages is set to 1, it subtracts the message size from the total size of the queue and decrements the length by 1, then returns the message.

  7. If the retrieved message's size is less than the maxSize parameter and maxMessages is greater than 1, it continues to accumulate messages until either the maxMessages limit is reached or the accumulated size (including the next message's size) exceeds the maxSize.

  8. For each additional message accumulated, it increases the total size and count, updates the next reference in the previously accumulated message, and moves on to the next message in the queue until the maxMessages limit is reached or the accumulated size exceeds the maxSize.

  9. After accumulating all the messages, it subtracts the accumulated size from the total size of the queue and decrements the length by the number of accumulated messages.

  10. Finally, it returns the first accumulated message.

sequence diagram

void filter(Predicate p)

void filter(Predicate<NatsMessage> p) {
    this.filterLock.lock();
    try {
        if (this.isRunning()) {
            throw new IllegalStateException("Filter is only supported when the queue is paused");
        }
        ArrayList<NatsMessage> newQueue = new ArrayList<>();
        NatsMessage cursor = this.queue.poll();
        while (cursor != null) {
            if (!p.test(cursor)) {
                newQueue.add(cursor);
            } else {
                this.sizeInBytes.addAndGet(-cursor.getSizeInBytes());
                this.length.decrementAndGet();
            }
            cursor = this.queue.poll();
        }
        this.queue.addAll(newQueue);
    } finally {
        this.filterLock.unlock();
    }
}

The filter method in the MessageQueue class is used to filter the messages in the queue based on a given predicate.

Here is a step-by-step description of what the method does:

  1. Acquire a lock to ensure thread safety for the filtering operation.
  2. Check if the queue is currently running. If it is, throw an IllegalStateException with the message "Filter is only supported when the queue is paused".
  3. Create a new ArrayList to hold the filtered messages.
  4. Poll the queue to retrieve the next message (starting from the front of the queue) and assign it to the cursor variable.
  5. Enter a loop that iterates until the cursor variable is null.
  6. Inside the loop, check if the cursor message satisfies the provided predicate (p.test(cursor)). If it does not pass the predicate test, add it to the newQueue list.
  7. If the cursor message does pass the predicate test, subtract its size in bytes from the total size of all messages (sizeInBytes) using this.sizeInBytes.addAndGet(-cursor.getSizeInBytes()), and decrement the length of the queue (length.decrementAndGet()).
  8. Poll the queue again to retrieve the next message and update the cursor variable.
  9. Once the loop completes, add all the messages from the newQueue list back to the original queue using this.queue.addAll(newQueue).
  10. Release the lock used for synchronization by calling this.filterLock.unlock() in a finally block.

The filter method in the MessageQueue class is used to remove messages from the queue based on a provided predicate.

  • It first checks if the queue is running, and if it is not, it throws an IllegalStateException since filtering is only supported when the queue is paused.
  • Then, it creates a new ArrayList to store the filtered messages.
  • It retrieves a message from the queue using poll and checks if the message satisfies the predicate. If the message does not satisfy the predicate, it is added to the new queue.
  • If the message satisfies the predicate, the size in bytes and the length of the queue are updated accordingly.
  • This process continues until all messages in the queue have been processed.
  • Finally, the new queue is added back to the original queue. The filter method is useful for selectively removing messages from the queue based on specific criteria defined by the Predicate.

sequence diagram

ServerPoolEntry

ServerPoolEntry

The ServerPoolEntry class is a public class that represents an entry in a server pool. It is used to manage the connection to a server in a pool.

NatsKeyValueManagement

NatsKeyValueManagement

The NatsKeyValueManagement class is a public implementation of the KeyValueManagement interface. It provides functionality for managing key-value pairs.

@Override

public KeyValueStatus create(KeyValueConfiguration config) throws IOException, JetStreamApiException

@Override
public KeyValueStatus create(KeyValueConfiguration config) throws IOException, JetStreamApiException {
    StreamConfiguration sc = config.getBackingConfig();
    // most validation / KVC setup is done in the KeyValueConfiguration Builder
    // but this is done here because the context has a connection which has the server info with a version
    if (serverOlderThan272) {
        // null discard policy will use default
        sc = StreamConfiguration.builder(sc).discardPolicy(null).build();
    }
    return new KeyValueStatus(jsm.addStream(sc));
}

The create method in the NatsKeyValueManagement class is used to create a key-value store based on a given configuration.

  1. The method takes a KeyValueConfiguration object as an argument.
  2. Inside the method, the StreamConfiguration object is extracted from the KeyValueConfiguration.
  3. The method checks if the server version is older than 2.7.2. If it is, it proceeds to the next step; otherwise, it skips the next step.
  4. If the server version is older than 2.7.2, the method creates a new StreamConfiguration object using a builder pattern. It sets the discard policy to null to use the default discard policy, and builds the new StreamConfiguration object.
  5. Finally, the method calls the addStream method on the underlying JetStream management (jsm) instance, passing in the StreamConfiguration object as a parameter.
  6. The method wraps the result of the addStream method call in a KeyValueStatus object and returns it.

The create method is implemented in the NatsKeyValueManagement class and it is used to create a key-value store using the given KeyValueConfiguration.

Here's a breakdown of what the method does:

  1. It retrieves the StreamConfiguration from the KeyValueConfiguration.
  2. It checks if the server version is older than 2.7.2.
  3. If the server version is older than 2.7.2, it sets the discard policy of the StreamConfiguration to null (which will use the default discard policy).
  4. It then adds the stream to the JetStream Manager (jsm) using the modified StreamConfiguration.
  5. Finally, it returns a KeyValueStatus object created with the addStream operation on the JetStream Manager.

This method handles the creation of a key-value store by configuring the stream and adding it to the JetStream Manager.

sequence diagram

IncomingMessage

IncomingMessage

IncomingMessage is a public class that extends the NatsMessage class. It represents an incoming message in the Nats messaging system. This class inherits all the properties and methods of the NatsMessage class and provides additional functionality specific to incoming messages.

ProtocolMessage

ProtocolMessage

The ProtocolMessage class is a special version of the NatsMessage class that represents a protocol message. It inherits all the properties and functionalities of the NatsMessage class.

StreamListReader

StreamListReader

The StreamListReader class is a subclass of AbstractListReader class. It extends the functionality of the AbstractListReader by implementing additional methods and behaviors specific to reading data from a stream.

@Override

void processItems(List items)

@Override
void processItems(List<JsonValue> items) {
    for (JsonValue v : items) {
        streams.add(new StreamInfo(v));
    }
}

The processItems method in the io.nats.client.impl.StreamListReader class is declared with the @Override annotation, indicating that it overrides a method from a parent class or interface.

This method takes a List of JsonValues as its parameter, named items. Here is the step-by-step description of what this method does:

  1. Start a loop to iterate through each JsonValue in the items List.
  2. For each JsonValue v in items, do the following:
    • Create a new StreamInfo object, passing v as the argument to its constructor.
    • Add the newly created StreamInfo object to the streams list.

The purpose of this method is to iterate through a list of JsonValues and create a new StreamInfo object for each value, then add it to the streams list. This method is likely used to process and handle a list of JsonValues representing streams in the application.

The processItems method in the io.nats.client.impl.StreamListReader class takes a list of JsonValue items as input. It iterates through each item in the list and creates a new StreamInfo object by passing the current item as a parameter. The created StreamInfo objects are then added to a streams list.

NatsMessageConsumerBase

NatsMessageConsumerBase

The NatsMessageConsumerBase class is an implementation of the MessageConsumer interface. It provides the functionality to consume messages from a NATS messaging system.

protected void initSub(NatsJetStreamPullSubscription sub) throws JetStreamApiException, IOException

protected void initSub(NatsJetStreamPullSubscription sub) throws JetStreamApiException, IOException {
    this.sub = sub;
    if (lastConsumerInfo == null) {
        lastConsumerInfo = sub.getConsumerInfo();
    }
    pmm = (PullMessageManager) sub.manager;
}

Method: initSub

The initSub method is defined in the io.nats.client.impl.NatsMessageConsumerBase class. This method takes in a NatsJetStreamPullSubscription parameter and is responsible for initializing the subscription by setting the sub variable and performing additional setup operations.

Steps

  1. Set the sub variable to the provided NatsJetStreamPullSubscription parameter.
  2. Check if the lastConsumerInfo variable is null.
    • If lastConsumerInfo is null, proceed to the next step.
    • If lastConsumerInfo is not null, skip to step 5.
  3. Call the getConsumerInfo method of the sub object to retrieve the consumer information.
  4. Assign the consumer information returned by getConsumerInfo to the lastConsumerInfo variable.
  5. Cast the sub.manager object to a PullMessageManager and assign it to the pmm variable.

Exceptions

  • JetStreamApiException: This exception is thrown when there is an error with the JetStream API.
  • IOException: This exception is thrown when there is an I/O operation failure.

The method initSub in the class NatsMessageConsumerBase is responsible for initializing a NatsJetStreamPullSubscription and setting it as the current subscription. The method is called with a parameter of type NatsJetStreamPullSubscription, which is assigned to the class variable sub.

In the method, it first assigns the passed subscription to the sub variable. If the lastConsumerInfo is null, it obtains the consumer information from the subscription and assigns it to lastConsumerInfo. Finally, it assigns the PullMessageManager instance from the subscription's manager to the class variable pmm.

This method potentially throws JetStreamApiException and IOException.

sequence diagram

@Override

public void stop(long timeout) throws InterruptedException

@Override
public void stop(long timeout) throws InterruptedException {
    synchronized (subLock) {
        if (!stopped) {
            try {
                if (sub.getNatsDispatcher() != null) {
                    sub.getDispatcher().drain(Duration.ofMillis(timeout));
                } else {
                    sub.drain(Duration.ofMillis(timeout));
                }
            } finally {
                stopped = true;
            }
        }
    }
}

The stop(long timeout) method in the NatsMessageConsumerBase class is used to stop the message consumer. Here is a step-by-step description of what the method is doing:

  1. It acquires a lock on the subLock object to ensure thread safety.
  2. It checks if the consumer has already been stopped. If it has, the method does nothing.
  3. If the consumer has not been stopped, it proceeds to drain the message dispatcher.
  4. Inside a try-finally block: a. It checks if the consumer has a NatsDispatcher by calling the getNatsDispatcher() method on the sub object. b. If a NatsDispatcher is available, it calls the drain(Duration timeout) method on the Dispatcher object associated with the consumer, passing the specified timeout value. c. If a NatsDispatcher is not available, it calls the drain(Duration timeout) method on the sub object, passing the specified timeout value. d. The drain() method blocks until all messages have been processed or the timeout is reached.
  5. Finally, it sets the stopped flag to true, indicating that the consumer has been stopped.

Note: The Duration.ofMillis(timeout) method is used to specify the timeout duration in milliseconds. The InterruptedException may be thrown if the thread is interrupted while waiting for the drain() method to complete.

The stop method in the NatsMessageConsumerBase class is responsible for stopping the message consumer by performing certain actions:

  1. It acquires a lock using the subLock object to ensure thread safety during the execution of the method.
  2. It checks if the message consumer has not already been stopped by examining the stopped variable.
  3. If the consumer has not been stopped, it proceeds with the following actions: a. It checks if the underlying NATS dispatcher is available through the sub.getNatsDispatcher() method. If available, it drains the dispatcher by calling the drain method with the specified timeout duration. b. If the dispatcher is not available, it directly drains the subscription object by calling the drain method with the specified timeout duration.
  4. Once the draining process is completed, the finally block sets the stopped variable to true to indicate that the consumer has been stopped.

The method takes a timeout parameter of type long which specifies the maximum time to wait for the draining process to complete. It throws an InterruptedException if the thread is interrupted while waiting for the draining process to finish.

sequence diagram

@Override

public void close() throws Exception

@Override
public void close() throws Exception {
    try {
        if (!stopped && sub.isActive()) {
            if (sub.getNatsDispatcher() != null) {
                sub.getDispatcher().unsubscribe(sub);
            } else {
                sub.unsubscribe();
            }
        }
    } catch (Throwable ignore) {
        // nothing to do
    }
}

The close() method in the NatsMessageConsumerBase class is used to unsubscribe the NATS message consumer from its associated topic.

Here is a step-by-step description of what the close() method does based on its body:

  1. Checks if the NATS message consumer is not already stopped (stopped variable is false) and the subscription is still active (sub.isActive() returns true).
  2. If the NATS message consumer has a NATS dispatcher assigned (sub.getNatsDispatcher() != null), it calls the unsubscribe() method on the dispatcher and passes in the sub object. This allows the NATS dispatcher to handle the unsubscription process.
  3. If the NATS message consumer does not have a NATS dispatcher assigned, it directly calls the unsubscribe() method on the subscription itself (sub.unsubscribe()). This is the case when the subscription was created without a dispatcher.
  4. If any exception or error occurs during the unsubscription process, it is caught and ignored (Throwable ignore). This is done to keep the close() method from throwing any exceptions.

Overall, the close() method ensures that the NATS message consumer is safely unsubscribed from the topic it is consuming messages from.

The close method defined in the NatsMessageConsumerBase class is responsible for unsubscribing and closing a NATS message consumer.

Here's a breakdown of what the method does:

  1. It first checks if the consumer is not already stopped and the subscription is active.
  2. If the NatsDispatcher associated with the consumer is not null, it calls the unsubscribe method on the dispatcher and passes the subscription object as a parameter.
  3. If the NatsDispatcher is null, it calls the unsubscribe method directly on the subscription object.
  4. Any exceptions or errors thrown during the execution of the method are caught and ignored.

In summary, the purpose of this method is to safely unsubscribe and close the NATS message consumer.

sequence diagram

PushMessageManager

PushMessageManager

The PushMessageManager class is a subclass of MessageManager that is responsible for managing the sending and receiving of push notifications. It inherits all the functionalities of the MessageManager class and adds specific methods and features for handling push messages in a software application.

@Override

protected void startup(NatsJetStreamSubscription sub)

@Override
protected void startup(NatsJetStreamSubscription sub) {
    super.startup(sub);
    sub.setBeforeQueueProcessor(this::beforeQueueProcessorImpl);
    if (hb) {
        initOrResetHeartbeatTimer();
    }
}

Method: startup(NatsJetStreamSubscription sub)

This method is defined in the io.nats.client.impl.PushMessageManager class and is used to initialize the NatsJetStreamSubscription and perform additional operations before starting the subscription.

Step 1: Call the superclass method

The method starts by calling the super.startup(sub) to execute the startup logic in the parent class.

Step 2: Set the beforeQueueProcessor

The method sets the beforeQueueProcessor of the NatsJetStreamSubscription to this::beforeQueueProcessorImpl. This means that the beforeQueueProcessorImpl method will be called before processing each message in the subscription queue.

Step 3: Check for heartbeat flag

The method checks if the hb flag is enabled. If it is, the method proceeds to the next step. If not, the method skips to the end.

Step 4: Initialize or reset the heartbeat timer

If the hb flag is enabled, the method calls the initOrResetHeartbeatTimer method. This method is responsible for initializing or resetting the heartbeat timer used for monitoring the health of the subscription.

End of method

The method ends after completing the above steps. Any further logic or methods outside of this method will be executed after the startup process is completed.

Note: The details of the beforeQueueProcessorImpl and initOrResetHeartbeatTimer methods are not provided in the given code snippet. They could be defined elsewhere in the PushMessageManager class or its parent class.

The startup method is an overridden method in the PushMessageManager class of the io.nats.client.impl package. It takes a NatsJetStreamSubscription object as a parameter.

The purpose of this method is to perform initialization tasks when starting up the PushMessageManager for the given subscription.

  1. It calls the super.startup(sub) method to invoke the startup method of the parent class, which likely performs some general initialization tasks.
  2. It sets a specific "before queue processor" for the given subscription by calling the sub.setBeforeQueueProcessor(this::beforeQueueProcessorImpl) method. This means that the specified implementation (beforeQueueProcessorImpl method) will be executed before each message is pushed to the queue of the subscription.
  3. If a heartbeat flag (hb) is set to true, it initializes or resets a heartbeat timer by calling the initOrResetHeartbeatTimer() method. This suggests that the PushMessageManager might be responsible for managing heartbeats in some way.

Overall, this method sets up necessary components and configurations for the PushMessageManager associated with the given subscription.

sequence diagram

@Override

protected Boolean beforeQueueProcessorImpl(NatsMessage msg)

@Override
protected Boolean beforeQueueProcessorImpl(NatsMessage msg) {
    if (hb) {
        // only need to track when heartbeats are expected
        messageReceived();
        Status status = msg.getStatus();
        if (status != null) {
            // only fc heartbeats get queued
            if (status.isHeartbeat()) {
                // true if a fc hb
                return hasFcSubject(msg);
            }
        }
    }
    return true;
}

The beforeQueueProcessorImpl method, defined in the PushMessageManager class of the io.nats.client.impl package, performs the following steps:

  1. Overrides the beforeQueueProcessorImpl method from the superclass.
  2. Takes a NatsMessage object as a parameter.
  3. Checks if the variable hb is true.
  4. If hb is true, calls the messageReceived method to track the message received.
  5. Gets the status from the NatsMessage object and assigns it to the status variable.
  6. Checks if the status variable is not null.
  7. If the status variable is not null, checks if it represents a heartbeat message.
  8. If the status represents a heartbeat message, calls the hasFcSubject method to determine if it is a fc (flow control) heartbeat.
  9. Returns true if hb is false or if the status does not represent a heartbeat message, otherwise returns the result of the hasFcSubject method.

This method is protected, meaning it can only be accessed within the same class or its subclasses. It is intended for internal use and its purpose is to perform logic related to message processing before adding the message to the queue.

The method beforeQueueProcessorImpl in the class PushMessageManager is an overridden method that takes in a NatsMessage object as its parameter and returns a Boolean value.

The purpose of this method is to process a message before it is added to the message queue.

The method starts by checking if the hb variable is true. If it is, it means that heartbeats are expected, so the method proceeds with further processing.

Next, it calls the messageReceived() method to track the receipt of the message.

Then, it retrieves the status of the message using the getStatus() method and assigns it to the status variable.

If the status variable is not null, it checks if the status is a heartbeat. If it is, it calls the hasFcSubject() method to determine if the message has a "fc" subject.

Finally, if any of the conditions mentioned above are satisfied, it returns true. Otherwise, it returns true as the default value.

sequence diagram

@Override

protected ManageResult manage(Message msg)

@Override
protected ManageResult manage(Message msg) {
    if (msg.getStatus() == null) {
        trackJsMessage(msg);
        return MESSAGE;
    }
    return manageStatus(msg);
}

The manage method in the PushMessageManager class in the io.nats.client.impl package is responsible for handling incoming messages and processing them based on their status.

Here is a step-by-step description of what the manage method does based on its body:

  1. The method is overridden with the @Override annotation, indicating that it overrides a method from a superclass or interface.

  2. The manage method takes a Message object as its parameter, which represents an incoming message.

  3. The method checks if the msg object's status is null using the getStatus() method. This condition is checked using the == operator.

  4. If the status is null, it means that the message does not have a status. In this case, the trackJsMessage method is called to track the message for JavaScript. The exact details of this method are not provided in the given code snippet.

  5. After tracking the JavaScript message, the method returns the ManageResult enum value MESSAGE. The ManageResult enum likely represents the result of managing the message, which can be used for further processing or error handling.

  6. If the status is not null, meaning the message has a status, the method calls the manageStatus method to handle the message based on its status. The details of this method are not provided in the given code snippet.

It is important to note that the exact behavior of the manage method and the methods it calls (trackJsMessage and manageStatus) may vary depending on the implementation details of the PushMessageManager class and its dependencies. Without further context or code details, it is not possible to provide a more specific description of the functionality of these methods.

The manage method in the PushMessageManager class is responsible for handling messages received from a Message object.

The method works as follows:

  • It first checks if the status of the message is null.
  • If the status is null, it tracks the message using the trackJsMessage method and returns a ManageResult of type MESSAGE.
  • If the status is not null, it delegates the handling of the message to the manageStatus method and returns the result of that method.

Overall, the manage method performs the necessary processing based on the status of the message and returns the appropriate result.

sequence diagram

protected ManageResult manageStatus(Message msg)

protected ManageResult manageStatus(Message msg) {
    // this checks fc, hb and unknown
    // only process fc and hb if those flags are set
    // otherwise they are simply known statuses
    Status status = msg.getStatus();
    if (fc) {
        boolean isFcNotHb = status.isFlowControl();
        String fcSubject = isFcNotHb ? msg.getReplyTo() : extractFcSubject(msg);
        if (fcSubject != null) {
            processFlowControl(fcSubject, isFcNotHb ? FLOW_CONTROL : HEARTBEAT);
            return STATUS_HANDLED;
        }
    }
    conn.executeCallback((c, el) -> el.unhandledStatus(c, sub, status));
    return STATUS_ERROR;
}

The manageStatus method is defined in the class io.nats.client.impl.PushMessageManager and takes a Message as its parameter.

The method starts by retrieving the status from the input msg.

Then, it checks if the fc flag is set. If it is set, it determines whether the status is related to flow control or heartbeat. If it is related to flow control, it gets the flow control subject from the replyTo field of the msg object, otherwise it calls the extractFcSubject method to get the flow control subject.

If a valid flow control subject is obtained, it then calls the processFlowControl method, passing the flow control subject and the appropriate type (FLOW_CONTROL or HEARTBEAT). Finally, it returns STATUS_HANDLED.

If the fc flag is not set, or if the flow control subject is not obtained, it calls the executeCallback method of the conn object, passing a lambda function that handles unhandled statuses. The lambda function calls the unhandledStatus method of the el (element) object, passing the connection c, the subscription sub, and the status.

Ultimately, the method returns STATUS_ERROR if no flow control subject is obtained or if the status is not handled.

Please note that the code snippets provided in the description are not complete and might not compile as-is. The description assumes that the missing code and relevant objects and variables are properly defined and accessible within the class and its scope.

The method manageStatus in class PushMessageManager is responsible for managing the status of a Message object.

It starts by retrieving the Status from the message.

Then, it checks if the flag fc is set. If fc is true, it determines whether the status is flow control or heartbeat.

If the status is flow control, it extracts the flow control subject and processes it accordingly. It returns STATUS_HANDLED to indicate that the status was successfully handled.

If the status is not flow control, it executes the callback defined in conn.executeCallback passing the connection, event loop, and status as arguments. This callback handles unhandled statuses.

Finally, if any errors occur during the process, it returns STATUS_ERROR.

sequence diagram

private void processFlowControl(String fcSubject, FlowControlSource source)

private void processFlowControl(String fcSubject, FlowControlSource source) {
    // we may get multiple fc/hb messages with the same reply
    // only need to post to that subject once
    if (fcSubject != null && !fcSubject.equals(lastFcSubject)) {
        conn.publishInternal(fcSubject, null, null, null);
        // set after publish in case the pub fails
        lastFcSubject = fcSubject;
        conn.executeCallback((c, el) -> el.flowControlProcessed(c, sub, fcSubject, source));
    }
}

The processFlowControl method is defined in the io.nats.client.impl.PushMessageManager class. It takes two parameters, fcSubject and source, both of unspecified types.

  1. Check if fcSubject is not null and is different from the lastFcSubject variable.
  2. If the above condition is true, publish an internal message to the fcSubject using the conn.publishInternal method. The message being published is null and all other parameters for the publishInternal method are also null.
  3. After publishing the message, set the lastFcSubject variable equal to fcSubject.
  4. Call the executeCallback method of the conn object passing a lambda expression as a parameter. The lambda expression takes two parameters, c and el, and calls the flowControlProcessed method of el passing c, sub, fcSubject, and source as arguments.

Note: The specific types of the parameters fcSubject and source, as well as the objects conn and sub, are not provided in the given code snippet.

The processFlowControl method in the PushMessageManager class handles the flow control logic for receiving flow control messages. It takes in a fcSubject and a source as arguments.

This method first checks if the fcSubject is not null and different from the last processed flow control subject. If it is a new flow control subject, the method publishes a message to the fcSubject using the conn.publishInternal method. After successfully publishing the message, it updates the lastFcSubject variable with the new fcSubject.

Finally, it executes a callback function, passing in the client connection, the event loop, the subscriber, the fcSubject, and the source. This allows for additional processing or handling of the flow control event.

sequence diagram

NatsJetStreamMessage

NatsJetStreamMessage

The NatsJetStreamMessage class is a subclass of IncomingMessage that represents a message received from the NATS JetStream messaging system. It provides additional functionality and properties specific to JetStream messages.

@Override

public void ackSync(Duration d) throws InterruptedException, TimeoutException

@Override
public void ackSync(Duration d) throws InterruptedException, TimeoutException {
    if (ackHasntBeenTermed()) {
        validateDurationRequired(d);
        Connection nc = getJetStreamValidatedConnection();
        if (nc.request(replyTo, AckAck.bytes, d) == null) {
            throw new TimeoutException("Ack response timed out.");
        }
        lastAck = AckAck;
    }
}

Method: ackSync

Description:

This method is used to acknowledge the successful processing of a JetStream message. It sends a synchronous acknowledgement to the server and waits for a response within the given timeout duration.

Parameters:

  • d (Duration): The timeout duration for waiting for the acknowledgement response.

Steps:

  1. Check if the message has not already been acknowledged (ackHasntBeenTermed).

  2. Validate the provided duration (validateDurationRequired).

  3. Get the validated JetStream connection (getJetStreamValidatedConnection).

  4. Send a request to the server using the replyTo subject and the AckAck bytes as the payload. Use the provided duration as the timeout.

  5. If the response is null, throw a TimeoutException with the message "Ack response timed out."

  6. Set the lastAck to AckAck to indicate that the acknowledgement was successful.

The ackSync method in class NatsJetStreamMessage is responsible for synchronously acknowledging the receipt of a message.

This method takes a duration parameter (d) which represents the maximum time to wait for an acknowledgment response before throwing a TimeoutException.

In this method, it first checks if the acknowledgment has already been terminated by checking the status using ackHasntBeenTermed(). If the acknowledgment has not been terminated, it proceeds with the acknowledgment process.

It validates the provided duration by calling validateDurationRequired(d), ensuring it meets the necessary requirements.

Next, it retrieves the validated JetStream connection by calling getJetStreamValidatedConnection().

The method then sends an acknowledgment request to the NATS server using the request method of the connection object (nc). It specifies the reply-to subject (replyTo) and the acknowledgment bytes (AckAck.bytes) to be sent. If the acknowledgment response times out (i.e., nc.request returns null), a TimeoutException is thrown.

Finally, if the acknowledgment request is successful, the method updates the last acknowledgment status (lastAck) to AckAck.

sequence diagram

@Override

public NatsJetStreamMetaData metaData()

@Override
public NatsJetStreamMetaData metaData() {
    if (this.jsMetaData == null) {
        this.jsMetaData = new NatsJetStreamMetaData(this);
    }
    return this.jsMetaData;
}

The metaData method in the NatsJetStreamMessage class is responsible for returning the metadata associated with the current instance of the NatsJetStreamMessage class.

The method begins with an @Override annotation, indicating that it overrides a method declared in a parent class or implemented in an interface.

The first statement in the method checks if the jsMetaData instance variable is null. This variable is of type NatsJetStreamMetaData and is declared in the class itself. If it is indeed null, it means that the metadata has not been initialized yet.

In that case, the method proceeds to instantiate a new NatsJetStreamMetaData object, passing the current instance of the NatsJetStreamMessage class as a parameter to the constructor. This allows the NatsJetStreamMetaData object to access and work with the properties and methods of the NatsJetStreamMessage class.

Once the jsMetaData variable is initialized with the new NatsJetStreamMetaData object, it is assigned to the class instance's jsMetaData variable, ensuring that subsequent calls to metaData will return the same object without re-instantiating it.

Finally, the method returns the jsMetaData object.

It is important to note that without further information on the NatsJetStreamMetaData class and its members, it is not possible to provide a detailed description of what the metaData method does with the BODY parameter. However, based on the provided code snippet, it can be inferred that the metaData method is responsible for initializing and returning the associated metadata object for the current NatsJetStreamMessage instance.

The metaData method in the NatsJetStreamMessage class of the io.nats.client.impl package is used to retrieve the metadata associated with the current message. If the jsMetaData object is not yet initialized, the method creates a new instance of NatsJetStreamMetaData using the current message and assigns it to the jsMetaData variable. Finally, the method returns the jsMetaData object containing the metadata of the message.

sequence diagram

private void ackReply(AckType ackType, long delayNanos)

private void ackReply(AckType ackType, long delayNanos) {
    if (ackHasntBeenTermed()) {
        Connection nc = getJetStreamValidatedConnection();
        nc.publish(replyTo, ackType.bodyBytes(delayNanos));
        lastAck = ackType;
    }
}

The ackReply method in the NatsJetStreamMessage class is responsible for acknowledging a message received from a NATS JetStream subscription. Here is a step-by-step description of what the method does based on its body:

  1. Check if the message has not been terminated yet by calling the ackHasntBeenTermed() method.
  2. Obtain a validated connection to the JetStream server by calling the getJetStreamValidatedConnection() method.
  3. Publish an acknowledgement message to the reply subject using the obtained connection. The acknowledgement message is created by calling the bodyBytes(delayNanos) method on the ackType parameter.
  4. Set the lastAck instance variable to the value of ackType.

Note: It is assumed that the replyTo instance variable is already set with the appropriate reply subject before calling the ackReply method.

The ackReply method in the NatsJetStreamMessage class is used to send an acknowledgment reply for a JetStream message.

This method takes two parameters: ackType and delayNanos.

If the acknowledgement for the message has not already been terminated, the method gets the validated JetStream connection, publishes an acknowledgment message to the replyTo subject with the appropriate body bytes based on the specified ackType and delayNanos, and updates the lastAck variable with the provided ackType.

The purpose of this method is to allow the software to acknowledge the receipt of a JetStream message and provide the necessary information for further processing or handling of the message.

sequence diagram

NatsJetStreamPullSubscription

NatsJetStreamPullSubscription

The NatsJetStreamPullSubscription class is a subclass of NatsJetStreamSubscription and represents a subscription to a JetStream pull-based consumer. It provides functionalities for receiving and processing messages from a JetStream pull-based source.

protected String _pull(PullRequestOptions pullRequestOptions, boolean raiseStatusWarnings, TrackPendingListener trackPendingListener)

protected String _pull(PullRequestOptions pullRequestOptions, boolean raiseStatusWarnings, TrackPendingListener trackPendingListener) {
    String publishSubject = js.prependPrefix(String.format(JSAPI_CONSUMER_MSG_NEXT, stream, consumerName));
    String pullSubject = getSubject().replace("*", Long.toString(this.pullSubjectIdHolder.incrementAndGet()));
    manager.startPullRequest(pullSubject, pullRequestOptions, raiseStatusWarnings, trackPendingListener);
    connection.publish(publishSubject, pullSubject, pullRequestOptions.serialize());
    connection.lenientFlushBuffer();
    return pullSubject;
}

The method _pull is defined in the class io.nats.client.impl.NatsJetStreamPullSubscription. Its purpose is to perform a pull operation on a JetStream subscription.

Here is a step-by-step description of what the method is doing based on its body:

  1. It initializes a publishSubject variable by prepending the prefix to the formatted string JSAPI_CONSUMER_MSG_NEXT using the values of stream and consumerName.
  2. It generates a pullSubject by replacing the asterisk (*) wildcard in the subject with the current value of this.pullSubjectIdHolder.incrementAndGet(). This value represents a unique identifier for the pull request.
  3. It invokes the startPullRequest method of the manager object with the pullSubject, pullRequestOptions, raiseStatusWarnings, and trackPendingListener arguments. This method is responsible for initiating the pull request.
  4. It publishes a message to the publishSubject using the pullSubject and the serialized form of pullRequestOptions.
  5. It flushes the connection's buffer using the lenientFlushBuffer method. This ensures that the pull request message is sent immediately.
  6. Finally, it returns the pullSubject, which can be used to track the pull request.

Please note that some variable types and method calls are inferred and may not be explicitly mentioned in the provided code snippet.

The _pull method in the NatsJetStreamPullSubscription class is used to pull messages from a JetStream consumer.

Here is a breakdown of what the method does:

  1. It constructs the subject to publish the next pull request.
  2. It generates a unique pullSubject by replacing "*" with the incremented value of pullSubjectIdHolder.
  3. It starts a pull request with the JetStream manager using the pullSubject.
  4. It publishes the pullSubject and the serialized pull request options to the publishSubject.
  5. It flushes the connection buffer to ensure that the publish call is sent immediately.
  6. Finally, it returns the pullSubject that was used for the pull request.

sequence diagram

private List _fetch(int batchSize, long maxWaitMillis)

private List<Message> _fetch(int batchSize, long maxWaitMillis) {
    List<Message> messages = drainAlreadyBuffered(batchSize);
    int batchLeft = batchSize - messages.size();
    if (batchLeft == 0) {
        return messages;
    }
    try {
        long start = System.nanoTime();
        Duration expires = Duration.ofMillis(maxWaitMillis > MIN_EXPIRE_MILLIS ? maxWaitMillis - EXPIRE_ADJUSTMENT : maxWaitMillis);
        String pullSubject = _pull(PullRequestOptions.builder(batchLeft).expiresIn(expires).build(), false, null);
        // timeout > 0 process as many messages we can in that time period
        // If we get a message that either manager handles, we try again, but
        // with a shorter timeout based on what we already used up
        long maxWaitNanos = maxWaitMillis * 1_000_000;
        long timeLeftNanos = maxWaitNanos;
        while (batchLeft > 0 && timeLeftNanos > 0) {
            Message msg = nextMessageInternal(Duration.ofNanos(timeLeftNanos));
            if (msg == null) {
                // normal timeout
                return messages;
            }
            switch(manager.manage(msg)) {
                case MESSAGE:
                    messages.add(msg);
                    batchLeft--;
                    break;
                case STATUS_TERMINUS:
                    // if there is a match, the status applies otherwise it's ignored
                    if (pullSubject.equals(msg.getSubject())) {
                        return messages;
                    }
                    break;
                case STATUS_ERROR:
                    // if there is a match, the status applies otherwise it's ignored
                    if (pullSubject.equals(msg.getSubject())) {
                        throw new JetStreamStatusException(msg.getStatus(), this);
                    }
                    break;
            }
            // anything else, try again while we have time
            timeLeftNanos = maxWaitNanos - (System.nanoTime() - start);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    return messages;
}

The _fetch method is defined in the io.nats.client.impl.NatsJetStreamPullSubscription class and takes two parameters: batchSize (the number of messages to fetch) and maxWaitMillis (the maximum time to wait for messages).

  1. The method first calls the drainAlreadyBuffered method to retrieve any messages that have already been buffered. These messages are added to the messages list.

  2. The method checks how many more messages are needed to reach the desired batchSize.

  3. If no more messages are needed (i.e., batchLeft == 0), the method returns the messages list.

  4. If more messages are needed, the method proceeds to set up the fetching process.

  5. It calculates the expiry time for the fetch operation based on the maxWaitMillis parameter. If maxWaitMillis is greater than MIN_EXPIRE_MILLIS, it subtracts EXPIRE_ADJUSTMENT; otherwise, it uses maxWaitMillis directly.

  6. It calls the _pull method to initiate pulling messages from the server. The _pull method takes a PullRequestOptions object with the batchLeft value and the expiresIn duration (set in the previous step). It also specifies false for the noWait and lastBatch parameters, and null for the statusCodes parameter.

  7. It sets the maximum wait time in nanoseconds (maxWaitNanos) based on the maxWaitMillis value.

  8. It enters a loop that continues until either batchLeft becomes 0 or the timeLeftNanos becomes 0.

  9. In each iteration, it calls the nextMessageInternal method to fetch the next message. This method takes a Duration object that represents the remaining time (timeLeftNanos) for waiting. If no message is received within this time, the method returns the messages list (normal timeout case).

  10. If a message is received, the method checks the message with the manager to determine its status.

  11. If the manager returns MESSAGE, the message is added to the messages list, batchLeft is decremented, and the loop continues for the next message.

  12. If the manager returns STATUS_TERMINUS, it checks if the message subject matches the pullSubject. If it does, the method returns the messages list.

  13. If the manager returns STATUS_ERROR, it again checks if the message subject matches the pullSubject. If it does, it throws a JetStreamStatusException with the message status and the current NatsJetStreamPullSubscription instance.

  14. The method then calculates the remaining time (timeLeftNanos) for the loop by subtracting the elapsed time from the maxWaitNanos.

  15. The loop continues until either batchLeft becomes 0 or the timeLeftNanos becomes 0.

  16. If the loop ends without returning, the method completes by returning the messages list.

  17. The method handles the InterruptedException by interrupting the current thread.

This method is responsible for fetching messages from the server based on the provided batchSize and maxWaitMillis parameters, using the drainAlreadyBuffered method and the _pull method, and processing each message with the manager. It returns a list of messages fetched within the specified limits.

The _fetch method is used to retrieve a batch of messages from a NATS JetStream pull subscription.

It takes in two parameters: batchSize - the maximum number of messages to fetch in one batch, and maxWaitMillis - the maximum time to wait for new messages to arrive.

The method first drains any messages that have already been buffered from previous fetch operations. It then calculates the number of messages still needed to reach the desired batch size.

If there are already enough buffered messages, the method returns the buffered messages.

Otherwise, it starts a loop to fetch new messages from the subscription. It tries to fetch as many messages as possible within the remaining time, based on the maxWaitMillis parameter. The loop continues until either the desired batch size is reached or the maximum wait time has been exceeded.

For each fetched message, it checks the result of the manager.manage(msg) method. If the result is MESSAGE, it adds the message to the list of fetched messages and decrements the count of messages still needed. If the result is STATUS_TERMINUS, it checks if the pull subject matches the message's subject. If it does, it returns the fetched messages. If the result is STATUS_ERROR, it checks if the pull subject matches the message's subject and throws a JetStreamStatusException if it does.

After processing each message, it calculates the remaining time and continues the loop if there is still time left.

If an InterruptedException is caught, the method interrupts the current thread and returns the already fetched messages.

Finally, it returns the list of fetched messages.

private List drainAlreadyBuffered(int batchSize)

private List<Message> drainAlreadyBuffered(int batchSize) {
    List<Message> messages = new ArrayList<>(batchSize);
    try {
        while (true) {
            Message msg = nextMessageInternal(null);
            if (msg == null) {
                // no more message currently queued
                return messages;
            }
            if (manager.manage(msg) == MessageManager.ManageResult.MESSAGE) {
                messages.add(msg);
                if (messages.size() == batchSize) {
                    return messages;
                }
            }
            // since this is buffered, no non-message applies, try again
        }
    } catch (InterruptedException ignore) {
        Thread.currentThread().interrupt();
    }
    return messages;
}

The drainAlreadyBuffered method, defined in the class io.nats.client.impl.NatsJetStreamPullSubscription, is used to retrieve a batch of messages from the subscription's buffer.

Here is a step-by-step description of what the method does:

  1. The method takes an integer parameter batchSize indicating the maximum number of messages to retrieve in the batch.

  2. It initializes an empty ArrayList called messages with a capacity of batchSize.

  3. The method enters a while loop that runs indefinitely until a return statement is reached.

  4. Inside the loop, the nextMessageInternal method is called with null as a parameter to retrieve the next message from the subscription's internal buffer.

  5. If nextMessageInternal returns null, it means there are no more messages currently queued in the buffer. In this case, the messages list (which may contain previously retrieved messages) is returned.

  6. If nextMessageInternal returns a non-null Message object, it means a message has been retrieved from the buffer.

  7. The retrieved message is then passed to the manager.manage method, which performs some management operations on the message. Depending on the result of this operation, the message may or may not be added to the messages list.

  8. If the manager.manage method returns MessageManager.ManageResult.MESSAGE, indicating that the message should be processed, it is added to the messages list using the messages.add method.

  9. After adding the message to the list, the method checks if the size of the messages list has reached the batchSize limit. If so, it means the desired number of messages for the batch has been reached, and the messages list is returned.

  10. If the messages list has not reached the batchSize limit, the loop continues, and the next message is retrieved from the buffer.

  11. If an InterruptedException is caught during the execution of the loop, it means the thread executing the method has been interrupted. In this case, the interrupt flag is set again by calling Thread.currentThread().interrupt(), and the method exits.

  12. Finally, the method returns the messages list, which may contain some messages retrieved from the buffer.

Note: The exact behavior and purpose of the nextMessageInternal method, as well as the manager object and its manage method, are not described in the provided code snippet. Further information about these components would be necessary to fully understand the functionality of the drainAlreadyBuffered method.

The drainAlreadyBuffered method in the NatsJetStreamPullSubscription class is responsible for retrieving a batch of already buffered messages from the subscription.

It takes in a parameter batchSize, which specifies the maximum number of messages to retrieve in a batch. The method creates an empty list called messages to store the retrieved messages.

The method then enters a while loop that continues until there are no more messages currently queued. Within the loop, it calls the nextMessageInternal method to fetch the next message. If there is no message available, the method returns the list of retrieved messages.

If a message is found, it is passed to the manager.manage method, which determines if the message should be added to the messages list based on some condition. If the condition is met, the message is added to the list.

The method checks if the messages list has reached the batchSize limit, and if so, it returns the list of retrieved messages.

If the loop is interrupted by an InterruptedException, the method interrupts the current thread and returns the messages list.

Finally, if there are no more messages and the loop exits, the method returns the messages list.

In summary, the drainAlreadyBuffered method iteratively retrieves messages from an already buffered queue until no more messages are available or until the desired batch size is reached. It returns the retrieved messages in a list.

sequence diagram

private Iterator _iterate(final int batchSize, long maxWaitMillis)

private Iterator<Message> _iterate(final int batchSize, long maxWaitMillis) {
    final List<Message> buffered = drainAlreadyBuffered(batchSize);
    // if there was a full batch buffered, no need to pull, just iterate over the list you already have
    int batchLeft = batchSize - buffered.size();
    if (batchLeft == 0) {
        return new Iterator<Message>() {

            @Override
            public boolean hasNext() {
                return buffered.size() > 0;
            }

            @Override
            public Message next() {
                return buffered.remove(0);
            }
        };
    }
    // if there were some messages buffered, reduce the raw pull batch size
    String pullSubject = _pull(PullRequestOptions.builder(batchLeft).expiresIn(maxWaitMillis).build(), false, null);
    final long timeout = maxWaitMillis;
    // the iterator is also more complicated
    return new Iterator<Message>() {

        int received = 0;

        boolean done = false;

        Message msg = null;

        @Override
        public boolean hasNext() {
            try {
                if (msg != null) {
                    return true;
                }
                if (done) {
                    return false;
                }
                if (buffered.size() == 0) {
                    msg = _nextUnmanaged(timeout, pullSubject);
                    if (msg == null) {
                        done = true;
                        return false;
                    }
                } else {
                    msg = buffered.remove(0);
                }
                done = ++received == batchSize;
                return true;
            } catch (InterruptedException e) {
                msg = null;
                done = true;
                Thread.currentThread().interrupt();
                return false;
            }
        }

        @Override
        public Message next() {
            Message next = msg;
            msg = null;
            return next;
        }
    };
}

The _iterate method in the NatsJetStreamPullSubscription class is a private method that returns an iterator over a batch of messages.

  1. The method starts by draining any already buffered messages from a list called buffered using the drainAlreadyBuffered method. The batchSize parameter determines the number of messages to drain.

  2. If the number of messages drained is equal to the batchSize, meaning a full batch was already buffered, the method returns a new iterator that iterates over the buffered list.

  3. If some messages were drained from the buffer, the method reduces the raw pull batch size by the number of messages remaining to reach the batchSize. It does this by calling the _pull method with the reduced batch size, false for the expectsSingle parameter, and null for the pullSubject parameter.

  4. The method then creates a new iterator. This iterator keeps track of the number of received messages (received), whether the iteration is done (done), and the current message being iterated (msg).

  5. The hasNext method of the iterator is implemented as follows:

    • If msg is not null, indicating that there is a message already fetched, the method returns true.
    • If done is true, indicating that the iteration is completed, the method returns false.
    • If the buffered list is empty, meaning there are no more buffered messages, the method calls the _nextUnmanaged method with the timeout and pullSubject parameters to fetch the next message from the server. If the returned message is null, indicating that there are no more messages, the method sets done to true and returns false.
    • If the buffered list is not empty, the method removes the first message from the list and assigns it to the msg variable.
    • The method then increments the received count and checks if it is equal to the batchSize. If it is, the method sets done to true.
  6. The next method of the iterator simply returns the current message (msg) and resets it to null to prepare for the next iteration.

The iterator returned by the _iterate method allows iterating over a batch of messages, fetching from the buffer or pulling from the server as needed, until the batch size is reached or there are no more messages.

The _iterate method is a private method defined in the NatsJetStreamPullSubscription class. This method is used to iterate over a batch of messages from a pull subscription in the NATS JetStream client library.

Here's a breakdown of what the method does:

  1. It first drains any messages that are already buffered up to the specified batch size.
  2. If there are a full batch of messages already buffered, it returns an iterator that iterates over the existing list of messages.
  3. If there are some messages already buffered, it adjusts the pull batch size accordingly.
  4. It calls the _pull method to initiate a pull request for the remaining messages, with a reduced batch size and a maximum wait time.
  5. It then returns an iterator that fetches messages from the pulled messages or the buffered messages until the batch size is reached.
  6. The hasNext method is used to check if there is a next message available.
    • If a message is already fetched from the pull or buffered messages, it returns true.
    • If the iterator is done iterating, it returns false.
    • If there are no buffered messages, it calls the _nextUnmanaged method to fetch the next message from the pull subscription, with a timeout value.
    • If there are buffered messages, it removes the first message from the buffer and assigns it to the msg variable.
    • It tracks the number of received messages and checks if the batch size has been reached to set the done flag.
  7. The next method is used to retrieve the next message from the iterator. It returns the msg variable and resets it to null.

Overall, this method is responsible for iterating over a batch of messages from a pull subscription, pulling new messages if necessary, and returning them through an iterator.

sequence diagram

NatsJetStreamSubscription

NatsJetStreamSubscription

The NatsJetStreamSubscription class is a JetStream specific subscription that extends the NatsSubscription class and implements the JetStreamSubscription and NatsJetStreamConstants interfaces. It provides functionality for subscribing to messages using the JetStream protocol.

@Override

public Message nextMessage(Duration timeout) throws InterruptedException, IllegalStateException

@Override
public Message nextMessage(Duration timeout) throws InterruptedException, IllegalStateException {
    if (timeout == null) {
        return _nextUnmanagedNoWait(null);
    }
    long millis = timeout.toMillis();
    if (millis <= 0) {
        return _nextUnmanagedWaitForever(null);
    }
    return _nextUnmanaged(millis, null);
}

Method: nextMessage(Duration timeout)

This method is used to retrieve the next message from the NatsJetStreamSubscription.

Parameters

  • timeout: A Duration object representing the maximum amount of time to wait for a message. If null, the method will attempt to retrieve a message without waiting.

Return Value

  • Returns the next message that is received from the NatsJetStreamSubscription.

Steps

  1. Check if the timeout parameter is null.

    • If it is null, proceed to step 2.
    • Otherwise, continue to step 3.
  2. Call the method _nextUnmanagedNoWait(null) to retrieve the next message without waiting.

    • Return the message.
  3. Convert the timeout into milliseconds by calling timeout.toMillis().

    • Store the result in the variable millis.
  4. Check if the value of millis is less than or equal to 0.

    • If it is less than or equal to 0, proceed to step 5.
    • Otherwise, continue to step 6.
  5. Call the method _nextUnmanagedWaitForever(null) to retrieve the next message, waiting indefinitely.

    • Return the message.
  6. Call the method _nextUnmanaged(millis, null) to retrieve the next message, waiting for the specified duration.

    • Return the message.

The nextMessage method in the io.nats.client.impl.NatsJetStreamSubscription class is responsible for retrieving the next message from the subscription, with an optional timeout parameter.

If the timeout parameter is null, the method calls the _nextUnmanagedNoWait method to immediately return the next available message, if any.

If the timeout parameter is not null, the method converts it to milliseconds and checks if it is less than or equal to 0. If so, it calls the _nextUnmanagedWaitForever method to wait indefinitely for the next message.

Otherwise, it calls the _nextUnmanaged method with the timeout in milliseconds to wait for the next message. The retrieved Message object is then returned.

The method can throw InterruptedException if the thread is interrupted while waiting for the next message, and IllegalStateException if the subscription is in an invalid state.

sequence diagram

@Override

public Message nextMessage(long timeoutMillis) throws InterruptedException, IllegalStateException

@Override
public Message nextMessage(long timeoutMillis) throws InterruptedException, IllegalStateException {
    if (timeoutMillis <= 0) {
        return _nextUnmanagedWaitForever(null);
    }
    return _nextUnmanaged(timeoutMillis, null);
}

The nextMessage method in the NatsJetStreamSubscription class is responsible for retrieving the next message from the subscription queue.

The method takes a timeoutMillis parameter, which represents the maximum amount of time in milliseconds to wait for a message to become available.

If the timeoutMillis value is less than or equal to 0, the method calls the _nextUnmanagedWaitForever method to wait indefinitely until a message is received. Otherwise, it calls the _nextUnmanaged method with the given timeout to wait for a message until the timeout expires.

The method returns the next available message or null if no message is received within the specified timeout.

sequence diagram

protected Message _nextUnmanagedWaitForever(String expectedPullSubject) throws InterruptedException

protected Message _nextUnmanagedWaitForever(String expectedPullSubject) throws InterruptedException {
    while (true) {
        Message msg = nextMessageInternal(Duration.ZERO);
        if (msg != null) {
            // null shouldn't happen, so just a code guard b/c nextMessageInternal can return null
            switch(manager.manage(msg)) {
                case MESSAGE:
                    return msg;
                case STATUS_ERROR:
                    // if the status applies throw exception, otherwise it's ignored, fall through
                    if (expectedPullSubject == null || expectedPullSubject.equals(msg.getSubject())) {
                        throw new JetStreamStatusException(msg.getStatus(), this);
                    }
                    break;
            }
            // Check again since waiting forever when:
            // 1. Any STATUS_HANDLED or STATUS_TERMINUS
            // 2. STATUS_ERRORS that aren't for expected pullSubject
        }
    }
}

The _nextUnmanagedWaitForever method defined in class io.nats.client.impl.NatsJetStreamSubscription is a method that waits indefinitely for the next message from the NATS server.

Here is a step-by-step description of what this method does based on its body:

  1. The method enters an infinite loop using while (true).
  2. Within the loop, it calls the nextMessageInternal method with a duration of zero, indicating that it should wait for the next message immediately.
  3. If a message is returned by nextMessageInternal (i.e., msg is not null), the method proceeds to the next step.
  4. It checks the status of the message by calling manager.manage(msg). The manager object is responsible for handling the message and returning a status.
  5. If the status returned by manager.manage(msg) is MESSAGE, it means that the message is a regular message, and the method returns the message by return msg.
  6. If the status is STATUS_ERROR, the method checks whether the expected pull subject is null or equal to the subject of the message. If it is, it means that the message is an error related to the expected pull subject, and the method throws a JetStreamStatusException with the message's status and a reference to the subscription.
  7. If the status is neither MESSAGE nor STATUS_ERROR, the method continues to the next iteration of the loop and waits for the next message.
  8. The method repeats the above steps, checking for a new message in each iteration of the loop.

This method is useful when you want to wait indefinitely for the next message from the NATS server without specifying a timeout duration. It is often used in scenarios where a continuous stream of messages needs to be processed without interruption.

The _nextUnmanagedWaitForever method in the NatsJetStreamSubscription class is responsible for retrieving the next message from the subscription and handling it according to certain conditions.

The method uses an infinite loop to continuously retrieve the next message using the nextMessageInternal method with a duration of zero (indicating no timeout). It then checks the retrieved message against several conditions.

If the message is not null, it proceeds to check the status of the message using the manage method of the subscription's manager. Based on the returned status, the method performs different actions:

  • If the status is MESSAGE, the method returns the message.
  • If the status is STATUS_ERROR, the method checks if the expectedPullSubject (passed as a parameter) is null or if it matches the subject of the message. If it does, the method throws a JetStreamStatusException with the message's status and a reference to the subscription.
  • If none of the conditions above apply, the method continues to the next iteration of the loop to check for more messages.

The purpose of this method is to continuously retrieve messages from the subscription and handle them appropriately based on their status and the expected pull subject, if provided.

sequence diagram

protected Message _nextUnmanagedNoWait(String expectedPullSubject) throws InterruptedException

protected Message _nextUnmanagedNoWait(String expectedPullSubject) throws InterruptedException {
    while (true) {
        Message msg = nextMessageInternal(null);
        if (msg == null) {
            return null;
        }
        switch(manager.manage(msg)) {
            case MESSAGE:
                return msg;
            case STATUS_TERMINUS:
                // if the status applies return null, otherwise it's ignored, fall through
                if (expectedPullSubject == null || expectedPullSubject.equals(msg.getSubject())) {
                    return null;
                }
                break;
            case STATUS_ERROR:
                // if the status applies throw exception, otherwise it's ignored, fall through
                if (expectedPullSubject == null || expectedPullSubject.equals(msg.getSubject())) {
                    throw new JetStreamStatusException(msg.getStatus(), this);
                }
                break;
        }
        // Check again when, regular messages might have arrived
        // 1. Any STATUS_HANDLED
        // 2. STATUS_TERMINUS or STATUS_ERRORS that aren't for expected pullSubject
    }
}

The _nextUnmanagedNoWait method is defined in the NatsJetStreamSubscription class and it takes an expectedPullSubject parameter. Here is a step-by-step description of what this method does:

  1. The method enters an infinite loop using the while (true) statement.
  2. Inside the loop, it calls the nextMessageInternal method to retrieve the next message from the subscription. If there are no more messages, it returns null.
  3. After getting a message, the method calls the manage method of the subscription's manager object (manager.manage(msg)) and performs different actions based on the returned value.
  4. If the returned value is MESSAGE, it means that the message is a regular message, and the method returns the message.
  5. If the returned value is STATUS_TERMINUS, it means that the message contains a termination status. The method checks if expectedPullSubject is null or if it matches the subject of the message. If it does, the method returns null to indicate that the status applies. If it doesn't match, it continues to the next iteration of the loop.
  6. If the returned value is STATUS_ERROR, it means that the message contains an error status. The method follows a similar logic as in step 5, but instead of returning null, it throws a JetStreamStatusException with the status and the current subscription as parameters.
  7. After handling the message, the method checks again for any new messages that might have arrived in the meantime.
    • If there is any message with STATUS_HANDLED, it means that another thread has already handled the message and it continues to the next iteration of the loop.
    • If there is any message with STATUS_TERMINUS or STATUS_ERROR that doesn't match the expectedPullSubject, it means that another message has arrived that is not relevant to the current pull operation, and it continues to the next iteration of the loop.

The method keeps looping until a regular message is returned or an error occurs.

The method _nextUnmanagedNoWait is a protected method defined in the NatsJetStreamSubscription class of the io.nats.client.impl package.

This method is responsible for retrieving the next message from the subscription, without waiting for new messages to arrive. It takes the expectedPullSubject as a parameter, which is the subject that the client is expecting to pull messages from.

Inside the method, there is an infinite while loop that continuously checks for the next message using the nextMessageInternal method. If there is no message available, the method returns null.

If a message is received, it is passed to the manage method of the manager object. The manage method is expected to return one of the following enum values: MESSAGE, STATUS_TERMINUS, or STATUS_ERROR.

Based on the returned value, the method takes appropriate actions. If the message indicates a regular message (enum value MESSAGE), the message is returned. If the message indicates a termination status for the subscription (enum value STATUS_TERMINUS), the method checks if this termination status is applicable for the expectedPullSubject. If it is, the method returns null; otherwise, it continues to the next iteration of the loop. If the message indicates an error status (enum value STATUS_ERROR), the method checks if this error status is applicable for the expectedPullSubject. If it is, a JetStreamStatusException is thrown; otherwise, it continues to the next iteration of the loop.

After processing the message, the loop checks again for any regular messages that might have arrived. It specifically looks for:

  1. Any STATUS_HANDLED messages.
  2. STATUS_TERMINUS or STATUS_ERRORS messages that are not for the expectedPullSubject.

This method is used for retrieving messages from a JetStream subscription in a non-blocking manner and handling different statuses appropriately.

sequence diagram

protected Message _nextUnmanaged(long timeout, String expectedPullSubject) throws InterruptedException

protected Message _nextUnmanaged(long timeout, String expectedPullSubject) throws InterruptedException {
    long timeoutNanos = timeout * 1_000_000;
    long timeLeftNanos = timeoutNanos;
    long start = System.nanoTime();
    while (timeLeftNanos > 0) {
        Message msg = nextMessageInternal(Duration.ofNanos(timeLeftNanos));
        if (msg == null) {
            // normal timeout
            return null;
        }
        switch(manager.manage(msg)) {
            case MESSAGE:
                return msg;
            case STATUS_TERMINUS:
                // if the status applies return null, otherwise it's ignored, fall through
                if (expectedPullSubject == null || expectedPullSubject.equals(msg.getSubject())) {
                    return null;
                }
                break;
            case STATUS_ERROR:
                // if the status applies throw exception, otherwise it's ignored, fall through
                if (expectedPullSubject == null || expectedPullSubject.equals(msg.getSubject())) {
                    throw new JetStreamStatusException(msg.getStatus(), this);
                }
                break;
        }
        // anything else, try again while we have time
        timeLeftNanos = timeoutNanos - (System.nanoTime() - start);
    }
    return null;
}

The _nextUnmanaged method is defined in the io.nats.client.impl.NatsJetStreamSubscription class. It is a protected method that returns a Message object. The method takes two parameters: timeout, which is a long representing the timeout period in milliseconds, and expectedPullSubject, which is a String representing the expected pull subject.

Here is a step-by-step description of what the method does:

  1. Convert the timeout in milliseconds to nanoseconds by multiplying it by 1_000_000.
  2. Initialize the timeLeftNanos variable to the value of timeoutNanos.
  3. Get the current time in nanoseconds and store it in the start variable.
  4. Enter a while loop that runs as long as timeLeftNanos is greater than 0.
  5. Call the nextMessageInternal method, passing in a Duration object created from timeLeftNanos.
  6. If the returned message is null, it means the timeout period has elapsed, so return null.
  7. Switch on the result of calling the manage method of the manager object, passing in the received message.
  8. If the result is MESSAGE, return the message.
  9. If the result is STATUS_TERMINUS, check if the expectedPullSubject is null or if it is equal to the subject of the received message. If either condition is true, return null, otherwise continue to the next case.
  10. If the result is STATUS_ERROR, check if the expectedPullSubject is null or if it is equal to the subject of the received message. If either condition is true, throw a JetStreamStatusException with the status from the received message and the current subscription.
  11. If none of the above cases apply, continue to the next iteration of the loop.
  12. Calculate the remaining time by subtracting the elapsed time since the start from timeoutNanos, and store it in timeLeftNanos.
  13. Return null if the timeout period has elapsed.

That's the step-by-step description of the _nextUnmanaged method.

The _nextUnmanaged method is used in the NatsJetStreamSubscription class to retrieve the next message from the subscription.

This method takes in a timeout value (in milliseconds) and an expected pull subject. It then calculates the timeout in nanoseconds and iteratively checks for the next message until either a message is received or the timeout expires.

Within the loop, the method calls the nextMessageInternal method to get the next message. If no message is received within the timeout, null is returned.

If a message is received, the method uses the manager to manage the message. Depending on the result, the method either returns the message (MESSAGE result), returns null if the message's subject matches the expected pull subject (STATUS_TERMINUS result), or throws a JetStreamStatusException if the message's subject matches the expected pull subject and the result is STATUS_ERROR.

If none of the above conditions are met, the method continues to try again until there is no time left, and then returns null.

Overall, the _nextUnmanaged method is responsible for fetching the next message from the subscription, managing the message, and handling various scenarios based on the result.

sequence diagram

NatsDispatcher

NatsDispatcher

The NatsDispatcher class is a subclass of NatsConsumer and implements the Dispatcher and Runnable interfaces. It provides functionality for dispatching events and running background tasks.

public void run()

public void run() {
    try {
        while (this.running.get()) {
            // start
            NatsMessage msg = this.incoming.pop(this.waitForMessage);
            if (msg != null) {
                NatsSubscription sub = msg.getNatsSubscription();
                if (sub != null && sub.isActive()) {
                    MessageHandler handler = subscriptionHandlers.get(sub.getSID());
                    if (handler == null) {
                        handler = defaultHandler;
                    }
                    // A dispatcher can have a null defaultHandler. You can't subscribe without a handler,
                    // but messages might come in while the dispatcher is being closed or after unsubscribe
                    // and the [non-default] handler has already been removed from subscriptionHandlers
                    if (handler != null) {
                        sub.incrementDeliveredCount();
                        this.incrementDeliveredCount();
                        try {
                            handler.onMessage(msg);
                        } catch (Exception exp) {
                            connection.processException(exp);
                        }
                        if (sub.reachedUnsubLimit()) {
                            connection.invalidate(sub);
                        }
                    }
                }
            }
            if (breakRunLoop()) {
                return;
            }
        }
    } catch (InterruptedException exp) {
        if (this.running.get()) {
            this.connection.processException(exp);
        }
        //otherwise we did it
    } finally {
        this.running.set(false);
        this.thread = null;
    }
}

The run() method in class io.nats.client.impl.NatsDispatcher is responsible for running the message dispatching loop. Here is a step-by-step description of what this method does:

  1. The method is wrapped in a try-catch block.
  2. There is a while loop that continues running as long as the running flag is set to true. This flag is a AtomicBoolean that can be toggled by calling the stop() method on the dispatcher.
  3. Inside the loop, the method pops a message from the incoming queue, using the pop() method. The waitForMessage parameter determines the maximum time to wait for a message to be available.
  4. If a message is retrieved (msg != null), the method checks if the message's associated subscription is active (sub.isActive()).
  5. If the subscription is active, the method retrieves the message handler for the subscription from a map called subscriptionHandlers. If there is no specific handler found for the subscription, it uses the defaultHandler.
  6. If a handler is found (either specific or default), the method increments the delivery count for both the subscription and the dispatcher.
  7. The method then invokes the onMessage() method of the handler, passing the message as an argument. If an exception is thrown during the invocation, the exception is processed by the connection to which the dispatcher belongs.
  8. After handling the message, the method checks if the subscription has reached its unsubscribe limit (sub.reachedUnsubLimit()). If so, it invalidates the subscription by calling connection.invalidate(sub).
  9. After processing the message, the method checks if the breakRunLoop() method returns true. This method can be overridden by subclasses to provide custom logic for breaking out of the loop. If true is returned, the method returns, ending the loop.
  10. If no message is retrieved from the queue or the subscription is not active, or the subscription handler is missing, the method continues to the next iteration of the loop.
  11. If an InterruptedException is thrown while waiting for a message, the method checks if the running flag is still set to true. If it is, the exception is processed by the connection.
  12. Finally, the running flag is set to false, indicating that the dispatcher has stopped running. The thread field is set to null, indicating that the underlying thread has terminated.

This method essentially fetches messages from the incoming queue, checks if the associated subscriptions are active, invokes the appropriate message handler, and takes appropriate actions based on the subscription's state. It continues to run until explicitly stopped or interrupted.

The run method in the NatsDispatcher class is responsible for continuously processing incoming messages from the NATS server. It runs in a loop until a specific condition is met.

Within the loop, it pops an incoming message from the message queue and checks if it is not null. If the message exists, it retrieves the corresponding subscription and checks if it is active. If the subscription is active, it retrieves the message handler associated with the subscription from the subscriptionHandlers map. If no handler is found, it falls back to the default handler.

The method then increments the delivered count for both the subscription and the dispatcher, and proceeds to invoke the onMessage method of the handler, passing the message as a parameter. If any exception occurs during the execution of the handler, it is caught and processed by the connection object.

After handling the message, the method checks if the subscription has reached its unsubscribe limit. If so, it invalidates the subscription on the connection.

The method also checks for a condition to break the run loop, and if it is met, the method returns.

If any interrupted exception is thrown during the execution of the method, it is caught and processed by the connection. Finally, the method sets the running flag to false and clears the thread variable.

sequence diagram

void stop(boolean unsubscribeAll)

void stop(boolean unsubscribeAll) {
    this.running.set(false);
    this.incoming.pause();
    if (this.thread != null) {
        try {
            if (!this.thread.isCancelled()) {
                this.thread.cancel(true);
            }
        } catch (Exception exp) {
            // let it go
        }
    }
    if (unsubscribeAll) {
        this.subscriptionsUsingDefaultHandler.forEach((subj, sub) -> {
            this.connection.unsubscribe(sub, -1);
        });
        this.subscriptionsWithHandlers.forEach((sid, sub) -> {
            this.connection.unsubscribe(sub, -1);
        });
    }
    this.subscriptionsUsingDefaultHandler.clear();
    this.subscriptionsWithHandlers.clear();
    this.subscriptionHandlers.clear();
}

The stop method of the NatsDispatcher class is used to stop the dispatcher and perform cleanup tasks. It takes a boolean parameter unsubscribeAll that indicates whether to unsubscribe from all currently subscribed subjects or not.

Here is a step-by-step description of the method:

  1. Set running flag to false using the set method of the AtomicBoolean variable running. This flag is used to control the main loop of the dispatcher.

  2. Pause the incoming message processing by calling the pause method of the Incoming object this.incoming. This method will stop the processing of incoming messages until it is resumed.

  3. Check if the thread object is not null (indicating that the dispatcher is running on a separate thread).

  4. Inside the conditional block, check if the thread is not cancelled by calling the isCancelled method of the Future object this.thread. This method returns true if the task running on the thread has been cancelled.

  5. If the thread is not cancelled, cancel it by calling the cancel method of the Future object this.thread. The true parameter indicates that the task should be interrupted if it is currently running.

  6. Handle any exceptions that may occur during the cancellation process by catching Exception objects and ignoring them. The comment "// let it go" indicates that any exceptions should be ignored.

  7. If the unsubscribeAll parameter is true, perform unsubscribe operations for all subscriptions using default handlers and for subscriptions with specific handlers.

  8. Iterate over each entry in the subscriptionsUsingDefaultHandler map using a lambda expression. For each entry, call the unsubscribe method of the Connection object this.connection to unsubscribe from the subject specified by the subscription. The -1 indicates that the subscription should be immediately unsubscribed.

  9. Iterate over each entry in the subscriptionsWithHandlers map using a lambda expression. For each entry, call the unsubscribe method of the Connection object this.connection to unsubscribe from the subject specified by the subscription. The -1 indicates that the subscription should be immediately unsubscribed.

  10. Clear the subscriptionsUsingDefaultHandler, subscriptionsWithHandlers, and subscriptionHandlers maps to remove all stored subscription data.

The stop method is used to gracefully stop the dispatcher and clean up any resources associated with it.

The stop method in the NatsDispatcher class is responsible for stopping the dispatcher processing and clearing all subscriptions.

Here is a brief description of what the method does:

  • It sets the running flag to false, indicating that the dispatcher is no longer running.
  • It pauses the incoming channel, preventing any further message processing.
  • It checks if there is a running thread associated with the dispatcher, and if so, tries to cancel it.
  • If the unsubscribeAll parameter is true, it unsubscribes from all subscriptions.
  • It clears the collections that store the subscriptions and their associated handlers.

sequence diagram

void resendSubscriptions()

void resendSubscriptions() {
    this.subscriptionsUsingDefaultHandler.forEach((id, sub) -> {
        this.connection.sendSubscriptionMessage(sub.getSID(), sub.getSubject(), sub.getQueueName(), true);
    });
    this.subscriptionsWithHandlers.forEach((sid, sub) -> {
        this.connection.sendSubscriptionMessage(sub.getSID(), sub.getSubject(), sub.getQueueName(), true);
    });
}

The resendSubscriptions method in the NatsDispatcher class is used to resend all the subscriptions stored in two different maps, namely subscriptionsUsingDefaultHandler and subscriptionsWithHandlers.

Here is a step-by-step description of what the method does:

  1. Iterate over each entry in the subscriptionsUsingDefaultHandler map.
  2. For each entry, retrieve the subscription ID and the corresponding subscription object.
  3. Use the connection object to send a subscription message using the following parameters:
  • Subscription ID: Retrieved from the subscription object using sub.getSID()
  • Subject: Retrieved from the subscription object using sub.getSubject()
  • Queue Name: Retrieved from the subscription object using sub.getQueueName()
  • Resend Flag: Set to true
  1. Repeat steps 2-3 for all entries in the subscriptionsUsingDefaultHandler map.
  2. Iterate over each entry in the subscriptionsWithHandlers map.
  3. For each entry, retrieve the subscription ID and the corresponding subscription object.
  4. Use the connection object to send a subscription message using the same parameters as in step 3.
  5. Repeat steps 6-7 for all entries in the subscriptionsWithHandlers map.

Overall, the resendSubscriptions method is responsible for resending all the subscriptions in the NatsDispatcher class by iterating over the two maps and using the connection object to send subscription messages with the subscription details.

The resendSubscriptions method in the NatsDispatcher class is responsible for resending subscriptions to the NATS server.

The method iterates over two sets of subscriptions: subscriptionsUsingDefaultHandler and subscriptionsWithHandlers. For each subscription, it retrieves the subscription ID, subject, and queue name using the getSID(), getSubject(), and getQueueName() methods, respectively. It then calls the sendSubscriptionMessage() method on the connection object, passing in the subscription ID, subject, queue name, and a boolean value of true to indicate that it should be resent.

The purpose of this method is to ensure that all subscriptions are reestablished with the NATS server, in case any were lost or not initially sent properly.

sequence diagram

// Called by the connection when a subscription is removed.

// We will first attempt to remove from subscriptionsWithHandlers // using the sub's SID, and if we don't find it there, we'll check // the subscriptionsUsingDefaultHandler Map and verify the SID // matches before removing. By verifying the SID in all cases we can // be certain we're removing the correct Subscription. void remove(NatsSubscription sub)

// Called by the connection when a subscription is removed.
// We will first attempt to remove from subscriptionsWithHandlers
// using the sub's SID, and if we don't find it there, we'll check
// the subscriptionsUsingDefaultHandler Map and verify the SID
// matches before removing. By verifying the SID in all cases we can
// be certain we're removing the correct Subscription.
void remove(NatsSubscription sub) {
    if (this.subscriptionsWithHandlers.remove(sub.getSID()) != null) {
        this.subscriptionHandlers.remove(sub.getSID());
    } else {
        NatsSubscription s = this.subscriptionsUsingDefaultHandler.get(sub.getSubject());
        if (s.getSID().equals(sub.getSID())) {
            this.subscriptionsUsingDefaultHandler.remove(sub.getSubject());
        }
    }
}

The remove method in the NatsDispatcher class is used to remove a subscription from the dispatcher. Here is a step-by-step description of what this method is doing:

  1. Check if the given subscription sub exists in the subscriptionsWithHandlers map using its subscription ID (SID).
  2. If the subscription is found in the subscriptionsWithHandlers map, remove it by calling the remove method on the map and passing the subscription ID.
  3. If the removal is successful (i.e., the subscription ID is found in the map), also remove the subscription's handler from the subscriptionHandlers map using the subscription ID as the key.
  4. If the subscription is not found in the subscriptionsWithHandlers map, retrieve the subscription with the same subject from the subscriptionsUsingDefaultHandler map.
  5. Check if the retrieved subscription's subscription ID matches the ID of the given subscription (sub).
  6. If the subscription IDs match, remove the retrieved subscription from the subscriptionsUsingDefaultHandler map by calling the remove method on the map and passing the subscription's subject as the key.

By performing these steps, the remove method ensures that the correct subscription is removed from the dispatcher, either by searching for its subscription ID in the subscriptionsWithHandlers map or by matching its ID with the retrieved subscription from the subscriptionsUsingDefaultHandler map.

The remove method in the NatsDispatcher class is used to remove a subscription from the dispatcher. This method is called when a subscription is being removed from the connection.

First, it tries to remove the subscription from the subscriptionsWithHandlers map using the subscription's SID (unique identifier). If the subscription is found in this map, it is removed along with its handler from the subscriptionHandlers map.

If the subscription is not found in the subscriptionsWithHandlers map, the method checks the subscriptionsUsingDefaultHandler map to find a subscription with the same subject and verifies that the SID matches. If the SID matches, the subscription is removed from the subscriptionsUsingDefaultHandler map.

By verifying the SID in both cases, the method ensures that it is removing the correct subscription.

sequence diagram

public Dispatcher subscribe(String subject)

public Dispatcher subscribe(String subject) {
    if (subject == null || subject.length() == 0) {
        throw new IllegalArgumentException("Subject is required in subscribe");
    }
    this.subscribeImplCore(subject, null, null);
    return this;
}

The subscribe method in the NatsDispatcher class is used to subscribe to a specific subject in a NATS system.

Here is a step-by-step description of what the method is doing based on its body:

  1. Check if the subject parameter is null or an empty string.
  2. If subject is null or an empty string, throw an IllegalArgumentException with a message stating that the subject is required for subscribing.
  3. If the subject is valid (not null or empty), call the subscribeImplCore method with the subject, null, and null parameters.
  4. The subscribeImplCore method is responsible for the actual implementation of subscribing to the subject.
  5. Return a reference to the current instance of the NatsDispatcher class (i.e., this).

In summary, the subscribe method ensures that a valid subject is provided, calls the subscribeImplCore method to perform the subscription, and returns a reference to the NatsDispatcher object.

The subscribe method is defined in the class NatsDispatcher and is used to subscribe to a specific subject in the NATS messaging system.

The method takes a subject as a parameter, which represents the name of the subject to subscribe to. If the subject is null or an empty string, an IllegalArgumentException is thrown, indicating that a valid subject is required for the subscription.

Internally, the method calls the subscribeImplCore method, passing the subject along with null values for message handlers and options.

Finally, the method returns a reference to the current NatsDispatcher instance, allowing for method chaining if desired.

sequence diagram

NatsSubscription subscribeReturningSubscription(String subject)

NatsSubscription subscribeReturningSubscription(String subject) {
    if (subject == null || subject.length() == 0) {
        throw new IllegalArgumentException("Subject is required in subscribe");
    }
    return this.subscribeImplCore(subject, null, null);
}

The subscribeReturningSubscription method in the NatsDispatcher class is responsible for subscribing to a NATS subject and returning a NatsSubscription object.

Here is a step-by-step description of what this method does:

  1. It takes a subject as input parameter.
  2. It first checks if the subject is null or empty. If it is, it throws an IllegalArgumentException with a message stating that the subject is required for subscription.
  3. If the subject is not null or empty, it calls the subscribeImplCore method passing the subject, as well as null for the queue and null for the durable.
  4. The subscribeImplCore method is responsible for actually subscribing to the subject and returning a NatsSubscription object.
  5. Finally, the subscribeReturningSubscription method returns the NatsSubscription object obtained from the subscribeImplCore method.

That's the step-by-step description of the subscribeReturningSubscription method and what it does based on its implementation.

The subscribeReturningSubscription method is a functionality defined in the NatsDispatcher class that allows the software engineer to subscribe to a specific NATS subject and return a NatsSubscription object.

The method takes in a subject parameter, which is the name of the NATS subject to subscribe to. If the subject is null or has a length of 0, the method throws an IllegalArgumentException with the message "Subject is required in subscribe".

The method then calls the subscribeImplCore method with the subject parameter and null values for the queue and callback parameters. Finally, it returns the result of the subscribeImplCore method, which is a NatsSubscription object.

sequence diagram

public Subscription subscribe(String subject, MessageHandler handler)

public Subscription subscribe(String subject, MessageHandler handler) {
    if (subject == null || subject.length() == 0) {
        throw new IllegalArgumentException("Subject is required in subscribe");
    }
    if (handler == null) {
        throw new IllegalArgumentException("MessageHandler is required in subscribe");
    }
    return this.subscribeImplCore(subject, null, handler);
}

The subscribe method in the NatsDispatcher class is used to subscribe to a specified subject and register a MessageHandler to handle incoming messages on that subject. Here is a step-by-step description of what this method is doing:

  1. Check if the subject parameter is null or empty.
  2. If the subject is null or empty, throw an IllegalArgumentException with the message "Subject is required in subscribe".
  3. Check if the handler parameter is null.
  4. If the handler is null, throw an IllegalArgumentException with the message "MessageHandler is required in subscribe".
  5. If both the subject and handler are valid, call the subscribeImplCore method with the subject, null, and the handler as parameters.
  6. Return the result of the subscribeImplCore method, which is a Subscription object.

Note: The subscribeImplCore method is not included in the provided code snippet, so it's unclear what specific actions it performs.

The subscribe method in the io.nats.client.impl.NatsDispatcher class is used to subscribe to a specific subject for receiving messages from NATS server.

The method takes two parameters: subject (the subject to subscribe to) and handler (the message handler to process incoming messages).

If the subject parameter is null or empty, an IllegalArgumentException is thrown. If the handler parameter is null, an IllegalArgumentException is also thrown.

The method then calls the subscribeImplCore method, passing the subject, null (indicating no specific queue group), and the handler. This method is responsible for the actual subscription and returns a Subscription object that represents the subscription.

In summary, the subscribe method is used to subscribe to a subject and provide a message handler to process incoming messages.

sequence diagram

public Dispatcher subscribe(String subject, String queueName)

public Dispatcher subscribe(String subject, String queueName) {
    if (subject == null || subject.length() == 0) {
        throw new IllegalArgumentException("Subject is required in subscribe");
    }
    if (queueName == null || queueName.length() == 0) {
        throw new IllegalArgumentException("QueueName is required in subscribe");
    }
    this.subscribeImplCore(subject, queueName, null);
    return this;
}

The subscribe method in the NatsDispatcher class allows you to subscribe to a specific subject using a queue group name. Below is a step-by-step description of what this method does:

  1. Checks if the subject parameter is null or empty.

    • If it is null or empty, an IllegalArgumentException is thrown with the message "Subject is required in subscribe".
  2. Checks if the queueName parameter is null or empty.

    • If it is null or empty, an IllegalArgumentException is thrown with the message "QueueName is required in subscribe".
  3. If both subject and queueName are valid, the method calls the subscribeImplCore method with the provided subject, queue group name, and a null subscription options argument.

    • The subscribeImplCore method is responsible for the actual implementation of the subscribe logic.
  4. Finally, the subscribe method returns the current instance of the NatsDispatcher object, allowing for method chaining.

Overall, the subscribe method ensures that valid parameters are provided, throws exceptions if they are missing, and then passes the parameters to the subscribeImplCore method to handle the actual subscription logic.

The subscribe method in the NatsDispatcher class is used to subscribe to a specific subject and receive messages from it.

It takes two parameters: subject and queueName.

If the subject or queueName is null or an empty string, an IllegalArgumentException is thrown with a corresponding error message.

Otherwise, it calls the subscribeImplCore method passing the subject, queueName, and null for the option parameter.

Finally, it returns the instance of the NatsDispatcher class itself, allowing for method chaining.

sequence diagram

public Subscription subscribe(String subject, String queueName, MessageHandler handler)

public Subscription subscribe(String subject, String queueName, MessageHandler handler) {
    if (subject == null || subject.length() == 0) {
        throw new IllegalArgumentException("Subject is required in subscribe");
    }
    if (queueName == null || queueName.length() == 0) {
        throw new IllegalArgumentException("QueueName is required in subscribe");
    }
    if (handler == null) {
        throw new IllegalArgumentException("MessageHandler is required in subscribe");
    }
    return this.subscribeImplCore(subject, queueName, handler);
}

The subscribe method in the NatsDispatcher class is responsible for subscribing to a specific subject with a given queue name and message handler.

Here is a step-by-step description of what this method does:

  1. Check if the subject parameter is null or empty:

    • If true, throw an IllegalArgumentException with the message "Subject is required in subscribe".
  2. Check if the queueName parameter is null or empty:

    • If true, throw an IllegalArgumentException with the message "QueueName is required in subscribe".
  3. Check if the handler parameter is null:

    • If true, throw an IllegalArgumentException with the message "MessageHandler is required in subscribe".
  4. Call the subscribeImplCore method with the subject, queueName, and handler parameters and return the result:

    • This is a private method that handles the actual subscription logic.
    • It is responsible for subscribing to the specified subject with the given queue name and message handler.

In summary, the subscribe method performs some input validation and then delegates the actual subscription logic to the private subscribeImplCore method.

The subscribe method in the NatsDispatcher class is used to subscribe to a specific subject and queue in the NATS messaging system.

The method takes three parameters:

  • subject: The subject to which the subscriber wants to subscribe.
  • queueName: The name of the queue to which the subscriber wants to join. This parameter is used for load balancing messages between multiple subscribers.
  • handler: The MessageHandler instance that will be invoked to process incoming messages.

First, the method checks if the subject, queueName, and handler parameters are not null and have valid values. If any of these parameters are missing or invalid, an IllegalArgumentException is thrown.

If all the parameters are valid, the method calls the subscribeImplCore method to create and register a subscription for the given subject and queue. The subscribeImplCore method is a private method defined in the NatsDispatcher class, and it handles the actual subscribing logic.

The method returns a Subscription object, which represents the subscription created for the given subject and queue. This object can be used to control the subscription, such as unsubscribing or modifying options.

sequence diagram

// Assumes the subj/queuename checks are done, does check for closed status

NatsSubscription subscribeImplCore(String subject, String queueName, MessageHandler handler)

// Assumes the subj/queuename checks are done, does check for closed status
NatsSubscription subscribeImplCore(String subject, String queueName, MessageHandler handler) {
    checkBeforeSubImpl();
    // If the handler is null, then we use the default handler, which will not allow
    // duplicate subscriptions to exist.
    if (handler == null) {
        NatsSubscription sub = this.subscriptionsUsingDefaultHandler.get(subject);
        if (sub == null) {
            sub = connection.createSubscription(subject, queueName, this, null);
            NatsSubscription wonTheRace = this.subscriptionsUsingDefaultHandler.putIfAbsent(subject, sub);
            if (wonTheRace != null) {
                // Could happen on very bad timing
                this.connection.unsubscribe(sub, -1);
            }
        }
        return sub;
    }
    return _subscribeImplHandlerProvided(subject, queueName, handler, null);
}

The subscribeImplCore method is defined in the NatsDispatcher class and is responsible for subscribing to a NATS subject with the given queue name and message handler.

Here is a step-by-step description of what the method does:

  1. It starts by calling the checkBeforeSubImpl method to ensure that the pre-subscription checks are done.

  2. It checks if the provided message handler (handler) is null. If it is null, it means that the default handler should be used.

  3. If the handler is null, it retrieves the previously created subscription using the subject from the subscriptionsUsingDefaultHandler map. If there is no existing subscription for the subject, it creates a new subscription using the NATS connection with the given subject and queue name, and registers itself as the message handler.

  4. It then calls the putIfAbsent method on subscriptionsUsingDefaultHandler map, which atomically puts the new subscription in the map if the subject doesn't already exist. If another thread wins the race and adds a subscription before this one, it unsubscribes the newly created subscription.

  5. Finally, if the message handler is not null, it calls a private method _subscribeImplHandlerProvided to handle the subscription with the provided handler, subject, queue name, and null as options.

The method returns the subscription object sub whether it was obtained from the subscriptionsUsingDefaultHandler map or created in the _subscribeImplHandlerProvided method.

The subscribeImplCore method in the NatsDispatcher class is responsible for subscribing to a subject and queue name, with an optional message handler.

It first checks for any necessary conditions before proceeding with the subscription.

If the message handler is null, it uses the default handler, which ensures that duplicate subscriptions do not exist. It checks if a subscription for the given subject already exists using the default handler. If it doesn't exist, it creates a new subscription using the connection.createSubscription method and adds it to the subscriptionsUsingDefaultHandler map with the subject as the key. If another thread won the race and created the subscription, the method unsubscribes from the subscription it created.

If the message handler is not null, it calls the _subscribeImplHandlerProvided method with the given subject, queue name, and message handler.

The method returns the subscription object.

sequence diagram

private NatsSubscription _subscribeImplHandlerProvided(String subject, String queueName, MessageHandler handler, NatsSubscriptionFactory nsf)

private NatsSubscription _subscribeImplHandlerProvided(String subject, String queueName, MessageHandler handler, NatsSubscriptionFactory nsf) {
    NatsSubscription sub = connection.createSubscription(subject, queueName, this, nsf);
    this.subscriptionsWithHandlers.put(sub.getSID(), sub);
    this.subscriptionHandlers.put(sub.getSID(), handler);
    return sub;
}

The method _subscribeImplHandlerProvided is defined in the NatsDispatcher class within the io.nats.client.impl package. Here is a step-by-step description of what this method is doing:

  1. The method is receiving four parameters: subject (the subject to subscribe to), queueName (the optional name of the queue group), handler (the message handler to be assigned to the subscription), and nsf (an instance of NatsSubscriptionFactory).

  2. Inside the method, a new NatsSubscription object is created by calling the createSubscription method on the connection object. This method is responsible for actually subscribing to the specified subject and queue group (if provided). The this keyword refers to the current instance of the NatsDispatcher class, which acts as the listener for the subscription.

  3. The newly created NatsSubscription object is added to the subscriptionsWithHandlers map, using the subscription ID (sub.getSID()) as the key. This map is used to keep track of all subscriptions associated with their respective handlers.

  4. The handler parameter is also added to the subscriptionHandlers map, using the subscription ID as the key. This map is used to associate each subscription ID with its specific message handler.

  5. Finally, the method returns the newly created NatsSubscription object.

In summary, the _subscribeImplHandlerProvided method creates a new subscription to the specified subject and queue group (if provided), associates it with the provided message handler, and keeps track of the subscription and its handler using two separate maps.

The _subscribeImplHandlerProvided method in the NatsDispatcher class is responsible for subscribing to a specific subject using a provided message handler. It takes the subject, queue name, message handler, and an instance of NatsSubscriptionFactory as parameters.

Internally, the method creates a new NatsSubscription object by calling the createSubscription method of the connection object. It then adds the subscription to the subscriptionsWithHandlers map using the subscription ID as the key, and stores the message handler in the subscriptionHandlers map using the same subscription ID.

Finally, the method returns the newly created NatsSubscription object.

sequence diagram

String reSubscribe(NatsSubscription sub, String subject, String queueName, MessageHandler handler)

String reSubscribe(NatsSubscription sub, String subject, String queueName, MessageHandler handler) {
    String sid = connection.reSubscribe(sub, subject, queueName);
    this.subscriptionsWithHandlers.put(sid, sub);
    this.subscriptionHandlers.put(sid, handler);
    return sid;
}

Method: reSubscribe

Description:

This method is responsible for re-subscribing to a NATS subject using a specified queue and message handler. It takes in a NATS subscription object, the subject to subscribe to, the name of the queue, and a message handler as parameters. Upon successful re-subscription, it returns the subscription ID.

Parameters:

  • sub: A NATS subscription object representing the current subscription.
  • subject: A string specifying the subject to subscribe to.
  • queueName: A string specifying the name of the queue to use for load balancing.
  • handler: A MessageHandler object responsible for handling incoming messages.

Steps:

  1. Obtain the connection object from the current instance.
  2. Call the reSubscribe method of the connection object, passing in the sub, subject, and queueName parameters.
  3. Assign the returned subscription ID (sid) to the local variable sid.
  4. Add the sub object to the subscriptionsWithHandlers map using sid as the key.
  5. Add the handler object to the subscriptionHandlers map using sid as the key.
  6. Return the subscription ID (sid).

The reSubscribe method is defined in the NatsDispatcher class. It takes in a NatsSubscription object, a subject as a String, a queue name as a String, and a MessageHandler object.

The method first calls the reSubscribe method of the connection object (assuming it is of type Connection), passing in the subscription, subject, and queue name. It captures the returned subscription ID as a String variable named sid.

Next, it adds the subscription sid as a key and the sub object as the value to the subscriptionsWithHandlers map.

Then, it adds the sid as a key and the handler object as the value to the subscriptionHandlers map.

Finally, the method returns the sid.

In summary, the reSubscribe method re-subscribes a provided subscription (NatsSubscription) to a specified subject and queue name, and associates a message handler (MessageHandler) with the subscription. It keeps track of the association between the subscription and the handler using two separate maps. The method returns the subscription ID.

sequence diagram

private void checkBeforeSubImpl()

private void checkBeforeSubImpl() {
    if (!running.get()) {
        throw new IllegalStateException("Dispatcher is closed");
    }
    if (isDraining()) {
        throw new IllegalStateException("Dispatcher is draining");
    }
}

The method checkBeforeSubImpl() in the NatsDispatcher class is responsible for checking certain conditions before subscribing to a subject.

Here is a step-by-step description of what this method does:

  1. Checks if the running flag is set to false. If it is, it means that the dispatcher is closed. If the flag is false, the method throws an IllegalStateException with the message "Dispatcher is closed".

  2. Checks if the dispatcher is currently in the process of draining messages. The isDraining() method is used to determine this. If the dispatcher is indeed draining, the method throws an IllegalStateException with the message "Dispatcher is draining".

These checks ensure that the dispatcher is in a valid state before allowing a subscription to a subject. If either of these checks fail, an exception is thrown, indicating that the subscription cannot proceed.

The checkBeforeSubImpl method is a private method defined in the NatsDispatcher class from the io.nats.client.impl package. This method is responsible for checking if certain conditions are met before subscribing to a message dispatcher.

First, it checks if the running flag is set to false. If the dispatcher is not currently running, it throws an IllegalStateException with the message "Dispatcher is closed".

Next, it checks if the dispatcher is currently in the process of draining. If the dispatcher is draining, it throws an IllegalStateException with the message "Dispatcher is draining".

In summary, this method ensures that the dispatcher is in a valid state for subscribing before allowing a subscription to be created.

sequence diagram

public Dispatcher unsubscribe(String subject, int after)

public Dispatcher unsubscribe(String subject, int after) {
    if (!this.running.get()) {
        throw new IllegalStateException("Dispatcher is closed");
    }
    if (isDraining()) {
        // No op while draining
        return this;
    }
    if (subject == null || subject.length() == 0) {
        throw new IllegalArgumentException("Subject is required in unsubscribe");
    }
    NatsSubscription sub = this.subscriptionsUsingDefaultHandler.get(subject);
    if (sub != null) {
        // Connection will tell us when to remove from the map
        this.connection.unsubscribe(sub, after);
    }
    return this;
}

unsubscribe method

The unsubscribe method in the NatsDispatcher class, located in the package io.nats.client.impl, is used to unsubscribe from a subject.

Step by step description

  1. Check if the dispatcher is closed. If it is closed, throw an IllegalStateException with the message "Dispatcher is closed". This ensures that the method cannot be called when the dispatcher is closed.

  2. Check if the dispatcher is currently draining messages. If it is, return the current instance of the dispatcher without performing any further actions. This is to prevent any operations while the dispatcher is draining.

  3. Check if the subject parameter is null or empty. If it is, throw an IllegalArgumentException with the message "Subject is required in unsubscribe". This ensures that a valid subject is provided for unsubscribing.

  4. Get the subscription associated with the subject from the subscriptionsUsingDefaultHandler map. If a matching subscription is found, proceed to the next step; otherwise, skip to step 7.

  5. Call the unsubscribe method on the connection object, passing the found subscription and the after parameter. This effectively unsubscribes from the subject. The connection object is responsible for informing the dispatcher when to remove the subscription from the subscriptionsUsingDefaultHandler map.

  6. Return the current instance of the dispatcher. This allows for method chaining.

  7. End the method. If no subscription was found for the provided subject, the method returns the current instance of the dispatcher without performing any further actions.

The unsubscribe method in the NatsDispatcher class allows a client to unsubscribe from a specific subject.

It first checks if the Dispatcher is running, and if not, throws an IllegalStateException. Next, if the Dispatcher is in the process of draining, it simply returns itself without performing any further operations. Then, it ensures that a valid subject is provided; otherwise, it throws an IllegalArgumentException. If a subscription exists for the provided subject, it delegates the actual unsubscribe operation to the connection and passes the subscription and a "time to wait" parameter. Finally, it returns itself, allowing for method chaining.

sequence diagram

public Dispatcher unsubscribe(Subscription subscription, int after)

public Dispatcher unsubscribe(Subscription subscription, int after) {
    if (!this.running.get()) {
        throw new IllegalStateException("Dispatcher is closed");
    }
    if (isDraining()) {
        // No op while draining
        return this;
    }
    if (subscription.getDispatcher() != this) {
        throw new IllegalStateException("Subscription is not managed by this Dispatcher");
    }
    // We can probably optimize this path by adding getSID() to the Subscription interface.
    if (!(subscription instanceof NatsSubscription)) {
        throw new IllegalArgumentException("This Subscription implementation is not known by Dispatcher");
    }
    NatsSubscription ns = ((NatsSubscription) subscription);
    // Grab the NatsSubscription to verify we weren't given a different manager's subscription.
    NatsSubscription sub = this.subscriptionsWithHandlers.get(ns.getSID());
    if (sub != null) {
        // Connection will tell us when to remove from the map
        this.connection.unsubscribe(sub, after);
    }
    return this;
}

Method unsubscribe()

This method is defined in the class io.nats.client.impl.NatsDispatcher and takes two parameters: subscription and after.

Step 1: Check if the dispatcher is closed

If the running flag is set to false, an IllegalStateException is thrown with the message "Dispatcher is closed".

Step 2: Check if draining is in progress

If the isDraining() method returns true, the method simply returns the current Dispatcher object without performing any operations.

Step 3: Check if the subscription is managed by this dispatcher

If the subscription object's dispatcher is not equal to the current dispatcher, an IllegalStateException is thrown with the message "Subscription is not managed by this Dispatcher".

Step 4: Check the type of the subscription

If the subscription object is not an instance of NatsSubscription, an IllegalArgumentException is thrown with the message "This Subscription implementation is not known by Dispatcher".

Step 5: Retrieve the NatsSubscription object

Cast the subscription object to NatsSubscription and assign it to the variable ns.

Step 6: Verify the subscription with the dispatcher

Retrieve the NatsSubscription object associated with the same subscription ID (ns.getSID()) from the subscriptionsWithHandlers map. If it exists, assign it to the variable sub.

Step 7: Unsubscribe from the connection

If sub is not null, call the unsubscribe() method of the connection object (of type NatsConnection) and pass in the sub object and after parameter.

Step 8: Return the current Dispatcher object

Finally, return the current Dispatcher object.

Note: This method is used to unsubscribe a subscription from the NATS connection. It performs various checks and verifies that the subscription is managed by the dispatcher before unsubscribing it from the connection.

The unsubscribe method in the NatsDispatcher class allows the software engineer to unsubscribe a particular subscription from the NATS messaging system.

The method first checks if the dispatcher is closed and throws an exception if it is. Then, it checks if the dispatcher is currently draining, and if so, it returns without performing any operation.

Next, it verifies that the given subscription is managed by this dispatcher. If not, it throws an exception.

Then, it checks if the given subscription is an instance of the NatsSubscription class. If not, it throws an exception.

After that, it obtains the NatsSubscription instance from the given subscription. It then retrieves the subscription with the same subscription ID (SID) from the subscriptionsWithHandlers map.

If a matching subscription is found, it delegates the unsubscribe operation to the underlying NATS connection by calling the unsubscribe method on the connection. The after parameter specifies the number of expected messages to be processed before actually unsubscribing.

Finally, the method returns the NatsDispatcher instance to allow for method chaining.

sequence diagram

void sendUnsubForDrain()

void sendUnsubForDrain() {
    this.subscriptionsUsingDefaultHandler.forEach((id, sub) -> {
        this.connection.sendUnsub(sub, -1);
    });
    this.subscriptionsWithHandlers.forEach((sid, sub) -> {
        this.connection.sendUnsub(sub, -1);
    });
}

The sendUnsubForDrain method is defined in the NatsDispatcher class within the io.nats.client.impl package. This method performs the following steps:

  1. Iterate over each subscription that is using the default handler, using the forEach method on the subscriptionsUsingDefaultHandler map.
  2. For each subscription, invoke the sendUnsub method on the connection object passing in the subscription and -1 as arguments. This method is responsible for sending an UNSUB message to the NATS server to unsubscribe from the specified subscription.
  3. Iterate over each subscription with individual handlers, using the forEach method on the subscriptionsWithHandlers map.
  4. For each subscription, again invoke the sendUnsub method on the connection object passing in the subscription and -1 as arguments. This ensures that all subscriptions are unsubscribed from the NATS server.

In summary, the sendUnsubForDrain method unsubscribes all subscriptions associated with the NatsDispatcher, both with default handlers and individual handlers, by invoking the sendUnsub method on the underlying connection object.

The sendUnsubForDrain method in the NatsDispatcher class is used to send unsubscribe messages for draining existing subscriptions. It iterates over the subscriptionsUsingDefaultHandler map and subscriptionsWithHandlers map to get the subscriptions associated with the default handler and custom handlers respectively. For each subscription, it calls the sendUnsub method of the underlying connection object with a timeout value of -1, indicating an immediate unsubscribe. This method is used in situations where the client needs to unsubscribe from all active subscriptions as part of a draining process.

sequence diagram

NatsIterableConsumer

NatsIterableConsumer

The NatsIterableConsumer class is a public class that extends the NatsMessageConsumer class and implements the IterableConsumer interface. It provides functionality for consuming messages from a NATS message broker and allows for iteration over the received messages.

@Override

public Message nextMessage(Duration timeout) throws InterruptedException, JetStreamStatusCheckedException

@Override
public Message nextMessage(Duration timeout) throws InterruptedException, JetStreamStatusCheckedException {
    try {
        return sub.nextMessage(timeout);
    } catch (JetStreamStatusException e) {
        throw new JetStreamStatusCheckedException(e);
    } catch (IllegalStateException i) {
        // this happens if the consumer is stopped, since it is
        // drained/unsubscribed, so don't pass it on if it's expected
        return null;
    }
}

The nextMessage method described in the class io.nats.client.impl.NatsIterableConsumer is responsible for retrieving the next message from a subscription, with a specified timeout for waiting. Here is a step-by-step description of what the method does:

  1. It overrides the nextMessage method from the parent class or interface.
  2. It has a parameter timeout of type Duration which represents the maximum amount of time to wait for a message.
  3. It may throw two types of exceptions: InterruptedException and JetStreamStatusCheckedException.
  4. The method tries to retrieve the next message from a subscription by invoking the nextMessage method on a sub object. The sub object is presumably a subscription object associated with the consumer.
  5. If the nextMessage call throws a JetStreamStatusException, it catches the exception and throws a JetStreamStatusCheckedException instead. This is done by creating a new instance of JetStreamStatusCheckedException with the caught exception as the cause.
  6. If the nextMessage call throws an IllegalStateException, it means that the consumer is stopped (possibly due to being drained or unsubscribed). In this case, the method returns null without re-throwing the exception.
  7. If the nextMessage call succeeds and returns a message, it is returned by the method.

Overall, the nextMessage method provides a way to fetch the next message from a subscription with a specified timeout, handling any potential exceptions that may occur during the process.

The nextMessage method in the NatsIterableConsumer class is used to retrieve the next message from the NATS streaming server within a specified timeout duration.

The method overrides the nextMessage method defined in the MessageIterator interface. It takes a Duration parameter representing the maximum amount of time to wait for a message before timing out.

Inside the method, it calls the nextMessage method of the underlying subscription object (sub) and returns the message it receives. If the underlying NATS stream throws a JetStreamStatusException, it converts it to a JetStreamStatusCheckedException and throws it.

If the underlying subscription object is in an illegal state, which typically happens when the consumer is stopped or unsubscribed, the method returns null without passing on the exception.

sequence diagram

NatsConsumerContext

NatsConsumerContext

The NatsConsumerContext class is a public class that implements the ConsumerContext interface. It provides simplification functionalities, but please note that these features are experimental and subject to change.

@Override

public Message next(Duration maxWait) throws IOException, InterruptedException, JetStreamStatusCheckedException, JetStreamApiException

@Override
public Message next(Duration maxWait) throws IOException, InterruptedException, JetStreamStatusCheckedException, JetStreamApiException {
    if (maxWait == null) {
        return new NextSub(DEFAULT_EXPIRES_IN_MILLIS, streamContext.js, bindPso).next();
    }
    return next(maxWait.toMillis());
}

The next method in class io.nats.client.impl.NatsConsumerContext is used to retrieve the next message from the NATS server.

Here is a step-by-step description of what the method does based on its body:

  1. The method overrides the next method from its superclass and takes a maxWait parameter of type Duration, which represents the maximum amount of time to wait for a message.

  2. The method first checks if the maxWait parameter is null.

  3. If the maxWait parameter is null, it creates a new instance of the NextSub class, passing the DEFAULT_EXPIRES_IN_MILLIS, streamContext.js, and bindPso as arguments. It then calls the next method on this instance to retrieve the next message. The NextSub class is likely responsible for interacting with the NATS server to fetch messages.

  4. If the maxWait parameter is not null, it converts the duration to milliseconds using the toMillis method and calls the next method with the converted value. This suggests that the next method is overloaded with a version that takes a long parameter representing the number of milliseconds to wait for a message.

  5. The method then returns the retrieved message.

Please note that the full functionality and behavior of the method may depend on the implementation details of the NextSub class and other related classes.

The next method in the NatsConsumerContext class is responsible for retrieving the next message from a NATS consumer.

The method takes a parameter maxWait of type Duration, which defines the maximum time to wait for a message.

In the method body, it checks if maxWait is null. If it is null, it calls the next method of the NextSub class, passing the default expiration value, the JetStream context, and the bindPso (presumed to be some sort of bind point) as parameters. This is done by creating a new instance of NextSub and invoking the next method on it.

If maxWait is not null, the method converts the duration to milliseconds using the toMillis method and then calls the next method recursively with the converted value.

The method returns a Message object representing the next message obtained from the NATS consumer. It can throw various exceptions including IOException, InterruptedException, JetStreamStatusCheckedException, and JetStreamApiException.

sequence diagram

@Override

public Message next(long maxWaitMillis) throws IOException, InterruptedException, JetStreamStatusCheckedException, JetStreamApiException

@Override
public Message next(long maxWaitMillis) throws IOException, InterruptedException, JetStreamStatusCheckedException, JetStreamApiException {
    if (maxWaitMillis < MIN_EXPIRES_MILLS) {
        throw new IllegalArgumentException("Max wait must be at least " + MIN_EXPIRES_MILLS + " milliseconds.");
    }
    return new NextSub(maxWaitMillis, streamContext.js, bindPso).next();
}

This method next(long maxWaitMillis) is defined in the io.nats.client.impl.NatsConsumerContext class. It is an overridden method from its parent class.

Step-by-Step Description:

  1. Check if the value of maxWaitMillis is less than the constant MIN_EXPIRES_MILLS.

    • If it is less, throw an IllegalArgumentException with the message "Max wait must be at least MIN_EXPIRES_MILLS milliseconds.".
  2. Create a new instance of the NextSub class, passing the value of maxWaitMillis, the streamContext.js object, and the bindPso object as arguments.

  3. Call the next() method on the NextSub instance to retrieve the next message.

  4. Return the retrieved message.

The purpose of this method is to retrieve the next message from the NextSub instance with a specified maximum wait time. It performs some basic argument validation and then delegates the task to the NextSub class.

The method next in the NatsConsumerContext class is responsible for retrieving the next message from a NATS consumer.

Here is a breakdown of what the method does:

  • It takes a parameter maxWaitMillis which specifies the maximum time to wait for a message.
  • If the maxWaitMillis is less than a constant value MIN_EXPIRES_MILLS, an IllegalArgumentException is thrown with a corresponding error message.
  • Otherwise, a new instance of the NextSub class is created, passing the maxWaitMillis, the streamContext.js object, and the bindPso object as parameters.
  • The next method of the NextSub instance is then called to retrieve the next message from the NATS consumer.
  • The retrieved message is returned as the result of the method.

sequence diagram

@Override

public MessageConsumer consume(MessageHandler handler, ConsumeOptions consumeOptions) throws IOException, JetStreamApiException

@Override
public MessageConsumer consume(MessageHandler handler, ConsumeOptions consumeOptions) throws IOException, JetStreamApiException {
    Validator.required(handler, "Message Handler");
    Validator.required(consumeOptions, "Consume Options");
    return new NatsMessageConsumer(new SimplifiedSubscriptionMaker(streamContext.js, bindPso), handler, consumeOptions, lastConsumerInfo);
}

The consume method in the NatsConsumerContext class is responsible for creating a new MessageConsumer and returning it. Here is a step-by-step description of what the method does:

  1. It overrides the consume method from the MessageConsumer interface.
  2. The method takes two parameters: handler of type MessageHandler and consumeOptions of type ConsumeOptions.
  3. It performs validation checks to ensure that the handler and consumeOptions parameters are not null. If either of them is null, an exception is thrown with a descriptive error message.
  4. It creates a new instance of the NatsMessageConsumer class.
  5. The NatsMessageConsumer constructor takes four parameters: streamContext.js, bindPso, handler, consumeOptions, and lastConsumerInfo.
  6. The streamContext.js is a field of the NatsConsumerContext class and is used to access the JetStream context.
  7. The bindPso is another field of the NatsConsumerContext class.
  8. The handler is the MessageHandler passed as a parameter to the consume method.
  9. The consumeOptions is the ConsumeOptions passed as a parameter to the consume method.
  10. The lastConsumerInfo is another field of the NatsConsumerContext class.
  11. The instantiated NatsMessageConsumer object is then returned as the result of the consume method.

In summary, the consume method creates a new NatsMessageConsumer object using the provided MessageHandler, ConsumeOptions, and other required information, and returns it as a MessageConsumer.

The consume method in the NatsConsumerContext class is responsible for creating a MessageConsumer object that will consume messages from a NATS server.

The method requires a MessageHandler object and ConsumeOptions object as input parameters. These objects define how the messages will be handled (through the handler parameter) and the specific options for consuming messages (through the consumeOptions parameter).

Internally, the method creates a new NatsMessageConsumer object, which is a specialized implementation of the MessageConsumer interface. This object is instantiated with a SimplifiedSubscriptionMaker object, the handler parameter, the consumeOptions parameter, and the lastConsumerInfo object.

Overall, the consume method provides a convenient way to create a NATS message consumer with the specified message handler and consume options.

sequence diagram

NatsConnection

NatsConnection

The NatsConnection class is an implementation of the Connection interface. It represents a connection to a NATS messaging system, allowing software engineers to interact with it programmatically. This class provides the necessary functionalities to establish, manage, and handle communications with the NATS server.

// Connect is only called after creation

void connect(boolean reconnectOnConnect) throws InterruptedException, IOException

// Connect is only called after creation
void connect(boolean reconnectOnConnect) throws InterruptedException, IOException {
    if (options.getServers().size() == 0) {
        throw new IllegalArgumentException("No servers provided in options");
    }
    boolean trace = options.isTraceConnection();
    long start = System.nanoTime();
    this.lastError.set("");
    timeTrace(trace, "starting connect loop");
    Set<NatsUri> failList = new HashSet<>();
    boolean keepGoing = true;
    NatsUri first = null;
    NatsUri cur;
    while (keepGoing && (cur = serverPool.peekNextServer()) != null) {
        if (first == null) {
            first = cur;
        } else if (cur.equals(first)) {
            // connect only goes through loop once
            break;
        }
        // b/c we only peeked.
        serverPool.nextServer();
        // let server pool resolve hostnames, then loop through resolved
        List<NatsUri> resolvedList = resolveHost(cur);
        while (resolvedList.size() > 0) {
            if (isClosed()) {
                keepGoing = false;
                break;
            }
            // new on each attempt
            connectError.set("");
            timeTrace(trace, "setting status to connecting");
            updateStatus(Status.CONNECTING);
            timeTrace(trace, "trying to connect to %s", cur);
            NatsUri resolved = resolvedList.remove(0);
            tryToConnect(cur, resolved, System.nanoTime());
            if (isConnected()) {
                serverPool.connectSucceeded(cur);
                keepGoing = false;
                break;
            }
            timeTrace(trace, "setting status to disconnected");
            updateStatus(Status.DISCONNECTED);
            failList.add(cur);
            serverPool.connectFailed(cur);
            String err = connectError.get();
            if (this.isAuthenticationError(err)) {
                this.serverAuthErrors.put(resolved, err);
            }
        }
    }
    if (!isConnected() && !isClosed()) {
        if (reconnectOnConnect) {
            timeTrace(trace, "trying to reconnect on connect");
            reconnect();
        } else {
            timeTrace(trace, "connection failed, closing to cleanup");
            c
⚠️ **GitHub.com Fallback** ⚠️