/*
* (C) Copyright 2015-2016 the original author or authors.
*
* 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.
*
* Contributors:
* ohun@live.cn (夜色)
*/
package com.mpush.client.push;
import com.mpush.api.Constants;
import com.mpush.api.push.*;
import com.mpush.api.router.ClientLocation;
import com.mpush.client.gateway.connection.GatewayConnectionFactory;
import com.mpush.common.message.gateway.GatewayPushMessage;
import com.mpush.common.push.GatewayPushResult;
import com.mpush.common.router.CachedRemoteRouterManager;
import com.mpush.common.router.RemoteRouter;
import com.mpush.tools.Jsons;
import com.mpush.tools.common.TimeLine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicReference;
/**
* Created by ohun on 2015/12/30.
*
* @author ohun@live.cn
*/
public final class PushRequest extends FutureTask<PushResult> {
private static final Logger LOGGER = LoggerFactory.getLogger(PushRequest.class);
private static final Callable<PushResult> NONE = () -> new PushResult(PushResult.CODE_FAILURE);
private enum Status {init, success, failure, offline, timeout}
private final AtomicReference<Status> status = new AtomicReference<>(Status.init);
private final TimeLine timeLine = new TimeLine("Push-Time-Line");
private final GatewayConnectionFactory connectionFactory;
private AckModel ackModel;
private Set<String> tags;
private String condition;
private PushCallback callback;
private String userId;
private byte[] content;
private int timeout;
private ClientLocation location;
private int sessionId;
private Future<?> future;
private PushResult result;
private void sendToConnServer(RemoteRouter remoteRouter) {
timeLine.addTimePoint("lookup-remote");
if (remoteRouter != null) {
location = remoteRouter.getRouteValue();
}
if (remoteRouter == null || remoteRouter.isOffline()) {
//1.1没有查到说明用户已经下线
offline();
return;
}
timeLine.addTimePoint("check-gateway-conn");
//2.通过网关连接,把消息发送到所在机器
boolean success = connectionFactory.send(
location.getHostAndPort(),
connection -> GatewayPushMessage
.build(connection)
.setUserId(userId)
.setContent(content)
.setClientType(location.getClientType())
.setTimeout(timeout - 500)
.setTags(tags)
.addFlag(ackModel.flag)
,
pushMessage -> {
timeLine.addTimePoint("send-to-gateway-begin");
pushMessage.sendRaw(f -> {
timeLine.addTimePoint("send-to-gateway-end");
if (f.isSuccess()) {
LOGGER.debug("send to gateway server success, location={}, conn={}", location, f.channel());
} else {
LOGGER.error("send to gateway server failure, location={}, conn={}", location, f.channel(), f.cause());
failure();
}
});
PushRequest.this.content = null;//释放内存
sessionId = pushMessage.getSessionId();
future = PushRequestBus.I.put(sessionId, PushRequest.this);
}
);
if (!success) {
LOGGER.error("get gateway connection failure, location={}", location);
failure();
}
}
private void submit(Status status) {
if (this.status.compareAndSet(Status.init, status)) {//防止重复调用
boolean isTimeoutEnd = status == Status.timeout;//任务是否超时结束
if (future != null && !isTimeoutEnd) {//是超时结束任务不用再取消一次
future.cancel(true);//取消超时任务
}
this.timeLine.end();//结束时间流统计
super.set(getResult());//设置同步调用的返回结果
if (callback != null) {//回调callback
if (isTimeoutEnd) {//超时结束时,当前线程已经是线程池里的线程,直接调用callback
callback.onResult(getResult());
} else {//非超时结束时,当前线程为Netty线程池,要异步执行callback
PushRequestBus.I.asyncCall(this);//会执行run方法
}
}
}
LOGGER.info("push request {} end, {}, {}, {}", status, userId, location, timeLine);
}
/**
* run方法会有两个地方的线程调用
* 1. 任务超时时会调用,见PushRequestBus.I.put(sessionId, PushRequest.this);
* 2. 异步执行callback的时候,见PushRequestBus.I.asyncCall(this);
*/
@Override
public void run() {
//判断任务是否超时,如果超时了此时状态是init,否则应该是其他状态, 因为从submit方法过来的状态都不是init
if (status.get() == Status.init) {
timeout();
} else {
callback.onResult(getResult());
}
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
throw new UnsupportedOperationException();
}
public FutureTask<PushResult> send(RemoteRouter router) {
timeLine.begin();
sendToConnServer(router);
return this;
}
public FutureTask<PushResult> broadcast() {
timeLine.begin();
boolean success = connectionFactory.broadcast(
connection -> GatewayPushMessage
.build(connection)
.setUserId(userId)
.setContent(content)
.setTags(tags)
.setCondition(condition)
.addFlag(ackModel.flag),
pushMessage -> {
pushMessage.sendRaw(f -> {
if (f.isSuccess()) {
LOGGER.debug("send broadcast to gateway server success, userId={}, conn={}", userId, f.channel());
} else {
failure();
LOGGER.error("send broadcast to gateway server failure, userId={}, conn={}", userId, f.channel(), f.cause());
}
});
if (pushMessage.taskId == null) {
sessionId = pushMessage.getSessionId();
future = PushRequestBus.I.put(sessionId, PushRequest.this);
} else {
success();
}
}
);
if (!success) {
LOGGER.error("get gateway connection failure when broadcast.");
failure();
}
return this;
}
private void offline() {
CachedRemoteRouterManager.I.invalidateLocalCache(userId);
submit(Status.offline);
}
private void timeout() {
if (PushRequestBus.I.getAndRemove(sessionId) != null) {
submit(Status.timeout);
}
}
private void success() {
submit(Status.success);
}
private void failure() {
submit(Status.failure);
}
public void onFailure() {
failure();
}
public void onRedirect() {
timeLine.addTimePoint("redirect");
LOGGER.warn("user route has changed, userId={}, location={}", userId, location);
CachedRemoteRouterManager.I.invalidateLocalCache(userId);
if (status.get() == Status.init) {//表示任务还没有完成,还可以重新发送
RemoteRouter remoteRouter = CachedRemoteRouterManager.I.lookup(userId, location.getClientType());
send(remoteRouter);
}
}
public FutureTask<PushResult> onOffline() {
offline();
return this;
}
public void onSuccess(GatewayPushResult result) {
if (result != null) timeLine.addTimePoints(result.timePoints);
submit(Status.success);
}
public long getTimeout() {
return timeout;
}
public PushRequest(GatewayConnectionFactory factory) {
super(NONE);
this.connectionFactory = factory;
}
public static PushRequest build(GatewayConnectionFactory factory, PushContext ctx) {
byte[] content = ctx.getContext();
PushMsg msg = ctx.getPushMsg();
if (msg != null) {
String json = Jsons.toJson(msg);
if (json != null) {
content = json.getBytes(Constants.UTF_8);
}
}
Objects.requireNonNull(content, "push content can not be null.");
return new PushRequest(factory)
.setAckModel(ctx.getAckModel())
.setUserId(ctx.getUserId())
.setTags(ctx.getTags())
.setCondition(ctx.getCondition())
.setContent(content)
.setTimeout(ctx.getTimeout())
.setCallback(ctx.getCallback());
}
private PushResult getResult() {
if (result == null) {
result = new PushResult(status.get().ordinal())
.setUserId(userId)
.setLocation(location)
.setTimeLine(timeLine.getTimePoints());
}
return result;
}
public PushRequest setCallback(PushCallback callback) {
this.callback = callback;
return this;
}
public PushRequest setUserId(String userId) {
this.userId = userId;
return this;
}
public PushRequest setContent(byte[] content) {
this.content = content;
return this;
}
public PushRequest setTimeout(int timeout) {
this.timeout = timeout;
return this;
}
public PushRequest setAckModel(AckModel ackModel) {
this.ackModel = ackModel;
return this;
}
public PushRequest setTags(Set<String> tags) {
this.tags = tags;
return this;
}
public PushRequest setCondition(String condition) {
this.condition = condition;
return this;
}
@Override
public String toString() {
return "PushRequest{" +
"content='" + (content == null ? -1 : content.length) + '\'' +
", userId='" + userId + '\'' +
", timeout=" + timeout +
", location=" + location +
'}';
}
}