统一节点请求生命周期并释放重连资源
This commit is contained in:
@@ -44,9 +44,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@Slf4j
|
||||
public class RemoteService {
|
||||
|
||||
ChannelFuture channelFuture;
|
||||
volatile ChannelFuture channelFuture;
|
||||
|
||||
Channel channel;
|
||||
volatile Channel channel;
|
||||
|
||||
@Value("${remote.ip:5.255.110.45}")
|
||||
String ip;
|
||||
@@ -62,7 +62,12 @@ public class RemoteService {
|
||||
ConcurrentHashMap<Integer, CopyOnWriteArrayList<CompletableFuture<String>>> retryStatusWaiters =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
EventLoop eventLoopGroup = new DefaultEventLoop();
|
||||
final EventLoop eventLoopGroup = new DefaultEventLoop();
|
||||
final EventLoopGroup networkGroup = new NioEventLoopGroup(2);
|
||||
final AtomicBoolean connecting = new AtomicBoolean();
|
||||
final AtomicBoolean monitoring = new AtomicBoolean();
|
||||
volatile boolean stopping;
|
||||
volatile ServerSocket monitorSocket;
|
||||
|
||||
ExecutorService downloadThread = Executors.newCachedThreadPool();
|
||||
|
||||
@@ -89,20 +94,22 @@ public class RemoteService {
|
||||
|
||||
@PostConstruct
|
||||
void init() {
|
||||
if(!initChannel()){ //如果远程服务器连接失败,则开启本地监听
|
||||
monitor = new Thread(this::monitorFunc);
|
||||
monitor.start();
|
||||
}
|
||||
initChannel();
|
||||
}
|
||||
|
||||
public boolean initChannel(){
|
||||
if (stopping || !connecting.compareAndSet(false, true))
|
||||
return !isDead();
|
||||
try {
|
||||
if (!isDead())
|
||||
return true;
|
||||
int i;
|
||||
for(i=0; i<20; i++) {
|
||||
try {
|
||||
channelFuture = new Bootstrap()
|
||||
.channel(NioSocketChannel.class)
|
||||
.group(new NioEventLoopGroup())
|
||||
.group(networkGroup)
|
||||
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3_000)
|
||||
.handler(new ChannelInitializer<NioSocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(NioSocketChannel channel) {
|
||||
@@ -114,9 +121,14 @@ public class RemoteService {
|
||||
}
|
||||
}).connect(new InetSocketAddress(ip, port + i)).sync();
|
||||
break;
|
||||
}catch (Exception e){
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.error("连接storageNode失败,端口偏移量(重试次数):{}", i);
|
||||
}
|
||||
if (stopping)
|
||||
return false;
|
||||
}
|
||||
|
||||
//超过二十次连不上,主动抛出错误,由下方catch
|
||||
@@ -124,11 +136,16 @@ public class RemoteService {
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
if (stopping) {
|
||||
channelFuture.channel().close();
|
||||
return false;
|
||||
}
|
||||
log.info("connect success");
|
||||
if(pushService != null)
|
||||
pushService.storageNodeOnline();
|
||||
|
||||
channel = channelFuture.channel();
|
||||
closeMonitorSocket();
|
||||
channel.writeAndFlush(new IdentityMessage("lionwebsite"));
|
||||
|
||||
//子节点上线时,发送未完成的任务
|
||||
@@ -138,6 +155,10 @@ public class RemoteService {
|
||||
}catch (Exception e){
|
||||
log.error("connect node failed, wait for node back online", e);
|
||||
return false;
|
||||
} finally {
|
||||
connecting.set(false);
|
||||
if (isDead())
|
||||
startMonitor();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +168,7 @@ public class RemoteService {
|
||||
return -2;
|
||||
}
|
||||
|
||||
channelFuture.channel().close();
|
||||
channelFuture.channel().close().awaitUninterruptibly();
|
||||
|
||||
if(initChannel()){
|
||||
return 0;
|
||||
@@ -157,34 +178,49 @@ public class RemoteService {
|
||||
}
|
||||
|
||||
public byte checkAvailability(){
|
||||
AvailableCheckMessage acm = new AvailableCheckMessage();
|
||||
acm.setMessageId(atomicInteger.getAndIncrement());
|
||||
return sendRequest(new AvailableCheckMessage(), 10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
channel.writeAndFlush(acm);
|
||||
byte sendRequest(AbstractMessage message, long timeout, TimeUnit unit) {
|
||||
Channel target = channel;
|
||||
if (stopping || target == null || !target.isActive())
|
||||
return -1;
|
||||
message.setMessageId(atomicInteger.getAndIncrement());
|
||||
DefaultPromise<AbstractMessage> promise = new DefaultPromise<>(eventLoopGroup);
|
||||
promiseHashMap.put(acm.messageId, promise);
|
||||
promiseHashMap.put(message.messageId, promise);
|
||||
try {
|
||||
boolean result = promise.await(10, TimeUnit.SECONDS);
|
||||
if(result){
|
||||
ResponseMessage rsm = (ResponseMessage)promise.getNow();
|
||||
return rsm.getResult();
|
||||
}
|
||||
else return -1;
|
||||
}catch (InterruptedException e){
|
||||
log.warn("checkAvailability interrupted", e);
|
||||
target.writeAndFlush(message).addListener(future -> {
|
||||
if (!future.isSuccess())
|
||||
promise.tryFailure(future.cause() == null ? new IOException("节点发送失败") : future.cause());
|
||||
});
|
||||
if (promise.await(timeout, unit) && promise.isSuccess()
|
||||
&& promise.getNow() instanceof ResponseMessage response)
|
||||
return response.getResult();
|
||||
return -1;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return -1;
|
||||
} catch (Exception e) {
|
||||
log.warn("节点请求失败: messageId={}", message.messageId, e);
|
||||
return -1;
|
||||
} finally {
|
||||
promiseHashMap.remove(message.messageId, promise);
|
||||
}
|
||||
}
|
||||
|
||||
/** 请求将当前全部订阅状态异步同步到存储节点,短时间内的多次请求会合并。 */
|
||||
public void requestSubscriptionSync() {
|
||||
if (!subscriptionSyncEnabled)
|
||||
if (stopping || !subscriptionSyncEnabled)
|
||||
return;
|
||||
subscriptionSyncQueued.set(true);
|
||||
if (!subscriptionSyncRunning.compareAndSet(false, true))
|
||||
return;
|
||||
subscriptionSyncExecutor.execute(this::drainSubscriptionSyncQueue);
|
||||
try {
|
||||
subscriptionSyncExecutor.execute(this::drainSubscriptionSyncQueue);
|
||||
} catch (java.util.concurrent.RejectedExecutionException e) {
|
||||
subscriptionSyncRunning.set(false);
|
||||
if (!stopping) throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "${subscription.standby.retry-interval-ms:60000}")
|
||||
@@ -207,32 +243,15 @@ public class RemoteService {
|
||||
}
|
||||
|
||||
private void syncSubscriptionSnapshotOnce() {
|
||||
SubscriptionSnapshotMessage message = null;
|
||||
DefaultPromise<AbstractMessage> promise = null;
|
||||
try {
|
||||
message = subscriptionStandbySnapshotService.build();
|
||||
message.setMessageId(atomicInteger.getAndIncrement());
|
||||
promise = new DefaultPromise<>(eventLoopGroup);
|
||||
promiseHashMap.put(message.messageId, promise);
|
||||
channel.writeAndFlush(message);
|
||||
if (promise.await(30, TimeUnit.SECONDS)) {
|
||||
AbstractMessage reply = promise.getNow();
|
||||
if (reply instanceof ResponseMessage response && (response.getResult() == 0 || response.getResult() == 3))
|
||||
log.info("订阅快照同步完成 revision={} result={}", shortRevision(message.getRevision()), response.getResult());
|
||||
else
|
||||
log.warn("订阅快照同步失败 revision={} result={}", shortRevision(message.getRevision()),
|
||||
reply instanceof ResponseMessage response ? response.getResult() : "invalid-response");
|
||||
} else {
|
||||
log.warn("订阅快照同步超时 revision={}", shortRevision(message.getRevision()));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("订阅快照同步线程被中断");
|
||||
SubscriptionSnapshotMessage message = subscriptionStandbySnapshotService.build();
|
||||
byte result = sendRequest(message, 30, TimeUnit.SECONDS);
|
||||
if (result == 0 || result == 3)
|
||||
log.info("订阅快照同步完成 revision={} result={}", shortRevision(message.getRevision()), result);
|
||||
else
|
||||
log.warn("订阅快照同步失败或超时 revision={} result={}", shortRevision(message.getRevision()), result);
|
||||
} catch (Exception e) {
|
||||
log.warn("生成或发送订阅快照失败: {}", e.getMessage());
|
||||
} finally {
|
||||
if (message != null && promise != null)
|
||||
promiseHashMap.remove(message.messageId, promise);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,8 +260,24 @@ public class RemoteService {
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
void shutdownSubscriptionSync() {
|
||||
void shutdownResources() {
|
||||
stopping = true;
|
||||
closeMonitorSocket();
|
||||
if (monitor != null) monitor.interrupt();
|
||||
failPendingRequests();
|
||||
if (channel != null) channel.close();
|
||||
subscriptionSyncExecutor.shutdownNow();
|
||||
downloadThread.shutdownNow();
|
||||
networkGroup.shutdownGracefully();
|
||||
eventLoopGroup.shutdownGracefully();
|
||||
}
|
||||
|
||||
private void failPendingRequests() {
|
||||
promiseHashMap.forEach((id, promise) -> promise.tryFailure(new IOException("节点连接已关闭")));
|
||||
promiseHashMap.clear();
|
||||
retryStatusWaiters.forEach((gid, waiters) ->
|
||||
waiters.forEach(waiter -> waiter.completeExceptionally(new IOException("节点连接已关闭"))));
|
||||
retryStatusWaiters.clear();
|
||||
}
|
||||
|
||||
public boolean isDead(){
|
||||
@@ -266,28 +301,9 @@ public class RemoteService {
|
||||
GalleryTask galleryTask = new GalleryTask();
|
||||
galleryTask.setGid(gallery.getGid());
|
||||
galleryTask.setName(gallery.getName());
|
||||
|
||||
DownloadPostMessage dpm = new DownloadPostMessage();
|
||||
dpm.messageId = atomicInteger.getAndIncrement();
|
||||
dpm.setGalleryTask(galleryTask);
|
||||
|
||||
DefaultPromise<AbstractMessage> promise = new DefaultPromise<>(eventLoopGroup);
|
||||
promiseHashMap.put(dpm.messageId, promise);
|
||||
channel.writeAndFlush(dpm);
|
||||
try {
|
||||
boolean result = promise.await(10, TimeUnit.SECONDS);
|
||||
if(result){
|
||||
ResponseMessage rsm = (ResponseMessage)promise.getNow();
|
||||
return rsm.getResult();
|
||||
}
|
||||
else return -1;
|
||||
}catch (InterruptedException e){
|
||||
log.warn("addGalleryToQueue interrupted", e);
|
||||
Thread.currentThread().interrupt();
|
||||
return -1;
|
||||
}finally {
|
||||
promiseHashMap.remove(dpm.messageId, promise);
|
||||
}
|
||||
DownloadPostMessage message = new DownloadPostMessage();
|
||||
message.setGalleryTask(galleryTask);
|
||||
return sendRequest(message, 10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public RetryResult retryGallery(Gallery gallery){
|
||||
@@ -325,49 +341,51 @@ public class RemoteService {
|
||||
public record RetryResult(boolean success, String message) {}
|
||||
|
||||
public byte deleteGallery(Gallery gallery){
|
||||
DeleteGalleryMessage dgm = new DeleteGalleryMessage();
|
||||
dgm.setGalleryName(gallery.getName());
|
||||
dgm.messageId = atomicInteger.getAndIncrement();
|
||||
DeleteGalleryMessage message = new DeleteGalleryMessage();
|
||||
message.setGalleryName(gallery.getName());
|
||||
return sendRequest(message, 10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
channel.writeAndFlush(dgm);
|
||||
DefaultPromise<AbstractMessage> promise = new DefaultPromise<>(eventLoopGroup);
|
||||
promiseHashMap.put(dgm.messageId, promise);
|
||||
try{
|
||||
boolean result = promise.await(10, TimeUnit.SECONDS);
|
||||
if(result){
|
||||
ResponseMessage rsm = (ResponseMessage) promise.getNow();
|
||||
return rsm.getResult();
|
||||
}else return -1;
|
||||
}catch (InterruptedException e){
|
||||
log.warn("deleteGallery interrupted", e);
|
||||
Thread.currentThread().interrupt();
|
||||
return -1;
|
||||
private void startMonitor() {
|
||||
if (stopping || !monitoring.compareAndSet(false, true))
|
||||
return;
|
||||
monitor = new Thread(this::monitorFunc, "storage-node-monitor");
|
||||
monitor.setDaemon(true);
|
||||
monitor.start();
|
||||
}
|
||||
|
||||
private void closeMonitorSocket() {
|
||||
ServerSocket socket = monitorSocket;
|
||||
if (socket != null) {
|
||||
try { socket.close(); }
|
||||
catch (IOException e) { log.debug("关闭节点监听失败", e); }
|
||||
}
|
||||
}
|
||||
|
||||
public void monitorFunc(){
|
||||
int real_port = CustomUtil._findIdlePort(port + 1);
|
||||
log.info("监听端口: {}等待节点上线", real_port);
|
||||
try(ServerSocket socket = new ServerSocket(real_port)) {
|
||||
Socket client;
|
||||
while(true){
|
||||
client = socket.accept();
|
||||
|
||||
if(client.getInetAddress().getHostAddress().equals(ip)){
|
||||
//连接之后发送lionwebsite,否则存储节点不能确认这个端口是否有效
|
||||
OutputStream outputStream = client.getOutputStream();
|
||||
outputStream.write("lionwebsite".getBytes());
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
log.info("尝试连接");
|
||||
initChannel();
|
||||
client.close();
|
||||
socket.close();
|
||||
break;
|
||||
try (ServerSocket socket = new ServerSocket(CustomUtil._findIdlePort(port + 1))) {
|
||||
monitorSocket = socket;
|
||||
if (stopping || !isDead())
|
||||
return;
|
||||
log.info("监听端口: {}等待节点上线", socket.getLocalPort());
|
||||
while (!stopping) {
|
||||
try (Socket client = socket.accept()) {
|
||||
if (!client.getInetAddress().getHostAddress().equals(ip))
|
||||
continue;
|
||||
OutputStream output = client.getOutputStream();
|
||||
output.write("lionwebsite".getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
output.flush();
|
||||
client.shutdownOutput();
|
||||
if (initChannel())
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
if (!stopping && isDead())
|
||||
log.warn("等待节点上线失败", e);
|
||||
} finally {
|
||||
monitorSocket = null;
|
||||
monitoring.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,7 +434,7 @@ public class RemoteService {
|
||||
else if(msg instanceof ResponseMessage rsm) {
|
||||
Promise<AbstractMessage> promise = promiseHashMap.remove(rsm.messageId);
|
||||
if(promise != null)
|
||||
promise.setSuccess(rsm);
|
||||
promise.trySuccess(rsm);
|
||||
else
|
||||
log.warn("收到无等待者的响应消息: messageId={}", rsm.messageId);
|
||||
}
|
||||
@@ -424,13 +442,12 @@ public class RemoteService {
|
||||
|
||||
@Override
|
||||
public void channelUnregistered(ChannelHandlerContext ctx) {
|
||||
log.info("{}", ctx.channel());
|
||||
log.info("{}", channel);
|
||||
if(ctx.channel() != null && ctx.channel().remoteAddress().toString().equals(channel.remoteAddress().toString())){
|
||||
log.info("activate monitor thread, waiting for node back online");
|
||||
pushService.storageNodeOffline();
|
||||
monitor = new Thread(RemoteService.this::monitorFunc);
|
||||
monitor.start();
|
||||
if (ctx.channel() == channel) {
|
||||
failPendingRequests();
|
||||
if (!stopping) {
|
||||
pushService.storageNodeOffline();
|
||||
startMonitor();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.lion.lionwebsite.Service;
|
||||
|
||||
import com.lion.lionwebsite.Dao.normal.GalleryMapper;
|
||||
import com.lion.lionwebsite.Message.*;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class RemoteServiceTest {
|
||||
private RemoteService service() {
|
||||
return new RemoteService(mock(GalleryMapper.class), mock(PushService.class),
|
||||
mock(WebSocketService.class), mock(SubscriptionStandbySnapshotService.class));
|
||||
}
|
||||
|
||||
@Test void immediateResponseHasRegisteredWaiter() {
|
||||
RemoteService service = service();
|
||||
EmbeddedChannel channel = new EmbeddedChannel(new ChannelOutboundHandlerAdapter() {
|
||||
@Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
|
||||
AbstractMessage request = (AbstractMessage) msg;
|
||||
ctx.fireChannelRead(new ResponseMessage(request.messageId, (byte) 0));
|
||||
promise.setSuccess();
|
||||
}
|
||||
}, service.new MyChannelInboundHandlerAdapter());
|
||||
service.channel = channel;
|
||||
try {
|
||||
assertEquals(0, service.checkAvailability());
|
||||
assertTrue(service.promiseHashMap.isEmpty());
|
||||
} finally { service.shutdownResources(); channel.finishAndReleaseAll(); }
|
||||
}
|
||||
|
||||
@Test void timeoutAndWriteFailureRemoveWaiters() {
|
||||
RemoteService service = service();
|
||||
EmbeddedChannel channel = new EmbeddedChannel(new ChannelOutboundHandlerAdapter() {
|
||||
@Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
|
||||
promise.setSuccess(); // no reply
|
||||
}
|
||||
});
|
||||
service.channel = channel;
|
||||
try {
|
||||
assertEquals(-1, service.sendRequest(new AvailableCheckMessage(), 1, TimeUnit.MILLISECONDS));
|
||||
assertTrue(service.promiseHashMap.isEmpty());
|
||||
channel.pipeline().addLast(new ChannelOutboundHandlerAdapter() {
|
||||
@Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
|
||||
promise.setFailure(new IOException("test failure"));
|
||||
}
|
||||
});
|
||||
assertEquals(-1, service.sendRequest(new AvailableCheckMessage(), 1, TimeUnit.SECONDS));
|
||||
assertTrue(service.promiseHashMap.isEmpty());
|
||||
} finally { service.shutdownResources(); channel.finishAndReleaseAll(); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user