clearing-service kafka queue-consumer fix consumer timeout
This commit is contained in:
parent
a8ad3eb474
commit
07c5dba41e
2 changed files with 271 additions and 2 deletions
|
|
@ -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.registry.RegistrySplitDepositActionRequest;
|
||||||
import ru.spcex.clearing.platform.messaging.domain.cud.schedule.LauncherCommandRequest;
|
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.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.RequestInfoUpdate;
|
||||||
import ru.spcex.clearing.platform.messaging.service.Status;
|
import ru.spcex.clearing.platform.messaging.service.Status;
|
||||||
import ru.spcex.clearing.service.executors.Sdf06Executor;
|
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;
|
import ru.spcex.platform.utils.error.ValidationException;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class EventsReceiver extends QueueConsumer implements InitializingBean {
|
public class EventsReceiver extends QueueConsumerV2 implements InitializingBean {
|
||||||
private final Logger log = LoggerFactory.getLogger(getClass());
|
private final Logger log = LoggerFactory.getLogger(getClass());
|
||||||
private final Environment environment;
|
private final Environment environment;
|
||||||
private final IMessageResolver errorResolver;
|
private final IMessageResolver errorResolver;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue