SmallRye Mutiny

repository·main·Indexed 21 days ago

https://github.com/smallrye/smallrye-mutiny

An intuitive, event-driven reactive programming library for Java designed to simplify building asynchronous, non-blocking applications. It provides a navigable API for building reactive processing pipelines and is based on the Reactive Streams protocol. The library includes features for branching logic in Uni and Multi pipelines, broadcasting events to multiple subscribers, and a dedicated Math library for stream computations via the io.smallrye.math.Math class.

Tokens
35.3K
Snippets
118
Records
163
Agent score
75%

What's inside SmallRye Mutiny

  1. Overview of Mutiny Reactive Programming

    main

    Mutiny is an intuitive, event-driven reactive programming library for Java designed to handle asynchronous operations and non-blocking I/O. It focuses on creating readable processing pipelines by observing and reacting to events.

    Key characteristics include:

    • Event-Driven: The core design revolves around observing and reacting to events.
    • Navigable API: The API is designed to be explicit, guiding developers toward the correct operators.
    • Non-blocking I/O: Ideal for composing declarative operations, transforming data, enforcing progress, and recovering from failures in asynchronous environments.
    • Interoperability: Based on the Reactive Streams protocol and Java Flow, allowing integration with other reactive libraries. It also provides built-in converters for popular libraries.
    • Ecosystem Integration: It is the native reactive API for Quarkus and provides bindings for Eclipse Vert.x clients, though it can be used independently in any Java application.
  2. What is Mutiny?

    main

    Mutiny is a modern, event-driven reactive programming library for Java designed to simplify asynchronous development. It provides a navigable and explicit API to build reactive processing pipelines, making it suitable for microservices, data streaming, event processing, and non-blocking I/O applications.

    Key characteristics include:

    • Event-Driven: Focuses on observing and reacting to events within processing pipelines.
    • Navigable API: Designed to guide developers toward the correct operators via explicit API design.
    • Non-Blocking I/O: Optimized for composing operations, transforming data, and recovering from failures in asynchronous environments.
    • Interoperability: Based on the Reactive Streams protocol, allowing integration with other libraries. It includes built-in converters for Kotlin and other popular reactive libraries.
  3. Transform Multi items using Merge vs Concatenate

    main

    When transforming items from an upstream Multi into new streams (either Uni or Multi), you must decide how to handle the order of the resulting items. Mutiny provides two primary strategies:

    1. Merging: Does not preserve the original order. It emits items from the produced streams as soon as they are available. This allows for interleaved responses and potential concurrency.
    2. Concatenating: Maintains the original order. It ensures that the streams produced for each item are concatenated sequentially, waiting for one to complete before starting the next.

    API Methods

    • For Uni transformations:
      • onItem().transformToUniAndMerge(Function<T, Uni<O>>)
      • onItem().transformToUniAndConcatenate(Function<T, Uni<O>>)
    • For Multi transformations:
      • onItem().transformToMultiAndMerge(Function<T, Multi<O>>)
      • onItem().transformToMultiAndConcatenate(Function<T, Multi<O>>)
  4. Understand Mutiny Context passing

    main

    Mutiny provides a subscriber-provided context to allow operators in a reactive pipeline to share implicit data (such as correlation identifiers or security tokens) without needing to wrap every item in a tuple.

    Key Characteristics

    • Implicit Data: Data is carried alongside items through the pipeline.
    • Thread-safe: Context objects are thread-safe.
    • Storage: A Context is a key/value in-memory storage. It can be created empty, from a Java Map, or from a sequence of key/value pairs.
    • Best Practices:
      • Use contexts for transient data used for networked I/O (e.g., correlation IDs, tokens).
      • Do not use contexts as general-purpose data structures for large amounts of data or frequent updates.
  5. Null support in Uni vs Multi

    main

    It is critical to distinguish between Uni and Multi regarding null support:

    • Uni: Supports emitting null as an item. This is commonly used for Uni<Void> scenarios.
    • Multi: Does not support null items. Emitting null in a Multi would break compatibility with the Reactive Streams protocol.

    Best Practice: While Uni supports null, you should avoid emitting null items whenever possible, except when working with Uni<Void>.

  6. Understanding the importance of asynchronous programming in distributed systems

    main

    In modern distributed systems (Cloud, IoT, microservices), communications are inherently asynchronous and unreliable. Traditional synchronous development models often use a 'one thread per request' approach, which leads to several issues in I/O intensive applications:

    • Thread Blocking: Worker threads block while waiting for network responses, requiring complex watchdog/timeout logic to handle failures.
    • Resource Costs: Each thread consumes memory and increases CPU overhead due to context switching.
    • Scalability Limits: Increasing concurrency requires more threads, which limits deployment density and increases cloud infrastructure costs.

    To build efficient, scalable distributed applications, you should adopt an asynchronous development model using non-blocking I/O. This allows you to handle I/O interactions without requiring additional threads.

    Critical Constraint: When using non-blocking I/O, you must never block the I/O thread.

  7. How Mutiny pipeline construction works

    main

    Mutiny uses a builder API where each stage of the pipeline returns a new object. The API is not strictly fluent in the sense that you cannot assume the original object is mutated; instead, you must capture the new object returned by each transformation stage.

    If you append a stage to a Uni but do not use the resulting object, the appended stage will not be part of the executed pipeline.

    Incorrect usage (ignoring the returned object):

    // This only executes the first part; the transformations are lost
    Uni<String> uni = Uni.createFrom().item("hello");
    uni.onItem().transform(i -> i + " mutiny"); // The result of this is discarded
    uni.subscribe().with(System.out::println);

    Correct usage (chaining or re-assigning):

    // The transformations are part of the final object being subscribed to
    Uni.createFrom().item("hello")
        .onItem().transform(i -> i + " mutiny")
        .subscribe().with(System.out::println);
  8. Understand the differences between Uni and CompletionStage

    main

    While CompletionStage and CompletableFuture are used for asynchronous actions, they differ fundamentally from Mutiny's Uni in two ways:

    1. Eagerness vs. Laziness: CompletionStage is eager. When a method returns one, the operation has already been triggered. Uni is lazy; the operation only triggers upon subscription.
    2. Caching vs. Re-triggering: CompletionStage caches its outcome. Every retrieval returns the same result. A Uni can re-trigger the operation on every subscription, potentially producing different results.

    Tip: If you want a Uni to behave like a CompletionStage by caching its result, use Uni.memoize().indefinitely().

  9. Understand Mutiny's scheduling behavior

    main

    Mutiny does not automatically make your code asynchronous. It does not perform any scheduling work by default, except when using the emitOn or runSubscriptionOn operators.

    Operators like join do not schedule tasks to run concurrently. Instead, join subscribes to each Uni and collects results as they arrive. The actual execution thread depends on the underlying implementation of the Uni. If the underlying operation (e.g., a fetch method) uses asynchronous I/O, you will see concurrency. If the operation emits a value immediately upon subscription, the execution will be sequential.

  10. How Mutiny Math Operators emit values

    main

    Mutiny Math operators produce streams (Multi) that emit new values whenever the computed value changes based on the items received from the upstream.

    Because these operators often compute values based on the entire history of the stream, if you only want the final result after the upstream completes, you should use .collect().last() to retrieve the final computed value.

    String max = Multi.createFrom().items("e", "b", "c", "f", "g", "e")
                    .plug(Math.max())
                    .collect().last()
                    .await().indefinitely();
  11. Flatten batches of items using disjoint()

    main

    The disjoint() operator is used to transform a stream of collections (like lists) into a stream of individual elements. It takes each item from the emitted collections and passes them downstream one by one.

    Example Flow:

    1. Multi emits [a, b, c] $\rightarrow$ disjoint emits a, then b, then c.
    2. Multi emits [d, e] $\rightarrow$ disjoint emits d, then e.
  12. Compare emitOn and runSubscriptionOn

    main

    When deciding between emitOn and runSubscriptionOn, consider the direction and type of event you want to influence:

    OperatorTarget Event TypeDirection/Effect
    emitOnItems, Completion, FailureReplays upstream events downstream on the specified executor.
    runSubscriptionOnSubscription (the subscribe call)Executes the upstream subscription process on the specified executor.

    Execution Flow Summary:

    • emitOn: Upstream emits on Caller thread $\rightarrow$ emitOn replays on Executor thread $\rightarrow$ Subscriber receives on Executor thread.
    • runSubscriptionOn: Subscriber calls subscribe on Caller thread $\rightarrow$ runSubscriptionOn triggers upstream subscribe on Executor thread.