-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-14491: [11/N] Add metered wrapper for versioned stores #13252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,227 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.apache.kafka.streams.state.internals; | ||
|
|
||
| import static org.apache.kafka.streams.processor.internals.metrics.StreamsMetricsImpl.maybeMeasureLatency; | ||
|
|
||
| import java.util.Objects; | ||
| import org.apache.kafka.common.serialization.Serde; | ||
| import org.apache.kafka.common.utils.Time; | ||
| import org.apache.kafka.streams.errors.ProcessorStateException; | ||
| import org.apache.kafka.streams.processor.ProcessorContext; | ||
| import org.apache.kafka.streams.processor.StateStore; | ||
| import org.apache.kafka.streams.processor.StateStoreContext; | ||
| import org.apache.kafka.streams.processor.internals.SerdeGetter; | ||
| import org.apache.kafka.streams.query.Position; | ||
| import org.apache.kafka.streams.query.PositionBound; | ||
| import org.apache.kafka.streams.query.Query; | ||
| import org.apache.kafka.streams.query.QueryConfig; | ||
| import org.apache.kafka.streams.query.QueryResult; | ||
| import org.apache.kafka.streams.state.KeyValueStore; | ||
| import org.apache.kafka.streams.state.TimestampedKeyValueStore; | ||
| import org.apache.kafka.streams.state.ValueAndTimestamp; | ||
| import org.apache.kafka.streams.state.VersionedBytesStore; | ||
| import org.apache.kafka.streams.state.VersionedKeyValueStore; | ||
| import org.apache.kafka.streams.state.VersionedRecord; | ||
|
|
||
| /** | ||
| * A metered {@link VersionedKeyValueStore} wrapper that is used for recording operation | ||
| * metrics, and hence its inner {@link VersionedBytesStore} implementation does not need to provide | ||
| * its own metrics collecting functionality. The inner {@code VersionedBytesStore} of this class | ||
| * is a {@link KeyValueStore} of type <Bytes,byte[]>, so we use {@link Serde}s | ||
| * to convert from <K,ValueAndTimestamp<V>> to <Bytes,byte[]>. In particular, | ||
| * {@link NullableValueAndTimestampSerde} is used since putting a tombstone to a versioned key-value | ||
| * store requires putting a null value associated with a timestamp. | ||
| * | ||
| * @param <K> The key type | ||
| * @param <V> The (raw) value type | ||
| */ | ||
| public class MeteredVersionedKeyValueStore<K, V> | ||
| extends WrappedStateStore<VersionedBytesStore, K, V> | ||
| implements VersionedKeyValueStore<K, V> { | ||
|
|
||
| private final MeteredVersionedKeyValueStoreInternal internal; | ||
|
|
||
| MeteredVersionedKeyValueStore(final VersionedBytesStore inner, | ||
| final String metricScope, | ||
| final Time time, | ||
| final Serde<K> keySerde, | ||
| final Serde<ValueAndTimestamp<V>> valueSerde) { | ||
| super(inner); | ||
| internal = new MeteredVersionedKeyValueStoreInternal(inner, metricScope, time, keySerde, valueSerde); | ||
| } | ||
|
|
||
| /** | ||
| * Private helper class which represents the functionality of a {@link VersionedKeyValueStore} | ||
| * as a {@link TimestampedKeyValueStore} so that the bulk of the metering logic may be | ||
| * inherited from {@link MeteredKeyValueStore}. As a result, the implementation of | ||
| * {@link MeteredVersionedKeyValueStore} is a simple wrapper to translate from this | ||
| * {@link TimestampedKeyValueStore} representation of a versioned key-value store into the | ||
| * {@link VersionedKeyValueStore} interface itself. | ||
| */ | ||
| private class MeteredVersionedKeyValueStoreInternal | ||
| extends MeteredKeyValueStore<K, ValueAndTimestamp<V>> | ||
| implements TimestampedKeyValueStore<K, V> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we not directly
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If you look at the code for Also, IMO |
||
|
|
||
| private final VersionedBytesStore inner; | ||
|
|
||
| MeteredVersionedKeyValueStoreInternal(final VersionedBytesStore inner, | ||
| final String metricScope, | ||
| final Time time, | ||
| final Serde<K> keySerde, | ||
| final Serde<ValueAndTimestamp<V>> valueSerde) { | ||
| super(inner, metricScope, time, keySerde, valueSerde); | ||
| this.inner = inner; | ||
| } | ||
|
|
||
| @Override | ||
| public void put(final K key, final ValueAndTimestamp<V> value) { | ||
| super.put( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thinking about this once more, it seems not ideal to do it this way? We get Seems overly complicated. I merged 10/N already, but now I am wondering if it the right approach? Right now, we need to do this such that we can swap in/out the changeloggging store that only has a In N/10 you said that the changelogger implements We also add
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me try to repeat back your suggestion to make sure I understand correctly: as you've pointed out, the current If so, this suggestion is not possible for the innermost store ( The changelogging layer is a different story. If we wanted to, we could indeed have |
||
| key, | ||
| // versioned stores require a timestamp associated with all puts, including tombstones/deletes | ||
| value == null | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one is tricky -- not sure if we should allow it, but instead change the usage of the store in the DSL to avoid calling this method? Just substituting I guess the current idea was to bridge from TKV to VKV -- could we instead bridge the other way around, ie, we change the DSL to "assume" the VKV interface but if the underlying store is a TKV we translate with an adaptor?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm I think this code here is a remnant of my attempt at an earlier approach where we expose MeteredVersionedKeyValueStore itself as a TimestampedKeyValueStore -- which did not work out for other reasons, thus prompting #13264. The line you've highlighted here is within MeteredVersionedKeyValueStoreInternal, which is only called from one place (MeteredVersionedKeyValueStore#put()) and that one place never passes null, so this can be removed. Good catch :)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just for an internal safe-guard, we should put a |
||
| ? ValueAndTimestamp.makeAllowNullable(null, context.timestamp()) | ||
| : value | ||
| ); | ||
| } | ||
|
|
||
| public ValueAndTimestamp<V> get(final K key, final long asOfTimestamp) { | ||
| Objects.requireNonNull(key, "key cannot be null"); | ||
| try { | ||
| return maybeMeasureLatency(() -> outerValue(inner.get(keyBytes(key), asOfTimestamp)), time, getSensor); | ||
| } catch (final ProcessorStateException e) { | ||
| final String message = String.format(e.getMessage(), key); | ||
| throw new ProcessorStateException(message, e); | ||
| } | ||
| } | ||
|
|
||
| public ValueAndTimestamp<V> delete(final K key, final long timestamp) { | ||
| Objects.requireNonNull(key, "key cannot be null"); | ||
| try { | ||
| return maybeMeasureLatency(() -> outerValue(inner.delete(keyBytes(key), timestamp)), time, deleteSensor); | ||
| } catch (final ProcessorStateException e) { | ||
| final String message = String.format(e.getMessage(), key); | ||
| throw new ProcessorStateException(message, e); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public <R> QueryResult<R> query(final Query<R> query, | ||
| final PositionBound positionBound, | ||
| final QueryConfig config) { | ||
| final long start = time.nanoseconds(); | ||
| final QueryResult<R> result = wrapped().query(query, positionBound, config); | ||
| if (config.isCollectExecutionInfo()) { | ||
| result.addExecutionInfo( | ||
| "Handled in " + getClass() + " in " + (time.nanoseconds() - start) + "ns"); | ||
| } | ||
| // do not convert query or return types to/from inner bytes store to user-friendly types | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This design choice has implications -- I'm curious to hear reviewer thoughts. The existing MeteredKeyValueStore and MeteredTimestampedKeyValueStore both contain logic to assist in translating So we have three options in the meantime:
I'm curious to know whether there's existing precedent / discussion on this compatibility concern for introducing custom logic at the metered layer for handling specific types of IQv2 queries.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if I understand point (2). The implementation of a query, is bound to the implementation of the store, not the store type. Thus, if there is a Thus I think it ok to have the code as-is going with (2) (as a matter of fact, I think we can remove this comment as it seems unnecessary to me). At least that's my understanding on IQv2. Maybe @vvcephei can chime in and verify my understanding.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, I think we might be talking about slightly different things. Here's my current understanding of how this works for the existing key-value stores today:
In order to provide the same convenience for users issuing IQv2 requests to versioned stores,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I spoke to @vvcephei who confirmed that allowing IQv2 queries to pass through the metered layer in this PR and then later following up with an in-built |
||
| // at this time, since we'll want a kip to align on what the types should be, and | ||
| // because we'll likely want to introduce a new return type which includes key, | ||
| // value, and timestamp for range queries | ||
| return result; | ||
| } | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
| @Override | ||
| protected Serde<ValueAndTimestamp<V>> prepareValueSerdeForStore( | ||
| final Serde<ValueAndTimestamp<V>> valueSerde, | ||
| final SerdeGetter getter | ||
| ) { | ||
| if (valueSerde == null) { | ||
| return new NullableValueAndTimestampSerde<>((Serde<V>) getter.valueSerde()); | ||
| } else { | ||
| return super.prepareValueSerdeForStore(valueSerde, getter); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void put(final K key, final V value, final long timestamp) { | ||
| internal.put(key, ValueAndTimestamp.makeAllowNullable(value, timestamp)); | ||
| } | ||
|
|
||
| @Override | ||
| public VersionedRecord<V> delete(final K key, final long timestamp) { | ||
| final ValueAndTimestamp<V> valueAndTimestamp = internal.delete(key, timestamp); | ||
| return valueAndTimestamp == null | ||
| ? null | ||
| : new VersionedRecord<>(valueAndTimestamp.value(), valueAndTimestamp.timestamp()); | ||
| } | ||
|
|
||
| @Override | ||
| public VersionedRecord<V> get(final K key) { | ||
| final ValueAndTimestamp<V> valueAndTimestamp = internal.get(key); | ||
| return valueAndTimestamp == null | ||
| ? null | ||
| : new VersionedRecord<>(valueAndTimestamp.value(), valueAndTimestamp.timestamp()); | ||
| } | ||
|
|
||
| @Override | ||
| public VersionedRecord<V> get(final K key, final long asOfTimestamp) { | ||
| final ValueAndTimestamp<V> valueAndTimestamp = internal.get(key, asOfTimestamp); | ||
| return valueAndTimestamp == null | ||
| ? null | ||
| : new VersionedRecord<>(valueAndTimestamp.value(), valueAndTimestamp.timestamp()); | ||
| } | ||
|
|
||
| @Override | ||
| public String name() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems we overwrite some method to delegate to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All methods from |
||
| return internal.name(); | ||
| } | ||
|
|
||
| @Deprecated | ||
| @Override | ||
| public void init(final ProcessorContext context, final StateStore root) { | ||
| internal.init(context, root); | ||
| } | ||
|
|
||
| @Override | ||
| public void init(final StateStoreContext context, final StateStore root) { | ||
| internal.init(context, root); | ||
| } | ||
|
|
||
| @Override | ||
| public void flush() { | ||
| internal.flush(); | ||
| } | ||
|
|
||
| @Override | ||
| public void close() { | ||
| internal.close(); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean persistent() { | ||
| return internal.persistent(); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isOpen() { | ||
| return internal.isOpen(); | ||
| } | ||
|
|
||
| @Override | ||
| public <R> QueryResult<R> query(final Query<R> query, final PositionBound positionBound, final QueryConfig config) { | ||
| return internal.query(query, positionBound, config); | ||
| } | ||
|
|
||
| @Override | ||
| public Position getPosition() { | ||
| return internal.getPosition(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this comments need some more information. What about this:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestions! Incorporated this into the latest commit. I used
KeyValueStorein the comments instead ofTimestampedKeyValueStore(pending our other discussion about whetherMeteredVersionedKeyValueStoreis conceptuallyMeteredTimestampedKeyValueStoreorMeteredKeyValueStore) and also modified the last line since I wasn't sure whichget()override you were referring to.