@@ -26,7 +26,10 @@ public class HybridClusterTransport : IClusterTransport
2626 private readonly NodeInfo _nodeInfo ;
2727 private readonly ConcurrentDictionary < string , NodeInfo > _knownNodes ;
2828 private readonly CancellationTokenSource _cancellationTokenSource ;
29- private readonly ConcurrentDictionary < string , DateTime > _processedMessageIds ; // 已处理的消息ID,用于去重
29+ // 已处理的消息ID,用于去重
30+ // Key format: "MessageId:FromNodeId" for broadcast messages, "MessageId:FromNodeId:ToNodeId" for targeted messages
31+ // 键格式:广播消息为 "MessageId:FromNodeId",定向消息为 "MessageId:FromNodeId:ToNodeId"
32+ private readonly ConcurrentDictionary < string , DateTime > _processedMessageIds ;
3033 private readonly Timer _messageIdCleanupTimer ; // 定期清理过期的消息ID
3134 private bool _disposed = false ;
3235 private bool _started = false ;
@@ -217,6 +220,20 @@ public async Task SendAsync(string nodeId, ClusterMessage message)
217220 message . FromNodeId = _nodeId ;
218221 message . ToNodeId = nodeId ;
219222
223+ // Ensure MessageId is set for deduplication / 确保设置 MessageId 以便去重
224+ // If MessageId is empty, generate a unique one based on node and timestamp
225+ // 如果 MessageId 为空,基于节点和时间戳生成唯一ID
226+ if ( string . IsNullOrEmpty ( message . MessageId ) )
227+ {
228+ message . MessageId = $ "{ _nodeId } :{ nodeId } :{ DateTime . UtcNow . Ticks } :{ Guid . NewGuid ( ) : N} ";
229+ }
230+
231+ // Ensure Timestamp is set / 确保设置时间戳
232+ if ( message . Timestamp == default )
233+ {
234+ message . Timestamp = DateTime . UtcNow ;
235+ }
236+
220237 var routingKey = $ "node.{ nodeId } ";
221238 var messageBytes = Encoding . UTF8 . GetBytes ( JsonSerializer . Serialize ( message ) ) ;
222239
@@ -254,6 +271,20 @@ public async Task BroadcastAsync(ClusterMessage message)
254271 message . FromNodeId = _nodeId ;
255272 message . ToNodeId = null ; // Broadcast / 广播
256273
274+ // Ensure MessageId is set for deduplication / 确保设置 MessageId 以便去重
275+ // If MessageId is empty, generate a unique one
276+ // 如果 MessageId 为空,生成唯一ID
277+ if ( string . IsNullOrEmpty ( message . MessageId ) )
278+ {
279+ message . MessageId = $ "{ _nodeId } :broadcast:{ DateTime . UtcNow . Ticks } :{ Guid . NewGuid ( ) : N} ";
280+ }
281+
282+ // Ensure Timestamp is set / 确保设置时间戳
283+ if ( message . Timestamp == default )
284+ {
285+ message . Timestamp = DateTime . UtcNow ;
286+ }
287+
257288 var messageBytes = Encoding . UTF8 . GetBytes ( JsonSerializer . Serialize ( message ) ) ;
258289
259290 var properties = new MessageProperties
@@ -274,6 +305,73 @@ public async Task BroadcastAsync(ClusterMessage message)
274305 }
275306 }
276307
308+ /// <summary>
309+ /// Send message to multiple nodes / 向多个节点发送消息
310+ /// </summary>
311+ /// <param name="nodeIds">Target node IDs / 目标节点 ID 列表</param>
312+ /// <param name="message">Message to send / 要发送的消息</param>
313+ /// <returns>Dictionary of node ID and send result / 节点ID和发送结果的字典</returns>
314+ /// <remarks>
315+ /// This method sends the same message to multiple nodes. Each node will receive a copy of the message.
316+ /// If you want to send different messages to different nodes, call SendAsync multiple times.
317+ /// 此方法向多个节点发送相同的消息。每个节点都会收到消息的副本。
318+ /// 如果要向不同节点发送不同的消息,请多次调用 SendAsync。
319+ /// </remarks>
320+ public async Task < Dictionary < string , bool > > SendToMultipleNodesAsync ( IEnumerable < string > nodeIds , ClusterMessage message )
321+ {
322+ if ( nodeIds == null )
323+ {
324+ throw new ArgumentNullException ( nameof ( nodeIds ) ) ;
325+ }
326+
327+ if ( message == null )
328+ {
329+ throw new ArgumentNullException ( nameof ( message ) ) ;
330+ }
331+
332+ var results = new Dictionary < string , bool > ( ) ;
333+ var tasks = new List < Task > ( ) ;
334+
335+ foreach ( var nodeId in nodeIds )
336+ {
337+ if ( string . IsNullOrEmpty ( nodeId ) )
338+ {
339+ continue ;
340+ }
341+
342+ var task = Task . Run ( async ( ) =>
343+ {
344+ try
345+ {
346+ // Create a copy of the message for each node to ensure unique MessageId per node
347+ // 为每个节点创建消息副本,确保每个节点都有唯一的 MessageId
348+ var nodeMessage = new ClusterMessage
349+ {
350+ Type = message . Type ,
351+ FromNodeId = message . FromNodeId ,
352+ ToNodeId = nodeId ,
353+ Payload = message . Payload ,
354+ MessageId = message . MessageId , // Will be auto-generated if empty / 如果为空将自动生成
355+ Timestamp = message . Timestamp
356+ } ;
357+
358+ await SendAsync ( nodeId , nodeMessage ) ;
359+ results [ nodeId ] = true ;
360+ }
361+ catch ( Exception ex )
362+ {
363+ _logger . LogError ( ex , $ "Failed to send message to node { nodeId } ") ;
364+ results [ nodeId ] = false ;
365+ }
366+ } ) ;
367+
368+ tasks . Add ( task ) ;
369+ }
370+
371+ await Task . WhenAll ( tasks ) ;
372+ return results ;
373+ }
374+
277375 /// <summary>
278376 /// Check if node is connected / 检查节点是否已连接
279377 /// </summary>
@@ -373,23 +471,44 @@ private async Task<bool> HandleMessageAsync(byte[] body, MessageProperties prope
373471 if ( ! string . IsNullOrEmpty ( message . ToNodeId ) && message . ToNodeId != _nodeId )
374472 {
375473 // This message is for another node, ignore it / 此消息是发送给其他节点的,忽略它
376- _logger . LogDebug ( $ "Ignoring message { message . MessageId } - target node is { message . ToNodeId } , current node is { _nodeId } ") ;
474+ // Note: This is expected behavior for targeted messages. Use BroadcastAsync to send to all nodes.
475+ // 注意:这是定向消息的预期行为。使用 BroadcastAsync 可以向所有节点发送消息。
476+ _logger . LogTrace ( $ "Ignoring message { message . MessageId } (type: { message . Type } ) - target node is { message . ToNodeId } , current node is { _nodeId } . This is normal for targeted messages.") ;
377477 return true ; // Ack to remove from queue / 确认以从队列中移除
378478 }
379479
380480 // Check for duplicate messages using message ID / 使用消息ID检查重复消息
481+ // For broadcast messages, use "MessageId:FromNodeId" as key to allow same message from same sender to be processed once per node
482+ // For targeted messages, use "MessageId:FromNodeId:ToNodeId" to allow same message to different targets
483+ // 对于广播消息,使用 "MessageId:FromNodeId" 作为键,允许来自同一发送者的相同消息在每个节点上处理一次
484+ // 对于定向消息,使用 "MessageId:FromNodeId:ToNodeId" 允许相同消息发送到不同目标
381485 if ( ! string . IsNullOrEmpty ( message . MessageId ) )
382486 {
487+ // Create unique key based on message type / 根据消息类型创建唯一键
488+ string deduplicationKey ;
489+ if ( string . IsNullOrEmpty ( message . ToNodeId ) )
490+ {
491+ // Broadcast message: same message from same sender should be processed once per node
492+ // 广播消息:来自同一发送者的相同消息应在每个节点上处理一次
493+ deduplicationKey = $ "{ message . MessageId } :{ message . FromNodeId } ";
494+ }
495+ else
496+ {
497+ // Targeted message: same message can be sent to different nodes
498+ // 定向消息:相同消息可以发送到不同节点
499+ deduplicationKey = $ "{ message . MessageId } :{ message . FromNodeId } :{ message . ToNodeId } ";
500+ }
501+
383502 // Check if we've already processed this message / 检查是否已处理过此消息
384- if ( _processedMessageIds . TryGetValue ( message . MessageId , out var processedTime ) )
503+ if ( _processedMessageIds . TryGetValue ( deduplicationKey , out var processedTime ) )
385504 {
386505 // Message already processed, ignore it / 消息已处理,忽略它
387- _logger . LogDebug ( $ "Ignoring duplicate message { message . MessageId } (processed at { processedTime : yyyy-MM-dd HH:mm:ss} )") ;
506+ _logger . LogDebug ( $ "Ignoring duplicate message { message . MessageId } (key: { deduplicationKey } , processed at { processedTime : yyyy-MM-dd HH:mm:ss} )") ;
388507 return true ; // Ack to remove from queue / 确认以从队列中移除
389508 }
390509
391510 // Mark message as processed / 标记消息为已处理
392- _processedMessageIds . TryAdd ( message . MessageId , DateTime . UtcNow ) ;
511+ _processedMessageIds . TryAdd ( deduplicationKey , DateTime . UtcNow ) ;
393512 }
394513
395514 // Trigger message received event / 触发消息接收事件
0 commit comments