aliehsaeedii commented on code in PR #22882:
URL: https://github.com/apache/kafka/pull/22882#discussion_r3778759561
##########
docs/streams/developer-guide/interactive-queries.md:
##########
@@ -257,6 +257,237 @@ After the application has started, you can get access to
"CountsWindowStore" and
System.out.println("Count of 'world' @ time " + windowTimestamp + " is "
+ next.value);
}
+## Header-aware stores and interactive queries
{#header-aware-stores-interactive-queries}
+
+A [header-aware
store](/{version}/streams/developer-guide/processor-api/#headers-in-state-stores)
— built with a `*WithHeaders` supplier and its matching builder
([KIP-1271](../../upgrade-guide/#kip-1271-headers-aware-stores)) — persists
each record's
[headers](</{version}/javadoc/org/apache/kafka/streams/processor/api/Record.html#headers()>)
alongside its value (and, for key-value and window stores, its timestamp).
This section shows how to read those headers back interactively, through both
the `store()` API and the IQv2 `query()` API.
+
+### Reading headers with the `store()` API
+
+Look up the store with the `*WithHeaders()` entry from `QueryableStoreTypes`
that matches your store type. The returned `ReadOnly*Store` surfaces the
headers as part of its value type:
[ValueTimestampHeaders](/{version}/javadoc/org/apache/kafka/streams/state/ValueTimestampHeaders.html)
for key-value and window stores, and
[AggregationWithHeaders](/{version}/javadoc/org/apache/kafka/streams/state/AggregationWithHeaders.html)
for session stores. These examples assume a header-aware store built with a
`*WithHeaders` supplier, as shown in [Headers in State
Stores](/{version}/streams/developer-guide/processor-api/#headers-in-state-stores).
There are only three such helpers — `timestampedKeyValueStoreWithHeaders()`,
`timestampedWindowStoreWithHeaders()`, and `sessionStoreWithHeaders()`; there
is no `*WithHeaders()` helper for a plain (non-timestamped) key-value or window
store. What the store returns also depends on the supplier the `*WithHeaders`
builder wraps (see the store-build tabl
e below): on the adapter paths the `store()` API degrades silently — empty
headers, or a `-1` timestamp — rather than failing the way the IQv2 `query()`
API does.
Review Comment:
[content] "rather than failing the way the IQv2 `query()` API does" is too
broad. IQv2 only fails on the *plain* adapter; on the timestamped adapter it
succeeds and just returns empty headers — which is what your own table row says
("All succeed", line 458). Narrow this to the plain path.
##########
docs/streams/developer-guide/interactive-queries.md:
##########
@@ -257,6 +257,237 @@ After the application has started, you can get access to
"CountsWindowStore" and
System.out.println("Count of 'world' @ time " + windowTimestamp + " is "
+ next.value);
}
+## Header-aware stores and interactive queries
{#header-aware-stores-interactive-queries}
+
+A [header-aware
store](/{version}/streams/developer-guide/processor-api/#headers-in-state-stores)
— built with a `*WithHeaders` supplier and its matching builder
([KIP-1271](../../upgrade-guide/#kip-1271-headers-aware-stores)) — persists
each record's
[headers](</{version}/javadoc/org/apache/kafka/streams/processor/api/Record.html#headers()>)
alongside its value (and, for key-value and window stores, its timestamp).
This section shows how to read those headers back interactively, through both
the `store()` API and the IQv2 `query()` API.
+
+### Reading headers with the `store()` API
+
+Look up the store with the `*WithHeaders()` entry from `QueryableStoreTypes`
that matches your store type. The returned `ReadOnly*Store` surfaces the
headers as part of its value type:
[ValueTimestampHeaders](/{version}/javadoc/org/apache/kafka/streams/state/ValueTimestampHeaders.html)
for key-value and window stores, and
[AggregationWithHeaders](/{version}/javadoc/org/apache/kafka/streams/state/AggregationWithHeaders.html)
for session stores. These examples assume a header-aware store built with a
`*WithHeaders` supplier, as shown in [Headers in State
Stores](/{version}/streams/developer-guide/processor-api/#headers-in-state-stores).
There are only three such helpers — `timestampedKeyValueStoreWithHeaders()`,
`timestampedWindowStoreWithHeaders()`, and `sessionStoreWithHeaders()`; there
is no `*WithHeaders()` helper for a plain (non-timestamped) key-value or window
store. What the store returns also depends on the supplier the `*WithHeaders`
builder wraps (see the store-build tabl
e below): on the adapter paths the `store()` API degrades silently — empty
headers, or a `-1` timestamp — rather than failing the way the IQv2 `query()`
API does.
+
+
+ // Key-value store built with a *WithHeaders supplier
+ ReadOnlyKeyValueStore<String, ValueTimestampHeaders<Long>> keyValueStore =
+ streams.store(StoreQueryParameters.fromNameAndType(
+ "counts-store",
QueryableStoreTypes.timestampedKeyValueStoreWithHeaders()));
+
+ ValueTimestampHeaders<Long> vth = keyValueStore.get("hello");
+ if (vth != null) {
+ System.out.println("value: " + vth.value());
+ System.out.println("timestamp: " + vth.timestamp());
+ System.out.println("headers: " + vth.headers());
+ }
+
+ // Window store built with a *WithHeaders supplier
+ ReadOnlyWindowStore<String, ValueTimestampHeaders<Long>> windowStore =
+ streams.store(StoreQueryParameters.fromNameAndType(
+ "counts-window-store",
QueryableStoreTypes.timestampedWindowStoreWithHeaders()));
+
+ // fetch returns a WindowStoreIterator whose values carry headers
+ try (WindowStoreIterator<ValueTimestampHeaders<Long>> it =
+ windowStore.fetch("hello", Instant.ofEpochMilli(0),
Instant.now())) {
+ while (it.hasNext()) {
+ ValueTimestampHeaders<Long> wv = it.next().value;
+ System.out.println("value: " + wv.value() + " headers: " +
wv.headers());
+ }
+ }
+
+Session stores return `AggregationWithHeaders<V>`, which exposes the
aggregated value via `aggregation()` (not `value()`) and the headers via
`headers()`.
+
+
+ // Session store built with a *WithHeaders supplier
+ ReadOnlySessionStore<String, AggregationWithHeaders<Long>> sessionStore =
+ streams.store(StoreQueryParameters.fromNameAndType(
+ "counts-session-store",
QueryableStoreTypes.sessionStoreWithHeaders()));
+
+ try (KeyValueIterator<Windowed<String>, AggregationWithHeaders<Long>> it =
+ sessionStore.fetch("hello")) {
+ while (it.hasNext()) {
+ AggregationWithHeaders<Long> awh = it.next().value;
+ System.out.println("aggregation: " + awh.aggregation());
+ System.out.println("headers: " + awh.headers());
+ }
+ }
+
+### Reading headers with the IQv2 `query()` API
+
+Interactive Queries v2 (IQv2) is the query-based interactive-queries API:
instead of accessing a store object directly, you build a `Query`, wrap it in a
[StateQueryRequest](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryRequest.html),
and run it with `KafkaStreams#query(...)`. The call returns a
[StateQueryResult](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryResult.html)
that holds a per-partition
[QueryResult](/{version}/javadoc/org/apache/kafka/streams/query/QueryResult.html):
use `getOnlyPartitionResult()` for a single-key lookup, or
`getPartitionResults()` for the full `Map<Integer, QueryResult<R>>`. Each
`QueryResult` exposes the query result via `getResult()` and its data-freshness
`Position` via `getPosition()`. To require a minimum freshness on the request
side, bound it with `StateQueryRequest#withPositionBound(...)`; a
not-up-to-bound failure (described in the behavior notes below) means the store
had not yet reached that bound.
+
+Before [KIP-1356](../../upgrade-guide/#kip-1356-iqv2-header-queries), no IQv2
query type exposed record headers.
[KIP-1356](../../upgrade-guide/#kip-1356-iqv2-header-queries) adds four
`@Evolving` query types whose results carry headers. Each returns a
[ReadOnlyRecord](/{version}/javadoc/org/apache/kafka/streams/processor/api/ReadOnlyRecord.html)
— a read-only view exposing `key()`, `value()`, `timestamp()`, and `headers()`
— or, for the range and window queries, a closeable
[ReadOnlyRecordIterator](/{version}/javadoc/org/apache/kafka/streams/state/ReadOnlyRecordIterator.html)
of such records. `headers()` is never null (an empty `Headers` when the record
had none) and must be treated as read-only: records served as IQv2 results have
their headers frozen, so adding or removing a header (for example `add(...)`)
throws `IllegalStateException`. The freeze is shallow, though — the byte array
behind an individual header value can still be mutated in place, so treat
header values as
read-only too.
+
+`TimestampedKeyWithHeadersQuery` is a single-key lookup against a header-aware
key-value store, parallel to `TimestampedKeyQuery`:
+
+
+ TimestampedKeyWithHeadersQuery<String, Long> query =
+ TimestampedKeyWithHeadersQuery.withKey("hello");
+
+ StateQueryRequest<ReadOnlyRecord<String, Long>> request =
+ StateQueryRequest.inStore("counts-store").withQuery(query);
+
+ StateQueryResult<ReadOnlyRecord<String, Long>> result =
streams.query(request);
+ QueryResult<ReadOnlyRecord<String, Long>> partitionResult =
result.getOnlyPartitionResult();
+ if (partitionResult != null && partitionResult.isSuccess()) {
+ ReadOnlyRecord<String, Long> record = partitionResult.getResult();
+ if (record != null) {
+ System.out.println("value: " + record.value());
+ System.out.println("headers: " + record.headers());
+ }
+ }
+
+Chain `skipCache()` when building the query —
`TimestampedKeyWithHeadersQuery.withKey("hello").skipCache()` — to bypass the
record cache and read directly from the underlying store. The query types are
immutable, so `skipCache()` returns a new query rather than mutating the one
you already built (of the four header-aware queries, only this single-key one
offers `skipCache()`).
Review Comment:
[content] This chained form doesn't compile. `withKey("hello")` sits in
receiver position, so it isn't target-typed and `V` infers to `Object` —
`TimestampedKeyWithHeadersQuery<String,Object>` won't assign to `<String,
Long>`. It needs an explicit type witness:
`TimestampedKeyWithHeadersQuery.<String, Long>withKey("hello").skipCache()`,
which is exactly what Kafka's own test writes
(`TimestampedKeyValueStoreBuilderWithHeadersTest.java:402`). The unchained form
at line 321 is fine because the assignment target-types it. This one's on me —
the chained wording was my suggestion last round.
##########
docs/streams/developer-guide/interactive-queries.md:
##########
@@ -257,6 +257,237 @@ After the application has started, you can get access to
"CountsWindowStore" and
System.out.println("Count of 'world' @ time " + windowTimestamp + " is "
+ next.value);
}
+## Header-aware stores and interactive queries
{#header-aware-stores-interactive-queries}
+
+A [header-aware
store](/{version}/streams/developer-guide/processor-api/#headers-in-state-stores)
— built with a `*WithHeaders` supplier and its matching builder
([KIP-1271](../../upgrade-guide/#kip-1271-headers-aware-stores)) — persists
each record's
[headers](</{version}/javadoc/org/apache/kafka/streams/processor/api/Record.html#headers()>)
alongside its value (and, for key-value and window stores, its timestamp).
This section shows how to read those headers back interactively, through both
the `store()` API and the IQv2 `query()` API.
+
+### Reading headers with the `store()` API
+
+Look up the store with the `*WithHeaders()` entry from `QueryableStoreTypes`
that matches your store type. The returned `ReadOnly*Store` surfaces the
headers as part of its value type:
[ValueTimestampHeaders](/{version}/javadoc/org/apache/kafka/streams/state/ValueTimestampHeaders.html)
for key-value and window stores, and
[AggregationWithHeaders](/{version}/javadoc/org/apache/kafka/streams/state/AggregationWithHeaders.html)
for session stores. These examples assume a header-aware store built with a
`*WithHeaders` supplier, as shown in [Headers in State
Stores](/{version}/streams/developer-guide/processor-api/#headers-in-state-stores).
There are only three such helpers — `timestampedKeyValueStoreWithHeaders()`,
`timestampedWindowStoreWithHeaders()`, and `sessionStoreWithHeaders()`; there
is no `*WithHeaders()` helper for a plain (non-timestamped) key-value or window
store. What the store returns also depends on the supplier the `*WithHeaders`
builder wraps (see the store-build tabl
e below): on the adapter paths the `store()` API degrades silently — empty
headers, or a `-1` timestamp — rather than failing the way the IQv2 `query()`
API does.
+
+
+ // Key-value store built with a *WithHeaders supplier
+ ReadOnlyKeyValueStore<String, ValueTimestampHeaders<Long>> keyValueStore =
+ streams.store(StoreQueryParameters.fromNameAndType(
+ "counts-store",
QueryableStoreTypes.timestampedKeyValueStoreWithHeaders()));
+
+ ValueTimestampHeaders<Long> vth = keyValueStore.get("hello");
+ if (vth != null) {
+ System.out.println("value: " + vth.value());
+ System.out.println("timestamp: " + vth.timestamp());
+ System.out.println("headers: " + vth.headers());
+ }
+
+ // Window store built with a *WithHeaders supplier
+ ReadOnlyWindowStore<String, ValueTimestampHeaders<Long>> windowStore =
+ streams.store(StoreQueryParameters.fromNameAndType(
+ "counts-window-store",
QueryableStoreTypes.timestampedWindowStoreWithHeaders()));
+
+ // fetch returns a WindowStoreIterator whose values carry headers
+ try (WindowStoreIterator<ValueTimestampHeaders<Long>> it =
+ windowStore.fetch("hello", Instant.ofEpochMilli(0),
Instant.now())) {
+ while (it.hasNext()) {
+ ValueTimestampHeaders<Long> wv = it.next().value;
+ System.out.println("value: " + wv.value() + " headers: " +
wv.headers());
+ }
+ }
+
+Session stores return `AggregationWithHeaders<V>`, which exposes the
aggregated value via `aggregation()` (not `value()`) and the headers via
`headers()`.
+
+
+ // Session store built with a *WithHeaders supplier
+ ReadOnlySessionStore<String, AggregationWithHeaders<Long>> sessionStore =
+ streams.store(StoreQueryParameters.fromNameAndType(
+ "counts-session-store",
QueryableStoreTypes.sessionStoreWithHeaders()));
+
+ try (KeyValueIterator<Windowed<String>, AggregationWithHeaders<Long>> it =
+ sessionStore.fetch("hello")) {
+ while (it.hasNext()) {
+ AggregationWithHeaders<Long> awh = it.next().value;
+ System.out.println("aggregation: " + awh.aggregation());
+ System.out.println("headers: " + awh.headers());
+ }
+ }
+
+### Reading headers with the IQv2 `query()` API
+
+Interactive Queries v2 (IQv2) is the query-based interactive-queries API:
instead of accessing a store object directly, you build a `Query`, wrap it in a
[StateQueryRequest](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryRequest.html),
and run it with `KafkaStreams#query(...)`. The call returns a
[StateQueryResult](/{version}/javadoc/org/apache/kafka/streams/query/StateQueryResult.html)
that holds a per-partition
[QueryResult](/{version}/javadoc/org/apache/kafka/streams/query/QueryResult.html):
use `getOnlyPartitionResult()` for a single-key lookup, or
`getPartitionResults()` for the full `Map<Integer, QueryResult<R>>`. Each
`QueryResult` exposes the query result via `getResult()` and its data-freshness
`Position` via `getPosition()`. To require a minimum freshness on the request
side, bound it with `StateQueryRequest#withPositionBound(...)`; a
not-up-to-bound failure (described in the behavior notes below) means the store
had not yet reached that bound.
+
+Before [KIP-1356](../../upgrade-guide/#kip-1356-iqv2-header-queries), no IQv2
query type exposed record headers.
[KIP-1356](../../upgrade-guide/#kip-1356-iqv2-header-queries) adds four
`@Evolving` query types whose results carry headers. Each returns a
[ReadOnlyRecord](/{version}/javadoc/org/apache/kafka/streams/processor/api/ReadOnlyRecord.html)
— a read-only view exposing `key()`, `value()`, `timestamp()`, and `headers()`
— or, for the range and window queries, a closeable
[ReadOnlyRecordIterator](/{version}/javadoc/org/apache/kafka/streams/state/ReadOnlyRecordIterator.html)
of such records. `headers()` is never null (an empty `Headers` when the record
had none) and must be treated as read-only: records served as IQv2 results have
their headers frozen, so adding or removing a header (for example `add(...)`)
throws `IllegalStateException`. The freeze is shallow, though — the byte array
behind an individual header value can still be mutated in place, so treat
header values as
read-only too.
+
+`TimestampedKeyWithHeadersQuery` is a single-key lookup against a header-aware
key-value store, parallel to `TimestampedKeyQuery`:
+
+
+ TimestampedKeyWithHeadersQuery<String, Long> query =
+ TimestampedKeyWithHeadersQuery.withKey("hello");
+
+ StateQueryRequest<ReadOnlyRecord<String, Long>> request =
+ StateQueryRequest.inStore("counts-store").withQuery(query);
+
+ StateQueryResult<ReadOnlyRecord<String, Long>> result =
streams.query(request);
+ QueryResult<ReadOnlyRecord<String, Long>> partitionResult =
result.getOnlyPartitionResult();
+ if (partitionResult != null && partitionResult.isSuccess()) {
+ ReadOnlyRecord<String, Long> record = partitionResult.getResult();
+ if (record != null) {
+ System.out.println("value: " + record.value());
+ System.out.println("headers: " + record.headers());
+ }
+ }
+
+Chain `skipCache()` when building the query —
`TimestampedKeyWithHeadersQuery.withKey("hello").skipCache()` — to bypass the
record cache and read directly from the underlying store. The query types are
immutable, so `skipCache()` returns a new query rather than mutating the one
you already built (of the four header-aware queries, only this single-key one
offers `skipCache()`).
+
+`TimestampedRangeWithHeadersQuery` is a key-range scan, parallel to
`TimestampedRangeQuery`. It returns a `ReadOnlyRecordIterator`, so close it
when done (for example, with try-with-resources). A range can span several
local partitions, so iterate `getPartitionResults()`:
+
+
+ TimestampedRangeWithHeadersQuery<String, Long> query =
+ TimestampedRangeWithHeadersQuery.withRange("a", "n");
+
+ StateQueryRequest<ReadOnlyRecordIterator<String, Long>> request =
+ StateQueryRequest.inStore("counts-store").withQuery(query);
+
+ StateQueryResult<ReadOnlyRecordIterator<String, Long>> result =
streams.query(request);
+ for (QueryResult<ReadOnlyRecordIterator<String, Long>> partition :
result.getPartitionResults().values()) {
+ if (partition.isFailure()) {
+ System.out.println("failed: " + partition.getFailureReason() + " - " +
partition.getFailureMessage());
+ continue;
+ }
+ try (ReadOnlyRecordIterator<String, Long> iterator =
partition.getResult()) {
+ while (iterator.hasNext()) {
+ ReadOnlyRecord<String, Long> record = iterator.next();
+ System.out.println(record.key() + " -> " + record.value() + " " +
record.headers());
+ }
+ }
+ }
+
+Use `withLowerBound`, `withUpperBound`, or `withNoBounds` for open-ended or
full scans. Results are unordered by default; call `withAscendingKeys()` or
`withDescendingKeys()` to fix the order, which is defined over the serialized
`byte[]` of the keys rather than their logical order.
+
+`TimestampedWindowKeyWithHeadersQuery` fetches all windows for a single key
within a window-start range from a header-aware window store. It parallels
`WindowKeyQuery`, but with a different result shape: `WindowKeyQuery` returns a
`WindowStoreIterator<V>` keyed by the window-start `long`, whereas this query
returns a `ReadOnlyRecordIterator<Windowed<K>, V>` whose records are keyed by
`Windowed<K>` (the window lives in the key; `timestamp()` is the stored record
event-time). Build and consume it as for the range query above, but note the
`Windowed<String>` in the request and result types:
+
+
+ TimestampedWindowKeyWithHeadersQuery<String, Long> query =
+ TimestampedWindowKeyWithHeadersQuery.withKeyAndWindowStartRange(
+ "hello", Instant.ofEpochMilli(0), Instant.now());
+
+ StateQueryRequest<ReadOnlyRecordIterator<Windowed<String>, Long>> request =
+ StateQueryRequest.inStore("counts-window-store").withQuery(query);
+
+ StateQueryResult<ReadOnlyRecordIterator<Windowed<String>, Long>> result =
streams.query(request);
+ // Iterate result.getPartitionResults() and close each
ReadOnlyRecordIterator, as in the range example.
+
+`TimestampedWindowRangeWithHeadersQuery` is parallel to `WindowRangeQuery` and
has two forms. Use `withWindowStartRange(timeFrom, timeTo)` against a
header-aware window store to fetch every key across a window-start range, or
`withKey(key)` against a header-aware session store to fetch all sessions for a
key (for session results, `timestamp()` is the session-window end). As with
`WindowRangeQuery`, each store accepts only its corresponding form; submitting
the wrong form fails with an unknown-query-type error. Both forms are
`Query<ReadOnlyRecordIterator<Windowed<K>, V>>` — including the session
`withKey` form, whose records are keyed by the session's `Windowed<K>`.
+
+
+ // Window store: every key across a window-start range
+ TimestampedWindowRangeWithHeadersQuery<String, Long> byWindow =
+ TimestampedWindowRangeWithHeadersQuery.withWindowStartRange(
+ Instant.ofEpochMilli(0), Instant.now());
+
+ // Session store: all sessions for one key
+ TimestampedWindowRangeWithHeadersQuery<String, Long> byKey =
+ TimestampedWindowRangeWithHeadersQuery.withKey("hello");
+
+ // Both forms have the same result type (element type
ReadOnlyRecord<Windowed<String>, Long>),
+ // but each must target its own store type — submitting the wrong form
fails with an unknown-query-type error:
+ StateQueryRequest<ReadOnlyRecordIterator<Windowed<String>, Long>>
windowRequest =
+ StateQueryRequest.inStore("counts-window-store").withQuery(byWindow);
+ StateQueryRequest<ReadOnlyRecordIterator<Windowed<String>, Long>>
sessionRequest =
+ StateQueryRequest.inStore("counts-session-store").withQuery(byKey);
+
+ StateQueryResult<ReadOnlyRecordIterator<Windowed<String>, Long>> result =
streams.query(windowRequest);
+ // Iterate result.getPartitionResults() and close each
ReadOnlyRecordIterator, as in the range example.
+
+**Behavior notes**
+
+ * **Window start range is required.** As with the existing window queries,
`TimestampedWindowKeyWithHeadersQuery` and the `withWindowStartRange` form of
`TimestampedWindowRangeWithHeadersQuery` require a closed window-start range —
both `timeFrom` and `timeTo` must be present, and both bounds are inclusive.
+ * **Close iterators exactly once.** The range and window queries return a
`ReadOnlyRecordIterator`; close it when you are done — always, even if a
`next()` call throws partway through — or the underlying store iterator (and
the store's `num-open-iterators` metric) leaks. A try-with-resources block does
this correctly. The iterator does not support `remove()`.
+ * **Read-your-writes applies only to the single-key query.** Only
`TimestampedKeyWithHeadersQuery` reads through the record cache, so it sees a
write that has not yet been flushed to the store — unless you call
`skipCache()`, or the entry has already been flushed. The range, window, and
session queries bypass the cache entirely, so a not-yet-flushed write is
invisible to them and, with a position bound, fails with a not-up-to-bound
error.
+
+**How the store was built determines what the queries return.** For key-value
and window stores, the outcome depends on the supplier the `*WithHeaders`
builder wraps:
+
+<table>
+<tr>
+<th>
+
+`*WithHeaders` store built over…
+</th>
+<th>
+
+Headers
+</th>
+<th>
+
+Query outcome
+</th> </tr>
+<tr>
+<td>
+
+Native (RocksDB) header supplier
+</td>
+<td>
+
+Returned
+</td>
+<td>
+
+All succeed
+</td> </tr>
+<tr>
+<td>
+
+In-memory non-header supplier
+</td>
+<td>
+
+Returned (a marker keeps the header-format bytes verbatim)
+</td>
+<td>
+
+All succeed
+</td> </tr>
+<tr>
+<td>
+
+Persistent *timestamped* non-header supplier
+</td>
+<td>
+
+Empty
Review Comment:
[content] "Empty" needs the same cache-served caveat you gave the plain row.
On a timestamped-adapter build a warm `TimestampedKeyWithHeadersQuery` returns
the written headers — the record cache holds the full serialized bytes and sits
above the adapter — and they only go empty once flushed or with `skipCache()`.
`TimestampedKeyValueStoreBuilderWithHeadersTest.java:393` asserts exactly that:
`expectedHeaders = cachingEnabled ? headers : new RecordHeaders()`.
--
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]