clearing-service kafka queue-consumer fix consumer timeout

This commit is contained in:
ialbert 2024-08-01 19:40:05 +03:00
parent a8ad3eb474
commit 07c5dba41e
2 changed files with 271 additions and 2 deletions

View file

@ -28,7 +28,7 @@ import ru.spcex.clearing.platform.messaging.domain.cud.registry.RegistryReturnDe
import ru.spcex.clearing.platform.messaging.domain.cud.registry.RegistrySplitDepositActionRequest;
import ru.spcex.clearing.platform.messaging.domain.cud.schedule.LauncherCommandRequest;
import ru.spcex.clearing.platform.messaging.domain.cud.utilities.STradesImportedRequest;
import ru.spcex.clearing.platform.messaging.service.QueueConsumer;
import ru.spcex.clearing.platform.messaging.service.QueueConsumerV2;
import ru.spcex.clearing.platform.messaging.service.RequestInfoUpdate;
import ru.spcex.clearing.platform.messaging.service.Status;
import ru.spcex.clearing.service.executors.Sdf06Executor;
@ -53,7 +53,7 @@ import ru.spcex.platform.utils.enumeration.IMessageResolver;
import ru.spcex.platform.utils.error.ValidationException;
@Service
public class EventsReceiver extends QueueConsumer implements InitializingBean {
public class EventsReceiver extends QueueConsumerV2 implements InitializingBean {
private final Logger log = LoggerFactory.getLogger(getClass());
private final Environment environment;
private final IMessageResolver errorResolver;

View file

@ -0,0 +1,269 @@
package ru.spcex.clearing.platform.messaging.service;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.errors.WakeupException;
import org.apache.kafka.common.header.Header;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.support.KafkaHeaders;
import ru.spcex.clearing.platform.messaging.domain.ActionType;
import ru.spcex.clearing.platform.messaging.domain.BaseRequest;
import ru.spcex.clearing.platform.messaging.domain.Consts;
import ru.spcex.clearing.platform.messaging.logic.functional.BuilderConsumerStep;
import ru.spcex.clearing.platform.messaging.logic.functional.ConsumerSpecificClass;
import ru.spcex.clearing.platform.messaging.service.sender.CorrelationHeader;
import ru.spcex.platform.utils.enumeration.EnumMessage;
import ru.spcex.platform.utils.enumeration.IMessageResolver;
import ru.spcex.platform.utils.log.ExceptionUtils;
import ru.spcex.platform.utils.validation.IValidator;
/**
* утилитный класс для обработки сообщений из очереди
*/
public class QueueConsumerV2 implements AutoCloseable {
protected final Map<String, ConsumerSpecificClass<?>> callbacks;
private final Logger log = LoggerFactory.getLogger(getClass());
private final AtomicBoolean closed = new AtomicBoolean(false);
private final Consumer<String, Object> consumer;
private final ExecutorService inputExecutor;
private final ExecutorService processExecutor;
private final ExecutorService outputExecutor;
private final ObjectMapper json;
protected boolean supportStartOffsetTimeWindow;
private Producer<String, Object> producer;
public QueueConsumerV2(Consumer<String, Object> kafkaQueue) {
this.consumer = kafkaQueue;
this.callbacks = new HashMap<>();
this.inputExecutor = Executors.newSingleThreadExecutor();
this.processExecutor = Executors.newSingleThreadExecutor();
this.outputExecutor = Executors.newSingleThreadExecutor();
this.json = new ObjectMapper();
this.supportStartOffsetTimeWindow = false;
if (producer == null) {
log.debug("For {} kafka producer not set. Did not send reply.", getClass().getName());
}
}
/**
* используем этот конструктор, если хотим класть
* в кафку "ответ" - информацию о статусе обработки команд
*
* @param kafkaQueue
* @param kafkaResponseQueue
*/
public QueueConsumerV2(Consumer<String, Object> kafkaQueue, Producer<String, Object> kafkaResponseQueue) {
this(kafkaQueue);
this.producer = kafkaResponseQueue;
if (producer == null) {
log.warn("For {} kafka producer not set. Did not send reply.", getClass().getName());
}
}
protected boolean needsProcessing(String topicName, BaseRequest<?> request) {
return true;
}
private final AtomicInteger lastErrors = new AtomicInteger(0);
public void init() {
final Exception debugCallerStacktrace = new Exception("init call");
inputExecutor.submit(() -> {
try {
log.debug("Using kafka consumer {} for subscribe on \"{}\"", consumer, callbacks.keySet());
if (supportStartOffsetTimeWindow) {
consumer.subscribe(callbacks.keySet(), new OffsetChanger(consumer, callbacks.keySet()));
} else {
consumer.subscribe(callbacks.keySet());
}
Object o = null;
Header correlationId = null;
while (!closed.get()) {
String lastTopic = null;
try {
if (this.lastErrors.get() > 20) {
log.warn("Too many error at row, {}. Sleep.", lastErrors);
try {
Thread.sleep(1000L);
} catch (InterruptedException e){
log.info("Thread interrupted. {}", ExceptionUtils.getStackTrace(e));
break;
}
}
ConsumerRecords<String, Object> records = consumer.poll(Duration.of(10, ChronoUnit.SECONDS));
for (ConsumerRecord<String, Object> next : records) {
lastTopic = next.topic();
correlationId = next.headers().lastHeader(KafkaHeaders.CORRELATION_ID);
ConsumerSpecificClass<?> callback = callbacks.get(next.topic());
Class<?> clazz = callback.getClazz();
JavaType payloadType = json.getTypeFactory().constructParametricType(BaseRequest.class, clazz);
o = json.readValue((String) next.value(), payloadType);
if (correlationId != null) {
((BaseRequest<?>) o).setCorrelationId(correlationId.value());
}
if (needsProcessing(next.topic(), (BaseRequest<?>) o)) {
Object fO = o;
Header fCI = correlationId;
String fLastTopic = lastTopic;
CompletableFuture
.supplyAsync(() -> callback.acceptRaw(fO), processExecutor)
.thenAccept(topicResponse -> {
this.lastErrors.set(0);
if (producer != null) {
sendResponse((BaseRequest<?>) fO, topicResponse, fCI);
}
}
)
.exceptionally(e -> {
processError(e, fO, fLastTopic, fCI, debugCallerStacktrace);
return null;
});
}
}
} catch (Throwable e) {
processError(e, o, lastTopic, correlationId, debugCallerStacktrace);
}
}
} catch (WakeupException e) {
if (!closed.get()) throw e;
} catch (Throwable e) {
log.error("QueueConsumer error: {}; main thread stacktrace: {}",
ExceptionUtils.getStackTrace(e), ExceptionUtils.getStackTrace(debugCallerStacktrace));
} finally {
consumer.close();
}
});
}
private void processError(Throwable e, Object o, String lastTopic, Header correlationId, Exception debugCallerStacktrace) {
log.error("Listener {} last topic \"{}\", error: {}",
QueueConsumerV2.this.getClass().getName(), lastTopic, ExceptionUtils.getStackTrace(e));
if (lastTopic == null) {
log.debug("main thread stacktrace: {}", ExceptionUtils.getStackTrace(debugCallerStacktrace));
}
if (producer != null && o != null) {
sendErrorResponse((BaseRequest<?>) o, correlationId);
}
lastErrors.incrementAndGet();
}
//мб перенести в другой класс
private void sendErrorResponse(BaseRequest<?> o, Header correlationId) {
outputExecutor.submit(() -> {
try {
Future<RecordMetadata> send;
//default response
BaseRequest<RequestInfoUpdate> req = new BaseRequest<>();
RequestInfoUpdate success = new RequestInfoUpdate();
success.setId(o.getId());
success.setStatus(Status.Error);
req.setRequestPayload(success);
req.setId(o.getId());
req.setActionType(ActionType.SYSTEM);
ProducerRecord<String, Object> respRec = new ProducerRecord<>(Consts.REQUEST_INFO_UPDATE, req);
if (correlationId != null) {
respRec.headers().add(KafkaHeaders.CORRELATION_ID, correlationId.value());
}
send = producer.send(respRec);
send.get();
} catch (Exception e) {
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
log.error(ExceptionUtils.getStackTrace(e));
}
});
}
protected void sendResponse(BaseRequest<?> o, Object response, Header correlationId) {
outputExecutor.submit(() -> {
try {
Future<RecordMetadata> send;
BaseRequest<Object> req = new BaseRequest<>();
req.setId(o.getId());
req.setActionType(ActionType.SYSTEM);
if (response != null) {
req.setRequestPayload(response);
} else {
//default response
RequestInfoUpdate success = new RequestInfoUpdate();
success.setId(o.getId());
success.setStatus(Status.Success);
req.setRequestPayload(success);
}
ProducerRecord<String, Object> respRec = new ProducerRecord<>(Consts.REQUEST_INFO_UPDATE, req);
if (correlationId != null) {
respRec.headers().add(new CorrelationHeader(correlationId.value().clone()));
}
send = producer.send(respRec);
send.get();
} catch (Exception e) {
log.error(ExceptionUtils.getStackTrace(e));
}
});
}
protected <T> BuilderConsumerStep<T> callback(Class<T> clazz) {
return ConsumerSpecificClass.build(clazz);
}
/**
* Валидация запроса
*
* @param <R> Класс проверяемого запроса
* @return null если ошибок нет
*/
@Deprecated
public <R> RequestInfoUpdate validate(BaseRequest<R> userRequest,
Function<R, IValidator> validatorBuilder,
IMessageResolver messageResolver) {
if (validatorBuilder != null) {
R req = userRequest.getRequestPayload();
IValidator validator = validatorBuilder.apply(req);
Optional<EnumMessage> validationError = validator.tillFirstError();
if (validationError.isPresent()) {
String errorMsg = messageResolver.resolve(validationError.get());
log.warn("validation error for {} error={}, id={}: {}", req.getClass().getSimpleName(), validationError.get().getSubject(), userRequest.getId(), errorMsg);
return new RequestInfoUpdate()
.setId(userRequest.getId())
.setStatus(Status.Error)
.setMessage(errorMsg);
}
}
return null;
}
@Override
public void close() {
log.debug("Closing queue consumer {}", getClass().getSimpleName());
closed.set(true);
consumer.wakeup();
inputExecutor.shutdown();
processExecutor.shutdown();
outputExecutor.shutdown();
}
// нужен для тестов поскольку у каждого сервиса свой consumer
public Consumer getConsumer() {
return consumer;
}
}