Browse Source
This PR contains three main changes: - Support for transactions in MetadataLoader - Abort in-progress transaction during controller failover - Utilize transactions for ZK to KRaft migration A new MetadataBatchLoader class is added to decouple the loading of record batches from the publishing of metadata in MetadataLoader. Since a transaction can span across multiple batches (or multiple transactions could exist within one batch), some buffering of metadata updates was needed before publishing out to the MetadataPublishers. MetadataBatchLoader accumulates changes into a MetadataDelta, and uses a callback to publish to the publishers when needed. One small oddity with this approach is that since we can "splitting" batches in some cases, the number of bytes returned in the LogDeltaManifest has new semantics. The number of bytes included in a batch is now only included in the last metadata update that is published as a result of a batch. Reviewers: Colin P. McCabe <cmccabe@apache.org>pull/14267/head
David Arthur
1 year ago
committed by
GitHub
23 changed files with 1944 additions and 300 deletions
@ -0,0 +1,233 @@
@@ -0,0 +1,233 @@
|
||||
/* |
||||
* 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.controller; |
||||
|
||||
import org.apache.kafka.common.metadata.AbortTransactionRecord; |
||||
import org.apache.kafka.common.metadata.BeginTransactionRecord; |
||||
import org.apache.kafka.common.metadata.EndTransactionRecord; |
||||
import org.apache.kafka.metadata.bootstrap.BootstrapMetadata; |
||||
import org.apache.kafka.metadata.migration.ZkMigrationState; |
||||
import org.apache.kafka.server.common.ApiMessageAndVersion; |
||||
import org.apache.kafka.server.common.MetadataVersion; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.function.Consumer; |
||||
|
||||
public class ActivationRecordsGenerator { |
||||
|
||||
static ControllerResult<Void> recordsForEmptyLog( |
||||
Consumer<String> activationMessageConsumer, |
||||
long transactionStartOffset, |
||||
boolean zkMigrationEnabled, |
||||
BootstrapMetadata bootstrapMetadata, |
||||
MetadataVersion metadataVersion |
||||
) { |
||||
StringBuilder logMessageBuilder = new StringBuilder("Performing controller activation. "); |
||||
List<ApiMessageAndVersion> records = new ArrayList<>(); |
||||
|
||||
if (transactionStartOffset != -1L) { |
||||
// In-flight bootstrap transaction
|
||||
if (!metadataVersion.isMetadataTransactionSupported()) { |
||||
throw new RuntimeException("Detected partial bootstrap records transaction at " + |
||||
transactionStartOffset + ", but the metadata.version " + metadataVersion + |
||||
" does not support transactions. Cannot continue."); |
||||
} else { |
||||
logMessageBuilder |
||||
.append("Aborting partial bootstrap records transaction at offset ") |
||||
.append(transactionStartOffset) |
||||
.append(". Re-appending ") |
||||
.append(bootstrapMetadata.records().size()) |
||||
.append(" bootstrap record(s) in new metadata transaction at metadata.version ") |
||||
.append(metadataVersion) |
||||
.append(" from bootstrap source '") |
||||
.append(bootstrapMetadata.source()) |
||||
.append("'. "); |
||||
records.add(new ApiMessageAndVersion( |
||||
new AbortTransactionRecord().setReason("Controller failover"), (short) 0)); |
||||
records.add(new ApiMessageAndVersion( |
||||
new BeginTransactionRecord().setName("Bootstrap records"), (short) 0)); |
||||
} |
||||
} else { |
||||
// No in-flight transaction
|
||||
logMessageBuilder |
||||
.append("The metadata log appears to be empty. ") |
||||
.append("Appending ") |
||||
.append(bootstrapMetadata.records().size()) |
||||
.append(" bootstrap record(s) "); |
||||
if (metadataVersion.isMetadataTransactionSupported()) { |
||||
records.add(new ApiMessageAndVersion( |
||||
new BeginTransactionRecord().setName("Bootstrap records"), (short) 0)); |
||||
logMessageBuilder.append("in metadata transaction "); |
||||
} |
||||
logMessageBuilder |
||||
.append("at metadata.version ") |
||||
.append(metadataVersion) |
||||
.append(" from bootstrap source '") |
||||
.append(bootstrapMetadata.source()) |
||||
.append("'. "); |
||||
} |
||||
|
||||
// If no records have been replayed, we need to write out the bootstrap records.
|
||||
// This will include the new metadata.version, as well as things like SCRAM
|
||||
// initialization, etc.
|
||||
records.addAll(bootstrapMetadata.records()); |
||||
|
||||
if (metadataVersion.isMigrationSupported()) { |
||||
if (zkMigrationEnabled) { |
||||
logMessageBuilder.append("Putting the controller into pre-migration mode. No metadata updates " + |
||||
"will be allowed until the ZK metadata has been migrated. "); |
||||
records.add(ZkMigrationState.PRE_MIGRATION.toRecord()); |
||||
} else { |
||||
logMessageBuilder.append("Setting the ZK migration state to NONE since this is a de-novo " + |
||||
"KRaft cluster. "); |
||||
records.add(ZkMigrationState.NONE.toRecord()); |
||||
} |
||||
} else { |
||||
if (zkMigrationEnabled) { |
||||
throw new RuntimeException("The bootstrap metadata.version " + bootstrapMetadata.metadataVersion() + |
||||
" does not support ZK migrations. Cannot continue with ZK migrations enabled."); |
||||
} |
||||
} |
||||
|
||||
activationMessageConsumer.accept(logMessageBuilder.toString().trim()); |
||||
if (metadataVersion.isMetadataTransactionSupported()) { |
||||
records.add(new ApiMessageAndVersion(new EndTransactionRecord(), (short) 0)); |
||||
return ControllerResult.of(records, null); |
||||
} else { |
||||
return ControllerResult.atomicOf(records, null); |
||||
} |
||||
} |
||||
|
||||
static ControllerResult<Void> recordsForNonEmptyLog( |
||||
Consumer<String> activationMessageConsumer, |
||||
long transactionStartOffset, |
||||
boolean zkMigrationEnabled, |
||||
FeatureControlManager featureControl, |
||||
MetadataVersion metadataVersion |
||||
) { |
||||
StringBuilder logMessageBuilder = new StringBuilder("Performing controller activation. "); |
||||
|
||||
// Logs have been replayed. We need to initialize some things here if upgrading from older KRaft versions
|
||||
List<ApiMessageAndVersion> records = new ArrayList<>(); |
||||
|
||||
// Check for in-flight transaction
|
||||
if (transactionStartOffset != -1L) { |
||||
if (!metadataVersion.isMetadataTransactionSupported()) { |
||||
throw new RuntimeException("Detected in-progress transaction at offset " + transactionStartOffset + |
||||
", but the metadata.version " + metadataVersion + |
||||
" does not support transactions. Cannot continue."); |
||||
} else { |
||||
logMessageBuilder |
||||
.append("Aborting in-progress metadata transaction at offset ") |
||||
.append(transactionStartOffset) |
||||
.append(". "); |
||||
records.add(new ApiMessageAndVersion( |
||||
new AbortTransactionRecord().setReason("Controller failover"), (short) 0)); |
||||
} |
||||
} |
||||
|
||||
if (metadataVersion.equals(MetadataVersion.MINIMUM_KRAFT_VERSION)) { |
||||
logMessageBuilder.append("No metadata.version feature level record was found in the log. ") |
||||
.append("Treating the log as version ") |
||||
.append(MetadataVersion.MINIMUM_KRAFT_VERSION) |
||||
.append(". "); |
||||
} |
||||
|
||||
if (zkMigrationEnabled && !metadataVersion.isMigrationSupported()) { |
||||
throw new RuntimeException("Should not have ZK migrations enabled on a cluster running " + |
||||
"metadata.version " + featureControl.metadataVersion()); |
||||
} else if (metadataVersion.isMigrationSupported()) { |
||||
logMessageBuilder |
||||
.append("Loaded ZK migration state of ") |
||||
.append(featureControl.zkMigrationState()) |
||||
.append(". "); |
||||
switch (featureControl.zkMigrationState()) { |
||||
case NONE: |
||||
// Since this is the default state there may or may not be an actual NONE in the log. Regardless,
|
||||
// it will eventually be persisted in a snapshot, so we don't need to explicitly write it here.
|
||||
if (zkMigrationEnabled) { |
||||
throw new RuntimeException("Should not have ZK migrations enabled on a cluster that was " + |
||||
"created in KRaft mode."); |
||||
} |
||||
break; |
||||
case PRE_MIGRATION: |
||||
if (!metadataVersion.isMetadataTransactionSupported()) { |
||||
logMessageBuilder |
||||
.append("Activating pre-migration controller without empty log. ") |
||||
.append("There may be a partial migration. "); |
||||
} |
||||
break; |
||||
case MIGRATION: |
||||
if (!zkMigrationEnabled) { |
||||
// This can happen if controller leadership transfers to a controller with migrations enabled
|
||||
// after another controller had finalized the migration. For example, during a rolling restart
|
||||
// of the controller quorum during which the migration config is being set to false.
|
||||
logMessageBuilder |
||||
.append("Completing the ZK migration since this controller was configured with ") |
||||
.append("'zookeeper.metadata.migration.enable' set to 'false'. "); |
||||
records.add(ZkMigrationState.POST_MIGRATION.toRecord()); |
||||
} else { |
||||
logMessageBuilder |
||||
.append("Staying in ZK migration mode since 'zookeeper.metadata.migration.enable' ") |
||||
.append("is still 'true'. "); |
||||
} |
||||
break; |
||||
case POST_MIGRATION: |
||||
if (zkMigrationEnabled) { |
||||
logMessageBuilder |
||||
.append("Ignoring 'zookeeper.metadata.migration.enable' value of 'true' since ") |
||||
.append("the ZK migration has been completed. "); |
||||
} |
||||
break; |
||||
default: |
||||
throw new IllegalStateException("Unsupported ZkMigrationState " + featureControl.zkMigrationState()); |
||||
} |
||||
} |
||||
|
||||
activationMessageConsumer.accept(logMessageBuilder.toString().trim()); |
||||
return ControllerResult.atomicOf(records, null); |
||||
} |
||||
|
||||
/** |
||||
* Generate the set of activation records. |
||||
* </p> |
||||
* If the log is empty, write the bootstrap records. If the log is not empty, do some validation and |
||||
* possibly write some records to put the log into a valid state. For bootstrap records, if KIP-868 |
||||
* metadata transactions are supported, ues them. Otherwise, write the bootstrap records as an |
||||
* atomic batch. The single atomic batch can be problematic if the bootstrap records are too large |
||||
* (e.g., lots of SCRAM credentials). If ZK migrations are enabled, the activation records will |
||||
* include a ZkMigrationState record regardless of whether the log was empty or not. |
||||
*/ |
||||
static ControllerResult<Void> generate( |
||||
Consumer<String> activationMessageConsumer, |
||||
boolean isEmpty, |
||||
long transactionStartOffset, |
||||
boolean zkMigrationEnabled, |
||||
BootstrapMetadata bootstrapMetadata, |
||||
FeatureControlManager featureControl |
||||
) { |
||||
if (isEmpty) { |
||||
return recordsForEmptyLog(activationMessageConsumer, transactionStartOffset, zkMigrationEnabled, |
||||
bootstrapMetadata, bootstrapMetadata.metadataVersion()); |
||||
} else { |
||||
return recordsForNonEmptyLog(activationMessageConsumer, transactionStartOffset, zkMigrationEnabled, |
||||
featureControl, featureControl.metadataVersion()); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,262 @@
@@ -0,0 +1,262 @@
|
||||
/* |
||||
* 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.image.loader; |
||||
|
||||
import org.apache.kafka.common.metadata.MetadataRecordType; |
||||
import org.apache.kafka.common.utils.LogContext; |
||||
import org.apache.kafka.common.utils.Time; |
||||
import org.apache.kafka.image.MetadataDelta; |
||||
import org.apache.kafka.image.MetadataImage; |
||||
import org.apache.kafka.image.MetadataProvenance; |
||||
import org.apache.kafka.raft.Batch; |
||||
import org.apache.kafka.raft.LeaderAndEpoch; |
||||
import org.apache.kafka.server.common.ApiMessageAndVersion; |
||||
import org.apache.kafka.server.fault.FaultHandler; |
||||
import org.slf4j.Logger; |
||||
|
||||
import static java.util.concurrent.TimeUnit.NANOSECONDS; |
||||
|
||||
/** |
||||
* Loads batches of metadata updates from Raft commits into MetadataDelta-s. Multiple batches from a commit |
||||
* are buffered into a MetadataDelta to achieve batching of records and reduce the number of times |
||||
* MetadataPublishers must be updated. This class also supports metadata transactions (KIP-866). |
||||
* |
||||
* |
||||
*/ |
||||
public class MetadataBatchLoader { |
||||
|
||||
enum TransactionState { |
||||
NO_TRANSACTION, |
||||
STARTED_TRANSACTION, |
||||
CONTINUED_TRANSACTION, |
||||
ENDED_TRANSACTION, |
||||
ABORTED_TRANSACTION; |
||||
} |
||||
|
||||
@FunctionalInterface |
||||
public interface MetadataUpdater { |
||||
void update(MetadataDelta delta, MetadataImage image, LogDeltaManifest manifest); |
||||
} |
||||
|
||||
private final Logger log; |
||||
private final Time time; |
||||
private final FaultHandler faultHandler; |
||||
private final MetadataUpdater callback; |
||||
|
||||
private MetadataImage image; |
||||
private MetadataDelta delta; |
||||
private long lastOffset; |
||||
private int lastEpoch; |
||||
private long lastContainedLogTimeMs; |
||||
private long numBytes; |
||||
private int numBatches; |
||||
private long totalBatchElapsedNs; |
||||
private TransactionState transactionState; |
||||
|
||||
public MetadataBatchLoader( |
||||
LogContext logContext, |
||||
Time time, |
||||
FaultHandler faultHandler, |
||||
MetadataUpdater callback |
||||
) { |
||||
this.log = logContext.logger(MetadataBatchLoader.class); |
||||
this.time = time; |
||||
this.faultHandler = faultHandler; |
||||
this.callback = callback; |
||||
} |
||||
|
||||
/** |
||||
* Reset the state of this batch loader to the given image. Any un-flushed state will be |
||||
* discarded. |
||||
* |
||||
* @param image Metadata image to reset this batch loader's state to. |
||||
*/ |
||||
public void resetToImage(MetadataImage image) { |
||||
this.image = image; |
||||
this.delta = new MetadataDelta.Builder().setImage(image).build(); |
||||
this.transactionState = TransactionState.NO_TRANSACTION; |
||||
this.lastOffset = image.provenance().lastContainedOffset(); |
||||
this.lastEpoch = image.provenance().lastContainedEpoch(); |
||||
this.lastContainedLogTimeMs = image.provenance().lastContainedLogTimeMs(); |
||||
this.numBytes = 0; |
||||
this.numBatches = 0; |
||||
this.totalBatchElapsedNs = 0; |
||||
} |
||||
|
||||
/** |
||||
* Load a batch of records from the log. We have to do some bookkeeping here to |
||||
* translate between batch offsets and record offsets, and track the number of bytes we |
||||
* have read. Additionally, there is the chance that one of the records is a metadata |
||||
* version change which needs to be handled differently. |
||||
* </p> |
||||
* If this batch starts a transaction, any records preceding the transaction in this |
||||
* batch will be implicitly added to the transaction. |
||||
* |
||||
* @param batch The reader which yields the batches. |
||||
* @return The time in nanoseconds that elapsed while loading this batch |
||||
*/ |
||||
|
||||
public long loadBatch(Batch<ApiMessageAndVersion> batch, LeaderAndEpoch leaderAndEpoch) { |
||||
long startNs = time.nanoseconds(); |
||||
int indexWithinBatch = 0; |
||||
|
||||
lastContainedLogTimeMs = batch.appendTimestamp(); |
||||
lastEpoch = batch.epoch(); |
||||
|
||||
for (ApiMessageAndVersion record : batch.records()) { |
||||
try { |
||||
replay(record); |
||||
} catch (Throwable e) { |
||||
faultHandler.handleFault("Error loading metadata log record from offset " + |
||||
batch.baseOffset() + indexWithinBatch, e); |
||||
} |
||||
|
||||
// Emit the accumulated delta if a new transaction has been started and one of the following is true
|
||||
// 1) this is not the first record in this batch
|
||||
// 2) this is not the first batch since last emitting a delta
|
||||
if (transactionState == TransactionState.STARTED_TRANSACTION && (indexWithinBatch > 0 || numBatches > 0)) { |
||||
MetadataProvenance provenance = new MetadataProvenance(lastOffset, lastEpoch, lastContainedLogTimeMs); |
||||
LogDeltaManifest manifest = LogDeltaManifest.newBuilder() |
||||
.provenance(provenance) |
||||
.leaderAndEpoch(leaderAndEpoch) |
||||
.numBatches(numBatches) // This will be zero if we have not yet read a batch
|
||||
.elapsedNs(totalBatchElapsedNs) |
||||
.numBytes(numBytes) // This will be zero if we have not yet read a batch
|
||||
.build(); |
||||
if (log.isDebugEnabled()) { |
||||
log.debug("handleCommit: Generated a metadata delta between {} and {} from {} batch(es) in {} us.", |
||||
image.offset(), manifest.provenance().lastContainedOffset(), |
||||
manifest.numBatches(), NANOSECONDS.toMicros(manifest.elapsedNs())); |
||||
} |
||||
applyDeltaAndUpdate(delta, manifest); |
||||
transactionState = TransactionState.STARTED_TRANSACTION; |
||||
} |
||||
|
||||
lastOffset = batch.baseOffset() + indexWithinBatch; |
||||
indexWithinBatch++; |
||||
} |
||||
long elapsedNs = time.nanoseconds() - startNs; |
||||
|
||||
// Update state for the manifest. The actual byte count will only be included in the last delta emitted for
|
||||
// a given batch or transaction.
|
||||
lastOffset = batch.lastOffset(); |
||||
numBytes += batch.sizeInBytes(); |
||||
numBatches += 1; |
||||
totalBatchElapsedNs += elapsedNs; |
||||
return totalBatchElapsedNs; |
||||
} |
||||
|
||||
/** |
||||
* Flush the metadata accumulated in this batch loader if not in the middle of a transaction. The |
||||
* flushed metadata will be passed to the {@link MetadataUpdater} configured for this class. |
||||
*/ |
||||
public void maybeFlushBatches(LeaderAndEpoch leaderAndEpoch) { |
||||
MetadataProvenance provenance = new MetadataProvenance(lastOffset, lastEpoch, lastContainedLogTimeMs); |
||||
LogDeltaManifest manifest = LogDeltaManifest.newBuilder() |
||||
.provenance(provenance) |
||||
.leaderAndEpoch(leaderAndEpoch) |
||||
.numBatches(numBatches) |
||||
.elapsedNs(totalBatchElapsedNs) |
||||
.numBytes(numBytes) |
||||
.build(); |
||||
switch (transactionState) { |
||||
case STARTED_TRANSACTION: |
||||
case CONTINUED_TRANSACTION: |
||||
log.debug("handleCommit: not publishing since a transaction starting at {} is still in progress. " + |
||||
"{} batch(es) processed so far.", image.offset(), numBatches); |
||||
break; |
||||
case ABORTED_TRANSACTION: |
||||
log.debug("handleCommit: publishing empty delta between {} and {} from {} batch(es) " + |
||||
"since a transaction was aborted", image.offset(), manifest.provenance().lastContainedOffset(), |
||||
manifest.numBatches()); |
||||
applyDeltaAndUpdate(new MetadataDelta.Builder().setImage(image).build(), manifest); |
||||
break; |
||||
case ENDED_TRANSACTION: |
||||
case NO_TRANSACTION: |
||||
if (log.isDebugEnabled()) { |
||||
log.debug("handleCommit: Generated a metadata delta between {} and {} from {} batch(es) in {} us.", |
||||
image.offset(), manifest.provenance().lastContainedOffset(), |
||||
manifest.numBatches(), NANOSECONDS.toMicros(manifest.elapsedNs())); |
||||
} |
||||
applyDeltaAndUpdate(delta, manifest); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
private void replay(ApiMessageAndVersion record) { |
||||
MetadataRecordType type = MetadataRecordType.fromId(record.message().apiKey()); |
||||
switch (type) { |
||||
case BEGIN_TRANSACTION_RECORD: |
||||
if (transactionState == TransactionState.STARTED_TRANSACTION || |
||||
transactionState == TransactionState.CONTINUED_TRANSACTION) { |
||||
throw new RuntimeException("Encountered BeginTransactionRecord while already in a transaction"); |
||||
} else { |
||||
transactionState = TransactionState.STARTED_TRANSACTION; |
||||
} |
||||
break; |
||||
case END_TRANSACTION_RECORD: |
||||
if (transactionState == TransactionState.CONTINUED_TRANSACTION || |
||||
transactionState == TransactionState.STARTED_TRANSACTION) { |
||||
transactionState = TransactionState.ENDED_TRANSACTION; |
||||
} else { |
||||
throw new RuntimeException("Encountered EndTransactionRecord without having seen a BeginTransactionRecord"); |
||||
} |
||||
break; |
||||
case ABORT_TRANSACTION_RECORD: |
||||
if (transactionState == TransactionState.CONTINUED_TRANSACTION || |
||||
transactionState == TransactionState.STARTED_TRANSACTION) { |
||||
transactionState = TransactionState.ABORTED_TRANSACTION; |
||||
} else { |
||||
throw new RuntimeException("Encountered AbortTransactionRecord without having seen a BeginTransactionRecord"); |
||||
} |
||||
break; |
||||
default: |
||||
switch (transactionState) { |
||||
case STARTED_TRANSACTION: |
||||
// If we see a non-transaction record after starting a transaction, transition to CONTINUED_TRANSACTION
|
||||
transactionState = TransactionState.CONTINUED_TRANSACTION; |
||||
break; |
||||
case ENDED_TRANSACTION: |
||||
case ABORTED_TRANSACTION: |
||||
// If we see a non-transaction record after ending a transaction, transition back to NO_TRANSACTION
|
||||
transactionState = TransactionState.NO_TRANSACTION; |
||||
break; |
||||
case CONTINUED_TRANSACTION: |
||||
case NO_TRANSACTION: |
||||
default: |
||||
break; |
||||
} |
||||
delta.replay(record.message()); |
||||
} |
||||
} |
||||
|
||||
private void applyDeltaAndUpdate(MetadataDelta delta, LogDeltaManifest manifest) { |
||||
try { |
||||
image = delta.apply(manifest.provenance()); |
||||
} catch (Throwable e) { |
||||
faultHandler.handleFault("Error generating new metadata image from " + |
||||
"metadata delta between offset " + image.offset() + |
||||
" and " + manifest.provenance().lastContainedOffset(), e); |
||||
} |
||||
|
||||
// Whether we can apply the delta or not, we need to make sure the batch loader gets reset
|
||||
// to the image known to MetadataLoader
|
||||
callback.update(delta, image, manifest); |
||||
resetToImage(image); |
||||
} |
||||
} |
@ -0,0 +1,361 @@
@@ -0,0 +1,361 @@
|
||||
/* |
||||
* 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.controller; |
||||
|
||||
import org.apache.kafka.common.metadata.ZkMigrationStateRecord; |
||||
import org.apache.kafka.metadata.bootstrap.BootstrapMetadata; |
||||
import org.apache.kafka.metadata.migration.ZkMigrationState; |
||||
import org.apache.kafka.server.common.MetadataVersion; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import java.util.Optional; |
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals; |
||||
import static org.junit.jupiter.api.Assertions.assertFalse; |
||||
import static org.junit.jupiter.api.Assertions.assertThrows; |
||||
import static org.junit.jupiter.api.Assertions.assertTrue; |
||||
import static org.junit.jupiter.api.Assertions.fail; |
||||
|
||||
/** |
||||
* This class is for testing the log message or exception produced by ActivationRecordsGenerator. For tests that |
||||
* verify the semantics of the returned records, see QuorumControllerTest. |
||||
*/ |
||||
public class ActivationRecordsGeneratorTest { |
||||
|
||||
@Test |
||||
public void testActivationMessageForEmptyLog() { |
||||
ControllerResult<Void> result; |
||||
result = ActivationRecordsGenerator.recordsForEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. The metadata log appears to be empty. " + |
||||
"Appending 1 bootstrap record(s) at metadata.version 3.0-IV1 from bootstrap source 'test'.", logMsg), |
||||
-1L, |
||||
false, |
||||
BootstrapMetadata.fromVersion(MetadataVersion.MINIMUM_BOOTSTRAP_VERSION, "test"), |
||||
MetadataVersion.MINIMUM_KRAFT_VERSION |
||||
); |
||||
assertTrue(result.isAtomic()); |
||||
assertEquals(1, result.records().size()); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. The metadata log appears to be empty. " + |
||||
"Appending 1 bootstrap record(s) at metadata.version 3.4-IV0 from bootstrap " + |
||||
"source 'test'. Setting the ZK migration state to NONE since this is a de-novo KRaft cluster.", logMsg), |
||||
-1L, |
||||
false, |
||||
BootstrapMetadata.fromVersion(MetadataVersion.IBP_3_4_IV0, "test"), |
||||
MetadataVersion.IBP_3_4_IV0 |
||||
); |
||||
assertTrue(result.isAtomic()); |
||||
assertEquals(2, result.records().size()); |
||||
|
||||
|
||||
result = ActivationRecordsGenerator.recordsForEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. The metadata log appears to be empty. " + |
||||
"Appending 1 bootstrap record(s) at metadata.version 3.4-IV0 from bootstrap " + |
||||
"source 'test'. Putting the controller into pre-migration mode. No metadata updates will be allowed " + |
||||
"until the ZK metadata has been migrated.", logMsg), |
||||
-1L, |
||||
true, |
||||
BootstrapMetadata.fromVersion(MetadataVersion.IBP_3_4_IV0, "test"), |
||||
MetadataVersion.IBP_3_4_IV0 |
||||
); |
||||
assertTrue(result.isAtomic()); |
||||
assertEquals(2, result.records().size()); |
||||
|
||||
assertEquals( |
||||
"The bootstrap metadata.version 3.3-IV2 does not support ZK migrations. Cannot continue with ZK migrations enabled.", |
||||
assertThrows(RuntimeException.class, () -> |
||||
ActivationRecordsGenerator.recordsForEmptyLog( |
||||
logMsg -> fail(), |
||||
-1L, |
||||
true, |
||||
BootstrapMetadata.fromVersion(MetadataVersion.IBP_3_3_IV2, "test"), |
||||
MetadataVersion.IBP_3_3_IV2 |
||||
)).getMessage() |
||||
); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. The metadata log appears to be empty. " + |
||||
"Appending 1 bootstrap record(s) in metadata transaction at metadata.version 3.6-IV1 from bootstrap " + |
||||
"source 'test'. Setting the ZK migration state to NONE since this is a de-novo KRaft cluster.", logMsg), |
||||
-1L, |
||||
false, |
||||
BootstrapMetadata.fromVersion(MetadataVersion.IBP_3_6_IV1, "test"), |
||||
MetadataVersion.IBP_3_6_IV1 |
||||
); |
||||
assertFalse(result.isAtomic()); |
||||
assertEquals(4, result.records().size()); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. The metadata log appears to be empty. " + |
||||
"Appending 1 bootstrap record(s) in metadata transaction at metadata.version 3.6-IV1 from bootstrap " + |
||||
"source 'test'. Putting the controller into pre-migration mode. No metadata updates will be allowed " + |
||||
"until the ZK metadata has been migrated.", logMsg), |
||||
-1L, |
||||
true, |
||||
BootstrapMetadata.fromVersion(MetadataVersion.IBP_3_6_IV1, "test"), |
||||
MetadataVersion.IBP_3_6_IV1 |
||||
); |
||||
assertFalse(result.isAtomic()); |
||||
assertEquals(4, result.records().size()); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. Aborting partial bootstrap records " + |
||||
"transaction at offset 0. Re-appending 1 bootstrap record(s) in new metadata transaction at " + |
||||
"metadata.version 3.6-IV1 from bootstrap source 'test'. Setting the ZK migration state to NONE " + |
||||
"since this is a de-novo KRaft cluster.", logMsg), |
||||
0L, |
||||
false, |
||||
BootstrapMetadata.fromVersion(MetadataVersion.IBP_3_6_IV1, "test"), |
||||
MetadataVersion.IBP_3_6_IV1 |
||||
); |
||||
assertFalse(result.isAtomic()); |
||||
assertEquals(5, result.records().size()); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. Aborting partial bootstrap records " + |
||||
"transaction at offset 0. Re-appending 1 bootstrap record(s) in new metadata transaction at " + |
||||
"metadata.version 3.6-IV1 from bootstrap source 'test'. Putting the controller into pre-migration " + |
||||
"mode. No metadata updates will be allowed until the ZK metadata has been migrated.", logMsg), |
||||
0L, |
||||
true, |
||||
BootstrapMetadata.fromVersion(MetadataVersion.IBP_3_6_IV1, "test"), |
||||
MetadataVersion.IBP_3_6_IV1 |
||||
); |
||||
assertFalse(result.isAtomic()); |
||||
assertEquals(5, result.records().size()); |
||||
|
||||
assertEquals( |
||||
"Detected partial bootstrap records transaction at 0, but the metadata.version 3.6-IV0 does not " + |
||||
"support transactions. Cannot continue.", |
||||
assertThrows(RuntimeException.class, () -> |
||||
ActivationRecordsGenerator.recordsForEmptyLog( |
||||
logMsg -> assertEquals("", logMsg), |
||||
0L, |
||||
true, |
||||
BootstrapMetadata.fromVersion(MetadataVersion.IBP_3_6_IV0, "test"), |
||||
MetadataVersion.IBP_3_6_IV0 |
||||
)).getMessage() |
||||
); |
||||
} |
||||
|
||||
FeatureControlManager buildFeatureControl( |
||||
MetadataVersion metadataVersion, |
||||
Optional<ZkMigrationState> zkMigrationState |
||||
) { |
||||
FeatureControlManager featureControl = new FeatureControlManager.Builder() |
||||
.setMetadataVersion(metadataVersion).build(); |
||||
zkMigrationState.ifPresent(migrationState -> |
||||
featureControl.replay((ZkMigrationStateRecord) migrationState.toRecord().message())); |
||||
return featureControl; |
||||
} |
||||
|
||||
@Test |
||||
public void testActivationMessageForNonEmptyLogNoMigrations() { |
||||
ControllerResult<Void> result; |
||||
|
||||
result = ActivationRecordsGenerator.recordsForNonEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. No metadata.version feature level " + |
||||
"record was found in the log. Treating the log as version 3.0-IV1.", logMsg), |
||||
-1L, |
||||
false, |
||||
buildFeatureControl(MetadataVersion.MINIMUM_KRAFT_VERSION, Optional.empty()), |
||||
MetadataVersion.MINIMUM_KRAFT_VERSION |
||||
); |
||||
assertTrue(result.isAtomic()); |
||||
assertEquals(0, result.records().size()); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForNonEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation.", logMsg), |
||||
-1L, |
||||
false, |
||||
buildFeatureControl(MetadataVersion.IBP_3_3_IV0, Optional.empty()), |
||||
MetadataVersion.IBP_3_3_IV0 |
||||
); |
||||
assertTrue(result.isAtomic()); |
||||
assertEquals(0, result.records().size()); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForNonEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. Loaded ZK migration state of NONE.", logMsg), |
||||
-1L, |
||||
false, |
||||
buildFeatureControl(MetadataVersion.IBP_3_4_IV0, Optional.empty()), |
||||
MetadataVersion.IBP_3_4_IV0 |
||||
); |
||||
assertTrue(result.isAtomic()); |
||||
assertEquals(0, result.records().size()); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForNonEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. Aborting in-progress metadata " + |
||||
"transaction at offset 42. Loaded ZK migration state of NONE.", logMsg), |
||||
42L, |
||||
false, |
||||
buildFeatureControl(MetadataVersion.IBP_3_6_IV1, Optional.empty()), |
||||
MetadataVersion.IBP_3_6_IV1 |
||||
); |
||||
assertTrue(result.isAtomic()); |
||||
assertEquals(1, result.records().size()); |
||||
|
||||
assertEquals( |
||||
"Detected in-progress transaction at offset 42, but the metadata.version 3.6-IV0 does not support " + |
||||
"transactions. Cannot continue.", |
||||
assertThrows(RuntimeException.class, () -> |
||||
ActivationRecordsGenerator.recordsForNonEmptyLog( |
||||
logMsg -> fail(), |
||||
42L, |
||||
false, |
||||
buildFeatureControl(MetadataVersion.IBP_3_6_IV0, Optional.empty()), |
||||
MetadataVersion.IBP_3_6_IV0 |
||||
)).getMessage() |
||||
); |
||||
} |
||||
|
||||
@Test |
||||
public void testActivationMessageForNonEmptyLogWithMigrations() { |
||||
ControllerResult<Void> result; |
||||
|
||||
assertEquals( |
||||
"Should not have ZK migrations enabled on a cluster running metadata.version 3.3-IV0", |
||||
assertThrows(RuntimeException.class, () -> |
||||
ActivationRecordsGenerator.recordsForNonEmptyLog( |
||||
logMsg -> fail(), |
||||
-1L, |
||||
true, |
||||
buildFeatureControl(MetadataVersion.IBP_3_3_IV0, Optional.empty()), |
||||
MetadataVersion.IBP_3_3_IV0 |
||||
)).getMessage() |
||||
); |
||||
|
||||
assertEquals( |
||||
"Should not have ZK migrations enabled on a cluster that was created in KRaft mode.", |
||||
assertThrows(RuntimeException.class, () -> { |
||||
ActivationRecordsGenerator.recordsForNonEmptyLog( |
||||
logMsg -> fail(), |
||||
-1L, |
||||
true, |
||||
buildFeatureControl(MetadataVersion.IBP_3_4_IV0, Optional.empty()), |
||||
MetadataVersion.IBP_3_4_IV0 |
||||
); |
||||
}).getMessage() |
||||
); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForNonEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. Loaded ZK migration state of " + |
||||
"PRE_MIGRATION. Activating pre-migration controller without empty log. There may be a partial " + |
||||
"migration.", logMsg), |
||||
-1L, |
||||
true, |
||||
buildFeatureControl(MetadataVersion.IBP_3_4_IV0, Optional.of(ZkMigrationState.PRE_MIGRATION)), |
||||
MetadataVersion.IBP_3_4_IV0 |
||||
); |
||||
assertTrue(result.isAtomic()); |
||||
assertEquals(0, result.records().size()); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForNonEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. Loaded ZK migration state of " + |
||||
"PRE_MIGRATION.", logMsg), |
||||
-1L, |
||||
true, |
||||
buildFeatureControl(MetadataVersion.IBP_3_6_IV1, Optional.of(ZkMigrationState.PRE_MIGRATION)), |
||||
MetadataVersion.IBP_3_6_IV1 |
||||
); |
||||
assertTrue(result.isAtomic()); |
||||
assertEquals(0, result.records().size()); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForNonEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. Loaded ZK migration state of MIGRATION. " + |
||||
"Staying in ZK migration mode since 'zookeeper.metadata.migration.enable' is still 'true'.", logMsg), |
||||
-1L, |
||||
true, |
||||
buildFeatureControl(MetadataVersion.IBP_3_4_IV0, Optional.of(ZkMigrationState.MIGRATION)), |
||||
MetadataVersion.IBP_3_4_IV0 |
||||
); |
||||
assertTrue(result.isAtomic()); |
||||
assertEquals(0, result.records().size()); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForNonEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. Loaded ZK migration state of MIGRATION. " + |
||||
"Completing the ZK migration since this controller was configured with " + |
||||
"'zookeeper.metadata.migration.enable' set to 'false'.", logMsg), |
||||
-1L, |
||||
false, |
||||
buildFeatureControl(MetadataVersion.IBP_3_4_IV0, Optional.of(ZkMigrationState.MIGRATION)), |
||||
MetadataVersion.IBP_3_4_IV0 |
||||
); |
||||
assertTrue(result.isAtomic()); |
||||
assertEquals(1, result.records().size()); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForNonEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. Aborting in-progress metadata " + |
||||
"transaction at offset 42. Loaded ZK migration state of MIGRATION. Completing the ZK migration " + |
||||
"since this controller was configured with 'zookeeper.metadata.migration.enable' set to 'false'.", logMsg), |
||||
42L, |
||||
false, |
||||
buildFeatureControl(MetadataVersion.IBP_3_6_IV1, Optional.of(ZkMigrationState.MIGRATION)), |
||||
MetadataVersion.IBP_3_6_IV1 |
||||
); |
||||
assertTrue(result.isAtomic()); |
||||
assertEquals(2, result.records().size()); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForNonEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. Loaded ZK migration state of " + |
||||
"POST_MIGRATION.", logMsg), |
||||
-1L, |
||||
false, |
||||
buildFeatureControl(MetadataVersion.IBP_3_4_IV0, Optional.of(ZkMigrationState.POST_MIGRATION)), |
||||
MetadataVersion.IBP_3_4_IV0 |
||||
); |
||||
assertTrue(result.isAtomic()); |
||||
assertEquals(0, result.records().size()); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForNonEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. Aborting in-progress metadata " + |
||||
"transaction at offset 42. Loaded ZK migration state of POST_MIGRATION.", logMsg), |
||||
42L, |
||||
false, |
||||
buildFeatureControl(MetadataVersion.IBP_3_6_IV1, Optional.of(ZkMigrationState.POST_MIGRATION)), |
||||
MetadataVersion.IBP_3_6_IV1 |
||||
); |
||||
assertTrue(result.isAtomic()); |
||||
assertEquals(1, result.records().size()); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForNonEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. Loaded ZK migration state of " + |
||||
"POST_MIGRATION. Ignoring 'zookeeper.metadata.migration.enable' value of 'true' since the " + |
||||
"ZK migration has been completed.", logMsg), |
||||
-1L, |
||||
true, |
||||
buildFeatureControl(MetadataVersion.IBP_3_4_IV0, Optional.of(ZkMigrationState.POST_MIGRATION)), |
||||
MetadataVersion.IBP_3_4_IV0 |
||||
); |
||||
assertTrue(result.isAtomic()); |
||||
assertEquals(0, result.records().size()); |
||||
|
||||
result = ActivationRecordsGenerator.recordsForNonEmptyLog( |
||||
logMsg -> assertEquals("Performing controller activation. Aborting in-progress metadata " + |
||||
"transaction at offset 42. Loaded ZK migration state of POST_MIGRATION. Ignoring " + |
||||
"'zookeeper.metadata.migration.enable' value of 'true' since the ZK migration has been completed.", logMsg), |
||||
42L, |
||||
true, |
||||
buildFeatureControl(MetadataVersion.IBP_3_6_IV1, Optional.of(ZkMigrationState.POST_MIGRATION)), |
||||
MetadataVersion.IBP_3_6_IV1 |
||||
); |
||||
assertTrue(result.isAtomic()); |
||||
assertEquals(1, result.records().size()); |
||||
} |
||||
} |
@ -0,0 +1,444 @@
@@ -0,0 +1,444 @@
|
||||
/* |
||||
* 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.image.loader; |
||||
|
||||
import org.apache.kafka.common.Uuid; |
||||
import org.apache.kafka.common.metadata.AbortTransactionRecord; |
||||
import org.apache.kafka.common.metadata.BeginTransactionRecord; |
||||
import org.apache.kafka.common.metadata.EndTransactionRecord; |
||||
import org.apache.kafka.common.metadata.NoOpRecord; |
||||
import org.apache.kafka.common.metadata.PartitionRecord; |
||||
import org.apache.kafka.common.metadata.TopicRecord; |
||||
import org.apache.kafka.common.utils.LogContext; |
||||
import org.apache.kafka.common.utils.MockTime; |
||||
import org.apache.kafka.image.MetadataDelta; |
||||
import org.apache.kafka.image.MetadataImage; |
||||
import org.apache.kafka.raft.Batch; |
||||
import org.apache.kafka.raft.LeaderAndEpoch; |
||||
import org.apache.kafka.server.common.ApiMessageAndVersion; |
||||
import org.apache.kafka.server.fault.MockFaultHandler; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.junit.jupiter.params.ParameterizedTest; |
||||
import org.junit.jupiter.params.provider.ValueSource; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.OptionalInt; |
||||
import java.util.stream.Collectors; |
||||
import java.util.stream.IntStream; |
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals; |
||||
import static org.junit.jupiter.api.Assertions.assertNotNull; |
||||
import static org.junit.jupiter.api.Assertions.assertNull; |
||||
|
||||
public class MetadataBatchLoaderTest { |
||||
|
||||
static final Uuid TOPIC_FOO = Uuid.fromString("c6uHMgPkRp2Urjlh-RxMNQ"); |
||||
static final Uuid TOPIC_BAR = Uuid.fromString("tUWOOPvzQhmZZ_eXmTCcig"); |
||||
static final List<ApiMessageAndVersion> TOPIC_TXN_BATCH_1; |
||||
static final List<ApiMessageAndVersion> TOPIC_TXN_BATCH_2; |
||||
static final List<ApiMessageAndVersion> TOPIC_NO_TXN_BATCH; |
||||
static final List<ApiMessageAndVersion> TXN_BEGIN_SINGLETON; |
||||
static final List<ApiMessageAndVersion> TXN_END_SINGLETON; |
||||
static final List<ApiMessageAndVersion> TXN_ABORT_SINGLETON; |
||||
static final LeaderAndEpoch LEADER_AND_EPOCH = new LeaderAndEpoch(OptionalInt.of(1), 42); |
||||
|
||||
static { |
||||
{ |
||||
TOPIC_TXN_BATCH_1 = Arrays.asList( |
||||
new ApiMessageAndVersion(new BeginTransactionRecord().setName("txn-1"), (short) 0), |
||||
new ApiMessageAndVersion(new TopicRecord() |
||||
.setName("foo") |
||||
.setTopicId(TOPIC_FOO), (short) 0), |
||||
new ApiMessageAndVersion(new PartitionRecord() |
||||
.setPartitionId(0) |
||||
.setTopicId(TOPIC_FOO), (short) 0) |
||||
); |
||||
|
||||
TOPIC_TXN_BATCH_2 = Arrays.asList( |
||||
new ApiMessageAndVersion(new PartitionRecord() |
||||
.setPartitionId(1) |
||||
.setTopicId(TOPIC_FOO), (short) 0), |
||||
new ApiMessageAndVersion(new PartitionRecord() |
||||
.setPartitionId(2) |
||||
.setTopicId(TOPIC_FOO), (short) 0), |
||||
new ApiMessageAndVersion(new EndTransactionRecord(), (short) 0) |
||||
); |
||||
|
||||
TOPIC_NO_TXN_BATCH = Arrays.asList( |
||||
new ApiMessageAndVersion(new TopicRecord() |
||||
.setName("bar") |
||||
.setTopicId(TOPIC_BAR), (short) 0), |
||||
new ApiMessageAndVersion(new PartitionRecord() |
||||
.setPartitionId(0) |
||||
.setTopicId(TOPIC_BAR), (short) 0), |
||||
new ApiMessageAndVersion(new PartitionRecord() |
||||
.setPartitionId(1) |
||||
.setTopicId(TOPIC_BAR), (short) 0) |
||||
); |
||||
|
||||
TXN_BEGIN_SINGLETON = Collections.singletonList( |
||||
new ApiMessageAndVersion(new BeginTransactionRecord().setName("txn-1"), (short) 0)); |
||||
|
||||
TXN_END_SINGLETON = Collections.singletonList( |
||||
new ApiMessageAndVersion(new EndTransactionRecord(), (short) 0)); |
||||
|
||||
TXN_ABORT_SINGLETON = Collections.singletonList( |
||||
new ApiMessageAndVersion(new AbortTransactionRecord(), (short) 0)); |
||||
} |
||||
} |
||||
|
||||
static List<ApiMessageAndVersion> noOpRecords(int n) { |
||||
return IntStream.range(0, n) |
||||
.mapToObj(__ -> new ApiMessageAndVersion(new NoOpRecord(), (short) 0)) |
||||
.collect(Collectors.toList()); |
||||
} |
||||
|
||||
|
||||
static class MockMetadataUpdater implements MetadataBatchLoader.MetadataUpdater { |
||||
MetadataImage latestImage = null; |
||||
MetadataDelta latestDelta = null; |
||||
LogDeltaManifest latestManifest = null; |
||||
int updates = 0; |
||||
|
||||
@Override |
||||
public void update(MetadataDelta delta, MetadataImage image, LogDeltaManifest manifest) { |
||||
latestDelta = delta; |
||||
latestImage = image; |
||||
latestManifest = manifest; |
||||
updates++; |
||||
} |
||||
|
||||
public void reset() { |
||||
latestImage = null; |
||||
latestDelta = null; |
||||
latestManifest = null; |
||||
updates = 0; |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void testAlignedTransactionBatches() { |
||||
Batch<ApiMessageAndVersion> batch1 = Batch.data( |
||||
10, 1, 0, 10, TOPIC_TXN_BATCH_1); |
||||
Batch<ApiMessageAndVersion> batch2 = Batch.data( |
||||
13, 2, 0, 10, noOpRecords(3)); |
||||
Batch<ApiMessageAndVersion> batch3 = Batch.data( |
||||
16, 2, 0, 30, TOPIC_TXN_BATCH_2); |
||||
|
||||
MockMetadataUpdater updater = new MockMetadataUpdater(); |
||||
MetadataBatchLoader batchLoader = new MetadataBatchLoader( |
||||
new LogContext(), |
||||
new MockTime(), |
||||
new MockFaultHandler("testAlignedTransactionBatches"), |
||||
updater |
||||
); |
||||
|
||||
batchLoader.resetToImage(MetadataImage.EMPTY); |
||||
batchLoader.loadBatch(batch1, LEADER_AND_EPOCH); |
||||
assertEquals(0, updater.updates); |
||||
batchLoader.loadBatch(batch2, LEADER_AND_EPOCH); |
||||
assertEquals(0, updater.updates); |
||||
batchLoader.loadBatch(batch3, LEADER_AND_EPOCH); |
||||
assertEquals(0, updater.updates); |
||||
batchLoader.maybeFlushBatches(LEADER_AND_EPOCH); |
||||
assertEquals(1, updater.updates); |
||||
assertNotNull(updater.latestImage.topics().getTopic("foo")); |
||||
assertEquals(18, updater.latestImage.provenance().lastContainedOffset()); |
||||
assertEquals(2, updater.latestImage.provenance().lastContainedEpoch()); |
||||
} |
||||
|
||||
@Test |
||||
public void testSingletonBeginAndEnd() { |
||||
Batch<ApiMessageAndVersion> batch1 = Batch.data( |
||||
13, 1, 0, 30, noOpRecords(3)); |
||||
|
||||
Batch<ApiMessageAndVersion> batch2 = Batch.data( |
||||
16, 2, 0, 30, TXN_BEGIN_SINGLETON); |
||||
|
||||
Batch<ApiMessageAndVersion> batch3 = Batch.data( |
||||
17, 3, 0, 10, TOPIC_NO_TXN_BATCH); |
||||
|
||||
Batch<ApiMessageAndVersion> batch4 = Batch.data( |
||||
20, 4, 0, 10, TXN_END_SINGLETON); |
||||
MockMetadataUpdater updater = new MockMetadataUpdater(); |
||||
MetadataBatchLoader batchLoader = new MetadataBatchLoader( |
||||
new LogContext(), |
||||
new MockTime(), |
||||
new MockFaultHandler("testSingletonBeginAndEnd"), |
||||
updater |
||||
); |
||||
|
||||
// All in one commit
|
||||
batchLoader.resetToImage(MetadataImage.EMPTY); |
||||
batchLoader.loadBatch(batch1, LEADER_AND_EPOCH); |
||||
assertEquals(0, updater.updates); |
||||
batchLoader.loadBatch(batch2, LEADER_AND_EPOCH); |
||||
assertEquals(1, updater.updates); |
||||
assertNull(updater.latestImage.topics().getTopic("bar")); |
||||
batchLoader.loadBatch(batch3, LEADER_AND_EPOCH); |
||||
assertEquals(1, updater.updates); |
||||
batchLoader.loadBatch(batch4, LEADER_AND_EPOCH); |
||||
assertEquals(1, updater.updates); |
||||
batchLoader.maybeFlushBatches(LEADER_AND_EPOCH); |
||||
assertNotNull(updater.latestImage.topics().getTopic("bar")); |
||||
assertEquals(20, updater.latestImage.provenance().lastContainedOffset()); |
||||
assertEquals(4, updater.latestImage.provenance().lastContainedEpoch()); |
||||
|
||||
// Each batch in a separate commit
|
||||
updater.reset(); |
||||
batchLoader.resetToImage(MetadataImage.EMPTY); |
||||
batchLoader.loadBatch(batch1, LEADER_AND_EPOCH); |
||||
batchLoader.maybeFlushBatches(LEADER_AND_EPOCH); |
||||
assertEquals(1, updater.updates); |
||||
|
||||
batchLoader.loadBatch(batch2, LEADER_AND_EPOCH); |
||||
batchLoader.maybeFlushBatches(LEADER_AND_EPOCH); |
||||
assertEquals(1, updater.updates); |
||||
|
||||
batchLoader.loadBatch(batch3, LEADER_AND_EPOCH); |
||||
batchLoader.maybeFlushBatches(LEADER_AND_EPOCH); |
||||
assertEquals(1, updater.updates); |
||||
|
||||
batchLoader.loadBatch(batch4, LEADER_AND_EPOCH); |
||||
batchLoader.maybeFlushBatches(LEADER_AND_EPOCH); |
||||
assertEquals(2, updater.updates); |
||||
} |
||||
|
||||
@Test |
||||
public void testUnexpectedBeginTransaction() { |
||||
MockMetadataUpdater updater = new MockMetadataUpdater(); |
||||
MockFaultHandler faultHandler = new MockFaultHandler("testUnexpectedBeginTransaction"); |
||||
MetadataBatchLoader batchLoader = new MetadataBatchLoader( |
||||
new LogContext(), |
||||
new MockTime(), |
||||
faultHandler, |
||||
updater |
||||
); |
||||
|
||||
Batch<ApiMessageAndVersion> batch1 = Batch.data( |
||||
10, 2, 0, 30, TOPIC_TXN_BATCH_1); |
||||
|
||||
Batch<ApiMessageAndVersion> batch2 = Batch.data( |
||||
13, 2, 0, 30, TXN_BEGIN_SINGLETON); |
||||
|
||||
batchLoader.resetToImage(MetadataImage.EMPTY); |
||||
batchLoader.loadBatch(batch1, LEADER_AND_EPOCH); |
||||
assertNull(faultHandler.firstException()); |
||||
batchLoader.loadBatch(batch2, LEADER_AND_EPOCH); |
||||
assertEquals(RuntimeException.class, faultHandler.firstException().getCause().getClass()); |
||||
assertEquals( |
||||
"Encountered BeginTransactionRecord while already in a transaction", |
||||
faultHandler.firstException().getCause().getMessage() |
||||
); |
||||
batchLoader.maybeFlushBatches(LEADER_AND_EPOCH); |
||||
assertEquals(0, updater.updates); |
||||
} |
||||
|
||||
@Test |
||||
public void testUnexpectedEndTransaction() { |
||||
MockMetadataUpdater updater = new MockMetadataUpdater(); |
||||
MockFaultHandler faultHandler = new MockFaultHandler("testUnexpectedAbortTransaction"); |
||||
MetadataBatchLoader batchLoader = new MetadataBatchLoader( |
||||
new LogContext(), |
||||
new MockTime(), |
||||
faultHandler, |
||||
updater |
||||
); |
||||
|
||||
// First batch gets loaded fine
|
||||
Batch<ApiMessageAndVersion> batch1 = Batch.data( |
||||
10, 2, 0, 30, TOPIC_NO_TXN_BATCH); |
||||
|
||||
// Second batch throws an error, but shouldn't interfere with prior batches
|
||||
Batch<ApiMessageAndVersion> batch2 = Batch.data( |
||||
13, 2, 0, 30, TXN_END_SINGLETON); |
||||
|
||||
batchLoader.resetToImage(MetadataImage.EMPTY); |
||||
batchLoader.loadBatch(batch1, LEADER_AND_EPOCH); |
||||
assertNull(faultHandler.firstException()); |
||||
batchLoader.loadBatch(batch2, LEADER_AND_EPOCH); |
||||
assertEquals(RuntimeException.class, faultHandler.firstException().getCause().getClass()); |
||||
assertEquals( |
||||
"Encountered EndTransactionRecord without having seen a BeginTransactionRecord", |
||||
faultHandler.firstException().getCause().getMessage() |
||||
); |
||||
batchLoader.maybeFlushBatches(LEADER_AND_EPOCH); |
||||
assertEquals(1, updater.updates); |
||||
assertNotNull(updater.latestImage.topics().getTopic("bar")); |
||||
} |
||||
|
||||
@Test |
||||
public void testUnexpectedAbortTransaction() { |
||||
MockMetadataUpdater updater = new MockMetadataUpdater(); |
||||
MockFaultHandler faultHandler = new MockFaultHandler("testUnexpectedAbortTransaction"); |
||||
MetadataBatchLoader batchLoader = new MetadataBatchLoader( |
||||
new LogContext(), |
||||
new MockTime(), |
||||
faultHandler, |
||||
updater |
||||
); |
||||
|
||||
// First batch gets loaded fine
|
||||
Batch<ApiMessageAndVersion> batch1 = Batch.data( |
||||
10, 2, 0, 30, TOPIC_NO_TXN_BATCH); |
||||
|
||||
// Second batch throws an error, but shouldn't interfere with prior batches
|
||||
Batch<ApiMessageAndVersion> batch2 = Batch.data( |
||||
13, 2, 0, 30, TXN_ABORT_SINGLETON); |
||||
|
||||
batchLoader.resetToImage(MetadataImage.EMPTY); |
||||
batchLoader.loadBatch(batch1, LEADER_AND_EPOCH); |
||||
assertNull(faultHandler.firstException()); |
||||
batchLoader.loadBatch(batch2, LEADER_AND_EPOCH); |
||||
assertEquals(RuntimeException.class, faultHandler.firstException().getCause().getClass()); |
||||
assertEquals( |
||||
"Encountered AbortTransactionRecord without having seen a BeginTransactionRecord", |
||||
faultHandler.firstException().getCause().getMessage() |
||||
); |
||||
batchLoader.maybeFlushBatches(LEADER_AND_EPOCH); |
||||
assertEquals(1, updater.updates); |
||||
assertNotNull(updater.latestImage.topics().getTopic("bar")); |
||||
} |
||||
|
||||
private MetadataBatchLoader loadSingleBatch( |
||||
MockMetadataUpdater updater, |
||||
MockFaultHandler faultHandler, |
||||
List<ApiMessageAndVersion> batchRecords |
||||
) { |
||||
Batch<ApiMessageAndVersion> batch = Batch.data( |
||||
10, 42, 0, 100, batchRecords); |
||||
|
||||
MetadataBatchLoader batchLoader = new MetadataBatchLoader( |
||||
new LogContext(), |
||||
new MockTime(), |
||||
faultHandler, |
||||
updater |
||||
); |
||||
|
||||
batchLoader.resetToImage(MetadataImage.EMPTY); |
||||
batchLoader.loadBatch(batch, LEADER_AND_EPOCH); |
||||
return batchLoader; |
||||
} |
||||
|
||||
@Test |
||||
public void testMultipleTransactionsInOneBatch() { |
||||
List<ApiMessageAndVersion> batchRecords = new ArrayList<>(); |
||||
batchRecords.addAll(TOPIC_TXN_BATCH_1); |
||||
batchRecords.addAll(TOPIC_TXN_BATCH_2); |
||||
batchRecords.addAll(TXN_BEGIN_SINGLETON); |
||||
batchRecords.addAll(TOPIC_NO_TXN_BATCH); |
||||
batchRecords.addAll(TXN_END_SINGLETON); |
||||
|
||||
MockMetadataUpdater updater = new MockMetadataUpdater(); |
||||
MockFaultHandler faultHandler = new MockFaultHandler("testMultipleTransactionsInOneBatch"); |
||||
MetadataBatchLoader batchLoader = loadSingleBatch(updater, faultHandler, batchRecords); |
||||
|
||||
assertEquals(1, updater.updates); |
||||
assertEquals(0, updater.latestManifest.numBytes()); |
||||
assertEquals(15, updater.latestImage.provenance().lastContainedOffset()); |
||||
assertEquals(42, updater.latestImage.provenance().lastContainedEpoch()); |
||||
|
||||
assertNotNull(updater.latestImage.topics().getTopic("foo")); |
||||
assertNull(updater.latestImage.topics().getTopic("bar")); |
||||
batchLoader.maybeFlushBatches(LEADER_AND_EPOCH); |
||||
assertEquals(2, updater.updates); |
||||
assertEquals(100, updater.latestManifest.numBytes()); |
||||
assertEquals(20, updater.latestImage.provenance().lastContainedOffset()); |
||||
assertEquals(42, updater.latestImage.provenance().lastContainedEpoch()); |
||||
assertNotNull(updater.latestImage.topics().getTopic("foo")); |
||||
assertNotNull(updater.latestImage.topics().getTopic("bar")); |
||||
} |
||||
|
||||
@Test |
||||
public void testMultipleTransactionsInOneBatchesWithNoOp() { |
||||
List<ApiMessageAndVersion> batchRecords = new ArrayList<>(); |
||||
batchRecords.addAll(noOpRecords(1)); |
||||
batchRecords.addAll(TOPIC_TXN_BATCH_1); |
||||
batchRecords.addAll(noOpRecords(1)); |
||||
batchRecords.addAll(TOPIC_TXN_BATCH_2); |
||||
// A batch with non-transactional records between two transactions causes a delta to get published
|
||||
batchRecords.addAll(noOpRecords(1)); |
||||
batchRecords.addAll(TXN_BEGIN_SINGLETON); |
||||
batchRecords.addAll(noOpRecords(1)); |
||||
batchRecords.addAll(TOPIC_NO_TXN_BATCH); |
||||
batchRecords.addAll(noOpRecords(1)); |
||||
batchRecords.addAll(TXN_END_SINGLETON); |
||||
batchRecords.addAll(noOpRecords(1)); |
||||
|
||||
MockMetadataUpdater updater = new MockMetadataUpdater(); |
||||
MockFaultHandler faultHandler = new MockFaultHandler("testMultipleTransactionsInOneBatches"); |
||||
MetadataBatchLoader batchLoader = loadSingleBatch(updater, faultHandler, batchRecords); |
||||
|
||||
assertEquals(2, updater.updates); |
||||
assertEquals(0, updater.latestManifest.numBytes()); |
||||
assertEquals(18, updater.latestImage.provenance().lastContainedOffset()); |
||||
assertEquals(42, updater.latestImage.provenance().lastContainedEpoch()); |
||||
assertNotNull(updater.latestImage.topics().getTopic("foo")); |
||||
assertNull(updater.latestImage.topics().getTopic("bar")); |
||||
batchLoader.maybeFlushBatches(LEADER_AND_EPOCH); |
||||
assertEquals(3, updater.updates); |
||||
assertEquals(100, updater.latestManifest.numBytes()); |
||||
assertEquals(26, updater.latestImage.provenance().lastContainedOffset()); |
||||
assertEquals(42, updater.latestImage.provenance().lastContainedEpoch()); |
||||
assertNotNull(updater.latestImage.topics().getTopic("foo")); |
||||
assertNotNull(updater.latestImage.topics().getTopic("bar")); |
||||
} |
||||
|
||||
@ParameterizedTest |
||||
@ValueSource(booleans = {true, false}) |
||||
public void testOneTransactionInMultipleBatches(boolean abortTxn) { |
||||
MockMetadataUpdater updater = new MockMetadataUpdater(); |
||||
MetadataBatchLoader batchLoader = new MetadataBatchLoader( |
||||
new LogContext(), |
||||
new MockTime(), |
||||
new MockFaultHandler("testOneTransactionInMultipleBatches"), |
||||
updater |
||||
); |
||||
|
||||
batchLoader.resetToImage(MetadataImage.EMPTY); |
||||
batchLoader.loadBatch(Batch.data( |
||||
16, 2, 0, 10, TXN_BEGIN_SINGLETON), LEADER_AND_EPOCH); |
||||
assertEquals(0, updater.updates); |
||||
batchLoader.loadBatch(Batch.data( |
||||
17, 3, 0, 30, TOPIC_NO_TXN_BATCH), LEADER_AND_EPOCH); |
||||
assertEquals(0, updater.updates); |
||||
if (abortTxn) { |
||||
batchLoader.loadBatch(Batch.data( |
||||
20, 4, 0, 10, TXN_ABORT_SINGLETON), LEADER_AND_EPOCH); |
||||
} else { |
||||
batchLoader.loadBatch(Batch.data( |
||||
20, 4, 0, 10, TXN_END_SINGLETON), LEADER_AND_EPOCH); |
||||
} |
||||
assertEquals(0, updater.updates); |
||||
batchLoader.maybeFlushBatches(LEADER_AND_EPOCH); |
||||
|
||||
// Regardless of end/abort, we should publish an updated MetadataProvenance and manifest
|
||||
assertEquals(50, updater.latestManifest.numBytes()); |
||||
assertEquals(3, updater.latestManifest.numBatches()); |
||||
assertEquals(20, updater.latestImage.provenance().lastContainedOffset()); |
||||
assertEquals(4, updater.latestImage.provenance().lastContainedEpoch()); |
||||
if (abortTxn) { |
||||
assertNull(updater.latestImage.topics().getTopic("bar")); |
||||
} else { |
||||
assertNotNull(updater.latestImage.topics().getTopic("bar")); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue