Skip to content

KAFKA-20787: Implement KIP-1340: Expose Both Mapped Key and Stream Key in Streams-GlobalKTable Joins#22807

Open
lucliu1108 wants to merge 6 commits into
apache:trunkfrom
lucliu1108:KAFKA-20787
Open

KAFKA-20787: Implement KIP-1340: Expose Both Mapped Key and Stream Key in Streams-GlobalKTable Joins#22807
lucliu1108 wants to merge 6 commits into
apache:trunkfrom
lucliu1108:KAFKA-20787

Conversation

@lucliu1108

@lucliu1108 lucliu1108 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR implements KIP-1340. Prior to this kIP, ValueJoinerWithKey-based stream-globalKTable join pass the KStream record key as the readOnlyKey, so users had no way to access the join key produced by their KeyValueMapper inside the joiner without recomputing it.

This change adds a new joiner interface and new overrides of existing join/leftJoin methods to allow passing in both the mappedKey and the streamKey.

New Public API

ValueJoinerWithMappedAndStreamKey<K1, K2, V1, V2, VR>

A new functional interface with:

VR apply(K1 mappedKey, K2 streamKey, V1 value1, V2 value2);
- mappedKeythe join key produced by the KeyValueMapper (i.e. the
GlobalKTable lookup key); may be null for a left-join when the
mapper returns null.
- streamKeythe KStream record's key.
- Both keys are read-only.

4 new KStream join/leftJoin overloads

They accept the new ValueJoinerWithMappedAndStreamKey joiner instead of ValueJoinerWithKey:

  • join(GlobalKTable, KeyValueMapper, ValueJoinerWithMappedAndStreamKey)
  • join(GlobalKTable, KeyValueMapper, ValueJoinerWithMappedAndStreamKey, Named)
  • leftJoin(GlobalKTable, KeyValueMapper, ValueJoinerWithMappedAndStreamKey)
  • leftJoin(GlobalKTable, KeyValueMapper, ValueJoinerWithMappedAndStreamKey, Named)

Deprecations

The 4 existing ValueJoinerWithKey-based join/leftjoin for stream-globalKtable are marked as @Deprecated.

Reviewers: Matthias J. Sax matthias@confluent.io

@github-actions github-actions Bot added triage PRs from the community streams labels Jul 10, 2026
@lucliu1108 lucliu1108 marked this pull request as ready for review July 10, 2026 20:19
@mjsax mjsax added kip Requires or implements a KIP ci-approved and removed triage PRs from the community labels Jul 11, 2026

@mjsax mjsax left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made an initial pass -- did not look into test code yet.

private final ValueJoinerWithKey<? super K1, ? super V1, ? super V2, ? extends VOut> joiner;
private final KeyValueMapper<? super K1, ? super V1, ? extends K2> mapper;
private final KTableValueGetterSupplier<TableKey, TableValue> valueGetterSupplier;
private final ValueJoinerWithMappedAndStreamKey<? super TableKey, ? super StreamKey, ? super StreamValue, ? super TableValue, ? extends VOut> joiner;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am just wondering about the order or generic types? As it's a "stream-globalTable" join, it's a little bit odd that we start with TableKey?

I think, we have two options?

  • first steam types, second table types: <StreamKey, StreamValue, TableKey, TableValue, VOut>
  • first both key, seconds both values: <StreamKey, TableKey, StreamValue, TableValue, VOut>

Thoughts? Sorry, that we missed this during KIP discussion, but easy to still change and update the KIP.

import static org.apache.kafka.streams.processor.internals.metrics.TaskMetrics.droppedRecordsSensor;
import static org.apache.kafka.streams.state.ValueTimestampHeaders.getValueOrNull;

class KStreamGlobalKTableJoinProcessor<StreamKey, StreamValue, TableKey, TableValue, VOut>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this new class duplicates KStreamKTableJoinProcessor to 99%, with the key difference of calling joiner.apply(mappedKey, record.key(), record.value(), value2) (different class with an additional argument).

