-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-14491: [7/N] Enforce strict grace period for versioned stores #13243
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 all commits
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 |
|---|---|---|
|
|
@@ -22,7 +22,6 @@ | |
| import java.nio.ByteBuffer; | ||
| import java.util.Collection; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
| import org.apache.kafka.clients.consumer.ConsumerRecord; | ||
| import org.apache.kafka.common.metrics.Sensor; | ||
| import org.apache.kafka.common.utils.Bytes; | ||
|
|
@@ -82,6 +81,7 @@ public class RocksDBVersionedStore implements VersionedKeyValueStore<Bytes, byte | |
|
|
||
| private final String name; | ||
| private final long historyRetention; | ||
| private final long gracePeriod; | ||
| private final RocksDBMetricsRecorder metricsRecorder; | ||
|
|
||
| private final RocksDBStore latestValueStore; | ||
|
|
@@ -101,6 +101,10 @@ public class RocksDBVersionedStore implements VersionedKeyValueStore<Bytes, byte | |
| RocksDBVersionedStore(final String name, final String metricsScope, final long historyRetention, final long segmentInterval) { | ||
| this.name = name; | ||
| this.historyRetention = historyRetention; | ||
| // history retention doubles as grace period for now. could be nice to allow users to | ||
| // configure the two separately in the future. if/when we do, we should enforce that | ||
| // history retention >= grace period, for sound semantics. | ||
| this.gracePeriod = historyRetention; | ||
| this.metricsRecorder = new RocksDBMetricsRecorder(metricsScope, name); | ||
| this.latestValueStore = new RocksDBStore(latestValueStoreName(name), name, metricsRecorder); | ||
| this.segmentStores = new LogicalKeyValueSegments(segmentsStoreName(name), name, historyRetention, segmentInterval, metricsRecorder); | ||
|
|
@@ -111,15 +115,20 @@ public class RocksDBVersionedStore implements VersionedKeyValueStore<Bytes, byte | |
| @Override | ||
| public void put(final Bytes key, final byte[] value, final long timestamp) { | ||
|
|
||
| if (timestamp < observedStreamTime - gracePeriod) { | ||
| expiredRecordSensor.record(1.0d, context.currentSystemTimeMs()); | ||
| LOG.warn("Skipping record for expired put."); | ||
| return; | ||
| } | ||
| observedStreamTime = Math.max(observedStreamTime, timestamp); | ||
|
|
||
| doPut( | ||
| versionedStoreClient, | ||
| Optional.of(expiredRecordSensor), | ||
| observedStreamTime, | ||
| key, | ||
| value, | ||
| timestamp | ||
| ); | ||
|
|
||
| observedStreamTime = Math.max(observedStreamTime, timestamp); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -147,6 +156,7 @@ public VersionedRecord<byte[]> get(final Bytes key) { | |
| public VersionedRecord<byte[]> get(final Bytes key, final long asOfTimestamp) { | ||
|
|
||
| if (asOfTimestamp < observedStreamTime - historyRetention) { | ||
| LOG.warn("Returning null for expired get."); | ||
| // history retention has elapsed. return null for predictability, even if data | ||
| // is still present in store. | ||
| return null; | ||
|
|
@@ -283,9 +293,13 @@ public void init(final StateStoreContext context, final StateStore root) { | |
|
|
||
| // VisibleForTesting | ||
| void restoreBatch(final Collection<ConsumerRecord<byte[], byte[]>> records) { | ||
| // advance stream time to the max timestamp in the batch | ||
|
|
||
| // compute the observed stream time at the end of the restore batch, in order to speed up | ||
| // restore by not bothering to read from/write to segments which will have expired by the | ||
| // time the restoration process is complete. | ||
| long endOfBatchStreamTime = observedStreamTime; | ||
| for (final ConsumerRecord<byte[], byte[]> record : records) { | ||
| observedStreamTime = Math.max(observedStreamTime, record.timestamp()); | ||
| endOfBatchStreamTime = Math.max(endOfBatchStreamTime, record.timestamp()); | ||
| } | ||
|
|
||
| final VersionedStoreClient<?> restoreClient = restoreWriteBuffer.getClient(); | ||
|
|
@@ -297,6 +311,14 @@ void restoreBatch(final Collection<ConsumerRecord<byte[], byte[]>> records) { | |
| // records into memory. how high this memory amplification will be is very much dependent | ||
| // on the specific workload and the value of the "segment interval" parameter. | ||
| for (final ConsumerRecord<byte[], byte[]> record : records) { | ||
| if (record.timestamp() < observedStreamTime - gracePeriod) { | ||
| // record is older than grace period and was therefore never written to the store | ||
| continue; | ||
| } | ||
| // advance observed stream time as usual, for use in deciding whether records have | ||
| // exceeded the store's grace period and should be dropped. | ||
| observedStreamTime = Math.max(observedStreamTime, record.timestamp()); | ||
|
|
||
| ChangelogRecordDeserializationHelper.applyChecksAndUpdatePosition( | ||
| record, | ||
| consistencyEnabled, | ||
|
|
@@ -306,7 +328,7 @@ void restoreBatch(final Collection<ConsumerRecord<byte[], byte[]>> records) { | |
| // put records to write buffer | ||
| doPut( | ||
| restoreClient, | ||
| Optional.empty(), | ||
| endOfBatchStreamTime, | ||
| new Bytes(record.key()), | ||
| record.value(), | ||
| record.timestamp() | ||
|
|
@@ -438,9 +460,22 @@ public void writeLatestValues(final WriteBatch batch) throws RocksDBException { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Helper method shared between put and restore. | ||
| * <p> | ||
| * This method does not check whether the record being put is expired based on grace period | ||
| * or not; that is the caller's responsibility. This method does, however, check whether the | ||
| * record is expired based on history retention, by using the current | ||
| * {@code observedStreamTime}, and returns without inserting into the store if so. It can be | ||
| * possible that a record is not expired based on grace period but is expired based on | ||
| * history retention, even though history retention is always at least the grace period, | ||
| * during restore because restore advances {@code observedStreamTime} to the largest timestamp | ||
| * in the entire restore batch at the beginning of restore, in order to optimize for not | ||
| * putting records into the store which will have expired by the end of the restore. | ||
|
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. Thanks for adding this! Great addition! |
||
| */ | ||
| private <T extends VersionedStoreSegment> void doPut( | ||
| final VersionedStoreClient<T> versionedStoreClient, | ||
| final Optional<Sensor> expiredRecordSensor, | ||
| final long observedStreamTime, | ||
| final Bytes key, | ||
| final byte[] value, | ||
| final long timestamp | ||
|
|
@@ -453,6 +488,7 @@ private <T extends VersionedStoreSegment> void doPut( | |
| // check latest value store | ||
| PutStatus status = maybePutToLatestValueStore( | ||
| versionedStoreClient, | ||
| observedStreamTime, | ||
| key, | ||
| value, | ||
| timestamp | ||
|
|
@@ -466,7 +502,7 @@ private <T extends VersionedStoreSegment> void doPut( | |
| // continue search in segments | ||
| status = maybePutToSegments( | ||
| versionedStoreClient, | ||
| expiredRecordSensor, | ||
| observedStreamTime, | ||
| key, | ||
| value, | ||
| timestamp, | ||
|
|
@@ -482,7 +518,7 @@ private <T extends VersionedStoreSegment> void doPut( | |
| // or segments store). insert based on foundTs here instead. | ||
| finishPut( | ||
| versionedStoreClient, | ||
| expiredRecordSensor, | ||
| observedStreamTime, | ||
| key, | ||
| value, | ||
| timestamp, | ||
|
|
@@ -515,6 +551,7 @@ private static class PutStatus { | |
|
|
||
| private <T extends VersionedStoreSegment> PutStatus maybePutToLatestValueStore( | ||
| final VersionedStoreClient<T> versionedStoreClient, | ||
| final long observedStreamTime, | ||
| final Bytes key, | ||
| final byte[] value, | ||
| final long timestamp | ||
|
|
@@ -578,7 +615,7 @@ private <T extends VersionedStoreSegment> PutStatus maybePutToLatestValueStore( | |
|
|
||
| private <T extends VersionedStoreSegment> PutStatus maybePutToSegments( | ||
| final VersionedStoreClient<T> versionedStoreClient, | ||
| final Optional<Sensor> expiredRecordSensor, | ||
| final long observedStreamTime, | ||
| final Bytes key, | ||
| final byte[] value, | ||
| final long timestamp, | ||
|
|
@@ -605,6 +642,7 @@ private <T extends VersionedStoreSegment> PutStatus maybePutToSegments( | |
| // insert and conclude the procedure. | ||
| putToSegment( | ||
| versionedStoreClient, | ||
| observedStreamTime, | ||
| segment, | ||
| rawSegmentValue, | ||
| key, | ||
|
|
@@ -615,11 +653,10 @@ private <T extends VersionedStoreSegment> PutStatus maybePutToSegments( | |
| } | ||
|
|
||
| if (foundMinTs < observedStreamTime - historyRetention) { | ||
| // the record being inserted does not affect version history. discard and return | ||
| if (expiredRecordSensor.isPresent()) { | ||
| expiredRecordSensor.get().record(1.0d, context.currentSystemTimeMs()); | ||
| LOG.warn("Skipping record for expired put."); | ||
| } | ||
| // the record being inserted does not affect version history. discard and return. | ||
|
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 can follow. Why did we record this in the sensor first, but not any longer? Same below (2x).
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. With the changes in this PR, it is only possible to hit this case during restore now. Previously, we passed The reason it is not possible to hit this case during non-restore is because |
||
| // this can happen during restore because individual put calls are executed after | ||
| // observedStreamTime is fast-forwarded for the entire batch of records being | ||
| // restored at once. | ||
| return new PutStatus(true, foundTs); | ||
| } | ||
|
|
||
|
|
@@ -634,6 +671,7 @@ private <T extends VersionedStoreSegment> PutStatus maybePutToSegments( | |
|
|
||
| private <T extends VersionedStoreSegment> void putToSegment( | ||
| final VersionedStoreClient<T> versionedStoreClient, | ||
| final long observedStreamTime, | ||
| final T segment, | ||
| final byte[] rawSegmentValue, | ||
| final Bytes key, | ||
|
|
@@ -714,7 +752,7 @@ private <T extends VersionedStoreSegment> void putToSegment( | |
|
|
||
| private <T extends VersionedStoreSegment> void finishPut( | ||
| final VersionedStoreClient<T> versionedStoreClient, | ||
| final Optional<Sensor> expiredRecordSensor, | ||
| final long observedStreamTime, | ||
| final Bytes key, | ||
| final byte[] value, | ||
| final long timestamp, | ||
|
|
@@ -730,10 +768,10 @@ private <T extends VersionedStoreSegment> void finishPut( | |
| final T segment = versionedStoreClient.getOrCreateSegmentIfLive( | ||
| versionedStoreClient.segmentIdForTimestamp(timestamp), context, observedStreamTime); | ||
| if (segment == null) { | ||
| if (expiredRecordSensor.isPresent()) { | ||
| expiredRecordSensor.get().record(1.0d, context.currentSystemTimeMs()); | ||
| LOG.warn("Skipping record for expired put."); | ||
| } | ||
| // the record being inserted does not affect version history. discard and return. | ||
| // this can happen during restore because individual put calls are executed after | ||
| // observedStreamTime is fast-forwarded for the entire batch of records being | ||
| // restored at once. | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -777,10 +815,10 @@ private <T extends VersionedStoreSegment> void finishPut( | |
| final T segment = versionedStoreClient.getOrCreateSegmentIfLive( | ||
| versionedStoreClient.segmentIdForTimestamp(foundTs), context, observedStreamTime); | ||
| if (segment == null) { | ||
| if (expiredRecordSensor.isPresent()) { | ||
| expiredRecordSensor.get().record(1.0d, context.currentSystemTimeMs()); | ||
| LOG.warn("Skipping record for expired put."); | ||
| } | ||
| // the record being inserted does not affect version history. discard and return. | ||
| // this can happen during restore because individual put calls are executed after | ||
| // observedStreamTime is fast-forwarded for the entire batch of records being | ||
| // restored at once. | ||
| return; | ||
| } | ||
|
|
||
|
|
||
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.
If it was never written to the store, if should also not be in the changelog topic?
This might still be useful if we read from the input topic for a KTable I guess? But we might want to update the JavaDoc for to mention this case?
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.
Ideally, but unfortunately no. Only the inner layer (RocksDBVersionedStore) contains logic for deciding when grace period has elapsed and a call to
put()should return without updating the store. The changelogging layer wrapped around this inner layer does not know about grace period, nor do any of the other outer layers. The changelogging layer does callput()before callinglog(), but becauseput()has no return type, it does not convey information about whether an update was actually made or ifput()simply returned without doing anything. So, the changelogging layer callslog()in either case.This is the existing behavior for window stores, and what I had planned to replicate for versioned stores as well. If we don't want this, we could:
put()to return a boolean, indicating whether the update was actually performed, orlog()at the changelogging layer if it's not needed.I don't particularly like either option. Curious to hear your thoughts.
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. Makes sense.
I think it's ok to leave it as-is for now. But could you maybe file a Jira ticket (with all the glory details) for tracking? Might be worth to do some follow up work later to change it (but not worth to delay the KIP implemenation at this point).
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.
Sounds good. Here's the ticket: https://issues.apache.org/jira/browse/KAFKA-14723