/**
* Copyright (C) 2010-2013 Alibaba Group Holding Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.rocketmq.store.schedule;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.rocketmq.common.ConfigManager;
import com.alibaba.rocketmq.common.TopicFilterType;
import com.alibaba.rocketmq.common.constant.LoggerName;
import com.alibaba.rocketmq.common.message.MessageConst;
import com.alibaba.rocketmq.common.message.MessageDecoder;
import com.alibaba.rocketmq.common.message.MessageExt;
import com.alibaba.rocketmq.common.running.RunningStats;
import com.alibaba.rocketmq.store.ConsumeQueue;
import com.alibaba.rocketmq.store.DefaultMessageStore;
import com.alibaba.rocketmq.store.MessageExtBrokerInner;
import com.alibaba.rocketmq.store.PutMessageResult;
import com.alibaba.rocketmq.store.PutMessageStatus;
import com.alibaba.rocketmq.store.SelectMapedBufferResult;
/**
* 定时消息服务
*
* @author shijia.wxr<vintage.wang@gmail.com>
* @since 2013-7-21
*/
public class ScheduleMessageService extends ConfigManager {
/**
* chen.si 定时消息,继续采用cq的模式,使用 固定的topic名称,其中每个delayLeve 对应一个 topic的分区
*/
public static final String SCHEDULE_TOPIC = "SCHEDULE_TOPIC_XXXX";
private static final Logger log = LoggerFactory.getLogger(LoggerName.StoreLoggerName);
private static final long FIRST_DELAY_TIME = 1000L;
private static final long DELAY_FOR_A_WHILE = 100L;
private static final long DELAY_FOR_A_PERIOD = 10000L;
// 每个level对应的延时时间
private final ConcurrentHashMap<Integer /* level */, Long/* delay timeMillis */> delayLevelTable =
new ConcurrentHashMap<Integer, Long>(32);
// 延时计算到了哪里
private final ConcurrentHashMap<Integer /* level */, Long/* offset */> offsetTable =
new ConcurrentHashMap<Integer, Long>(32);
// 定时器
private final Timer timer = new Timer("ScheduleMessageTimerThread", true);
// 存储顶层对象
private final DefaultMessageStore defaultMessageStore;
// 最大值
private int maxDelayLevel;
public ScheduleMessageService(final DefaultMessageStore defaultMessageStore) {
this.defaultMessageStore = defaultMessageStore;
}
public void buildRunningStats(HashMap<String, String> stats) {
Iterator<Entry<Integer, Long>> it = this.offsetTable.entrySet().iterator();
while (it.hasNext()) {
Entry<Integer, Long> next = it.next();
int queueId = delayLevel2QueueId(next.getKey());
long delayOffset = next.getValue();
long maxOffset = this.defaultMessageStore.getMaxOffsetInQuque(SCHEDULE_TOPIC, queueId);
String value = String.format("%d,%d", delayOffset, maxOffset);
String key = String.format("%s_%d", RunningStats.scheduleMessageOffset.name(), next.getKey());
stats.put(key, value);
}
}
public static int queueId2DelayLevel(final int queueId) {
return queueId + 1;
}
public static int delayLevel2QueueId(final int delayLevel) {
return delayLevel - 1;
}
private void updateOffset(int delayLevel, long offset) {
/**
* chen.si 更新offset的 缓存
*/
this.offsetTable.put(delayLevel, offset);
}
public long computeDeliverTimestamp(final int delayLevel, final long storeTimestamp) {
/**
* chen.si 根据deplayLevel 计算出 真实的消息延迟发送绝对时间
*/
Long time = this.delayLevelTable.get(delayLevel);
if (time != null) {
return time + storeTimestamp;
}
return storeTimestamp + 1000;
}
public void start() {
// 为每个延时队列增加定时器
for (Integer level : this.delayLevelTable.keySet()) {
Long timeDelay = this.delayLevelTable.get(level);
Long offset = this.offsetTable.get(level);
if (null == offset) {
offset = 0L;
}
if (timeDelay != null) {
this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME);
}
}
// 定时将延时进度刷盘
this.timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
try {
ScheduleMessageService.this.persist();
}
catch (Exception e) {
log.error("scheduleAtFixedRate flush exception", e);
}
}
}, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());
}
public void shutdown() {
this.timer.cancel();
}
public int getMaxDelayLevel() {
return maxDelayLevel;
}
public String encode() {
return this.encode(false);
}
public String encode(final boolean prettyFormat) {
DelayOffsetSerializeWrapper delayOffsetSerializeWrapper = new DelayOffsetSerializeWrapper();
delayOffsetSerializeWrapper.setOffsetTable(this.offsetTable);
return delayOffsetSerializeWrapper.toJson(prettyFormat);
}
@Override
public void decode(String jsonString) {
/**
* chen.si 加载 定时处理进度 文件
*/
if (jsonString != null) {
DelayOffsetSerializeWrapper delayOffsetSerializeWrapper =
DelayOffsetSerializeWrapper.fromJson(jsonString, DelayOffsetSerializeWrapper.class);
if (delayOffsetSerializeWrapper != null) {
this.offsetTable.putAll(delayOffsetSerializeWrapper.getOffsetTable());
}
}
}
@Override
public String configFilePath() {
/**
* chen.si 定时处理进度 文件: store\config\delayOffset.json
*/
return this.defaultMessageStore.getMessageStoreConfig().getDelayOffsetStorePath();
}
public boolean load() {
boolean result = super.load();
result = result && this.parseDelayLevel();
return result;
}
public boolean parseDelayLevel() {
HashMap<String, Long> timeUnitTable = new HashMap<String, Long>();
timeUnitTable.put("s", 1000L);
timeUnitTable.put("m", 1000L * 60);
timeUnitTable.put("h", 1000L * 60 * 60);
timeUnitTable.put("d", 1000L * 60 * 60 * 24);
String levelString = this.defaultMessageStore.getMessageStoreConfig().getMessageDelayLevel();
try {
String[] levelArray = levelString.split(" ");
for (int i = 0; i < levelArray.length; i++) {
String value = levelArray[i];
String ch = value.substring(value.length() - 1);
Long tu = timeUnitTable.get(ch);
int level = i + 1;
if (level > this.maxDelayLevel) {
this.maxDelayLevel = level;
}
long num = Long.parseLong(value.substring(0, value.length() - 1));
long delayTimeMillis = tu * num;
this.delayLevelTable.put(level, delayTimeMillis);
}
}
catch (Exception e) {
log.error("parseDelayLevel exception", e);
log.info("levelString String = {}", levelString);
return false;
}
return true;
}
class DeliverDelayedMessageTimerTask extends TimerTask {
private final int delayLevel;
/**
* chen.si queue的logic offset
*/
private final long offset;
public DeliverDelayedMessageTimerTask(int delayLevel, long offset) {
this.delayLevel = delayLevel;
this.offset = offset;
}
@Override
public void run() {
try {
this.executeOnTimeup();
}
catch (Exception e) {
log.error("executeOnTimeup exception", e);
ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(
this.delayLevel, this.offset), DELAY_FOR_A_PERIOD);
}
}
public void executeOnTimeup() {
/**
* chen.si 获取 delayLevel 对应的 consume queue
*/
ConsumeQueue cq =
ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(SCHEDULE_TOPIC,
delayLevel2QueueId(delayLevel));
if (cq != null) {
/**
* chen.si 从 指定的 位置 开始,寻找待发送消息
*/
SelectMapedBufferResult bufferCQ = cq.getIndexBuffer(this.offset);
if (bufferCQ != null) {
try {
/**
* chen.si 记录 定时队列 的处理进度
*/
long nextOffset = offset;
int i = 0;
/**
* chen.si: https://github.com/alibaba/RocketMQ/issues/470
*
* 定时服务 处理 cq中的定时消息时,将当前文件的可用缓冲区 的 到期消息 一次全部 写入commit log,才会更新offset。
假设这样的场景:极端情况下,整个文件的到期消息都写入commit log完成,但是此时宕机,offset没来得及更新,最终整个文件的到期消息会全部被重新处理写入commit log一遍。
在大量使用定时消息时,这样造成的消息重复量太大。
建议增加如下功能:
cq的到期消息一次批量处理 超过X条,立刻更新offset
超过Y条到期消息 被处理, 也触发 定时处理进度写磁盘 的操作(目前是Y秒会写一次,Y可配置)
*/
for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQStoreUnitSize) {
/**
* chen.si 定时消息的3个索引信息
*/
long offsetPy = bufferCQ.getByteBuffer().getLong();
int sizePy = bufferCQ.getByteBuffer().getInt();
long tagsCode = bufferCQ.getByteBuffer().getLong();
// 队列里存储的tagsCode实际是一个时间点
long deliverTimestamp = tagsCode;
/**
* chen.si 计算下一个定时消息的位置
*/
nextOffset = offset + (i / ConsumeQueue.CQStoreUnitSize);
/**
* chen.si 是否到期了
*/
long countdown = deliverTimestamp - System.currentTimeMillis();
// 时间到了,该投递
if (countdown <= 0) {
/**
* chen.si 从commit log中找到定时的数据消息
*/
MessageExt msgExt =
ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset(
offsetPy, sizePy);
if (msgExt != null) {
/**
* chen.si 重新构建 到期的 数据消息
*/
MessageExtBrokerInner msgInner = this.messageTimeup(msgExt);
/**
* chen.si 作为普通消息,放入commit log
*/
PutMessageResult putMessageResult =
ScheduleMessageService.this.defaultMessageStore
.putMessage(msgInner);
// 成功
if (putMessageResult != null
&& putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) {
/**
* chen.si 继续读取文件,尝试下一条消息
*/
continue;
}
// 失败
else {
/**
* chen.si 当前到期的消息 处理失败,只能跳过忽略。 进行下一条消息的处理
*/
log.error(
"a message time up, but reput it failed, topic: {} msgId {}",
msgExt.getTopic(), msgExt.getMsgId());
/**
* chen.si TODO 这个重新启动timer,为什么间隔10s这么长,会导致消息不及时被处理吧
*/
ScheduleMessageService.this.timer.schedule(
new DeliverDelayedMessageTimerTask(this.delayLevel, nextOffset),
DELAY_FOR_A_PERIOD);
/**
* chen.si 更新当前定时队列的 处理进度
*/
ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
return;
}
}
}
// 时候未到,继续定时
else {
/**
* chen.si 精确控制,只等待 剩余的超时间隔
*/
ScheduleMessageService.this.timer.schedule(
new DeliverDelayedMessageTimerTask(this.delayLevel, nextOffset),
countdown);
/**
* chen.si 更新当前定时队列的 处理进度
*/
ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
return;
}
} // end of for
/**
* chen.si 当前的定时消息缓冲 处理结束,后续从nextOff接着处理
*/
nextOffset = offset + (i / ConsumeQueue.CQStoreUnitSize);
ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(
this.delayLevel, nextOffset), DELAY_FOR_A_WHILE);
ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
return;
}
finally {
// 必须释放资源
bufferCQ.release();
}
} // end of if (bufferCQ != null)
} // end of if (cq != null)
/**
* chen.si 如果cq 或者 buffer 未生成,则 下一次再检查
*/
ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel,
this.offset), DELAY_FOR_A_WHILE);
}
private MessageExtBrokerInner messageTimeup(MessageExt msgExt) {
MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
msgInner.setBody(msgExt.getBody());
msgInner.setFlag(msgExt.getFlag());
msgInner.setProperties(msgExt.getProperties());
TopicFilterType topicFilterType = MessageExt.parseTopicFilterType(msgInner.getSysFlag());
long tagsCodeValue =
MessageExtBrokerInner.tagsString2tagsCode(topicFilterType, msgInner.getTags());
msgInner.setTagsCode(tagsCodeValue);
msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties()));
msgInner.setSysFlag(msgExt.getSysFlag());
msgInner.setBornTimestamp(msgExt.getBornTimestamp());
msgInner.setBornHost(msgExt.getBornHost());
msgInner.setStoreHost(msgExt.getStoreHost());
msgInner.setReconsumeTimes(msgExt.getReconsumeTimes());
msgInner.setWaitStoreMsgOK(false);
/**
* chen.si 已经到期,需要作为普通消息进行处理,去除 定时 的属性
*/
msgInner.clearProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL);
/**
* chen.si 借助定时队列 的 topic 和 queueId,来记录定时消息。 到期后,需要恢复topic和queueId,准备重新放入commit log,作为普通消息处理
*/
// 恢复Topic
msgInner.setTopic(msgInner.getProperty(MessageConst.PROPERTY_REAL_TOPIC));
// 恢复QueueId
String queueIdStr = msgInner.getProperty(MessageConst.PROPERTY_REAL_QUEUE_ID);
int queueId = Integer.parseInt(queueIdStr);
msgInner.setQueueId(queueId);
return msgInner;
}
}
}