7
7
import uuid
8
8
import warnings
9
9
from collections import defaultdict
10
+ from datetime import datetime
10
11
from pathlib import Path
11
12
from typing import Callable , List , Literal , Optional , Protocol , Tuple , Type , Union
12
13
30
31
from .logging import LogLevel , configure_logging
31
32
from .models import (
32
33
DeserializerType ,
34
+ MessageContext ,
35
+ Row ,
33
36
SerializerType ,
34
37
TimestampExtractor ,
35
38
Topic ,
@@ -153,6 +156,7 @@ def __init__(
153
156
topic_create_timeout : float = 60 ,
154
157
processing_guarantee : ProcessingGuarantee = "at-least-once" ,
155
158
max_partition_buffer_size : int = 10000 ,
159
+ heartbeat_interval : float = 0.0 ,
156
160
):
157
161
"""
158
162
:param broker_address: Connection settings for Kafka.
@@ -222,6 +226,12 @@ def __init__(
222
226
It is a soft limit, and the actual number of buffered messages can be up to x2 higher.
223
227
Lower value decreases the memory use, but increases the latency.
224
228
Default - `10000`.
229
+ :param heartbeat_interval: the interval (seconds) at which to send heartbeat messages.
230
+ The heartbeat timing starts counting from application start.
231
+ TODO: Save and respect last heartbeat timestamp.
232
+ The heartbeat is sent for every partition of every topic with registered heartbeat streams.
233
+ If the value is 0, no heartbeat messages will be sent.
234
+ Default - `0.0`.
225
235
226
236
<br><br>***Error Handlers***<br>
227
237
To handle errors, `Application` accepts callbacks triggered when
@@ -370,6 +380,10 @@ def __init__(
370
380
recovery_manager = recovery_manager ,
371
381
)
372
382
383
+ self ._heartbeat_active = heartbeat_interval > 0
384
+ self ._heartbeat_interval = heartbeat_interval
385
+ self ._heartbeat_last_sent = datetime .now ().timestamp ()
386
+
373
387
self ._source_manager = SourceManager ()
374
388
self ._sink_manager = SinkManager ()
375
389
self ._dataframe_registry = DataFrameRegistry ()
@@ -899,6 +913,7 @@ def _run_dataframe(self, sink: Optional[VoidExecutor] = None):
899
913
processing_context = self ._processing_context
900
914
source_manager = self ._source_manager
901
915
process_message = self ._process_message
916
+ process_heartbeat = self ._process_heartbeat
902
917
printer = self ._processing_context .printer
903
918
run_tracker = self ._run_tracker
904
919
consumer = self ._consumer
@@ -911,6 +926,9 @@ def _run_dataframe(self, sink: Optional[VoidExecutor] = None):
911
926
)
912
927
913
928
dataframes_composed = self ._dataframe_registry .compose_all (sink = sink )
929
+ heartbeats_composed = self ._dataframe_registry .compose_heartbeats ()
930
+ if not heartbeats_composed :
931
+ self ._heartbeat_active = False
914
932
915
933
processing_context .init_checkpoint ()
916
934
run_tracker .set_as_running ()
@@ -922,6 +940,7 @@ def _run_dataframe(self, sink: Optional[VoidExecutor] = None):
922
940
run_tracker .timeout_refresh ()
923
941
else :
924
942
process_message (dataframes_composed )
943
+ process_heartbeat (heartbeats_composed )
925
944
processing_context .commit_checkpoint ()
926
945
consumer .resume_backpressured ()
927
946
source_manager .raise_for_error ()
@@ -1005,6 +1024,41 @@ def _process_message(self, dataframe_composed):
1005
1024
if self ._on_message_processed is not None :
1006
1025
self ._on_message_processed (topic_name , partition , offset )
1007
1026
1027
+ def _process_heartbeat (self , heartbeats_composed ):
1028
+ if not self ._heartbeat_active :
1029
+ return
1030
+
1031
+ now = datetime .now ().timestamp ()
1032
+ if self ._heartbeat_last_sent > now - self ._heartbeat_interval :
1033
+ return
1034
+
1035
+ value , key , timestamp , headers = None , None , int (now * 1000 ), {}
1036
+
1037
+ for tp in self ._consumer .assignment ():
1038
+ if executor := heartbeats_composed .get (tp .topic ):
1039
+ row = Row (
1040
+ value = value ,
1041
+ key = key ,
1042
+ timestamp = timestamp ,
1043
+ context = MessageContext (
1044
+ topic = tp .topic ,
1045
+ partition = tp .partition ,
1046
+ offset = - 1 , # TODO: get correct offsets
1047
+ size = - 1 ,
1048
+ ),
1049
+ headers = headers ,
1050
+ )
1051
+ context = copy_context ()
1052
+ context .run (set_message_context , row .context )
1053
+ try :
1054
+ context .run (executor , value , key , timestamp , headers )
1055
+ except Exception as exc :
1056
+ to_suppress = self ._on_processing_error (exc , row , logger )
1057
+ if not to_suppress :
1058
+ raise
1059
+
1060
+ self ._heartbeat_last_sent = now
1061
+
1008
1062
def _on_assign (self , _ , topic_partitions : List [TopicPartition ]):
1009
1063
"""
1010
1064
Assign new topic partitions to consumer and state.
0 commit comments