2020import org .apache .eventmesh .api .SendCallback ;
2121import org .apache .eventmesh .api .SendResult ;
2222import org .apache .eventmesh .api .storage .MeshStoragePlugin ;
23+ import org .apache .eventmesh .api .storage .OffsetExtensions ;
2324import org .apache .eventmesh .runtime .delivery .DeadLetterSink ;
2425import org .apache .eventmesh .runtime .delivery .PushChannel ;
2526import org .apache .eventmesh .runtime .delivery .ReliableDispatcher ;
2627import org .apache .eventmesh .runtime .metrics .UniMetrics ;
2728import org .apache .eventmesh .runtime .metrics .UniTrace ;
29+ import org .apache .eventmesh .runtime .offset .InMemoryPushOffsetStore ;
2830import org .apache .eventmesh .runtime .offset .OffsetStore ;
31+ import org .apache .eventmesh .runtime .offset .PushOffsetStore ;
2932import org .apache .eventmesh .runtime .push .BufferedEvent ;
3033import org .apache .eventmesh .runtime .push .LongPollingChannel ;
3134import org .apache .eventmesh .runtime .push .PushService ;
@@ -69,6 +72,7 @@ public class UniIngressService {
6972
7073 private final MeshStoragePlugin storage ;
7174 private final OffsetStore offsetStore ;
75+ private final PushOffsetStore pushOffsetStore ;
7276 private final SubscriptionManager subscriptionManager ;
7377 private final ReliableDispatcher dispatcher ;
7478 private final PushService pushService ;
@@ -84,7 +88,6 @@ public class UniIngressService {
8488 private final UniMetrics metrics ;
8589
8690 private final ConcurrentHashMap <String , PushChannel > channels = new ConcurrentHashMap <>();
87- private final ConcurrentHashMap <String , AtomicLong > topicOffsetSeq = new ConcurrentHashMap <>();
8891 private final ConcurrentHashMap <String , CompletableFuture <CloudEvent >> pendingRequests = new ConcurrentHashMap <>();
8992 private final AtomicLong requestSeq = new AtomicLong ();
9093 private final ConcurrentHashMap <String , TokenBucketRateLimiter > topicLimiters = new ConcurrentHashMap <>();
@@ -99,19 +102,20 @@ public class UniIngressService {
99102 public static final String EXT_CORRELATION_ID = "emcorrelationid" ;
100103
101104 public UniIngressService (MeshStoragePlugin storage , OffsetStore offsetStore ) {
102- this (storage , offsetStore , new SubscriptionManager (), new PushService (),
105+ this (storage , offsetStore , new InMemoryPushOffsetStore (), new SubscriptionManager (), new PushService (),
103106 ReliableDispatcher .DEFAULT_ACK_TIMEOUT_MS , ReliableDispatcher .DEFAULT_MAX_ATTEMPTS ,
104107 System ::currentTimeMillis );
105108 }
106109
107110 /**
108111 * Test-friendly constructor with an injectable clock and retry parameters.
109112 */
110- public UniIngressService (MeshStoragePlugin storage , OffsetStore offsetStore ,
113+ public UniIngressService (MeshStoragePlugin storage , OffsetStore offsetStore , PushOffsetStore pushOffsetStore ,
111114 SubscriptionManager subscriptionManager , PushService pushService ,
112115 long ackTimeoutMs , int maxAttempts , java .util .function .LongSupplier clock ) {
113116 this .storage = storage ;
114117 this .offsetStore = offsetStore ;
118+ this .pushOffsetStore = pushOffsetStore ;
115119 this .subscriptionManager = subscriptionManager ;
116120 this .pushService = pushService ;
117121 this .metrics = new UniMetrics ();
@@ -299,9 +303,15 @@ private int pullAndDispatchPartition(String topic, int partition, int maxEvents,
299303 // Multi-instance: route via the cluster coordinator (local vs cross-instance forward).
300304 cluster .dispatch (topic , event );
301305 } else {
302- long offset = nextOffset (topic );
306+ // Read MQ physical offset from CloudEvent extension (written by storage plugin on poll)
307+ long mqOffset = OffsetExtensions .readMqOffset (event );
308+ int mqPartition = OffsetExtensions .readMqPartition (event );
303309 for (Subscription target : subscriptionManager .targetsFor (topic , event )) {
304- dispatcher .deliver (topic , partition , offset , event , target .getClientId (), channelFor (target .getClientId ()));
310+ dispatcher .deliver (topic , mqPartition , mqOffset , event , target .getClientId (), channelFor (target .getClientId ()));
311+ // Record push watermark for offset tracking
312+ if (mqOffset >= 0 && mqPartition >= 0 ) {
313+ pushOffsetStore .writePushOffset (topic , target .getClientId (), mqPartition , mqOffset );
314+ }
305315 }
306316 }
307317 UniTrace .end (dispatchSpan );
@@ -335,8 +345,14 @@ private boolean isExpired(CloudEvent event) {
335345 * same-instance targets here while forwarding remote ones.
336346 */
337347 public boolean deliverLocal (String topic , String clientId , CloudEvent event ) {
338- long offset = nextOffset (topic );
339- dispatcher .deliver (topic , -1 , offset , event , clientId , channelFor (clientId ));
348+ // Read MQ physical offset from CloudEvent extension (written by storage plugin on poll)
349+ long mqOffset = OffsetExtensions .readMqOffset (event );
350+ int mqPartition = OffsetExtensions .readMqPartition (event );
351+ dispatcher .deliver (topic , mqPartition , mqOffset , event , clientId , channelFor (clientId ));
352+ // Record push watermark for offset tracking
353+ if (mqOffset >= 0 && mqPartition >= 0 ) {
354+ pushOffsetStore .writePushOffset (topic , clientId , mqPartition , mqOffset );
355+ }
340356 return true ;
341357 }
342358
@@ -571,7 +587,7 @@ public void registerRuntimeGauges() {
571587 });
572588
573589 metrics .registerLabelledGauge ("eventmesh_offset_lag" ,
574- "MQ end offset - distributed offset (per topic/partition)" ,
590+ "MQ end offset - max ACK offset (per topic/partition) — total consumer lag " ,
575591 () -> {
576592 java .util .List <UniMetrics .LabelledLong > out = new java .util .ArrayList <>();
577593 if (partitionOwnership == null ) {
@@ -582,27 +598,72 @@ public void registerRuntimeGauges() {
582598 if (owned == null ) {
583599 continue ;
584600 }
585- // Max distributed offset per partition across all clients (key = clientId#partition).
586- java .util .Map <Integer , Long > maxByPart = new java .util .HashMap <>();
601+ // Max ACK offset per partition across all clients (key = clientId#partition).
602+ java .util .Map <Integer , Long > maxAckByPart = new java .util .HashMap <>();
587603 for (java .util .Map .Entry <String , Long > e : offsetStore .readAllOffsets (t ).entrySet ()) {
588604 int sep = e .getKey ().lastIndexOf ('#' );
589605 if (sep > 0 ) {
590606 try {
591607 int p = Integer .parseInt (e .getKey ().substring (sep + 1 ));
592- maxByPart .merge (p , e .getValue (), Math ::max );
608+ maxAckByPart .merge (p , e .getValue (), Math ::max );
593609 } catch (NumberFormatException expected ) {
594610 }
595611 }
596612 }
597613 for (int p : owned ) {
598614 long end = storage .endOffset (t , p );
599- long dist = maxByPart .getOrDefault (p , -1L );
600- if (end >= 0 && dist >= 0 ) {
615+ long ack = maxAckByPart .getOrDefault (p , -1L );
616+ if (end >= 0 && ack >= 0 ) {
617+ out .add (new UniMetrics .LabelledLong (
618+ io .opentelemetry .api .common .Attributes .of (
619+ io .opentelemetry .api .common .AttributeKey .stringKey ("topic" ), t ,
620+ io .opentelemetry .api .common .AttributeKey .longKey ("partition" ), (long ) p ),
621+ Math .max (0 , end - ack )));
622+ }
623+ }
624+ }
625+ return out ;
626+ });
627+
628+ metrics .registerLabelledGauge ("eventmesh_push_ack_lag" ,
629+ "max push offset - max ACK offset (per topic/partition) — in-flight deliveries" ,
630+ () -> {
631+ java .util .List <UniMetrics .LabelledLong > out = new java .util .ArrayList <>();
632+ for (String t : subscriptionManager .activeTopics ()) {
633+ // Max push offset per partition across all clients
634+ java .util .Map <Integer , Long > maxPushByPart = new java .util .HashMap <>();
635+ for (java .util .Map .Entry <String , Long > e : pushOffsetStore .readAllPushOffsets (t ).entrySet ()) {
636+ int sep = e .getKey ().lastIndexOf ('#' );
637+ if (sep > 0 ) {
638+ try {
639+ int p = Integer .parseInt (e .getKey ().substring (sep + 1 ));
640+ maxPushByPart .merge (p , e .getValue (), Math ::max );
641+ } catch (NumberFormatException expected ) {
642+ }
643+ }
644+ }
645+ // Max ACK offset per partition across all clients
646+ java .util .Map <Integer , Long > maxAckByPart = new java .util .HashMap <>();
647+ for (java .util .Map .Entry <String , Long > e : offsetStore .readAllOffsets (t ).entrySet ()) {
648+ int sep = e .getKey ().lastIndexOf ('#' );
649+ if (sep > 0 ) {
650+ try {
651+ int p = Integer .parseInt (e .getKey ().substring (sep + 1 ));
652+ maxAckByPart .merge (p , e .getValue (), Math ::max );
653+ } catch (NumberFormatException expected ) {
654+ }
655+ }
656+ }
657+ for (java .util .Map .Entry <Integer , Long > pushEntry : maxPushByPart .entrySet ()) {
658+ int p = pushEntry .getKey ();
659+ long pushOff = pushEntry .getValue ();
660+ long ackOff = maxAckByPart .getOrDefault (p , -1L );
661+ if (pushOff >= 0 && ackOff >= 0 ) {
601662 out .add (new UniMetrics .LabelledLong (
602663 io .opentelemetry .api .common .Attributes .of (
603664 io .opentelemetry .api .common .AttributeKey .stringKey ("topic" ), t ,
604665 io .opentelemetry .api .common .AttributeKey .longKey ("partition" ), (long ) p ),
605- Math .max (0 , end - dist )));
666+ Math .max (0 , pushOff - ackOff )));
606667 }
607668 }
608669 }
@@ -675,8 +736,11 @@ private PushChannel channelFor(String clientId) {
675736 return channels .computeIfAbsent (clientId , id -> new LongPollingChannel (pushService , id ));
676737 }
677738
678- private long nextOffset (String topic ) {
679- return topicOffsetSeq .computeIfAbsent (topic , k -> new AtomicLong ()).incrementAndGet ();
739+ /**
740+ * Expose PushOffsetStore for metrics and admin queries.
741+ */
742+ public PushOffsetStore getPushOffsetStore () {
743+ return pushOffsetStore ;
680744 }
681745
682746 private DeadLetterSink deadLetterSink () {
0 commit comments