I think the age-old "Hello world" example has outlived its usefulness. While it once served as a foundational teaching tool, its simplicity no longer suffices in today's world. In the current landscape, where real-world scenarios are markedly more intricate, I present a "Hello world" example that truly mirrors the complexity of modern development.
Let's jump into the implementation of a signup service with the following requirements:
- The signup service takes a JSON input containing at least two fields: email and address, both expected as strings. The service's first step is to validate and standardize the address using the Google Geocode API. The results obtained from Google are then presented to the frontend for user selection or rejection.
- In addition to address validation, the service stores the client's information in a MongoDB database. The MongoDB identifier returned becomes the client identifier, which must be sent back to the frontend. If the client is successfully saved in the database and the user doesn't exist in an LDAP system, two additional actions occur:
- The user is sent to the LDAP service.
- If the previous operation succeeds, an activation email is sent to the user.
- The signup service also provides information about the total number of existing clients in the MongoDB database. This information can be utilized by the frontend to display a welcoming message to the user, such as "You're the user number 3000!" If an error occurs the service returns -1, and the frontend will not display the message.
- Crucially, the signup service is designed to perform all these operations in parallel. This includes the request to Google for address validation and the MongoDB operations, which encompass both data persistence and counting.
The response from the signup service follows this structure:
{
"number_users": integer,
"id": string,
"addresses": array
}
The SignupService
orchestrates all the operations with elegance and efficiency. This service is
constructed with a set of lambdas, where a lambda is essentially a function that takes
an input and produces an output. Unlike traditional functions, lambdas don't throw exceptions;
instead, they gracefully return exceptions as regular values.
import jio.*;
import jio.time.Clock;
import jsonvalues.*;
import java.time.Instant;
import static java.util.Objects.requireNonNull;
public class SignupService implements Lambda<JsObj, JsObj> {
Lambda<JsObj, Void> persistLDAP;
Lambda<String, JsArray> normalizeAddress;
IO<Integer> countUsers;
Lambda<JsObj, String> persistMongo;
Lambda<JsObj, Void> sendEmail;
Lambda<String, Boolean> existsInLDAP;
//constructor
@Override
public IO<JsObj> apply(JsObj user) {
String email = user.getStr("email");
String address = user.getStr("address");
String context = "signup";
Lambda<String, String> LDAPFlow =
id -> IfElseExp.<String>predicate(existsInLDAP.apply(email))
.consequence(() -> IO.succeed(id))
.alternative(() -> PairExp.seq(persistLDAP.apply(user),
sendEmail.apply(user)
)
.debugEach(context)
.map(n -> id)
)
.debugEach(context);
return JsObjExp.par("number_users",
countUsers.recover(exc -> -1)
.map(JsInt::of),
"id",
persistMongo.then(LDAPFlow)
.apply(user)
.map(JsStr::of),
"addresses",
normalizeAddress.apply(address)
)
.debugEach(context);
}
}
Noteworthy points:
-
JsObjExp: The
JsObjExp
expression is highly expressive. It allows us to define the structure of the resulting JSON object in a clear and declarative manner. In our code, we use it to construct a JSON object with multiple key-value pairs, each representing a specific piece of information ("number_users"
,"id"
,"addresses"
,"timestamp"
, etc.). This approach simplifies the creation of complex JSON structures and enhances code readability. -
Error handling is handled gracefully with the
recover
functions, providing alternative values (e.g., -1 forcountUsers
) in case of errors. -
IfElseExp: The
IfElseExp
expression is a clear and concise way to handle conditional logic. It enables us to specify the consequence and alternative branches based on a predicate (existsInLDAP.apply(email)
in this case). This expressiveness makes it evident that if the user exists in LDAP, we succeed with an ID, otherwise, we perform a sequence of operations usingPairExp
. It enhances the readability of the code, making it easy to understand the branching logic. -
PairExp: The
PairExp
expression streamlines the execution of two effects, either sequentially or in parallel, and then combines their results into a pair. In this scenario, we utilizePairExp.seq
to execute thepersistLDAP
andsendEmail
operations sequentially. However, it's essential to emphasize that in this particular example, our primary concern is the successful completion of both operations. Therefore, in the absence of any failures, the result will be a pair containing twonull
values: (null, null), as both operations returnVoid
. -
debugEach: Debugging plays a pivotal role in software development, and real-world applications often handle a multitude of messages from various users and requests. When issues arise, identifying which log events are pertinent to the problem can be challenging, particularly in high-traffic scenarios. JIO streamlines the debugging process and enhances contextual logging through its
debug
anddebugEach
methods. -
JFR (Java Flight Recorder): JIO leverages JFR for logging and debugging purposes. This choice offers several advantages. First, it's Java-native, which means it seamlessly integrates with the Java ecosystem, ensuring compatibility and performance. Second, it avoids the complexities and potential conflicts associated with using external logging libraries, of which there are many in the Java landscape. By relying on JFR, we maintain a lightweight and efficient approach to logging that is both reliable and highly effective.
-
Last but not least, the backbone of JIO is the
IO
class that we'll explore in detail in the next section.
JIO offers an elegant and efficient approach to testing. It eliminates the need for external libraries like Mockito, making your testing experience smoother and more expressive. Since Lambdas are just functions, you can implement them in your test class, directly. This approach enables you to tailor the behavior of each lambda to your specific test scenario, making your tests highly adaptable and expressive:
public class SignupTests {
@RegisterExtension
static Debugger debugger = Debugger.of(Duration.ofSeconds(2));
@Test
public void test() {
Lambda<JsObj, Void> persistLDAP = _ -> IO.NULL();
Lambda<String, JsArray> normalizeAddress =
_ -> IO.succeed(JsArray.of("address1",
"address2"));
IO<Integer> countUsers =
IO.lazy(() -> ThreadLocalRandom.current()
.nextInt(0,
10));
Lambda<JsObj, String> persistMongo = _ -> IO.succeed("id");
Lambda<JsObj, Void> sendEmail = _ -> IO.NULL();
Lambda<String, Boolean> existsInLDAP = _ -> IO.FALSE;
JsObj user = JsObj.of("email",
JsStr.of("[email protected]"),
"address",
JsStr.of("Elm's Street")
);
JsObj resp = new SignupService(persistLDAP,
normalizeAddress,
countUsers,
persistMongo,
sendEmail,
existsInLDAP
)
.apply(user) //returns an IO effect, nothing is computed
.compute() //computes the effect a return a Result (either Success of Failure)
.getOutput(); //get the sucessful output or throws RuntimeException
Assertions.assertTrue(resp.containsKey("number_users"));
}
}
When it comes to debugging your code during testing, having access to detailed information is invaluable. JIO's Debugger extension simplifies this process by creating an event stream for a specified duration and printing all the events sent to the Java Flight Recorder (JFR) system during that period.
Here's a breakdown of how it works:
-
Debugger Extension Registration: In your test class, you register the Debugger JUnit extension using the
@RegisterExtension
annotation. You specify the duration for which the debugger captures events. -
Using
debug
anddebugEach
: Within your code, you utilize thedebug
anddebugEach
methods provided by JIO. These methods allow you to send events to the JFR system after a value or expression is evaluated. -
Event Printing: During the execution of the test for the specified duration, the Debugger extension prints out all the events that were sent to the JFR system. These events include information about the expressions being evaluated, their results, execution durations, contextual data, and more.
-
Stream Ordering: Importantly, the event stream is ordered. Events are printed in the order in which they occurred, providing a clear chronological view of your code's execution.
-
Pinpointing Bugs and Issues: With the event stream and detailed logs in hand, you can easily pinpoint any bugs, unexpected behavior, or performance bottlenecks.
In summary, the Debugger extension in JIO transforms the testing and debugging process into a streamlined and informative experience with minimal effort from developers. It empowers developers to gain deep insights into their code's behavior without relying on external logging libraries or complex setups.
Find below all the events that are printed out during the execution of the previous JUnit test.
Started JFR stream for 2,000 sg in SignupTests
------ eval-exp --------
| Expression: count_number_users
| Result: SUCCESS
| Duration: 69,833 µs
| Output: 3
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.920563792+01:00
-------------------------
------ eval-exp --------
| Expression: JsObjExpPar[number_users]
| Result: SUCCESS
| Duration: 1,418 ms
| Output: 3
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.91973025+01:00
-------------------------
------ eval-exp --------
| Expression: JsObjExpPar[addresses]
| Result: SUCCESS
| Duration: 4,583 µs
| Output: ["address1","address2"]
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.921166292+01:00
-------------------------
------ eval-exp --------
| Expression: IfElseExp-predicate
| Result: SUCCESS
| Duration: 5,958 µs
| Output: false
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.924032792+01:00
-------------------------
------ eval-exp --------
| Expression: PairExpSeq[1]
| Result: SUCCESS
| Duration: 4,709 µs
| Output: null
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.924848208+01:00
-------------------------
------ eval-exp --------
| Expression: PairExpSeq[2]
| Result: SUCCESS
| Duration: 4,291 µs
| Output: null
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.924969417+01:00
-------------------------
------ eval-exp --------
| Expression: PairExpSeq
| Result: SUCCESS
| Duration: 284,875 µs
| Output: (null, null)
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.924846917+01:00
-------------------------
------ eval-exp --------
| Expression: IfElseExp-alternative
| Result: SUCCESS
| Duration: 2,744 ms
| Output: id
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.924842+01:00
-------------------------
------ eval-exp --------
| Expression: IfElseExp
| Result: SUCCESS
| Duration: 3,568 ms
| Output: id
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.924030958+01:00
-------------------------
------ eval-exp --------
| Expression: JsObjExpPar[id]
| Result: SUCCESS
| Duration: 4,058 ms
| Output: id
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.923546958+01:00
-------------------------
------ eval-exp --------
| Expression: JsObjExpPar[timestamp]
| Result: SUCCESS
| Duration: 219,208 µs
| Output: 2024-02-13T09:08:09.927Z
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.927616125+01:00
-------------------------
------ eval-exp --------
| Expression: JsObjExpPar
| Result: SUCCESS
| Duration: 8,925 ms
| Output: {"addresses":["address1","address2"],"number_users":3,"timestamp":"2024-02-13T09:08:09.927Z","id":"id"}
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:08:09.919238167+01:00
-------------------------
The displayed events are self-explanatory. If you're wondering whether this is the actual format of events, the answer is yes. When testing, it's preferable to opt for a format that's easy to read and comprehend, rather than cramming all the information into a single line.
In summary, these traces are like breadcrumbs that guide you through your code, making testing and debugging more efficient and effective. They enable you to pinpoint issues, optimize performance, and gain a deeper understanding of how your code behaves during testing.
In the previous example, you may have observed that all evaluations were performed by the main
thread. This is because the IO effects returned by the lambdas were essentially constants, and no
specific Executor
was defined. Even if an Executor
were specified, there are cases where the
CompletableFuture framework, heavily relied upon by JIO, may choose not to switch contexts between
threads if it deems it unnecessary.
However, you can introduce random delays and leverage virtual threads to create a more realistic
example. To achieve this, more complex stubs are used from the jio-test
library through the
StubBuilder
class. These stubs allow you to specify generators for their creation, ensuring
different values are returned every time. Here's how you can utilize them:
@Test
public void test() {
Gen<Duration> delayGen = IntGen.arbitrary(0,
200)
.map(Duration::ofMillis);
IO<Integer> countUsers =
StubBuilder.ofSucGen(IntGen.arbitrary(0,
100000))
.withDelays(delayGen)
.withExecutor(Executors.newVirtualThreadPerTaskExecutor())
.get();
Lambda<JsObj, String> persistMongo =
_ -> StubBuilder.ofSucGen(StrGen.alphabetic(20,
20))
.withDelays(delayGen)
.withExecutor(Executors.newVirtualThreadPerTaskExecutor())
.get();
Lambda<JsObj, Void> sendEmail =
_ -> StubBuilder.<Void>ofSucGen(Gen.cons(null))
.withDelays(delayGen)
.withExecutor(Executors.newVirtualThreadPerTaskExecutor())
.get();
Lambda<String, Boolean> existsInLDAP =
_ -> StubBuilder.ofSucGen(BoolGen.arbitrary())
.withDelays(delayGen)
.withExecutor(Executors.newVirtualThreadPerTaskExecutor())
.get();
Lambda<JsObj, Void> persistLDAP =
_ -> StubBuilder.<Void>ofSucGen(Gen.cons(null))
.withDelays(delayGen)
.withExecutor(Executors.newVirtualThreadPerTaskExecutor())
.get();
Lambda<String, JsArray> normalizeAddresses =
_ -> StubBuilder.ofSucGen(JsArrayGen.ofN(JsStrGen.alphabetic(),
3))
.withDelays(delayGen)
.withExecutor(Executors.newVirtualThreadPerTaskExecutor())
.get();
}
These StubBuilder
instances are essentially builders that create IO stubs. They allow you to
introduce variability and randomness into your tests, making them more realistic and ensuring your
code can handle different scenarios effectively. I recommend you take a look at
jio-test and property-based-testing.
Using these stubs, the following events were printed out:
Started JFR stream for 2,000 sg in SignupTests
------ eval-exp --------
| Expression: JsObjExpPar[timestamp]
| Result: SUCCESS
| Duration: 293,209 µs
| Output: 2024-02-13T09:18:21.071Z
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:18:21.071499125+01:00
-------------------------
------ eval-exp --------
| Expression: count_number_users
| Result: SUCCESS
| Duration: 65,372 ms
| Output: 32634
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:18:21.066073417+01:00
-------------------------
------ eval-exp --------
| Expression: JsObjExpPar[number_users]
| Result: SUCCESS
| Duration: 66,663 ms
| Output: 32634
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:18:21.065248375+01:00
-------------------------
------ eval-exp --------
| Expression: JsObjExpPar[addresses]
| Result: SUCCESS
| Duration: 116,944 ms
| Output: ["m","n","g"]
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:18:21.071124667+01:00
-------------------------
------ eval-exp --------
| Expression: IfElseExp-predicate
| Result: SUCCESS
| Duration: 37,233 ms
| Output: true
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:18:21.218791959+01:00
-------------------------
------ eval-exp --------
| Expression: IfElseExp-consequence
| Result: SUCCESS
| Duration: 9,667 µs
| Output: QREuMrvmtunCvhbZxykT
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:18:21.256122125+01:00
-------------------------
------ eval-exp --------
| Expression: IfElseExp
| Result: SUCCESS
| Duration: 37,386 ms
| Output: QREuMrvmtunCvhbZxykT
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:18:21.218788042+01:00
-------------------------
------ eval-exp --------
| Expression: JsObjExpPar[id]
| Result: SUCCESS
| Duration: 184,728 ms
| Output: QREuMrvmtunCvhbZxykT
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:18:21.071473417+01:00
-------------------------
------ eval-exp --------
| Expression: JsObjExpPar
| Result: SUCCESS
| Duration: 191,692 ms
| Output: {"addresses":["m","n","g"],"number_users":32634,"timestamp":"2024-02-13T09:18:21.071Z","id":"QREuMrvmtunCvhbZxykT"}
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:18:21.064957792+01:00
-------------------------
To enhance the resilience of our code, let's introduce some retry logic for the countUsers
supplier. We want to allow up to three retries:
// let's add up to three retries
countUsers.debug(EventBuilder.of("count_users", context))
.retry(RetryPolicies.limitRetries(3))
.recover(_ -> -1)
In this code:
-
The
countUsers
supplier is executed, and for each execution, thedebug
method creates an event. TheEventBuilder
allows you to specify the name of the expression being evaluated ("count_users") and the context. This helps customize the events sent to the JFR system. -
The
retry
method is used to introduce retry logic. In case of failure,countUser
will be retried up to three times. -
The
recover
method specifies what value to return in case of a failure.
And to test it, let's change the stub for the countUser
supplier:
//let's change the delay of every stub to 1 sec, for the sake of clarity
Gen<Duration> delayGen = Gen.cons(1)
.map(Duration::ofSeconds);
IO<Integer> countUsers =
StubBuilder.ofGen(Gen.seq(n -> n <= 4 ?
IO.fail(new RuntimeException(n + "")) :
IO.succeed(n)
)
)
.withDelays(delayGen)
.withExecutor(Executors.newVirtualThreadPerTaskExecutor())
.get();
In this code:
-
The generator
delayGen
provides a constant delay of 1 second. -
The
countUsers
effect is defined to use theStubBuilder
with a sequence generator (Gen.seq
) that allows you to choose different values for each call. In this case, the first four calls trigger a failure, which is treated as a value that can be returned.
This setup allows you to test and observe the retry logic in action:
Started JFR stream for 10,000 sg in SignupTests
------ eval-exp --------
| Expression: JsObjExpPar[timestamp]
| Result: SUCCESS
| Duration: 281,583 µs
| Output: 2024-02-13T09:30:42.681Z
| Context: signup
| Thread: main
| Event Start Time: 2024-02-13T10:30:42.681326792+01:00
-------------------------
------ eval-exp --------
| Expression: count_number_users
| Result: FAILURE
| Duration: 1,010 sg
| Output: java.lang.RuntimeException: 1
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:42.678849959+01:00
-------------------------
------ eval-exp --------
| Expression: JsObjExpPar[addresses]
| Result: SUCCESS
| Duration: 1,007 sg
| Output: ["l","e","B"]
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:42.681127792+01:00
-------------------------
------ eval-exp --------
| Expression: IfElseExp-predicate
| Result: SUCCESS
| Duration: 1,006 sg
| Output: false
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:43.6904075+01:00
-------------------------
------ eval-exp --------
| Expression: count_number_users
| Result: FAILURE
| Duration: 1,006 sg
| Output: java.lang.RuntimeException: 2
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:43.690528334+01:00
-------------------------
------ eval-exp --------
| Expression: count_number_users
| Result: FAILURE
| Duration: 1,004 sg
| Output: java.lang.RuntimeException: 3
| Context: signup
| Thread: not recorded
| Event Start Time: 2024-02-13T10:30:44.696579667+01:00
-------------------------
------ eval-exp --------
| Expression: PairExpSeq[1]
| Result: SUCCESS
| Duration: 1,001 sg
| Output: null
| Context: signup
| Thread: not recorded
| Event Start Time: 2024-02-13T10:30:44.702844292+01:00
-------------------------
------ eval-exp --------
| Expression: PairExpSeq[2]
| Result: SUCCESS
| Duration: 1,003 sg
| Output: null
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:45.704042667+01:00
-------------------------
------ eval-exp --------
| Expression: count_number_users
| Result: FAILURE
| Duration: 1,006 sg
| Output: java.lang.RuntimeException: 4
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:45.700588667+01:00
-------------------------
------ eval-exp --------
| Expression: PairExpSeq
| Result: SUCCESS
| Duration: 2,004 sg
| Output: (null, null)
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:44.702836584+01:00
-------------------------
------ eval-exp --------
| Expression: JsObjExpPar[number_users]
| Result: SUCCESS
| Duration: 4,030 sg
| Output: -1
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:42.67800425+01:00
-------------------------
------ eval-exp --------
| Expression: IfElseExp-alternative
| Result: SUCCESS
| Duration: 2,015 sg
| Output: adnhvqDPCgmEINgqiteV
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:44.702804125+01:00
-------------------------
------ eval-exp --------
| Expression: IfElseExp
| Result: SUCCESS
| Duration: 3,028 sg
| Output: adnhvqDPCgmEINgqiteV
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:43.690404584+01:00
-------------------------
------ eval-exp --------
| Expression: JsObjExpPar[id]
| Result: SUCCESS
| Duration: 4,037 sg
| Output: adnhvqDPCgmEINgqiteV
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:42.681302584+01:00
-------------------------
------ eval-exp --------
| Expression: JsObjExpPar
| Result: SUCCESS
| Duration: 4,042 sg
| Output: {"addresses":["l","e","B"],"number_users":-1,"timestamp":"2024-02-13T09:30:42.681Z","id":"adnhvqDPCgmEINgqiteV"}
| Context: signup
| Thread: virtual--1
| Event Start Time: 2024-02-13T10:30:42.676874584+01:00
-------------------------
Key points:
-
After the first failure and three retries, the value -1 from the
recover
function is returned -
The
retry
method can accept a predicate, allowing you to specify which errors should trigger a retry. This fine-grained control is valuable for handling specific error scenarios. -
Retry policies in JIO are composable, making it easy to build complex retry strategies. For example, you can create a policy like this:
RetryPolicies.constantDelay(Duration.ofMillis(50)) .limitRetriesByCumulativeDelay(Duration.ofMillis(300))
This policy specifies a constant delay of 50 milliseconds between retries and limits retries by a cumulative delay of 300 milliseconds.
-
JIO excels at scalability. Even when dealing with complex logic, it maintains simplicity in the expressions you write, avoiding the complexities of callback hell or other frameworks.
-
JIO offers a high signal-to-noise ratio. It reduces verbosity, allowing you to express complex operations succinctly and clearly.
Functional Programming is all about working with pure functions and values. That's all. However, where FP especially shines is dealing with effects.
But what is an effect?
First take a look at the following piece of code:
int a = sum(1,2) + 3;
int b = sum(1,2) + 1;
As far as the function sum
is pure, you can refactor the previous piece of code and call the
function just once:
int c = sum(1,2);
int a = c + 3;
int b = c + 1;
Both programs are equivalents and wherever you see sum(1,2)
you can replace it by c
without
changing the meaning of the program at all.
An effect, on the other hand, is something you can't call more than once unless you intended to:
Instant a = Instant.now().plus(Period.ofDays(1));
Instant b = Instant.now().plus(Period.ofDays(2));
Because now() returns a different value each time it's called and therefore is not a pure function, the following refactoring would change completely the meaning of the program (and still your favourite IDE suggests you to do it at times!):
Instant now = Instant.now();
Instant a = now.plus(Period.ofDays(1));
Instant b = now.plus(Period.ofDays(2));
Here's when laziness comes into play. Since Java 8, we have suppliers. They are indispensable to do FP in Java. The following piece of code is equivalent to the original one without changing the meaning of the program:
Supplier<Instant> now = () -> Instant.now();
Instant a = now.get().plus(Period.ofDays(1));
Instant b = now.get().plus(Period.ofDays(2));
This property that allows you to factor out expressions is called referential transparency, and it's fundamental to create and compose expressions.
What can you expect from JIO:
- Simple and powerful API
- Errors are first-class citizens
- Simple and powerful testing tools (jio-test)
- Easy to extend and get benefit from all the above. Examples are jio-http, jio-mongodb and jio-jdbc. And you can create your own integrations!
- I don't fall into the logging-library war. This is something that sucks in Java. I just use Java Flight Recording!
- Almost zero dependencies (just plain Java!)
- JIO doesn't transliterate any functional API from other languages. This way, any standard Java programmer will find JIO quite easy and familiar.
Let's model a functional effect in Java!
import java.util.function.Supplier;
import java.util.concurrent.CompletableFuture;
public sealed abstract class IO<Output> implements Callable<Result<Output>> permits Exp, Val {
@Override
Result<Output> call() throws Exception;
Result<O> compute();
//other methods
}
public sealed interface Result<Output> permits Result.Success, Result.Failure {
//methods
}
Key Concepts:
-
IO
Definition: TheIO
class is a fundamental component of JIO. It's an abstract class designed to represent functional effects or computations. -
Lazy Computation:
IO
is a lazy computation and is realized as aCallable
. In essence, it merely outlines a computation without immediate execution, awaiting the explicit invocation of methods likecall()
orcompute()
. It's important to note that both operations are blocking, which isn't an issue when employing virtual threads. -
Handling Errors: A critical aspect of JIO is that
Result
can represent both successful and failed computations. This approach ensures that errors are treated as first-class citizens, avoiding the need to throw exceptions whenever an error occurs. -
According to Erik Meyer, as mentioned in this video, honesty is at the core of functional programming. I find this perspective to be quite insightful. Latency and failures hold such a significance that they should be explicitly denoted in a function or method's signature with the
IO
type. Without this distinction, it becomes impossible to differentiate functions that are free from failure and latency from those that aren't, making our code difficult to reason about. -
The
call
andcompute
methods exhibit significant similarity. Thecall
method is essential due to IO's implementation ofCallable
, facilitating seamless integration with the structural concurrency API:
IO first = ???;
IO second = ???;
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// Since IO is a callable, we can pass it in the `fork` method
Subtask<Result<First>> first = scope.fork(first);
Subtask<Result<Second>> second = scope.fork(second);
....
}
In most cases, apart from the one described above, it is advisable to use the compute
method. This
method does not throw a checked exception and returns a Result
object encapsulating the outcome of
the computation (either success or failure)."
-
According to its
permits
definition, IO has two distinct subclasses:Val
andExp
-
Val
: This subclass denotes an effect that is computed and returned as a result. -
Exp
: This subclass signifies an expression composed of multiple effects, which will be computed and combined into the final result through an expression. Some examples of expressions that we will see later arePairExp
,JsObjExp
,CondExp
,ListExp
,SwitchExp
etc.
Now that we got the ball rolling, let's learn how to create IO effects.
From a constant or a computed value
IO<String> effect = IO.succeed("hi");
JsObj get(int id) { ??? }
IO<String> effect = IO.succeed(get(1)); //get(1) is invoked before constructing the effect
In both of the above examples, the effect will always compute the same value: either "hi" or the
result of calling get(1)
. There is no lazynes here, a value is computed right away and used to
create the IO effect
From an exception
IO<String> effect = IO.fail(new RuntimeException("something went wrong :("));
Like with succeed
, the effect will always produce the same result, in this case it fails always
with the same exception, which is instantiated before creating the effect. Do notice that no
exception is thrown!
From a lazy computation or a supplier
This is a very common case, and you will use it all the time to create effects.
Suplier<JsObj> computation = ???;
IO<Long> effect = IO.lazy(computation);
In this example and effect is created but not like in succeed
and fail
, nothing is evaluated
since a Supplier
is lazy. It's very important to notice the difference. On the other hand, each
time the get
or result
methods are invoked a potentially new value can be returned.
From a callable
We can think of a Callable
as lazy computations like Suppliers
, but with the important
difference that they can fail.
Callable<Long> callable = ???;
IO<Long> effect = IO.task(callable);
From a Future:
Future<JsObj> get(String id){ ??? }
IO<JsObj> effect = IO.effect( () -> get(1) );
Like with lazy
and task
, the previous example doesn't evaluate anything to create the effect
since the effect method takes in a Supplier
.
From auto-closable resources
The resource
method is used to create an IO effect that manages a resource implementing the
AutoCloseable
interface. It takes a Callable
that supplies the closable resource and a mapping
function to transform the resource into a value. This method ensures proper resource management,
including automatic closing of the resource after the map function is executed, to prevent memory
leaks. It returns an IO effect encapsulating both the resource handling and mapping.
static <O, I extends AutoCloseable> IO<O> resource(Callable<I> resource,
Lambda<I, O> map
);
and an example:
Callable<FileInputStream> callable = () -> new FileInputStream("example.txt");
// Create an IO effect using the resource method
IO<String> resultEffect =
IO.resource(callable,
inputStream -> {
try {
// Read the content of the file and return it as a String
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
return IO.succeed(new String(bytes,
StandardCharsets.UTF_8));
}
catch (IOException e) {
return IO.fail(e);
}
}
);
Other regular IO effects
IO<Boolean> t = IO.TRUE
IO<Boolean> f = IO.FALSE;
IO<String> s = IO.NULL();
IO<Integer> s = IO.NULL();
The NULL
method creates an IO effect that always produces a result of null. It is a generic method
that captures the type of the caller, allowing you to create null effects with different result
types. This can be useful when you need to type of the caller, allowing you to create null effects
with different result types. This can be useful when you need to represent a null result in your
functional code. These constants, TRUE
and FALSE
, represent IO effects that always succeed with
true
and false
, respectively.
In the world of JIO, working with effectful functions is a common practice. The following functions
return IO
effects, and you'll often encounter them in your code:
Function<I, IO<O>>
BiFunction<A,B, IO<O>>
To make our code more concise and readable, we can give these effectful functions an alias. Let's call them "Lambdas":
interface Lambda<I, O> extends Function<I, IO<O>> {}
interface BiLambda<A, B, O> extends BiFunction<A, B, IO<O>> {}
Lambdas are similar to regular functions, but there's one key difference: they never throw exceptions. In JIO, exceptions are treated as first-class citizens, just like regular values.
Converting regular functions or predicates into Lambdas is straightforward using the lift methods:
Function<Integer, Integer> opposite = n -> -n;
BiFunction<Integer, Integer, Integer> sum = (a,b) -> a + b;
Predicate<Integer> isOdd = n -> n % 2 == 1;
Lambda<Integer, Integer> l1 = Lambda.liftFunction(opposite);
Lambda<Boolean, Integer> l2 = Lambda.liftPredicate(isOdd);
BiLambda<Integer, Integer, Integer> l3 = BiLambda.liftFunction(sum);
The then
method is a powerful feature of the Lambda
interface in JIO that allows you to compose
and sequence effects in a functional and expressive manner. When you have two Lambda
instances,
you can use the then
method to create a new Lambda
that combines the effects of the two original
lambdas. When you apply the composed Lambda
to an input, it executes the first Lambda
, followed
by the second Lambda
, creating a sequence of effects. This composition is especially useful when
building complex workflows or pipelines of operations. It enhances the readability and
expressiveness of your code by chaining together effects in a natural and intuitive way.
Lambda<A,B> first = ???;
Lambda<B,C> second = ???
Lambda<A,C> third = first.then(second);
Retrying failed operations is a crucial aspect of handling errors effectively in JIO. In real-world
scenarios, errors can sometimes be transient or caused by temporary issues, such as network glitches
or resource unavailability.
By incorporating retry mechanisms, JIO empowers you to gracefully recover from such errors without
compromising the stability of your application. Whether it's a network request, database query, or
any other effect, JIO's built-in retry functionality allows you to define retry policies, such as
exponential backoff or custom strategies, to ensure that your operations have a higher chance of
succeeding. This approach not only enhances the robustness of your application but also minimizes
the impact of transient errors, making JIO a valuable tool for building resilient and reliable
systems.
public sealed abstract class IO<Output> implements Callable<Result<Output>> permits Exp, Val {
IO<O> retry(Predicate<Throwable> predicate,
RetryPolicy policy
);
IO<O> repeat(Predicate<Output> predicate,
RetryPolicy policy
);
}
While the retry
method is primarily used to retry an operation when an error occurs (based on a
specified exception condition), the repeat
method allows you to repeat an operation based on the
result or outcome of the effect itself,
providing flexibility for scenarios where retries are needed for reasons other than errors. Retry
policies are created in a very declarative and composable way, for example:
Duration oneHundredMillis = Duration.ofMillis(100);
Duration oneSec = Duration.ofSeconds(1);
// up to five retries waiting 100 ms
RetryPolicies.constantDelay(oneHundredMillis)
.append(limitRetries(5))
// during 3 seconds up to 10 times
RetryPolicies.limitRetries(10)
.limitRetriesByCumulativeDelay(Duration.ofSeconds(3))
// 5 times without delay and then, if it keeps failing,
// an incremental delay from 100 ms up to 1 second
RetryPolicies.limiteRetries(5)
.followedBy(incrementalDelay(oneHundredMillis)
.capDelay(oneSec))
There are very interesting policies implemented based on this article: exponential backoff, full jitter, equal jitter, decorrelated jitter etc
In scenarios where errors persist despite retries, JIO offers robust error-handling mechanisms to ensure your application maintains resilience. Three key methods come into play:
public sealed abstract class IO<Output> implements Callable<Result<Output>> permits Exp, Val {
IO<Output> recover(Function<Throwable, Output> fn);
IO<Output> recoverWith(Lambda<Throwable, Output> fn);
IO<Output> fallbackTo(Lambda<Throwable, Output> fn);
}
recover: This method allows you to gracefully recover from an error by providing a function that maps the encountered exception to a fallback value of type 'O.' It ensures that your application can continue its operation even in the face of unexpected errors.
recoverWith: This method allows you to gracefully recover from an error by providing a function that maps the encountered exception to a fallback value of type 'O.' It ensures that your application can continue its operation even in the face of unexpected errors.
fallbackTo: Similar to 'recoverWith,' 'fallbackTo' allows you to switch to an alternative effect (specified by the provided function) when an error occurs. However, it introduces an important distinction: if the alternative effect also encounters an error, 'fallbackTo' will return the original error from the first effect. This ensures that error propagation is maintained while enabling you to gracefully handle errors and fallback to alternative operations when needed.
JIO encourages a functional programming style with the following methods:
public sealed abstract class IO<Output> implements Callable<Result<Output>> permits Exp, Val {
IO<MappedOutput> map(Function<Output, MappedOutput> fn);
IO<Output> mapFailure(Function<Throwable, Throwable>);
IO<MappedOutput> then(Lambda<Output, MappedOutput> fn);
}
map
: Transforms the successful result of an effect using a provided function, allowing you to map values from one type to another.mapFailure
: Transforms the failure result of an effect using a provided function, allowing you to map exceptions from one type to another.then
(akin toflatMap
orbind
in other languages and a core function in monads): Applies a lambda function to
the result of the effect, creating a new effect that depends on the previous result. The name 'then' is used here for conciseness.
public sealed abstract class IO<Output> implements Callable<Result<Output>> permits Exp, Val {
IO<Output> debug();
IO<Output> debug(EventBuilder builder);
}
Debugging and logging events play a pivotal role in software development. Some of the advantages that you'll get are:
-
Simplifying Debugging: Debugging is the process of identifying and fixing issues in your code. When testing an application, developers often need to trace the execution of specific parts of their code to locate bugs or performance bottlenecks. These debugging methods allow you to attach debug mechanisms to expressions, making it easier to monitor and log the execution of each operand individually. This granular level of insight is invaluable when diagnosing and resolving problems.
-
Testing Efficiency: During the testing phase, the ability to debug and log events efficiently is a time-saving and productivity-enhancing feature. These methods let you generate and send events to the Flight Recorder system, which can then be analyzed to gain insights into the behavior of your code. You can customize these events using the provided
EventBuilder
, tailoring the information collected to suit your specific testing needs. -
No Setup Overhead: One of the key advantages of these methods is that they require minimal setup. This means that you can integrate debugging and event logging into your codebase without the need for extensive configuration or external tools. It's a straightforward and hassle-free way to gather essential information about the execution of your code.
-
Recursive Debugging with debugEach: When you apply the debugEach method to an expression, it attaches a debugging mechanism to that expression and all its subexpressions. If any subexpression within the main expression is itself an expression, debugEach is applied to it recursively, passing along the same context.
-
Customization with EventBuilder
Customization with the EventBuilder
is a powerful feature that streamlines your debugging and
event logging. It allows you to tailor events to your specific needs and focus on the most relevant
information:
-
Event Naming: You can categorize events by specifying the name for the expression being evaluated and the context you're observing, making it easier to analyze different aspects of your code.
-
Mapping Success Output: Transform the result of a successful expression into a format that provides clear and concise information, helping you capture expected outcomes.
-
Mapping Failure Output: Customize how exceptions are presented by mapping
Throwable
objects into a format that aids in debugging and troubleshooting. This can include error messages, stack traces, or other relevant details.
In conclusion, the ability to debug and log events with minimal setup is highly valuable for developers. It simplifies the debugging process, enhances testing efficiency, and provides data-rich insights into code execution. These methods are indispensable tools for maintaining software quality, diagnosing issues, and optimizing performance during the development and testing phases.
public sealed abstract class IO<Output> implements Callable<Result<Output>> permits Exp, Val {
IO<Void> async();
}
async
: Allows executing an action without waiting for its result and returns immediately. It is useful when you are not interested in the outcome of the effect (returns void) and want to trigger it asynchronously.
Sometimes, you need to sneak a peek into the execution of an effect:
public sealed abstract class IO<Output> implements Callable<Result<Output>> permits Exp, Val {
IO<Output> peekFailure(Consumer<Throwable> failConsumer);
IO<Output> peekSuccess(Consumer<Output> successConsumer);
IO<O> peek(Consumer<Output> successConsumer,
Consumer<Throwable> failureConsumer
);
}
peekFailure
: Allows you to observe and potentially handle failures by providing a consumer that logs exceptions in the JFR (Java Flight Recorder) system.peekSuccess
: Similarly, you can observe and process successful results using a success consumer.peek
: Combines both success and failure consumers, giving you full visibility into the effect's execution.
Exceptions occurring here are logged in the JFR system and do not alter the result of the effect.
When you require a result as quickly as possible among multiple alternatives, and you're uncertain which one will be the fastest:
public sealed abstract class IO<Output> implements Callable<Result<Output>> permits Exp, Val {
static <Output> IO<Output> race(IO<Output> first, IO<Output>... others);
}
race
method returns the result of the first effect that completes successfully, allowing you to
make quick decisions based on the outcome. "It employs foundational structural concurrency, which is
a preview functionality introduced in Java 21. Specifically, it leverages the
StructuredTaskScope.ShutdownOnSuccess
scope.
"To initiate the computation for deriving the final result, one must block and await the completion
by invoking the compute()
method. Notably, blocking is not a concern in this context as it is
expected to be executed from a virtual thread. This operation does not throw exceptions directly;
instead, it encapsulates potential outcomes within the Result
type:
//nothing is evaluated here
IO<Integer> countUsers = ???;
//evaluation is triggered
Result<Integer> countUsersResult = countUsers.compute();
//we take advantage of patter matching to process the result for
//both possible outcomes: success or failure
String message =
switch (countUsersResult) {
case Success<Integer> success -> {
switch (success.output()) {
case 0 -> {
yield "no users!";
}
case Integer users when users > 0 -> {
yield "number of users is greater than zero!";
}
case Integer _ -> {
yield "number of users is lower than zero";
}
}
}
case Failure<Integer> failure -> {
switch (failure.exception()) {
case ConnectException _ -> {
yield "Try again!";
}
case SocketTimeoutException _ -> {
yield "so impatient!";
}
case Exception _ -> {
yield "maybe the next time!";
}
}
}
};
When dealing with a Result
, pattern matching is preferred whenever feasible. However, you can
still utilize the Result
API to retrieve the final output:
Result<Integer> countUsersResult;
//throws an unchecked exception in case of failure (wraps the real failure in a `CompletionException`)
Integer users = effect.getOutput();
//throws a checked exception (the real failure) in case of failure
Integer users = effect.getOutputOrThrow();
getOutput
is more suitable when the API being used does not align well with checked exceptions,
such as with Stream
or Function
. Conversely, getOutputOrThrow
mandates handling the potential
exception and ensures that the thrown exception is the real failure and not a CompletionException
Using expressions and function composition is how we deal with complexity in Functional
Programming.
With the following expressions, you will have a comprehensive toolkit to model effects, combine them
in powerful ways, and tame complexity effectively.
The IfElseExp
expression allows you to conditionally choose between two computations based on a
predicate. If the
predicate evaluates to true, the consequence is computed, and if it's false, the alternative
computation is chosen. Both the consequence and alternative are represented as lazy computations of
IO effects.
IO<O> exp = IfElseExp.<O>predicate(IO<Boolean> condition)
.consequence(Supplier<IO<O>> consequence)
.alternative(Supplier<IO<O>> alternative);
In this code, the consequence
and alternative
parameters are represented as Supplier
instances, which means they are not computed during the construction of the expression but rather
evaluated only when needed, based on the result of the condition. This deferred execution allows for
efficient conditional evaluation of IO effects.
Your original sentence is mostly correct but could benefit from a minor improvement for clarity:
The SwitchExp
expression simulates the behavior of a switch construct, enabling multiple
pattern-value branches. It evaluates an effect or a value of type I
and facilitates the use of
multiple clauses based on the evaluation. The match
method compares the value or effect with
patterns and selects the corresponding lambda (which takes in the value of type I
). Patterns can
encompass values, lists of values, or even predicates. It's possible to specify a default branch, in
case no pattern matches the input (otherwise the expression is reduced to IO.NULL
).
// matches a value of type I
IO<O> exp =
SwitchExp<I,O>.eval(I value)
.match(I pattern1, Lambda<I,O> lambda1,
I pattern2, Lambda<I,O> lambda2,
I pattern3, Lambda<I,O> lambda3,
Lambda<I,O> otherwise //optional
);
// matches an effect of type I
IO<O> exp=
SwitchExp<I, O>.eval(IO<I> effect)
.match(I pattern1, Lambda<I,O> lambda1,
I pattern2, Lambda<I,O> lambda2,
I pattern3, Lambda<I,O> lambda3,
Lambda<I,O> otherwise //optional
);
// For example, the following expression reduces to "3 is Wednesday"
IO<O> exp=
SwitchExp<String>.eval(3)
.match(1, n -> IO.succedd(n + " is Monday"),
2, n -> IO.succedd(n + " is Tuesday"),
3, n -> IO.succedd(n + " is Wednesday"),
4, n -> IO.succedd(n + " is Thursday"),
5, n -> IO.succedd(n + " is Friday"),
n -> IO.succedd(n + " is weekend")
);
The same as before but using lists instead of constants as patterns.
IO<O> exp =
SwitchExp<I, O>.eval(I value)
.matchList(List<I> pattern1, Lambda<I,O> lambda1,
List<I> pattern2, Lambda<I,O> lambda2,
List<I> pattern3, Lambda<I,O> lambda3,
Lamda<I,O> otherwise
);
// For example, the following expression reduces to "20 falls into the third week"
IO<O> exp=
SwitchExp<Integer, String>.eval(20)
..matchList(List.of(1, 2, 3, 4, 5, 6, 7),
n -> IO.succeed(n + " falls into the first week"),
List.of(8, 9, 10, 11, 12, 13, 14),
n -> IO.succeed(n + " falls into the second week"),
List.of(15, 16, 17, 18, 19, 20),
n -> IO.succeed(n + " falls into the third week"),
List.of(21, 12, 23, 24, 25, 26, 27),
n -> IO.succeedd(n + " falls into the forth week"),
n -> IO.succeed(n + " falls into the last days of the month")
);
Last but not least, you can use predicates as patterns instead of values or list of values:
IO<O> exp=
SwitchExp<I, O>.eval(IO<I> value)
.matchPredicate(Predicate<I> pattern1, Lambda<I,O> lambda1,
Predicate<I> pattern2, Lambda<I,O> lambda2,
Predicate<I> pattern3, Lambda<I,O> lambda3
);
// For example, the following expression reduces to the default value:
// "20 is greater or equal to ten"
IO<O> exp=
SwitchExp<Integer, String>.eval(IO.succeed(20))
.matchPredicate(n -> n < 5,
n -> IO.succeed(n + "is lower than five"),
n -> n < 10,
n -> IO.succeed(n + "is lower than ten"),
n-> n > 10,
n -> IO.succeed(n + "is greater or equal to ten")
);
CondExp
is a set of branches and a default effect. Each branch consists of an effect that computes
a boolean (the
condition) and its associated effect. The expression is reduced to the value of the first branch
with a true condition, making the order of branches significant. If no condition is true, it
computes the default effect if specified (otherwise the expression is reduced to IO.NULL
)
IO<O> exp=
CondExp.<O>seq(IO<Boolean> cond1, Supplier<IO<O>> effect1,
IO<Boolean> cond2, Supplier<IO<O>> effect2,
IO<Boolean> cond3, Supplier<IO<O>> effect3,
Supplier<IO<O>> otherwise //optional
);
IO<O> exp =
CondExp.<O>par(IO<Boolean> cond1, Supplier<IO<O>> effect1,
IO<Boolean> cond2, Supplier<IO<O>> effect2,
IO<Boolean> cond3, Supplier<IO<O>> effect3,
Supplier<IO<O>> otherwise //optional
);
AllExp
and AnyExp
provide idiomatic boolean expressions for "AND" and "OR." They allow you to
compute multiple boolean effects, either sequentially or in parallel.
IO<Boolean> allPar = AllExp.par(IO<Boolean> cond1, IO<Boolean> cond2,....);
IO<Boolean> allSeq = AllExp.seq(IO<Boolean> cond1, IO<Boolean> cond2,....);
IO<Boolean> anyPar = AnyExp.par(IO<Boolean> cond1, IO<Boolean> cond2,...);
IO<Boolean> anySeq = AnyExp.seq(IO<Boolean> cond1, IO<Boolean> cond2,...);
You can also create AllExp or AnyExp from streams of IO using the parCollector
and
seqCollector
Lambda<Vehicle, Boolean> isFerrari = ???
List<Vehicle> vehicles = ???;
AllExp allFerrariPar = vehicles.stream()
.map(isFerrary)
.collector(AllExp.parCollector());
AllExp allFerrariSeq = vehicles.stream()
.map(isFerrary)
.collector(AllExp.seqCollector());
AnyExp anyFerrariSeq = vehicles.stream()
.map(isFerrary)
.collector(AnyExp.seqCollector());
PairExp
and TripleExp
allow you to zip effects into tuples of two and three elements,
respectively. You can compute each element either in parallel or sequentially.
IO<Pair<A, B> pairPar = PairExp.par(IO<A> effect1,
IO<B> effect2);
IO<Pair<A, B> pairSeq = PairExp.seq(IO<A> effect1,
IO<B> effect2);
IO<Triple<A, B, C> triplePar = TripleExp.par(IO<A> effect1,
IO<B> effect2,
IO<C> effect3);
IO<Triple<A, B, C> tripleSeq = TripleExp.seq(IO<A> effect1,
IO<B> effect2,
IO<C> effect3);
JsObjExp
and JsArrayExp
are data structures resembling raw JSON. You can compute their values
sequentially or in parallel. You can mix all the expressions discussed so far and nest them,
providing you with immense flexibility and
power in handling complex data structures.
IfElseExp<JsStr> a = IfElseExp.<JsStr>predicate(IO<Boolean> cond1)
.consequence(Supplier<IO<JsStr>> consequence)
.alternative(Supplier<IO<JsStr>> alternative);
JsArrayExp b =
JsArrayExp.seq(SwitchExp<Integer, JsValue>.match(n)
.patterns(n -> n <= 0, Supplier<IO<JsValue>> e1,
n -> n > 0, Supplier<IO<JsValue>> e2
),
CondExp.par(IO<Boolean> cond2, Supplier<IO<JsValue>> e3,
IO<Boolean> cond3, Supplier<IO<JsValue>> e4,
Supplier<IO<JsValue>> otherwise
)
);
JsObjExp c = JsObjExp.seq("d", AnyExp.seq(IO<JsBool> cond1, IO<JsBool> cond2)
"e", AllExp.par(IO<JsBool> cond2, IO<JsBool> cond3)
"f", JsArrayExp.par(IO<JsValue> e5, IO<JsValue> e6)
);
JsObjExp exp = JsObjExp.par("a",a,
"b",b,
"c",c
);
Result<JsObj> json = exp.compute();
Here are some key points about the code example:
-
Readability: The code is relatively easy to read and understand, thanks to the fluent API style provided by JIO's expressions. This makes it straightforward to follow the logic of constructing a
JsObj
with multiple key-value pairs. -
Modularity: Each key-value pair is constructed separately, making it easy to add, modify, or remove components without affecting the others. This modularity is a significant advantage when dealing with complex data structures.
-
Parallelism: The example demonstrates the ability to perform computations in parallel when constructing
theJsObj
. By using expressions likeJsObjExp.par
, you can take advantage of multicore processors and improve performance. -
Nesting: The example also shows that you can nest expressions within each other, allowing for recursive data
structures. This is valuable when dealing with deeply nested expressions or other complex data formats.
Overall, the code example effectively illustrates how JIO's expressions enable you to create,
manipulate, and compose functional effects to handle complex data scenarios. It highlights the
conciseness and expressiveness of the library
when dealing with such tasks.
Represents an expression that is reduced to a list of values. You can create ListExp expressions
using the seq
method to evaluate effects sequentially or using the par
method to evaluate
effects in parallel. If one effect fails, the entire expression fails.
ListExp<String> par = ListExp.par(IO<String> effect1, IO<String> effect2, ...)
ListExp<Integer> seq = ListExp.seq(IO<String> effect3, IO<String> effect3, ...)
Result<List<String>> xs = par.compute();
Result<List<Integer>> ys = seq.compute();
It's possible to create ListExp from stream of effects of the same type using the collectors
parCollector
and seqCollector
:
Lambda<String, Person> getPersonFromId = ???;
List<String> ids = ???;
ListExp<Person> xs = ids.stream()
.filter(id -> id > 0)
.map(getPersonFromId)
.collect(ListExp.parCollector());
Result<List<Person>> persons = xs.compute();
In functional programming, it's crucial to maintain a clear separation between inputs and outputs of
a function. When dealing with time-related operations, such as retrieving the current date or time,
it becomes even more critical to adhere to this principle. This is where the concept of clocks in
JIO comes into play. A clock in JIO is essentially a supplier that returns a numeric value,
representing time. There are three types of
clocks available:
- Realtime: This clock is affected by Network Time Protocol (NTP) adjustments and can move both forwards and backward in time. It is implemented using the System.currentTimeMillis() method. Realtime clocks are typically used when you need to work with the current wall-clock time.
- Monotonic: Monotonic clocks are useful for measuring time intervals and performing time-related comparisons. They are not affected by NTP adjustments and provide a consistent and continuous time source. Monotonic clocks are implemented using the System.nanoTime() method.
- Custom: JIO allows you to create your custom clocks. Custom clocks are particularly valuable for
testing scenarios
where you want to control the flow of time, possibly simulating the past or future.
sealed interface Clock extends Supplier<Long> permits Monotonic,RealTime, CustomClock {}
Every time you write new Date() or Instant.now() in the body of a method or function, you are
creating a side effect.
Remember that in FP, all the inputs must appear in the signature of a function. Dealing with time,
it's even more
important. Also, it's impossible to control by any test the value of that timestamp which leads to
code difficult to
test.
The reason why dealing with time as an input is crucial in functional programming is to make code
more predictable,
testable, and less error-prone. Consider the following scenario, which is a common source of bugs:
Bug Scenario:
public class PaymentService {
public boolean processPayment(double amount) {
// Get the current date and time
Instant currentTime = Instant.now();
// Perform payment processing logic
// ...
// Check if the payment was made within a specific time window
Instant windowStart = currentTime.minus(Duration.ofHours(1));
Instant windowEnd = currentTime.plus(Duration.ofHours(1));
return paymentTime.isAfter(windowStart) && paymentTime.isBefore(windowEnd);
}
}
Better Version Using a Clock
A better approach is to pass a clock as a method parameter:
public class PaymentService {
public boolean processPayment(double amount, Clock clock) {
// Get the current time from the provided clock
Instant currentTime = Instant.ofEpochMilli(clock.get());
// Perform payment processing logic
// ...
// Check if the payment was made within a specific time window
Instant windowStart = currentTime.minus(Duration.ofHours(1));
Instant windowEnd = currentTime.plus(Duration.ofHours(1));
return paymentTime.isAfter(windowStart) && paymentTime.isBefore(windowEnd);
}
}
In this improved version, we pass a Clock object as a parameter to the processPayment method. This
approach offers
several advantages:
- Testability: During testing, you can provide a custom clock that allows you to control the current time, making tests more predictable and reliable.
- Predictability: The behavior of the method is consistent regardless of when it's called since it
depends on the
provided clock.
By using a clock as a parameter, you enhance the reliability and maintainability of your code,
especially in scenarios
where time plays a critical role.
In the world of Java, there has long been a multitude of logging libraries and frameworks, each with its strengths and limitations. However, the introduction of Java Flight Recorder (JFR) has been a game-changer. JFR is a native and highly efficient profiling and event recording mechanism embedded within the Java Virtual Machine (JVM). Its native integration means it operates seamlessly with your Java applications, imposing minimal performance overhead. JFR provides unparalleled visibility into the inner workings of your code, allowing you to capture and analyze events with precision.
Unlike external logging libraries, JFR doesn't rely on third-party dependencies or introduce
additional complexity to
your projects. By using JFR within JIO, you harness the power of this built-in tool to gain deep
insights into the
behavior of your functional effects and expressions, all while keeping your codebase clean and
efficient. JFR is the
dream solution for Java developers seeking robust debugging and monitoring capabilities with minimal
hassle."
Debugging and monitoring the behavior of your JIO-based applications is essential for
troubleshooting, performance
optimization, and gaining insights into your functional effects and expressions. JIO provides
comprehensive support for debugging and integration with Java Flight Recorder (JFR) to capture and
analyze events.
You can enable debugging for individual effects using the debug
method. When this method is used,
a new effect is created that generates a RecordedEvent
and sends it to the Flight Recorder system.
You can also customize the event by providing an EventBuilder
. Here's an overview:
The IO
class has the following methods for debugging:
public sealed abstract class IO<Output> implements Callable<Result<Output>> permits Exp, Val
{
IO<O> debug();
IO<O> debug(EventBuilder<O> builder);
}
The resulting JFR event is defined as follows:
import jdk.jfr.*;
@Label("Expression Evaluation Info")
@Name("jio.exp.EvalExp")
@Category({"JIO", "EXP"})
@Description("Duration, output, context and other info related to a computation")
@StackTrace(value = false)
class ExpEvent extends Event {
@Label("exp")
public String expression;
@Label("value")
public String value;
@Label("context")
public String context;
@Label("result")
public String result;
public enum RESULT {SUCCESS, FAILURE}
@Label("exception")
public String exception;
}
You can use the JIO debugger to print the events sent to the JFR system. Here's an example:
@RegisterExtension
static Debugger debugger = Debugger of (Duration.ofSeconds(1));
@Test
public void test() {
Result<Integer> value =
IO.succeed(10)
.debug()
.compute();
Result<Integer> failure =
IO.<Integer>fail(new RuntimeException("JIO is great!"))
.debug()
.compute();
}
The result includes events like this:
------ eval-exp --------
| Expression: Val
| Result: SUCCESS
| Duration: 14,249 ms
| Output: 10
| Context:
| Thread: main
| Event Start Time: 2024-03-23T11:23:28.745121208+01:00
-------------------------
------ eval-exp --------
| Expression: Val
| Result: FAILURE
| Duration: 39,583 µs
| Output: java.lang.RuntimeException: JIO is great!
| Context:
| Thread: main
| Event Start Time: 2024-03-23T11:23:28.759822+01:00
-------------------------
The event type is always "eval-exp" (for evaluation), and the expression is "Val" (for Value), reflecting the evaluation of two irreducible expressions. The result is "SUCCESS" for the first evaluation, and "FAILURE" for the second. The context in both cases is the default (an empty string).
You can customize event messages using an EventBuilder
. For example:
EventBuilder<Integer> eb =
EventBuilder.<Integer>of("other_exp_name", "fun")
.withSuccessOutput(output -> "XXX")
.withFailureOutput(Throwable::getMessage);
Result<Integer> value =
IO.succeed(10)
.debug(eb)
.compute();
Result<Integer> failure =
IO.<Integer>fail(new RuntimeException("JIO is great!"))
.debug(eb)
.compute();
The result with this customization is:
------ eval-exp --------
| Expression: other_exp_name
| Result: SUCCESS
| Duration: 19,285 ms
| Output: XXX
| Context: fun
| Thread: main
| Event Start Time: 2024-03-23T11:26:56.386859042+01:00
-------------------------
------ eval-exp --------
| Expression: other_exp_name
| Result: FAILURE
| Duration: 44,417 µs
| Output: JIO is great!
| Context: fun
| Thread: main
| Event Start Time: 2024-03-23T11:26:56.407043375+01:00
-------------------------
The EventBuilder
provides key points for customization, including specifying event messages for
successful and failed computations and associating events with specific expressions and contexts.
JIO's debugging capabilities extend beyond individual effects. You can attach a debug mechanism to
each operand of an expression using the debugEach
method. This allows you to monitor and log the
execution of each operand individually. Here's an overview:
sealed abstract class Exp<Output> extends IO<Output>
permits AllExp, AnyExp, CondExp, IfElseExp, JsArrayExp,
JsObjExp, ListExp, PairExp, SwitchExp, TripleExp {
Exp<Output> debugEach(EventBuilder<O> builder);
Exp<Output> debugEach(String context);
}
By using debugEach
, you can gain insights into the behavior of complex expressions and identify
any issues or bottlenecks that may arise during execution. This mechanism is recursive, meaning that
if subexpressions are expressions themselves, debugEach
will be called on them, copying the
context of the event (if specified).
You can also provide an EventBuilder
or a descriptive context to customize the debug events for
each operand.
Here's an example of using debugEach
:
@RegisterExtension
static Debugger debugger = Debugger.of(Duration.ofSeconds(2));
@Test
public void testMain(){
Supplier<Boolean> isLowerCase = BoolGen.arbitrary().sample();
Supplier<String> lowerCase = Combinators.oneOf("a", "e", "i", "o", "u").sample();
Supplier<String> upperCase = Combinators.oneOf("A", "E", "I", "O", "U").sample();
SwitchExp<String, String> match =
SwitchExp.<String, String>eval(IfElseExp.<String>predicate(IO.lazy(isLowerCase))
.consequence(() -> IO.lazy(lowerCase))
.alternative(() -> IO.lazy(upperCase))
)
.matchList(List.of("a", "e", "i", "o", "u"),
letter -> IO.succeed("%s %s".formatted(letter, letter.toUpperCase())),
List.of("A", "E", "I", "O", "U"),
letter -> IO.succeed("%s %s".formatted(letter, letter.toLowerCase()))
)
.debugEach("context");
}
The result after executing this test includes events related to each operand:
------ eval-exp --------
| Expression: SwitchExp-eval-predicate
| Result: SUCCESS
| Duration: 13,613 ms
| Output: false
| Context: context
| Thread: main
| Event Start Time: 2024-03-23T12:31:42.229576959+01:00
-------------------------
------ eval-exp --------
| Expression: SwitchExp-eval-alternative
| Result: SUCCESS
| Duration: 21,000 µs
| Output: O
| Context: context
| Thread: main
| Event Start Time: 2024-03-23T12:31:42.244025125+01:00
-------------------------
------ eval-exp --------
| Expression: SwitchExp-eval
| Result: SUCCESS
| Duration: 15,562 ms
| Output: O
| Context: context
| Thread: main
| Event Start Time: 2024-03-23T12:31:42.228762834+01:00
-------------------------
------ eval-exp --------
| Expression: SwitchExp-branch[1]
| Result: SUCCESS
| Duration: 7,583 µs
| Output: O o
| Context: context
| Thread: main
| Event Start Time: 2024-03-23T12:31:42.244515667+01:00
-------------------------
------ eval-exp --------
| Expression: SwitchExp
| Result: SUCCESS
| Duration: 15,772 ms
| Output: O o
| Context: context
| Thread: main
| Event Start Time: 2024-03-23T12:31:42.228759417+01:00
-------------------------
As mentioned earlier, the debugExp
function is recursive. Since the eval
subexpression of the
SwitchExp
is an IfElseExp
, you can see events associated with it: SwitchExp-eval-predicate
and
SwitchExp-eval-alternative
. On the other hand you can see how the first branch of the Switch was
returned.
It requires Java 21 or greater
<dependency>
<groupId>com.github.imrafaelmerino</groupId>
<artifactId>jio-exp</artifactId>
<version>3.0.0-RC2</version>
</dependency>
json-values is the only dependency
In JIO, you can build and deploy HTTP servers using the HttpServerBuilder
. This builder is a
versatile tool for defining and launching HTTP servers for various purposes, including testing. The
HttpServerBuilder
allows you to create HttpServer
or HttpsServer
instances with ease.
Employing the HttpServer
native class to initiate servers for your tests simplifies both setup and
teardown procedures, as the server is embedded within the Java process running the test. This
ensures that you'll never leave a port lingering.
Specifying the Request Handlers
To handle specific URI paths, you can associate each path with an HTTP request handler. For each path, specify a handler that will be invoked for incoming requests.
HttpHandler handler = ???;
HttpHandler handler1 = ???;
HttpServerBuilder serverBuilder = HttpServerBuilder.of("/your-path", handler,
"/your-path1", handler1);
Specifying an Executor
When creating an HttpServer
is possible to specify an Executor
. All HTTP requests received by
the server will be handled in tasks provided to this executor. By default, virtual threads are used:
Executors.newVirtualThreadPerTaskExecutor()
Setting the Socket Backlog
The HttpServerBuilder
allows you to specify the socket backlog, which defines the number of
incoming connections that can be queued for acceptance. You can set the backlog using the
withBacklog(int backlog)
method.
int backlog = ???; // Your desired backlog value
serverBuilder.withBacklog(backlog);
Enabling SSL
If you want to accept only SSL connections:
HttpsConfigurator httpsConfigurator = ???;
serverBuilder.withSSL(httpsConfigurator);
Recording JFR Events (Java Flight Recorder)
By default, the HttpServer
records Java Flight Recorder (JFR) events for HTTP requests, which can
be helpful for debugging and performance analysis. However, you can disable this feature:
serverBuilder.withoutRecordedEvents();
Starting the server on a Specific Port
The start methods allow you to create and start the HTTP server at your convenience.
String host = "localhost";
int port = 8080;
Result<HttpServer> server = serverBuilder.start(host, port);
Starting the server on a Random Available Port
You can even pick a random port, which is useful for local testing as we'll see later.
int startPort = 8000;
int endPort = 9000;
Result<HttpServer> = serverBuilder.startAtRandom(startPort, endPort);
In conclusion, with the HttpServerBuilder
, you can easily create and deploy HTTP/HTTPS servers in
your JIO applications, making it convenient for testing and development. Whether you need to specify
an executor, add request handlers, or start on specific or random ports, this builder provides the
flexibility and functionality to meet your server deployment needs.
Find below a complete example and the events sent to the JFR system:
import com.sun.net.httpserver.HttpHandler;
HttpHandler tokenHandler =
PostStub.of(BodyStub.gen(JsObjGen.of("access_token", JsStrGen.alphanumeric(10, 10))
.map(JsObj::toString)),
StatusCodeStub.cons(200)
);
HttpHandler thankHandler =
GetStub.of(BodyStub.cons("your welcome!"),
StatusCodeStub.gen(Combinators.freq(Pair.of(5, IntGen.arbitrary(200, 299)),
Pair.of(1, Gen.cons(401))))
);
Result<HttpServer> server =
HttpServerBuilder.of(Map.of("/token", tokenHandler,
"/thanks", thankHandler
)
)
.startAtRandom(8000, 9000);
The example code sets up a test environment for an HTTP client with OAuth support (Client
Credentials flow). It uses stubs from jio-test to create HTTP handlers for testing
different scenarios. The tokenHandler
simulates an OAuth token request, and the thankHandler
simulates a response that includes a "your welcome!" message. The status code for the thankHandler
is generated to return a 401 response approximately 1 out of 6 times, simulating the case where the
access token has expired. The HttpServerBuilder
is used to create an HTTP server on a random port
to handle these requests. This setup allows testing of various scenarios, including token expiration
handling.
In JIO, I create an HTTP client on top of the Java HttpClient introduced in Java 11. JIO's goal is
to work with Java's native objects (no abstraction on top of them) while treating errors as normal
values (Lambdas can help us here!). This approach allows us to define an HTTP exchange as a
function, where the input is an HTTP request (modeled using java.net.http.HttpRequest.Builder
),
and the output is an IO
object representing the response. The function signature for this is as
follows:
<O> Lambda<HttpRequest.Builder, HttpResponse<O>>
To make this type more concise, we give it an alias in JIO-HTTP. We call the previous function an
HttpLambda<O>
, where O
represents the response body type (typically String
or byte[]
):
interface HttpLambda<RespBody> extends Lambda<HttpRequest.Builder, HttpResponse<RespBody>> {}
JIO-HTTP offers an HTTP client with various options for handling different response types. Depending on your desired response type, you can use one of the following methods:
public interface JioHttpClient {
HttpLambda<String> ofString();
HttpLambda<byte[]> ofBytes();
HttpLambda<Void> discarding();
<RespBody> HttpLambda<RespBody> bodyHandler(HttpResponse.BodyHandler<RespBody> handler);
}
You can create and configure a JioHttpClient
using the builder JioHttpClientBuilder
. This
builder allows you to customize the HTTP client, including specifying a retry policy, a retry
predicate for selecting what errors to retry, and enabling or disabling the recording of Java Flight
Recorder (JFR) events for HTTP requests and responses. JFR event recording is enabled by default:
withRetryPolicy
: Sets a default retry policy for handling exceptions during requests.withRetryPredicate
: Sets a default predicate for selectively applying the retry policy based on the type or condition of the exception.withoutRecordEvents
: Disables the recording of JFR events for HTTP requests.
Below is a complete example, making requests to the famous PetStore service, illustrating how to use create and use the JIO HTTP client.
public class TestHttpClient {
@RegisterExtension
static Debugger debugger = Debugger.of(Duration.ofSeconds(2));
static JioHttpClient client =
JioHttpClientBuilder.of(HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(300))
)
.withRetryPolicy(RetryPolicies.incrementalDelay(Duration.ofMillis(10))
.append(RetryPolicies.limitRetries(5)))
.withRetryPredicate(IS_CONNECTION_TIMEOUT.or(IS_CONNECTION_REFUSE))
.get();
static String uri = "https://petstore.swagger.io/v2/%s/%s";
static BiFunction<String, String, Builder> GET =
(entity, id) -> HttpRequest.newBuilder()
.GET()
.uri(URI.create(uri.formatted(entity,
id)));
@Test
public void testGetPetStoreMethods() {
IO<HttpResponse<String>> getPet = client.ofString()
.apply(GET.apply("pet",
"1"));
IO<HttpResponse<String>> getOrder = client.ofString()
.apply(GET.apply("store/order",
"1"));
Result<List<Integer>> result = ListExp.par(getPet,
getOrder)
.map(responses -> responses.stream()
.map(HttpResponse::statusCode)
.toList()
)
.compute();
Assertions.assertTrue(result.isSuccess() && result.getOutput().size() == 2);
}
A possible outcome is:
Started JFR stream for 2,000 sg in Properties
------ httpclient-req -----
| Result: FAILURE
| Exception: java.net.ConnectException: HTTP connect timed out
| Duration: 331,717 ms
| Method: GET
| URI Host: petstore.swagger.io
| URI Path: /v2/pet/1
| Request Counter: 1
| Thread: virtual--1
| Event Start Time: 2024-03-23T11:52:48.919435Z
----------------------
------ httpclient-req -----
| Result: FAILURE
| Exception: java.net.ConnectException: HTTP connect timed out
| Duration: 331,716 ms
| Method: GET
| URI Host: petstore.swagger.io
| URI Path: /v2/store/order/1
| Request Counter: 2
| Thread: virtual--1
| Event Start Time: 2024-03-23T11:52:48.919444083Z
----------------------
------ httpclient-req -----
| Result: CLIENT_ERROR
| Status Code: 404
| Duration: 415,771 ms
| Method: GET
| URI Host: petstore.swagger.io
| URI Path: /v2/store/order/1
| Request Counter: 4
| Thread: virtual--1
| Event Start Time: 2024-03-23T11:52:49.271747042Z
----------------------
------ httpclient-req -----
| Result: SUCCESS
| Status Code: 200
| Duration: 417,817 ms
| Method: GET
| URI Host: petstore.swagger.io
| URI Path: /v2/pet/1
| Request Counter: 3
| Thread: virtual--1
| Event Start Time: 2024-03-23T11:52:49.271737625Z
----------------------
Some errors occurred due to the connection timeout being too short for this particular scenario.
Thankfully, the retry mechanism came to the rescue! Additionally, the HttpExceptions
class
provides numerous predicates to help identify the most common errors that can occur during request
execution. As you can see in the thread field, jio-http client uses virtual threads.
jio-http provides support for client credentials flow OAuth. Here are the possible customizations
for the ClientCredentialsBuilder
builder:
- The request sent to the server to get the access token:
-
accessTokenReq
parameter: A lambda that takes the regular HTTP client and returns the HTTP request to get the token. There is a factory method to build a specific request in the classAccessTokenRequest
. For example one that takes in the client id and secret and anURI
to make the following request:curl -X POST -H "Accept: application/json" \ -H "Authorization: Basic ${Base64(ClientId:ClientSecret)}" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ https://host:port/token
- A function to read the access token from the server response:
getAccessToken
parameter: A lambda that takes the server response and returns the OAuth token. You can use the existing implementationGetAccessToken
, which parses the response into aJsObj
and returns the access token located at the "access_token" field. If the token is not found, the lambda fails with the exceptionAccessTokenNotFound
. TheGetAccessToken
class is a singleton with a private constructor, and you can use theGetAccessToken.DEFAULT
instance for this purpose.
- A predicate that checks if the access token needs to be refreshed:
refreshTokenPredicate
parameter: A predicate that checks the response to determine if the access token needs to be refreshed.
- The authorization header name:
authorizationHeaderName
field: The name of the authorization header, which is set to "Authorization" by default.
- A function to create the authorization header value from the access token:
authorizationHeaderValue
field: A function that takes the access token and returns the authorization header value. By default, it is set to "Bearer ${Access Token}".
You can customize these options when creating an instance of ClientCredentialsBuilder
to configure
the behavior of the OAuth client credentials flow support in your HTTP client. Since you need a
JioHttpClientBuilder instance to create ClientCredentialsBuilder
, you can specify retry policies
and predicates, and of course you can disable the recording of JFR events for every exchange.
The builder returns an instance of ClientCredentialsBuilder
, which is an implementation of
OauthHttpClient
:
package jio.http.client.oauth;
import jio.http.client.HttpLambda;
import jio.http.client.JioHttpClient;
import java.net.http.HttpResponse;
public interface OauthHttpClient extends JioHttpClient {
// since it extends JioHttpClient: ofString() ofBytes() and so on are available as well!
HttpLambda<String> oauthOfString();
HttpLambda<byte[]> oauthOfBytes();
HttpLambda<Void> oauthDiscarding();
<RespBody> HttpLambda<RespBody> oauthBodyHandler(final HttpResponse.BodyHandler<RespBody> handler);
}
The advantage of the oauthXXX
methods is that they handle all token retrieval and refresh requests
on behalf of the client, relieving developers from the burden of implementing these processes.
Here's an illustrative example, where I created an http server and two stubs:
import java.net.URI;
public class TestOauthHttpClient {
@RegisterExtension
Debugger debugger = Debugger.of(Duration.ofSeconds(2));
int PORT = 7777;
Result<HttpServer> server;
{
BodyStub getTokenBodyStub = serverReqCounter -> reqBody -> reqUri -> reqHeaders ->
JsObj.of("access_token", JsStr.of(String.valueOf(serverReqCounter))).toString();
StatusCodeStub getTokenStatusCodeStub = StatusCodeStub.cons(200);
BodyStub handlerBodyStub = serverReqCounter -> body -> uri -> headers ->
serverReqCounter == 2 ? "" : String.valueOf(serverReqCounter);
StatusCodeStub handlerStatusCodeStub =
serverReqCounter -> body -> uri -> headers -> serverReqCounter == 2 ? 401 : 200;
server = HttpServerBuilder.of(Map.of("/token",
PostStub.of(getTokenBodyStub,
getTokenStatusCodeStub
),
"/thanks",
GetStub.of(handlerBodyStub,
handlerStatusCodeStub
)
)
)
.start(PORT);
}
JioHttpClientBuilder clientBuilder =
JioHttpClientBuilder.of(HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(300)))
.withRetryPolicy(RetryPolicies.incrementalDelay(Duration.ofMillis(10))
.append(RetryPolicies.limitRetries(5)))
.withRetryPredicate(IS_CONNECTION_TIMEOUT.or(IS_CONNECTION_REFUSE));
URI tokenUri = URI.create("http://localhost:%s/token".formatted(7777));
OauthHttpClient client =
ClientCredentialsBuilder.of(clientBuilder,
AccessTokenRequest.of("client_id",
"client_secret",
tokenUri
),
GetAccessToken.DEFAULT,
//token in access_token key in a JSON
resp -> resp.statusCode() == 401
// if 401 go for a new token
)
.get();
@Test
public void testOuth() {
URI uri = URI.create("http://localhost:%s/thanks".formatted(PORT));
Assertions.assertEquals(new Success<>("4"),
client.oauthOfString()
.apply(HttpRequest.newBuilder()
.uri(uri))
.compute()
.map(resp -> resp.body())
);
}
}
Let's pick some events from the console. Notice that both the events from the server and the client are printed.
------ httpserver-req -----
| Result: SUCCESS
| Status Code: 200
| Duration: 29,349 ms
| Protocol: HTTP/1.1
| Method: POST
| URI: /token
| Request Counter: 1
| Remote Host Address: localhost
| Remote Host Port: 52625
| Headers: Accept:application/json, Connection:Upgrade, HTTP2-Settings, Http2-settings:AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA, Host:localhost:7777, Upgrade:h2c, User-agent:Java-http-client/21.0.2, Authorization:Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=, Content-type:application/x-www-form-urlencoded, Content-length:29
| Thread: virtual--1
| Event Start Time: 2024-03-23T12:06:20.820267708Z
----------------------
------ httpclient-req -----
| Result: SUCCESS
| Status Code: 200
| Duration: 69,062 ms
| Method: POST
| URI Host: localhost
| URI Path: /token
| Request Counter: 1
| Thread: main
| Event Start Time: 2024-03-23T12:06:20.785406958Z
----------------------
------ httpserver-req -----
| Result: CLIENT_ERROR
| Status Code: 401
| Duration: 552,542 µs
| Protocol: HTTP/1.1
| Method: GET
| URI: /thanks
| Request Counter: 2
| Remote Host Address: localhost
| Remote Host Port: 52625
| Headers: Connection:Upgrade, HTTP2-Settings, Http2-settings:AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA, Host:localhost:7777, User-agent:Java-http-client/21.0.2, Upgrade:h2c, Authorization:Bearer 1
| Thread: virtual--1
| Event Start Time: 2024-03-23T12:06:20.865515083Z
----------------------
------ httpclient-req -----
| Result: CLIENT_ERROR
| Status Code: 401
| Duration: 2,182 ms
| Method: GET
| URI Host: localhost
| URI Path: /thanks
| Request Counter: 2
| Thread: main
| Event Start Time: 2024-03-23T12:06:20.864685333Z
----------------------
------ httpserver-req -----
| Result: SUCCESS
| Status Code: 200
| Duration: 153,750 µs
| Protocol: HTTP/1.1
| Method: POST
| URI: /token
| Request Counter: 3
| Remote Host Address: localhost
| Remote Host Port: 52625
| Headers: Accept:application/json, Connection:Upgrade, HTTP2-Settings, Http2-settings:AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA, Host:localhost:7777, Upgrade:h2c, User-agent:Java-http-client/21.0.2, Authorization:Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=, Content-type:application/x-www-form-urlencoded, Content-length:29
| Thread: virtual--1
| Event Start Time: 2024-03-23T12:06:20.867587833Z
----------------------
------ httpclient-req -----
| Result: SUCCESS
| Status Code: 200
| Duration: 1,090 ms
| Method: POST
| URI Host: localhost
| URI Path: /token
| Request Counter: 3
| Thread: main
| Event Start Time: 2024-03-23T12:06:20.866968542Z
----------------------
------ httpserver-req -----
| Result: SUCCESS
| Status Code: 200
| Duration: 118,459 µs
| Protocol: HTTP/1.1
| Method: GET
| URI: /thanks
| Request Counter: 4
| Remote Host Address: localhost
| Remote Host Port: 52625
| Headers: Connection:Upgrade, HTTP2-Settings, Http2-settings:AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA, Host:localhost:7777, User-agent:Java-http-client/21.0.2, Upgrade:h2c, Authorization:Bearer 3
| Thread: virtual--1
| Event Start Time: 2024-03-23T12:06:20.868637833Z
----------------------
------ httpclient-req -----
| Result: SUCCESS
| Status Code: 200
| Duration: 933,667 µs
| Method: GET
| URI Host: localhost
| URI Path: /thanks
| Request Counter: 4
| Thread: main
| Event Start Time: 2024-03-23T12:06:20.868134875Z
----------------------
In the server event generated during the token request, you can observe the Authorization header
sent by the client, with the value "Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=". If we decode this value
from Base64, we obtain "client_id: client_secret," which corresponds to the exact values we provided
when configuring the ClientCredsBuilder
.
It requires Java 21 or greater
<dependency>
<groupId>com.github.imrafaelmerino</groupId>
<artifactId>jio-http</artifactId>
<version>3.0.0-RC2</version>
</dependency>
To enable debugging Jio provides a JUnit extension called Debugger
. This extension offers the
flexibility to enable and configure debugging for different JIO components, such as HTTP clients and
HTTP servers from jio-http, MongoDB clients from jio-mongodb, and any expression evaluation
(jio-exp).
Here's an example of how to use the Debugger
extension in your JUnit test class:
import jio.test.junit.Debugger;
import org.junit.jupiter.api.extension.RegisterExtension;
public class MyTest {
@RegisterExtension
static Debugger debugger = Debugger.of(Duration.ofSeconds(2));
// Your test methods go here
}
In this example, we've created a Debugger
instance with a duration of 2 seconds. This means that
the test execution will be monitored for debugging events for a duration of 2 seconds.
You can configure debugging by specifying a custom Java Flight Recorder (JFR) configuration
@RegisterExtension
static Debugger debugger = Debugger.of("profile", Duration.ofSeconds(2));
For testing purposes, leaving the configuration parameter empty and using the default one is sufficient.
There are two pre-installed configurations: Default and Profile. The Default configuration has low overhead (about 1%). That's why it works well for continuous profiling. The Profile configuration has overhead about 2% and can be used for more detailed application profiling.
It's essential to set an appropriate debugging duration to avoid unnecessary delays in test execution. Keep in mind that the test execution may not finish until the stream duration has elapsed.
By using the Jio Debugger
extension, you can gain valuable insights into the behavior of the
components in your tests, helping you identify and resolve issues more effectively.
In the realm of testing, it's often necessary to construct stubs that simulate specific behaviors or
responses within your code. To address this need, the StubBuilder
and Gens
classes offer
practical solutions for crafting IO
instances with tailored behaviors designed for testing
scenarios. The StubBuilder
class empowers you to produce stubs for generating IO
instances
through generators. These stubs offer extensive customization and are instrumental for simulating a
wide range of behaviors, spanning from successful executions to failures and controlled delays. The
Gens
class complements this by providing a diverse set of generator methods, each adept at
generating IO instances with unique behaviors.
You can create a StubBuilder
using various methods, depending on your testing needs:
-
ofGen
: Creates a stub using a generator ofIO
effects. IO generators can produce exceptions as normal values, which is useful for testing how your code reacts to errors. -
ofSucGen
: Creates a stub using a generator of values (never fails). It's syntactic sugar for mapping values into effects:Gen<O> gen = ???; //generator of values Gen<IO<O>> io = gen.map(IO::succeed) //generator of effects
You can also configure your stub as follows:
-
withExecutor
: Set an executor to generate values using threads from this executor. This can be useful for controlling the concurrency of value generation. -
withDelays
: Specify delays for the stub using a generator ofDuration
. This can be useful for testing retry policies where retries are executed after waiting for some time.
Some examples:
IO<Integer> x =
StubBuilder.ofGen(Gen.seq(n -> n < 3 ? IO.fail(new RuntimeException(n +" is < 3"))
: IO.succeed(n)
)
)
.withDelays(Gen.seq(n -> n < 3 ? Duration.ofSeconds(1) : Duration.ZERO))
.withExecutor(Executors.newVirtualThreadPerTaskExecutor())
.get();
x.compute(); // fails after 1 second
x.compute(); // fails after 1 second
x.compute(); // computes 3 immediatly
x.compute(); // computes 4 immediatly
With these tools, you can easily create stubs and generators for testing your code with various scenarios, behaviors, and timing conditions. This flexibility makes it easier to ensure the robustness of your code in different situations.
Happy testing!
The ClockStub
class provides a simple way to create clock stubs for controlling time-related
behavior in your applications during testing and development. Clock stubs allow you to simulate
various time scenarios, making it easier to test time-dependent functionalities in your code.
In testing and development scenarios, controlling time can be crucial when working with applications
that have time-sensitive functionality. The ClockStub
class is designed to create clock stubs that
mimic the behavior of actual clocks. These clock stubs give you the ability to manipulate
time-related behavior to ensure your application behaves as expected under various time conditions.
You can create a clock stub using one of the following methods:
The fromReference
static factory method allows you to create a clock stub that starts ticking from
the provided reference time. This method is useful when you want the clock to behave as if it
started at a specific instant.
Instant reference = Instant.parse("2023-01-15T12:00:00.00Z");
Clock clock = ClockStub.fromReference(reference);
The fromSeqCalls
static factory method enables you to create a clock stub where you can control
the ticking time based on the number of calls made to the clock. This method provides dynamic time
simulation, allowing you to simulate time progression based on your specific requirements.
// Simulate time progressing by 1 hour with each call
Function<Integer, Long> timeFunction = n -> Instant.now()
.plus(Duration.ofHours(n))
.toEpochMilli();
Clock clock = ClockStub.fromCalls(timeFunction);
jio-http provides dedicated stubs for various HTTP methods, allowing you to define the HttpHandler
of an HTTP server with precision. Here's a list of available HTTP method stubs:
- DeleteStub: Simulates the DELETE HTTP method.
- GetStub: Simulates the GET HTTP method.
- OptionsStub: Simulates the OPTIONS HTTP method.
- PatchStub: Simulates the PATCH HTTP method.
- PostStub: Simulates the POST HTTP method.
- PutStub: Simulates the PUT HTTP method.
Each of these HTTP method stubs is constructed from three core functions: BodyStub
,
StatusCodeStub
, and HeadersStub
. These functions each accept four input parameters with the
following signature:
IntFunction<Function<InputStream, Function<URI, Function<Headers, R>>>
Here's what each parameter represents:
-
The first integer parameter is the request number received by the server. In Jio-HTTP, the server maintains an internal counter that increments with each incoming request.
-
The
InputStream
parameter stands for the input of the request body. -
The
URI
parameter represents the request's URI, including all query and path parameters. -
The
Headers
parameter captures the request headers.
The type R
can be a string in the case of BodyStub
, an integer for StatusCodeStub
, and headers
for HeadersStub
.
There are some predefined BodyStub
options:
interface BodyStub {
static BodyStub gen(Gen<String> gen);
static BodyStub cons(String body);
static BodyStub consAfter(String body, Duration delay);
}
You can create your own BodyStub
by implementing a function like this:
BodyStub myStub = reqCounter -> bodyStream -> uri -> reqHeaders -> {
String responseBody = ???;
// Your custom stub implementation here
return responseBody;
}
Predefined StatusCodeStub
options include:
interface StatusCodeStub {
static StatusCodeStub cons(int code);
}
Just like BodyStub
, you can craft your own StatusCodeStub
by creating a function like this:
StatusCodeStub myStub = reqCounter -> bodyStream -> uri -> reqHeaders -> {
int statusCodeResponse = ???;
// Your custom stub implementation here
return statusCodeResponse;
}
For HeadersStub
, there are some predefined options:
interface HeadersStub {
HeadersStub EMPTY;
static HeadersStub cons(Map<String, List<String>> map);
}
You can also define a custom HeadersStub
function. For instance, here's one that returns the exact
headers of the request:
HeadersStub bounceHeaders = reqCounter -> bodyStream -> uri -> reqHeaders -> reqHeaders;
Now, let's put it all together with a complete example:
HttpHandler saludate = GetStub.of(BodyStub.consAfter("hi", Duration.ofSeconds(1)),
StatusCodeStub.cons(200),
HeadersStub.EMPTY
);
JsObjGen personGen = ???;
HttpHandler create = PostStub.of(BodyStub.gen(personGen),
StatusCodeStub.gen(Combinators.oneOf(200, 201)),
HeadersStub.EMPTY
);
Result<HttpServer> server =
HttpServerBuilder.of(Map.of("/saludate", saludate))
.startAtRandom("localhost", 8000, 9000);
Creating servers and adding stubs for testing purposes with jio-http is remarkably straightforward. Here's why:
-
Abundance of Stub Types: Jio-HTTP offers a variety of pre-defined stubs, such as
DeleteStub
,GetStub
,OptionsStub
, and more. You can choose the stub that fits your testing scenario. -
Customizable Behavior: With
BodyStub
,StatusCodeStub
, andHeadersStub
, you have fine-grained control over your stubs. These stubs allow you to tailor the responses, status codes, and headers, as needed. -
Simple Stub Creation: You can create your own custom stubs with just a simple function implementation. This makes it easy to simulate specific behaviors or responses that your test cases require.
-
Ease of Testing: The ability to add these stubs to your
HttpServer
facilitates efficient and reliable testing. You can simulate various scenarios, including successes, failures, and delays. -
Integration with Generators: The stubs can be integrated with generators to further diversify your test cases. For instance, you can use
JsObjGen
to generate complex JSON objects for testing.
In summary, jio-http simplifies the process of creating HTTP servers and adding stubs for testing purposes. Its user-friendly design and flexibility make it a valuable tool for ensuring the robustness and reliability of your HTTP-based applications.
Consider a seemingly straightforward function, medium, designed to calculate the average of two integers.
BiFunction<Integer, Integer, Integer> medium = (a, b) -> (a + b) / 2;
At first glance, you might think it's bug-free – after all, it's just a sum and a division. However, in the world of software development, assumptions like this can be misleading. Bugs can lurk even in the simplest-looking code.
Let's start by creating a generator to produce inputs, which are pairs of integers (a
and b
)
with the constraint that a
is less than b
. We can achieve this using the
java-fun library:
Gen<Pair<Integer, Integer>> intervalGen =
PairGen.of(IntGen.biased(0),
IntGen.biased(0))
.suchThat(pair -> pair.first() < pair.second());
Next, we need to define a meaningful property that the medium
function must satisfy. For example,
we can ensure that the medium
value always stays within the bounds defined by a
and b
:
Function<Pair<Integer, Integer>, TestResult> mediumMustFallsInInterval =
pair -> {
var a = pair.first();
var b = pair.second();
var mean = medium.apply(a, b);
if (mean < a) return TestFailure.reason("mean lower than a");
if (mean > b) return TestFailure.reason("mean greater than b");
return TestResult.SUCCESS;
};
Finally, we can create the Property
using the PropBuilder
:
public class TestProperties {
static BiFunction<Integer, Integer, Integer> medium = (a, b) -> (a + b) / 2;
static Property<Pair<Integer, Integer>> mediumProperty =
PropertyBuilder.of("mediumMustFallsInInterval",
intervalGen,
mediumMustFallsInInterval
)
.withDescription("medium must fall between bounds")
.get();
@Test
public void testMedium() {
Resport report = mediumProperty.check();
report.assertAllSuccess();
}
}
Upon executing the test, a summary of the Report
is displayed in the console, providing essential
information about the test run. Here's what you'll see:
Property medium executed 1000 times at 2023-10-15T20:23:52.879607+02:00 for 7,434 ms:
! KO, passed 661 tests (66.1%) and 339 tests (33.9%) ended with a failure.
Some generated values that caused a failure:
(2147483647, 2147483647), (1533547426, 2147483647), (1430393771, 1952366707), (361315880, 2147483647), (1810849822, 2147483647), (127, 2147483647), (1571969759, 1917332428), (32767, 2147483647), (571967758, 2131558478), (1725653506, 2147483647), (650519903, 1591944271), (720273507, 1909947616), (2147483647, 2147483647), (127, 2147483647), (32767, 2147483647), (1878197279, 2147483647), (1074371667, 1961393980), (1560183752, 2147483647), (1104900558, 1836571965), (771721558, 2147483647)
Additionally, JUnit provides a more detailed report. If there are any issues or failures during testing, a message from JUnit containing the full report is printed. This comprehensive report is invaluable for identifying and addressing any problems in your code.
org.opentest4j.AssertionFailedError: Property medium with failures. JSON report: {"exceptions":[],"max_time":796000,"failures":[{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.880368Z","seq_number":1,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.882210Z","seq_number":2,"input":"(1533547426, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.882431Z","seq_number":9,"input":"(1430393771, 1952366707)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.882449Z","seq_number":10,"input":"(361315880, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.882472Z","seq_number":11,"input":"(1810849822, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.882499Z","seq_number":12,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.882698Z","seq_number":17,"input":"(1571969759, 1917332428)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.882766Z","seq_number":21,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.882795Z","seq_number":23,"input":"(571967758, 2131558478)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.882884Z","seq_number":29,"input":"(1725653506, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.882913Z","seq_number":31,"input":"(650519903, 1591944271)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.882971Z","seq_number":35,"input":"(720273507, 1909947616)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.882987Z","seq_number":36,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883034Z","seq_number":39,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883049Z","seq_number":40,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883114Z","seq_number":43,"input":"(1878197279, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883176Z","seq_number":48,"input":"(1074371667, 1961393980)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883205Z","seq_number":50,"input":"(1560183752, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883310Z","seq_number":59,"input":"(1104900558, 1836571965)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883427Z","seq_number":68,"input":"(771721558, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883443Z","seq_number":69,"input":"(731930322, 1719814114)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883464Z","seq_number":70,"input":"(2029797654, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883478Z","seq_number":71,"input":"(1394973094, 1707960802)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883578Z","seq_number":79,"input":"(567176572, 1738145432)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883705Z","seq_number":90,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883720Z","seq_number":91,"input":"(1423813192, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883750Z","seq_number":93,"input":"(1554444177, 2035900421)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883766Z","seq_number":94,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883878Z","seq_number":102,"input":"(1048619250, 2091571032)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883963Z","seq_number":107,"input":"(1343494247, 2119894620)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.883980Z","seq_number":108,"input":"(1780942969, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884084Z","seq_number":116,"input":"(706379444, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884097Z","seq_number":117,"input":"(674724176, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884111Z","seq_number":118,"input":"(366376675, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884127Z","seq_number":119,"input":"(1152380205, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884139Z","seq_number":120,"input":"(1055540429, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884152Z","seq_number":121,"input":"(734494005, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884302Z","seq_number":132,"input":"(909065340, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884354Z","seq_number":137,"input":"(1513674269, 1559743529)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884366Z","seq_number":138,"input":"(1620658962, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884391Z","seq_number":140,"input":"(1735048665, 1744964508)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884427Z","seq_number":141,"input":"(2017221897, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884477Z","seq_number":142,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884601Z","seq_number":152,"input":"(436667400, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884637Z","seq_number":155,"input":"(505112404, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884660Z","seq_number":157,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884674Z","seq_number":158,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884728Z","seq_number":164,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884741Z","seq_number":165,"input":"(218907640, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884765Z","seq_number":167,"input":"(2077759235, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884797Z","seq_number":170,"input":"(2004905038, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884839Z","seq_number":172,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884851Z","seq_number":173,"input":"(382907835, 1848300132)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.884952Z","seq_number":183,"input":"(1414453865, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.885072Z","seq_number":195,"input":"(622296040, 1799782576)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.885087Z","seq_number":196,"input":"(1079508732, 1613762528)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.885132Z","seq_number":201,"input":"(1017853265, 1645045517)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.885175Z","seq_number":205,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.885189Z","seq_number":206,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.885214Z","seq_number":208,"input":"(808300701, 1508868391)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.885237Z","seq_number":210,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.885249Z","seq_number":211,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.885261Z","seq_number":212,"input":"(1280816822, 1838911650)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.885391Z","seq_number":214,"input":"(1176996537, 1552966381)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.885658Z","seq_number":220,"input":"(534655680, 1682705416)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.885757Z","seq_number":223,"input":"(840735221, 1865377218)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.885837Z","seq_number":226,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886003Z","seq_number":232,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886071Z","seq_number":235,"input":"(2110462688, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886112Z","seq_number":240,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886126Z","seq_number":241,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886185Z","seq_number":248,"input":"(330895231, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886243Z","seq_number":255,"input":"(2006720972, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886320Z","seq_number":259,"input":"(1039274967, 2096915296)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886336Z","seq_number":260,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886363Z","seq_number":263,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886374Z","seq_number":264,"input":"(1771073553, 1909337551)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886429Z","seq_number":271,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886440Z","seq_number":272,"input":"(1393438628, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886472Z","seq_number":275,"input":"(1920536036, 1977100767)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886484Z","seq_number":276,"input":"(1560328013, 1829719404)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886513Z","seq_number":279,"input":"(1868735802, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886523Z","seq_number":280,"input":"(2091377121, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886548Z","seq_number":283,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886565Z","seq_number":285,"input":"(1911190342, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886580Z","seq_number":286,"input":"(1423433283, 2118621215)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886608Z","seq_number":289,"input":"(1156816267, 1891419274)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886618Z","seq_number":290,"input":"(2084155731, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886629Z","seq_number":291,"input":"(688685438, 2082711398)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886647Z","seq_number":293,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886666Z","seq_number":295,"input":"(710722643, 1943259540)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886685Z","seq_number":297,"input":"(1989883084, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886718Z","seq_number":301,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886737Z","seq_number":303,"input":"(952934441, 1264833469)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886747Z","seq_number":304,"input":"(1479506487, 2144218067)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886761Z","seq_number":305,"input":"(995701763, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886780Z","seq_number":307,"input":"(1065422693, 1141060350)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886797Z","seq_number":309,"input":"(838787512, 1570227631)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886837Z","seq_number":313,"input":"(1922938597, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886847Z","seq_number":314,"input":"(1384459064, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886879Z","seq_number":318,"input":"(2103714568, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886977Z","seq_number":326,"input":"(629883470, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.886995Z","seq_number":328,"input":"(1222774129, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.887188Z","seq_number":333,"input":"(436704288, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.887316Z","seq_number":338,"input":"(1416215288, 1528151612)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.887365Z","seq_number":340,"input":"(1944946961, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.887514Z","seq_number":345,"input":"(1382945771, 1736161785)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.887758Z","seq_number":348,"input":"(1036530737, 1130341056)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.887929Z","seq_number":353,"input":"(1117591028, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.887995Z","seq_number":354,"input":"(1051688088, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888037Z","seq_number":357,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888068Z","seq_number":361,"input":"(868669640, 1832432901)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888083Z","seq_number":363,"input":"(882929322, 1696248740)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888166Z","seq_number":375,"input":"(1033032313, 1470736814)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888192Z","seq_number":379,"input":"(113260360, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888216Z","seq_number":382,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888261Z","seq_number":387,"input":"(855281174, 1466139241)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888275Z","seq_number":389,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888310Z","seq_number":395,"input":"(1519370977, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888328Z","seq_number":397,"input":"(1976125086, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888359Z","seq_number":402,"input":"(986813649, 1391802775)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888389Z","seq_number":407,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888416Z","seq_number":411,"input":"(970877341, 1954125251)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888425Z","seq_number":412,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888455Z","seq_number":417,"input":"(805566230, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888492Z","seq_number":421,"input":"(882645466, 1948381384)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888504Z","seq_number":422,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888531Z","seq_number":425,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888625Z","seq_number":427,"input":"(1299405979, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888647Z","seq_number":429,"input":"(982556170, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888725Z","seq_number":443,"input":"(1740380870, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888734Z","seq_number":444,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888743Z","seq_number":445,"input":"(344239145, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888759Z","seq_number":447,"input":"(906361824, 1942743697)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888769Z","seq_number":448,"input":"(425551720, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888792Z","seq_number":452,"input":"(1155656966, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888803Z","seq_number":453,"input":"(907954938, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888823Z","seq_number":456,"input":"(518529371, 1855173894)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888859Z","seq_number":462,"input":"(836410708, 1957909592)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888874Z","seq_number":464,"input":"(903880823, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888882Z","seq_number":465,"input":"(616202131, 2133953153)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888922Z","seq_number":470,"input":"(923678298, 1806218916)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888937Z","seq_number":472,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888946Z","seq_number":473,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.888954Z","seq_number":474,"input":"(189462362, 2098996680)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889015Z","seq_number":485,"input":"(1051312913, 1499092845)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889055Z","seq_number":490,"input":"(805804501, 1945212418)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889064Z","seq_number":491,"input":"(277242231, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889080Z","seq_number":493,"input":"(1317055049, 1998339480)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889102Z","seq_number":496,"input":"(482842086, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889111Z","seq_number":497,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889119Z","seq_number":498,"input":"(444387494, 2146872001)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889128Z","seq_number":499,"input":"(1617230533, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889137Z","seq_number":500,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889154Z","seq_number":501,"input":"(91280385, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889163Z","seq_number":502,"input":"(1454427132, 1661157669)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889183Z","seq_number":504,"input":"(269008726, 2124929106)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889197Z","seq_number":506,"input":"(1307767957, 1391173566)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889214Z","seq_number":509,"input":"(893786159, 1964223850)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889226Z","seq_number":510,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889490Z","seq_number":513,"input":"(1166711722, 1251978122)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889539Z","seq_number":514,"input":"(1043382459, 1707616638)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889569Z","seq_number":515,"input":"(270411969, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889812Z","seq_number":517,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889908Z","seq_number":519,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889945Z","seq_number":523,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889969Z","seq_number":527,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889984Z","seq_number":529,"input":"(1324763053, 1736659860)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.889996Z","seq_number":530,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890040Z","seq_number":538,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890054Z","seq_number":540,"input":"(1135649012, 1834245677)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890069Z","seq_number":541,"input":"(602837450, 2141544313)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890090Z","seq_number":544,"input":"(41896800, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890101Z","seq_number":545,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890115Z","seq_number":547,"input":"(37227507, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890129Z","seq_number":549,"input":"(1440018474, 1958760257)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890145Z","seq_number":551,"input":"(1697124112, 1944740028)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890183Z","seq_number":558,"input":"(1295204224, 2137668739)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890211Z","seq_number":562,"input":"(1550938271, 1932209568)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890243Z","seq_number":567,"input":"(1738722138, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890251Z","seq_number":568,"input":"(713621152, 1780866493)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890265Z","seq_number":570,"input":"(69642088, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890273Z","seq_number":571,"input":"(551912087, 2052962274)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890282Z","seq_number":572,"input":"(1214726683, 1504893506)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890292Z","seq_number":573,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890301Z","seq_number":574,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890319Z","seq_number":577,"input":"(578265987, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890337Z","seq_number":580,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890351Z","seq_number":582,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890375Z","seq_number":586,"input":"(1066328829, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890407Z","seq_number":591,"input":"(285273422, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890416Z","seq_number":592,"input":"(1413157748, 2098424603)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890430Z","seq_number":594,"input":"(407555756, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890446Z","seq_number":596,"input":"(851447433, 2092038634)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890459Z","seq_number":598,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890517Z","seq_number":609,"input":"(773270128, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890540Z","seq_number":613,"input":"(2139444449, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890554Z","seq_number":615,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890562Z","seq_number":616,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890570Z","seq_number":617,"input":"(1620792408, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890580Z","seq_number":618,"input":"(1583038973, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890599Z","seq_number":621,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890617Z","seq_number":624,"input":"(1797938107, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890630Z","seq_number":626,"input":"(39190679, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890639Z","seq_number":627,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890655Z","seq_number":629,"input":"(1535588156, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890664Z","seq_number":630,"input":"(707958319, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890673Z","seq_number":631,"input":"(1016134140, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890682Z","seq_number":632,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890690Z","seq_number":633,"input":"(718701334, 2015075069)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890709Z","seq_number":636,"input":"(639752449, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890722Z","seq_number":638,"input":"(1026759501, 1816073653)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890743Z","seq_number":640,"input":"(755208510, 1630134993)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890776Z","seq_number":645,"input":"(1087073212, 2000648031)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890796Z","seq_number":648,"input":"(2107938535, 2132599319)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890819Z","seq_number":652,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890827Z","seq_number":653,"input":"(380412771, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890836Z","seq_number":654,"input":"(587368522, 2033125475)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890880Z","seq_number":662,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890889Z","seq_number":663,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890918Z","seq_number":668,"input":"(1949618185, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890931Z","seq_number":670,"input":"(956826446, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890946Z","seq_number":672,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890977Z","seq_number":678,"input":"(942928282, 1371882480)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890990Z","seq_number":680,"input":"(894268799, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.890999Z","seq_number":681,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891012Z","seq_number":682,"input":"(378486568, 1861852690)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891042Z","seq_number":686,"input":"(874459943, 2093323680)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891051Z","seq_number":687,"input":"(436802747, 1924781651)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891059Z","seq_number":688,"input":"(1790563692, 1935297081)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891067Z","seq_number":689,"input":"(1348194523, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891104Z","seq_number":692,"input":"(858483285, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891129Z","seq_number":696,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891138Z","seq_number":697,"input":"(1214300513, 1226918032)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891151Z","seq_number":699,"input":"(683765884, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891163Z","seq_number":701,"input":"(545407906, 1736824987)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891172Z","seq_number":702,"input":"(900934545, 1702720800)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891199Z","seq_number":707,"input":"(1192003054, 1288261712)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891207Z","seq_number":708,"input":"(565991852, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891242Z","seq_number":715,"input":"(738007098, 1425430498)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891263Z","seq_number":719,"input":"(1306676240, 1491381364)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891275Z","seq_number":721,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891283Z","seq_number":722,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891291Z","seq_number":723,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891304Z","seq_number":725,"input":"(568574277, 1973911480)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891313Z","seq_number":726,"input":"(1866109956, 1953496580)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891321Z","seq_number":727,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891337Z","seq_number":730,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891369Z","seq_number":736,"input":"(684314658, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891386Z","seq_number":739,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891402Z","seq_number":742,"input":"(993182651, 1783676486)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891427Z","seq_number":747,"input":"(1486492496, 1940960434)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891450Z","seq_number":751,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891457Z","seq_number":752,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891465Z","seq_number":753,"input":"(32839816, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891504Z","seq_number":761,"input":"(1213027194, 1323414570)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891547Z","seq_number":766,"input":"(774507716, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891557Z","seq_number":767,"input":"(1016203779, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891574Z","seq_number":768,"input":"(263026299, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891609Z","seq_number":770,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891636Z","seq_number":773,"input":"(534408511, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891644Z","seq_number":774,"input":"(780687568, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891678Z","seq_number":781,"input":"(436114393, 1846777799)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891692Z","seq_number":783,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891713Z","seq_number":787,"input":"(214238791, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891747Z","seq_number":794,"input":"(865896003, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.891969Z","seq_number":801,"input":"(1267488726, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892010Z","seq_number":802,"input":"(429573759, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892085Z","seq_number":804,"input":"(1482568316, 1576959952)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892187Z","seq_number":807,"input":"(844380207, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892305Z","seq_number":815,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892319Z","seq_number":816,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892351Z","seq_number":822,"input":"(1104582403, 1922248364)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892367Z","seq_number":824,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892384Z","seq_number":827,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892414Z","seq_number":833,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892421Z","seq_number":834,"input":"(1451241867, 1624341585)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892432Z","seq_number":835,"input":"(1090749970, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892452Z","seq_number":839,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892464Z","seq_number":841,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892472Z","seq_number":842,"input":"(111986732, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892480Z","seq_number":843,"input":"(1505837398, 1967307454)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892494Z","seq_number":845,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892502Z","seq_number":846,"input":"(1260787896, 2037136929)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892515Z","seq_number":848,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892544Z","seq_number":854,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892572Z","seq_number":857,"input":"(1529152543, 2121232889)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892584Z","seq_number":859,"input":"(167160142, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892598Z","seq_number":861,"input":"(936522204, 1966244605)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892610Z","seq_number":863,"input":"(953060657, 1213407250)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892808Z","seq_number":876,"input":"(1663866546, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892824Z","seq_number":877,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892833Z","seq_number":878,"input":"(610223755, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892850Z","seq_number":880,"input":"(1622538860, 1982476399)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892865Z","seq_number":883,"input":"(1095492927, 2024002429)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892877Z","seq_number":885,"input":"(1228064703, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892913Z","seq_number":891,"input":"(612987569, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892927Z","seq_number":893,"input":"(1377208541, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.892945Z","seq_number":896,"input":"(832044347, 2061995995)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893010Z","seq_number":900,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893030Z","seq_number":904,"input":"(1400023269, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893043Z","seq_number":906,"input":"(2144941239, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893069Z","seq_number":912,"input":"(1766078309, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893077Z","seq_number":913,"input":"(1395540578, 2015357750)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893084Z","seq_number":914,"input":"(1022812679, 2101600553)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893094Z","seq_number":915,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893112Z","seq_number":919,"input":"(863738927, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893124Z","seq_number":921,"input":"(747538073, 1940070970)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893156Z","seq_number":927,"input":"(1499271332, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893167Z","seq_number":928,"input":"(1629081796, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893182Z","seq_number":931,"input":"(768103063, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893197Z","seq_number":934,"input":"(1494904277, 1924084632)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893205Z","seq_number":935,"input":"(1723488605, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893218Z","seq_number":937,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893239Z","seq_number":940,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893260Z","seq_number":945,"input":"(488587306, 2023394003)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893268Z","seq_number":946,"input":"(598474836, 1724197832)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893275Z","seq_number":947,"input":"(1164446800, 1583907093)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893292Z","seq_number":950,"input":"(101347090, 2139479838)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893317Z","seq_number":956,"input":"(1082871153, 1531510322)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893326Z","seq_number":957,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893346Z","seq_number":961,"input":"(1513907009, 2071928523)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893354Z","seq_number":962,"input":"(717055627, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893362Z","seq_number":963,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893381Z","seq_number":967,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893405Z","seq_number":972,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893423Z","seq_number":976,"input":"(32767, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893434Z","seq_number":978,"input":"(1604943254, 1956899451)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893445Z","seq_number":980,"input":"(752835236, 1740776594)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893461Z","seq_number":983,"input":"(536848588, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893479Z","seq_number":987,"input":"(328953301, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893487Z","seq_number":988,"input":"(2041725148, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893504Z","seq_number":991,"input":"(915426351, 1384635069)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893512Z","seq_number":992,"input":"(310305665, 2043297547)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893525Z","seq_number":994,"input":"(85861090, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893532Z","seq_number":995,"input":"(1262801644, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893540Z","seq_number":996,"input":"(2147483647, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893547Z","seq_number":997,"input":"(127, 2147483647)","seed":-8287889810276844822,"tags":""}},{"reason":"mean lower than a","context":{"start":"2023-10-15T18:23:52.893554Z","seq_number":998,"input":"(396828458, 2147483647)","seed":-8287889810276844822,"tags":""}}],"start_time":"2023-10-15T18:23:52.879607Z","name":"medium","n_tests":1000,"end_time":"2023-10-15T18:23:52.893568Z","n_exceptions":0,"accumulative_time":7434000,"avg_time":7434,"min_time":1000,"n_failures":339,"description":""} ==>
You might not anticipate a failure in such a straightforward function, but don't be too concerned. It's worth noting that this issue has persisted for a considerable period in various programming languages, even affecting binary search algorithms. For more insights into this matter, I recommend reading the detailed account provided by Joshua Bloch in his informative post: Google Research Blog.
It's important to mention that, as of now, jio-test doesn't include a feature called "shrinking,"
which is a technique used to minimize the failing example to its simplest form. However, the
framework does offer methods to help you identify the reasons behind test failures. One useful
practice is to classify the generated data. In the example you've shared, it's evident that larger
numbers, specifically 2147483647 (which corresponds to Integer.MAX_VALUE
), are causing failures
more frequently. This is because biased integer generators tend to produce this value with a higher
probability.
To address this, you can classify the generated data by adding tags to the pairs (a,b)
based on
their characteristics:
- "both" for pairs where both values are greater than
Integer.MAX_VALUE / 2
. - "none" for pairs where neither value exceeds
Integer.MAX_VALUE / 2
. - "one" for pairs where only one element of the pair surpasses `Integer.MAX_VALUE / 2."
This classification allows you to gain better insights into the nature of test failures and helps you focus on specific scenarios where problems are more likely to occur. This can be incredibly powerful for spotting and addressing bugs that might be extremely challenging to reproduce otherwise.
static Property<Pair<Integer, Integer>> mediumProperty =
PropBuilder.of("medium",
gen,
prop
)
.withClassifiers(Map.of("both",
p -> p.first() > Integer.MAX_VALUE / 2
&& p.second() > Integer.MAX_VALUE / 2,
"none",
p -> p.first() < Integer.MAX_VALUE / 2
&& p.second() < Integer.MAX_VALUE / 2
),
"one"
)
.withDescription("medium must fall between bounds")
.get();
Now, if we examine the output, you'll notice the following:
Property medium executed 1000 times at 2023-10-15T20:28:11.580574+02:00 for 19,361 ms:
! KO, passed 674 tests (67,4 %) and 326 (32,6 %) ended with a failure.
Some generated values that caused a failure:
((32767, 2147483647), one),((2147483647, 2147483647), both),((127, 2147483647), one),((32767, 2147483647), one),((1367738157, 2147483647), both),((32767, 2147483647), one),((2147483647, 2147483647), both),((1365877821, 1519800076), both),((929386296, 2147483647), one),((1663592320, 2147483647), both),((687170227, 2147483647), one),((127, 2147483647), one),((127, 2147483647), one),((32767, 2147483647), one),((686789583, 1494899307), one),((261853601, 2066273042), one),((23902795, 2147483647), one),((2147483647, 2147483647), both),((1746060008, 2147483647), both),((32767, 2147483647), one)
1000 values collected in total:
44,4 % one
40,7 % none
14,9 % both
From this output, you can see that there were no test failures when the "none" tag was applied to
the generated values. However, when at least one element, either a
or b
, is greater than
Integer.MAX_VALUE / 2
, it may result in an overflow. If you read Joshua Bloch's article, you'll
recognize that the correct way to calculate the medium is as follows:
static BiFunction<Integer, Integer, Integer> medium = (a,b) -> a + (b-a)/2;
// Alternatively
static BiFunction<Integer, Integer, Integer> medium = (a,b) -> (a+b) >>> 1;
With this corrected implementation, there are no test failures:
Property medium executed 1000 times at 2023-10-15T20:34:15.031869+02:00 for 15,734 ms:
+ OK, passed 1000 tests.
1000 values collected in total:
45.8 % one
39.7 % none
14.5 % both
This change ensures that the average of two integers is calculated correctly, and all tests pass without any failures.
In the realm of property-based testing, a "property" is a fundamental concept representing a condition or invariant that a piece of code or a program should always satisfy, without failing under any circumstances. These properties serve as essential checks to ensure the correctness and reliability of software.
A "Property" is represented in jio-test by a class called Property
. This class encapsulates a
specific property that you want to test. The primary components of a Property
include:
- Name: A descriptive label that identifies the property being tested.
- Data Generator (
Gen<O>
): A generator that produces pseudorandom data of typeO
. This data is used to feed the property tests. - Testing Function or Lambda: A function or lambda that tests the property, taking the generated
data as input and producing a
TestResult
, which indicates whether the property holds or fails.
Let's look at the key elements that constitute a Property
:
The name provides a descriptive label for the property being tested. It should succinctly describe the behavior or condition that the property is checking.
The data generator, represented by a java.fun.Gen<O>
object, is responsible for creating
pseudorandom data. This data serves as input for property testing. The quality and diversity of the
generated data play a crucial role in the effectiveness of property-based testing. In jio-test, I
utilize the data generators provided by the java-fun
library. These generators can create a wide
range of data types, from simple values like integers and strings to more complex data structures.
For exploratory purposes, it's often recommended to use biased generators. Biased generators assign
higher probabilities to values that are known to produce more bugs or exceptional cases. For
example, consider values like zero, empty strings, blank strings, Integer.MAX_VALUE
, and more.
Biased generators can help you uncover hidden issues in your code by focusing on these critical
cases during testing.
The testing function is a critical part of the Property
. It is the code or logic that evaluates
whether the property holds true for the generated data. There are two primary ways to define the
testing function:
-
Function (
Function<O, TestResult>
): This form of the testing function takes the generated data (O
) and returns aTestResult
. TheTestResult
indicates the success or failure of the property test. -
Lambda (
Lambda<O, TestResult>
): Certain properties may be defined using Lambdas instead of functions, especially when they involve IO operations where exceptions can occur. In such cases, these exceptions do not halt the property's execution but are considered and reported in the final test report.
Creating a property involves creating a PropertyBuilder
instance to build the property you want to
test:
-
PropertyBuilder.of(String name, Gen<O> gen, Function<O, TestResult> property)
: Use this method when your testing function takes generated data (O
) as input. This form of the testing function is useful when no additional configuration is needed for the property test. -
PropertyBuilder.ofLambda(String name, Gen<O> gen, Lambda<O, TestResult> property)
: When your testing function is a lambda that only requires generated data (O
), this method is the appropriate choice.
With the builder instance you can further customize the property by specifying the number of test executions, providing a description, setting classifiers, and more.
Exploratory testing can benefit from classifying generated data into different categories.
Classifiers are created using the
withClassifiers(Map<String, Predicate<O>> classifiers, String defaultTag)
method of the builder.
These classifiers can help you group data into various categories based on specific criteria and
identify tags assigned to values that produce errors.
To gather data for debugging and analysis, you can enable data collection using the
withCollector()
method of the builder. This feature allows you to collect data about the generated
values, helping you identify patterns or trends in the generated data.
After executing a property, you obtain a Report
containing detailed information about the test
execution. The Report
class has the following fields and their meanings (which can be serialized
into a JSON format):
name
: The name of the property.n_tests
: The number of executed tests.n_failures
: The number of test failures.n_exceptions
: The number of exceptions raised during testing.description
: The description of the property.start_time
: The start time of test execution.end_time
: The end time of test execution.avg_time
: The average execution time in milliseconds.max_time
: The maximum execution time in milliseconds.min_time
: The minimum execution time in milliseconds.accumulative_time
: The accumulative execution time in milliseconds.failures
: An array of failure contexts, each containing a reason and context.exceptions
: An array of exception contexts, each containing a message, type, and stack trace.
Exceptions or failures has an associated context (Context
class) with the following fields:
start
: Represents the instant when a test starts. This timestamp is essential for tracking the timing of test execution.seed
: Signifies the seed for random data generation. This seed is crucial for reproducing bugs since pseudo-random generators always produce the same sequence of values when fed with the same seed. It ensures the ability to recreate the exact data sequence.generatedSeqNumber
: Denotes the sequence number for data generation. This number helps in understanding the order of data generation and identifying patterns or issues in the data.input
: Represents the input data of the test. This field provides insight into the specific data that was used during a test execution. It's important for analyzing the test's behavior and identifying problematic inputs.tags
: Contains a string that can be used to categorize the input data, based on classifiers or conditions. This information helps in identifying and categorizing specific inputs and associating them with potential issues.
These fields collectively provide valuable context information for each test execution. In
particular, the seed
and generatedSeqNumber
fields are vital for reproducing bugs because they
allow you to precisely recreate the sequence of random data that led to a specific issue.
By examining the Report
class, you can gain valuable insights into the performance of your
property-based tests, identify failures, and pinpoint exceptions.
The Report
class defines several methods that are used for assertions in JUnit tests. Here's a
breakdown of these methods and how they are used for assertions:
-
assertAllSuccess()
: This method is used to assert that all tests associated with the report have passed successfully. If there are any failures or exceptions, this assertion will fail. It checks if bothgetExceptions()
andgetFailures()
lists are empty. If they are not empty, it raises an assertion failure with a message indicating the presence of failures and exceptions. -
assertNoFailures()
: This method is used to assert that there are no failures associated with the report. If there are any failures, this assertion will fail. It checks if thegetFailures()
list is empty. If it's not empty, it raises an assertion failure with a message indicating the presence of failures. -
assertThat(Predicate<Report> condition, Supplier<String> message)
: This is a custom assertion method that allows for a user-defined condition to be evaluated against the report. Thecondition
parameter is a predicate that is applied to the report, and themessage
parameter is a supplier that provides a message to be used if the condition fails. If the condition is not satisfied, an assertion failure is raised with the message supplied by themessage
supplier.
These methods are used in unit tests to verify the correctness of property-based tests and to check if the reported results match the expected outcomes. They help ensure that the properties defined in property-based tests hold true and that the tests are executed without failures or exceptions.
The results of property testing can be exported to a file using the withExportPath(Path path)
method. This file contains a JSON representation of the test report, which is useful for sharing and
archiving test results.
The Property
class is part of a property-based testing framework in Java. It represents a property
of a piece of code that should always be true. The class allows you to define, customize, execute,
and report on property tests.
Here, I'll explain two methods within the Property
class: repeatSeq
and repeatPar
.
This method returns a new Property
instance that represents the property and specifies that it
should be executed sequentially for the specified number of times (n
).
n
(int): The number of sequential executions for the property.
Use Case:
- This method is useful when you want to run the property test multiple times in a sequential manner, one after the other. It's suitable for situations where you don't require parallel execution and want to ensure the property holds true consistently.
This method returns a new Property
instance that represents the property and specifies that it
should be executed in parallel for the specified number of times (n
). Parallel execution involves
running the property tests concurrently using multiple threads from the common ForkJoinPool.
n
(int): The number of parallel executions for the property.
Use Case:
- When you want to take advantage of parallelism to speed up the property-based testing process, you can use this method. It's beneficial for running multiple property tests concurrently, which can be more time-efficient.
- Provides the ability to conduct property tests in parallel, executing them a specified number of times. Parallel property testing can be a valuable tool for detecting bugs related to concurrency, as it runs multiple tests concurrently using multiple threads. This can help uncover issues that might not be apparent in sequential testing, making it particularly useful for identifying and addressing concurrency-related defects in your code.
In both cases, the original Property
instance is not modified, and a new instance is returned with
the specified execution mode.
These methods allow you to customize how property tests are executed, whether sequentially or in parallel, depending on your testing requirements and constraints. They provide flexibility in adapting the testing strategy to the specific needs of your codebase or project.
It requires Java 21 or greater
<dependency>
<groupId>com.github.imrafaelmerino</groupId>
<artifactId>jio-test</artifactId>
<version>3.0.0-RC2</version>
</dependency>
jio-mongodb
leverages the persistent JSON from
json-values and the set of codecs defined in
mongo-values, making it an efficient solution for
MongoDB operations.
It uses Virtual threads from Java 21.
jio-mongodb is composed of a set of MongoLambdas to perform operations against the database. With
Lambdas, you can benefit from all the powerful features of jio-exp
and jio-test
. jio-mongodb
is an example of how you can make any API under the sun jio-friendly, unleashing the full potential
of your code.
The MongoLambda
interface in Jio provides a versatile way to define MongoDB operations that
produce IO effects within a MongoDB client session. These lambdas can be used with or without
transactions, offering flexibility in working with MongoDB databases.
The MongoLambda<I, O>
interface represents a function that takes an input of type I
and produces
an IO effect of type O
within a MongoDB client session.
interface MongoLambda<I, O> extends BiLambda<ClientSession, I, O> {
Lambda<I, O> standalone();
<B> MongoLambda<I, B> then(MongoLambda<O, B> other) { }
<B> MongoLambda<I, B> then(Lambda<O, B> other) {}
<C> MongoLambda<I, C> map(Function<O, C> fn) {}
}
Key points:
-
Using transactions with MongoDB is optional. You can create a
MongoLambda
that produces effects independently of any transaction by calling thestandalone()
method. This is useful when you want to perform operations that don't require transactional behavior. -
One powerful feature of
MongoLambda
is the ability to chain operations together. You can chain this interface with anotherMongoLambda
to create a newMongoLambda
. The result is a sequence of operations executed within the same MongoDB client session, with the output of one operation becoming the input to the next one. -
You can also chain a
MongoLambda
with a non-transactionalLambda
to create a newMongoLambda
. This allows you to mix transactional and non-transactional operations while maintaining session continuity. -
The
MongoLambda
interface lets you map the output of an operation using a provided function. You can create a newMongoLambda
where the mapping function is applied to the output of the original operation.
This allows you to create flexible and expressive MongoDB operations with transactional or non-transactional behavior as required.
In the upcoming sections, we'll explore a variety of pre-defined MongoLambda
functions that are
available in the jio-mongodb
.
To get started, you need a MongoClient
, a DatabaseBuilder
, and finally a CollectionBuilder
that provides access to a MongoDB collection. Below is an example of how to create both:
String connectionStr = "mongodb://localhost:27017,localhost:27018,localhost:27019/?replicaSet=rs0"
MongoClient mongoClient = MongoClientBuilder.DEFAULT.build(connectionStr);
String databaseName = "test";
DatabaseBuilder database = DatabaseBuilder.of(mongoClient,databaseName);
String collectionName = "Data";
CollectionBuilder collection = CollectionBuilder.of(database,collectionName);
The MongoClientBuilder
class is for creating MongoDB client instances with custom configurations.
This class provides flexibility in building MongoDB client instances and allows you to specify your
own connection string and settings functions. The default instance, DEFAULT
is a pre-configured
builder with the default settings and codecs from
mongo-values to work with JSON data from the
json-values library.
Now that you have a CollectionBuilder
, you can perform various operations on it.
The key class for creating queries and specifying options for find operations is FindBuilder
.
Here's how to perform find operations:
CollectionBuilder collection = ???;
JsObj query = ???;
Lambda<FindBuilder, JsObj> find = FindOne.of(collection).standalone();
FindBuilder builder = FindBuilder.of(query);
IO<JsObj> io = find.apply(builder);
CollectionBuilder collection = ???;
JsObj query = ???;
Lambda<FindBuilder, FindIterable<JsObj>>find = FindAll.of(collection).standalone();
FindBuilder builder = FindBuilder.of(query);
IO<FindIterable<JsObj>> xs = find.apply(builder);
//map the output with predefined converters
IO<List<JsObj>> ioList = xs.map(Converters::toListOfJsObj);
IO<JsArray> ioArray = xs.map(Converters::toJsArray);
JsObj doc = ???;
Lambda<JsObj, InsertOneResult> insert = InsertOne.of(collection).standalone();
IO<String> x = insert.apply(doc).map(Converters::toHexId);
List<JsObj> docs = ???;
Lambda<List<JsObj>, InsertManyResult> insert = InsertMany.of(collection).standalone();
IO<List<String>> xs = insert.apply(docs).map(Converters::toListOfHexIds);
JsObj query = ???;
Lambda<JsObj, DeleteResult> deleteOne = DeleteOne.of(collection).standalone();
IO<JsObj> x = deleteOne.apply(query).map(Converters::toJsObj);
JsObj query = ???;
Lambda<JsObj, DeleteResult> deleteMany = DeleteMany.of(collection).standalone();
IO<JsObj> x = deleteMany.apply(query).map(Converters::toJsObj);
JsObj query = ???;
JsObj update = ???;
Lambda<QueryUpdate, UpdateResult> updateOne = UpdateOne.of(collection).standalone();
IO<JsObj> x = updateOne.apply(new QueryUpdate(query,update)).map(Converters::toJsObj);
JsObj query = ???;
JsObj update = ???;
Lambda<QueryUpdate, UpdateResult> updateOne = UpdateMany.of(collection).standalone();
IO<JsObj> x = updateOne.apply(new QueryUpdate(query,update)).map(Converters::toJsObj);
JsObj query = ???;
JsObj newDoc = ???;
Lambda<QueryReplace, UpdateResult> replaceOne = ReplaceOne.of(collection).standalone();
IO<JsObj> x = replaceOne.apply(new QueryReplace(query,newDoc)).map(Converters::toJsObj);
JsObj query = ???;
Lambda<JsObj, Long> count = Count.of(collection).standalone();
IO<Long> io = count.apply(query);
JsObj query = ???;
JsObj update = ???;
Lambda<QueryUpdate, JsObj> findOneUpdate = FindOneAndUpdate.of(collection).standalone();
IO<JsObj> x = findOneUpdate.apply(new QueryUpdate(query,update));
JsObj query = ???;
JsObj newDoc = ???;
Lambda<QueryReplace, JsObj> findOneReplace = FindOneAndReplace.of(collection).standalone();
IO<JsObj> x = findOneReplace.apply(new QueryReplace(query,newDoc));
JsObj query = ???;
Lambda<JsObj, JsObj> findOneDelete = FindOneAndDelete.of(collection).standalone();
IO<JsObj> x = findOneDelete.apply(query);
List<Bson> stages = ???;
Lambda<List<Bson>, AggregateIterable<JsObj>> aggregate = Aggregate.of(collection).standalone();
IO<List<JsObj>> x = aggregate.apply(stages).map(Converters::toListOfJsObj);
//or using JsObj as an input instead of Bson
Lambda<List<JsObj>, List<JsObj>> y =
list -> {
List<Bson> bsons = list.stream().map(Converters::toBson).toList();
return aggregate.apply(bsons).map(Converters::toListOfJsObj);
};
You can set up a change stream on a MongoDB collection to monitor changes using the Watcher
class:
CollectionBuilder builder = CollectionBuilder.of(database,collectionName);
Consumer<ChangeStreamIterable<JsObj>> consumer = iter -> { ??? };
Watcher.of(consumer).accept(builder);
You can also configure various options for MongoDB operations using the withOptions
method
available for some operations. Options allow you to specify things like the write concern, bypass
document validation, and more.
For instance, if you want to configure custom UpdateOptions
for an UpdateOne
operation:
UpdateOptions customOptions = new UpdateOptions().upsert(true) // Example option
Lambda<QueryUpdate, UpdateResult> updateOne =
UpdateOne.of(collection)
.withOptions(customOptions)
.standalone();
By passing custom options, you can fine-tune the behavior of your MongoDB operations to match your specific use case.
Up to this point, we have been using the standalone method provided by MongoLambdas for operations that do not require transactions. Now, let's explore how to create and work with transactions in jio-mongodb.
jio-mongodb provides a convenient way to work with MongoDB transactions. Transactions in MongoDB allow you to perform multiple operations within a single session, ensuring that either all the operations are executed or none of them, providing a consistent view of your data.
The TxBuilder
class is used to create transactions in a MongoDB client session. It offers options
to configure transaction settings and create transaction instances.
TxBuilder
methods:
of(ClientSessionBuilder sessionBuilder)
: Creates a newTxBuilder
instance with the provided session builder.withTxOptions(TransactionOptions transactionOptions)
: Sets the transaction options for the transactions created with this builder.build(MongoLambda<I, O> mongoLambda)
: Builds a transactionTx
with the specified MongoDB Lambda function and transaction options.
The Tx
class represents a MongoDB transaction that can be applied within a MongoDB client session.
This class ensures that the transaction is executed consistently and provides methods for defining
and applying the transaction.
MongoDB's sessions are not multithreaded. Only one thread should operate within a MongoDB session at a time to avoid errors like "Only servers in a sharded cluster can start a new transaction at the active transaction number."
The following example demonstrates how to use jio-mongodb to insert a list of JSON documents within a MongoDB transaction:
public class TestTx {
@RegisterExtension
static Debugger debugger = Debugger.of(Duration.ofSeconds(2));
String connection = "mongodb://localhost:27017,localhost:27018,localhost:27019/?replicaSet=rs0";
MongoClient mongoClient =
MongoClientBuilder.DEFAULT
.build(connection);
ClientSessionBuilder session = ClientSessionBuilder.of(mongoClient);
DatabaseBuilder testDb = DatabaseBuilder.of(mongoClient, "test");
CollectionBuilder personCol = CollectionBuilder.of(testDb, "Person");
@Test
public void test() {
MongoLambda<JsObj, String> insertOne = InsertOne.of(personCol)
.map(Converters::toHexId);
MongoLambda<List<JsObj>, List<String>> insertAll =
(session, jsons) ->
jsons.stream()
.map(json -> insertOne.apply(session, json))
.collect(ListExp.seqCollector());
Tx tx = TxBuilder.of(session).build(insertAll);
Result<List<String>> result = tx.apply(List.of(JsObj.of("a", JsInt.of(0)),
JsObj.of("b", JsInt.of(1))
)
)
.compute();
Assertions.assertTrue(result.isSucess() && result.getOutput().size()==2);
}
}
It's worth highlighting the advantages of using the jio-exp
API to create a MongoLambda
. In the
previous example, we used a ListExp.seq
to insert all the JSON documents sequentially. As
mentioned earlier, using the ListExp.parCollector()
to insert all the JSON documents in parallel
is not feasible due to MongoDB sessions not being multi-threaded.
On the other hand, when using TxBuilder
and Tx
, you don't have to worry about the intricacies of
committing or rolling back the transaction in the event of an error, or explicitly closing the
session. All of these essential operations are automatically handled for you, making your code more
robust and convenient.
The MongoExceptionFun
utility class provides predicates to handle common exceptions:
IS_READ_TIMEOUT
:
- Description: This predicate checks if the given Throwable is an instance of
MongoSocketReadTimeoutException
. It returns true if the exception is a read timeout exception and false otherwise. Read timeout exceptions typically occur when a read operation (e.g., reading data from the database) takes longer than the specified timeout.
IS_CONNECTION_TIMEOUT
:
- Description: This predicate checks if the given Throwable is an instance of
MongoTimeoutException
. It returns true if the exception is a connection timeout exception and false otherwise. Connection timeout exceptions usually happen when there's a timeout while attempting to establish a connection to the MongoDB server.
Here's an example of how to use these predicates for resilient applications:
JsObj query = ???;
var builder = FindBuilder.of(query);
IO<JsObj> io = FindOne.of(collection)
.apply(builder)
.retry(MongoExceptionFun.IS_CONNECTION_TIMEOUT,
RetryPolicies.limitRetries(3)
);
By default, all operations create an event when finished and send it to the Java Flight Recorder
(JFR) system. You can disable this behavior using the withoutRecordedEvents
method.
Register the Junit Debugger extension from jio-test
in your tests:
@RegisterExtension
static Debugger debugger = Debugger.of(Duration.ofSeconds(2));
This extension enables you to see printed-out events like the following:
event: mongodb, op: INSERT_ONE, duration: 37,800 ms, result: SUCCESS
thread: ForkJoinPool.commonPool-worker-23, event-start-time: 2023-10-17T19:10:30.86140575+02:00
event: mongodb, op: INSERT_ONE, duration: 37,416 ms, result: SUCCESS
thread: ForkJoinPool.commonPool-worker-27, event-start-time: 2023-10-17T19:10:30.861923625+02:00
event: mongodb, op: FIND, duration: 1,362 ms, result: SUCCESS
thread: ForkJoinPool.commonPool-worker-18, event-start-time: 2023-10-17T19:10:30.899902583+02:00
It requires Java 21 or greater
<dependency>
<groupId>com.github.imrafaelmerino</groupId>
<artifactId>jio-mongodb</artifactId>
<version>3.0.0-RC2</version>
</dependency>
The jio-cli
library allows users to interact easily with complex systems through a CLI. It
provides built-in commands for common tasks such as encoding/decoding, file operations, JSON
manipulation, and more. Additionally, users can extend the CLI by adding custom commands tailored to
their specific needs. This flexibility makes jio-cli
an excellent choice for experimenting,
automating workflows, and integrating with other systems.
- Extensive Built-in Commands: Includes a wide range of commands for handling JSON data, encoding/decoding, file operations, and more.
- Extensible: Easily add custom and complex commands to fit your specific requirements
- Interactive Programs: Create interactive programs to compose JSON objects and more.
- Command History: View and re-execute previously executed commands.
- Session File: If the directory is specified in the configuration, a session file is created to store all the commands typed by the user and their respective outputs for later reference and analysis.
- help: Provides descriptions of other commands.
- exit: Shuts down the console.
- list: Lists all available commands, optionally filtered by a specified prefix.
- history: Lists executed commands and their positions in the command history.
- last: Executes the last command one or more times, optionally with a repetition interval or duration.
- clear: Clears the console screen.
- echo: Prints a message to the console.
- var-get: Reads the content of a specified variable.
- var-set: Stores a value into the specified variable.
- var-clear: Removes a specified variable from the current state.
- file-read: Reads the content from a specified file.
- file-dump: Writes the content of the output variable into a specified file, appending if the file exists.
- script: Executes a script file containing multiple commands.
- base64-encode: Encodes a string into Base64 format.
- base64-decode: Decodes a Base64 encoded string into its original form.
- url-encode: Translates a string into application/x-www-form-urlencoded format.
- json-get: Retrieves the value at the specified path from the JSON stored in the 'output' variable.
- json-pairs: Returns the list of path/value pairs of the JSON stored in the 'output' variable.
- json-pretty: Pretty-prints the JSON stored in the 'output' variable.
- json-console: Executes interactive programs that allow the user to compose a JSON object given a provided spec.
The output of the last executed command is stored in a special variable called output
. Some
commands may not update the output
variable.
To start using jio-cli
, create a Console
instance with your custom commands and call the eval
method with a configuration JSON object.
import jio.cli.*;
import java.util.List;
public class MyCLI {
public static void main(String[] args) {
List<Command> customCommands = List.of(
// Add your custom commands here
);
Console console = new Console(customCommands);
console.eval(JsObj.empty()); // Pass your configuration JSON here
}
}
The configuration JSON can include settings for aliases, welcome messages, session file directory, and colors for different types of messages. Here is an example configuration:
{
"conf": {
"aliases": {
"var-get": "vg",
"var-set": "vs",
"file-read": "fr",
"file-dump": "fd"
},
"welcome_message": "Welcome to jio-cli!",
"session_file_dir": "/path/to/session/files",
"colors": {
"error": "\u001B[0;31m",
"result": "\u001B[0;34m",
"prompt": "\u001B[0;32m"
}
}
}
- help
Provides descriptions of other commands.
Usage:
help [command_name]
- exit
Shuts down the console.
Usage:
exit
- list
Lists all available commands, optionally filtered by a specified prefix.
Usage:
list [prefix]
- history
Lists executed commands and their positions in the command history.
Usage:
history [positions | interval]
- last
Executes the last command one or more times, optionally with a repetition interval or duration.
Usage:
last [count]
last every <interval>
last every <interval> for <duration>
- clear
Clears the console screen.
Usage:
clear
- echo
Prints a message to the console.
Usage:
echo [text]
- var-get
Reads the content of a specified variable.
Usage:
var-get [variable_name]
- var-set
Stores a value into the specified variable.
Usage:
var-set [name] [value]
- var-clear
Removes a specified variable from the current state.
Usage:
var-clear [variable_name]
- file-read
Reads the content from a specified file.
Usage:
file-read [path_to_file]
- file-dump
Writes the content of the output variable into a specified file, appending if the file exists.
Usage:
file-dump [path_to_file]
- script
Executes a script file containing multiple commands.
Usage:
script [path_to_script]
- base64-encode
Encodes a string into Base64 format.
Usage:
base64-encode [text]
- base64-decode
Decodes a Base64 encoded string into its original form.
Usage:
base64-decode [encoded_string]
- url-encode
Translates a string into application/x-www-form-urlencoded format.
Usage:
url-encode [text]
- json-get
Retrieves the value at the specified path from the JSON stored in the 'output' variable.
Usage:
json-get [path]
- json-pairs
Returns the list of path/value pairs of the JSON stored in the 'output' variable.
Usage:
json-pairs [substring]
- json-pretty*
Pretty-prints the JSON stored in the 'output' variable.
Usage:
json-pretty
- json-console
Executes interactive programs that allow the user to compose a JSON object given a provided spec.
Usage:
json-console [command_name]
jio-cli
provides a powerful and flexible way to create command-line interfaces for various
applications. With its extensive built-in commands and the ability to add custom commands, jio-cli
is suitable for a wide range of use cases, from simple automation tasks to complex interactive
programs.
documentation is on progress