I am wondering, if we should instead change KStreamKTableJoinProcessor and let is use ValueJoinerWithMappedAndStreamKey? And for the case when the user passed ValueJoinerWithKey, we wrap it with a ValueJoinerWithMappedAndStreamKey as an "adaptor" which implements apply(...) as

 VR apply(final K1 mappedKey, final K2 streamKey, final V1 value1, final V2 value2) {
  return valueJoinerWithKey.apply(streamKey, value1, value2);
}

just dropping the additional mappedKey parameter. I believe, we do the same already to bridge between ValueJoiner and ValueJoinerWithKey -- uses can pass either one into a join, and the join Processor uses ValueJoinerWithKey so we have a single impl, and a ValueJoiner would be wrapped similarly, dropping the key argument.

* the join key via {@code keySelector}, so {@code readOnlyKey} does <b>not</b> necessarily
* match the key of the {@link GlobalKTable} record being joined.
*
* @deprecated Use {@link #join(GlobalKTable, KeyValueMapper, ValueJoinerWithMappedAndStreamKey)}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @deprecated Use {@link #join(GlobalKTable, KeyValueMapper, ValueJoinerWithMappedAndStreamKey)}
* @deprecated Since 4.4. Use {@link #join(GlobalKTable, KeyValueMapper, ValueJoinerWithMappedAndStreamKey)}

* match the key of the {@link GlobalKTable} record being joined.
*
* @deprecated Use {@link #join(GlobalKTable, KeyValueMapper, ValueJoinerWithMappedAndStreamKey)}
* instead, which exposes both the mapped join key and the stream record's key.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* instead, which exposes both the mapped join key and the stream record's key.
* instead.

I would keep it simple

* <b>Warning:</b> {@code readOnlyKey} is the {@code KStream} record's key, <b>not</b>
* the join key produced by {@code keySelector}. Unlike {@link #join(KTable, ValueJoinerWithKey)
* KStream-KTable} and {@link #join(KStream, ValueJoinerWithKey, JoinWindows) KStream-KStream}
* joins — where the stream key <em>is</em> the join key — {@link GlobalKTable} joins derive

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* joins where the stream key <em>is</em> the join key {@link GlobalKTable} joins derive
* joins &mdash; where the stream key <em>is</em> the join key &mdash; {@link GlobalKTable} joins derive

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if an actuallm-dash would render as expected... using html markup seems to be better? Or remove m-dash and replace with something else.

<GlobalKey, GlobalValue, VOut> KStream<K, VOut> join(final GlobalKTable<GlobalKey, GlobalValue> globalTable,
final KeyValueMapper<? super K, ? super V, ? extends GlobalKey> keySelector,
final ValueJoinerWithKey<? super K, ? super V, ? super GlobalValue, ? extends VOut> joiner);

/**
* As {@link #join(GlobalKTable, KeyValueMapper, ValueJoiner)}, but the joiner receives both
* the mapped join key (used to look up the {@link GlobalKTable} value) and the {@link KStream} record key.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should try to use a unify JavaDocs style. Existing style is:

/**
 * See {@link #join(GlobalKTable, KeyValueMapper, ValueJoiner)}.
 *
 * <maybe explain important differences>
 */

We want to avoid duplicating the same description, so we put a full description into the "simples" variant, and point to overloads. So maybe we need to update the JavaDoc of join(GlobalKTable, KeyValueMapper, ValueJoiner) which currently point to the now deprecated method and let it point to the new one?

If you need read access to the {@code KStream} key, use {@link #join(GlobalKTable, KeyValueMapper, ValueJoinerWithKey)}.

-> change to:

If you need read access to the {@code KStream} key or {@link KTable} key (which is the same as the extract join key), use {@link #join(GlobalKTable, KeyValueMapper, ValueJoinerWithMappedAndStreamKey)}.

Similar for leftJoin() case.

* @param <VR> the type of the joined result value
*/
@FunctionalInterface
public interface ValueJoinerWithMappedAndStreamKey<K1, K2, V1, V2, VR> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use StreamKey, StreamValue etc similar to other interfaces...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-approved kip Requires or implements a KIP streams

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants