---初始化项目

This commit is contained in:
2025-09-19 16:14:08 +08:00
parent 902d3d7e3b
commit afee7c03ac
767 changed files with 75809 additions and 82 deletions

View File

@ -0,0 +1,122 @@
package tech.powerjob.remote.mu;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import tech.powerjob.common.serialize.JsonUtils;
import tech.powerjob.remote.framework.base.Address;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Channel manager for maintaining worker address to channel mapping
* Supports both tell and ask modes for reverse communication
*
* @author claude
* @since 2025/1/1
*/
@Slf4j
public class ChannelManager {
private final ConcurrentMap<String, Channel> workerChannels = new ConcurrentHashMap<>();
private final ConcurrentMap<String, CompletableFuture<Object>> pendingRequests = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Class<?>> requestResponseTypes = new ConcurrentHashMap<>();
/**
* Register a worker channel
* @param workerAddress worker address
* @param channel Netty channel
*/
public void registerWorkerChannel(Address workerAddress, Channel channel) {
String key = workerAddress.getHost() + ":" + workerAddress.getPort();
workerChannels.put(key, channel);
log.info("[ChannelManager] Registered worker channel: {}", key);
// Remove channel when it becomes inactive
channel.closeFuture().addListener(future -> {
workerChannels.remove(key);
log.info("[ChannelManager] Removed inactive worker channel: {}", key);
});
}
/**
* Get channel for worker
* @param workerAddress worker address
* @return Channel or null if not found
*/
public Channel getWorkerChannel(Address workerAddress) {
String key = workerAddress.getHost() + ":" + workerAddress.getPort();
return workerChannels.get(key);
}
/**
* Store pending request for ask mode
* @param requestId request ID
* @param future future to complete when response received
* @param responseType expected response type
*/
public void registerPendingRequest(String requestId, CompletableFuture<Object> future, Class<?> responseType) {
pendingRequests.put(requestId, future);
requestResponseTypes.put(requestId, responseType);
}
/**
* Complete pending request with response
* @param requestId request ID
* @param response response object
*/
public void completePendingRequest(String requestId, Object response) {
CompletableFuture<Object> future = pendingRequests.remove(requestId);
Class<?> responseType = requestResponseTypes.remove(requestId);
if (future != null) {
Object convertedResponse = convertResponse(response, responseType);
future.complete(convertedResponse);
} else {
log.warn("[ChannelManager] No pending request found for ID: {}", requestId);
}
}
/**
* Complete pending request with exception
* @param requestId request ID
* @param exception exception
*/
public void completePendingRequestExceptionally(String requestId, Throwable exception) {
CompletableFuture<Object> future = pendingRequests.remove(requestId);
requestResponseTypes.remove(requestId); // Clean up response type mapping
if (future != null) {
future.completeExceptionally(exception);
} else {
log.warn("[ChannelManager] No pending request found for ID: {}", requestId);
}
}
/**
* Remove pending request (timeout cleanup)
* @param requestId request ID
*/
public void removePendingRequest(String requestId) {
pendingRequests.remove(requestId);
requestResponseTypes.remove(requestId);
}
/**
* Convert response to expected type
* @param response raw response object
* @param responseType expected response type
* @return converted response
*/
private Object convertResponse(Object response, Class<?> responseType) {
if (response == null || responseType == null) {
return response;
}
if (responseType.isInstance(response)) {
return response;
}
return JsonUtils.toJavaObject(response, responseType);
}
}

View File

@ -0,0 +1,179 @@
package tech.powerjob.remote.mu;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;
import tech.powerjob.remote.framework.actor.ActorInfo;
import tech.powerjob.remote.framework.base.ServerType;
import tech.powerjob.remote.framework.cs.CSInitializer;
import tech.powerjob.remote.framework.cs.CSInitializerConfig;
import tech.powerjob.remote.framework.transporter.Transporter;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Mu CSInitializer implementation using Netty
* Supports bidirectional communication with worker-only outbound connectivity
*
* @author claude
* @since 2025/1/1
*/
@Slf4j
public class MuCSInitializer implements CSInitializer {
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
private Channel serverChannel;
private CSInitializerConfig config;
private final ChannelManager channelManager = new ChannelManager();
private MuServerHandler serverHandler;
private MuWorkerHandler workerHandler;
private MuConnectionManager connectionManager;
@Override
public String type() {
return "MU";
}
@Override
public void init(CSInitializerConfig config) {
this.config = config;
bossGroup = new NioEventLoopGroup(1);
workerGroup = new NioEventLoopGroup();
if (config.getServerType() == ServerType.SERVER) {
initServer();
} else {
initWorker();
}
}
private void initServer() {
try {
serverHandler = new MuServerHandler(channelManager);
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new IdleStateHandler(0, 0, 60, TimeUnit.SECONDS))
.addLast(new MuMessageCodec())
.addLast(serverHandler);
}
});
ChannelFuture future = bootstrap.bind(
config.getBindAddress().getHost(),
config.getBindAddress().getPort()
).sync();
serverChannel = future.channel();
log.info("[MuCSInitializer] Server started on {}:{}",
config.getBindAddress().getHost(),
config.getBindAddress().getPort());
// 初始化连接管理器用于Server连接到其他Server
connectionManager = new MuConnectionManager(workerGroup, channelManager, serverHandler, config.getBindAddress());
log.info("[MuCSInitializer] Server initialized with client capabilities for server-to-server communication");
} catch (Exception e) {
log.error("[MuCSInitializer] Failed to start server", e);
throw new RuntimeException("Failed to start Mu server", e);
}
}
private void initWorker() {
try {
// Worker需要同时具备服务端和客户端能力
// 服务端接受其他Worker的连接
// 客户端连接到Server或其他Worker
// 初始化handler
workerHandler = new MuWorkerHandler(channelManager);
// 启动服务端接受其他Worker的连接
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new IdleStateHandler(0, 0, 60, TimeUnit.SECONDS))
.addLast(new MuMessageCodec())
.addLast(workerHandler);
}
});
ChannelFuture serverFuture = serverBootstrap.bind(
config.getBindAddress().getHost(),
config.getBindAddress().getPort()
).sync();
serverChannel = serverFuture.channel();
log.info("[MuCSInitializer] Worker server started on {}:{}",
config.getBindAddress().getHost(),
config.getBindAddress().getPort());
// 初始化连接管理器,用于连接到其他节点
connectionManager = new MuConnectionManager(workerGroup, channelManager, workerHandler, config.getBindAddress());
log.info("[MuCSInitializer] Worker initialized with server and client capabilities");
} catch (Exception e) {
log.error("[MuCSInitializer] Failed to initialize worker", e);
throw new RuntimeException("Failed to initialize Mu worker", e);
}
}
@Override
public Transporter buildTransporter() {
return new MuTransporter(channelManager, config.getServerType(), connectionManager);
}
@Override
public void bindHandlers(List<ActorInfo> actorInfos) {
if (config.getServerType() == ServerType.SERVER && serverHandler != null) {
serverHandler.bindHandlers(actorInfos);
} else if (config.getServerType() == ServerType.WORKER && workerHandler != null) {
workerHandler.bindHandlers(actorInfos);
}
}
@Override
public void close() throws IOException {
try {
if (serverChannel != null) {
serverChannel.close().sync();
}
if (connectionManager != null) {
connectionManager.closeAllConnections();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("[MuCSInitializer] Interrupted while closing channels", e);
} finally {
if (bossGroup != null) {
bossGroup.shutdownGracefully();
}
if (workerGroup != null) {
workerGroup.shutdownGracefully();
}
}
log.info("[MuCSInitializer] Mu CSInitializer closed");
}
}

View File

@ -0,0 +1,136 @@
package tech.powerjob.remote.mu;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;
import tech.powerjob.remote.framework.base.Address;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
/**
* Connection manager for worker-side lazy connection to server
*
* @author claude
* @since 2025/1/1
*/
@Slf4j
public class MuConnectionManager {
private final EventLoopGroup workerGroup;
private final ChannelManager channelManager;
private final Object messageHandler; // Can be MuWorkerHandler or MuServerHandler
private final Address localAddress;
private final ConcurrentMap<String, Channel> serverConnections = new ConcurrentHashMap<>();
private final ConcurrentMap<String, CompletableFuture<Channel>> pendingConnections = new ConcurrentHashMap<>();
public MuConnectionManager(EventLoopGroup workerGroup, ChannelManager channelManager,
Object messageHandler, Address localAddress) {
this.workerGroup = workerGroup;
this.channelManager = channelManager;
this.messageHandler = messageHandler;
this.localAddress = localAddress;
}
/**
* Get or create connection to target address
* @param targetAddress target address
* @return CompletableFuture of channel
*/
public CompletableFuture<Channel> getOrCreateConnection(Address targetAddress) {
String key = targetAddress.getHost() + ":" + targetAddress.getPort();
// Check if we already have an active connection
Channel existingChannel = serverConnections.get(key);
if (existingChannel != null && existingChannel.isActive()) {
return CompletableFuture.completedFuture(existingChannel);
}
// Check if there's already a pending connection
CompletableFuture<Channel> pendingConnection = pendingConnections.get(key);
if (pendingConnection != null) {
return pendingConnection;
}
// Create new connection
CompletableFuture<Channel> connectionFuture = new CompletableFuture<>();
pendingConnections.put(key, connectionFuture);
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new IdleStateHandler(0, 30, 0, TimeUnit.SECONDS))
.addLast(new MuMessageCodec())
.addLast((io.netty.channel.ChannelHandler) messageHandler);
// Only add heartbeat handler for Worker connections
if (messageHandler instanceof MuWorkerHandler) {
ch.pipeline().addLast(new MuWorkerHeartbeatHandler(localAddress));
}
}
});
ChannelFuture future = bootstrap.connect(targetAddress.getHost(), targetAddress.getPort());
future.addListener(f -> {
pendingConnections.remove(key);
if (f.isSuccess()) {
Channel channel = future.channel();
serverConnections.put(key, channel);
// Remove connection when it becomes inactive
channel.closeFuture().addListener(closeFuture -> {
serverConnections.remove(key);
log.info("[MuConnectionManager] Removed inactive server connection: {}", key);
});
connectionFuture.complete(channel);
log.info("[MuConnectionManager] Connected to server: {}", key);
} else {
connectionFuture.completeExceptionally(f.cause());
log.error("[MuConnectionManager] Failed to connect to server: {}", key, f.cause());
}
});
} catch (Exception e) {
pendingConnections.remove(key);
connectionFuture.completeExceptionally(e);
log.error("[MuConnectionManager] Error creating connection to server: {}", key, e);
}
return connectionFuture;
}
/**
* Close all connections
*/
public void closeAllConnections() {
for (Channel channel : serverConnections.values()) {
if (channel.isActive()) {
channel.close();
}
}
serverConnections.clear();
// Complete all pending connections with exception
for (CompletableFuture<Channel> future : pendingConnections.values()) {
future.completeExceptionally(new RuntimeException("Connection manager is closing"));
}
pendingConnections.clear();
}
}

View File

@ -0,0 +1,37 @@
package tech.powerjob.remote.mu;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import tech.powerjob.common.PowerSerializable;
import tech.powerjob.remote.framework.base.Address;
/**
* Mu protocol message format
*
* @author claude
* @since 2025/1/1
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MuMessage implements PowerSerializable {
/**
* Message types
*/
public enum MessageType {
TELL, // Fire-and-forget
ASK, // Request-response
RESPONSE, // Response to ASK
HEARTBEAT, // Worker heartbeat/registration
ERROR // Error response
}
private MessageType messageType;
private String requestId; // Unique ID for ask/response correlation
private String path; // Handler path
private Address senderAddress; // Sender address for registration
private Object payload; // Actual message payload
private String errorMessage; // Error message for ERROR type
}

View File

@ -0,0 +1,75 @@
package tech.powerjob.remote.mu;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageCodec;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* Mu message codec for encoding/decoding messages over Netty
*
* @author claude
* @since 2025/1/1
*/
@Slf4j
public class MuMessageCodec extends ByteToMessageCodec<MuMessage> {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final int MAX_MESSAGE_SIZE = 64 * 1024 * 1024; // 64MB
@Override
protected void encode(ChannelHandlerContext ctx, MuMessage msg, ByteBuf out) throws Exception {
try {
byte[] data = OBJECT_MAPPER.writeValueAsBytes(msg);
if (data.length > MAX_MESSAGE_SIZE) {
throw new IllegalArgumentException("Message too large: " + data.length + " bytes");
}
// Write message length followed by message data
out.writeInt(data.length);
out.writeBytes(data);
} catch (Exception e) {
log.error("[MuMessageCodec] Failed to encode message", e);
throw e;
}
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
try {
// Need at least 4 bytes for length
if (in.readableBytes() < 4) {
return;
}
// Mark reader index to reset if not enough data
in.markReaderIndex();
// Read message length
int length = in.readInt();
if (length <= 0 || length > MAX_MESSAGE_SIZE) {
throw new IllegalArgumentException("Invalid message length: " + length);
}
// Check if we have enough bytes for the full message
if (in.readableBytes() < length) {
in.resetReaderIndex();
return;
}
// Read and decode message
byte[] data = new byte[length];
in.readBytes(data);
MuMessage message = OBJECT_MAPPER.readValue(data, MuMessage.class);
out.add(message);
} catch (Exception e) {
log.error("[MuMessageCodec] Failed to decode message", e);
throw e;
}
}
}

View File

@ -0,0 +1,18 @@
package tech.powerjob.remote.mu;
import tech.powerjob.remote.framework.transporter.Protocol;
/**
* Mu Protocol implementation using Netty for bidirectional communication
* with support for worker-only outbound connectivity
*
* @author claude
* @since 2025/1/1
*/
public class MuProtocol implements Protocol {
@Override
public String name() {
return tech.powerjob.common.enums.Protocol.MU.name();
}
}

View File

@ -0,0 +1,185 @@
package tech.powerjob.remote.mu;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import tech.powerjob.common.serialize.JsonUtils;
import tech.powerjob.remote.framework.actor.ActorInfo;
import tech.powerjob.remote.framework.actor.HandlerInfo;
import tech.powerjob.remote.framework.utils.RemoteUtils;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* Server-side message handler for Mu protocol
* Handles incoming messages from workers and manages channel registration
*
* @author claude
* @since 2025/1/1
*/
@Slf4j
@ChannelHandler.Sharable
public class MuServerHandler extends SimpleChannelInboundHandler<MuMessage> {
private final ChannelManager channelManager;
private final Map<String, ActorInfo> handlerMap = new ConcurrentHashMap<>();
public MuServerHandler(ChannelManager channelManager) {
this.channelManager = channelManager;
}
public void bindHandlers(List<ActorInfo> actorInfos) {
for (ActorInfo actorInfo : actorInfos) {
if (actorInfo.getHandlerInfos() != null) {
for (HandlerInfo handlerInfo : actorInfo.getHandlerInfos()) {
String path = handlerInfo.getLocation().toPath();
handlerMap.put(path, actorInfo);
log.info("[MuServerHandler] Bound handler: {}", path);
}
}
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, MuMessage msg) throws Exception {
try {
switch (msg.getMessageType()) {
case HEARTBEAT:
handleHeartbeat(ctx, msg);
break;
case TELL:
handleTell(ctx, msg);
break;
case ASK:
handleAsk(ctx, msg);
break;
case RESPONSE:
handleResponse(ctx, msg);
break;
case ERROR:
handleError(ctx, msg);
break;
default:
log.warn("[MuServerHandler] Unknown message type: {}", msg.getMessageType());
}
} catch (Exception e) {
log.error("[MuServerHandler] Error processing message", e);
if (msg.getMessageType() == MuMessage.MessageType.ASK) {
// Send error response for ASK messages
MuMessage errorResponse = new MuMessage(
MuMessage.MessageType.ERROR,
msg.getRequestId(),
null,
null,
null,
"Internal server error: " + e.getMessage()
);
ctx.writeAndFlush(errorResponse);
}
}
}
private void handleHeartbeat(ChannelHandlerContext ctx, MuMessage msg) {
if (msg.getSenderAddress() != null) {
channelManager.registerWorkerChannel(msg.getSenderAddress(), ctx.channel());
log.debug("[MuServerHandler] Registered worker: {}", msg.getSenderAddress());
}
}
private void handleTell(ChannelHandlerContext ctx, MuMessage msg) {
invokeHandler(msg, false, ctx);
}
private void handleAsk(ChannelHandlerContext ctx, MuMessage msg) {
Object response = invokeHandler(msg, true, ctx);
MuMessage.MessageType responseType = response != null ?
MuMessage.MessageType.RESPONSE : MuMessage.MessageType.ERROR;
String errorMessage = response == null ? "Handler returned null" : null;
MuMessage responseMsg = new MuMessage(
responseType,
msg.getRequestId(),
null,
null,
response,
errorMessage
);
ctx.writeAndFlush(responseMsg);
}
private void handleResponse(ChannelHandlerContext ctx, MuMessage msg) {
channelManager.completePendingRequest(msg.getRequestId(), msg.getPayload());
}
private void handleError(ChannelHandlerContext ctx, MuMessage msg) {
Exception exception = new RuntimeException(msg.getErrorMessage());
channelManager.completePendingRequestExceptionally(msg.getRequestId(), exception);
}
private Object invokeHandler(MuMessage msg, boolean needResponse, ChannelHandlerContext ctx) {
try {
String path = msg.getPath();
ActorInfo actorInfo = handlerMap.get(path);
if (actorInfo == null) {
log.warn("[MuServerHandler] No handler found for path: {}", path);
return null;
}
HandlerInfo handlerInfo = actorInfo.getHandlerInfos().stream()
.filter(h -> h.getLocation().toPath().equals(path))
.findFirst()
.orElse(null);
if (handlerInfo == null) {
log.warn("[MuServerHandler] Handler info not found for path: {}", path);
return null;
}
Method method = handlerInfo.getMethod();
Optional<Class<?>> powerSerializeClz = RemoteUtils.findPowerSerialize(method.getParameterTypes());
if (!powerSerializeClz.isPresent()) {
log.error("[MuServerHandler] No PowerSerializable parameter found for handler: {}", path);
return null;
}
Object convertedPayload = convertPayload(msg.getPayload(), powerSerializeClz.get());
Object response = method.invoke(actorInfo.getActor(), convertedPayload);
if (needResponse) {
return response;
}
return null;
} catch (Exception e) {
log.error("[MuServerHandler] Failed to invoke handler for path: {}", msg.getPath(), e);
return null;
}
}
private Object convertPayload(Object payload, Class<?> targetClass) {
if (payload == null) {
return null;
}
if (targetClass.isInstance(payload)) {
return payload;
}
return JsonUtils.toJavaObject(payload, targetClass);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("[MuServerHandler] Channel exception", cause);
ctx.close();
}
}

View File

@ -0,0 +1,195 @@
package tech.powerjob.remote.mu;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import tech.powerjob.common.PowerSerializable;
import tech.powerjob.remote.framework.base.RemotingException;
import tech.powerjob.remote.framework.base.ServerType;
import tech.powerjob.remote.framework.base.URL;
import tech.powerjob.remote.framework.transporter.Protocol;
import tech.powerjob.remote.framework.transporter.Transporter;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
/**
* Mu protocol transporter implementation
* Handles both client-side (worker) and reverse (server-to-worker) communication
*
* @author claude
* @since 2025/1/1
*/
@Slf4j
public class MuTransporter implements Transporter {
private static final Protocol PROTOCOL = new MuProtocol();
private static final long ASK_TIMEOUT_SECONDS = 30;
private final ChannelManager channelManager;
private final ServerType serverType;
private final MuConnectionManager connectionManager; // For worker-side lazy connection
public MuTransporter(ChannelManager channelManager, ServerType serverType, MuConnectionManager connectionManager) {
this.channelManager = channelManager;
this.serverType = serverType;
this.connectionManager = connectionManager;
}
@Override
public Protocol getProtocol() {
return PROTOCOL;
}
@Override
public void tell(URL url, PowerSerializable request) {
try {
MuMessage message = new MuMessage(
MuMessage.MessageType.TELL,
null,
url.getLocation().toPath(),
null,
request,
null
);
if (serverType == ServerType.WORKER) {
// Worker to server/worker: use connection manager for lazy connection
connectionManager.getOrCreateConnection(url.getAddress())
.thenAccept(channel -> {
if (channel.isActive()) {
channel.writeAndFlush(message);
log.debug("[MuTransporter] Sent TELL message to {}", url);
} else {
log.error("[MuTransporter] Channel is not active for {}", url);
}
})
.exceptionally(throwable -> {
log.error("[MuTransporter] Failed to get connection for TELL to {}", url, throwable);
return null;
});
} else {
// Server side: distinguish between worker and server targets
if (url.getServerType() == ServerType.WORKER) {
// Server to worker: use stored channel from worker registration
Channel channel = channelManager.getWorkerChannel(url.getAddress());
if (channel != null && channel.isActive()) {
channel.writeAndFlush(message);
log.debug("[MuTransporter] Sent TELL message to worker {}", url);
} else {
log.error("[MuTransporter] No active channel available for worker {}", url);
throw new RemotingException("No active channel available for " + url);
}
} else {
// Server to server: use connection manager for direct connection
connectionManager.getOrCreateConnection(url.getAddress())
.thenAccept(channel -> {
if (channel.isActive()) {
channel.writeAndFlush(message);
log.debug("[MuTransporter] Sent TELL message to server {}", url);
} else {
log.error("[MuTransporter] Channel is not active for server {}", url);
}
})
.exceptionally(throwable -> {
log.error("[MuTransporter] Failed to get connection for TELL to server {}", url, throwable);
return null;
});
}
}
} catch (Exception e) {
log.error("[MuTransporter] Failed to send TELL message to {}", url, e);
throw new RemotingException("Failed to send TELL message", e);
}
}
@Override
public <T> CompletionStage<T> ask(URL url, PowerSerializable request, Class<T> clz) throws RemotingException {
try {
String requestId = UUID.randomUUID().toString();
CompletableFuture<T> future = new CompletableFuture<>();
// Register the future for response handling
channelManager.registerPendingRequest(requestId, (CompletableFuture<Object>) future, clz);
// Set timeout for the request (JDK8 compatible)
future.whenComplete((result, throwable) -> {
if (throwable != null) {
channelManager.removePendingRequest(requestId);
}
});
// Schedule timeout manually for JDK8 compatibility
java.util.concurrent.Executors.newSingleThreadScheduledExecutor().schedule(() -> {
if (!future.isDone()) {
channelManager.removePendingRequest(requestId);
future.completeExceptionally(new java.util.concurrent.TimeoutException("Request timeout after " + ASK_TIMEOUT_SECONDS + " seconds"));
}
}, ASK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
MuMessage message = new MuMessage(
MuMessage.MessageType.ASK,
requestId,
url.getLocation().toPath(),
null,
request,
null
);
if (serverType == ServerType.WORKER) {
// Worker to server/worker: use connection manager for lazy connection
connectionManager.getOrCreateConnection(url.getAddress())
.thenAccept(channel -> {
if (channel.isActive()) {
channel.writeAndFlush(message);
log.debug("[MuTransporter] Sent ASK message to {} with requestId {}", url, requestId);
} else {
channelManager.removePendingRequest(requestId);
future.completeExceptionally(new RemotingException("Channel is not active for " + url));
}
})
.exceptionally(throwable -> {
channelManager.removePendingRequest(requestId);
future.completeExceptionally(new RemotingException("Failed to get connection for ASK to " + url, throwable));
return null;
});
} else {
// Server side: distinguish between worker and server targets
if (url.getServerType() == ServerType.WORKER) {
// Server to worker: use stored channel from worker registration
Channel channel = channelManager.getWorkerChannel(url.getAddress());
if (channel != null && channel.isActive()) {
channel.writeAndFlush(message);
log.debug("[MuTransporter] Sent ASK message to worker {} with requestId {}", url, requestId);
} else {
channelManager.removePendingRequest(requestId);
future.completeExceptionally(new RemotingException("No active channel available for " + url));
}
} else {
// Server to server: use connection manager for direct connection
connectionManager.getOrCreateConnection(url.getAddress())
.thenAccept(channel -> {
if (channel.isActive()) {
channel.writeAndFlush(message);
log.debug("[MuTransporter] Sent ASK message to server {} with requestId {}", url, requestId);
} else {
channelManager.removePendingRequest(requestId);
future.completeExceptionally(new RemotingException("Channel is not active for server " + url));
}
})
.exceptionally(throwable -> {
channelManager.removePendingRequest(requestId);
future.completeExceptionally(new RemotingException("Failed to get connection for ASK to server " + url, throwable));
return null;
});
}
}
return future;
} catch (Exception e) {
log.error("[MuTransporter] Failed to send ASK message to {}", url, e);
throw new RemotingException("Failed to send ASK message", e);
}
}
}

View File

@ -0,0 +1,190 @@
package tech.powerjob.remote.mu;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import tech.powerjob.common.serialize.JsonUtils;
import tech.powerjob.remote.framework.actor.ActorInfo;
import tech.powerjob.remote.framework.actor.HandlerInfo;
import tech.powerjob.remote.framework.utils.RemoteUtils;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* Worker-side message handler for Mu protocol
* Handles incoming messages from server and processes local handlers
*
* @author claude
* @since 2025/1/1
*/
@Slf4j
@ChannelHandler.Sharable
public class MuWorkerHandler extends SimpleChannelInboundHandler<MuMessage> {
private final Map<String, ActorInfo> handlerMap = new ConcurrentHashMap<>();
private final ChannelManager channelManager;
public MuWorkerHandler(ChannelManager channelManager) {
this.channelManager = channelManager;
}
public void bindHandlers(List<ActorInfo> actorInfos) {
for (ActorInfo actorInfo : actorInfos) {
if (actorInfo.getHandlerInfos() != null) {
for (HandlerInfo handlerInfo : actorInfo.getHandlerInfos()) {
String path = handlerInfo.getLocation().toPath();
handlerMap.put(path, actorInfo);
log.info("[MuWorkerHandler] Bound handler: {}", path);
}
}
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, MuMessage msg) throws Exception {
try {
switch (msg.getMessageType()) {
case TELL:
handleTell(ctx, msg);
break;
case ASK:
handleAsk(ctx, msg);
break;
case RESPONSE:
handleResponse(ctx, msg);
break;
case ERROR:
handleError(ctx, msg);
break;
case HEARTBEAT:
handleHeartbeat(ctx, msg);
break;
default:
log.warn("[MuWorkerHandler] Unknown message type: {}", msg.getMessageType());
}
} catch (Exception e) {
log.error("[MuWorkerHandler] Error processing message", e);
if (msg.getMessageType() == MuMessage.MessageType.ASK) {
// Send error response for ASK messages
MuMessage errorResponse = new MuMessage(
MuMessage.MessageType.ERROR,
msg.getRequestId(),
null,
null,
null,
"Internal worker error: " + e.getMessage()
);
ctx.writeAndFlush(errorResponse);
}
}
}
private void handleTell(ChannelHandlerContext ctx, MuMessage msg) {
invokeHandler(msg, false, ctx);
}
private void handleAsk(ChannelHandlerContext ctx, MuMessage msg) {
Object response = invokeHandler(msg, true, ctx);
MuMessage.MessageType responseType = response != null ?
MuMessage.MessageType.RESPONSE : MuMessage.MessageType.ERROR;
String errorMessage = response == null ? "Handler returned null" : null;
MuMessage responseMsg = new MuMessage(
responseType,
msg.getRequestId(),
null,
null,
response,
errorMessage
);
ctx.writeAndFlush(responseMsg);
}
private void handleResponse(ChannelHandlerContext ctx, MuMessage msg) {
channelManager.completePendingRequest(msg.getRequestId(), msg.getPayload());
}
private void handleError(ChannelHandlerContext ctx, MuMessage msg) {
Exception exception = new RuntimeException(msg.getErrorMessage());
channelManager.completePendingRequestExceptionally(msg.getRequestId(), exception);
}
private void handleHeartbeat(ChannelHandlerContext ctx, MuMessage msg) {
// Worker接收到心跳消息时通常不需要特殊处理
// 但记录一下调试信息,表明收到了心跳
if (msg.getSenderAddress() != null) {
log.debug("[MuWorkerHandler] Received heartbeat from: {}", msg.getSenderAddress());
} else {
log.debug("[MuWorkerHandler] Received heartbeat");
}
}
private Object invokeHandler(MuMessage msg, boolean needResponse, ChannelHandlerContext ctx) {
try {
String path = msg.getPath();
ActorInfo actorInfo = handlerMap.get(path);
if (actorInfo == null) {
log.warn("[MuWorkerHandler] No handler found for path: {}", path);
return null;
}
HandlerInfo handlerInfo = actorInfo.getHandlerInfos().stream()
.filter(h -> h.getLocation().toPath().equals(path))
.findFirst()
.orElse(null);
if (handlerInfo == null) {
log.warn("[MuWorkerHandler] Handler info not found for path: {}", path);
return null;
}
Method method = handlerInfo.getMethod();
Optional<Class<?>> powerSerializeClz = RemoteUtils.findPowerSerialize(method.getParameterTypes());
if (!powerSerializeClz.isPresent()) {
log.error("[MuWorkerHandler] No PowerSerializable parameter found for handler: {}", path);
return null;
}
Object convertedPayload = convertPayload(msg.getPayload(), powerSerializeClz.get());
Object response = method.invoke(actorInfo.getActor(), convertedPayload);
if (needResponse) {
return response;
}
return null;
} catch (Exception e) {
log.error("[MuWorkerHandler] Failed to invoke handler for path: {}", msg.getPath(), e);
return null;
}
}
@SneakyThrows
private Object convertPayload(Object payload, Class<?> targetClass) {
if (payload == null) {
return null;
}
if (targetClass.isInstance(payload)) {
return payload;
}
return JsonUtils.toJavaObject(payload, targetClass);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("[MuWorkerHandler] Channel exception", cause);
ctx.close();
}
}

View File

@ -0,0 +1,61 @@
package tech.powerjob.remote.mu;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import tech.powerjob.remote.framework.base.Address;
/**
* Worker heartbeat handler for maintaining connection and registration
*
* @author claude
* @since 2025/1/1
*/
@Slf4j
public class MuWorkerHeartbeatHandler extends ChannelInboundHandlerAdapter {
private final Address workerAddress;
public MuWorkerHeartbeatHandler(Address workerAddress) {
this.workerAddress = workerAddress;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
sendHeartbeat(ctx);
log.info("[MuWorkerHeartbeatHandler] Worker connected and sent initial heartbeat");
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.WRITER_IDLE) {
sendHeartbeat(ctx);
log.debug("[MuWorkerHeartbeatHandler] Sent heartbeat");
}
}
super.userEventTriggered(ctx, evt);
}
private void sendHeartbeat(ChannelHandlerContext ctx) {
MuMessage heartbeat = new MuMessage(
MuMessage.MessageType.HEARTBEAT,
null,
null,
workerAddress,
null,
null
);
ctx.writeAndFlush(heartbeat);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("[MuWorkerHeartbeatHandler] Exception in heartbeat handler", cause);
ctx.close();
}
}