KAFKA-14491: [11/N] Add metered wrapper for versioned stores#13252
Conversation
75bebe8 to
760dbf9
Compare
There was a problem hiding this comment.
This design choice has implications -- I'm curious to hear reviewer thoughts.
The existing MeteredKeyValueStore and MeteredTimestampedKeyValueStore both contain logic to assist in translating KeyQuery<K, V> and RangeQuery<K, V> types to queries understandable by the underlying bytes store, and to deserialize the returned bytes result back for the original K and V result types. The return type of these queries for MeteredKeyValueStore and MeteredTimestampedKeyValueStore is V for the key query, and KeyValueIterator<K, V> for the range query. We'll want to introduce new return types for versioned stores, perhaps VersionedRecord<V> for the key query and Iterator<VersionedRecord<V>> for the range query. I'd like to postpone this decision-making for a follow-up KIP, but that means not assisting in translating to/from the inner bytes store for now.
So we have three options in the meantime:
- Reject all IQv2 queries, since RocksDBVersionedKeyValueStore doesn't support IQv2 yet anyway. I don't think we should do this, because then users who create their own VersionedKeyValueStore implementations won't be able to issue IQv2 queries either.
- Allow IQv2 queries as a pass-through at the metered layer. This is what the PR currently does. It means that users who implement their own VersionedKeyValueStores will be able to issue IQv2 queries, but also has backwards compatibility implications for when we introduce type serialization/deserialization support for key and range queries in the future. The behavior at this metered store layer would suddenly change for those query types.
- Reject KeyQuery and RangeQuery types in order to reserve them for future behaviors, and pass-through all other query types. This avoids compatibility concerns for the future, while still allowing users to issue IQv2 queries to their own VersionedKeyValueStore implementations 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.
There was a problem hiding this comment.
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 UserVersionedKeyValueStore that supports a RangeQuery (as example), the semantics and types are bound to UverVersionedKeyValueStore -- when we later add RangeQuery to RocksDBVersionedStore, it seem ok if it's different because it comes with it's own implementation?
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.
There was a problem hiding this comment.
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:
- The user passes a
KeyValueStore<Bytes, byte[]>implementation (via supplier/materializer) which is used as the innermost store. This store only knows how to serve queries from bytes, and doesn't know anything about what the original key or value types are. - The user's store gets wrapped inside some extra layers, including the metered layer which knows how to serialize the actual key and value types to bytes, and deserialize back.
- When the user issues a IQv2 query to the store, the query hits the outer store (i.e., metered layer), and is passed to the inner layers from there.
- What this means is that when a user issues an IQv2
KeyQuery, the key that they pass is the actual value type, and not bytes, even though the innermost store implementation they provided only knows about bytes.MeteredKeyValueStorehas logic for serializing the key in theKeyQueryto bytes, and passing that to the inner store, and also deserializing the bytes from the result into the actual value type before returning the result to the user. The same thing happens withRangeQuery, but these are the only two query types whichMeteredKeyValueStoreprovides serialization/deserialization assistance for. All other query types are direct pass-throughs to the inner store.
In order to provide the same convenience for users issuing IQv2 requests to versioned stores, MeteredVersionedKeyValueStore should also assist in serializing/deserializing KeyQuery and RangeQuery for users, so that they can issue KeyQuery<K, V> instead of KeyQuery<Bytes, byte[]> to their inner store, but I don't want to add this support now as part of KIP-889, since it should have its own KIP. So if a user wants to issue key queries to their custom versioned store implementation today, they will have to use KeyQuery<Bytes, byte[]>. But then later when we do add support for KeyQuery<K, V> at the metered store layer, users will have to stop using KeyQuery<Bytes, byte[]> and instead start using KeyQuery<K, V> -- this is the compatibility concern I have called out above. Does this extra context help?
There was a problem hiding this comment.
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 KeyQuery and/or RangeQuery implementation (to assist with serialization/deserialization of results) would be a breaking change that should be avoided. I will update this PR to reserve the KeyQuery and RangeQuery query types instead, i.e., throw an exception if those queries are issued while allowing other types of queries (e.g., user custom queries) to pass through.
760dbf9 to
418184d
Compare
| super.put( | ||
| key, | ||
| // versioned stores require a timestamp associated with all puts, including tombstones/deletes | ||
| value == null |
There was a problem hiding this comment.
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 context.timestamp() does not sound right to me?
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?
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
Just for an internal safe-guard, we should put a requiresNotNull in place for this case.
| super.put( | ||
| key, | ||
| // versioned stores require a timestamp associated with all puts, including tombstones/deletes | ||
| value == null |
There was a problem hiding this comment.
Just for an internal safe-guard, we should put a requiresNotNull in place for this case.
There was a problem hiding this comment.
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 UserVersionedKeyValueStore that supports a RangeQuery (as example), the semantics and types are bound to UverVersionedKeyValueStore -- when we later add RangeQuery to RocksDBVersionedStore, it seem ok if it's different because it comes with it's own implementation?
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.
| * 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. |
There was a problem hiding this comment.
I think this comments need some more information. What about this:
Conceptually, MeteredVersionedKeyValueStore should <code>extend</code> MeteredTimestampKeyValueStore, but due to type conflicts, we cannot do this. Thus, we use an <it>instance</it> of MeteredTimestampKeyValueStore to mimic inheritance instead. This instance of MeteredTimestampKeyValueStore wraps the inner VersionedKeyValueStore, and thus we need to do some type translation inside the internal wrapped below.
It's not ideal in the sense that we need to translate between the APIs/types twice, but reusing code is still better than c&p it.
Note that we overwrite `get()` but newly add `put(k, v, ts)` as `delete(k, ts)` to get the same API as defined on VersionedKeyValueStore.
There was a problem hiding this comment.
Thanks for the suggestions! Incorporated this into the latest commit. I used KeyValueStore in the comments instead of TimestampedKeyValueStore (pending our other discussion about whether MeteredVersionedKeyValueStore is conceptually MeteredTimestampedKeyValueStore or MeteredKeyValueStore) and also modified the last line since I wasn't sure which get() override you were referring to.
| */ | ||
| private class MeteredVersionedKeyValueStoreInternal | ||
| extends MeteredKeyValueStore<K, ValueAndTimestamp<V>> | ||
| implements TimestampedKeyValueStore<K, V> { |
There was a problem hiding this comment.
Why do we not directly extend MeteredTimestampKeyValueStore but extend MeteredKeyValueStore plus implement TimestampedKeyValueStore?
There was a problem hiding this comment.
MeteredVersionedKeyValueStoreInternal and MeteredTimestampKeyValueStore use different serdes -- MeteredTimestampKeyValueStore uses ValueAndTimestampSerde while MeteredVersionedKeyValueStoreInternal uses NullableValueAndTimestampSerde.
If you look at the code for MeteredTimestampKeyValueStore, the only method it overrides from MeteredKeyValueStore is prepareValueSerdeForStore(). We could have MeteredVersionedKeyValueStoreInternal extend MeteredTimestampKeyValueStore instead of MeteredKeyValueStore if we want but MeteredVersionedKeyValueStoreInternal needs to override prepareValueSerdeForStore() with its own serde anyway, so MeteredVersionedKeyValueStoreInternal would get zero functionality from MeteredTimestampKeyValueStore.
Also, IMO MeteredVersionedKeyValueStoreInternal and MeteredTimestampKeyValueStore are different conceptually even though they both have the signature MeteredKeyValueStore<K, ValueAndTimestamp<V>>. The reason is because the metered layer (in addition to collecting metrics) is responsible for handling serialization to/from the inner bytes stores (pending your other comment, which we can discuss there), and the serialization behavior of the two stores is different, since they use different serdes. This is a more minor reason, though. If you think it's conceptually easier to think of versioned stores as a special case of timestamped stores, we can have MeteredVersionedKeyValueStoreInternal extends MeteredTimestampKeyValueStore instead.
|
|
||
| @Override | ||
| public void put(final K key, final ValueAndTimestamp<V> value) { | ||
| super.put( |
There was a problem hiding this comment.
Thinking about this once more, it seems not ideal to do it this way? We get ValueAndTimestamp and call super.put(), what will do the serialization for use, and than call the VersionedKeyValueToBytesStoreAdapter that will split out raw-value, and raw-ts to call inner.put(k,v,ts).
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 put(k,v) method, but why do we not add a put(k,v,ts) to the changeloggging (and also caching?) store and also only have put(k,v,ts) here?
In N/10 you said that the changelogger implements VersionedBytesStore instead of VersionedKeyValueStore, and I am now questioning if it's the right call?
We also add get() and delete() below without overwriting. It seems we should also add put(k,v,ts) in the same manner?
There was a problem hiding this comment.
Let me try to repeat back your suggestion to make sure I understand correctly: as you've pointed out, the current MeteredVersionedKeyValueStore implementation first serializes value and timestamp to bytes and concatenates them into a special format (given by NullableValueAndTimestampSerde) to pass to inner stores. Then the inner stores have to separate the value and timestamp bytes again and deserialize the timestamp, for use in writing to the changelog or inserting into the inner store. You're suggesting that we make this more efficient by not having the metered layer serialize the timestamp and concatenate with the value bytes, and instead pass an unserialized timestamp and separate value bytes to the inner stores. Is that correct?
If so, this suggestion is not possible for the innermost store (RocksDBVersionedStore, above which the VersionedKeyValueToBytesStoreAdapter is used) because of our decision to maintain compatibility with existing DSL methods for passing key-value stores, e.g., StreamsBuilder#table() and KTable methods, which are explicitly typed to accept Materialized<K, V, KeyValueStore<Bytes, byte[]>, rather than adding new versions of these methods which accept Materialized instances to materialize stores other than KeyValueStore<Bytes, byte[]>. During the design phase we had discussed this tradeoff already and decided that the benefit of not needing to introduce new methods here is substantial enough to warrant the performance hit from the extra round of serialization and deserialization.
The changelogging layer is a different story. If we wanted to, we could indeed have ChangeLoggingVersionedKeyValueBytesStore implement VersionedKeyValueStore<Bytes, byte[]> instead of VersionedBytesStore to avoid needing to extract value and timestamp bytes from the serialized rawValueAndTimestamp. The cost of doing so, however, is extra duplicated code. If we do this, then we cannot have ChangeLoggingVersionedKeyValueBytesStore extends ChangeLoggingKeyValueBytesStore anymore because ChangeLoggingKeyValueBytesStore implements KeyValueStore<Bytes, byte[]> which clashes with VersionedKeyValueStore<Bytes, byte[]>. We also wouldn't be able to have the metered versioned store layer extend MeteredKeyValueStore anymore either because MeteredKeyValueStore is a WrappedStateStore with KeyValueStore<Bytes, byte[]> inside, and ChangeLoggingVersionedKeyValueBytesStore would no longer count as a KeyValueStore<Bytes, byte[]>. So, we'd end up more-or-less duplicating the existing code in both MeteredKeyValueStore and ChangeLoggingKeyValueBytesStore into their versioned counterparts. As a result, I don't think the performance benefit saved from avoiding the extra deserialization in the changelogging layer is worth this extra complexity. FWIW, ChangeLoggingTimestampedKeyValueBytesStore also has this same extra deserialization step (separating the concatenated value and timestamp bytes, and deserializing the timestamp for writing to the changelog), I assume for the same reason.
|
|
||
| @Test | ||
| public void shouldDelegateAndRecordMetricsOnGet() { | ||
| store.get(KEY); |
There was a problem hiding this comment.
I think we should also mock inner.get() to let it return something useful and check that we get the expected result handed back here.
|
|
||
| @Test | ||
| public void shouldDelegateAndRecordMetricsOnGetWithTimestamp() { | ||
| store.get(KEY, TIMESTAMP); |
|
|
||
| @Test | ||
| public void shouldDelegateAndRecordMetricsOnDelete() { | ||
| store.delete(KEY, TIMESTAMP); |
| @Test | ||
| public void shouldDelegateInit() { | ||
| // init is already called in setUp() | ||
| verify(inner).init((StateStoreContext) context, store); |
There was a problem hiding this comment.
Should we also verify the delegation of persistent(), isOpen(), getPosition() and name() (hope I did not forget any method).
| } | ||
|
|
||
| @Override | ||
| public String name() { |
There was a problem hiding this comment.
It seems we overwrite some method to delegate to internal.x() instead of inner.x() but I am not sure what the pattern is. Can you elaborate?
There was a problem hiding this comment.
All methods from MeteredVersionedKeyValueStore delegate to internal; the methods in MeteredVersionedKeyValueStore are purely a wrapper for the internal MeteredVersionedKeyValueStoreInternal instance. All the actual logic happens in MeteredVersionedKeyValueStoreInternal, which delegates its methods to inner, which is the actual inner store (either a changelogging layer or the inner versioned store implementation itself). The only reason that MeteredVersionedKeyValueStore implements WrappedStateStore at all is because there are places in the DSL code which expect the outermost store layer to be a WrappedStateStore in order to call methods such as setFlushListener() and flushCache().
| } | ||
|
|
||
| @Test | ||
| public void shouldDelegateAndRemoveMetricsOnClose() { |
There was a problem hiding this comment.
Do we need this test? Seem it's rather testing the wrapped-store?
There was a problem hiding this comment.
Not sure I follow. This test verifies that metrics are properly cleaned up when the store is closed, and this functionality is unique to this metered store layer. Maybe you mean that this test tests functionality which is inherited from MeteredKeyValueStore? That is true, but I think it is important to re-test here because we want to ensure that MeteredVersionedKeyValueStore properly cleans up its metrics as well, i.e., MeteredVersionedKeyValueStore is separate from MeteredKeyValueStore.
| } | ||
|
|
||
| @Test | ||
| public void shouldRemoveMetricsOnCloseEvenIfInnerThrows() { |
| } | ||
|
|
||
| @Test | ||
| public void shouldNotSetFlushListenerIfInnerIsNotCaching() { |
There was a problem hiding this comment.
Same reasoning as above -- the functionality that this test checks for is also inherited from MeteredKeyValueStore, but I do think it's worth verifying that MeteredVersionedKeyValueStore properly inherits it.
This PR introduces the metered store layer for the new versioned key-value store introduced in KIP-889. This outermost, metered store layer handles all serialization/deserialization from
VersionedKeyValueStoreto a bytes-representation (VersionedBytesStore) so that all inner stores may operate only with bytes types (see changelogging layer in #13251 as an example).Committer Checklist (excluded from commit message)