aliehsaeedii commented on code in PR #22896:
URL: https://github.com/apache/kafka/pull/22896#discussion_r3711254891
##########
docs/streams/developer-guide/interactive-queries.md:
##########
@@ -499,6 +499,198 @@ At this point the full state of the application is
interactively queryable:
+# Interactive Queries v2 (IQv2) {#interactive-queries-v2}
+
+The sections above describe the original interactive queries API (informally,
"IQv1"), where you obtain a read-only store facade from
[KafkaStreams#store(...)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
and call methods such as `get(key)` or `range(from, to)` on it. Interactive
Queries v2 (IQv2), introduced in
[KIP-796](https://cwiki.apache.org/confluence/x/34xnCw), is an alternative,
more flexible API for the same purpose: querying the local state of a running
Kafka Streams application.
+
+Instead of a fixed store facade, IQv2 models every query as a first-class
[Query](/{version}/javadoc/org/apache/kafka/streams/query/Query.html) object
that you submit through a single
[KafkaStreams#query(StateQueryRequest)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
method. The response is a
[StateQueryResult](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryResult.html)
that contains a separate
[QueryResult](/{version}/javadoc/org/apache/kafka/streams/query/QueryResult.html)
for each partition that executed the query, along with metadata such as each
partition's
[Position](/{version}/javadoc/org/apache/kafka/streams/query/Position.html).
+
+IQv2 offers several advantages over the original API:
+
+ * A single, uniform entry point for every kind of query.
+ * Query-level extensibility: custom state stores can handle their own
`Query` types (the runtime forwards unknown queries straight through to the
underlying byte store), rather than requiring a custom `QueryableStoreType`
store facade as the original API does.
+ * Rich, per-partition results with typed failure reasons instead of
exceptions.
+ * Fine-grained consistency control through `Position` and `PositionBound`.
+ * The ability to target specific partitions, require active (leader) tasks,
override the isolation level, and collect execution information.
+
+Both APIs are fully supported and can be used side by side. Note that IQv2 is
marked as an evolving API, so it may change between releases, and global stores
are not yet supported by `query(...)` (see [Limitations](#limitations-of-iqv2)
below).
+
+## How Interactive Queries v2 works
+
+Issuing an IQv2 query follows four steps:
+
+ 1. **Build a query.** Create a `Query` object that describes what you want
to read, for example a
[KeyQuery](/{version}/javadoc/org/apache/kafka/streams/query/KeyQuery.html) for
a single-key lookup or a
[RangeQuery](/{version}/javadoc/org/apache/kafka/streams/query/RangeQuery.html)
for a scan.
+ 2. **Build a request.** Wrap the query in a
[StateQueryRequest](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryRequest.html)
that names the store to query:
`StateQueryRequest.inStore(storeName).withQuery(query)`. Optionally configure
the request (partitions, position bound, isolation level, and so on).
+ 3. **Execute the request.** Call `streams.query(request)`, which runs the
query against every locally available partition of the store (or just the
partitions you requested) and returns a `StateQueryResult`.
+ 4. **Read the results.** For a query that targets a single partition, use
`getOnlyPartitionResult()`. For a query that may span multiple partitions, use
`getPartitionResults()` to get a `Map` from partition number to `QueryResult`.
Each `QueryResult` reports whether the query succeeded on that partition and
holds either the result value or a typed failure reason.
+
+## Building and executing a query
+
+The following example looks up a single key in the `CountsKeyValueStore` state
store from the word-count example used earlier on this page:
+
+```java
+import org.apache.kafka.streams.KafkaStreams;
+import org.apache.kafka.streams.query.KeyQuery;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.StateQueryRequest;
+import org.apache.kafka.streams.query.StateQueryResult;
+
+import static org.apache.kafka.streams.query.StateQueryRequest.inStore;
+
+KafkaStreams streams = ...;
+
+// 1. Build the query: retrieve the value for a single key.
+KeyQuery<String, Long> query = KeyQuery.withKey("alice");
+
+// 2. Build the request, naming the store to run the query against.
+// A KeyQuery<String, Long> is a Query<Long>, so the request is a
StateQueryRequest<Long>.
+StateQueryRequest<Long> request =
inStore("CountsKeyValueStore").withQuery(query);
+
+// 3. Execute the query.
+StateQueryResult<Long> result = streams.query(request);
+
+// 4. Read the result. A given key lives in exactly one partition, so at most
one partition
+// returns a value. getOnlyPartitionResult() returns that partition's
result, or null if
+// no locally available partition holds the key.
+QueryResult<Long> partitionResult = result.getOnlyPartitionResult();
+if (partitionResult != null && partitionResult.isSuccess()) {
+ Long count = partitionResult.getResult();
+ System.out.println("Count for alice: " + count);
+}
+```
+
+Note that the type parameter of `StateQueryRequest` (and of `StateQueryResult`
and `QueryResult`) is the query's *result* type, not the query type: a
`KeyQuery<String, Long>` implements `Query<Long>`, so the request is a
`StateQueryRequest<Long>`.
+
+## Built-in query types
+
+Kafka Streams ships with a set of query types covering the standard store
types. All of them live in the
[org.apache.kafka.streams.query](/{version}/javadoc/org/apache/kafka/streams/query/package-summary.html)
package.
+
+| Query | Result type (`R`) | Key factory / builder methods |
+| --- | --- | --- |
+| `KeyQuery<K, V>` | `V` | `withKey(key)`; `.skipCache()` |
+| `TimestampedKeyQuery<K, V>` | `ValueAndTimestamp<V>` | `withKey(key)`;
`.skipCache()` |
+| `RangeQuery<K, V>` | `KeyValueIterator<K, V>` | `withRange(lower, upper)`,
`withLowerBound(lower)`, `withUpperBound(upper)`, `withNoBounds()`;
`.withAscendingKeys()` / `.withDescendingKeys()` |
+| `TimestampedRangeQuery<K, V>` | `KeyValueIterator<K, ValueAndTimestamp<V>>`
| `withRange(lower, upper)`, `withLowerBound(lower)`, `withUpperBound(upper)`,
`withNoBounds()`; `.withAscendingKeys()` / `.withDescendingKeys()` |
+| `WindowKeyQuery<K, V>` | `WindowStoreIterator<V>` |
`withKeyAndWindowStartRange(key, timeFrom, timeTo)` |
+| `WindowRangeQuery<K, V>` | `KeyValueIterator<Windowed<K>, V>` |
`withKey(key)`, `withWindowStartRange(timeFrom, timeTo)` |
+| `VersionedKeyQuery<K, V>` | `VersionedRecord<V>` | `withKey(key)`;
`.asOf(instant)` |
+| `MultiVersionedKeyQuery<K, V>` | `VersionedRecordIterator<V>` |
`withKey(key)`; `.fromTime(instant)`, `.toTime(instant)`,
`.withAscendingTimestamps()` / `.withDescendingTimestamps()` |
+
+For range and scan queries (`RangeQuery`, `TimestampedRangeQuery`), passing no
bounds performs a full scan, and result ordering is based on the serialized
`byte[]` of the keys, not on the logical key order.
+
+Versioned key-value stores are queryable **only** through IQv2 — use
`VersionedKeyQuery` for a single version (latest, or as of a timestamp) and
`MultiVersionedKeyQuery` for a range of versions. The original
`KafkaStreams#store(...)` API has no queryable store type for versioned stores.
+
+Because IQv2 is extensible, a custom state store may implement additional
query types of its own. When a store does not know how to handle a query, it
does not throw; instead it returns a failed `QueryResult` with
`FailureReason.UNKNOWN_QUERY_TYPE`.
+
+## Handling query results
+
+A `StateQueryResult` aggregates one `QueryResult` per partition that ran the
query. By default a request runs against all locally available partitions of
the store (unless you narrow it with `withPartitions(...)`), so
`getPartitionResults()` may contain an entry for every one of those partitions.
+
+Which accessor to use is determined by the *query type* you chose, not by
inspecting the result at runtime:
+
+ * **Point lookups** (`KeyQuery`, `TimestampedKeyQuery`, `VersionedKeyQuery`)
can only match in the single partition that owns the key, so at most one
partition returns a value (the other queried partitions return a successful
result with `null`). Use `getOnlyPartitionResult()`.
+ * **Range, scan, and window queries** (`RangeQuery`, `WindowRangeQuery`,
`MultiVersionedKeyQuery`, and so on) can match records in every queried
partition, so results are spread across partitions. Use `getPartitionResults()`
and iterate.
+
+The accessors are:
+
+ * `getPartitionResults()` returns a `Map<Integer, QueryResult<R>>`, keyed by
partition number — one entry per partition that ran the query. This is the
general form and works for any query.
+ * `getOnlyPartitionResult()` is a convenience that filters to the single
partition that produced a value and returns it (or `null` if none did). It
throws `IllegalArgumentException` if more than one partition produced a value,
so only use it when you know the query matches at most one partition.
+ * `getPosition()` returns the merged `Position` observed across the
partition results.
+
+Each `QueryResult` reports the outcome for one partition. Use `isSuccess()` /
`isFailure()` before reading: `getResult()` returns the value on success (which
may itself be `null`, for example when a key is not found), while
`getFailureReason()` and `getFailureMessage()` describe a failure. Results are
always **per-partition** — a query may succeed on some partitions and fail on
others (for example, if one partition has migrated off this instance).
+
+When a query returns an iterator (`RangeQuery`, `WindowKeyQuery`,
`MultiVersionedKeyQuery`, and so on), the iterator must be closed after use.
Iterate over every partition's result and use a try-with-resources block:
+
+```java
+import org.apache.kafka.streams.KeyValue;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.RangeQuery;
+import org.apache.kafka.streams.query.StateQueryRequest;
+import org.apache.kafka.streams.query.StateQueryResult;
+import org.apache.kafka.streams.state.KeyValueIterator;
+
+import java.util.Map;
+
+import static org.apache.kafka.streams.query.StateQueryRequest.inStore;
+
+RangeQuery<String, Long> query = RangeQuery.withRange("a", "n");
+StateQueryRequest<KeyValueIterator<String, Long>> request =
+ inStore("CountsKeyValueStore").withQuery(query);
+StateQueryResult<KeyValueIterator<String, Long>> result =
streams.query(request);
+
+for (Map.Entry<Integer, QueryResult<KeyValueIterator<String, Long>>> entry
+ : result.getPartitionResults().entrySet()) {
+ QueryResult<KeyValueIterator<String, Long>> partitionResult =
entry.getValue();
+ if (partitionResult.isFailure()) {
+ System.out.println("Partition " + entry.getKey() + " failed: "
+ + partitionResult.getFailureReason() + " - " +
partitionResult.getFailureMessage());
+ continue;
+ }
+ try (KeyValueIterator<String, Long> iterator =
partitionResult.getResult()) {
+ while (iterator.hasNext()) {
+ KeyValue<String, Long> record = iterator.next();
+ System.out.println(record.key + ": " + record.value);
+ }
+ }
+}
+```
+
+A failed `QueryResult` carries one of the
[FailureReason](/{version}/javadoc/org/apache/kafka/streams/query/FailureReason.html)
values:
+
+| `FailureReason` | Meaning | Recommended action |
+| --- | --- | --- |
+| `UNKNOWN_QUERY_TYPE` | The store does not know how to execute this query. |
Verify the query is supported by that store type; for custom queries, contact
the store maintainer. |
+| `NOT_ACTIVE` | `requireActive()` was set, but the partition is a standby or
an active task that is not yet in the `RUNNING` state. | Retry later or query a
different replica. |
+| `NOT_UP_TO_BOUND` | The partition has not yet caught up to the requested
`PositionBound`. | Retry later or query a different replica. |
+| `NOT_PRESENT` | The requested partition is not present on this instance (for
example, it migrated during a rebalance). | Query a different replica. |
+| `DOES_NOT_EXIST` | The requested partition does not exist for this store. |
Correct the requested set of partitions. |
+| `STORE_EXCEPTION` | The store threw an exception while executing the query.
| Depending on the exception, retry this instance or a different one. |
+
+## Controlling consistency and availability
+
+A `StateQueryRequest` is immutable; each configuration method returns a new
request. Beyond `inStore(...).withQuery(...)`, the following options let you
trade off consistency, availability, and cost:
+
+ * `withPartitions(Set<Integer>)` / `withAllPartitions()`: run against a
specific set of partitions or against all locally available partitions (the
default). Partitions that are missing return `NOT_PRESENT`; partitions that do
not exist return `DOES_NOT_EXIST`.
+ * `requireActive()`: run only on active (leader) partitions. Non-active
partitions return `NOT_ACTIVE`. Use this when you need the most up-to-date data
and want to avoid reading from standby replicas.
+ * `withPositionBound(PositionBound)`: by default a request is
`PositionBound.unbounded()`. Use `PositionBound.at(position)` to require that
each queried partition has consumed up to a given `Position` before serving the
query; a partition that is behind returns `NOT_UP_TO_BOUND`. Combined with the
`Position` returned by `StateQueryResult#getPosition()`, this lets you
implement read-your-writes / monotonic reads: feed the position from one query
into the bound of the next so repeated queries never appear to move backwards
in time, while still allowing reads to be served from any replica that is
caught up.
+ * `withIsolationLevel(IsolationLevel)`: override the isolation level for
this query. When not set, the effective level falls back to the
`default.interactive.query.isolation.level` configuration.
+ * `enableExecutionInfo()`: ask stores and the runtime to record details
about how the query executed, retrievable via `QueryResult#getExecutionInfo()`.
+
+For example, the following request reads the latest committed value for a key
from the active replica, but only for partitions 0 and 1, and only once the
store has caught up to a known input position:
Review Comment:
[content] Two claims in this sentence don't hold for the example below it.
1. **"only once the store has caught up to a known input position"** — the
example sets both `.requireActive()` and
`.withPositionBound(PositionBound.at(inputPosition))`, but `KafkaStreams#query`
throws the bound away when `requireActive` is set:
```java
final QueryResult<R> r = store.query(
request.getQuery(),
request.isRequireActive()
? PositionBound.unbounded()
: request.getPositionBound(),
```
(`KafkaStreams.java:2219-2224`). So `withPositionBound(...)` has **no
effect** here and `NOT_UP_TO_BOUND` can never be returned.
2. **"reads the latest committed value"** — the request never calls
`withIsolationLevel(...)`, so it inherits
`default.interactive.query.isolation.level`, whose default is
`READ_UNCOMMITTED` (`StreamsConfig.java:553`). The read is not committed-only.
Suggest splitting the example into two (one showing `requireActive()`, one
showing `withPositionBound()` for monotonic reads), and dropping "latest
committed" unless the example adds
`.withIsolationLevel(IsolationLevel.READ_COMMITTED)`.
##########
docs/streams/developer-guide/interactive-queries.md:
##########
@@ -499,6 +499,198 @@ At this point the full state of the application is
interactively queryable:
+# Interactive Queries v2 (IQv2) {#interactive-queries-v2}
+
+The sections above describe the original interactive queries API (informally,
"IQv1"), where you obtain a read-only store facade from
[KafkaStreams#store(...)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
and call methods such as `get(key)` or `range(from, to)` on it. Interactive
Queries v2 (IQv2), introduced in
[KIP-796](https://cwiki.apache.org/confluence/x/34xnCw), is an alternative,
more flexible API for the same purpose: querying the local state of a running
Kafka Streams application.
+
+Instead of a fixed store facade, IQv2 models every query as a first-class
[Query](/{version}/javadoc/org/apache/kafka/streams/query/Query.html) object
that you submit through a single
[KafkaStreams#query(StateQueryRequest)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
method. The response is a
[StateQueryResult](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryResult.html)
that contains a separate
[QueryResult](/{version}/javadoc/org/apache/kafka/streams/query/QueryResult.html)
for each partition that executed the query, along with metadata such as each
partition's
[Position](/{version}/javadoc/org/apache/kafka/streams/query/Position.html).
+
+IQv2 offers several advantages over the original API:
+
+ * A single, uniform entry point for every kind of query.
+ * Query-level extensibility: custom state stores can handle their own
`Query` types (the runtime forwards unknown queries straight through to the
underlying byte store), rather than requiring a custom `QueryableStoreType`
store facade as the original API does.
+ * Rich, per-partition results with typed failure reasons instead of
exceptions.
+ * Fine-grained consistency control through `Position` and `PositionBound`.
+ * The ability to target specific partitions, require active (leader) tasks,
override the isolation level, and collect execution information.
+
+Both APIs are fully supported and can be used side by side. Note that IQv2 is
marked as an evolving API, so it may change between releases, and global stores
are not yet supported by `query(...)` (see [Limitations](#limitations-of-iqv2)
below).
+
+## How Interactive Queries v2 works
+
+Issuing an IQv2 query follows four steps:
+
+ 1. **Build a query.** Create a `Query` object that describes what you want
to read, for example a
[KeyQuery](/{version}/javadoc/org/apache/kafka/streams/query/KeyQuery.html) for
a single-key lookup or a
[RangeQuery](/{version}/javadoc/org/apache/kafka/streams/query/RangeQuery.html)
for a scan.
+ 2. **Build a request.** Wrap the query in a
[StateQueryRequest](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryRequest.html)
that names the store to query:
`StateQueryRequest.inStore(storeName).withQuery(query)`. Optionally configure
the request (partitions, position bound, isolation level, and so on).
+ 3. **Execute the request.** Call `streams.query(request)`, which runs the
query against every locally available partition of the store (or just the
partitions you requested) and returns a `StateQueryResult`.
+ 4. **Read the results.** For a query that targets a single partition, use
`getOnlyPartitionResult()`. For a query that may span multiple partitions, use
`getPartitionResults()` to get a `Map` from partition number to `QueryResult`.
Each `QueryResult` reports whether the query succeeded on that partition and
holds either the result value or a typed failure reason.
+
+## Building and executing a query
+
+The following example looks up a single key in the `CountsKeyValueStore` state
store from the word-count example used earlier on this page:
+
+```java
+import org.apache.kafka.streams.KafkaStreams;
+import org.apache.kafka.streams.query.KeyQuery;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.StateQueryRequest;
+import org.apache.kafka.streams.query.StateQueryResult;
+
+import static org.apache.kafka.streams.query.StateQueryRequest.inStore;
+
+KafkaStreams streams = ...;
+
+// 1. Build the query: retrieve the value for a single key.
+KeyQuery<String, Long> query = KeyQuery.withKey("alice");
+
+// 2. Build the request, naming the store to run the query against.
+// A KeyQuery<String, Long> is a Query<Long>, so the request is a
StateQueryRequest<Long>.
+StateQueryRequest<Long> request =
inStore("CountsKeyValueStore").withQuery(query);
+
+// 3. Execute the query.
+StateQueryResult<Long> result = streams.query(request);
+
+// 4. Read the result. A given key lives in exactly one partition, so at most
one partition
+// returns a value. getOnlyPartitionResult() returns that partition's
result, or null if
+// no locally available partition holds the key.
+QueryResult<Long> partitionResult = result.getOnlyPartitionResult();
+if (partitionResult != null && partitionResult.isSuccess()) {
+ Long count = partitionResult.getResult();
+ System.out.println("Count for alice: " + count);
+}
+```
+
+Note that the type parameter of `StateQueryRequest` (and of `StateQueryResult`
and `QueryResult`) is the query's *result* type, not the query type: a
`KeyQuery<String, Long>` implements `Query<Long>`, so the request is a
`StateQueryRequest<Long>`.
+
+## Built-in query types
+
+Kafka Streams ships with a set of query types covering the standard store
types. All of them live in the
[org.apache.kafka.streams.query](/{version}/javadoc/org/apache/kafka/streams/query/package-summary.html)
package.
+
+| Query | Result type (`R`) | Key factory / builder methods |
+| --- | --- | --- |
+| `KeyQuery<K, V>` | `V` | `withKey(key)`; `.skipCache()` |
+| `TimestampedKeyQuery<K, V>` | `ValueAndTimestamp<V>` | `withKey(key)`;
`.skipCache()` |
+| `RangeQuery<K, V>` | `KeyValueIterator<K, V>` | `withRange(lower, upper)`,
`withLowerBound(lower)`, `withUpperBound(upper)`, `withNoBounds()`;
`.withAscendingKeys()` / `.withDescendingKeys()` |
+| `TimestampedRangeQuery<K, V>` | `KeyValueIterator<K, ValueAndTimestamp<V>>`
| `withRange(lower, upper)`, `withLowerBound(lower)`, `withUpperBound(upper)`,
`withNoBounds()`; `.withAscendingKeys()` / `.withDescendingKeys()` |
+| `WindowKeyQuery<K, V>` | `WindowStoreIterator<V>` |
`withKeyAndWindowStartRange(key, timeFrom, timeTo)` |
+| `WindowRangeQuery<K, V>` | `KeyValueIterator<Windowed<K>, V>` |
`withKey(key)`, `withWindowStartRange(timeFrom, timeTo)` |
+| `VersionedKeyQuery<K, V>` | `VersionedRecord<V>` | `withKey(key)`;
`.asOf(instant)` |
+| `MultiVersionedKeyQuery<K, V>` | `VersionedRecordIterator<V>` |
`withKey(key)`; `.fromTime(instant)`, `.toTime(instant)`,
`.withAscendingTimestamps()` / `.withDescendingTimestamps()` |
+
+For range and scan queries (`RangeQuery`, `TimestampedRangeQuery`), passing no
bounds performs a full scan, and result ordering is based on the serialized
`byte[]` of the keys, not on the logical key order.
+
+Versioned key-value stores are queryable **only** through IQv2 — use
`VersionedKeyQuery` for a single version (latest, or as of a timestamp) and
`MultiVersionedKeyQuery` for a range of versions. The original
`KafkaStreams#store(...)` API has no queryable store type for versioned stores.
+
+Because IQv2 is extensible, a custom state store may implement additional
query types of its own. When a store does not know how to handle a query, it
does not throw; instead it returns a failed `QueryResult` with
`FailureReason.UNKNOWN_QUERY_TYPE`.
+
+## Handling query results
+
+A `StateQueryResult` aggregates one `QueryResult` per partition that ran the
query. By default a request runs against all locally available partitions of
the store (unless you narrow it with `withPartitions(...)`), so
`getPartitionResults()` may contain an entry for every one of those partitions.
+
+Which accessor to use is determined by the *query type* you chose, not by
inspecting the result at runtime:
+
+ * **Point lookups** (`KeyQuery`, `TimestampedKeyQuery`, `VersionedKeyQuery`)
can only match in the single partition that owns the key, so at most one
partition returns a value (the other queried partitions return a successful
result with `null`). Use `getOnlyPartitionResult()`.
+ * **Range, scan, and window queries** (`RangeQuery`, `WindowRangeQuery`,
`MultiVersionedKeyQuery`, and so on) can match records in every queried
partition, so results are spread across partitions. Use `getPartitionResults()`
and iterate.
+
+The accessors are:
+
+ * `getPartitionResults()` returns a `Map<Integer, QueryResult<R>>`, keyed by
partition number — one entry per partition that ran the query. This is the
general form and works for any query.
+ * `getOnlyPartitionResult()` is a convenience that filters to the single
partition that produced a value and returns it (or `null` if none did). It
throws `IllegalArgumentException` if more than one partition produced a value,
so only use it when you know the query matches at most one partition.
Review Comment:
[content] "filters to the single partition that produced a value" and
"throws `IllegalArgumentException` if more than one partition **produced a
value**" understate when this throws. The filter keeps failures as well as
values:
```java
final List<QueryResult<R>> nonempty =
partitionResults.values().stream()
.filter(r -> r.isFailure() || r.getResult() != null)
.collect(Collectors.toList());
if (nonempty.size() > 1) {
throw new IllegalArgumentException(...)
```
(`StateQueryResult.java:66-81`)
So it throws when more than one partition returns a value **or a failure**.
That matters for the pattern this section recommends: a `KeyQuery` issued with
`requireActive()` — or against an instance hosting several partitions of the
store during a rebalance — yields multiple `NOT_ACTIVE` / `NOT_PRESENT`
failures, and `getOnlyPartitionResult()` then throws instead of returning the
key's result.
Suggest: "returns the single partition result that is either a failure or a
non-`null` value, or `null` if there is none; throws `IllegalArgumentException`
if more than one partition returned a value or a failure." A one-line caution
that failures count towards that limit would save readers a surprise.
##########
docs/streams/developer-guide/interactive-queries.md:
##########
@@ -499,6 +499,198 @@ At this point the full state of the application is
interactively queryable:
+# Interactive Queries v2 (IQv2) {#interactive-queries-v2}
+
+The sections above describe the original interactive queries API (informally,
"IQv1"), where you obtain a read-only store facade from
[KafkaStreams#store(...)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
and call methods such as `get(key)` or `range(from, to)` on it. Interactive
Queries v2 (IQv2), introduced in
[KIP-796](https://cwiki.apache.org/confluence/x/34xnCw), is an alternative,
more flexible API for the same purpose: querying the local state of a running
Kafka Streams application.
+
+Instead of a fixed store facade, IQv2 models every query as a first-class
[Query](/{version}/javadoc/org/apache/kafka/streams/query/Query.html) object
that you submit through a single
[KafkaStreams#query(StateQueryRequest)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
method. The response is a
[StateQueryResult](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryResult.html)
that contains a separate
[QueryResult](/{version}/javadoc/org/apache/kafka/streams/query/QueryResult.html)
for each partition that executed the query, along with metadata such as each
partition's
[Position](/{version}/javadoc/org/apache/kafka/streams/query/Position.html).
+
+IQv2 offers several advantages over the original API:
+
+ * A single, uniform entry point for every kind of query.
+ * Query-level extensibility: custom state stores can handle their own
`Query` types (the runtime forwards unknown queries straight through to the
underlying byte store), rather than requiring a custom `QueryableStoreType`
store facade as the original API does.
+ * Rich, per-partition results with typed failure reasons instead of
exceptions.
+ * Fine-grained consistency control through `Position` and `PositionBound`.
+ * The ability to target specific partitions, require active (leader) tasks,
override the isolation level, and collect execution information.
+
+Both APIs are fully supported and can be used side by side. Note that IQv2 is
marked as an evolving API, so it may change between releases, and global stores
are not yet supported by `query(...)` (see [Limitations](#limitations-of-iqv2)
below).
+
+## How Interactive Queries v2 works
+
+Issuing an IQv2 query follows four steps:
+
+ 1. **Build a query.** Create a `Query` object that describes what you want
to read, for example a
[KeyQuery](/{version}/javadoc/org/apache/kafka/streams/query/KeyQuery.html) for
a single-key lookup or a
[RangeQuery](/{version}/javadoc/org/apache/kafka/streams/query/RangeQuery.html)
for a scan.
+ 2. **Build a request.** Wrap the query in a
[StateQueryRequest](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryRequest.html)
that names the store to query:
`StateQueryRequest.inStore(storeName).withQuery(query)`. Optionally configure
the request (partitions, position bound, isolation level, and so on).
+ 3. **Execute the request.** Call `streams.query(request)`, which runs the
query against every locally available partition of the store (or just the
partitions you requested) and returns a `StateQueryResult`.
+ 4. **Read the results.** For a query that targets a single partition, use
`getOnlyPartitionResult()`. For a query that may span multiple partitions, use
`getPartitionResults()` to get a `Map` from partition number to `QueryResult`.
Each `QueryResult` reports whether the query succeeded on that partition and
holds either the result value or a typed failure reason.
+
+## Building and executing a query
+
+The following example looks up a single key in the `CountsKeyValueStore` state
store from the word-count example used earlier on this page:
+
+```java
+import org.apache.kafka.streams.KafkaStreams;
+import org.apache.kafka.streams.query.KeyQuery;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.StateQueryRequest;
+import org.apache.kafka.streams.query.StateQueryResult;
+
+import static org.apache.kafka.streams.query.StateQueryRequest.inStore;
+
+KafkaStreams streams = ...;
+
+// 1. Build the query: retrieve the value for a single key.
+KeyQuery<String, Long> query = KeyQuery.withKey("alice");
+
+// 2. Build the request, naming the store to run the query against.
+// A KeyQuery<String, Long> is a Query<Long>, so the request is a
StateQueryRequest<Long>.
+StateQueryRequest<Long> request =
inStore("CountsKeyValueStore").withQuery(query);
+
+// 3. Execute the query.
+StateQueryResult<Long> result = streams.query(request);
+
+// 4. Read the result. A given key lives in exactly one partition, so at most
one partition
+// returns a value. getOnlyPartitionResult() returns that partition's
result, or null if
+// no locally available partition holds the key.
+QueryResult<Long> partitionResult = result.getOnlyPartitionResult();
+if (partitionResult != null && partitionResult.isSuccess()) {
+ Long count = partitionResult.getResult();
+ System.out.println("Count for alice: " + count);
+}
+```
+
+Note that the type parameter of `StateQueryRequest` (and of `StateQueryResult`
and `QueryResult`) is the query's *result* type, not the query type: a
`KeyQuery<String, Long>` implements `Query<Long>`, so the request is a
`StateQueryRequest<Long>`.
+
+## Built-in query types
+
+Kafka Streams ships with a set of query types covering the standard store
types. All of them live in the
[org.apache.kafka.streams.query](/{version}/javadoc/org/apache/kafka/streams/query/package-summary.html)
package.
+
+| Query | Result type (`R`) | Key factory / builder methods |
+| --- | --- | --- |
+| `KeyQuery<K, V>` | `V` | `withKey(key)`; `.skipCache()` |
+| `TimestampedKeyQuery<K, V>` | `ValueAndTimestamp<V>` | `withKey(key)`;
`.skipCache()` |
+| `RangeQuery<K, V>` | `KeyValueIterator<K, V>` | `withRange(lower, upper)`,
`withLowerBound(lower)`, `withUpperBound(upper)`, `withNoBounds()`;
`.withAscendingKeys()` / `.withDescendingKeys()` |
+| `TimestampedRangeQuery<K, V>` | `KeyValueIterator<K, ValueAndTimestamp<V>>`
| `withRange(lower, upper)`, `withLowerBound(lower)`, `withUpperBound(upper)`,
`withNoBounds()`; `.withAscendingKeys()` / `.withDescendingKeys()` |
+| `WindowKeyQuery<K, V>` | `WindowStoreIterator<V>` |
`withKeyAndWindowStartRange(key, timeFrom, timeTo)` |
+| `WindowRangeQuery<K, V>` | `KeyValueIterator<Windowed<K>, V>` |
`withKey(key)`, `withWindowStartRange(timeFrom, timeTo)` |
+| `VersionedKeyQuery<K, V>` | `VersionedRecord<V>` | `withKey(key)`;
`.asOf(instant)` |
+| `MultiVersionedKeyQuery<K, V>` | `VersionedRecordIterator<V>` |
`withKey(key)`; `.fromTime(instant)`, `.toTime(instant)`,
`.withAscendingTimestamps()` / `.withDescendingTimestamps()` |
+
+For range and scan queries (`RangeQuery`, `TimestampedRangeQuery`), passing no
bounds performs a full scan, and result ordering is based on the serialized
`byte[]` of the keys, not on the logical key order.
+
+Versioned key-value stores are queryable **only** through IQv2 — use
`VersionedKeyQuery` for a single version (latest, or as of a timestamp) and
`MultiVersionedKeyQuery` for a range of versions. The original
`KafkaStreams#store(...)` API has no queryable store type for versioned stores.
+
+Because IQv2 is extensible, a custom state store may implement additional
query types of its own. When a store does not know how to handle a query, it
does not throw; instead it returns a failed `QueryResult` with
`FailureReason.UNKNOWN_QUERY_TYPE`.
+
+## Handling query results
+
+A `StateQueryResult` aggregates one `QueryResult` per partition that ran the
query. By default a request runs against all locally available partitions of
the store (unless you narrow it with `withPartitions(...)`), so
`getPartitionResults()` may contain an entry for every one of those partitions.
+
+Which accessor to use is determined by the *query type* you chose, not by
inspecting the result at runtime:
+
+ * **Point lookups** (`KeyQuery`, `TimestampedKeyQuery`, `VersionedKeyQuery`)
can only match in the single partition that owns the key, so at most one
partition returns a value (the other queried partitions return a successful
result with `null`). Use `getOnlyPartitionResult()`.
+ * **Range, scan, and window queries** (`RangeQuery`, `WindowRangeQuery`,
`MultiVersionedKeyQuery`, and so on) can match records in every queried
partition, so results are spread across partitions. Use `getPartitionResults()`
and iterate.
+
+The accessors are:
+
+ * `getPartitionResults()` returns a `Map<Integer, QueryResult<R>>`, keyed by
partition number — one entry per partition that ran the query. This is the
general form and works for any query.
+ * `getOnlyPartitionResult()` is a convenience that filters to the single
partition that produced a value and returns it (or `null` if none did). It
throws `IllegalArgumentException` if more than one partition produced a value,
so only use it when you know the query matches at most one partition.
+ * `getPosition()` returns the merged `Position` observed across the
partition results.
Review Comment:
[completeness] The accessor list is missing `getGlobalResult()`, and its
absence interacts badly with the global-store limitation stated at line 516 and
line 690.
Querying a global store does **not** throw and does **not** populate the
partition results. `KafkaStreams#query` calls `result.setGlobalResult(...)`
with a failed result:
```java
if (globalStateStores.containsKey(storeName)) {
// See KAFKA-13523
result.setGlobalResult(
QueryResult.forFailure(
FailureReason.UNKNOWN_QUERY_TYPE,
"Global stores do not yet support the KafkaStreams#query API.
Use KafkaStreams#store instead."
```
(`KafkaStreams.java:2183-2191`)
So a reader who follows this section verbatim sees an empty
`getPartitionResults()` map and `null` from `getOnlyPartitionResult()`, with
the explanatory failure invisible in `getGlobalResult()`. Suggest a fourth
bullet: "`getGlobalResult()` returns the result for a global-store query and is
`null` for partitioned stores (conversely, `getPartitionResults()` is empty for
global-store queries). This is where the current global-store rejection
surfaces, as a failed result with `UNKNOWN_QUERY_TYPE`."
##########
docs/streams/developer-guide/interactive-queries.md:
##########
@@ -499,6 +499,198 @@ At this point the full state of the application is
interactively queryable:
+# Interactive Queries v2 (IQv2) {#interactive-queries-v2}
+
+The sections above describe the original interactive queries API (informally,
"IQv1"), where you obtain a read-only store facade from
[KafkaStreams#store(...)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
and call methods such as `get(key)` or `range(from, to)` on it. Interactive
Queries v2 (IQv2), introduced in
[KIP-796](https://cwiki.apache.org/confluence/x/34xnCw), is an alternative,
more flexible API for the same purpose: querying the local state of a running
Kafka Streams application.
+
+Instead of a fixed store facade, IQv2 models every query as a first-class
[Query](/{version}/javadoc/org/apache/kafka/streams/query/Query.html) object
that you submit through a single
[KafkaStreams#query(StateQueryRequest)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
method. The response is a
[StateQueryResult](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryResult.html)
that contains a separate
[QueryResult](/{version}/javadoc/org/apache/kafka/streams/query/QueryResult.html)
for each partition that executed the query, along with metadata such as each
partition's
[Position](/{version}/javadoc/org/apache/kafka/streams/query/Position.html).
+
+IQv2 offers several advantages over the original API:
+
+ * A single, uniform entry point for every kind of query.
+ * Query-level extensibility: custom state stores can handle their own
`Query` types (the runtime forwards unknown queries straight through to the
underlying byte store), rather than requiring a custom `QueryableStoreType`
store facade as the original API does.
+ * Rich, per-partition results with typed failure reasons instead of
exceptions.
+ * Fine-grained consistency control through `Position` and `PositionBound`.
+ * The ability to target specific partitions, require active (leader) tasks,
override the isolation level, and collect execution information.
+
+Both APIs are fully supported and can be used side by side. Note that IQv2 is
marked as an evolving API, so it may change between releases, and global stores
are not yet supported by `query(...)` (see [Limitations](#limitations-of-iqv2)
below).
+
+## How Interactive Queries v2 works
+
+Issuing an IQv2 query follows four steps:
+
+ 1. **Build a query.** Create a `Query` object that describes what you want
to read, for example a
[KeyQuery](/{version}/javadoc/org/apache/kafka/streams/query/KeyQuery.html) for
a single-key lookup or a
[RangeQuery](/{version}/javadoc/org/apache/kafka/streams/query/RangeQuery.html)
for a scan.
+ 2. **Build a request.** Wrap the query in a
[StateQueryRequest](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryRequest.html)
that names the store to query:
`StateQueryRequest.inStore(storeName).withQuery(query)`. Optionally configure
the request (partitions, position bound, isolation level, and so on).
+ 3. **Execute the request.** Call `streams.query(request)`, which runs the
query against every locally available partition of the store (or just the
partitions you requested) and returns a `StateQueryResult`.
Review Comment:
[completeness] This step correctly scopes `query()` to "every locally
available partition", but the section never says how IQv2 relates to
cross-instance / remote querying. A reader arriving from `# Querying remote
state stores for the entire app` will reasonably wonder whether instance
discovery (`StreamsMetadata`, `queryMetadataForKey`) applies to IQv2 or whether
`query()` transparently fans out across instances.
A one-line note — that `query()` only reads this instance's local state, and
that discovering which instance hosts a key works the same way as for IQv1 —
would close the gap and tie the two halves of the page together.
(Carried forward from an earlier pending review, now line-anchored.)
##########
docs/streams/developer-guide/interactive-queries.md:
##########
@@ -499,6 +499,198 @@ At this point the full state of the application is
interactively queryable:
+# Interactive Queries v2 (IQv2) {#interactive-queries-v2}
+
+The sections above describe the original interactive queries API (informally,
"IQv1"), where you obtain a read-only store facade from
[KafkaStreams#store(...)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
and call methods such as `get(key)` or `range(from, to)` on it. Interactive
Queries v2 (IQv2), introduced in
[KIP-796](https://cwiki.apache.org/confluence/x/34xnCw), is an alternative,
more flexible API for the same purpose: querying the local state of a running
Kafka Streams application.
+
+Instead of a fixed store facade, IQv2 models every query as a first-class
[Query](/{version}/javadoc/org/apache/kafka/streams/query/Query.html) object
that you submit through a single
[KafkaStreams#query(StateQueryRequest)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
method. The response is a
[StateQueryResult](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryResult.html)
that contains a separate
[QueryResult](/{version}/javadoc/org/apache/kafka/streams/query/QueryResult.html)
for each partition that executed the query, along with metadata such as each
partition's
[Position](/{version}/javadoc/org/apache/kafka/streams/query/Position.html).
+
+IQv2 offers several advantages over the original API:
+
+ * A single, uniform entry point for every kind of query.
+ * Query-level extensibility: custom state stores can handle their own
`Query` types (the runtime forwards unknown queries straight through to the
underlying byte store), rather than requiring a custom `QueryableStoreType`
store facade as the original API does.
+ * Rich, per-partition results with typed failure reasons instead of
exceptions.
+ * Fine-grained consistency control through `Position` and `PositionBound`.
+ * The ability to target specific partitions, require active (leader) tasks,
override the isolation level, and collect execution information.
+
+Both APIs are fully supported and can be used side by side. Note that IQv2 is
marked as an evolving API, so it may change between releases, and global stores
are not yet supported by `query(...)` (see [Limitations](#limitations-of-iqv2)
below).
+
+## How Interactive Queries v2 works
+
+Issuing an IQv2 query follows four steps:
+
+ 1. **Build a query.** Create a `Query` object that describes what you want
to read, for example a
[KeyQuery](/{version}/javadoc/org/apache/kafka/streams/query/KeyQuery.html) for
a single-key lookup or a
[RangeQuery](/{version}/javadoc/org/apache/kafka/streams/query/RangeQuery.html)
for a scan.
+ 2. **Build a request.** Wrap the query in a
[StateQueryRequest](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryRequest.html)
that names the store to query:
`StateQueryRequest.inStore(storeName).withQuery(query)`. Optionally configure
the request (partitions, position bound, isolation level, and so on).
+ 3. **Execute the request.** Call `streams.query(request)`, which runs the
query against every locally available partition of the store (or just the
partitions you requested) and returns a `StateQueryResult`.
+ 4. **Read the results.** For a query that targets a single partition, use
`getOnlyPartitionResult()`. For a query that may span multiple partitions, use
`getPartitionResults()` to get a `Map` from partition number to `QueryResult`.
Each `QueryResult` reports whether the query succeeded on that partition and
holds either the result value or a typed failure reason.
+
+## Building and executing a query
+
+The following example looks up a single key in the `CountsKeyValueStore` state
store from the word-count example used earlier on this page:
+
+```java
+import org.apache.kafka.streams.KafkaStreams;
+import org.apache.kafka.streams.query.KeyQuery;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.StateQueryRequest;
+import org.apache.kafka.streams.query.StateQueryResult;
+
+import static org.apache.kafka.streams.query.StateQueryRequest.inStore;
+
+KafkaStreams streams = ...;
+
+// 1. Build the query: retrieve the value for a single key.
+KeyQuery<String, Long> query = KeyQuery.withKey("alice");
+
+// 2. Build the request, naming the store to run the query against.
+// A KeyQuery<String, Long> is a Query<Long>, so the request is a
StateQueryRequest<Long>.
+StateQueryRequest<Long> request =
inStore("CountsKeyValueStore").withQuery(query);
+
+// 3. Execute the query.
+StateQueryResult<Long> result = streams.query(request);
+
+// 4. Read the result. A given key lives in exactly one partition, so at most
one partition
+// returns a value. getOnlyPartitionResult() returns that partition's
result, or null if
+// no locally available partition holds the key.
+QueryResult<Long> partitionResult = result.getOnlyPartitionResult();
+if (partitionResult != null && partitionResult.isSuccess()) {
+ Long count = partitionResult.getResult();
+ System.out.println("Count for alice: " + count);
+}
+```
+
+Note that the type parameter of `StateQueryRequest` (and of `StateQueryResult`
and `QueryResult`) is the query's *result* type, not the query type: a
`KeyQuery<String, Long>` implements `Query<Long>`, so the request is a
`StateQueryRequest<Long>`.
+
+## Built-in query types
+
+Kafka Streams ships with a set of query types covering the standard store
types. All of them live in the
[org.apache.kafka.streams.query](/{version}/javadoc/org/apache/kafka/streams/query/package-summary.html)
package.
+
+| Query | Result type (`R`) | Key factory / builder methods |
+| --- | --- | --- |
+| `KeyQuery<K, V>` | `V` | `withKey(key)`; `.skipCache()` |
+| `TimestampedKeyQuery<K, V>` | `ValueAndTimestamp<V>` | `withKey(key)`;
`.skipCache()` |
+| `RangeQuery<K, V>` | `KeyValueIterator<K, V>` | `withRange(lower, upper)`,
`withLowerBound(lower)`, `withUpperBound(upper)`, `withNoBounds()`;
`.withAscendingKeys()` / `.withDescendingKeys()` |
+| `TimestampedRangeQuery<K, V>` | `KeyValueIterator<K, ValueAndTimestamp<V>>`
| `withRange(lower, upper)`, `withLowerBound(lower)`, `withUpperBound(upper)`,
`withNoBounds()`; `.withAscendingKeys()` / `.withDescendingKeys()` |
+| `WindowKeyQuery<K, V>` | `WindowStoreIterator<V>` |
`withKeyAndWindowStartRange(key, timeFrom, timeTo)` |
+| `WindowRangeQuery<K, V>` | `KeyValueIterator<Windowed<K>, V>` |
`withKey(key)`, `withWindowStartRange(timeFrom, timeTo)` |
+| `VersionedKeyQuery<K, V>` | `VersionedRecord<V>` | `withKey(key)`;
`.asOf(instant)` |
+| `MultiVersionedKeyQuery<K, V>` | `VersionedRecordIterator<V>` |
`withKey(key)`; `.fromTime(instant)`, `.toTime(instant)`,
`.withAscendingTimestamps()` / `.withDescendingTimestamps()` |
+
+For range and scan queries (`RangeQuery`, `TimestampedRangeQuery`), passing no
bounds performs a full scan, and result ordering is based on the serialized
`byte[]` of the keys, not on the logical key order.
+
+Versioned key-value stores are queryable **only** through IQv2 — use
`VersionedKeyQuery` for a single version (latest, or as of a timestamp) and
`MultiVersionedKeyQuery` for a range of versions. The original
`KafkaStreams#store(...)` API has no queryable store type for versioned stores.
+
+Because IQv2 is extensible, a custom state store may implement additional
query types of its own. When a store does not know how to handle a query, it
does not throw; instead it returns a failed `QueryResult` with
`FailureReason.UNKNOWN_QUERY_TYPE`.
+
+## Handling query results
+
+A `StateQueryResult` aggregates one `QueryResult` per partition that ran the
query. By default a request runs against all locally available partitions of
the store (unless you narrow it with `withPartitions(...)`), so
`getPartitionResults()` may contain an entry for every one of those partitions.
+
+Which accessor to use is determined by the *query type* you chose, not by
inspecting the result at runtime:
+
+ * **Point lookups** (`KeyQuery`, `TimestampedKeyQuery`, `VersionedKeyQuery`)
can only match in the single partition that owns the key, so at most one
partition returns a value (the other queried partitions return a successful
result with `null`). Use `getOnlyPartitionResult()`.
+ * **Range, scan, and window queries** (`RangeQuery`, `WindowRangeQuery`,
`MultiVersionedKeyQuery`, and so on) can match records in every queried
partition, so results are spread across partitions. Use `getPartitionResults()`
and iterate.
+
+The accessors are:
+
+ * `getPartitionResults()` returns a `Map<Integer, QueryResult<R>>`, keyed by
partition number — one entry per partition that ran the query. This is the
general form and works for any query.
+ * `getOnlyPartitionResult()` is a convenience that filters to the single
partition that produced a value and returns it (or `null` if none did). It
throws `IllegalArgumentException` if more than one partition produced a value,
so only use it when you know the query matches at most one partition.
+ * `getPosition()` returns the merged `Position` observed across the
partition results.
+
+Each `QueryResult` reports the outcome for one partition. Use `isSuccess()` /
`isFailure()` before reading: `getResult()` returns the value on success (which
may itself be `null`, for example when a key is not found), while
`getFailureReason()` and `getFailureMessage()` describe a failure. Results are
always **per-partition** — a query may succeed on some partitions and fail on
others (for example, if one partition has migrated off this instance).
+
+When a query returns an iterator (`RangeQuery`, `WindowKeyQuery`,
`MultiVersionedKeyQuery`, and so on), the iterator must be closed after use.
Iterate over every partition's result and use a try-with-resources block:
+
+```java
+import org.apache.kafka.streams.KeyValue;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.RangeQuery;
+import org.apache.kafka.streams.query.StateQueryRequest;
+import org.apache.kafka.streams.query.StateQueryResult;
+import org.apache.kafka.streams.state.KeyValueIterator;
+
+import java.util.Map;
+
+import static org.apache.kafka.streams.query.StateQueryRequest.inStore;
+
+RangeQuery<String, Long> query = RangeQuery.withRange("a", "n");
+StateQueryRequest<KeyValueIterator<String, Long>> request =
+ inStore("CountsKeyValueStore").withQuery(query);
+StateQueryResult<KeyValueIterator<String, Long>> result =
streams.query(request);
+
+for (Map.Entry<Integer, QueryResult<KeyValueIterator<String, Long>>> entry
+ : result.getPartitionResults().entrySet()) {
+ QueryResult<KeyValueIterator<String, Long>> partitionResult =
entry.getValue();
+ if (partitionResult.isFailure()) {
+ System.out.println("Partition " + entry.getKey() + " failed: "
+ + partitionResult.getFailureReason() + " - " +
partitionResult.getFailureMessage());
+ continue;
+ }
+ try (KeyValueIterator<String, Long> iterator =
partitionResult.getResult()) {
+ while (iterator.hasNext()) {
+ KeyValue<String, Long> record = iterator.next();
+ System.out.println(record.key + ": " + record.value);
+ }
+ }
+}
+```
+
+A failed `QueryResult` carries one of the
[FailureReason](/{version}/javadoc/org/apache/kafka/streams/query/FailureReason.html)
values:
+
+| `FailureReason` | Meaning | Recommended action |
+| --- | --- | --- |
+| `UNKNOWN_QUERY_TYPE` | The store does not know how to execute this query. |
Verify the query is supported by that store type; for custom queries, contact
the store maintainer. |
+| `NOT_ACTIVE` | `requireActive()` was set, but the partition is a standby or
an active task that is not yet in the `RUNNING` state. | Retry later or query a
different replica. |
+| `NOT_UP_TO_BOUND` | The partition has not yet caught up to the requested
`PositionBound`. | Retry later or query a different replica. |
+| `NOT_PRESENT` | The requested partition is not present on this instance (for
example, it migrated during a rebalance). | Query a different replica. |
+| `DOES_NOT_EXIST` | The requested partition does not exist for this store. |
Correct the requested set of partitions. |
+| `STORE_EXCEPTION` | The store threw an exception while executing the query.
| Depending on the exception, retry this instance or a different one. |
+
+## Controlling consistency and availability
+
+A `StateQueryRequest` is immutable; each configuration method returns a new
request. Beyond `inStore(...).withQuery(...)`, the following options let you
trade off consistency, availability, and cost:
+
+ * `withPartitions(Set<Integer>)` / `withAllPartitions()`: run against a
specific set of partitions or against all locally available partitions (the
default). Partitions that are missing return `NOT_PRESENT`; partitions that do
not exist return `DOES_NOT_EXIST`.
+ * `requireActive()`: run only on active (leader) partitions. Non-active
partitions return `NOT_ACTIVE`. Use this when you need the most up-to-date data
and want to avoid reading from standby replicas.
+ * `withPositionBound(PositionBound)`: by default a request is
`PositionBound.unbounded()`. Use `PositionBound.at(position)` to require that
each queried partition has consumed up to a given `Position` before serving the
query; a partition that is behind returns `NOT_UP_TO_BOUND`. Combined with the
`Position` returned by `StateQueryResult#getPosition()`, this lets you
implement read-your-writes / monotonic reads: feed the position from one query
into the bound of the next so repeated queries never appear to move backwards
in time, while still allowing reads to be served from any replica that is
caught up.
+ * `withIsolationLevel(IsolationLevel)`: override the isolation level for
this query. When not set, the effective level falls back to the
`default.interactive.query.isolation.level` configuration.
Review Comment:
[completeness] The config name and fallback behaviour are both correct
(`StreamsConfig.java:547`; `StateQueryRequest.java:151-153`, and the runtime
resolves it via
`request.isolationLevel().orElseGet(applicationConfigs::defaultInteractiveQueryIsolationLevel)`
at `KafkaStreams.java:2225-2228`). What's missing is the precondition that
makes the option meaningful — the config's own documentation says:
> Only meaningful when `enable.transactional.statestores` is `true`:
`READ_UNCOMMITTED` reads include writes staged in the transaction buffer since
the last commit; `READ_COMMITTED` reads skip the transaction buffer and return
only committed data.
(`StreamsConfig.java:548-552`)
Also worth stating the default, `READ_UNCOMMITTED`
(`StreamsConfig.java:553`), since a reader may assume IQ defaults to committed
reads. A short parenthetical covering both would make this bullet
self-contained.
##########
docs/streams/developer-guide/interactive-queries.md:
##########
@@ -499,6 +499,198 @@ At this point the full state of the application is
interactively queryable:
+# Interactive Queries v2 (IQv2) {#interactive-queries-v2}
+
+The sections above describe the original interactive queries API (informally,
"IQv1"), where you obtain a read-only store facade from
[KafkaStreams#store(...)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
and call methods such as `get(key)` or `range(from, to)` on it. Interactive
Queries v2 (IQv2), introduced in
[KIP-796](https://cwiki.apache.org/confluence/x/34xnCw), is an alternative,
more flexible API for the same purpose: querying the local state of a running
Kafka Streams application.
+
+Instead of a fixed store facade, IQv2 models every query as a first-class
[Query](/{version}/javadoc/org/apache/kafka/streams/query/Query.html) object
that you submit through a single
[KafkaStreams#query(StateQueryRequest)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
method. The response is a
[StateQueryResult](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryResult.html)
that contains a separate
[QueryResult](/{version}/javadoc/org/apache/kafka/streams/query/QueryResult.html)
for each partition that executed the query, along with metadata such as each
partition's
[Position](/{version}/javadoc/org/apache/kafka/streams/query/Position.html).
+
+IQv2 offers several advantages over the original API:
+
+ * A single, uniform entry point for every kind of query.
+ * Query-level extensibility: custom state stores can handle their own
`Query` types (the runtime forwards unknown queries straight through to the
underlying byte store), rather than requiring a custom `QueryableStoreType`
store facade as the original API does.
+ * Rich, per-partition results with typed failure reasons instead of
exceptions.
+ * Fine-grained consistency control through `Position` and `PositionBound`.
+ * The ability to target specific partitions, require active (leader) tasks,
override the isolation level, and collect execution information.
+
+Both APIs are fully supported and can be used side by side. Note that IQv2 is
marked as an evolving API, so it may change between releases, and global stores
are not yet supported by `query(...)` (see [Limitations](#limitations-of-iqv2)
below).
+
+## How Interactive Queries v2 works
+
+Issuing an IQv2 query follows four steps:
+
+ 1. **Build a query.** Create a `Query` object that describes what you want
to read, for example a
[KeyQuery](/{version}/javadoc/org/apache/kafka/streams/query/KeyQuery.html) for
a single-key lookup or a
[RangeQuery](/{version}/javadoc/org/apache/kafka/streams/query/RangeQuery.html)
for a scan.
+ 2. **Build a request.** Wrap the query in a
[StateQueryRequest](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryRequest.html)
that names the store to query:
`StateQueryRequest.inStore(storeName).withQuery(query)`. Optionally configure
the request (partitions, position bound, isolation level, and so on).
+ 3. **Execute the request.** Call `streams.query(request)`, which runs the
query against every locally available partition of the store (or just the
partitions you requested) and returns a `StateQueryResult`.
+ 4. **Read the results.** For a query that targets a single partition, use
`getOnlyPartitionResult()`. For a query that may span multiple partitions, use
`getPartitionResults()` to get a `Map` from partition number to `QueryResult`.
Each `QueryResult` reports whether the query succeeded on that partition and
holds either the result value or a typed failure reason.
+
+## Building and executing a query
+
+The following example looks up a single key in the `CountsKeyValueStore` state
store from the word-count example used earlier on this page:
+
+```java
+import org.apache.kafka.streams.KafkaStreams;
+import org.apache.kafka.streams.query.KeyQuery;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.StateQueryRequest;
+import org.apache.kafka.streams.query.StateQueryResult;
+
+import static org.apache.kafka.streams.query.StateQueryRequest.inStore;
+
+KafkaStreams streams = ...;
+
+// 1. Build the query: retrieve the value for a single key.
+KeyQuery<String, Long> query = KeyQuery.withKey("alice");
+
+// 2. Build the request, naming the store to run the query against.
+// A KeyQuery<String, Long> is a Query<Long>, so the request is a
StateQueryRequest<Long>.
+StateQueryRequest<Long> request =
inStore("CountsKeyValueStore").withQuery(query);
+
+// 3. Execute the query.
+StateQueryResult<Long> result = streams.query(request);
+
+// 4. Read the result. A given key lives in exactly one partition, so at most
one partition
+// returns a value. getOnlyPartitionResult() returns that partition's
result, or null if
+// no locally available partition holds the key.
+QueryResult<Long> partitionResult = result.getOnlyPartitionResult();
+if (partitionResult != null && partitionResult.isSuccess()) {
+ Long count = partitionResult.getResult();
+ System.out.println("Count for alice: " + count);
+}
+```
+
+Note that the type parameter of `StateQueryRequest` (and of `StateQueryResult`
and `QueryResult`) is the query's *result* type, not the query type: a
`KeyQuery<String, Long>` implements `Query<Long>`, so the request is a
`StateQueryRequest<Long>`.
+
+## Built-in query types
+
+Kafka Streams ships with a set of query types covering the standard store
types. All of them live in the
[org.apache.kafka.streams.query](/{version}/javadoc/org/apache/kafka/streams/query/package-summary.html)
package.
+
+| Query | Result type (`R`) | Key factory / builder methods |
+| --- | --- | --- |
+| `KeyQuery<K, V>` | `V` | `withKey(key)`; `.skipCache()` |
+| `TimestampedKeyQuery<K, V>` | `ValueAndTimestamp<V>` | `withKey(key)`;
`.skipCache()` |
+| `RangeQuery<K, V>` | `KeyValueIterator<K, V>` | `withRange(lower, upper)`,
`withLowerBound(lower)`, `withUpperBound(upper)`, `withNoBounds()`;
`.withAscendingKeys()` / `.withDescendingKeys()` |
+| `TimestampedRangeQuery<K, V>` | `KeyValueIterator<K, ValueAndTimestamp<V>>`
| `withRange(lower, upper)`, `withLowerBound(lower)`, `withUpperBound(upper)`,
`withNoBounds()`; `.withAscendingKeys()` / `.withDescendingKeys()` |
+| `WindowKeyQuery<K, V>` | `WindowStoreIterator<V>` |
`withKeyAndWindowStartRange(key, timeFrom, timeTo)` |
+| `WindowRangeQuery<K, V>` | `KeyValueIterator<Windowed<K>, V>` |
`withKey(key)`, `withWindowStartRange(timeFrom, timeTo)` |
+| `VersionedKeyQuery<K, V>` | `VersionedRecord<V>` | `withKey(key)`;
`.asOf(instant)` |
+| `MultiVersionedKeyQuery<K, V>` | `VersionedRecordIterator<V>` |
`withKey(key)`; `.fromTime(instant)`, `.toTime(instant)`,
`.withAscendingTimestamps()` / `.withDescendingTimestamps()` |
+
+For range and scan queries (`RangeQuery`, `TimestampedRangeQuery`), passing no
bounds performs a full scan, and result ordering is based on the serialized
`byte[]` of the keys, not on the logical key order.
+
+Versioned key-value stores are queryable **only** through IQv2 — use
`VersionedKeyQuery` for a single version (latest, or as of a timestamp) and
`MultiVersionedKeyQuery` for a range of versions. The original
`KafkaStreams#store(...)` API has no queryable store type for versioned stores.
+
+Because IQv2 is extensible, a custom state store may implement additional
query types of its own. When a store does not know how to handle a query, it
does not throw; instead it returns a failed `QueryResult` with
`FailureReason.UNKNOWN_QUERY_TYPE`.
+
+## Handling query results
+
+A `StateQueryResult` aggregates one `QueryResult` per partition that ran the
query. By default a request runs against all locally available partitions of
the store (unless you narrow it with `withPartitions(...)`), so
`getPartitionResults()` may contain an entry for every one of those partitions.
+
+Which accessor to use is determined by the *query type* you chose, not by
inspecting the result at runtime:
+
+ * **Point lookups** (`KeyQuery`, `TimestampedKeyQuery`, `VersionedKeyQuery`)
can only match in the single partition that owns the key, so at most one
partition returns a value (the other queried partitions return a successful
result with `null`). Use `getOnlyPartitionResult()`.
+ * **Range, scan, and window queries** (`RangeQuery`, `WindowRangeQuery`,
`MultiVersionedKeyQuery`, and so on) can match records in every queried
partition, so results are spread across partitions. Use `getPartitionResults()`
and iterate.
+
+The accessors are:
+
+ * `getPartitionResults()` returns a `Map<Integer, QueryResult<R>>`, keyed by
partition number — one entry per partition that ran the query. This is the
general form and works for any query.
+ * `getOnlyPartitionResult()` is a convenience that filters to the single
partition that produced a value and returns it (or `null` if none did). It
throws `IllegalArgumentException` if more than one partition produced a value,
so only use it when you know the query matches at most one partition.
+ * `getPosition()` returns the merged `Position` observed across the
partition results.
+
+Each `QueryResult` reports the outcome for one partition. Use `isSuccess()` /
`isFailure()` before reading: `getResult()` returns the value on success (which
may itself be `null`, for example when a key is not found), while
`getFailureReason()` and `getFailureMessage()` describe a failure. Results are
always **per-partition** — a query may succeed on some partitions and fail on
others (for example, if one partition has migrated off this instance).
+
+When a query returns an iterator (`RangeQuery`, `WindowKeyQuery`,
`MultiVersionedKeyQuery`, and so on), the iterator must be closed after use.
Iterate over every partition's result and use a try-with-resources block:
+
+```java
+import org.apache.kafka.streams.KeyValue;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.RangeQuery;
+import org.apache.kafka.streams.query.StateQueryRequest;
+import org.apache.kafka.streams.query.StateQueryResult;
+import org.apache.kafka.streams.state.KeyValueIterator;
+
+import java.util.Map;
+
+import static org.apache.kafka.streams.query.StateQueryRequest.inStore;
+
+RangeQuery<String, Long> query = RangeQuery.withRange("a", "n");
+StateQueryRequest<KeyValueIterator<String, Long>> request =
+ inStore("CountsKeyValueStore").withQuery(query);
+StateQueryResult<KeyValueIterator<String, Long>> result =
streams.query(request);
+
+for (Map.Entry<Integer, QueryResult<KeyValueIterator<String, Long>>> entry
+ : result.getPartitionResults().entrySet()) {
+ QueryResult<KeyValueIterator<String, Long>> partitionResult =
entry.getValue();
+ if (partitionResult.isFailure()) {
+ System.out.println("Partition " + entry.getKey() + " failed: "
+ + partitionResult.getFailureReason() + " - " +
partitionResult.getFailureMessage());
+ continue;
+ }
+ try (KeyValueIterator<String, Long> iterator =
partitionResult.getResult()) {
+ while (iterator.hasNext()) {
+ KeyValue<String, Long> record = iterator.next();
+ System.out.println(record.key + ": " + record.value);
+ }
+ }
+}
+```
+
+A failed `QueryResult` carries one of the
[FailureReason](/{version}/javadoc/org/apache/kafka/streams/query/FailureReason.html)
values:
+
+| `FailureReason` | Meaning | Recommended action |
+| --- | --- | --- |
+| `UNKNOWN_QUERY_TYPE` | The store does not know how to execute this query. |
Verify the query is supported by that store type; for custom queries, contact
the store maintainer. |
+| `NOT_ACTIVE` | `requireActive()` was set, but the partition is a standby or
an active task that is not yet in the `RUNNING` state. | Retry later or query a
different replica. |
+| `NOT_UP_TO_BOUND` | The partition has not yet caught up to the requested
`PositionBound`. | Retry later or query a different replica. |
+| `NOT_PRESENT` | The requested partition is not present on this instance (for
example, it migrated during a rebalance). | Query a different replica. |
+| `DOES_NOT_EXIST` | The requested partition does not exist for this store. |
Correct the requested set of partitions. |
+| `STORE_EXCEPTION` | The store threw an exception while executing the query.
| Depending on the exception, retry this instance or a different one. |
+
+## Controlling consistency and availability
+
+A `StateQueryRequest` is immutable; each configuration method returns a new
request. Beyond `inStore(...).withQuery(...)`, the following options let you
trade off consistency, availability, and cost:
+
+ * `withPartitions(Set<Integer>)` / `withAllPartitions()`: run against a
specific set of partitions or against all locally available partitions (the
default). Partitions that are missing return `NOT_PRESENT`; partitions that do
not exist return `DOES_NOT_EXIST`.
+ * `requireActive()`: run only on active (leader) partitions. Non-active
partitions return `NOT_ACTIVE`. Use this when you need the most up-to-date data
and want to avoid reading from standby replicas.
+ * `withPositionBound(PositionBound)`: by default a request is
`PositionBound.unbounded()`. Use `PositionBound.at(position)` to require that
each queried partition has consumed up to a given `Position` before serving the
query; a partition that is behind returns `NOT_UP_TO_BOUND`. Combined with the
`Position` returned by `StateQueryResult#getPosition()`, this lets you
implement read-your-writes / monotonic reads: feed the position from one query
into the bound of the next so repeated queries never appear to move backwards
in time, while still allowing reads to be served from any replica that is
caught up.
Review Comment:
[content] This bullet needs the `requireActive()` interaction spelled out,
because it silently defeats the read-your-writes recipe described here.
`KafkaStreams#query` substitutes `PositionBound.unbounded()` for the request's
bound whenever `requireActive` is set (`KafkaStreams.java:2221-2224`), so a
request that combines the two ignores the bound entirely.
Suggested addition: "`withPositionBound(...)` is ignored when
`requireActive()` is also set — an active, running task is served without a
bound check." (This is what makes the example below at line 659 incorrect.)
##########
docs/streams/developer-guide/interactive-queries.md:
##########
@@ -499,6 +499,198 @@ At this point the full state of the application is
interactively queryable:
+# Interactive Queries v2 (IQv2) {#interactive-queries-v2}
+
+The sections above describe the original interactive queries API (informally,
"IQv1"), where you obtain a read-only store facade from
[KafkaStreams#store(...)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
and call methods such as `get(key)` or `range(from, to)` on it. Interactive
Queries v2 (IQv2), introduced in
[KIP-796](https://cwiki.apache.org/confluence/x/34xnCw), is an alternative,
more flexible API for the same purpose: querying the local state of a running
Kafka Streams application.
+
+Instead of a fixed store facade, IQv2 models every query as a first-class
[Query](/{version}/javadoc/org/apache/kafka/streams/query/Query.html) object
that you submit through a single
[KafkaStreams#query(StateQueryRequest)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
method. The response is a
[StateQueryResult](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryResult.html)
that contains a separate
[QueryResult](/{version}/javadoc/org/apache/kafka/streams/query/QueryResult.html)
for each partition that executed the query, along with metadata such as each
partition's
[Position](/{version}/javadoc/org/apache/kafka/streams/query/Position.html).
+
+IQv2 offers several advantages over the original API:
+
+ * A single, uniform entry point for every kind of query.
+ * Query-level extensibility: custom state stores can handle their own
`Query` types (the runtime forwards unknown queries straight through to the
underlying byte store), rather than requiring a custom `QueryableStoreType`
store facade as the original API does.
+ * Rich, per-partition results with typed failure reasons instead of
exceptions.
Review Comment:
[completeness] "typed failure reasons instead of exceptions" is right about
*per-partition* outcomes, but `query()` itself still throws, and the section
never says so. Per the `KafkaStreams#query` javadoc and its preconditions
(`KafkaStreams.java:2153-2180`):
- `UnknownStateStoreException` — the store name is not registered in the
topology
- `StreamsNotStartedException` — `start()` hasn't been called yet; retry
after starting
- `StreamsStoppedException` — the instance is in `PENDING_SHUTDOWN` /
`NOT_RUNNING` / `PENDING_ERROR` / `ERROR`; discover a new instance
Worth one line here (or in "Handling query results") so readers understand
the split: request-level problems raise exceptions, per-partition problems come
back as `FailureReason`s. Otherwise "instead of exceptions" reads as though
`query()` never throws.
##########
docs/streams/developer-guide/interactive-queries.md:
##########
@@ -499,6 +499,198 @@ At this point the full state of the application is
interactively queryable:
+# Interactive Queries v2 (IQv2) {#interactive-queries-v2}
+
+The sections above describe the original interactive queries API (informally,
"IQv1"), where you obtain a read-only store facade from
[KafkaStreams#store(...)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
and call methods such as `get(key)` or `range(from, to)` on it. Interactive
Queries v2 (IQv2), introduced in
[KIP-796](https://cwiki.apache.org/confluence/x/34xnCw), is an alternative,
more flexible API for the same purpose: querying the local state of a running
Kafka Streams application.
+
+Instead of a fixed store facade, IQv2 models every query as a first-class
[Query](/{version}/javadoc/org/apache/kafka/streams/query/Query.html) object
that you submit through a single
[KafkaStreams#query(StateQueryRequest)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
method. The response is a
[StateQueryResult](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryResult.html)
that contains a separate
[QueryResult](/{version}/javadoc/org/apache/kafka/streams/query/QueryResult.html)
for each partition that executed the query, along with metadata such as each
partition's
[Position](/{version}/javadoc/org/apache/kafka/streams/query/Position.html).
+
+IQv2 offers several advantages over the original API:
+
+ * A single, uniform entry point for every kind of query.
+ * Query-level extensibility: custom state stores can handle their own
`Query` types (the runtime forwards unknown queries straight through to the
underlying byte store), rather than requiring a custom `QueryableStoreType`
store facade as the original API does.
+ * Rich, per-partition results with typed failure reasons instead of
exceptions.
+ * Fine-grained consistency control through `Position` and `PositionBound`.
+ * The ability to target specific partitions, require active (leader) tasks,
override the isolation level, and collect execution information.
+
+Both APIs are fully supported and can be used side by side. Note that IQv2 is
marked as an evolving API, so it may change between releases, and global stores
are not yet supported by `query(...)` (see [Limitations](#limitations-of-iqv2)
below).
+
+## How Interactive Queries v2 works
+
+Issuing an IQv2 query follows four steps:
+
+ 1. **Build a query.** Create a `Query` object that describes what you want
to read, for example a
[KeyQuery](/{version}/javadoc/org/apache/kafka/streams/query/KeyQuery.html) for
a single-key lookup or a
[RangeQuery](/{version}/javadoc/org/apache/kafka/streams/query/RangeQuery.html)
for a scan.
+ 2. **Build a request.** Wrap the query in a
[StateQueryRequest](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryRequest.html)
that names the store to query:
`StateQueryRequest.inStore(storeName).withQuery(query)`. Optionally configure
the request (partitions, position bound, isolation level, and so on).
+ 3. **Execute the request.** Call `streams.query(request)`, which runs the
query against every locally available partition of the store (or just the
partitions you requested) and returns a `StateQueryResult`.
+ 4. **Read the results.** For a query that targets a single partition, use
`getOnlyPartitionResult()`. For a query that may span multiple partitions, use
`getPartitionResults()` to get a `Map` from partition number to `QueryResult`.
Each `QueryResult` reports whether the query succeeded on that partition and
holds either the result value or a typed failure reason.
+
+## Building and executing a query
+
+The following example looks up a single key in the `CountsKeyValueStore` state
store from the word-count example used earlier on this page:
+
+```java
+import org.apache.kafka.streams.KafkaStreams;
+import org.apache.kafka.streams.query.KeyQuery;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.StateQueryRequest;
+import org.apache.kafka.streams.query.StateQueryResult;
+
+import static org.apache.kafka.streams.query.StateQueryRequest.inStore;
+
+KafkaStreams streams = ...;
+
+// 1. Build the query: retrieve the value for a single key.
+KeyQuery<String, Long> query = KeyQuery.withKey("alice");
+
+// 2. Build the request, naming the store to run the query against.
+// A KeyQuery<String, Long> is a Query<Long>, so the request is a
StateQueryRequest<Long>.
+StateQueryRequest<Long> request =
inStore("CountsKeyValueStore").withQuery(query);
+
+// 3. Execute the query.
+StateQueryResult<Long> result = streams.query(request);
+
+// 4. Read the result. A given key lives in exactly one partition, so at most
one partition
+// returns a value. getOnlyPartitionResult() returns that partition's
result, or null if
+// no locally available partition holds the key.
+QueryResult<Long> partitionResult = result.getOnlyPartitionResult();
+if (partitionResult != null && partitionResult.isSuccess()) {
+ Long count = partitionResult.getResult();
+ System.out.println("Count for alice: " + count);
+}
+```
+
+Note that the type parameter of `StateQueryRequest` (and of `StateQueryResult`
and `QueryResult`) is the query's *result* type, not the query type: a
`KeyQuery<String, Long>` implements `Query<Long>`, so the request is a
`StateQueryRequest<Long>`.
+
+## Built-in query types
+
+Kafka Streams ships with a set of query types covering the standard store
types. All of them live in the
[org.apache.kafka.streams.query](/{version}/javadoc/org/apache/kafka/streams/query/package-summary.html)
package.
+
+| Query | Result type (`R`) | Key factory / builder methods |
+| --- | --- | --- |
+| `KeyQuery<K, V>` | `V` | `withKey(key)`; `.skipCache()` |
+| `TimestampedKeyQuery<K, V>` | `ValueAndTimestamp<V>` | `withKey(key)`;
`.skipCache()` |
+| `RangeQuery<K, V>` | `KeyValueIterator<K, V>` | `withRange(lower, upper)`,
`withLowerBound(lower)`, `withUpperBound(upper)`, `withNoBounds()`;
`.withAscendingKeys()` / `.withDescendingKeys()` |
+| `TimestampedRangeQuery<K, V>` | `KeyValueIterator<K, ValueAndTimestamp<V>>`
| `withRange(lower, upper)`, `withLowerBound(lower)`, `withUpperBound(upper)`,
`withNoBounds()`; `.withAscendingKeys()` / `.withDescendingKeys()` |
+| `WindowKeyQuery<K, V>` | `WindowStoreIterator<V>` |
`withKeyAndWindowStartRange(key, timeFrom, timeTo)` |
+| `WindowRangeQuery<K, V>` | `KeyValueIterator<Windowed<K>, V>` |
`withKey(key)`, `withWindowStartRange(timeFrom, timeTo)` |
Review Comment:
[content] Listing `withKey(key)` and `withWindowStartRange(timeFrom,
timeTo)` on one row implies both work against the same store. They don't — each
is accepted by a *different* store type, and the other fails:
- **Window stores** support only `withWindowStartRange`.
`MeteredWindowStore#runRangeQuery` returns `UNKNOWN_QUERY_TYPE` otherwise, with
the message `"WindowStores only supports
WindowRangeQuery.withWindowStartRange."`
- **Session stores** support only `withKey`. `MeteredSessionStore` registers
exactly one handler, for `WindowRangeQuery` (`MeteredSessionStore.java:86-92`),
and its `runRangeQuery` fails anything else with `"SessionStores only support
WindowRangeQuery.withKey."`
Suggest splitting into two rows (or adding a store column), e.g.
`WindowRangeQuery` / window store / `withWindowStartRange(timeFrom, timeTo)`
and `WindowRangeQuery` / session store / `withKey(key)`.
This is also the only place session stores could be mentioned — worth
stating explicitly that session stores *are* IQv2-queryable via
`WindowRangeQuery.withKey`, since a reader scanning this table would otherwise
conclude they aren't supported at all.
##########
docs/streams/developer-guide/interactive-queries.md:
##########
@@ -499,6 +499,198 @@ At this point the full state of the application is
interactively queryable:
+# Interactive Queries v2 (IQv2) {#interactive-queries-v2}
Review Comment:
[order] This new top-level `#` section is appended after `# Querying remote
state stores for the entire app` (line 377), but the section itself correctly
frames IQv2 as an alternative to the *local* querying API introduced in `#
Querying local state stores for an app instance` (line 126). Reading
top-to-bottom, a user meets remote querying and the RPC layer before learning
that IQv2 exists as a local alternative.
Consider moving this section to directly follow the local-querying section,
or adding a one-line forward pointer to it from there. The section is
self-contained, so this is a flow preference rather than a correctness issue.
(Carried forward from an earlier pending review, now line-anchored.)
##########
docs/streams/developer-guide/interactive-queries.md:
##########
@@ -499,6 +499,198 @@ At this point the full state of the application is
interactively queryable:
+# Interactive Queries v2 (IQv2) {#interactive-queries-v2}
+
+The sections above describe the original interactive queries API (informally,
"IQv1"), where you obtain a read-only store facade from
[KafkaStreams#store(...)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
and call methods such as `get(key)` or `range(from, to)` on it. Interactive
Queries v2 (IQv2), introduced in
[KIP-796](https://cwiki.apache.org/confluence/x/34xnCw), is an alternative,
more flexible API for the same purpose: querying the local state of a running
Kafka Streams application.
+
+Instead of a fixed store facade, IQv2 models every query as a first-class
[Query](/{version}/javadoc/org/apache/kafka/streams/query/Query.html) object
that you submit through a single
[KafkaStreams#query(StateQueryRequest)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
method. The response is a
[StateQueryResult](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryResult.html)
that contains a separate
[QueryResult](/{version}/javadoc/org/apache/kafka/streams/query/QueryResult.html)
for each partition that executed the query, along with metadata such as each
partition's
[Position](/{version}/javadoc/org/apache/kafka/streams/query/Position.html).
+
+IQv2 offers several advantages over the original API:
+
+ * A single, uniform entry point for every kind of query.
+ * Query-level extensibility: custom state stores can handle their own
`Query` types (the runtime forwards unknown queries straight through to the
underlying byte store), rather than requiring a custom `QueryableStoreType`
store facade as the original API does.
+ * Rich, per-partition results with typed failure reasons instead of
exceptions.
+ * Fine-grained consistency control through `Position` and `PositionBound`.
+ * The ability to target specific partitions, require active (leader) tasks,
override the isolation level, and collect execution information.
+
+Both APIs are fully supported and can be used side by side. Note that IQv2 is
marked as an evolving API, so it may change between releases, and global stores
are not yet supported by `query(...)` (see [Limitations](#limitations-of-iqv2)
below).
+
+## How Interactive Queries v2 works
+
+Issuing an IQv2 query follows four steps:
+
+ 1. **Build a query.** Create a `Query` object that describes what you want
to read, for example a
[KeyQuery](/{version}/javadoc/org/apache/kafka/streams/query/KeyQuery.html) for
a single-key lookup or a
[RangeQuery](/{version}/javadoc/org/apache/kafka/streams/query/RangeQuery.html)
for a scan.
+ 2. **Build a request.** Wrap the query in a
[StateQueryRequest](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryRequest.html)
that names the store to query:
`StateQueryRequest.inStore(storeName).withQuery(query)`. Optionally configure
the request (partitions, position bound, isolation level, and so on).
+ 3. **Execute the request.** Call `streams.query(request)`, which runs the
query against every locally available partition of the store (or just the
partitions you requested) and returns a `StateQueryResult`.
+ 4. **Read the results.** For a query that targets a single partition, use
`getOnlyPartitionResult()`. For a query that may span multiple partitions, use
`getPartitionResults()` to get a `Map` from partition number to `QueryResult`.
Each `QueryResult` reports whether the query succeeded on that partition and
holds either the result value or a typed failure reason.
+
+## Building and executing a query
+
+The following example looks up a single key in the `CountsKeyValueStore` state
store from the word-count example used earlier on this page:
+
+```java
+import org.apache.kafka.streams.KafkaStreams;
+import org.apache.kafka.streams.query.KeyQuery;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.StateQueryRequest;
+import org.apache.kafka.streams.query.StateQueryResult;
+
+import static org.apache.kafka.streams.query.StateQueryRequest.inStore;
+
+KafkaStreams streams = ...;
+
+// 1. Build the query: retrieve the value for a single key.
+KeyQuery<String, Long> query = KeyQuery.withKey("alice");
+
+// 2. Build the request, naming the store to run the query against.
+// A KeyQuery<String, Long> is a Query<Long>, so the request is a
StateQueryRequest<Long>.
+StateQueryRequest<Long> request =
inStore("CountsKeyValueStore").withQuery(query);
+
+// 3. Execute the query.
+StateQueryResult<Long> result = streams.query(request);
+
+// 4. Read the result. A given key lives in exactly one partition, so at most
one partition
+// returns a value. getOnlyPartitionResult() returns that partition's
result, or null if
+// no locally available partition holds the key.
+QueryResult<Long> partitionResult = result.getOnlyPartitionResult();
+if (partitionResult != null && partitionResult.isSuccess()) {
+ Long count = partitionResult.getResult();
+ System.out.println("Count for alice: " + count);
+}
+```
+
+Note that the type parameter of `StateQueryRequest` (and of `StateQueryResult`
and `QueryResult`) is the query's *result* type, not the query type: a
`KeyQuery<String, Long>` implements `Query<Long>`, so the request is a
`StateQueryRequest<Long>`.
+
+## Built-in query types
+
+Kafka Streams ships with a set of query types covering the standard store
types. All of them live in the
[org.apache.kafka.streams.query](/{version}/javadoc/org/apache/kafka/streams/query/package-summary.html)
package.
+
+| Query | Result type (`R`) | Key factory / builder methods |
+| --- | --- | --- |
+| `KeyQuery<K, V>` | `V` | `withKey(key)`; `.skipCache()` |
+| `TimestampedKeyQuery<K, V>` | `ValueAndTimestamp<V>` | `withKey(key)`;
`.skipCache()` |
+| `RangeQuery<K, V>` | `KeyValueIterator<K, V>` | `withRange(lower, upper)`,
`withLowerBound(lower)`, `withUpperBound(upper)`, `withNoBounds()`;
`.withAscendingKeys()` / `.withDescendingKeys()` |
+| `TimestampedRangeQuery<K, V>` | `KeyValueIterator<K, ValueAndTimestamp<V>>`
| `withRange(lower, upper)`, `withLowerBound(lower)`, `withUpperBound(upper)`,
`withNoBounds()`; `.withAscendingKeys()` / `.withDescendingKeys()` |
+| `WindowKeyQuery<K, V>` | `WindowStoreIterator<V>` |
`withKeyAndWindowStartRange(key, timeFrom, timeTo)` |
+| `WindowRangeQuery<K, V>` | `KeyValueIterator<Windowed<K>, V>` |
`withKey(key)`, `withWindowStartRange(timeFrom, timeTo)` |
+| `VersionedKeyQuery<K, V>` | `VersionedRecord<V>` | `withKey(key)`;
`.asOf(instant)` |
+| `MultiVersionedKeyQuery<K, V>` | `VersionedRecordIterator<V>` |
`withKey(key)`; `.fromTime(instant)`, `.toTime(instant)`,
`.withAscendingTimestamps()` / `.withDescendingTimestamps()` |
Review Comment:
[completeness] The table is introduced as the set of query types Kafka
Streams ships with, but four public query types in
`org.apache.kafka.streams.query` are missing — the KIP-1356 headers-aware
queries. All four are `@Evolving @InterfaceAudience.Public`, and all landed
before this PR was opened (2026-07-21):
| Query | Result type (`R`) | Commit |
| --- | --- | --- |
| `TimestampedKeyWithHeadersQuery<K, V>` | `ReadOnlyRecord<K, V>` |
`eb7096dcf6` (#22666) |
| `TimestampedRangeWithHeadersQuery<K, V>` | `ReadOnlyRecordIterator<K, V>`
| `a49d127f5b` (#22770) |
| `TimestampedWindowKeyWithHeadersQuery<K, V>` |
`ReadOnlyRecordIterator<Windowed<K>, V>` | `cacab1cc81` (#22799) |
| `TimestampedWindowRangeWithHeadersQuery<K, V>` |
`ReadOnlyRecordIterator<Windowed<K>, V>` | `8001408711` (#22853) |
Their factory methods mirror the non-headers variants (`withKey`,
`withRange` / `withLowerBound` / `withUpperBound` / `withNoBounds`,
`withKeyAndWindowStartRange`, `withWindowStartRange`). Since they require a
headers-aware store supplier, a short note to that effect would help. Worth
adding rows so the table doesn't go stale on arrival.
##########
docs/streams/developer-guide/interactive-queries.md:
##########
@@ -499,6 +499,198 @@ At this point the full state of the application is
interactively queryable:
+# Interactive Queries v2 (IQv2) {#interactive-queries-v2}
+
+The sections above describe the original interactive queries API (informally,
"IQv1"), where you obtain a read-only store facade from
[KafkaStreams#store(...)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
and call methods such as `get(key)` or `range(from, to)` on it. Interactive
Queries v2 (IQv2), introduced in
[KIP-796](https://cwiki.apache.org/confluence/x/34xnCw), is an alternative,
more flexible API for the same purpose: querying the local state of a running
Kafka Streams application.
+
+Instead of a fixed store facade, IQv2 models every query as a first-class
[Query](/{version}/javadoc/org/apache/kafka/streams/query/Query.html) object
that you submit through a single
[KafkaStreams#query(StateQueryRequest)](/{version}/javadoc/org/apache/kafka/streams/KafkaStreams.html)
method. The response is a
[StateQueryResult](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryResult.html)
that contains a separate
[QueryResult](/{version}/javadoc/org/apache/kafka/streams/query/QueryResult.html)
for each partition that executed the query, along with metadata such as each
partition's
[Position](/{version}/javadoc/org/apache/kafka/streams/query/Position.html).
+
+IQv2 offers several advantages over the original API:
+
+ * A single, uniform entry point for every kind of query.
+ * Query-level extensibility: custom state stores can handle their own
`Query` types (the runtime forwards unknown queries straight through to the
underlying byte store), rather than requiring a custom `QueryableStoreType`
store facade as the original API does.
+ * Rich, per-partition results with typed failure reasons instead of
exceptions.
+ * Fine-grained consistency control through `Position` and `PositionBound`.
+ * The ability to target specific partitions, require active (leader) tasks,
override the isolation level, and collect execution information.
+
+Both APIs are fully supported and can be used side by side. Note that IQv2 is
marked as an evolving API, so it may change between releases, and global stores
are not yet supported by `query(...)` (see [Limitations](#limitations-of-iqv2)
below).
+
+## How Interactive Queries v2 works
+
+Issuing an IQv2 query follows four steps:
+
+ 1. **Build a query.** Create a `Query` object that describes what you want
to read, for example a
[KeyQuery](/{version}/javadoc/org/apache/kafka/streams/query/KeyQuery.html) for
a single-key lookup or a
[RangeQuery](/{version}/javadoc/org/apache/kafka/streams/query/RangeQuery.html)
for a scan.
+ 2. **Build a request.** Wrap the query in a
[StateQueryRequest](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryRequest.html)
that names the store to query:
`StateQueryRequest.inStore(storeName).withQuery(query)`. Optionally configure
the request (partitions, position bound, isolation level, and so on).
+ 3. **Execute the request.** Call `streams.query(request)`, which runs the
query against every locally available partition of the store (or just the
partitions you requested) and returns a `StateQueryResult`.
+ 4. **Read the results.** For a query that targets a single partition, use
`getOnlyPartitionResult()`. For a query that may span multiple partitions, use
`getPartitionResults()` to get a `Map` from partition number to `QueryResult`.
Each `QueryResult` reports whether the query succeeded on that partition and
holds either the result value or a typed failure reason.
+
+## Building and executing a query
+
+The following example looks up a single key in the `CountsKeyValueStore` state
store from the word-count example used earlier on this page:
+
+```java
+import org.apache.kafka.streams.KafkaStreams;
+import org.apache.kafka.streams.query.KeyQuery;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.StateQueryRequest;
+import org.apache.kafka.streams.query.StateQueryResult;
+
+import static org.apache.kafka.streams.query.StateQueryRequest.inStore;
+
+KafkaStreams streams = ...;
+
+// 1. Build the query: retrieve the value for a single key.
+KeyQuery<String, Long> query = KeyQuery.withKey("alice");
+
+// 2. Build the request, naming the store to run the query against.
+// A KeyQuery<String, Long> is a Query<Long>, so the request is a
StateQueryRequest<Long>.
+StateQueryRequest<Long> request =
inStore("CountsKeyValueStore").withQuery(query);
+
+// 3. Execute the query.
+StateQueryResult<Long> result = streams.query(request);
+
+// 4. Read the result. A given key lives in exactly one partition, so at most
one partition
+// returns a value. getOnlyPartitionResult() returns that partition's
result, or null if
+// no locally available partition holds the key.
+QueryResult<Long> partitionResult = result.getOnlyPartitionResult();
+if (partitionResult != null && partitionResult.isSuccess()) {
+ Long count = partitionResult.getResult();
+ System.out.println("Count for alice: " + count);
+}
+```
+
+Note that the type parameter of `StateQueryRequest` (and of `StateQueryResult`
and `QueryResult`) is the query's *result* type, not the query type: a
`KeyQuery<String, Long>` implements `Query<Long>`, so the request is a
`StateQueryRequest<Long>`.
+
+## Built-in query types
+
+Kafka Streams ships with a set of query types covering the standard store
types. All of them live in the
[org.apache.kafka.streams.query](/{version}/javadoc/org/apache/kafka/streams/query/package-summary.html)
package.
+
+| Query | Result type (`R`) | Key factory / builder methods |
+| --- | --- | --- |
+| `KeyQuery<K, V>` | `V` | `withKey(key)`; `.skipCache()` |
+| `TimestampedKeyQuery<K, V>` | `ValueAndTimestamp<V>` | `withKey(key)`;
`.skipCache()` |
+| `RangeQuery<K, V>` | `KeyValueIterator<K, V>` | `withRange(lower, upper)`,
`withLowerBound(lower)`, `withUpperBound(upper)`, `withNoBounds()`;
`.withAscendingKeys()` / `.withDescendingKeys()` |
+| `TimestampedRangeQuery<K, V>` | `KeyValueIterator<K, ValueAndTimestamp<V>>`
| `withRange(lower, upper)`, `withLowerBound(lower)`, `withUpperBound(upper)`,
`withNoBounds()`; `.withAscendingKeys()` / `.withDescendingKeys()` |
+| `WindowKeyQuery<K, V>` | `WindowStoreIterator<V>` |
`withKeyAndWindowStartRange(key, timeFrom, timeTo)` |
+| `WindowRangeQuery<K, V>` | `KeyValueIterator<Windowed<K>, V>` |
`withKey(key)`, `withWindowStartRange(timeFrom, timeTo)` |
+| `VersionedKeyQuery<K, V>` | `VersionedRecord<V>` | `withKey(key)`;
`.asOf(instant)` |
+| `MultiVersionedKeyQuery<K, V>` | `VersionedRecordIterator<V>` |
`withKey(key)`; `.fromTime(instant)`, `.toTime(instant)`,
`.withAscendingTimestamps()` / `.withDescendingTimestamps()` |
+
+For range and scan queries (`RangeQuery`, `TimestampedRangeQuery`), passing no
bounds performs a full scan, and result ordering is based on the serialized
`byte[]` of the keys, not on the logical key order.
+
+Versioned key-value stores are queryable **only** through IQv2 — use
`VersionedKeyQuery` for a single version (latest, or as of a timestamp) and
`MultiVersionedKeyQuery` for a range of versions. The original
`KafkaStreams#store(...)` API has no queryable store type for versioned stores.
+
+Because IQv2 is extensible, a custom state store may implement additional
query types of its own. When a store does not know how to handle a query, it
does not throw; instead it returns a failed `QueryResult` with
`FailureReason.UNKNOWN_QUERY_TYPE`.
+
+## Handling query results
+
+A `StateQueryResult` aggregates one `QueryResult` per partition that ran the
query. By default a request runs against all locally available partitions of
the store (unless you narrow it with `withPartitions(...)`), so
`getPartitionResults()` may contain an entry for every one of those partitions.
+
+Which accessor to use is determined by the *query type* you chose, not by
inspecting the result at runtime:
+
+ * **Point lookups** (`KeyQuery`, `TimestampedKeyQuery`, `VersionedKeyQuery`)
can only match in the single partition that owns the key, so at most one
partition returns a value (the other queried partitions return a successful
result with `null`). Use `getOnlyPartitionResult()`.
+ * **Range, scan, and window queries** (`RangeQuery`, `WindowRangeQuery`,
`MultiVersionedKeyQuery`, and so on) can match records in every queried
partition, so results are spread across partitions. Use `getPartitionResults()`
and iterate.
+
+The accessors are:
+
+ * `getPartitionResults()` returns a `Map<Integer, QueryResult<R>>`, keyed by
partition number — one entry per partition that ran the query. This is the
general form and works for any query.
+ * `getOnlyPartitionResult()` is a convenience that filters to the single
partition that produced a value and returns it (or `null` if none did). It
throws `IllegalArgumentException` if more than one partition produced a value,
so only use it when you know the query matches at most one partition.
+ * `getPosition()` returns the merged `Position` observed across the
partition results.
+
+Each `QueryResult` reports the outcome for one partition. Use `isSuccess()` /
`isFailure()` before reading: `getResult()` returns the value on success (which
may itself be `null`, for example when a key is not found), while
`getFailureReason()` and `getFailureMessage()` describe a failure. Results are
always **per-partition** — a query may succeed on some partitions and fail on
others (for example, if one partition has migrated off this instance).
+
+When a query returns an iterator (`RangeQuery`, `WindowKeyQuery`,
`MultiVersionedKeyQuery`, and so on), the iterator must be closed after use.
Iterate over every partition's result and use a try-with-resources block:
+
+```java
+import org.apache.kafka.streams.KeyValue;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.RangeQuery;
+import org.apache.kafka.streams.query.StateQueryRequest;
+import org.apache.kafka.streams.query.StateQueryResult;
+import org.apache.kafka.streams.state.KeyValueIterator;
+
+import java.util.Map;
+
+import static org.apache.kafka.streams.query.StateQueryRequest.inStore;
+
+RangeQuery<String, Long> query = RangeQuery.withRange("a", "n");
+StateQueryRequest<KeyValueIterator<String, Long>> request =
+ inStore("CountsKeyValueStore").withQuery(query);
+StateQueryResult<KeyValueIterator<String, Long>> result =
streams.query(request);
+
+for (Map.Entry<Integer, QueryResult<KeyValueIterator<String, Long>>> entry
+ : result.getPartitionResults().entrySet()) {
+ QueryResult<KeyValueIterator<String, Long>> partitionResult =
entry.getValue();
+ if (partitionResult.isFailure()) {
+ System.out.println("Partition " + entry.getKey() + " failed: "
+ + partitionResult.getFailureReason() + " - " +
partitionResult.getFailureMessage());
+ continue;
+ }
+ try (KeyValueIterator<String, Long> iterator =
partitionResult.getResult()) {
+ while (iterator.hasNext()) {
+ KeyValue<String, Long> record = iterator.next();
+ System.out.println(record.key + ": " + record.value);
+ }
+ }
+}
+```
+
+A failed `QueryResult` carries one of the
[FailureReason](/{version}/javadoc/org/apache/kafka/streams/query/FailureReason.html)
values:
+
+| `FailureReason` | Meaning | Recommended action |
+| --- | --- | --- |
+| `UNKNOWN_QUERY_TYPE` | The store does not know how to execute this query. |
Verify the query is supported by that store type; for custom queries, contact
the store maintainer. |
+| `NOT_ACTIVE` | `requireActive()` was set, but the partition is a standby or
an active task that is not yet in the `RUNNING` state. | Retry later or query a
different replica. |
+| `NOT_UP_TO_BOUND` | The partition has not yet caught up to the requested
`PositionBound`. | Retry later or query a different replica. |
+| `NOT_PRESENT` | The requested partition is not present on this instance (for
example, it migrated during a rebalance). | Query a different replica. |
+| `DOES_NOT_EXIST` | The requested partition does not exist for this store. |
Correct the requested set of partitions. |
+| `STORE_EXCEPTION` | The store threw an exception while executing the query.
| Depending on the exception, retry this instance or a different one. |
+
+## Controlling consistency and availability
+
+A `StateQueryRequest` is immutable; each configuration method returns a new
request. Beyond `inStore(...).withQuery(...)`, the following options let you
trade off consistency, availability, and cost:
+
+ * `withPartitions(Set<Integer>)` / `withAllPartitions()`: run against a
specific set of partitions or against all locally available partitions (the
default). Partitions that are missing return `NOT_PRESENT`; partitions that do
not exist return `DOES_NOT_EXIST`.
Review Comment:
[content] "partitions that do not exist return `DOES_NOT_EXIST`" is not what
the runtime does. `FailureReason.DOES_NOT_EXIST` is described in the
`StateQueryRequest#withPartitions` javadoc (`StateQueryRequest.java:103-105`),
but no production code ever constructs it — `grep -rn DOES_NOT_EXIST
streams/src/main/java` only hits the enum declaration and that javadoc, and the
sole other occurrence in the repo is a unit test
(`StateQueryResultTest.java:36`).
What actually happens: after the task scan, `KafkaStreams#query` fills in
*every* requested-but-unresolved partition with `NOT_PRESENT`
(`KafkaStreams.java:2245-2253`), whether the partition merely migrated away or
does not exist for the store at all.
Same issue for the `DOES_NOT_EXIST` row of the `FailureReason` table at line
646. Suggest either dropping the claim that invalid partitions produce
`DOES_NOT_EXIST` (say `NOT_PRESENT` is returned for both cases) or noting that
`DOES_NOT_EXIST` is defined for store implementations but is not currently
emitted by the Streams runtime.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]