@zio.dev/zio-blocks 0.0.33 → 0.0.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/guides/compile-time-resource-safety-with-scope.md +16 -17
- package/guides/getting-started-with-mux.md +1507 -0
- package/guides/query-dsl-extending.md +161 -102
- package/guides/query-dsl-fluent-builder.md +217 -157
- package/guides/query-dsl-reified-optics.md +12 -10
- package/guides/query-dsl-sql.md +246 -165
- package/guides/telemetry-guide.md +1069 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +292 -50
- package/package.json +1 -1
- package/plans/config-follow-up-prs.md +188 -0
- package/plans/config-pr-assessment-roadmap.md +310 -0
- package/reference/MuxDataFlow.jsx +250 -0
- package/reference/async.md +651 -0
- package/reference/chunk.md +3533 -308
- package/reference/codegen/case-class.md +436 -0
- package/reference/codegen/emitter-config.md +383 -0
- package/reference/codegen/examples.md +664 -0
- package/reference/codegen/field.md +316 -0
- package/reference/codegen/index.md +317 -0
- package/reference/codegen/scala-emitter.md +392 -0
- package/reference/codegen/scala-file.md +276 -0
- package/reference/codegen/sealed-trait.md +408 -0
- package/reference/codegen/type-definition.md +340 -0
- package/reference/codegen/type-ref.md +201 -0
- package/reference/combinators.md +347 -117
- package/reference/config.md +158 -0
- package/reference/context.md +4 -4
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +825 -0
- package/reference/endpoint/path-codec.md +237 -0
- package/reference/endpoint/route-pattern.md +196 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +212 -0
- package/reference/html.md +1120 -0
- package/reference/htmx/attribute-values.md +359 -0
- package/reference/htmx/hx-encoding.md +111 -0
- package/reference/htmx/hx-params.md +204 -0
- package/reference/htmx/hx-swap.md +276 -0
- package/reference/htmx/hx-sync.md +251 -0
- package/reference/htmx/hx-target.md +314 -0
- package/reference/htmx/hx-trigger.md +457 -0
- package/reference/htmx/hx-url-update.md +239 -0
- package/reference/htmx/index.md +855 -0
- package/reference/http-model/index.md +47 -0
- package/reference/http-model/model.md +1481 -0
- package/reference/http-model/schema.md +747 -0
- package/reference/maybe.md +826 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.mdx +823 -0
- package/reference/openapi.md +1351 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -2
- package/reference/resource-management/scope.md +28 -12
- package/reference/resource-management/wire.md +3 -7
- package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
- package/reference/ringbuffer/MpscDiagram.jsx +618 -0
- package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
- package/reference/ringbuffer/SpscDiagram.jsx +677 -0
- package/reference/ringbuffer/advanced.mdx +109 -0
- package/reference/ringbuffer/index.mdx +145 -0
- package/reference/ringbuffer/mpmc.mdx +151 -0
- package/reference/ringbuffer/mpsc.mdx +132 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +344 -0
- package/reference/{allows.md → schema/allows.md} +4 -4
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +2 -3
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +480 -0
- package/reference/schema/built-in-codecs/csv.md +564 -0
- package/reference/schema/built-in-codecs/index.md +77 -0
- package/reference/schema/built-in-codecs/json/index.md +295 -0
- package/reference/schema/built-in-codecs/json/json-config.md +217 -0
- package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
- package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
- package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
- package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
- package/reference/schema/built-in-codecs/messagepack.md +508 -0
- package/reference/schema/built-in-codecs/thrift.md +433 -0
- package/reference/schema/built-in-codecs/toon.md +1078 -0
- package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
- package/reference/schema/built-in-codecs/yaml.md +552 -0
- package/reference/{codec.md → schema/codec.md} +10 -10
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +50 -0
- package/reference/schema/migration.md +297 -0
- package/reference/{modifier.md → schema/modifier.md} +58 -7
- package/reference/{optics.md → schema/optics.md} +2 -2
- package/reference/{patch.md → schema/patch.md} +1 -1
- package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
- package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
- package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
- package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
- package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
- package/reference/{schema.md → schema/schema.md} +12 -0
- package/reference/{structural-types.md → schema/structural-types.md} +1 -1
- package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
- package/reference/smithy.md +533 -0
- package/reference/sql/db-codec-deriver.md +71 -0
- package/reference/sql/db-codec.md +687 -0
- package/reference/sql/db-con.md +271 -0
- package/reference/sql/db-connection.md +153 -0
- package/reference/sql/db-param-writer.md +77 -0
- package/reference/sql/db-param.md +66 -0
- package/reference/sql/db-result-reader.md +146 -0
- package/reference/sql/db-tx.md +82 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +254 -0
- package/reference/sql/index.md +341 -0
- package/reference/sql/repo.md +600 -0
- package/reference/sql/sql-dialect.md +73 -0
- package/reference/sql/sql-logger.md +62 -0
- package/reference/sql/sql-name-mapper.md +70 -0
- package/reference/sql/table-metadata.md +134 -0
- package/reference/sql/table.md +448 -0
- package/reference/sql/transactor-zio.md +399 -0
- package/reference/sql/transactor.md +353 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/concurrent-operators.md +106 -0
- package/reference/streams/index.md +653 -0
- package/reference/streams/pipeline.md +718 -0
- package/reference/streams/reader.md +1284 -0
- package/reference/streams/scala-2-compatibility.md +55 -0
- package/reference/streams/sink.md +1426 -0
- package/reference/streams/stream.md +2526 -0
- package/reference/streams/writer.md +1045 -0
- package/reference/streams/zero-boxing.md +275 -0
- package/reference/telemetry.md +693 -0
- package/reference/typeid.md +5 -19
- package/sidebars.js +238 -43
- package/reference/formats.md +0 -694
- package/reference/http-model.md +0 -1716
- package/reference/streams.md +0 -989
- package/ringbuffer.md +0 -249
- /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
- /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
- /package/reference/{lazy.md → schema/lazy.md} +0 -0
- /package/reference/{reflect.md → schema/reflect.md} +0 -0
- /package/reference/{registers.md → schema/registers.md} +0 -0
- /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
- /package/reference/{syntax.md → schema/syntax.md} +0 -0
- /package/reference/{validation.md → schema/validation.md} +0 -0
|
@@ -0,0 +1,1507 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: getting-started-with-mux
|
|
3
|
+
title: "Getting Started with Mux"
|
|
4
|
+
description: "Learn how to manage multiplexed bidirectional message streams with capacity limits."
|
|
5
|
+
keywords: ["mux", "multiplexing", "message streams", "concurrency", "state machine"]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Welcome to Getting Started with Mux! This tutorial is for developers who want to understand how to manage concurrent, request-response style communication where multiple independent conversations happen over a single channel. You don't need any prior experience with multiplexing or the Mux library to follow along — we'll build your understanding step by step.
|
|
9
|
+
|
|
10
|
+
## Learning Objectives
|
|
11
|
+
|
|
12
|
+
By the end of this tutorial, you will understand:
|
|
13
|
+
|
|
14
|
+
- What a `Mux` is and the problem it solves
|
|
15
|
+
- How to create a mux and open streams within it
|
|
16
|
+
- The relationship between a protocol and your application code
|
|
17
|
+
- How `send()` and `receive()` work for application messages
|
|
18
|
+
- How `takeOutbound()` and `offerInbound()` work for protocol code
|
|
19
|
+
- The stream lifecycle: OPEN → HALF_CLOSED → CLOSED
|
|
20
|
+
- How to perform graceful shutdown (halfClose/signalRemoteClose) vs immediate closure (close)
|
|
21
|
+
- How to work with multiple independent streams
|
|
22
|
+
- When and how to handle errors from capacity limits
|
|
23
|
+
- Why thread safety matters and what it means in practice
|
|
24
|
+
|
|
25
|
+
We'll learn these concepts through:
|
|
26
|
+
|
|
27
|
+
1. **The Big Picture** — What Mux does and why it's useful
|
|
28
|
+
2. **Creating a Mux** — Your first mux and opening a stream
|
|
29
|
+
3. **Understanding Streams and Message Queues** — Two-way communication
|
|
30
|
+
4. **The Stream Lifecycle** — Open, half-close, and close
|
|
31
|
+
5. **Working with Multiple Streams** — Independence and isolation
|
|
32
|
+
6. **Managing Capacity** — When limits matter and how to handle them
|
|
33
|
+
7. **Thread Safety** — Who can call what, and from where
|
|
34
|
+
8. **Putting It Together** — A complete, runnable example
|
|
35
|
+
9. **Running the Examples** — Step-by-step commands
|
|
36
|
+
|
|
37
|
+
We recommend reading from top to bottom — each section builds on the previous one.
|
|
38
|
+
|
|
39
|
+
:::info[Scala 2 and Scala 3]
|
|
40
|
+
The code examples in this tutorial use **Scala 3 syntax**. In Scala 2.13, the API returns `Either[MuxError, T]` instead of union types, and pattern matching uses `case Right(value)` and `case Left(error)` instead of `case value` and `case error`. For complete API signatures with both syntaxes, see the [Mux Reference](../reference/mux.mdx).
|
|
41
|
+
|
|
42
|
+
**Example: Scala 2 equivalent:**
|
|
43
|
+
```scala
|
|
44
|
+
// Scala 3
|
|
45
|
+
stream.send("Hello") match {
|
|
46
|
+
case () => println("Success")
|
|
47
|
+
case error: MuxError => println(s"Error: $error")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Scala 2
|
|
51
|
+
stream.send("Hello") match {
|
|
52
|
+
case Right(()) => println("Success")
|
|
53
|
+
case Left(error) => println(s"Error: $error")
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
:::
|
|
57
|
+
|
|
58
|
+
## The Big Picture
|
|
59
|
+
|
|
60
|
+
Imagine you're building a communication protocol: a client sends requests to a server over a network connection, and the server sends responses back. Both directions use the same channel, but they're independent of each other. The client doesn't need to wait for one response before sending the next request — it can send five requests in a row, get three responses, send one more request, then get two more responses.
|
|
61
|
+
|
|
62
|
+
This is multiplexing: packing multiple independent conversations into a single bidirectional channel.
|
|
63
|
+
|
|
64
|
+
A `Mux` is a registry of these conversations. It manages:
|
|
65
|
+
|
|
66
|
+
- **Streams**: Each stream is one independent conversation, identified by a numeric ID.
|
|
67
|
+
- **Two message queues per stream**: One for messages going out (outbound), one for messages coming in (inbound).
|
|
68
|
+
- **Capacity limits**: A per-stream queue capacity and a per-mux overall capacity to prevent memory exhaustion.
|
|
69
|
+
- **Lifecycle management**: Each stream transitions through well-defined states (OPEN, HALF_CLOSED, CLOSED) to ensure orderly shutdown.
|
|
70
|
+
|
|
71
|
+
The key insight: application code calls `send()` and `receive()` to exchange messages with its peer. Protocol code (your networking layer) calls `takeOutbound()` and `offerInbound()` to move messages in and out of the mux. The mux itself never touches the network — it just manages queues and state.
|
|
72
|
+
|
|
73
|
+
## 1. Creating a Mux
|
|
74
|
+
|
|
75
|
+
The simplest starting point is to create a mux and open your first stream. A `Mux` is parameterized by three types:
|
|
76
|
+
|
|
77
|
+
- The **stream ID type** — typically `Int` or `Long`
|
|
78
|
+
- The **outbound message type** — what your application sends out
|
|
79
|
+
- The **inbound message type** — what your application receives
|
|
80
|
+
|
|
81
|
+
Let's create a mux that carries `String` messages in both directions, with stream IDs as integers:
|
|
82
|
+
|
|
83
|
+
```scala
|
|
84
|
+
import zio.blocks.mux._
|
|
85
|
+
|
|
86
|
+
// Create a mux that carries String messages in both directions
|
|
87
|
+
val mux = Mux[Int, String, String](100)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The code above:
|
|
91
|
+
- `Mux[Int, String, String]` — creates a mux where streams are identified by `Int`, outbound messages are `String`, and inbound messages are also `String`
|
|
92
|
+
- `(100)` — the mux can have up to 100 concurrent streams. Each stream has its own message queues with a fixed capacity of 256 messages per direction (inbound and outbound)
|
|
93
|
+
|
|
94
|
+
Now let's open a stream and pattern-match on the result (because opening can fail if capacity is exceeded):
|
|
95
|
+
|
|
96
|
+
```scala
|
|
97
|
+
import zio.blocks.mux._
|
|
98
|
+
|
|
99
|
+
val mux = Mux[Int, String, String](100)
|
|
100
|
+
|
|
101
|
+
val streamOrError = mux.open(1) // Try to open stream 1
|
|
102
|
+
streamOrError match {
|
|
103
|
+
case stream: MuxStream[Int, String, String] =>
|
|
104
|
+
println(s"Opened stream with ID 1: $stream")
|
|
105
|
+
case error: MuxError =>
|
|
106
|
+
println(s"Failed to open stream: $error")
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The code above:
|
|
111
|
+
- `mux.open(1)` — attempts to open a new stream with ID `1`
|
|
112
|
+
- The result is either a `MuxStream` (success) or a `MuxError` (failure — e.g., capacity exceeded)
|
|
113
|
+
- We pattern-match to handle both cases
|
|
114
|
+
|
|
115
|
+
:::note[Why Pattern-Matching?]
|
|
116
|
+
Opening a stream can fail. If the mux has reached its capacity limit, `open()` returns a `CapacityExceeded` error instead of a stream. By pattern-matching, we ensure our code handles both outcomes.
|
|
117
|
+
:::
|
|
118
|
+
|
|
119
|
+
## 2. Understanding Streams and Message Queues
|
|
120
|
+
|
|
121
|
+
Once you've opened a stream, you have access to a two-way communication channel. The key insight is that there are two different perspectives:
|
|
122
|
+
|
|
123
|
+
- **Application perspective**: You call `send()` to send outbound messages and `receive()` to get inbound messages.
|
|
124
|
+
- **Protocol perspective**: The protocol calls `takeOutbound()` to extract messages your application sent, and `offerInbound()` to deliver messages it received from the peer.
|
|
125
|
+
|
|
126
|
+
Let's see how this works:
|
|
127
|
+
|
|
128
|
+
```scala
|
|
129
|
+
import zio.blocks.mux._
|
|
130
|
+
|
|
131
|
+
val mux = Mux[Int, String, String](100)
|
|
132
|
+
val stream = mux.open(1) match {
|
|
133
|
+
case s: MuxStream[Int, String, String] => s
|
|
134
|
+
case e: MuxError => sys.error(s"Failed to open stream: $e")
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Application code: send a message
|
|
138
|
+
stream.send("Hello from app") match {
|
|
139
|
+
case () => println("✓ Message sent")
|
|
140
|
+
case error: MuxError => println(s"✗ Send failed: $error")
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Protocol code: extract the message to send over network
|
|
144
|
+
val outbound = stream.takeOutbound() match {
|
|
145
|
+
case Some(msg) => msg
|
|
146
|
+
case None => "no message available"
|
|
147
|
+
case error: MuxError => s"error: $error"
|
|
148
|
+
}
|
|
149
|
+
println(s"Protocol will send: $outbound")
|
|
150
|
+
|
|
151
|
+
// Protocol code: deliver a response from the peer
|
|
152
|
+
stream.offerInbound("Hello from peer") match {
|
|
153
|
+
case () => println("✓ Response delivered")
|
|
154
|
+
case error: MuxError => println(s"✗ Offer failed: $error")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Application code: receive the response
|
|
158
|
+
val inbound = stream.receive() match {
|
|
159
|
+
case Some(msg) => msg
|
|
160
|
+
case None => "no message available"
|
|
161
|
+
case error: MuxError => s"error: $error"
|
|
162
|
+
}
|
|
163
|
+
println(s"Application received: $inbound")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The code above demonstrates the `MuxStream` API:
|
|
167
|
+
- `MuxStream#send(msg)` returns `Unit | MuxError` (success or error); we pattern-match to handle both cases
|
|
168
|
+
- `MuxStream#takeOutbound()` returns `Option[String] | MuxError` (Some/None/error); we match all three
|
|
169
|
+
- `MuxStream#offerInbound(msg)` returns `Unit | MuxError` (success or error); we pattern-match to handle both
|
|
170
|
+
- `MuxStream#receive()` returns `Option[String] | MuxError` (Some/None/error); we match all three cases
|
|
171
|
+
|
|
172
|
+
:::tip[Two Queues, Two Roles]
|
|
173
|
+
Think of it as a pipeline: your application uses `send()` and `receive()`, the protocol uses `takeOutbound()` and `offerInbound()`. The mux sits in the middle, managing two FIFO queues per stream. This separation is intentional — it lets you reason about sending independently from receiving.
|
|
174
|
+
:::
|
|
175
|
+
|
|
176
|
+
## 3. The Stream Lifecycle
|
|
177
|
+
|
|
178
|
+
Every stream has a well-defined lifecycle with three states: OPEN, HALF_CLOSED, and CLOSED. This ensures graceful shutdown when both sides need to agree that communication is complete.
|
|
179
|
+
|
|
180
|
+
The state transitions look like this:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
OPEN
|
|
184
|
+
├─ (local side calls halfClose()) ──→ HALF_CLOSED_LOCAL
|
|
185
|
+
│ └─ (remote side calls signalRemoteClose()) ──→ CLOSED
|
|
186
|
+
│
|
|
187
|
+
└─ (remote side calls signalRemoteClose()) ──→ HALF_CLOSED_REMOTE
|
|
188
|
+
└─ (local side calls halfClose()) ──→ CLOSED
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Let's walk through a complete lifecycle:
|
|
192
|
+
|
|
193
|
+
```scala
|
|
194
|
+
import zio.blocks.mux._
|
|
195
|
+
|
|
196
|
+
val mux = Mux[Int, String, String](100)
|
|
197
|
+
val stream = mux.open(1) match {
|
|
198
|
+
case s: MuxStream[Int, String, String] => s
|
|
199
|
+
case e: MuxError => sys.error(s"Failed to open stream: $e")
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Stream starts in OPEN state
|
|
203
|
+
println(s"Is stream closed? ${stream.isClosed}")
|
|
204
|
+
println(s"Is stream half-closed? ${stream.isHalfClosed}")
|
|
205
|
+
|
|
206
|
+
// Application is done sending, signals half-close
|
|
207
|
+
stream.halfClose()
|
|
208
|
+
println(s"After halfClose(), is half-closed? ${stream.isHalfClosed}")
|
|
209
|
+
|
|
210
|
+
// Protocol receives a close signal from the peer
|
|
211
|
+
stream.signalRemoteClose()
|
|
212
|
+
println(s"After signalRemoteClose(), is closed? ${stream.isClosed}")
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The code above:
|
|
216
|
+
- `stream.isClosed` — checks if the stream is in CLOSED state
|
|
217
|
+
- `stream.isHalfClosed` — checks if the stream is in HALF_CLOSED state (one side done)
|
|
218
|
+
- `stream.halfClose()` — signals that this side is done sending (but can still receive)
|
|
219
|
+
- `stream.signalRemoteClose()` — signals that the peer is done sending
|
|
220
|
+
- Once you call both `halfClose()` and `signalRemoteClose()`, the stream becomes fully closed
|
|
221
|
+
|
|
222
|
+
However, sometimes you don't need a graceful handshake—you just want to close the stream immediately. The `close()` method transitions directly from any state to CLOSED without the half-close protocol:
|
|
223
|
+
|
|
224
|
+
```scala
|
|
225
|
+
import zio.blocks.mux._
|
|
226
|
+
|
|
227
|
+
val mux = Mux[Int, String, String](100)
|
|
228
|
+
val stream = mux.open(1) match {
|
|
229
|
+
case s: MuxStream[Int, String, String] => s
|
|
230
|
+
case e: MuxError => sys.error(s"Failed to open stream: $e")
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Application decides to close immediately (e.g., due to an error)
|
|
234
|
+
println(s"Before close: isClosed = ${stream.isClosed}")
|
|
235
|
+
|
|
236
|
+
stream.close()
|
|
237
|
+
println(s"After close: isClosed = ${stream.isClosed}")
|
|
238
|
+
|
|
239
|
+
// The close was synchronous and immediate
|
|
240
|
+
// Capacity is released, no handshake needed
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The code above:
|
|
244
|
+
- `stream.close()` — transitions the stream from OPEN (or any state) immediately to CLOSED
|
|
245
|
+
- The `close()` method also enqueues a terminal error (`StreamClosed`) that `receive()` returns after all buffered messages are drained, allowing applications to detect closure
|
|
246
|
+
- This bypasses the graceful half-close handshake and releases capacity immediately
|
|
247
|
+
- Use `close()` when you need abrupt termination (error conditions, cleanup)
|
|
248
|
+
- Use `halfClose()` + `signalRemoteClose()` for orderly shutdown where both sides need to agree
|
|
249
|
+
|
|
250
|
+
:::tip[When to Use Each Closure Method]
|
|
251
|
+
- **Graceful (halfClose + signalRemoteClose):** Normal shutdown where both sides need to drain pending messages and agree to close
|
|
252
|
+
- **Immediate (close):** When the stream should terminate right now (error recovery, cleanup, timeout)
|
|
253
|
+
- **External (mux.cancel(id, reason)):** When you need to close a stream from outside its context (thread-safe operation)
|
|
254
|
+
:::
|
|
255
|
+
|
|
256
|
+
### External Stream Cancellation: `mux.cancel(id, reason)`
|
|
257
|
+
|
|
258
|
+
While `halfClose()` and `close()` are stream-level operations that modify the stream's state directly, **`mux.cancel(id, reason)` is a thread-safe mux-level operation** that cancels a stream from any thread without needing to access the stream object itself. This is useful for cleanup handlers, timeouts, and other scenarios where you can't guarantee single-threaded access.
|
|
259
|
+
|
|
260
|
+
Here's how to use `mux.cancel()` to externally cancel a stream:
|
|
261
|
+
|
|
262
|
+
```scala
|
|
263
|
+
import zio.blocks.mux._
|
|
264
|
+
|
|
265
|
+
val mux = Mux[Int, String, String](100)
|
|
266
|
+
val stream = mux.open(1) match {
|
|
267
|
+
case s: MuxStream[Int, String, String] => s
|
|
268
|
+
case e: MuxError => sys.error(s"Failed to open stream: $e")
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// From any thread, you can cancel a stream by ID
|
|
272
|
+
// This is thread-safe and doesn't require access to the stream object
|
|
273
|
+
mux.cancel(1, MuxError.Cancelled(1, "Cleanup: application shutting down"))
|
|
274
|
+
println("✓ Stream 1 cancelled successfully")
|
|
275
|
+
|
|
276
|
+
// After cancellation, the stream is closed
|
|
277
|
+
println(s"Stream isClosed: ${stream.isClosed}")
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The code above:
|
|
281
|
+
- `mux.cancel(streamId, reason)` — cancels the stream with the given ID and enqueues a terminal error on its inbound queue
|
|
282
|
+
- This operation is **thread-safe** and you can call it from any thread
|
|
283
|
+
- Unlike `close()`, which requires direct access to the stream, `cancel()` works with just the stream ID
|
|
284
|
+
- The `reason` parameter becomes the terminal error that `receive()` returns
|
|
285
|
+
|
|
286
|
+
:::note[Important: `offerInbound()` After `signalRemoteClose()`]
|
|
287
|
+
When you call `signalRemoteClose()`, the stream transitions to `HALF_CLOSED_REMOTE` state. In this state, subsequent `offerInbound()` calls fail with `StreamClosed` error because the remote side signals it will not send any more messages. This prevents the protocol layer from enqueuing messages after the peer declares it's done sending.
|
|
288
|
+
:::
|
|
289
|
+
|
|
290
|
+
:::caution[Closing Is Coordinated]
|
|
291
|
+
Calling `halfClose()` when the stream is already in HALF_CLOSED_LOCAL or beyond is idempotent—the second call has no effect. Similarly, calling `signalRemoteClose()` when the stream is already in HALF_CLOSED_REMOTE or CLOSED has no effect. Calling `close()` at any time immediately transitions to CLOSED, bypassing any handshake.
|
|
292
|
+
:::
|
|
293
|
+
|
|
294
|
+
## 4. Working with Multiple Streams
|
|
295
|
+
|
|
296
|
+
The real power of mux comes from managing multiple independent streams simultaneously. Each stream is isolated: messages in stream 1 never appear in stream 2, and capacity is tracked separately.
|
|
297
|
+
|
|
298
|
+
Let's open three streams and exchange messages on each one:
|
|
299
|
+
|
|
300
|
+
```scala
|
|
301
|
+
import zio.blocks.mux._
|
|
302
|
+
|
|
303
|
+
val mux = Mux[Int, String, String](100)
|
|
304
|
+
|
|
305
|
+
// Open three streams
|
|
306
|
+
val stream1 = mux.open(1) match {
|
|
307
|
+
case s: MuxStream[Int, String, String] => s
|
|
308
|
+
case e: MuxError => sys.error(s"Failed to open stream 1: $e")
|
|
309
|
+
}
|
|
310
|
+
val stream2 = mux.open(2) match {
|
|
311
|
+
case s: MuxStream[Int, String, String] => s
|
|
312
|
+
case e: MuxError => sys.error(s"Failed to open stream 2: $e")
|
|
313
|
+
}
|
|
314
|
+
val stream3 = mux.open(3) match {
|
|
315
|
+
case s: MuxStream[Int, String, String] => s
|
|
316
|
+
case e: MuxError => sys.error(s"Failed to open stream 3: $e")
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Send different messages on each stream
|
|
320
|
+
stream1.send("Message for stream 1") match { case () => (); case e: MuxError => println(s"Stream 1 send error: $e") }
|
|
321
|
+
stream2.send("Message for stream 2") match { case () => (); case e: MuxError => println(s"Stream 2 send error: $e") }
|
|
322
|
+
stream3.send("Message for stream 3") match { case () => (); case e: MuxError => println(s"Stream 3 send error: $e") }
|
|
323
|
+
|
|
324
|
+
// Extract messages — they stay on their own stream
|
|
325
|
+
val out1 = stream1.takeOutbound() match { case Some(msg) => msg; case None => "(empty)"; case e: MuxError => s"error: $e" }
|
|
326
|
+
val out2 = stream2.takeOutbound() match { case Some(msg) => msg; case None => "(empty)"; case e: MuxError => s"error: $e" }
|
|
327
|
+
val out3 = stream3.takeOutbound() match { case Some(msg) => msg; case None => "(empty)"; case e: MuxError => s"error: $e" }
|
|
328
|
+
println(s"Stream 1 outbound: $out1")
|
|
329
|
+
println(s"Stream 2 outbound: $out2")
|
|
330
|
+
println(s"Stream 3 outbound: $out3")
|
|
331
|
+
|
|
332
|
+
// Deliver responses to different streams
|
|
333
|
+
stream1.offerInbound("Response for stream 1") match { case () => (); case e: MuxError => println(s"Stream 1 offer error: $e") }
|
|
334
|
+
stream2.offerInbound("Response for stream 2") match { case () => (); case e: MuxError => println(s"Stream 2 offer error: $e") }
|
|
335
|
+
stream3.offerInbound("Response for stream 3") match { case () => (); case e: MuxError => println(s"Stream 3 offer error: $e") }
|
|
336
|
+
|
|
337
|
+
// Receive on each stream — no crosstalk
|
|
338
|
+
val in1 = stream1.receive() match { case Some(msg) => msg; case None => "(empty)"; case e: MuxError => s"error: $e" }
|
|
339
|
+
val in2 = stream2.receive() match { case Some(msg) => msg; case None => "(empty)"; case e: MuxError => s"error: $e" }
|
|
340
|
+
val in3 = stream3.receive() match { case Some(msg) => msg; case None => "(empty)"; case e: MuxError => s"error: $e" }
|
|
341
|
+
println(s"Stream 1 inbound: $in1")
|
|
342
|
+
println(s"Stream 2 inbound: $in2")
|
|
343
|
+
println(s"Stream 3 inbound: $in3")
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
The code above:
|
|
347
|
+
- We open three streams with IDs 1, 2, and 3
|
|
348
|
+
- Each stream has its own outbound and inbound queues
|
|
349
|
+
- Sending on stream 1 doesn't affect stream 2 or stream 3
|
|
350
|
+
- Messages stay on their originating stream — there is no crosstalk
|
|
351
|
+
|
|
352
|
+
:::tip[Streams Are Independent]
|
|
353
|
+
Each stream is a completely independent message channel. If stream 1 fills up its queue, stream 2 is unaffected. If you close stream 1, streams 2 and 3 continue operating normally. This isolation is the core benefit of multiplexing.
|
|
354
|
+
:::
|
|
355
|
+
|
|
356
|
+
## 5. Managing Capacity
|
|
357
|
+
|
|
358
|
+
The mux enforces two separate capacity limits: **mux-level** (controlling how many streams can exist concurrently) and **per-stream** (controlling how many messages each stream's queues can hold).
|
|
359
|
+
|
|
360
|
+
- **Mux-level capacity** (set at creation): The total number of concurrent streams. When exceeded, `open()` fails with `CapacityExceeded`.
|
|
361
|
+
- **Per-stream capacity** (fixed at 256 messages per direction): Each stream's outbound and inbound queues. When exceeded, `send()` and `offerInbound()` fail with `QueueFull`.
|
|
362
|
+
|
|
363
|
+
:::note[Null Message Rejection]
|
|
364
|
+
Both `send()` and `offerInbound()` reject `null` messages with `MuxError.ProtocolError("null message")`. This ensures the protocol layer never enqueues null values, which helps catch bugs early.
|
|
365
|
+
:::
|
|
366
|
+
|
|
367
|
+
Let's see what happens when we hit these limits:
|
|
368
|
+
|
|
369
|
+
```scala
|
|
370
|
+
import zio.blocks.mux._
|
|
371
|
+
|
|
372
|
+
// Create a tiny mux with a small capacity
|
|
373
|
+
val mux = Mux[Int, String, String](10)
|
|
374
|
+
val stream = mux.open(1) match {
|
|
375
|
+
case s: MuxStream[Int, String, String] => s
|
|
376
|
+
case e: MuxError => sys.error(s"Failed to open stream: $e")
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Try to send many messages until the queue fills (per-stream capacity is 256 per direction)
|
|
380
|
+
for (i <- 1 to 300) {
|
|
381
|
+
val result = stream.send(s"Message $i")
|
|
382
|
+
result match {
|
|
383
|
+
case () => if (i <= 5 || i > 255) println(s"✓ Sent message $i")
|
|
384
|
+
case error: MuxError => println(s"✗ Message $i failed: $error")
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
The code above:
|
|
390
|
+
- We create a mux with total capacity 10 (very small)
|
|
391
|
+
- Each `send()` either succeeds (returns `Unit`) or fails with a `MuxError`
|
|
392
|
+
- When the per-stream queue is full, further sends return `MuxError.QueueFull`
|
|
393
|
+
- Each stream has its own per-stream message queues with a fixed capacity of 256 messages per direction
|
|
394
|
+
|
|
395
|
+
The standard recovery pattern is to drain messages from the outbound queue (by calling `takeOutbound()` repeatedly) until space is available:
|
|
396
|
+
|
|
397
|
+
```scala
|
|
398
|
+
import zio.blocks.mux._
|
|
399
|
+
|
|
400
|
+
val mux = Mux[Int, String, String](10)
|
|
401
|
+
val stream = mux.open(1) match {
|
|
402
|
+
case s: MuxStream[Int, String, String] => s
|
|
403
|
+
case e: MuxError => sys.error(s"Failed to open stream: $e")
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Fill the queue
|
|
407
|
+
for (i <- 1 to 5) {
|
|
408
|
+
stream.send(s"Message $i") match {
|
|
409
|
+
case () => ()
|
|
410
|
+
case e: MuxError => sys.error(s"send failed: $e")
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Drain one message from the outbound queue
|
|
415
|
+
println(s"Drained: ${stream.takeOutbound()}")
|
|
416
|
+
|
|
417
|
+
// Now we have space to send again
|
|
418
|
+
val result = stream.send("Message 6")
|
|
419
|
+
println(s"Sent after draining: $result is Unit")
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
The code above:
|
|
423
|
+
- We fill the queue with 5 messages
|
|
424
|
+
- We call `takeOutbound()` once to simulate the protocol extracting and sending a message
|
|
425
|
+
- This frees space in the queue, and the next `send()` succeeds
|
|
426
|
+
|
|
427
|
+
:::caution[Handling QueueFull in Real Code]
|
|
428
|
+
In a real application, `QueueFull` means the protocol is not keeping up with the application. You should either:
|
|
429
|
+
- Wait for the protocol to drain messages (using a poll loop or a callback mechanism)
|
|
430
|
+
- Close the stream if you decide recovery is impossible
|
|
431
|
+
- Implement a backpressure mechanism that slows the application
|
|
432
|
+
Never ignore `QueueFull` — it indicates a broken contract between layers.
|
|
433
|
+
:::
|
|
434
|
+
|
|
435
|
+
## 6. Thread Safety
|
|
436
|
+
|
|
437
|
+
The mux implementation is **thread-safe** for certain operations and **single-threaded** for others. Understanding this boundary is essential.
|
|
438
|
+
|
|
439
|
+
**Mux-level thread-safe operations** (call from any thread on the mux object):
|
|
440
|
+
- `mux.open(id)` — open a new stream
|
|
441
|
+
- `mux.get(id)` — retrieve a stream by ID
|
|
442
|
+
- `mux.cancel(id, reason)` — externally cancel a stream (thread-safe alternative to `close()`)
|
|
443
|
+
- `mux.closeAll(reason)` — close all streams
|
|
444
|
+
|
|
445
|
+
**Stream-level thread-safe operations** (call from multiple threads on a stream):
|
|
446
|
+
- `stream.send(msg)` — queue a message to send (uses lock-free ring buffer on JVM)
|
|
447
|
+
- `stream.offerInbound(msg)` — deliver a received message (uses lock-free ring buffer on JVM)
|
|
448
|
+
|
|
449
|
+
**Stream-level single-threaded operations** (call from the same dedicated thread only):
|
|
450
|
+
- `stream.receive()` — dequeue an inbound message
|
|
451
|
+
- `stream.takeOutbound()` — dequeue an outbound message
|
|
452
|
+
- `stream.halfClose()` — signal local side is done sending
|
|
453
|
+
- `stream.signalRemoteClose()` — signal remote side is done sending
|
|
454
|
+
- `stream.close()` — immediately close the stream
|
|
455
|
+
|
|
456
|
+
**Why?** The outbound and inbound queues are lock-free (on JVM) or single-threaded (on JS), which requires that the consumer threads remain stable. State transitions (`halfClose()`, `signalRemoteClose()`, `close()`) are not locked and should be coordinated from a single thread or with external synchronization.
|
|
457
|
+
|
|
458
|
+
Here's a correct usage pattern:
|
|
459
|
+
|
|
460
|
+
```scala
|
|
461
|
+
import zio.blocks.mux._
|
|
462
|
+
|
|
463
|
+
val mux = Mux[Int, String, String](100)
|
|
464
|
+
val stream = mux.open(1) match {
|
|
465
|
+
case s: MuxStream[Int, String, String] => s
|
|
466
|
+
case e: MuxError => sys.error(s"Failed to open stream: $e")
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Thread A: application code (single thread)
|
|
470
|
+
// Always call send() from the same thread
|
|
471
|
+
stream.send("Message 1") match { case () => println("✓ Sent 1"); case e: MuxError => println(s"✗ Error: $e") }
|
|
472
|
+
stream.send("Message 2") match { case () => println("✓ Sent 2"); case e: MuxError => println(s"✗ Error: $e") }
|
|
473
|
+
|
|
474
|
+
// Thread B: protocol code (single thread)
|
|
475
|
+
// Always call takeOutbound() from the same thread
|
|
476
|
+
stream.takeOutbound() match { case Some(msg) => println(s"Extracted: $msg"); case None => println("(empty)"); case e: MuxError => println(s"✗ Error: $e") }
|
|
477
|
+
stream.takeOutbound() match { case Some(msg) => println(s"Extracted: $msg"); case None => println("(empty)"); case e: MuxError => println(s"✗ Error: $e") }
|
|
478
|
+
|
|
479
|
+
// Thread C, D, E, ... : multiple threads can safely deliver inbound messages
|
|
480
|
+
// This is thread-safe
|
|
481
|
+
for (_ <- 1 to 3) {
|
|
482
|
+
stream.offerInbound("Response from peer") match { case () => (); case e: MuxError => println(s"✗ Offer error: $e") }
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Thread A: back on the application thread
|
|
486
|
+
// Always receive() on the same thread
|
|
487
|
+
stream.receive() match { case Some(msg) => println(s"Received: $msg"); case None => println("(empty)"); case e: MuxError => println(s"✗ Error: $e") }
|
|
488
|
+
stream.receive() match { case Some(msg) => println(s"Received: $msg"); case None => println("(empty)"); case e: MuxError => println(s"✗ Error: $e") }
|
|
489
|
+
stream.receive() match { case Some(msg) => println(s"Received: $msg"); case None => println("(empty)"); case e: MuxError => println(s"✗ Error: $e") }
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
The code above:
|
|
493
|
+
- Call `send()` and `receive()` each from a single dedicated thread, but they **can be different threads**
|
|
494
|
+
- Call `takeOutbound()` from a single dedicated thread (the protocol/reading thread)
|
|
495
|
+
- Multiple threads can safely call `offerInbound()` to deliver inbound messages
|
|
496
|
+
- Violating the single-threaded requirement for `receive()` and `takeOutbound()` corrupts the ring buffer
|
|
497
|
+
|
|
498
|
+
:::tip[Comparing Closure Methods]
|
|
499
|
+
There are three ways to close a stream, each with different semantics and thread safety properties:
|
|
500
|
+
|
|
501
|
+
| Method | Thread-Safe | Semantics | When to Use |
|
|
502
|
+
|--------|:---:|-----------|------------|
|
|
503
|
+
| **halfClose() + signalRemoteClose()** | ❌ Single-threaded | Graceful shutdown: both sides agree to close, pending messages drained | Normal orderly shutdown |
|
|
504
|
+
| **close()** | ❌ Single-threaded | Immediate closure: transitions to CLOSED right now, capacity released | Error recovery, cleanup, timeouts |
|
|
505
|
+
| **mux.cancel(id, reason)** | ✅ Thread-safe | External cancellation: close from any thread with an error reason | Cleanup from outside the stream, concurrent scenarios |
|
|
506
|
+
|
|
507
|
+
For example, use `mux.cancel(streamId, error)` when you're shutting down the entire application and need to close streams from a cleanup handler (which might run on a different thread).
|
|
508
|
+
:::
|
|
509
|
+
|
|
510
|
+
:::danger[Thread Safety Violation]
|
|
511
|
+
Do **not** call `receive()` or `takeOutbound()` from multiple threads on the same stream. These methods operate on lock-free ring buffers (JVM) or mutable arrays (JS) that are not thread-safe for concurrent access. Violating this will cause data corruption or panics.
|
|
512
|
+
:::
|
|
513
|
+
|
|
514
|
+
## 7. Putting It Together
|
|
515
|
+
|
|
516
|
+
Now let's write a complete example that demonstrates all the concepts: creating a mux, opening streams, exchanging messages, handling errors, managing capacity, and closing gracefully:
|
|
517
|
+
|
|
518
|
+
```scala
|
|
519
|
+
import zio.blocks.mux._
|
|
520
|
+
|
|
521
|
+
object MuxExample {
|
|
522
|
+
def main(args: Array[String]): Unit = {
|
|
523
|
+
// Create a mux: stream IDs are Int, messages are String in both directions
|
|
524
|
+
val mux = Mux[Int, String, String](100)
|
|
525
|
+
|
|
526
|
+
println("=== Opening Streams ===")
|
|
527
|
+
// Open three streams, handling potential capacity errors
|
|
528
|
+
val streams = (1 to 3).map { id =>
|
|
529
|
+
mux.open(id) match {
|
|
530
|
+
case stream: MuxStream[Int, String, String] =>
|
|
531
|
+
println(s"Opened stream $id")
|
|
532
|
+
stream
|
|
533
|
+
case error: MuxError =>
|
|
534
|
+
println(s"Failed to open stream $id: $error")
|
|
535
|
+
throw new RuntimeException(s"Cannot proceed without stream $id")
|
|
536
|
+
}
|
|
537
|
+
}.toList
|
|
538
|
+
|
|
539
|
+
println("\n=== Application Sends Messages ===")
|
|
540
|
+
// Application sends messages on each stream
|
|
541
|
+
streams.zipWithIndex.foreach { case (stream, idx) =>
|
|
542
|
+
val msg = s"Hello from stream ${idx + 1}"
|
|
543
|
+
stream.send(msg) match {
|
|
544
|
+
case () => println(s"Stream ${idx + 1} sent: $msg")
|
|
545
|
+
case error: MuxError => println(s"Stream ${idx + 1} send failed: $error")
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
println("\n=== Protocol Extracts Outbound Messages ===")
|
|
550
|
+
// Protocol extracts messages to send over the network
|
|
551
|
+
streams.zipWithIndex.foreach { case (stream, idx) =>
|
|
552
|
+
stream.takeOutbound() match {
|
|
553
|
+
case Some(msg) => println(s"Protocol will send on stream ${idx + 1}: $msg")
|
|
554
|
+
case None => println(s"Stream ${idx + 1} has no outbound messages")
|
|
555
|
+
case error: MuxError => println(s"Stream ${idx + 1} takeOutbound failed: $error")
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
println("\n=== Protocol Delivers Inbound Messages ===")
|
|
560
|
+
// Protocol delivers responses received from the peer
|
|
561
|
+
streams.zipWithIndex.foreach { case (stream, idx) =>
|
|
562
|
+
val response = s"Response from peer on stream ${idx + 1}"
|
|
563
|
+
stream.offerInbound(response) match {
|
|
564
|
+
case () => println(s"Protocol delivered on stream ${idx + 1}: $response")
|
|
565
|
+
case error: MuxError => println(s"Stream ${idx + 1} offerInbound failed: $error")
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
println("\n=== Application Receives Messages ===")
|
|
570
|
+
// Application receives the responses
|
|
571
|
+
streams.zipWithIndex.foreach { case (stream, idx) =>
|
|
572
|
+
stream.receive() match {
|
|
573
|
+
case Some(msg) => println(s"Stream ${idx + 1} received: $msg")
|
|
574
|
+
case None => println(s"Stream ${idx + 1} has no inbound messages")
|
|
575
|
+
case error: MuxError => println(s"Stream ${idx + 1} receive failed: $error")
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
println("\n=== Graceful Shutdown ===")
|
|
580
|
+
// Streams close gracefully
|
|
581
|
+
val stream1 = streams(0)
|
|
582
|
+
stream1.halfClose()
|
|
583
|
+
println("Stream 1: halfClose() called")
|
|
584
|
+
|
|
585
|
+
stream1.signalRemoteClose()
|
|
586
|
+
println(s"Stream 1: signalRemoteClose() called, isClosed = ${stream1.isClosed}")
|
|
587
|
+
|
|
588
|
+
// Verify the stream is closed
|
|
589
|
+
assert(stream1.isClosed, "Stream 1 should be closed")
|
|
590
|
+
|
|
591
|
+
println("\n=== Verify Independence ===")
|
|
592
|
+
// Streams 2 and 3 are unaffected by stream 1 closing
|
|
593
|
+
println(s"Stream 2 is open: ${!streams(1).isClosed}")
|
|
594
|
+
println(s"Stream 3 is open: ${!streams(2).isClosed}")
|
|
595
|
+
|
|
596
|
+
println("\nExample complete!")
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
This example demonstrates:
|
|
602
|
+
- Creating a mux with a specific capacity
|
|
603
|
+
- Opening multiple streams and handling errors
|
|
604
|
+
- Application code sending messages and receiving responses
|
|
605
|
+
- Protocol code extracting and delivering messages
|
|
606
|
+
- Graceful stream closure using halfClose and signalRemoteClose
|
|
607
|
+
- Verifying that streams remain independent
|
|
608
|
+
|
|
609
|
+
## What You've Learned
|
|
610
|
+
|
|
611
|
+
In this tutorial, you learned:
|
|
612
|
+
|
|
613
|
+
- What a `Mux` is: a registry that manages multiple independent, multiplexed message streams
|
|
614
|
+
- How to create a mux and open streams within it, handling capacity errors
|
|
615
|
+
- The two-role pattern: application code uses `send()` and `receive()`, protocol code uses `takeOutbound()` and `offerInbound()`
|
|
616
|
+
- How streams transition through a well-defined lifecycle (OPEN → HALF_CLOSED → CLOSED)
|
|
617
|
+
- How to perform graceful shutdown (halfClose/signalRemoteClose) vs immediate closure (close)
|
|
618
|
+
- Why multiple streams are completely independent with no crosstalk
|
|
619
|
+
- How capacity limits work and the standard recovery pattern (drain to free space)
|
|
620
|
+
- The thread-safety contract: only `send()`, `offerInbound()`, and `cancel()` are multi-threaded safe; state transitions and message dequeuing are single-threaded
|
|
621
|
+
- How to build a complete request-response system using mux
|
|
622
|
+
|
|
623
|
+
You now have a solid foundation in how mux simplifies multiplexed communication. The next step is to see how to build production-grade protocols using mux as the foundation.
|
|
624
|
+
|
|
625
|
+
## Running the Examples
|
|
626
|
+
|
|
627
|
+
All examples in this tutorial have corresponding runnable Scala files in the `mux-examples` module. Run them in order to progressively build your understanding in practice.
|
|
628
|
+
|
|
629
|
+
### Creating a Mux
|
|
630
|
+
|
|
631
|
+
This example creates a mux and opens your first stream, demonstrating the basic API for stream initialization and handling both success and failure cases when capacity is exceeded.
|
|
632
|
+
|
|
633
|
+
<details>
|
|
634
|
+
<summary>mux-examples/src/main/scala/mux/Example1CreatingAMux.scala</summary>
|
|
635
|
+
|
|
636
|
+
```scala title="mux-examples/src/main/scala/mux/Example1CreatingAMux.scala" showLineNumbers
|
|
637
|
+
/*
|
|
638
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
639
|
+
*
|
|
640
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
641
|
+
* you may not use this file except in compliance with the License.
|
|
642
|
+
* You may obtain a copy of the License at
|
|
643
|
+
*
|
|
644
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
645
|
+
*
|
|
646
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
647
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
648
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
649
|
+
* See the License for the specific language governing permissions and
|
|
650
|
+
* limitations under the License.
|
|
651
|
+
*/
|
|
652
|
+
|
|
653
|
+
package mux
|
|
654
|
+
|
|
655
|
+
import zio.blocks.mux._
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Title: Creating a Mux Description: Create a mux that carries String messages
|
|
659
|
+
* in both directions with integer stream IDs, then open a stream and handle
|
|
660
|
+
* both success and failure cases. Run: sbt "mux-examples/runMain
|
|
661
|
+
* mux.example1CreatingAMux"
|
|
662
|
+
*/
|
|
663
|
+
@main def example1CreatingAMux(): Unit = {
|
|
664
|
+
println("=== Creating a Mux ===")
|
|
665
|
+
|
|
666
|
+
// Create a mux that carries String messages in both directions
|
|
667
|
+
val mux = Mux[Int, String, String](100)
|
|
668
|
+
println("Created mux with capacity for 100 concurrent streams")
|
|
669
|
+
|
|
670
|
+
println("\n=== Opening a Stream ===")
|
|
671
|
+
|
|
672
|
+
// Try to open stream 1
|
|
673
|
+
val streamOrError = mux.open(1)
|
|
674
|
+
streamOrError match {
|
|
675
|
+
case stream: MuxStream[Int, String, String] =>
|
|
676
|
+
println(s"✓ Opened stream with ID 1: $stream")
|
|
677
|
+
println(s" Stream is closed: ${stream.isClosed}")
|
|
678
|
+
println(s" Stream is half-closed: ${stream.isHalfClosed}")
|
|
679
|
+
case error: MuxError =>
|
|
680
|
+
println(s"✗ Failed to open stream: $error")
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
</details>
|
|
686
|
+
|
|
687
|
+
Observe the output: the stream is successfully created and its initial state (not closed, not half-closed) is displayed.
|
|
688
|
+
|
|
689
|
+
```bash
|
|
690
|
+
sbt "mux-examples/runMain mux.example1CreatingAMux"
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
### Understanding Streams and Message Queues
|
|
694
|
+
|
|
695
|
+
This example demonstrates the two-perspective communication model where application code uses `send()`/`receive()` while protocol code uses `takeOutbound()`/`offerInbound()`, showing how messages flow through the queue system.
|
|
696
|
+
|
|
697
|
+
<details>
|
|
698
|
+
<summary>mux-examples/src/main/scala/mux/Example2UnderstandingStreamsAndMessageQueues.scala</summary>
|
|
699
|
+
|
|
700
|
+
```scala title="mux-examples/src/main/scala/mux/Example2UnderstandingStreamsAndMessageQueues.scala" showLineNumbers
|
|
701
|
+
/*
|
|
702
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
703
|
+
*
|
|
704
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
705
|
+
* you may not use this file except in compliance with the License.
|
|
706
|
+
* You may obtain a copy of the License at
|
|
707
|
+
*
|
|
708
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
709
|
+
*
|
|
710
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
711
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
712
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
713
|
+
* See the License for the specific language governing permissions and
|
|
714
|
+
* limitations under the License.
|
|
715
|
+
*/
|
|
716
|
+
|
|
717
|
+
package mux
|
|
718
|
+
|
|
719
|
+
import zio.blocks.mux._
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Title: Understanding Streams and Message Queues
|
|
723
|
+
*
|
|
724
|
+
* Description: Demonstrate the two-way communication pattern: application uses
|
|
725
|
+
* send()/receive(), protocol uses takeOutbound()/offerInbound().
|
|
726
|
+
*
|
|
727
|
+
* Run: sbt "mux-examples/runMain
|
|
728
|
+
* mux.example2UnderstandingStreamsAndMessageQueues"
|
|
729
|
+
*/
|
|
730
|
+
@main def example2UnderstandingStreamsAndMessageQueues(): Unit = {
|
|
731
|
+
println("=== Streams and Message Queues ===\n")
|
|
732
|
+
|
|
733
|
+
val mux = Mux[Int, String, String](100)
|
|
734
|
+
val stream = mux.open(1) match {
|
|
735
|
+
case s: MuxStream[Int, String, String] => s
|
|
736
|
+
case e: MuxError => throw new RuntimeException(s"Failed to open stream: $e")
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
println("--- Application sends a message ---")
|
|
740
|
+
// Application code: send a message
|
|
741
|
+
stream.send("Hello from app") match {
|
|
742
|
+
case () => println("✓ Message sent successfully")
|
|
743
|
+
case error: MuxError => println(s"✗ Send failed: $error")
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
println("\n--- Protocol extracts the message ---")
|
|
747
|
+
// Protocol code: extract the message to send over network
|
|
748
|
+
val outbound = stream.takeOutbound() match {
|
|
749
|
+
case Some(msg) => msg
|
|
750
|
+
case None => "(no message available)"
|
|
751
|
+
case error: MuxError => s"error: $error"
|
|
752
|
+
}
|
|
753
|
+
println(s"Protocol will send: '$outbound'")
|
|
754
|
+
|
|
755
|
+
println("\n--- Protocol delivers a response ---")
|
|
756
|
+
// Protocol code: deliver a response from the peer
|
|
757
|
+
stream.offerInbound("Hello from peer") match {
|
|
758
|
+
case () => println("✓ Response delivered successfully")
|
|
759
|
+
case error: MuxError => println(s"✗ Offer failed: $error")
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
println("\n--- Application receives the response ---")
|
|
763
|
+
// Application code: receive the response
|
|
764
|
+
val inbound = stream.receive() match {
|
|
765
|
+
case Some(msg) => msg
|
|
766
|
+
case None => "(no message available)"
|
|
767
|
+
case error: MuxError => s"error: $error"
|
|
768
|
+
}
|
|
769
|
+
println(s"Application received: '$inbound'")
|
|
770
|
+
|
|
771
|
+
println("\n=== Summary ===")
|
|
772
|
+
println("Application uses send() and receive()")
|
|
773
|
+
println("Protocol uses takeOutbound() and offerInbound()")
|
|
774
|
+
println("Messages stay in their FIFO queues per stream")
|
|
775
|
+
}
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
</details>
|
|
779
|
+
|
|
780
|
+
Observe the output: messages sent by the application are extracted by the protocol, and responses delivered by the protocol are received by the application.
|
|
781
|
+
|
|
782
|
+
```bash
|
|
783
|
+
sbt "mux-examples/runMain mux.example2UnderstandingStreamsAndMessageQueues"
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
### The Stream Lifecycle
|
|
787
|
+
|
|
788
|
+
This example shows all three ways to close a stream: graceful two-phase closure (halfClose + signalRemoteClose), immediate closure (close), and external thread-safe cancellation (mux.cancel).
|
|
789
|
+
|
|
790
|
+
<details>
|
|
791
|
+
<summary>mux-examples/src/main/scala/mux/Example3StreamLifecycle.scala</summary>
|
|
792
|
+
|
|
793
|
+
```scala title="mux-examples/src/main/scala/mux/Example3StreamLifecycle.scala" showLineNumbers
|
|
794
|
+
/*
|
|
795
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
796
|
+
*
|
|
797
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
798
|
+
* you may not use this file except in compliance with the License.
|
|
799
|
+
* You may obtain a copy of the License at
|
|
800
|
+
*
|
|
801
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
802
|
+
*
|
|
803
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
804
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
805
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
806
|
+
* See the License for the specific language governing permissions and
|
|
807
|
+
* limitations under the License.
|
|
808
|
+
*/
|
|
809
|
+
|
|
810
|
+
package mux
|
|
811
|
+
|
|
812
|
+
import zio.blocks.mux._
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Title: The Stream Lifecycle
|
|
816
|
+
*
|
|
817
|
+
* Description: Demonstrate state transitions through OPEN → HALF_CLOSED →
|
|
818
|
+
* CLOSED using graceful shutdown, immediate closure, and external cancellation.
|
|
819
|
+
*
|
|
820
|
+
* Run: sbt "mux-examples/runMain mux.example3StreamLifecycle"
|
|
821
|
+
*/
|
|
822
|
+
@main def example3StreamLifecycle(): Unit = {
|
|
823
|
+
println("=== Graceful Stream Lifecycle (halfClose + signalRemoteClose) ===\n")
|
|
824
|
+
|
|
825
|
+
val mux = Mux[Int, String, String](100)
|
|
826
|
+
val stream1 = mux.open(1) match {
|
|
827
|
+
case s: MuxStream[Int, String, String] => s
|
|
828
|
+
case e: MuxError => throw new RuntimeException(s"Failed to open stream 1: $e")
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Stream starts in OPEN state
|
|
832
|
+
println(s"Initial state:")
|
|
833
|
+
println(s" isClosed: ${stream1.isClosed}")
|
|
834
|
+
println(s" isHalfClosed: ${stream1.isHalfClosed}")
|
|
835
|
+
|
|
836
|
+
// Application is done sending, signals half-close
|
|
837
|
+
println(s"\nCalling halfClose()...")
|
|
838
|
+
stream1.halfClose()
|
|
839
|
+
println(s"After halfClose():")
|
|
840
|
+
println(s" isClosed: ${stream1.isClosed}")
|
|
841
|
+
println(s" isHalfClosed: ${stream1.isHalfClosed}")
|
|
842
|
+
|
|
843
|
+
// Protocol receives a close signal from the peer
|
|
844
|
+
println(s"\nCalling signalRemoteClose()...")
|
|
845
|
+
stream1.signalRemoteClose()
|
|
846
|
+
println(s"After signalRemoteClose():")
|
|
847
|
+
println(s" isClosed: ${stream1.isClosed}")
|
|
848
|
+
println(s" isHalfClosed: ${stream1.isHalfClosed}")
|
|
849
|
+
|
|
850
|
+
println("\n=== Immediate Stream Closure (close) ===\n")
|
|
851
|
+
|
|
852
|
+
val stream2 = mux.open(2) match {
|
|
853
|
+
case s: MuxStream[Int, String, String] => s
|
|
854
|
+
case e: MuxError => throw new RuntimeException(s"Failed to open stream 2: $e")
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
println(s"Before close():")
|
|
858
|
+
println(s" isClosed: ${stream2.isClosed}")
|
|
859
|
+
|
|
860
|
+
// Application decides to close immediately (e.g., due to an error)
|
|
861
|
+
println(s"\nCalling close()...")
|
|
862
|
+
stream2.close()
|
|
863
|
+
|
|
864
|
+
println(s"After close():")
|
|
865
|
+
println(s" isClosed: ${stream2.isClosed}")
|
|
866
|
+
|
|
867
|
+
println("\n=== External Stream Cancellation (mux.cancel) ===\n")
|
|
868
|
+
|
|
869
|
+
val stream3 = mux.open(3) match {
|
|
870
|
+
case s: MuxStream[Int, String, String] => s
|
|
871
|
+
case e: MuxError => throw new RuntimeException(s"Failed to open stream 3: $e")
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
println(s"Before cancel():")
|
|
875
|
+
println(s" isClosed: ${stream3.isClosed}")
|
|
876
|
+
|
|
877
|
+
// From any thread, cancel the stream by ID (thread-safe)
|
|
878
|
+
println(s"\nCalling mux.cancel(3, ...)...")
|
|
879
|
+
mux.cancel(3, MuxError.Cancelled(3, "Cleanup: application shutting down"))
|
|
880
|
+
|
|
881
|
+
println(s"After mux.cancel():")
|
|
882
|
+
println(s" isClosed: ${stream3.isClosed}")
|
|
883
|
+
|
|
884
|
+
println("\n=== Summary ===")
|
|
885
|
+
println("Graceful shutdown: halfClose() + signalRemoteClose() (single-threaded)")
|
|
886
|
+
println("Immediate closure: close() (single-threaded)")
|
|
887
|
+
println("External cancellation: mux.cancel(id, reason) (thread-safe)")
|
|
888
|
+
}
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
</details>
|
|
892
|
+
|
|
893
|
+
Observe the output: state transitions from OPEN → HALF_CLOSED → CLOSED in graceful shutdown, instant transition in immediate closure, and the final closed state after external cancellation.
|
|
894
|
+
|
|
895
|
+
```bash
|
|
896
|
+
sbt "mux-examples/runMain mux.example3StreamLifecycle"
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
### Working with Multiple Streams
|
|
900
|
+
|
|
901
|
+
This example opens three independent streams and exchanges messages on each one, demonstrating that streams are isolated with no message crosstalk and each has its own inbound and outbound queues.
|
|
902
|
+
|
|
903
|
+
<details>
|
|
904
|
+
<summary>mux-examples/src/main/scala/mux/Example4WorkingWithMultipleStreams.scala</summary>
|
|
905
|
+
|
|
906
|
+
```scala title="mux-examples/src/main/scala/mux/Example4WorkingWithMultipleStreams.scala" showLineNumbers
|
|
907
|
+
/*
|
|
908
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
909
|
+
*
|
|
910
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
911
|
+
* you may not use this file except in compliance with the License.
|
|
912
|
+
* You may obtain a copy of the License at
|
|
913
|
+
*
|
|
914
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
915
|
+
*
|
|
916
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
917
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
918
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
919
|
+
* See the License for the specific language governing permissions and
|
|
920
|
+
* limitations under the License.
|
|
921
|
+
*/
|
|
922
|
+
|
|
923
|
+
package mux
|
|
924
|
+
|
|
925
|
+
import zio.blocks.mux._
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Title: Working with Multiple Streams Description: Demonstrate that multiple
|
|
929
|
+
* streams are independent: each has its own queues with no crosstalk, and
|
|
930
|
+
* messages stay on their originating stream. Run: sbt "mux-examples/runMain
|
|
931
|
+
* mux.example4WorkingWithMultipleStreams"
|
|
932
|
+
*/
|
|
933
|
+
@main def example4WorkingWithMultipleStreams(): Unit = {
|
|
934
|
+
println("=== Working with Multiple Streams ===\n")
|
|
935
|
+
|
|
936
|
+
val mux = Mux[Int, String, String](100)
|
|
937
|
+
|
|
938
|
+
println("--- Opening three streams ---")
|
|
939
|
+
// Open three streams
|
|
940
|
+
val stream1 = mux.open(1) match {
|
|
941
|
+
case s: MuxStream[Int, String, String] => s
|
|
942
|
+
case e: MuxError => throw new RuntimeException(s"Failed to open stream 1: $e")
|
|
943
|
+
}
|
|
944
|
+
val stream2 = mux.open(2) match {
|
|
945
|
+
case s: MuxStream[Int, String, String] => s
|
|
946
|
+
case e: MuxError => throw new RuntimeException(s"Failed to open stream 2: $e")
|
|
947
|
+
}
|
|
948
|
+
val stream3 = mux.open(3) match {
|
|
949
|
+
case s: MuxStream[Int, String, String] => s
|
|
950
|
+
case e: MuxError => throw new RuntimeException(s"Failed to open stream 3: $e")
|
|
951
|
+
}
|
|
952
|
+
println("✓ Opened streams 1, 2, 3")
|
|
953
|
+
|
|
954
|
+
println("\n--- Application sends messages on each stream ---")
|
|
955
|
+
// Send different messages on each stream
|
|
956
|
+
stream1.send("Message for stream 1") match { case () => (); case e: MuxError => println(s"Stream 1 send error: $e") }
|
|
957
|
+
stream2.send("Message for stream 2") match { case () => (); case e: MuxError => println(s"Stream 2 send error: $e") }
|
|
958
|
+
stream3.send("Message for stream 3") match { case () => (); case e: MuxError => println(s"Stream 3 send error: $e") }
|
|
959
|
+
println("✓ Sent outbound messages on all streams")
|
|
960
|
+
|
|
961
|
+
println("\n--- Protocol extracts messages (no crosstalk) ---")
|
|
962
|
+
// Extract messages — they stay on their own stream
|
|
963
|
+
val out1 = stream1.takeOutbound() match {
|
|
964
|
+
case Some(msg) => msg
|
|
965
|
+
case None => "(empty)"
|
|
966
|
+
case e: MuxError => s"error: $e"
|
|
967
|
+
}
|
|
968
|
+
val out2 = stream2.takeOutbound() match {
|
|
969
|
+
case Some(msg) => msg
|
|
970
|
+
case None => "(empty)"
|
|
971
|
+
case e: MuxError => s"error: $e"
|
|
972
|
+
}
|
|
973
|
+
val out3 = stream3.takeOutbound() match {
|
|
974
|
+
case Some(msg) => msg
|
|
975
|
+
case None => "(empty)"
|
|
976
|
+
case e: MuxError => s"error: $e"
|
|
977
|
+
}
|
|
978
|
+
println(s"Stream 1 outbound: '$out1'")
|
|
979
|
+
println(s"Stream 2 outbound: '$out2'")
|
|
980
|
+
println(s"Stream 3 outbound: '$out3'")
|
|
981
|
+
|
|
982
|
+
println("\n--- Protocol delivers responses to different streams ---")
|
|
983
|
+
// Deliver responses to different streams
|
|
984
|
+
stream1.offerInbound("Response for stream 1") match {
|
|
985
|
+
case () => (); case e: MuxError => println(s"Stream 1 offer error: $e")
|
|
986
|
+
}
|
|
987
|
+
stream2.offerInbound("Response for stream 2") match {
|
|
988
|
+
case () => (); case e: MuxError => println(s"Stream 2 offer error: $e")
|
|
989
|
+
}
|
|
990
|
+
stream3.offerInbound("Response for stream 3") match {
|
|
991
|
+
case () => (); case e: MuxError => println(s"Stream 3 offer error: $e")
|
|
992
|
+
}
|
|
993
|
+
println("✓ Delivered inbound responses on all streams")
|
|
994
|
+
|
|
995
|
+
println("\n--- Application receives on each stream (no crosstalk) ---")
|
|
996
|
+
// Receive on each stream — no crosstalk
|
|
997
|
+
val in1 = stream1.receive() match {
|
|
998
|
+
case Some(msg) => msg
|
|
999
|
+
case None => "(empty)"
|
|
1000
|
+
case e: MuxError => s"error: $e"
|
|
1001
|
+
}
|
|
1002
|
+
val in2 = stream2.receive() match {
|
|
1003
|
+
case Some(msg) => msg
|
|
1004
|
+
case None => "(empty)"
|
|
1005
|
+
case e: MuxError => s"error: $e"
|
|
1006
|
+
}
|
|
1007
|
+
val in3 = stream3.receive() match {
|
|
1008
|
+
case Some(msg) => msg
|
|
1009
|
+
case None => "(empty)"
|
|
1010
|
+
case e: MuxError => s"error: $e"
|
|
1011
|
+
}
|
|
1012
|
+
println(s"Stream 1 inbound: '$in1'")
|
|
1013
|
+
println(s"Stream 2 inbound: '$in2'")
|
|
1014
|
+
println(s"Stream 3 inbound: '$in3'")
|
|
1015
|
+
|
|
1016
|
+
println("\n=== Summary ===")
|
|
1017
|
+
println("Each stream is independent:")
|
|
1018
|
+
println(" - Own outbound and inbound queues")
|
|
1019
|
+
println(" - No message crosstalk between streams")
|
|
1020
|
+
println(" - Closing one stream doesn't affect others")
|
|
1021
|
+
}
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
</details>
|
|
1025
|
+
|
|
1026
|
+
Observe the output: messages sent on stream 1 appear only in stream 1's outbound queue, never in streams 2 or 3, proving complete independence.
|
|
1027
|
+
|
|
1028
|
+
```bash
|
|
1029
|
+
sbt "mux-examples/runMain mux.example4WorkingWithMultipleStreams"
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
### Managing Capacity
|
|
1033
|
+
|
|
1034
|
+
This example demonstrates both mux-level capacity (controlling concurrent streams) and per-stream capacity (controlling message queue depth), showing how to handle QueueFull errors and the recovery pattern of draining to free space.
|
|
1035
|
+
|
|
1036
|
+
<details>
|
|
1037
|
+
<summary>mux-examples/src/main/scala/mux/Example5ManagingCapacity.scala</summary>
|
|
1038
|
+
|
|
1039
|
+
```scala title="mux-examples/src/main/scala/mux/Example5ManagingCapacity.scala" showLineNumbers
|
|
1040
|
+
/*
|
|
1041
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1042
|
+
*
|
|
1043
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1044
|
+
* you may not use this file except in compliance with the License.
|
|
1045
|
+
* You may obtain a copy of the License at
|
|
1046
|
+
*
|
|
1047
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1048
|
+
*
|
|
1049
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1050
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1051
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1052
|
+
* See the License for the specific language governing permissions and
|
|
1053
|
+
* limitations under the License.
|
|
1054
|
+
*/
|
|
1055
|
+
|
|
1056
|
+
package mux
|
|
1057
|
+
|
|
1058
|
+
import zio.blocks.mux._
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Title: Managing Capacity Description: Demonstrate mux-level and per-stream
|
|
1062
|
+
* capacity limits, handling QueueFull errors, and the recovery pattern of
|
|
1063
|
+
* draining outbound messages to free space. Run: sbt "mux-examples/runMain
|
|
1064
|
+
* mux.example5ManagingCapacity"
|
|
1065
|
+
*/
|
|
1066
|
+
@main def example5ManagingCapacity(): Unit = {
|
|
1067
|
+
println("=== Managing Capacity ===\n")
|
|
1068
|
+
|
|
1069
|
+
println("--- Mux-level capacity (maximum concurrent streams) ---")
|
|
1070
|
+
// Create a tiny mux with a small capacity (for demo)
|
|
1071
|
+
val mux = Mux[Int, String, String](3)
|
|
1072
|
+
println("Created mux with capacity for 3 concurrent streams")
|
|
1073
|
+
|
|
1074
|
+
// Open streams until capacity is exceeded
|
|
1075
|
+
println("\nOpening streams:")
|
|
1076
|
+
for (i <- 1 to 5) {
|
|
1077
|
+
mux.open(i) match {
|
|
1078
|
+
case _: MuxStream[Int, String, String] =>
|
|
1079
|
+
println(s" ✓ Opened stream $i")
|
|
1080
|
+
case error: MuxError =>
|
|
1081
|
+
println(s" ✗ Failed to open stream $i: $error")
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
println("\n--- Per-stream capacity (message queue limits) ---")
|
|
1086
|
+
// Create a fresh mux for this demo
|
|
1087
|
+
val mux2 = Mux[Int, String, String](100)
|
|
1088
|
+
val stream = mux2.open(10) match {
|
|
1089
|
+
case s: MuxStream[Int, String, String] => s
|
|
1090
|
+
case error: MuxError =>
|
|
1091
|
+
println(s"Failed to open stream: $error")
|
|
1092
|
+
throw new RuntimeException("Cannot proceed")
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
println("Sending messages until queue fills (per-stream capacity is 256):")
|
|
1096
|
+
var successCount = 0
|
|
1097
|
+
var failureCount = 0
|
|
1098
|
+
|
|
1099
|
+
for (i <- 1 to 300) {
|
|
1100
|
+
val result = stream.send(s"Message $i")
|
|
1101
|
+
result match {
|
|
1102
|
+
case () =>
|
|
1103
|
+
successCount += 1
|
|
1104
|
+
if (i <= 5 || i == 256) println(s" ✓ Message $i sent")
|
|
1105
|
+
case error: MuxError =>
|
|
1106
|
+
failureCount += 1
|
|
1107
|
+
if (failureCount <= 3) println(s" ✗ Message $i failed: $error")
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
println(s"Summary: $successCount succeeded, $failureCount failed")
|
|
1111
|
+
|
|
1112
|
+
println("\n--- Recovery pattern: draining to free space ---")
|
|
1113
|
+
// Fresh mux for recovery demo
|
|
1114
|
+
val mux3 = Mux[Int, String, String](100)
|
|
1115
|
+
val stream2 = mux3.open(11) match {
|
|
1116
|
+
case s: MuxStream[Int, String, String] => s
|
|
1117
|
+
case e: MuxError => throw new RuntimeException(s"Failed to open stream 11: $e")
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Fill the queue to capacity (256 messages per direction)
|
|
1121
|
+
println("Filling queue to capacity (256 messages):")
|
|
1122
|
+
for (i <- 1 to 256) {
|
|
1123
|
+
stream2.send(s"Message $i") match {
|
|
1124
|
+
case () => if (i == 1 || i == 256) println(s" ✓ Message $i sent")
|
|
1125
|
+
case e: MuxError => println(s" ✗ Unexpected failure at $i: $e")
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Queue is now full — the next send returns QueueFull
|
|
1130
|
+
println("\nAttempting send when queue is full:")
|
|
1131
|
+
stream2.send("overflow") match {
|
|
1132
|
+
case () => println(" (unexpected: send succeeded)")
|
|
1133
|
+
case e: MuxError => println(s" ✗ Expected error: $e")
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Protocol drains one message to free space
|
|
1137
|
+
println("\nProtocol drains one message from outbound queue:")
|
|
1138
|
+
stream2.takeOutbound() match {
|
|
1139
|
+
case Some(msg) => println(s" Extracted: '$msg'")
|
|
1140
|
+
case None => println(" (queue empty)")
|
|
1141
|
+
case e: MuxError => println(s" Error: $e")
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Retry after draining — now succeeds
|
|
1145
|
+
println("Retrying send after draining:")
|
|
1146
|
+
stream2.send("overflow") match {
|
|
1147
|
+
case () => println("✓ Send succeeded after draining (recovery pattern works)")
|
|
1148
|
+
case e: MuxError => println(s"✗ Still failed: $e")
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
println("\n=== Summary ===")
|
|
1152
|
+
println("Capacity limits prevent memory exhaustion:")
|
|
1153
|
+
println(" - Mux-level: controls number of concurrent streams")
|
|
1154
|
+
println(" - Per-stream: limits messages in each queue (256 per direction)")
|
|
1155
|
+
println("Recovery pattern: drain outbound queue to free space")
|
|
1156
|
+
}
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
</details>
|
|
1160
|
+
|
|
1161
|
+
Observe the output: the mux rejects stream 4 and 5 with CapacityExceeded, messages 257+ fail with QueueFull, and after draining one message, sending resumes successfully.
|
|
1162
|
+
|
|
1163
|
+
```bash
|
|
1164
|
+
sbt "mux-examples/runMain mux.example5ManagingCapacity"
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
### Thread Safety
|
|
1168
|
+
|
|
1169
|
+
This example documents which operations are thread-safe (mux-level and send/offerInbound) versus single-threaded (state transitions and message dequeuing), with a correct usage pattern showing how threads interact safely.
|
|
1170
|
+
|
|
1171
|
+
<details>
|
|
1172
|
+
<summary>mux-examples/src/main/scala/mux/Example6ThreadSafety.scala</summary>
|
|
1173
|
+
|
|
1174
|
+
```scala title="mux-examples/src/main/scala/mux/Example6ThreadSafety.scala"
|
|
1175
|
+
/*
|
|
1176
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1177
|
+
*
|
|
1178
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1179
|
+
* you may not use this file except in compliance with the License.
|
|
1180
|
+
* You may obtain a copy of the License at
|
|
1181
|
+
*
|
|
1182
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1183
|
+
*
|
|
1184
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1185
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1186
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1187
|
+
* See the License for the specific language governing permissions and
|
|
1188
|
+
* limitations under the License.
|
|
1189
|
+
*/
|
|
1190
|
+
|
|
1191
|
+
package mux
|
|
1192
|
+
|
|
1193
|
+
import zio.blocks.mux._
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Title: Thread Safety Description: Demonstrate thread-safe vs single-threaded
|
|
1197
|
+
* operations, correct usage patterns, and what operations can be called from
|
|
1198
|
+
* which context. Run: sbt "mux-examples/runMain mux.example6ThreadSafety"
|
|
1199
|
+
*/
|
|
1200
|
+
@main def example6ThreadSafety(): Unit = {
|
|
1201
|
+
println("=== Thread Safety: Operations and Constraints ===\n")
|
|
1202
|
+
|
|
1203
|
+
val mux = Mux[Int, String, String](100)
|
|
1204
|
+
|
|
1205
|
+
println("--- Mux-level thread-safe operations (call from any thread) ---")
|
|
1206
|
+
println("✓ mux.open(id) — thread-safe")
|
|
1207
|
+
println("✓ mux.get(id) — thread-safe")
|
|
1208
|
+
println("✓ mux.cancel(id, reason) — thread-safe")
|
|
1209
|
+
println("✓ mux.closeAll(reason) — thread-safe")
|
|
1210
|
+
|
|
1211
|
+
println("\n--- Stream-level thread-safe operations (multiple threads OK) ---")
|
|
1212
|
+
println("✓ stream.send(msg) — thread-safe (uses lock-free ring buffer on JVM)")
|
|
1213
|
+
println("✓ stream.offerInbound(msg) — thread-safe (uses lock-free ring buffer on JVM)")
|
|
1214
|
+
|
|
1215
|
+
println("\n--- Stream-level SINGLE-THREADED operations (ONE thread only) ---")
|
|
1216
|
+
println("✗ stream.receive() — single-threaded (corrupts if called concurrently)")
|
|
1217
|
+
println("✗ stream.takeOutbound() — single-threaded (corrupts if called concurrently)")
|
|
1218
|
+
println("✗ stream.halfClose() — single-threaded (state transition)")
|
|
1219
|
+
println("✗ stream.signalRemoteClose() — single-threaded (state transition)")
|
|
1220
|
+
println("✗ stream.close() — single-threaded (state transition)")
|
|
1221
|
+
|
|
1222
|
+
println("\n--- Correct usage pattern ---\n")
|
|
1223
|
+
|
|
1224
|
+
// Reset
|
|
1225
|
+
val stream2 = mux.open(2) match {
|
|
1226
|
+
case s: MuxStream[Int, String, String] => s
|
|
1227
|
+
case e: MuxError => throw new RuntimeException(s"Failed to open stream 2: $e")
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
println("Thread A (Application thread): calls send() consistently")
|
|
1231
|
+
stream2.send("Message 1") match {
|
|
1232
|
+
case () => println(" ✓ Sent message 1 from thread A")
|
|
1233
|
+
case e: MuxError => println(s" ✗ Error: $e")
|
|
1234
|
+
}
|
|
1235
|
+
stream2.send("Message 2") match {
|
|
1236
|
+
case () => println(" ✓ Sent message 2 from thread A")
|
|
1237
|
+
case e: MuxError => println(s" ✗ Error: $e")
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
println("\nThread B (Protocol/Read thread): calls takeOutbound() consistently")
|
|
1241
|
+
stream2.takeOutbound() match {
|
|
1242
|
+
case Some(msg) => println(s" ✓ Extracted from thread B: '$msg'")
|
|
1243
|
+
case None => println(" (queue empty)")
|
|
1244
|
+
case e: MuxError => println(s" ✗ Error: $e")
|
|
1245
|
+
}
|
|
1246
|
+
stream2.takeOutbound() match {
|
|
1247
|
+
case Some(msg) => println(s" ✓ Extracted from thread B: '$msg'")
|
|
1248
|
+
case None => println(" (queue empty)")
|
|
1249
|
+
case e: MuxError => println(s" ✗ Error: $e")
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
println("\nThreads C, D, E, ... (Multiple inbound delivery threads): can safely call offerInbound()")
|
|
1253
|
+
for (threadId <- 1 to 3) {
|
|
1254
|
+
stream2.offerInbound(s"Response from thread $threadId") match {
|
|
1255
|
+
case () => println(s" ✓ Offered inbound from thread $threadId (thread-safe)")
|
|
1256
|
+
case e: MuxError => println(s" ✗ Error: $e")
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
println("\nThread A (back on Application thread): calls receive() consistently")
|
|
1261
|
+
stream2.receive() match {
|
|
1262
|
+
case Some(msg) => println(s" ✓ Received from thread A: '$msg'")
|
|
1263
|
+
case None => println(" (queue empty)")
|
|
1264
|
+
case e: MuxError => println(s" ✗ Error: $e")
|
|
1265
|
+
}
|
|
1266
|
+
stream2.receive() match {
|
|
1267
|
+
case Some(msg) => println(s" ✓ Received from thread A: '$msg'")
|
|
1268
|
+
case None => println(" (queue empty)")
|
|
1269
|
+
case e: MuxError => println(s" ✗ Error: $e")
|
|
1270
|
+
}
|
|
1271
|
+
stream2.receive() match {
|
|
1272
|
+
case Some(msg) => println(s" ✓ Received from thread A: '$msg'")
|
|
1273
|
+
case None => println(" (queue empty)")
|
|
1274
|
+
case e: MuxError => println(s" ✗ Error: $e")
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
println("\n--- Closing streams: thread safety comparison ---\n")
|
|
1278
|
+
|
|
1279
|
+
println("Single-threaded close (from dedicated thread):")
|
|
1280
|
+
println(" stream.halfClose() + stream.signalRemoteClose()")
|
|
1281
|
+
println(" OR stream.close()")
|
|
1282
|
+
|
|
1283
|
+
println("\nThread-safe external cancel (from any thread):")
|
|
1284
|
+
println(" mux.cancel(id, reason)")
|
|
1285
|
+
println(" → Call this when closing from cleanup handler or different thread")
|
|
1286
|
+
mux.cancel(2, MuxError.Cancelled(2, "Example cleanup"))
|
|
1287
|
+
println(" ✓ Cancelled stream 2 from main thread (would work from any thread)")
|
|
1288
|
+
|
|
1289
|
+
println("\n=== Summary ===")
|
|
1290
|
+
println("✓ send() and offerInbound() are thread-safe (use lock-free queues)")
|
|
1291
|
+
println("✓ mux operations (open, cancel, closeAll) are thread-safe")
|
|
1292
|
+
println("✗ receive(), takeOutbound(), and state transitions must use ONE thread")
|
|
1293
|
+
println("✓ Use mux.cancel(id, reason) for thread-safe external cancellation")
|
|
1294
|
+
}
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
</details>
|
|
1298
|
+
|
|
1299
|
+
Observe the output: mux.open and mux.cancel are declared thread-safe, while receive() and takeOutbound() must use single threads, and the pattern shows multiple threads safely offering inbound messages.
|
|
1300
|
+
|
|
1301
|
+
```bash
|
|
1302
|
+
sbt "mux-examples/runMain mux.example6ThreadSafety"
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1305
|
+
### Putting It All Together
|
|
1306
|
+
|
|
1307
|
+
This comprehensive example demonstrates a complete request-response system that ties together all concepts: creating a mux, opening multiple streams, exchanging messages, handling errors, managing lifecycle transitions, and verifying independence.
|
|
1308
|
+
|
|
1309
|
+
<details>
|
|
1310
|
+
<summary>mux-examples/src/main/scala/mux/CompleteExample.scala</summary>
|
|
1311
|
+
|
|
1312
|
+
```scala title="mux-examples/src/main/scala/mux/CompleteExample.scala" showLineNumbers
|
|
1313
|
+
/*
|
|
1314
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1315
|
+
*
|
|
1316
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1317
|
+
* you may not use this file except in compliance with the License.
|
|
1318
|
+
* You may obtain a copy of the License at
|
|
1319
|
+
*
|
|
1320
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1321
|
+
*
|
|
1322
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1323
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1324
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1325
|
+
* See the License for the specific language governing permissions and
|
|
1326
|
+
* limitations under the License.
|
|
1327
|
+
*/
|
|
1328
|
+
|
|
1329
|
+
package mux
|
|
1330
|
+
|
|
1331
|
+
import zio.blocks.mux._
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Title: Putting It Together Description: Complete, runnable example that
|
|
1335
|
+
* demonstrates all mux concepts: creating mux, opening streams, exchanging
|
|
1336
|
+
* messages, handling errors, managing capacity, and closing gracefully. This is
|
|
1337
|
+
* the comprehensive end-to-end demonstration showing real request-response
|
|
1338
|
+
* communication. Run: sbt "mux-examples/runMain mux.completeExample"
|
|
1339
|
+
*/
|
|
1340
|
+
@main def completeExample(): Unit = {
|
|
1341
|
+
println("=" * 70)
|
|
1342
|
+
println("COMPLETE MUX EXAMPLE: Request-Response Communication")
|
|
1343
|
+
println("=" * 70)
|
|
1344
|
+
|
|
1345
|
+
// Create a mux: stream IDs are Int, messages are String in both directions
|
|
1346
|
+
val mux = Mux[Int, String, String](100)
|
|
1347
|
+
println("\n✓ Created mux with capacity for 100 concurrent streams\n")
|
|
1348
|
+
|
|
1349
|
+
println("=" * 70)
|
|
1350
|
+
println("OPENING STREAMS")
|
|
1351
|
+
println("=" * 70)
|
|
1352
|
+
|
|
1353
|
+
// Open three streams, handling potential capacity errors
|
|
1354
|
+
val streams = (1 to 3).map { id =>
|
|
1355
|
+
mux.open(id) match {
|
|
1356
|
+
case stream: MuxStream[Int, String, String] =>
|
|
1357
|
+
println(s"✓ Opened stream $id")
|
|
1358
|
+
stream
|
|
1359
|
+
case error: MuxError =>
|
|
1360
|
+
println(s"✗ Failed to open stream $id: $error")
|
|
1361
|
+
throw new RuntimeException(s"Cannot proceed without stream $id")
|
|
1362
|
+
}
|
|
1363
|
+
}.toList
|
|
1364
|
+
|
|
1365
|
+
println("\n" + "=" * 70)
|
|
1366
|
+
println("APPLICATION SENDS MESSAGES")
|
|
1367
|
+
println("=" * 70)
|
|
1368
|
+
|
|
1369
|
+
// Application sends messages on each stream
|
|
1370
|
+
println()
|
|
1371
|
+
streams.zipWithIndex.foreach { case (stream, idx) =>
|
|
1372
|
+
val msg = s"Hello from stream ${idx + 1}"
|
|
1373
|
+
stream.send(msg) match {
|
|
1374
|
+
case () => println(s"✓ Stream ${idx + 1} sent: '$msg'")
|
|
1375
|
+
case _: MuxError => println(s"✗ Stream ${idx + 1} send failed")
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
println("\n" + "=" * 70)
|
|
1380
|
+
println("PROTOCOL EXTRACTS OUTBOUND MESSAGES")
|
|
1381
|
+
println("=" * 70)
|
|
1382
|
+
|
|
1383
|
+
// Protocol extracts messages to send over the network
|
|
1384
|
+
println()
|
|
1385
|
+
streams.zipWithIndex.foreach { case (stream, idx) =>
|
|
1386
|
+
stream.takeOutbound() match {
|
|
1387
|
+
case Some(msg) =>
|
|
1388
|
+
println(s"✓ Protocol will send on stream ${idx + 1}: '$msg'")
|
|
1389
|
+
case None =>
|
|
1390
|
+
println(s"✗ Stream ${idx + 1} has no outbound messages")
|
|
1391
|
+
case _: MuxError =>
|
|
1392
|
+
println(s"✗ Stream ${idx + 1} takeOutbound failed")
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
println("\n" + "=" * 70)
|
|
1397
|
+
println("PROTOCOL DELIVERS INBOUND MESSAGES")
|
|
1398
|
+
println("=" * 70)
|
|
1399
|
+
|
|
1400
|
+
// Protocol delivers responses received from the peer
|
|
1401
|
+
println()
|
|
1402
|
+
streams.zipWithIndex.foreach { case (stream, idx) =>
|
|
1403
|
+
val response = s"Response from peer on stream ${idx + 1}"
|
|
1404
|
+
stream.offerInbound(response) match {
|
|
1405
|
+
case () => println(s"✓ Protocol delivered on stream ${idx + 1}: '$response'")
|
|
1406
|
+
case _: MuxError => println(s"✗ Stream ${idx + 1} offerInbound failed")
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
println("\n" + "=" * 70)
|
|
1411
|
+
println("APPLICATION RECEIVES MESSAGES")
|
|
1412
|
+
println("=" * 70)
|
|
1413
|
+
|
|
1414
|
+
// Application receives the responses
|
|
1415
|
+
println()
|
|
1416
|
+
streams.zipWithIndex.foreach { case (stream, idx) =>
|
|
1417
|
+
stream.receive() match {
|
|
1418
|
+
case Some(msg) =>
|
|
1419
|
+
println(s"✓ Stream ${idx + 1} received: '$msg'")
|
|
1420
|
+
case None =>
|
|
1421
|
+
println(s"✗ Stream ${idx + 1} has no inbound messages")
|
|
1422
|
+
case _: MuxError =>
|
|
1423
|
+
println(s"✗ Stream ${idx + 1} receive failed")
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
println("\n" + "=" * 70)
|
|
1428
|
+
println("GRACEFUL SHUTDOWN")
|
|
1429
|
+
println("=" * 70)
|
|
1430
|
+
|
|
1431
|
+
// Streams close gracefully
|
|
1432
|
+
println()
|
|
1433
|
+
val stream1 = streams(0)
|
|
1434
|
+
println("Stream 1: signalling local half-close...")
|
|
1435
|
+
stream1.halfClose()
|
|
1436
|
+
println(s" isClosed: ${stream1.isClosed}, isHalfClosed: ${stream1.isHalfClosed}")
|
|
1437
|
+
|
|
1438
|
+
println("Stream 1: signalling remote close...")
|
|
1439
|
+
stream1.signalRemoteClose()
|
|
1440
|
+
println(s" isClosed: ${stream1.isClosed}, isHalfClosed: ${stream1.isHalfClosed}")
|
|
1441
|
+
|
|
1442
|
+
// Verify the stream is closed
|
|
1443
|
+
assert(stream1.isClosed, "Stream 1 should be closed")
|
|
1444
|
+
println("✓ Stream 1 is fully closed")
|
|
1445
|
+
|
|
1446
|
+
println("\n" + "=" * 70)
|
|
1447
|
+
println("VERIFY INDEPENDENCE")
|
|
1448
|
+
println("=" * 70)
|
|
1449
|
+
|
|
1450
|
+
// Streams 2 and 3 are unaffected by stream 1 closing
|
|
1451
|
+
println()
|
|
1452
|
+
println(s"Stream 2 is open: ${!streams(1).isClosed}")
|
|
1453
|
+
println(s"Stream 3 is open: ${!streams(2).isClosed}")
|
|
1454
|
+
|
|
1455
|
+
println("\n" + "=" * 70)
|
|
1456
|
+
println("IMMEDIATE CLOSURE")
|
|
1457
|
+
println("=" * 70)
|
|
1458
|
+
|
|
1459
|
+
// Demonstrate immediate closure on stream 2
|
|
1460
|
+
println()
|
|
1461
|
+
val stream2 = streams(1)
|
|
1462
|
+
println("Stream 2: performing immediate close()...")
|
|
1463
|
+
stream2.close()
|
|
1464
|
+
println(s" isClosed: ${stream2.isClosed}")
|
|
1465
|
+
println("✓ Stream 2 is immediately closed")
|
|
1466
|
+
|
|
1467
|
+
println("\n" + "=" * 70)
|
|
1468
|
+
println("EXTERNAL CANCELLATION")
|
|
1469
|
+
println("=" * 70)
|
|
1470
|
+
|
|
1471
|
+
// Demonstrate external cancellation on stream 3
|
|
1472
|
+
println()
|
|
1473
|
+
println("Stream 3: performing external mux.cancel()...")
|
|
1474
|
+
mux.cancel(3, MuxError.Cancelled(3, "Application shutdown"))
|
|
1475
|
+
val stream3 = streams(2)
|
|
1476
|
+
println(s" isClosed: ${stream3.isClosed}")
|
|
1477
|
+
println("✓ Stream 3 is cancelled")
|
|
1478
|
+
|
|
1479
|
+
println("\n" + "=" * 70)
|
|
1480
|
+
println("EXAMPLE COMPLETE")
|
|
1481
|
+
println("=" * 70)
|
|
1482
|
+
println("\nYou've seen:")
|
|
1483
|
+
println(" • Creating a mux with capacity control")
|
|
1484
|
+
println(" • Opening multiple independent streams")
|
|
1485
|
+
println(" • Application sending messages (send/receive)")
|
|
1486
|
+
println(" • Protocol exchanging messages (takeOutbound/offerInbound)")
|
|
1487
|
+
println(" • Graceful shutdown (halfClose + signalRemoteClose)")
|
|
1488
|
+
println(" • Immediate closure (close())")
|
|
1489
|
+
println(" • External cancellation (mux.cancel())")
|
|
1490
|
+
println(" • Stream independence and isolation")
|
|
1491
|
+
println("\n✓ All concepts demonstrated successfully!")
|
|
1492
|
+
}
|
|
1493
|
+
```
|
|
1494
|
+
|
|
1495
|
+
</details>
|
|
1496
|
+
|
|
1497
|
+
Observe the output: three streams are opened, messages flow through send/receive and takeOutbound/offerInbound, graceful shutdown completes with halfClose/signalRemoteClose, and other streams remain unaffected.
|
|
1498
|
+
|
|
1499
|
+
```bash
|
|
1500
|
+
sbt "mux-examples/runMain mux.completeExample"
|
|
1501
|
+
```
|
|
1502
|
+
|
|
1503
|
+
## Where to Go Next
|
|
1504
|
+
|
|
1505
|
+
- **Want to dive deeper into the API?** Read the reference page for [`Mux`](../reference/mux.mdx) for complete method signatures and advanced patterns.
|
|
1506
|
+
- **Ready to explore related concepts?** Check out the [Ring Buffer](../reference/ringbuffer/index.mdx) reference for understanding lock-free data structures used internally.
|
|
1507
|
+
- **Interested in other concurrency and stream management?** Explore other libraries in ZIO Blocks for managing complex async workflows.
|