com.github.lontime.extredisson.service.RealProducerHandler Maven / Gradle / Ivy
The newest version!
package com.github.lontime.extredisson.service;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import com.github.lontime.base.commonj.result.ProcessResult;
import com.github.lontime.base.commonj.utils.CollectionHelper;
import com.github.lontime.base.commonj.utils.StringHelper;
import com.github.lontime.base.commonj.utils.UuidHelper;
import com.github.lontime.base.errors.ErrorCodeEnum;
import com.github.lontime.base.errors.ErrorException;
import com.github.lontime.base.logging.GLogger;
import com.github.lontime.base.serial.model.Header;
import com.github.lontime.base.serial.model.Message;
import com.github.lontime.extredisson.RedissonInstance;
import com.github.lontime.extredisson.codec.StringBytesMapCodec;
import com.github.lontime.extredisson.common.ListenerKind;
import com.github.lontime.extredisson.common.MessageWithId;
import com.github.lontime.extredisson.common.ProjectRedisKeyGenerator;
import com.github.lontime.extredisson.common.RedisKeys;
import com.github.lontime.extredisson.common.Topic;
import com.github.lontime.extredisson.configuration.OptionResolver;
import com.github.lontime.extredisson.configuration.ProducerOption;
import com.github.lontime.extredisson.listener.HandlerInterface;
import com.github.lontime.shaded.com.google.common.collect.Lists;
import com.github.lontime.shaded.org.redisson.api.RBatch;
import com.github.lontime.shaded.org.redisson.api.RScript;
import com.github.lontime.shaded.org.redisson.api.RedissonClient;
import com.github.lontime.shaded.org.redisson.api.stream.StreamAddArgs;
import com.github.lontime.shaded.org.redisson.client.codec.ByteArrayCodec;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
import static com.github.lontime.extredisson.common.Constants.SERVICE_DATA_KEY;
/**
* RealProducerHandler.
* @author lontime
* @since 1.0
*/
public class RealProducerHandler {
private final ProducerOption option;
private final Topic topic;
private final List tags;
private final boolean ref;
private final long keepAlive;
public RealProducerHandler(ProducerOption producerOption) {
this.option = producerOption;
this.topic = Topic.create(ListenerKind.SERVICE, producerOption);
this.tags = producerOption.getTags();
this.ref = producerOption.getRef();
this.keepAlive = OptionResolver.getInstance().registry().getKeepAlive().toMillis();
}
public List handle(final List messagesIn, final HandlerInterface handler) {
if (messagesIn.isEmpty()) {
return Collections.emptyList();
}
final List messages = enhanceMessages(messagesIn);
final List messageList = publishMessages(option, messages);
if (messageList.size() <= option.getPage()) {
return execute(messageList, handler);
}
final List> subList = Lists.partition(messageList, option.getPage());
final List outputs = Flux.fromIterable(subList)
.subscribeOn(Schedulers.boundedElastic())
.flatMapIterable(s -> execute(s, handler)).collectList().toFuture().join();
return outputs;
}
private List enhanceMessages(List messages) {
final List newMessages = new ArrayList<>(messages.size());
final List headers = option.getHeaders();
for (Message message : messages) {
if (StringHelper.isEmpty(message.getServiceName())) {
continue;
}
final Message.Builder builder = Message.builder().copy(message);
if (CollectionHelper.isNotEmpty(option.getHeaders())) {
builder.addHeadersIfAbsent(headers);
}
builder.topic(option.getTopic());
if (option.getEnableReply() && StringHelper.isEmpty(message.getReplyTo())) {
builder.replyTo(ProjectRedisKeyGenerator.reply(UuidHelper.fastUUID()).getFullName());
}
newMessages.add(builder.build());
}
return newMessages;
}
private List execute(List messages, HandlerInterface handler) {
//批量执行内部业务逻辑
final ProcessResult result = executeInternal(messages, handler);
return messages.stream().map(s -> buildReplyOutput(s, result)).collect(Collectors.toList());
}
private ProcessResult executeInternal(List messages, HandlerInterface handler) {
final List newMessages = messages.stream().filter(s -> StringHelper.hasText(s.getId())).
collect(Collectors.toList());
if (newMessages.isEmpty()) {
return ProcessResult.createFail("message is empty");
}
final String msgIds = newMessages.stream().map(MessageWithId::getId).collect(Collectors.joining(","));
CallbackContext callbackContext = CallbackContext.create(newMessages);
ProcessResult result = handler.run(callbackContext);
for (int i = 0; i < option.getMaxRecheck(); i++) {
if (result.successful()) {
if (option.shouldRef()) {
publishRefMessages(messages);
}
return ProcessResult.createSuccess();
} else if (result.failed()) {
handler.fail(callbackContext);
GLogger.defaults().infov("executeInternal<{0}> is fail! message-id: {1}", i, msgIds);
return result;
}
callbackContext = CallbackContext.create(i, newMessages);
result = handler.recheck(callbackContext);
}
throw ErrorException.from(ErrorCodeEnum.ERROR_SYSTEM, "executeInternal is error");
}
private List publishMessages(ProducerOption option, List messages) {
if (option.shouldRef()) {
return publishMessagesBatch(messages);
}
return publishMessagesScript(messages);
}
private List publishMessagesBatch(List messages) {
final RBatch batch = client().createBatch();
final List> futures = new ArrayList<>(messages.size());
for (final Message message : messages) {
futures.add(batch.getStream(topic.redisKeyReal(), StringBytesMapCodec.INSTANCE)
.addAsync(StreamAddArgs.entry(SERVICE_DATA_KEY, message.serializeToBytes()))
.thenApply(s -> MessageWithId.create(s.toString(), message)).toCompletableFuture());
}
batch.execute();
return futures.stream().map(CompletableFuture::join).collect(Collectors.toList());
}
private List publishMessagesScript(List messages) {
final List keys = new ArrayList<>();
final List values = new ArrayList<>();
for (int i = 0; i < messages.size(); i++) {
final Message message = messages.get(i);
keys.add(String.valueOf(i));
keys.add(topic.getName());
keys.add(topic.redisKeyReal());
keys.add(Long.toString(System.currentTimeMillis() - keepAlive));
keys.add(StringHelper.isNotEmpty(message.getReplyTo()) ? message.getReplyTo() : "??");
values.add(message.serializeToBytes());
}
final List byteIds = client().getScript(ByteArrayCodec.INSTANCE).eval(RScript.Mode.READ_ONLY,
"local result = {}; "
+ "local j = 1; "
+ "for i = 1, #ARGV, 1 do "
+ " local lastDate = 0; "
+ " local index = KEYS[j]; "
+ " local topic = KEYS[j+1]; "
+ " local key = KEYS[j+2]; "
+ " local expireDate = tonumber(KEYS[j+3]); "
+ " local replyTo = KEYS[j+4]; "
+ " j=j+5; "
+ " local lastIdle = redis.call('zscore', '" + ProjectRedisKeyGenerator.watchAllTopic().getFullName() + "', topic); "
+ " if lastIdle ~= false then "
+ " lastDate = tonumber(lastIdle); "
+ " end; "
+ " table.insert(result, index); "
+ " if lastDate >= expireDate then "
+ " local value = redis.call('xadd', key, '*', '" + SERVICE_DATA_KEY + "', ARGV[i]); "
+ " table.insert(result, value); "
+ " else "
+ " table.insert(result, ''); "
+ " if replyTo ~= '??' then"
+ " redis.call('lpush', replyTo, ''); "
+ " end; "
+ " end; "
+ "end; "
+ "return result; "
, RScript.ReturnType.MULTI, new ArrayList<>(keys), values.toArray());
final List list = byteIds.stream().map(s -> new String(s, StandardCharsets.UTF_8)).collect(Collectors.toList());
final List messageWithIds = new ArrayList<>(messages.size());
for (int i = 0; i < list.size(); i += 2) {
final int index = Integer.parseInt(list.get(i));
final String msgId = list.get(i + 1);
final Message message = messages.get(index);
messageWithIds.add(new MessageWithId(msgId, message));
}
return messageWithIds;
}
private List publishRefMessages(List messages) {
final List refMessages = new ArrayList<>();
for (final MessageWithId message : messages) {
if (ref) {
refMessages.add(createRefServiceMessage(message, null));
}
if (tags != null) {
for (String tag : tags) {
refMessages.add(createRefServiceMessage(message, tag));
}
}
}
return publishRefMessagesInternal(refMessages);
}
private RefServiceMessage createRefServiceMessage(MessageWithId message, String tag) {
final String to = Optional.ofNullable(message.getMessage().getReplyTo())
.map(s -> RedisKeys.create(null, s, tag).getFullName())
.orElse(null);
final RefServiceMessage refMessage = new RefServiceMessage();
refMessage.setMessage(message.getMessage());
refMessage.setTopic(Topic.service(topic.getOrig(), tag, true));
refMessage.setReplyTo(to);
refMessage.setMsgId(message.getId());
return refMessage;
}
private List publishRefMessagesInternal(List messages) {
final List keys = new ArrayList<>();
final List values = new ArrayList<>();
for (final RefServiceMessage refMessage : messages) {
keys.add(refMessage.getTopic().getName());
keys.add(refMessage.getTopic().redisKeyRef());
keys.add(Long.toString(System.currentTimeMillis() - keepAlive));
keys.add(StringHelper.isNotEmpty(refMessage.getReplyTo()) ? refMessage.getReplyTo() : "??");
values.add(refMessage.serialize());
}
final List byteIds = client().getScript(ByteArrayCodec.INSTANCE).eval(RScript.Mode.READ_ONLY,
"local result = {}; "
+ "local j = 1; "
+ "for i = 1, #ARGV, 1 do "
+ " local lastDate = 0; "
+ " local topic = KEYS[j]; "
+ " local key = KEYS[j+1]; "
+ " local expireDate = tonumber(KEYS[j+2]); "
+ " local replyTo = KEYS[j+3]; "
+ " j=j+4; "
+ " local lastIdle = redis.call('zscore', '" + ProjectRedisKeyGenerator.watchAllTopic().getFullName() + "', topic); "
+ " if lastIdle ~= false then "
+ " lastDate = tonumber(lastIdle); "
+ " end; "
+ " if lastDate >= expireDate then "
+ " local value = redis.call('xadd', key, '*', '" + SERVICE_DATA_KEY + "', ARGV[i]); "
+ " table.insert(result, value); "
+ " else "
+ " table.insert(result, ''); "
+ " if replyTo ~= '??' then"
+ " redis.call('lpush', replyTo, ''); "
+ " end; "
+ " end; "
+ "end; "
+ "return result; "
, RScript.ReturnType.MULTI, new ArrayList<>(keys), values.toArray());
final List ids = byteIds.stream().map(s -> new String(s, StandardCharsets.UTF_8)).collect(Collectors.toList());
final String refMsgIds = ids.stream().collect(Collectors.joining(",", "[", "]"));
GLogger.defaults().infov("publishRefMessages success, ref-message-id:{2}", refMsgIds);
return ids;
}
private Map> buildResponses(Message message) {
final List replyTos = option.getTags().stream()
.map(tag -> RedisKeys.create(null, message.getReplyTo(), tag).getFullName())
.collect(Collectors.toList());
return waitAsync(replyTos);
}
private CompletableFuture buildResponse(Message message) {
return waitAsync(message.getReplyTo());
}
private ReplyOutput buildReplyOutput(MessageWithId messageWithId, ProcessResult result) {
final String msgId = messageWithId.getId();
final Message message = messageWithId.getMessage();
final ReplyOutput output = new ReplyOutput(msgId, message, option.getTimeout());
output.setResult(result);
if (StringHelper.isEmpty(message.getReplyTo())) {
return output;
}
if (CollectionHelper.isEmpty(tags)) {
output.setResponse(buildResponse(message));
return output;
}
final Map> futureMap = buildResponses(message);
final Map> responses = new HashMap<>(futureMap.size());
for (Map.Entry> entry : futureMap.entrySet()) {
final String key = entry.getKey();
final int index = key.lastIndexOf(RedisKeys.SPLITTER);
responses.put(key.substring(index + 1), entry.getValue());
}
output.setResponses(responses);
return output;
}
private CompletableFuture waitAsync(String replyTo) {
final RedissonClient client = client();
return client.getBlockingQueue(replyTo, ByteArrayCodec.INSTANCE)
.pollAsync(OptionResolver.getInstance().getWaitPollTimeout().getSeconds(), TimeUnit.SECONDS)
.toCompletableFuture();
}
private Map> waitAsync(List replyTos) {
if (CollectionHelper.isEmpty(replyTos)) {
return Collections.emptyMap();
}
final RedissonClient client = client();
final Map> futures = new HashMap<>(replyTos.size());
for (String replyTo : replyTos) {
GLogger.defaults().infov("waitAsync replyTo:{0}", replyTo);
final CompletableFuture future = client.getBlockingQueue(replyTo, ByteArrayCodec.INSTANCE)
.pollAsync(OptionResolver.getInstance().getWaitPollTimeout().getSeconds(), TimeUnit.SECONDS)
.toCompletableFuture();
futures.put(replyTo, future);
}
return futures;
}
private RedissonClient client() {
return RedissonInstance.get().client(topic.getOrig());
}
}