All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.infobip.kafkistry.kafka.ops.TopicAssignmentsOps.kt Maven / Gradle / Ivy

There is a newer version: 0.9.0
Show newest version
package com.infobip.kafkistry.kafka.ops

import com.infobip.kafkistry.kafka.*
import com.infobip.kafkistry.model.TopicName
import com.infobip.kafkistry.service.KafkaClusterManagementException
import kafka.log.LogConfig
import org.apache.kafka.clients.admin.*
import org.apache.kafka.common.ElectionType
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.config.ConfigResource
import java.util.*
import java.util.concurrent.CompletableFuture

class TopicAssignmentsOps (
    clientCtx: ClientCtx,
    private val configOps: ConfigOps,
    private val topicOps: TopicOps,
    private val clusterOps: ClusterOps,
): BaseOps(clientCtx) {

    fun reAssignPartitions(
        topicPartitionsAssignments: Map>>, throttleBytesPerSec: Int
    ): CompletableFuture {
        val partitionsAssignments = topicPartitionsAssignments
            .flatMap { (topic, partitionAssignments) ->
                partitionAssignments.map { (partition, replicas) ->
                    TopicPartition(topic, partition) to replicas
                }
            }
            .toMap()

        runOperation("verify reassignments used brokers") {
            val brokerIds = partitionsAssignments.values.flatten().toSet()
            val allNodeIds = adminClient.describeCluster(DescribeClusterOptions().withReadTimeout())
                .nodes()
                .asCompletableFuture("describe cluster for re-assignments")
                .get().map { it.id() }.toSet()
            brokerIds.filter { it !in allNodeIds }.takeIf { it.isNotEmpty() }?.run {
                throw KafkaClusterManagementException("Unknown broker(s) used in assignments: $this")
            }
        }
        val currentPartitionAssignments = adminClient
            .describeTopics(topicPartitionsAssignments.keys, DescribeTopicsOptions().withReadTimeout())
            .allTopicNames()
            .asCompletableFuture("describe re-assigning topics").get()
            .flatMap { (topic, description) ->
                description.partitions()
                    .map { it.toPartitionAssignments() }
                    .map { partitionsAssignments ->
                        val partitionReplicas = partitionsAssignments.replicasAssignments.map { it.brokerId }
                        TopicPartition(topic, partitionsAssignments.partition) to partitionReplicas
                    }
            }
            .toMap()
        runOperation("verify reassignments partitions") {
            val nonExistentPartitions = partitionsAssignments.keys.filter { it !in currentPartitionAssignments.keys }
            if (nonExistentPartitions.isNotEmpty()) {
                throw KafkaClusterManagementException("Trying to reassign non-existent topic partitions: $nonExistentPartitions")
            }
        }
        fun setupThrottle(currentReassignments: Map) = runOperation("setup re-assignment throttle") {
            val throttleRate = ThrottleRate(throttleBytesPerSec.toLong(), throttleBytesPerSec.toLong())
            val topicsMoveMap = ReAssignmentSupport.calculateProposedMoveMap(
                currentReassignments, partitionsAssignments, currentPartitionAssignments
            )
            val leaderThrottlesMap = ReAssignmentSupport.calculateLeaderThrottles(topicsMoveMap)
            val followerThrottlesMap = ReAssignmentSupport.calculateFollowerThrottles(topicsMoveMap)
            val reassigningBrokersSet = ReAssignmentSupport.calculateReassigningBrokers(topicsMoveMap)

            val topicNames = leaderThrottlesMap.keys + followerThrottlesMap.keys
            topicNames.map { topic ->
                val topicThrottleConfig = mapOf(
                    LogConfig.LeaderReplicationThrottledReplicasProp() to leaderThrottlesMap[topic],
                    LogConfig.FollowerReplicationThrottledReplicasProp() to followerThrottlesMap[topic],
                ).filterValues { it != null }
                configOps.partialUpdateTopicConfig(topic, topicThrottleConfig)
            }.forEach { it.get() }
            reassigningBrokersSet.map { configOps.updateThrottleRate(it, throttleRate) }.forEach { it.get() }
        }
        return if (clusterVersion < VERSION_2_4) {
            runOperation("execute re-assignment via ZK") {
                if (throttleBytesPerSec >= 0) {
                    setupThrottle(currentReassignments = emptyMap())
                }
                val newAssignmentsScala = partitionsAssignments
                    .mapValues { it.value.toScalaList().toSeq() }
                    .toScalaMap()
                zkClient.createPartitionReassignment(newAssignmentsScala.cast())
            }
            CompletableFuture.completedFuture(Unit)
        } else {
            val reassignments = partitionsAssignments.mapValues {
                Optional.of(NewPartitionReassignment(it.value))
            }
            if (throttleBytesPerSec >= 0) {
                val currentReassignments = adminClient.listPartitionReassignments().reassignments().get()
                setupThrottle(currentReassignments)
            }
            adminClient
                .alterPartitionReassignments(reassignments, AlterPartitionReassignmentsOptions().withWriteTimeout())
                .all()
                .asCompletableFuture("alter partition reassignments")
                .thenApply { }
        }
    }

    fun listReAssignments(): CompletableFuture> {
        if (clusterVersion < VERSION_2_4) {
            val topicNames = topicOps.listAllTopicNames().get()
            val result = runOperation("get current re-assignments in ZK") {
                val targetAssignments = zkClient.partitionReassignment
                    .toJavaMap()
                    .mapValues { it.value.toJavaList().cast>() }
                val assignmentForTopics = zkClient.getFullReplicaAssignmentForTopics(
                    topicNames.toScalaList().toSet()
                )
                val partitionStates = zkClient.getTopicPartitionStates(assignmentForTopics.keys().toSeq())
                assignmentForTopics.toJavaMap()
                    .mapNotNull { (topicPartition, reAssignment) ->
                        val allReplicas = reAssignment.replicas().toJavaList().cast>()
                        val isr = partitionStates.get(topicPartition)
                            .map { it.leaderAndIsr().isr().toJavaList().cast>() }
                            .getOrElse { emptyList() }
                        if (allReplicas.toSet() == isr.toSet()) {
                            return@mapNotNull null
                        }
                        val targets = targetAssignments[topicPartition] ?: emptyList()
                        TopicPartitionReAssignment(
                            topic = topicPartition.topic(),
                            partition = topicPartition.partition(),
                            addingReplicas = targets - isr,
                            removingReplicas = allReplicas - targets,
                            allReplicas = allReplicas
                        )
                    }
            }
            return CompletableFuture.completedFuture(result)
        }
        return adminClient.listPartitionReassignments(ListPartitionReassignmentsOptions().withReadTimeout())
            .reassignments()
            .asCompletableFuture("list partition reassignments")
            .thenApply { partitionReAssignments ->
                partitionReAssignments.map { (topicPartition, reAssignment) ->
                    TopicPartitionReAssignment(
                        topic = topicPartition.topic(),
                        partition = topicPartition.partition(),
                        addingReplicas = reAssignment.addingReplicas(),
                        removingReplicas = reAssignment.removingReplicas(),
                        allReplicas = reAssignment.replicas()
                    )
                }
            }
    }

    fun verifyReAssignPartitions(topicName: TopicName, partitionsAssignments: Map>): String {
        val currentReAssignments = listReAssignments().get()
            .groupBy { it.topic }
            .mapValues { (_, partitionReAssignments) ->
                partitionReAssignments.associateBy { it.partition }
            }
        val currentTopicDescription = adminClient
            .describeTopics(listOf(topicName), DescribeTopicsOptions().withReadTimeout())
            .allTopicNames()
            .asCompletableFuture("describe topics for reassignment verification")
            .get().getOrElse(topicName) {
                throw KafkaClusterManagementException("Failed to get current topic description for topic: '$topicName'")
            }
        val currentAssignments = currentTopicDescription.partitions()
            .map { it.toPartitionAssignments() }
            .toPartitionReplicasMap()
        val partitionStatuses = partitionsAssignments.mapValues { (partition, replicas) ->
            val currentReplicas = currentAssignments[partition] ?: emptyList()
            val reAssignment = currentReAssignments[topicName]?.get(partition)
            when {
                reAssignment != null || currentReplicas.size > replicas.size -> ReAssignmentStatus.IN_PROGRESS
                currentReplicas == replicas -> ReAssignmentStatus.COMPLETED
                else -> ReAssignmentStatus.FAILED
            }
        }
        val resultMsg = StringBuilder()
        partitionStatuses.forEach { (partition, status) ->
            val topicPartition = TopicPartition(topicName, partition)
            when (status) {
                ReAssignmentStatus.COMPLETED -> resultMsg.append("Reassignment of partition $topicPartition completed successfully\n")
                ReAssignmentStatus.FAILED -> resultMsg.append("Reassignment of partition $topicPartition failed\n")
                ReAssignmentStatus.IN_PROGRESS -> resultMsg.append("Reassignment of partition $topicPartition is still in progress\n")
            }
        }
        if (partitionStatuses.values.all { it == ReAssignmentStatus.COMPLETED }) {
            val topicConfig = adminClient.describeConfigs(
                listOf(ConfigResource(ConfigResource.Type.TOPIC, topicName)),
                DescribeConfigsOptions().withWriteTimeout()
            )
                .all().asCompletableFuture("describe topic configs for reassignment verification")
                .get().getOrElse(ConfigResource(ConfigResource.Type.TOPIC, topicName)) {
                    throw KafkaClusterManagementException("Failed to get current topic description for topic: '$topicName'")
                }
                .entries().associate { it.name() to it.toTopicConfigValue() }
                .filterValues { !it.default }
                .mapValues { it.value.value }
                .plus(
                    mapOf(
                        LogConfig.FollowerReplicationThrottledReplicasProp() to null,
                        LogConfig.LeaderReplicationThrottledReplicasProp() to null,
                    )
                )
            configOps.updateTopicConfig(topicName, topicConfig).get()
            resultMsg.append("Topic: Throttle was removed.\n")
        }
        if (currentReAssignments.isEmpty()) {
            clusterOps.clusterInfo("").get().nodeIds
                .map { configOps.updateThrottleRate(it, ThrottleRate.NO_THROTTLE) }
                .forEach { it.get() }
            resultMsg.append("Brokers: Throttle was removed.\n")
        } else {
            resultMsg.append("Brokers: Keeping throttle because of topics re-assigning ${currentReAssignments.keys}.\n")
        }
        return resultMsg.toString()
    }

    fun cancelReAssignments(topicName: TopicName, partitions: List): CompletableFuture {
        if (clusterVersion < VERSION_2_4) {
            throw KafkaClusterManagementException("Unsupported operation for cluster version < $VERSION_2_4, current: $clusterVersion")
        }
        val topicPartitions = partitions
            .associate { TopicPartition(topicName, it) to Optional.empty() }
        return adminClient
            .alterPartitionReassignments(topicPartitions, AlterPartitionReassignmentsOptions().withWriteTimeout())
            .all()
            .asCompletableFuture("cancel partition reassignments")
            .thenApply { }
    }

    fun runPreferredReplicaElection(topicName: TopicName, partitions: List) {
        val topicPartitions = partitions.map { TopicPartition(topicName, it) }.toSet()
        when {
            clusterVersion < VERSION_2_2 -> runOperation("preferred replica election") {
                zkClient.createPreferredReplicaElection(topicPartitions.toScalaList().toSet())
            }
            else -> {
                adminClient
                    .electLeaders(
                        ElectionType.PREFERRED, topicPartitions, ElectLeadersOptions().withWriteTimeout()
                    )
                    .all().asCompletableFuture("run preferred leader election")
                    .get()
            }
        }
    }

    private enum class ReAssignmentStatus {
        COMPLETED, IN_PROGRESS, FAILED
    }


}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy