Skip to content

Mosaic 0.7.0

Latest

Choose a tag to compare

@github-actions github-actions released this 06 Oct 03:37
· 3 commits to main since this release
08aa4fe

Mosaic 0.7.0 release notes

Mosaic 0.7.0 gives each request an explicit execution lifetime, distinguishes
borrowed Canvas values from owned resources, and isolates keyed failures. This
guide covers upgrading from 0.6.0; see compatibility before
updating runtime dependencies and optional analysis tooling.

Scoped request execution

Use Canvas.withMosaic as the canonical request entry point. Keep application
services in a long-lived Canvas, bind request input in a child Canvas, and run the
complete handler inside one scoped block:

import org.buildmosaic.core.injection.withMosaic

// In a suspending request handler; RequestKey and ResponseTile are your declarations.
applicationCanvas.withLayer {
  instance(RequestKey, value)
}.withMosaic {
  compose(ResponseTile)
}

Tile producer work inherits the calling coroutine's dispatcher and context and
is scoped to that request. Producers are supervised: cancelling a consumer's
coroutine stops that consumer's wait without automatically cancelling shared
producer work. Cancelling the request cancels its unfinished producers. Leaving
withMosaic, normally or exceptionally, cancels unfinished speculative work and
waits for producer and attached-child cleanup.

Canvas.create() remains available but is deprecated at WARNING level.
Migration normally means moving the complete handler invocation into
requestCanvas.withMosaic { handler(this) }, rather than returning or retaining
a Mosaic between requests. Each block creates a fresh Mosaic and cache.

withMosaic does not close its Canvas. If the request child constructs owned
AutoCloseable resources, surround composition with use as shown in the
resource guide.

Canvas instance bindings

single { ... } constructs a Canvas-owned value. Canvas closes successfully
constructed local AutoCloseable values on close or construction rollback.
instance(existing) registers an already-existing, externally owned value:
Canvas must never close it, including when construction fails or is cancelled.
The external owner remains responsible for its lifetime.

The three forms mirror single. Each pair below shows alternatives for the same
binding: choose the owned constructor or the borrowed instance, not both.

single<Client> { Client() }
instance(existingClient)

single<Client>("primary") { Client() }
instance<Client>("primary", existingClient)

single(ClientKey) { Client() }
instance(ClientKey, existingClient)

Use instance<Service>(existingService) when binding an interface or supertype;
instance("primary", existingClient) infers the type when that is the intended
binding. Positional CanvasKey bindings also work for String values, such as
instance(OrderKey, orderId). Each key can be registered only once per layer.
Instance bindings are available to constructors through paint and use the same
typed-key, qualifier, local-first lookup, and parent-fallback rules.
A child does not close parent-owned resources. Replace single { existingService }
with instance(existingService) when the application or framework owns the service.

MultiTile outcomes

Within one Mosaic, the same MultiTile retains one terminal outcome per equal
key
, including failures. Repeating a composition is not a retry.

  • A present map entry is successful even when its value is null and V is nullable.
  • A requested key omitted from the returned map fails that key with
    NoSuchElementException; present sibling values remain retained.
  • A bulk provider invocation failure fails unfinished keys in that physical
    invocation, preserving outcomes already published.
  • perKeyTile failures are isolated to that key.
  • chunkedMultiTile failures are isolated to that physical chunk's unfinished keys.
  • Successful sibling keys remain cached and are reused by later callers.

compose(tile, keys) is strict: it throws if a requested key fails, rather than
returning a partial map. composeAsync(tile, keys) returns Map<K, Deferred<V>>,
so callers can await and handle each key separately. Pending uncached keys can
coalesce before execution; exact physical batch boundaries are not guaranteed.
Request cancellation still cancels unfinished work across keys. See
MultiTile for the current contract.

Deferred ownership

Every composeAsync overload returns Mosaic-owned shared Deferreds. Callers
may await them freely. If a consumer no longer needs a result, cancel that
consumer's coroutine or wait. Directly cancelling a returned shared Deferred can
affect other consumers waiting for the same Tile or key. No wrapper or new result
abstraction is required.

Optional analysis

Analysis recognizes supported withMosaic execution and instance bindings.
Proven synchronous Tile result cycles produce
MOSAIC_CYCLIC_TILE_DEPENDENCY: a hard correctness error that cannot be configured
or suppressed. MOSAIC_RECURSIVE_TILE and MOSAIC_RECURSIVE_MULTITILE both default
to ERROR, but can be configured to ERROR, WARNING, or OFF.
Kotlin @Suppress is available for supported intentional local recursion.

Deferred-await linkage and MultiTile key equality/termination reasoning remain
outside the proof boundary. A configurable recursive finding is not proof of a
same-Mosaic cyclic result dependency. Use the canonical
analysis configuration reference
for rule definitions, suppression scopes, enforcement, and project restrictions.

Metadata changes from format 3 / analysis-contract-2 to format 4 /
analysis-contract-3. Old dependency summaries are incompatible and must be
regenerated with matching 0.7 analysis tooling. The resource path
META-INF/mosaic-analysis/v1/summary.json is unchanged; compatibility comes from
its header. Keep the Gradle and compiler plugins at the same Mosaic version.

Updated examples

The Spring, Ktor, and Micronaut examples demonstrate delegated,
named Tiles, Canvas-supplied application services, instance(...), child request
Canvases, and one withMosaic lifetime per request. Ktor uses coroutine routes;
Spring and Micronaut use suspending controllers. Synchronous application setup
uses runBlocking only to construct the application Canvas.

Runtime cost

The six changed scoped-drain and fully cached MultiTile fixtures have fresh
0.7.0 baselines, commands, and measurement boundaries.
Scoped diamond/sibling requests measured 2.743/4.036 µs/op; fully cached reads
measured 0.042–10.912 µs/op for 1–512 keys. Allocation was measured separately.
These fixtures are not directly comparable with their old versions; historical
0.5/0.6 tables and application measurements are unchanged.

Compatibility

  • Runtime source/API: withMosaic and instance are additions;
    Canvas.create() is a WARNING deprecation, not a removal. Existing public
    runtime method signatures remain available. Builds treating warnings as errors
    may need to migrate deprecated calls.
  • JVM binary surface: the inspected Kotlin-public core API descriptors are retained.
    The internal MultiTile provider representation changed, including its mangled
    JVM getter; code calling internal members from Java or reflection can break.
    This is not a general binary compatibility guarantee for 0.x. Recompile and
    test consumers when upgrading, especially code using inlined DSLs.
  • Runtime behavior: scoped execution changes cancellation and cleanup;
    MultiTile now retains per-key outcomes and successful siblings across provider
    failures, and treats present nullable values as success. Review recovery,
    cancellation, and resource ownership in your application.
  • Runtime requirements: Kotlin/JVM, Java 17 or later, and Kotlin consumer
    compiler 2.3.0 or later remain the documented boundary. Dependencies remain
    stdlib/kotlin-test 2.4.20, coroutines core/test 1.11.0, and OpenTelemetry API 1.66.0.
  • Optional analysis: format/semantic metadata is incompatible with 0.6
    summaries. Only Kotlin compiler and Gradle plugin 2.4.20 are supported,
    within the documented pure main Kotlin/JVM boundary. This restriction does
    not apply to runtime-only consumers.

For upgrades from earlier releases, also read the
0.6.0 Canvas migration. The runtime BOM
continues to align core, test, and OpenTelemetry; analysis tooling remains outside
it. See runtime and analysis requirements.

[0.7.0] - 2026-10-05

Added

  • Canvas.withMosaic for one request lifetime: producer work inherits the calling
    coroutine; every exit cancels unfinished work and waits for cleanup.
  • Typed, qualified, and CanvasKey instance(...) bindings for externally owned
    values that Canvas never closes, including on construction rollback. Qualifiers
    precede values, mirroring single<T>("primary") { ... }.
  • Analysis recognition of supported withMosaic and instance usage, proven
    Tile-cycle detection, and configurable/suppressible recursive Tile/MultiTile
    policy with ERROR/WARNING/OFF severities. Proven cycles cannot be disabled.

Changed

  • MultiTile retains one terminal outcome per equal key. Per-key and chunk
    failures preserve successful siblings; strict bulk compose still throws on
    any requested failure. Returned Deferreds remain Mosaic-owned shared work.
  • Analysis metadata moves to format 4 / analysis-contract-3; dependency
    summaries must be rebuilt with matching 0.7 tooling. Optional analysis still
    supports exactly Kotlin compiler/Gradle plugin 2.4.20.
  • Spring, Ktor, and Micronaut examples use named Tiles, Canvas-supplied services,
    borrowed instances, child request Canvases, and suspending scoped handlers.

Deprecated

  • Canvas.create() at WARNING level. Move the complete handler invocation into
    withMosaic rather than retaining a Mosaic across requests.

Fixed

  • Present nullable MultiTile values succeed; omitted requested keys fail
    individually without discarding present values.
  • Per-key and chunk provider failures no longer discard successful sibling keys.
  • Tile body and attached-child failures use the same containment boundary with
    and without execution observation.