публикация одной outbox-записи паспорта маршрута
- добавить надёжные настройки Kafka producer - публиковать один record из RoutePassportOutboxEntity с timeout - сериализовать JsonNode payload напрямую как JSON object - пробрасывать ошибки Kafka send вызывающему коду - обновить тесты publisher и producer config
This commit is contained in:
+16
@@ -21,6 +21,18 @@ import org.springframework.kafka.core.ProducerFactory
|
||||
class KafkaProducerConfig(
|
||||
@param:Value("\${spring.kafka.bootstrap-servers}")
|
||||
private val bootstrapServers: String,
|
||||
|
||||
@param:Value("\${spring.kafka.producer.acks:all}")
|
||||
private val acks: String,
|
||||
|
||||
@param:Value("\${spring.kafka.producer.retries:10}")
|
||||
private val retries: Int,
|
||||
|
||||
@param:Value("\${spring.kafka.producer.properties.enable.idempotence:true}")
|
||||
private val enableIdempotence: Boolean,
|
||||
|
||||
@param:Value("\${spring.kafka.producer.properties.max.in.flight.requests.per.connection:5}")
|
||||
private val maxInFlightRequestsPerConnection: Int,
|
||||
) {
|
||||
|
||||
@Bean
|
||||
@@ -30,6 +42,10 @@ class KafkaProducerConfig(
|
||||
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers,
|
||||
ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java,
|
||||
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java,
|
||||
ProducerConfig.ACKS_CONFIG to acks,
|
||||
ProducerConfig.RETRIES_CONFIG to retries,
|
||||
ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG to enableIdempotence,
|
||||
ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION to maxInFlightRequestsPerConnection,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
+52
-85
@@ -3,111 +3,78 @@ package space.nstart.pcp.routepassportconsumer.service
|
||||
import org.apache.kafka.clients.producer.ProducerRecord
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.kafka.core.KafkaTemplate
|
||||
import org.springframework.kafka.support.SendResult
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.routepassportconsumer.config.KafkaTopicsProperties
|
||||
import space.nstart.pcp.pcp_types_lib.dto.routes.ParsedRoutePassportMessage
|
||||
import space.nstart.pcp.pcp_types_lib.message.KafkaMessage
|
||||
import space.nstart.pcp.pcp_types_lib.message.PcpKafkaEvent
|
||||
import space.nstart.pcp.routepassportconsumer.entity.RoutePassportOutboxEntity
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Сервис публикации обработанного маршрута в Kafka.
|
||||
* Сервис физической публикации одной готовой outbox-записи в Kafka.
|
||||
*/
|
||||
@Service
|
||||
class RoutePassportKafkaPublisherService(
|
||||
/** ObjectMapper сериализует итоговый DTO в JSON перед отправкой. */
|
||||
/** ObjectMapper сериализует сохранённый JsonNode payload в JSON object string перед отправкой. */
|
||||
private val objectMapper: ObjectMapper,
|
||||
|
||||
/** KafkaTemplate отправляет одно и то же сообщение в несколько topic'ов. */
|
||||
/** KafkaTemplate отправляет готовую outbox-запись в её целевой topic. */
|
||||
private val kafkaTemplate: KafkaTemplate<String, String>,
|
||||
|
||||
/** Набор Kafka topic'ов, в которые публикуется результат обработки. */
|
||||
private val kafkaTopicsProperties: KafkaTopicsProperties,
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
/**
|
||||
* Публикует полный RoutePassportDto в оба выходных topic'а.
|
||||
* Публикует ровно одну outbox-запись и пробрасывает ошибку отправки вызывающему коду.
|
||||
*/
|
||||
fun publish(parsedMessage: ParsedRoutePassportMessage) {
|
||||
val routePassportDto = parsedMessage.routePassportDto
|
||||
fun publish(outbox: RoutePassportOutboxEntity) {
|
||||
val payload = objectMapper.writeValueAsString(outbox.payload)
|
||||
val record = ProducerRecord(outbox.topic, outbox.messageKey, payload)
|
||||
record.headers().add(TYPE_HEADER, outbox.eventType.toByteArray(StandardCharsets.UTF_8))
|
||||
|
||||
log.info(
|
||||
"Publishing route passport: routeNameFull={}, processedRouteTopic={}, routeGeoreferenceTopic={}",
|
||||
routePassportDto.routeNameFull,
|
||||
kafkaTopicsProperties.processedRoute,
|
||||
kafkaTopicsProperties.routeGeoreference,
|
||||
)
|
||||
try {
|
||||
val result = kafkaTemplate.send(record)
|
||||
.get(DEFAULT_PUBLISH_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
|
||||
sendWithLogging(
|
||||
topic = kafkaTopicsProperties.processedRoute,
|
||||
eventType = PcpKafkaEvent.ModeStatusChangedEvent,
|
||||
payload = buildPayload(
|
||||
eventType = PcpKafkaEvent.ModeStatusChangedEvent,
|
||||
parsedMessage = parsedMessage,
|
||||
),
|
||||
traceId = parsedMessage.metadata.traceId,
|
||||
routeNameFull = routePassportDto.routeNameFull,
|
||||
)
|
||||
sendWithLogging(
|
||||
topic = kafkaTopicsProperties.routeGeoreference,
|
||||
eventType = PcpKafkaEvent.RouteGeoRefEvent,
|
||||
payload = buildPayload(
|
||||
eventType = PcpKafkaEvent.RouteGeoRefEvent,
|
||||
parsedMessage = parsedMessage,
|
||||
),
|
||||
traceId = parsedMessage.metadata.traceId,
|
||||
routeNameFull = routePassportDto.routeNameFull,
|
||||
)
|
||||
}
|
||||
log.info(
|
||||
"Kafka outbox message published: outboxId={}, routeId={}, eventType={}, topic={}, partition={}, offset={}",
|
||||
outbox.id,
|
||||
outbox.routeId,
|
||||
outbox.eventType,
|
||||
outbox.topic,
|
||||
result.recordMetadata.partition(),
|
||||
result.recordMetadata.offset(),
|
||||
)
|
||||
} catch (exception: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
logPublishFailure(outbox, exception)
|
||||
throw exception
|
||||
} catch (exception: ExecutionException) {
|
||||
val failure = exception.cause
|
||||
logPublishFailure(outbox, failure ?: exception)
|
||||
|
||||
private fun buildPayload(eventType: PcpKafkaEvent, parsedMessage: ParsedRoutePassportMessage): String {
|
||||
val message = KafkaMessage(
|
||||
type = eventType,
|
||||
data = parsedMessage.routePassportDto,
|
||||
traceId = parsedMessage.metadata.traceId,
|
||||
correlationId = parsedMessage.metadata.correlationId,
|
||||
group = "pcp",
|
||||
).apply {
|
||||
source = "route-processing"
|
||||
}
|
||||
|
||||
return objectMapper.writeValueAsString(message)
|
||||
}
|
||||
|
||||
private fun sendWithLogging(
|
||||
topic: String,
|
||||
eventType: PcpKafkaEvent,
|
||||
payload: String,
|
||||
traceId: String,
|
||||
routeNameFull: String,
|
||||
) {
|
||||
val record = ProducerRecord(topic, traceId, payload)
|
||||
record.headers().add("type", eventType.name.toByteArray(StandardCharsets.UTF_8))
|
||||
|
||||
val future: CompletableFuture<SendResult<String, String>> = kafkaTemplate.send(record)
|
||||
future.whenComplete { result, exception ->
|
||||
if (exception == null) {
|
||||
log.info(
|
||||
"Kafka message published: routeNameFull={}, topic={}, type={}, partition={}, offset={}",
|
||||
routeNameFull,
|
||||
topic,
|
||||
eventType,
|
||||
result.recordMetadata.partition(),
|
||||
result.recordMetadata.offset(),
|
||||
)
|
||||
} else {
|
||||
log.error(
|
||||
"Failed to publish kafka message: routeNameFull={}, topic={}, type={}",
|
||||
routeNameFull,
|
||||
topic,
|
||||
eventType,
|
||||
exception,
|
||||
)
|
||||
when (failure) {
|
||||
is Exception -> throw failure
|
||||
else -> throw exception
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
logPublishFailure(outbox, exception)
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
private fun logPublishFailure(outbox: RoutePassportOutboxEntity, exception: Throwable) {
|
||||
log.error(
|
||||
"Failed to publish kafka outbox message: outboxId={}, routeId={}, eventType={}, topic={}",
|
||||
outbox.id,
|
||||
outbox.routeId,
|
||||
outbox.eventType,
|
||||
outbox.topic,
|
||||
exception,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TYPE_HEADER = "type"
|
||||
const val DEFAULT_PUBLISH_TIMEOUT_SECONDS = 10L
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user