Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 0 additions & 70 deletions core/src/main/scala/kafka/admin/AdminClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import java.util.{Collections, Properties}
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.{ConcurrentLinkedQueue, Future, TimeUnit}

import kafka.admin.AdminClient.DeleteRecordsResult
import kafka.common.KafkaException
import kafka.coordinator.group.GroupOverview
import kafka.utils.Logging
Expand Down Expand Up @@ -216,73 +215,6 @@ class AdminClient(val time: Time,
broker -> Try[NodeApiVersions](new NodeApiVersions(getApiVersions(broker).asJava))
}.toMap

/*
* Remove all the messages whose offset is smaller than the given offset of the corresponding partition
*
* DeleteRecordsResult contains either lowWatermark of the partition or exception. We list the possible exception
* and their interpretations below:
*
* - DisconnectException if leader node of the partition is not available. Need retry by user.
* - PolicyViolationException if the topic is configured as non-deletable.
* - TopicAuthorizationException if the topic doesn't exist and the user doesn't have the authority to create the topic
* - TimeoutException if response is not available within the timeout specified by either Future's timeout or AdminClient's request timeout
* - UnknownTopicOrPartitionException if the partition doesn't exist or if the user doesn't have the authority to describe the topic
* - NotLeaderForPartitionException if broker is not leader of the partition. Need retry by user.
* - OffsetOutOfRangeException if the offset is larger than high watermark of this partition
*
*/

def deleteRecordsBefore(offsets: Map[TopicPartition, Long]): Future[Map[TopicPartition, DeleteRecordsResult]] = {
val metadataRequest = new MetadataRequest.Builder(offsets.keys.map(_.topic).toSet.toList.asJava, true)
val response = sendAnyNode(ApiKeys.METADATA, metadataRequest).asInstanceOf[MetadataResponse]
val errors = response.errors
if (!errors.isEmpty)
error(s"Metadata request contained errors: $errors")

val (partitionsWithoutError, partitionsWithError) = offsets.partition{ partitionAndOffset =>
!response.errors().containsKey(partitionAndOffset._1.topic())}

val (partitionsWithLeader, partitionsWithoutLeader) = partitionsWithoutError.partition{ partitionAndOffset =>
response.cluster().leaderFor(partitionAndOffset._1) != null}

val partitionsWithErrorResults = partitionsWithError.keys.map( partition =>
partition -> DeleteRecordsResult(DeleteRecordsResponse.INVALID_LOW_WATERMARK, response.errors().get(partition.topic()).exception())).toMap

val partitionsWithoutLeaderResults = partitionsWithoutLeader.mapValues( _ =>
DeleteRecordsResult(DeleteRecordsResponse.INVALID_LOW_WATERMARK, Errors.LEADER_NOT_AVAILABLE.exception()))

val partitionsGroupByLeader = partitionsWithLeader.groupBy(partitionAndOffset =>
response.cluster().leaderFor(partitionAndOffset._1))

// prepare requests and generate Future objects
val futures = partitionsGroupByLeader.map{ case (node, partitionAndOffsets) =>
val convertedMap: java.util.Map[TopicPartition, java.lang.Long] = partitionAndOffsets.mapValues(_.asInstanceOf[java.lang.Long]).asJava
val future = client.send(node, new DeleteRecordsRequest.Builder(requestTimeoutMs, convertedMap))
pendingFutures.add(future)
future.compose(new RequestFutureAdapter[ClientResponse, Map[TopicPartition, DeleteRecordsResult]]() {
override def onSuccess(response: ClientResponse, future: RequestFuture[Map[TopicPartition, DeleteRecordsResult]]) {
val deleteRecordsResponse = response.responseBody().asInstanceOf[DeleteRecordsResponse]
val result = deleteRecordsResponse.responses().asScala.mapValues(v => DeleteRecordsResult(v.lowWatermark, v.error.exception())).toMap
future.complete(result)
pendingFutures.remove(future)
}

override def onFailure(e: RuntimeException, future: RequestFuture[Map[TopicPartition, DeleteRecordsResult]]) {
val result = partitionAndOffsets.mapValues(_ => DeleteRecordsResult(DeleteRecordsResponse.INVALID_LOW_WATERMARK, e))
future.complete(result)
pendingFutures.remove(future)
}

})
}

// default output if not receiving DeleteRecordsResponse before timeout
val defaultResults = offsets.mapValues(_ =>
DeleteRecordsResult(DeleteRecordsResponse.INVALID_LOW_WATERMARK, Errors.REQUEST_TIMED_OUT.exception())) ++ partitionsWithErrorResults ++ partitionsWithoutLeaderResults

new CompositeFuture(time, defaultResults, futures.toList)
}

/**
* Case class used to represent a consumer of a consumer group
*/
Expand Down Expand Up @@ -473,8 +405,6 @@ object AdminClient {
config
}

case class DeleteRecordsResult(lowWatermark: Long, error: Exception)

class AdminConfig(originals: Map[_,_]) extends AbstractConfig(AdminConfigDef, originals.asJava, false)

def createSimplePlaintext(brokerUrl: String): AdminClient = {
Expand Down
28 changes: 18 additions & 10 deletions core/src/main/scala/kafka/admin/DeleteRecordsCommand.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ package kafka.admin
import java.io.PrintStream
import java.util.Properties

import kafka.admin.AdminClient.DeleteRecordsResult
import kafka.common.AdminCommandFailedException
import kafka.utils.{CoreUtils, Json, CommandLineUtils}
import kafka.utils.{CommandLineUtils, CoreUtils, Json}
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.utils.Utils
import org.apache.kafka.clients.admin
import org.apache.kafka.clients.admin.RecordsToDelete
import org.apache.kafka.clients.CommonClientConfigs
import joptsimple._

import scala.collection.JavaConverters._

/**
* A command for delete records of the given partitions down to the specified offset.
*/
Expand Down Expand Up @@ -61,26 +64,31 @@ object DeleteRecordsCommand {
if (duplicatePartitions.nonEmpty)
throw new AdminCommandFailedException("Offset json file contains duplicate topic partitions: %s".format(duplicatePartitions.mkString(",")))

val recordsToDelete = offsetSeq.map { case (topicPartition, offset) =>
(topicPartition, RecordsToDelete.beforeOffset(offset))
}.toMap.asJava

out.println("Executing records delete operation")
val deleteRecordsResult: Map[TopicPartition, DeleteRecordsResult] = adminClient.deleteRecordsBefore(offsetSeq.toMap).get()
val deleteRecordsResult = adminClient.deleteRecords(recordsToDelete)
out.println("Records delete operation completed:")

deleteRecordsResult.foreach{ case (tp, partitionResult) => {
if (partitionResult.error == null)
out.println(s"partition: $tp\tlow_watermark: ${partitionResult.lowWatermark}")
else
out.println(s"partition: $tp\terror: ${partitionResult.error.toString}")
deleteRecordsResult.lowWatermarks.asScala.foreach { case (tp, partitionResult) => {
try out.println(s"partition: $tp\tlow_watermark: ${partitionResult.get.lowWatermark}")
catch {
case e: Exception => out.println(s"partition: $tp\terror: ${e.getMessage}")
}
}}

adminClient.close()
}

private def createAdminClient(opts: DeleteRecordsCommandOptions): AdminClient = {
private def createAdminClient(opts: DeleteRecordsCommandOptions): admin.AdminClient = {
val props = if (opts.options.has(opts.commandConfigOpt))
Utils.loadProps(opts.options.valueOf(opts.commandConfigOpt))
else
new Properties()
props.put(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, opts.options.valueOf(opts.bootstrapServerOpt))
AdminClient.create(props)
admin.AdminClient.create(props)
}

class DeleteRecordsCommandOptions(args: Array[String]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ import scala.collection.JavaConverters._
import java.lang.{Long => JLong}

import kafka.zk.KafkaZkClient
import org.apache.kafka.common.internals.Topic
import org.scalatest.Assertions.intercept

import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}
Expand Down Expand Up @@ -291,8 +289,6 @@ class AdminClientIntegrationTest extends IntegrationTestHarness with Logging {
)
}
}

client.close()
}

@Test
Expand Down Expand Up @@ -746,7 +742,7 @@ class AdminClientIntegrationTest extends IntegrationTestHarness with Logging {
assertEquals(0L, consumer.position(topicPartition))

val result = client.deleteRecords(Map(topicPartition -> RecordsToDelete.beforeOffset(5L)).asJava)
val lowWatermark = result.lowWatermarks().get(topicPartition).get().lowWatermark()
val lowWatermark = result.lowWatermarks().get(topicPartition).get.lowWatermark
assertEquals(5L, lowWatermark)

consumer.seekToBeginning(Collections.singletonList(topicPartition))
Expand All @@ -755,7 +751,9 @@ class AdminClientIntegrationTest extends IntegrationTestHarness with Logging {
consumer.seek(topicPartition, 7L)
assertEquals(7L, consumer.position(topicPartition))

client.close()
client.deleteRecords(Map(topicPartition -> RecordsToDelete.beforeOffset(DeleteRecordsRequest.HIGH_WATERMARK)).asJava).all.get
consumer.seekToBeginning(Collections.singletonList(topicPartition))
assertEquals(10L, consumer.position(topicPartition))
}

@Test
Expand Down Expand Up @@ -794,7 +792,6 @@ class AdminClientIntegrationTest extends IntegrationTestHarness with Logging {
e.getCause.isInstanceOf[NotLeaderForPartitionException] => false
}
}, s"Expected low watermark of the partition to be 5 but got ${lowWatermark.getOrElse("no response within the timeout")}")
client.close()
}

@Test
Expand All @@ -807,13 +804,11 @@ class AdminClientIntegrationTest extends IntegrationTestHarness with Logging {

sendRecords(producers.head, 10, topicPartition)
val result = client.deleteRecords(Map(topicPartition -> RecordsToDelete.beforeOffset(3L)).asJava)
val lowWatermark = result.lowWatermarks().get(topicPartition).get().lowWatermark()
val lowWatermark = result.lowWatermarks.get(topicPartition).get.lowWatermark
assertEquals(3L, lowWatermark)

for (i <- 0 until serverCount)
assertEquals(3, servers(i).replicaManager.getReplica(topicPartition).get.logStartOffset)

client.close()
}

@Test
Expand All @@ -829,14 +824,68 @@ class AdminClientIntegrationTest extends IntegrationTestHarness with Logging {
assertEquals(0L, consumer.offsetsForTimes(Map(topicPartition -> JLong.valueOf(0L)).asJava).get(topicPartition).offset())

var result = client.deleteRecords(Map(topicPartition -> RecordsToDelete.beforeOffset(5L)).asJava)
result.all().get()
result.all.get
assertEquals(5L, consumer.offsetsForTimes(Map(topicPartition -> JLong.valueOf(0L)).asJava).get(topicPartition).offset())

result = client.deleteRecords(Map(topicPartition -> RecordsToDelete.beforeOffset(DeleteRecordsRequest.HIGH_WATERMARK)).asJava)
result.all().get()
result.all.get
assertNull(consumer.offsetsForTimes(Map(topicPartition -> JLong.valueOf(0L)).asJava).get(topicPartition))
}

