@zio.dev/zio-blocks 0.0.33 → 0.0.55
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/adr/2026-07-18-data-migration.md +123 -0
- package/guides/async-getting-started.md +687 -0
- package/guides/compile-time-resource-safety-with-scope.md +21 -16
- package/guides/getting-started-with-mux.md +1395 -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 +640 -165
- package/guides/sql-checked-interpolation.md +173 -0
- package/guides/sql-transactions.md +286 -0
- package/guides/telemetry-guide.md +1130 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +248 -389
- 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 +1499 -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/config-decoder.md +460 -0
- package/reference/config/config-source.md +489 -0
- package/reference/config/errors.md +278 -0
- package/reference/config/flags.md +369 -0
- package/reference/config/formats.md +314 -0
- package/reference/config/index.md +304 -0
- package/reference/config/rollout.md +336 -0
- package/reference/context.md +9 -52
- package/reference/data-migration.md +269 -0
- package/reference/datastar/attributes.md +302 -0
- package/reference/datastar/events.md +234 -0
- package/reference/datastar/index.md +256 -0
- package/reference/datastar/signals.md +230 -0
- package/reference/datastar/sse.md +295 -0
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/bulk-creation.md +96 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +745 -0
- package/reference/endpoint/path-codec.md +225 -0
- package/reference/endpoint/route-pattern.md +194 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +199 -0
- package/reference/html.md +1424 -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 +807 -0
- package/reference/htmx/response-headers.md +240 -0
- package/reference/http-model/headers.md +735 -0
- package/reference/http-model/index.md +49 -0
- package/reference/http-model/model.md +1517 -0
- package/reference/http-model/schema-codecs.md +522 -0
- package/reference/http-model/schema.md +750 -0
- package/reference/http-model/server-sent-event.md +341 -0
- package/reference/jwt.md +195 -0
- package/reference/maybe.md +943 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.md +254 -0
- package/reference/mux.mdx +828 -0
- package/reference/openapi.md +1351 -0
- package/reference/projection.md +654 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -98
- package/reference/resource-management/scope.md +28 -220
- package/reference/resource-management/wire.md +5 -55
- 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 +185 -0
- package/reference/ringbuffer/mpsc.mdx +164 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +416 -0
- package/reference/{allows.md → schema/allows.md} +4 -100
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +3 -4
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +510 -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} +11 -11
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +196 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +52 -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} +167 -72
- package/reference/schema/reflect-transformer.md +140 -0
- 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/schema-search.md +263 -0
- package/reference/{schema.md → schema/schema.md} +22 -2
- 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 +1032 -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 +148 -0
- package/reference/sql/db-tx.md +114 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +288 -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 +363 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/core/index.md +32 -0
- package/reference/streams/core/pipeline.md +854 -0
- package/reference/streams/core/sink.md +1404 -0
- package/reference/streams/core/stream.md +3236 -0
- package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
- package/reference/streams/execution-and-compatibility/index.md +35 -0
- package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
- package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
- package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
- package/reference/streams/index.md +726 -0
- package/reference/streams/primitives/index.md +30 -0
- package/reference/streams/primitives/reader.md +1992 -0
- package/reference/streams/primitives/writer.md +1201 -0
- package/reference/telemetry/common/any-value.md +90 -0
- package/reference/telemetry/common/attribute-key.md +87 -0
- package/reference/telemetry/common/attributes.md +118 -0
- package/reference/telemetry/common/index.md +39 -0
- package/reference/telemetry/common/instrumentation-scope.md +24 -0
- package/reference/telemetry/common/resource.md +34 -0
- package/reference/telemetry/index.md +311 -0
- package/reference/telemetry/logging/index.md +197 -0
- package/reference/telemetry/logging/log-enrichment.md +72 -0
- package/reference/telemetry/logging/log-formatter.md +100 -0
- package/reference/telemetry/logging/log-record-processor.md +56 -0
- package/reference/telemetry/logging/log-record.md +44 -0
- package/reference/telemetry/logging/log-writer.md +64 -0
- package/reference/telemetry/logging/logger-provider.md +142 -0
- package/reference/telemetry/logging/logger.md +83 -0
- package/reference/telemetry/logging/severity.md +62 -0
- package/reference/telemetry/metrics/index.md +150 -0
- package/reference/telemetry/metrics/instruments.md +183 -0
- package/reference/telemetry/metrics/labeled-instruments.md +74 -0
- package/reference/telemetry/metrics/meter-provider.md +76 -0
- package/reference/telemetry/metrics/meter.md +98 -0
- package/reference/telemetry/metrics/metric-data.md +57 -0
- package/reference/telemetry/otel/custom-exporter.md +216 -0
- package/reference/telemetry/otel/index.md +212 -0
- package/reference/telemetry/tracing/index.md +155 -0
- package/reference/telemetry/tracing/sampler.md +89 -0
- package/reference/telemetry/tracing/span-builder.md +57 -0
- package/reference/telemetry/tracing/span-context.md +39 -0
- package/reference/telemetry/tracing/span-data.md +32 -0
- package/reference/telemetry/tracing/span-kind.md +55 -0
- package/reference/telemetry/tracing/span-processor.md +53 -0
- package/reference/telemetry/tracing/span-status.md +47 -0
- package/reference/telemetry/tracing/span.md +117 -0
- package/reference/telemetry/tracing/tracer-provider.md +91 -0
- package/reference/telemetry/tracing/tracer.md +52 -0
- package/reference/typeid.md +5 -83
- package/sidebars.js +376 -43
- package/undocumented-report.md +528 -270
- 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,336 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: rollout
|
|
3
|
+
title: "Rollout"
|
|
4
|
+
sidebar_label: "Rollout"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`Rollout` is the expression language behind dynamic flags. An expression is a semicolon-separated list of choices, each optionally targeted at a slash-separated path pattern and a percentage bucket. `Rollout` parses expressions into `Rollout.Choices`, evaluates them against a path and a bucket, and computes the deterministic bucket for a key. Supporting types: `Rollout.Choice`, `Rollout.Selector`, `Rollout.Segment`, `Flag.ReloadResult`, `DynamicFlag.UpdateRecord`. The parsed form of an expression:
|
|
8
|
+
|
|
9
|
+
```scala
|
|
10
|
+
object Rollout {
|
|
11
|
+
final case class Choices(entries: List[Choice])
|
|
12
|
+
|
|
13
|
+
sealed trait Choice
|
|
14
|
+
object Choice {
|
|
15
|
+
final case class Targeted(value: String, selector: Selector) extends Choice
|
|
16
|
+
final case class CatchAll(value: String) extends Choice
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
final case class Selector(segments: List[Segment], percentage: Maybe[Int])
|
|
20
|
+
|
|
21
|
+
sealed trait Segment
|
|
22
|
+
object Segment {
|
|
23
|
+
case object Wildcard extends Segment
|
|
24
|
+
final case class Literal(value: String) extends Segment
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Motivation
|
|
30
|
+
|
|
31
|
+
A feature flag that is only on or off cannot express the thing teams actually want: on for 10% of users, on for everyone in one region, off everywhere else. Encoding that as three separate booleans means three flags to keep consistent, and encoding it as code means a deploy for every adjustment.
|
|
32
|
+
|
|
33
|
+
A rollout expression puts the whole decision in one string, which is what makes it deployable through a config source. `"true@*/eu/100%; true@*/50%; false"` reads as three rules in priority order: everyone in `eu`, then half of everyone else, then off. Changing the rollout is changing that string.
|
|
34
|
+
|
|
35
|
+
The percentage is a function of the key, not a coin flip. The same key always lands in the same bucket, so a user who sees the new checkout flow sees it on every request, and raising 50% to 60% keeps the original 50% inside the group rather than reshuffling everyone.
|
|
36
|
+
|
|
37
|
+
## Grammar
|
|
38
|
+
|
|
39
|
+
An expression is a list of choices; a targeted choice pairs a value with a selector; a selector is a path pattern with an optional percentage:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
expression = choice { ";" choice }
|
|
43
|
+
choice = value "@" selector | value
|
|
44
|
+
selector = path [ "/" percentage ]
|
|
45
|
+
path = segment { "/" segment }
|
|
46
|
+
segment = "*" | literal
|
|
47
|
+
percentage = digits "%"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Whitespace around choices and segments is trimmed. A choice with no `@` is a catch-all that matches everything.
|
|
51
|
+
|
|
52
|
+
The percentage is always the final slash-separated component, which is why `prod/50%` means "path `prod`, 50 percent" rather than a two-segment path. A selector consisting only of a percentage is rejected — there is no implicit "match everything" path, so write `*/50%` when you mean half of all keys.
|
|
53
|
+
|
|
54
|
+
## Evaluation
|
|
55
|
+
|
|
56
|
+
Evaluation walks the choices left to right and returns the first match. A catch-all matches immediately, so anything after it is unreachable.
|
|
57
|
+
|
|
58
|
+
### Path Matching
|
|
59
|
+
|
|
60
|
+
A path matches a selector only when both have the **same number of segments**, compared position by position. `Segment.Wildcard` matches any single segment; `Segment.Literal` requires equality:
|
|
61
|
+
|
|
62
|
+
```scala
|
|
63
|
+
import zio.blocks.config._
|
|
64
|
+
|
|
65
|
+
val bucket = Rollout.bucketFor("user-1234")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A single-literal selector matches only that exact one-segment path:
|
|
69
|
+
|
|
70
|
+
```scala
|
|
71
|
+
Rollout.select("on@beta; off", "beta", bucket)
|
|
72
|
+
// res0: Maybe[String] = "on"
|
|
73
|
+
Rollout.select("on@beta; off", "prod", bucket)
|
|
74
|
+
// res1: Maybe[String] = "off"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A wildcard matches any one segment, but still only one:
|
|
78
|
+
|
|
79
|
+
```scala
|
|
80
|
+
Rollout.select("on@*; off", "anything", bucket)
|
|
81
|
+
// res2: Maybe[String] = "on"
|
|
82
|
+
Rollout.select("on@*; off", "two/segments", bucket)
|
|
83
|
+
// res3: Maybe[String] = "off"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Multi-segment patterns mix the two, matching a path of exactly that length:
|
|
87
|
+
|
|
88
|
+
```scala
|
|
89
|
+
Rollout.select("on@*/eu; off", "user-1234/eu", bucket)
|
|
90
|
+
// res4: Maybe[String] = "on"
|
|
91
|
+
Rollout.select("on@*/eu; off", "user-1234/us", bucket)
|
|
92
|
+
// res5: Maybe[String] = "off"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
:::warning[Segment count is exact]
|
|
96
|
+
There is no prefix or suffix matching and no multi-segment wildcard. A selector with two segments never matches a three-segment path. This is the most common cause of a rollout that silently falls through to its catch-all.
|
|
97
|
+
:::
|
|
98
|
+
|
|
99
|
+
### Percentages
|
|
100
|
+
|
|
101
|
+
The percentage is compared against the bucket, with three values special-cased: an absent percentage always matches a matching path, `0%` never matches, and `100%` always matches. Otherwise the choice matches when `bucket < percentage`:
|
|
102
|
+
|
|
103
|
+
| Percentage | Matches when the path matches |
|
|
104
|
+
| ---------- | ------------------------------- |
|
|
105
|
+
| *(absent)* | Always |
|
|
106
|
+
| `0%` | Never |
|
|
107
|
+
| `100%` | Always |
|
|
108
|
+
| `n%` | `bucket < n` |
|
|
109
|
+
|
|
110
|
+
Because the comparison is a strict less-than against a bucket in `0..99`, a percentage behaves as the literal share of keys it names.
|
|
111
|
+
|
|
112
|
+
### Bucketing
|
|
113
|
+
|
|
114
|
+
`Rollout.bucketFor` hashes a key with MurmurHash3 and reduces it to `0..99`. The result is stable for a given key across processes and restarts:
|
|
115
|
+
|
|
116
|
+
```scala
|
|
117
|
+
Rollout.bucketFor("user-1234")
|
|
118
|
+
// res6: Int = 7
|
|
119
|
+
Rollout.bucketFor("user-1234") == Rollout.bucketFor("user-1234")
|
|
120
|
+
// res7: Boolean = true
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Different keys spread across the range, which is what makes a percentage approximate a uniform share:
|
|
124
|
+
|
|
125
|
+
```scala
|
|
126
|
+
(1 to 5).map(i => Rollout.bucketFor(s"user-$i"))
|
|
127
|
+
// res8: IndexedSeq[Int] = Vector(64, 97, 86, 68, 0)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Stability is the reason to pass a user or session identifier as the key rather than something that changes per call. A random key would re-roll the dice on every evaluation.
|
|
131
|
+
|
|
132
|
+
## API
|
|
133
|
+
|
|
134
|
+
Five methods cover parsing, evaluation, and diagnostics. `Rollout.select` is the one-shot form; the others let you parse once and evaluate many times.
|
|
135
|
+
|
|
136
|
+
### Rollout.select
|
|
137
|
+
|
|
138
|
+
`Rollout.select` parses and evaluates in one call, returning `Maybe.absent` both when nothing matches and when the expression will not parse:
|
|
139
|
+
|
|
140
|
+
```scala
|
|
141
|
+
Rollout.select("on@prod/50%; off", "prod", 10)
|
|
142
|
+
// res9: Maybe[String] = "on"
|
|
143
|
+
Rollout.select("@@@not-valid", "prod", 10)
|
|
144
|
+
// res10: Maybe[String] = zio.blocks.maybe.Absent$@16d4dcbe
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Because a parse failure is indistinguishable from a non-match, use `Rollout.select` only where a malformed expression should behave as "no decision". Parse explicitly when you need to know.
|
|
148
|
+
|
|
149
|
+
### Rollout.parseChoices
|
|
150
|
+
|
|
151
|
+
`Rollout.parseChoices` returns the parsed `Choices` or the first `ConfigError` it encountered:
|
|
152
|
+
|
|
153
|
+
```scala
|
|
154
|
+
Rollout.parseChoices("on@prod/50%; off")
|
|
155
|
+
// res11: Either[ConfigError, Choices] = Right(
|
|
156
|
+
// Choices(
|
|
157
|
+
// List(
|
|
158
|
+
// Targeted(
|
|
159
|
+
// value = "on",
|
|
160
|
+
// selector = Selector(segments = List(Literal("prod")), percentage = 50)
|
|
161
|
+
// ),
|
|
162
|
+
// CatchAll("off")
|
|
163
|
+
// )
|
|
164
|
+
// )
|
|
165
|
+
// )
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
An expression with a malformed choice reports the failure rather than skipping it:
|
|
169
|
+
|
|
170
|
+
```scala
|
|
171
|
+
Rollout.parseChoices("on@prod/150%")
|
|
172
|
+
// res12: Either[ConfigError, Choices] = Left(
|
|
173
|
+
// InvalidValue(
|
|
174
|
+
// path = "rollout",
|
|
175
|
+
// value = "prod/150%",
|
|
176
|
+
// expectedType = "percentage <= 100",
|
|
177
|
+
// source = "rollout",
|
|
178
|
+
// cause = None
|
|
179
|
+
// )
|
|
180
|
+
// )
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Parsing is what `DynamicFlag` does at initialization and on every update, so these are the errors a flag rejects.
|
|
184
|
+
|
|
185
|
+
### Rollout.evaluateIndex
|
|
186
|
+
|
|
187
|
+
`Rollout.evaluateIndex` evaluates already-parsed choices, which is what the flag does on the hot path — parsing happens once per update, not once per evaluation:
|
|
188
|
+
|
|
189
|
+
```scala
|
|
190
|
+
val choices = Rollout.parseChoices("on@*/eu; off").toOption.get
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Evaluating the same choices for several paths reuses the parse:
|
|
194
|
+
|
|
195
|
+
```scala
|
|
196
|
+
Rollout.evaluateIndex(choices, "user-1/eu", 42)
|
|
197
|
+
// res13: Maybe[String] = "on"
|
|
198
|
+
Rollout.evaluateIndex(choices, "user-1/us", 42)
|
|
199
|
+
// res14: Maybe[String] = "off"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Rollout.validate
|
|
203
|
+
|
|
204
|
+
`Rollout.validate` parses an expression and returns warnings about rules that are suspicious rather than invalid. It flags choices rendered unreachable by an earlier catch-all:
|
|
205
|
+
|
|
206
|
+
```scala
|
|
207
|
+
Rollout.validate("off; on@prod")
|
|
208
|
+
// res15: Either[ConfigError, List[String]] = Right(
|
|
209
|
+
// List("Choices after index 0 are unreachable (catch-all found)")
|
|
210
|
+
// )
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
It also flags a pattern whose percentages sum above 100, which usually means someone added a rule instead of adjusting one:
|
|
214
|
+
|
|
215
|
+
```scala
|
|
216
|
+
Rollout.validate("a@prod/70%; b@prod/60%")
|
|
217
|
+
// res16: Either[ConfigError, List[String]] = Right(
|
|
218
|
+
// List("Cumulative percentage for pattern 'prod' is 130% (exceeds 100%)")
|
|
219
|
+
// )
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
A clean expression returns an empty list:
|
|
223
|
+
|
|
224
|
+
```scala
|
|
225
|
+
Rollout.validate("on@*/eu; on@*/50%; off")
|
|
226
|
+
// res17: Either[ConfigError, List[String]] = Right(List())
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Warnings are advisory — `DynamicFlag` does not run them. Call `Rollout.validate` in a test or an admin endpoint that accepts expressions from operators.
|
|
230
|
+
|
|
231
|
+
### Parse Failures
|
|
232
|
+
|
|
233
|
+
Every parse failure is a `ConfigError.InvalidValue` with path and source `"rollout"`, and an `expectedType` describing what was wanted:
|
|
234
|
+
|
|
235
|
+
| Expression | Rejected because |
|
|
236
|
+
| ----------------- | ---------------------------------------------------- |
|
|
237
|
+
| `""` | Empty expression. |
|
|
238
|
+
| `"@prod"` | No value before `@`. |
|
|
239
|
+
| `"on@"` | No selector after `@`. |
|
|
240
|
+
| `"on@50%"` | Percentage with no path; write `on@*/50%`. |
|
|
241
|
+
| `"on@prod/"` | Trailing slash with no percentage. |
|
|
242
|
+
| `"on@prod/abc%"` | Non-numeric percentage. |
|
|
243
|
+
| `"on@prod/150%"` | Percentage above 100. |
|
|
244
|
+
|
|
245
|
+
## The Parsed Form
|
|
246
|
+
|
|
247
|
+
`Rollout.Choices` wraps an ordered `List[Choice]`, and the order is the evaluation order. Matching on the ADT is how an admin tool can render or rewrite an expression rather than treating it as opaque text:
|
|
248
|
+
|
|
249
|
+
```scala
|
|
250
|
+
import zio.blocks.config._
|
|
251
|
+
|
|
252
|
+
val parsed = Rollout.parseChoices("on@*/eu/25%; off").toOption.get
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
A `Choice.Targeted` carries its value and selector; a `Choice.CatchAll` carries only a value:
|
|
256
|
+
|
|
257
|
+
```scala
|
|
258
|
+
parsed.entries
|
|
259
|
+
// res19: List[Choice] = List(
|
|
260
|
+
// Targeted(
|
|
261
|
+
// value = "on",
|
|
262
|
+
// selector = Selector(
|
|
263
|
+
// segments = List(Wildcard, Literal("eu")),
|
|
264
|
+
// percentage = 25
|
|
265
|
+
// )
|
|
266
|
+
// ),
|
|
267
|
+
// CatchAll("off")
|
|
268
|
+
// )
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`Selector#segments` is the path pattern as a list, and `Selector#percentage` is a `Maybe[Int]` that is absent when the selector had no percentage:
|
|
272
|
+
|
|
273
|
+
```scala
|
|
274
|
+
parsed.entries.collect { case t: Rollout.Choice.Targeted => (t.selector.segments, t.selector.percentage) }
|
|
275
|
+
// res20: List[Tuple2[List[Segment], Maybe[Int]]] = List(
|
|
276
|
+
// (List(Wildcard, Literal("eu")), 25)
|
|
277
|
+
// )
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Reload Lifecycle
|
|
281
|
+
|
|
282
|
+
A dynamic flag's expression can change while the process runs, either from code or from a source. Both paths record what happened so the change is auditable.
|
|
283
|
+
|
|
284
|
+
### Flag.ReloadResult
|
|
285
|
+
|
|
286
|
+
`DynamicFlag#reload` re-reads the expression from `FlagSource.Registry` and reports the outcome:
|
|
287
|
+
|
|
288
|
+
| Result | Meaning |
|
|
289
|
+
| ----------------------------------- | ------------------------------------------------------------- |
|
|
290
|
+
| `Flag.ReloadResult.NoSource` | No registered source provides this flag's name; nothing changed. |
|
|
291
|
+
| `Flag.ReloadResult.Unchanged` | The source's expression is identical to the current one. |
|
|
292
|
+
| `Flag.ReloadResult.Updated(old, new)` | The expression was replaced; both versions are reported. |
|
|
293
|
+
| `Flag.ReloadResult.Failed(error)` | The source's expression will not parse; the old one is kept. |
|
|
294
|
+
|
|
295
|
+
`Failed` keeping the previous expression is deliberate: a bad push to a flag service degrades to "no change", not to the constructor default.
|
|
296
|
+
|
|
297
|
+
Reloading is manual. Nothing in the module polls, so drive it from whatever scheduler your application already has:
|
|
298
|
+
|
|
299
|
+
```scala
|
|
300
|
+
import zio.blocks.config._
|
|
301
|
+
|
|
302
|
+
object newCheckout extends DynamicFlag[Boolean](false, "false")
|
|
303
|
+
|
|
304
|
+
newCheckout.reload() match {
|
|
305
|
+
case Flag.ReloadResult.Updated(from, to) => println(s"rollout changed: $from -> $to")
|
|
306
|
+
case Flag.ReloadResult.Failed(error) => println(s"rollout rejected: ${error.message}")
|
|
307
|
+
case Flag.ReloadResult.Unchanged => ()
|
|
308
|
+
case Flag.ReloadResult.NoSource => println("no source registered for this flag")
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### DynamicFlag.UpdateRecord
|
|
313
|
+
|
|
314
|
+
Every successful change — from `DynamicFlag#update` or from a reload — appends a `DynamicFlag.UpdateRecord` holding the old expression, the new one, and a millisecond timestamp:
|
|
315
|
+
|
|
316
|
+
```scala
|
|
317
|
+
final case class UpdateRecord(oldExpression: String, newExpression: String, timestampMillis: Long)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
`DynamicFlag#updateHistory` returns them most-recent-first, capped at the last ten. Older records are discarded, so the history answers "what changed just now?" rather than serving as an audit log.
|
|
321
|
+
|
|
322
|
+
### Evaluation Counters
|
|
323
|
+
|
|
324
|
+
`DynamicFlag#apply` increments a per-key counter, and `DynamicFlag#counters` returns a snapshot as a `Map[String, Long]`. The counters are thread-safe but approximate — they use `LongAdder`, so a snapshot taken during traffic may miss in-flight increments.
|
|
325
|
+
|
|
326
|
+
Distinct keys are capped at 100. Once that many have been seen, every new key is counted under the single bucket `"other"` instead of getting its own entry. No exception is thrown and existing counters keep working, so a flag keyed by user id degrades to "100 known users plus a bulk count" rather than leaking memory.
|
|
327
|
+
|
|
328
|
+
`DynamicFlag#evaluate` bypasses counting entirely. Use it on paths where you do not want the bookkeeping, and reserve `DynamicFlag#apply` for the call sites whose distribution you want to observe.
|
|
329
|
+
|
|
330
|
+
`DynamicFlag#parseErrorCount` counts evaluations where a matched value could not be parsed into the flag's type, causing a fall back to the default. A non-zero count is always a misconfigured expression: the rollout matched, but the value it selected was not readable. Because every call still returns the default, this is otherwise silent.
|
|
331
|
+
|
|
332
|
+
## Integration Points
|
|
333
|
+
|
|
334
|
+
`Rollout` depends only on `Maybe` and `ConfigError`. It is used by `DynamicFlag` for initialization, updates, reloads, and every evaluation, and it can be used directly wherever a path-and-percentage decision is needed without a flag object.
|
|
335
|
+
|
|
336
|
+
See [Flags](./flags.md) for the flag types that drive it, and [Errors](./errors.md) for the error type its parser returns.
|
package/reference/context.md
CHANGED
|
@@ -116,7 +116,7 @@ val config = ctx.get[Config] // Compile-time proof it exists
|
|
|
116
116
|
Add the ZIO Blocks Context module to your `build.sbt`:
|
|
117
117
|
|
|
118
118
|
```scala
|
|
119
|
-
libraryDependencies += "dev.zio" %% "zio-blocks-context" % "0.0.
|
|
119
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-context" % "0.0.55"
|
|
120
120
|
```
|
|
121
121
|
|
|
122
122
|
## Construction
|
|
@@ -525,26 +525,10 @@ cd zio-blocks
|
|
|
525
525
|
**Context construction: creating contexts with apply, empty.add, and inspecting size/isEmpty/nonEmpty**
|
|
526
526
|
|
|
527
527
|
```scala title="schema-examples/src/main/scala/context/ContextConstructionExample.scala"
|
|
528
|
-
/*
|
|
529
|
-
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
530
|
-
*
|
|
531
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
532
|
-
* you may not use this file except in compliance with the License.
|
|
533
|
-
* You may obtain a copy of the License at
|
|
534
|
-
*
|
|
535
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
536
|
-
*
|
|
537
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
538
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
539
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
540
|
-
* See the License for the specific language governing permissions and
|
|
541
|
-
* limitations under the License.
|
|
542
|
-
*/
|
|
543
|
-
|
|
544
528
|
package context
|
|
545
529
|
|
|
546
530
|
import zio.blocks.context._
|
|
547
|
-
import
|
|
531
|
+
import zio.sbt.ExprEval.show
|
|
548
532
|
|
|
549
533
|
// Context.empty creates an empty, type-safe dependency container.
|
|
550
534
|
// Use Context.apply(...) to construct contexts with 1–10 values.
|
|
@@ -602,26 +586,10 @@ sbt "schema-examples/runMain context.ContextConstructionExample"
|
|
|
602
586
|
**Context retrieval: using get, supertype lookups, and getOption for safe access**
|
|
603
587
|
|
|
604
588
|
```scala title="schema-examples/src/main/scala/context/ContextRetrievalExample.scala"
|
|
605
|
-
/*
|
|
606
|
-
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
607
|
-
*
|
|
608
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
609
|
-
* you may not use this file except in compliance with the License.
|
|
610
|
-
* You may obtain a copy of the License at
|
|
611
|
-
*
|
|
612
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
613
|
-
*
|
|
614
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
615
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
616
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
617
|
-
* See the License for the specific language governing permissions and
|
|
618
|
-
* limitations under the License.
|
|
619
|
-
*/
|
|
620
|
-
|
|
621
589
|
package context
|
|
622
590
|
|
|
623
591
|
import zio.blocks.context._
|
|
624
|
-
import
|
|
592
|
+
import zio.sbt.ExprEval.show
|
|
625
593
|
|
|
626
594
|
// Context#get[A] retrieves a value by type with compile-time proof of existence.
|
|
627
595
|
// Context#getOption[A] retrieves a value if present, returning None if missing.
|
|
@@ -687,26 +655,10 @@ sbt "schema-examples/runMain context.ContextRetrievalExample"
|
|
|
687
655
|
**Context modification: adding values, updating existing ones, merging contexts, and pruning types**
|
|
688
656
|
|
|
689
657
|
```scala title="schema-examples/src/main/scala/context/ContextModificationExample.scala"
|
|
690
|
-
/*
|
|
691
|
-
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
692
|
-
*
|
|
693
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
694
|
-
* you may not use this file except in compliance with the License.
|
|
695
|
-
* You may obtain a copy of the License at
|
|
696
|
-
*
|
|
697
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
698
|
-
*
|
|
699
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
700
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
701
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
702
|
-
* See the License for the specific language governing permissions and
|
|
703
|
-
* limitations under the License.
|
|
704
|
-
*/
|
|
705
|
-
|
|
706
658
|
package context
|
|
707
659
|
|
|
708
660
|
import zio.blocks.context._
|
|
709
|
-
import
|
|
661
|
+
import zio.sbt.ExprEval.show
|
|
710
662
|
|
|
711
663
|
// Context is immutable; modification methods return new contexts.
|
|
712
664
|
// Context#add expands the context with a new value (or replaces if type exists).
|
|
@@ -775,3 +727,8 @@ object ContextModificationExample extends App {
|
|
|
775
727
|
```bash
|
|
776
728
|
sbt "schema-examples/runMain context.ContextModificationExample"
|
|
777
729
|
```
|
|
730
|
+
|
|
731
|
+
## See Also
|
|
732
|
+
|
|
733
|
+
- [Telemetry Reference](./telemetry/index.md) — Its `OtelContext` bridge snapshots the active `SpanContext` into a `Context[R & OtelContext]`
|
|
734
|
+
- [Telemetry Guide](../guides/telemetry-guide.md) — Architecture and patterns for the telemetry module, including where `Context` fits in
|