Skip to content

Releases: ThatScalaGuy/fs2-nats

v0.4.0

Choose a tag to compare

@ThatScalaGuy ThatScalaGuy released this 25 Aug 15:46

The headline of this release is fs2.nats.micro — typed NATS micro services
implementing ADR-32.
Alongside it, another round of hot-path work landed across core pub/sub, JetStream,
and Object Store, and 0.4 starts a new binary-compatibility series.

Micro services (fs2.nats.micro)

  • An endpoint is described once as a plain Rpc[P, I, E, O] value — subject pattern
    with typed captures, request payload, typed error, response payload — and interpreted
    on both sides: rpc.handle(...) inside a NatsService, Micro(client).call(rpc)(...)
    from the caller. Definitions can live in a shared module (#57)
  • Subject patterns are checked at compile time: pattern["orders.*"].bind[OrderId]
    rejects malformed literals, computes the capture arity, and binds captures through
    TokenCodecs (String, Int, Long, UUID, or your own)
  • Payloads via Payload.empty / bytes / string / json[A] (jsoniter-scala),
    with optional schema text published in the endpoint INFO metadata
  • Errors are typed end to end: handlers return F[Either[E, O]], errors travel as
    ADR-32 error headers and come back as Left(E) at the caller — transport failures
    are the only thing raised in F
  • Full ADR-32 discovery: every instance answers $SRV.PING / INFO / STATS in all
    nine subject forms with the standard response types, so nats micro CLI tooling
    works out of the box; per-endpoint stats are also available locally via
    NatsService#stats
  • Instances load-balance through a queue group by default; service and endpoint
    metadata per ADR-33; request headers are visible to handlers, and response headers
    can be set via Reply

The module ships inside the fs2-nats artifact and adds no new dependency
(jsoniter-scala was already on the classpath for JetStream). See
docs/micro.md
for the full tour.

Performance

  • Dialled sockets now set TCP_NODELAY, removing Nagle-induced latency (up to
    ~40 ms per round trip) from request/reply; socket options are configurable via
    the new TransportConfig.socketOptions (#44)
  • JetStream.publishAsync — and Object Store put / KV putAsync on top of it —
    no longer starts a supervised fiber per message; correlation is registered up
    front and the reply completes on the shared inbox drain fiber (#47)
  • Request correlation moved from four CAS loops over an immutable-map Ref to a
    ConcurrentHashMap plus AtomicLong, so pipelined request/reply scales instead
    of contending on one serialization point (#49)
  • Subscription lookup on the delivery path uses a primitive-keyed LongMap and a
    single volatile read instead of boxing the sid and reading two Refs per
    message (#50)
  • JetStream API replies are decoded in place in a single jsoniter pass instead of
    a payload copy plus two passes per PubAck (#51)
  • Object Store put feeds SHA-256 by wrapping each chunk's backing arrays instead
    of flattening them, dropping the extra full copy of all object data on the way
    to the wire (#53)

Compatibility

  • 0.4 starts a new early-semver series: not binary compatible with 0.3.x,
    recompile against 0.4.0. Source-level, typical users need no changes
  • TransportConfig gained a socketOptions field (default: TCP_NODELAY on for
    sockets the library dials — override socketOptions to opt out). Named-argument
    construction is unaffected; only full-arity positional construction needs the
    new argument
  • The NatsClient trait gained a package-private abstract method, so hand-written
    implementations of the trait outside fs2.nats (e.g. test stubs) no longer
    compile — delegate to a real client instead
  • Scala 3, cats-effect 3.7, fs2 3.13; JDK 11+
  • Dependency refresh: jsoniter-scala 2.38.17 → 2.40.1; munit 1.3.5, sbt 1.12.15,
    scalafmt 3.11.5 (test/build-scoped only)

What's Changed

Full Changelog: v0.3.1...v0.4.0

v0.3.1

Choose a tag to compare

@ThatScalaGuy ThatScalaGuy released this 27 Jul 17:56

A maintenance release. There are no library code changes — this is a dependency
refresh on top of 0.3.0, plus a couple of build/CI fixes.

Dependency updates

  • Bouncy Castle 1.84 → 1.85 (bcprov-jdk18on, used for NKey Ed25519 signing)
  • jsoniter-scala 2.38.14 → 2.38.17
  • Scala 3.3.7 → 3.3.8
  • munit 1.3.2 → 1.3.4, scalafmt 3.11.1 → 3.11.3
  • sbt 1.12.11 → 1.12.13, sbt-typelevel 0.8.6 → 0.8.7

Build & CI

  • Scala Steward now pins sbt to the 1.x line. sbt 2 is a breaking rewrite of the
    build definition, so that move stays a deliberate migration rather than
    something picked up from an automated dependency PR (#36)
  • The Validate Steward Config CI job runs on JDK 17 — it was failing on JDK 11,
    which scala-steward no longer supports

Compatibility

  • Binary compatible with 0.3.0 (MiMa-checked); no source changes required
  • Scala 3, cats-effect 3.7, fs2 3.13; JDK 11+

What's Changed

Full Changelog: v0.3.0...v0.3.1

v0.3.0

Choose a tag to compare

@ThatScalaGuy ThatScalaGuy released this 10 Jun 09:08
f9d8953

This release is almost entirely about performance. The client was benchmarked
head-to-head against the official Java client (jnats) across core pub/sub,
JetStream, and Object Store workloads, and the hot paths were rebuilt based on
what the profiles showed. There are no public API changes — everything
below is internal.

Performance

Core pub/sub

  • The protocol parser keeps a single flat array carry instead of an immutable
    chunk queue, fixing an O(n²) scan under streaming load — 2.4× parser
    throughput and −82% allocations in JMH, ~+40% end-to-end receive (#18)
  • Publishes enqueue a small write descriptor instead of building a combined
    header+payload array per message; the single writer fiber assembles batches
    into one reused buffer and flushes them in one socket write — per-publish
    allocation is now constant at 112 B regardless of payload size (#20, #21, #22)
  • The receive path builds the user-facing message once, directly in the parser,
    instead of materializing an intermediate frame that gets re-wrapped (#23)
  • Sends take a lock-free fast path while the connection is online (#19)
  • Sockets are read in 64 KiB requests instead of fs2-io's 8 KiB default,
    cutting async read round-trips 8× on large-payload streams (#26)
  • Internal bounded queues stay on cats-effect's lock-free implementation at
    any capacity. Queue.bounded silently degrades to a CAS-retry queue at
    capacities ≥ 65534, which cost >30% of client CPU under sustained load with
    large queueCapacity settings — high-capacity configs are now ~2× faster
    under CPU pressure (#24)

JetStream

  • consume processes inbox messages chunk-at-a-time instead of one effect per
    message: 2.1× consume throughput (#25)

Object Store

  • GET benefits from the larger socket reads and now hashes chunks without
    copying them — streaming GET reaches parity with jnats at steady state
    (~1.3 GB/s in our benchmarks) (#26)

Other changes

Compatibility

  • Binary compatible with 0.2.x (MiMa-checked); no source changes required
  • Scala 3, cats-effect 3.6, fs2 3.13; JDK 11+

What's Changed

  • docs: add sbt-typelevel-site documentation site by @ThatScalaGuy in #17
  • Test library updates by @ThatScalaGuy in #16
  • perf(parser): flat-array incremental decoder (fixes O(n²) control-line scan) by @ThatScalaGuy in #18
  • perf(client): skip the connRef.modify CAS on the Online publish path by @ThatScalaGuy in #19
  • perf(transport): reuse the writer coalescing buffer across drains by @ThatScalaGuy in #20
  • perf(transport): drop the per-publish Some box from the write queue by @ThatScalaGuy in #21
  • perf(publish): enqueue a write descriptor instead of a per-publish combined array by @ThatScalaGuy in #22
  • perf(receive): build the user NatsMessage once on the MSG/HMSG data path by @ThatScalaGuy in #23
  • perf(queues): keep hot-path queues on cats-effect's lock-free implementations at any capacity by @ThatScalaGuy in #24
  • perf(jetstream): process pull-consume deliveries a chunk at a time by @ThatScalaGuy in #25
  • perf(receive): 64 KiB socket reads + copy-free Object Store GET digest by @ThatScalaGuy in #26

Full Changelog: v0.2.0...v0.3.0

v0.2.0

Choose a tag to compare

@ThatScalaGuy ThatScalaGuy released this 06 Jun 14:35
1932a9f

What's Changed

New Contributors

Full Changelog: v0.1.0...v0.2.0

v0.1.0

Choose a tag to compare

@ThatScalaGuy ThatScalaGuy released this 10 Dec 11:53
refactor(CI): :white_check_mark: Remove unused imports from test spec…