client.close()
@Test
def testConsumeAfterDeleteRecords(): Unit = {
val consumer = consumers.head
subscribeAndWaitForAssignment(topic, consumer)

client = AdminClient.create(createConfig)

sendRecords(producers.head, 10, topicPartition)
var messageCount = 0
TestUtils.waitUntilTrue(() => {
messageCount += consumer.poll(0).count
messageCount == 10
}, "Expected 10 messages", 3000L)

client.deleteRecords(Map(topicPartition -> RecordsToDelete.beforeOffset(3L)).asJava).all.get
consumer.seek(topicPartition, 1)
messageCount = 0
TestUtils.waitUntilTrue(() => {
messageCount += consumer.poll(0).count
messageCount == 7
}, "Expected 7 messages", 3000L)

client.deleteRecords(Map(topicPartition -> RecordsToDelete.beforeOffset(8L)).asJava).all.get
consumer.seek(topicPartition, 1)
messageCount = 0
TestUtils.waitUntilTrue(() => {
messageCount += consumer.poll(0).count
messageCount == 2
}, "Expected 2 messages", 3000L)
}

@Test
def testDeleteRecordsWithException(): Unit = {
subscribeAndWaitForAssignment(topic, consumers.head)

client = AdminClient.create(createConfig)

sendRecords(producers.head, 10, topicPartition)

assertEquals(5L, client.deleteRecords(Map(topicPartition -> RecordsToDelete.beforeOffset(5L)).asJava)
.lowWatermarks.get(topicPartition).get.lowWatermark)

// OffsetOutOfRangeException if offset > high_watermark
var cause = intercept[ExecutionException] {
client.deleteRecords(Map(topicPartition -> RecordsToDelete.beforeOffset(20L)).asJava).lowWatermarks.get(topicPartition).get
}.getCause
assertEquals(classOf[OffsetOutOfRangeException], cause.getClass)

val nonExistPartition = new TopicPartition(topic, 3)
// LeaderNotAvailableException if non existent partition
cause = intercept[ExecutionException] {
client.deleteRecords(Map(nonExistPartition -> RecordsToDelete.beforeOffset(20L)).asJava).lowWatermarks.get(nonExistPartition).get
}.getCause
assertEquals(classOf[LeaderNotAvailableException], cause.getClass)
}

@Test
Expand All @@ -856,8 +905,6 @@ class AdminClientIntegrationTest extends IntegrationTestHarness with Logging {
val describeResult2 = client.describeConfigs(Collections.singletonList(invalidTopic))

assertTrue(intercept[ExecutionException](describeResult2.values.get(invalidTopic).get).getCause.isInstanceOf[InvalidTopicException])

client.close()
}

private def subscribeAndWaitForAssignment(topic: String, consumer: KafkaConsumer[Array[Byte], Array[Byte]]): Unit = {
Expand Down Expand Up @@ -902,7 +949,6 @@ class AdminClientIntegrationTest extends IntegrationTestHarness with Logging {
classOf[SecurityDisabledException])
assertFutureExceptionTypeEquals(client.deleteAcls(Collections.singleton(ACL1.toFilter())).all(),
classOf[SecurityDisabledException])
client.close()
}

/**
Expand Down Expand Up @@ -955,7 +1001,6 @@ class AdminClientIntegrationTest extends IntegrationTestHarness with Logging {
assertFutureExceptionTypeEquals(future, classOf[TimeoutException])
val endTimeMs = Time.SYSTEM.milliseconds()
assertTrue("Expected the timeout to take at least one millisecond.", endTimeMs > startTimeMs);
client.close()
}

/**
Expand All @@ -973,7 +1018,6 @@ class AdminClientIntegrationTest extends IntegrationTestHarness with Logging {
val future2 = client.createTopics(Seq("mytopic3", "mytopic4").map(new NewTopic(_, 1, 1)).asJava,
new CreateTopicsOptions().validateOnly(true)).all()
future2.get
client.close()
assertEquals(1, factory.failuresInjected)
}

Expand Down Expand Up @@ -1091,6 +1135,7 @@ class AdminClientIntegrationTest extends IntegrationTestHarness with Logging {
Utils.closeQuietly(client, "adminClient")
}
}

}

object AdminClientIntegrationTest {
Expand Down
Loading