Skip to content

Commit 5f2c89a

Browse files
authored
feat: Nack support for Sources (#252)
Signed-off-by: Sreekanth <prsreekanth920@gmail.com>
1 parent 1ecb5c7 commit 5f2c89a

13 files changed

Lines changed: 305 additions & 59 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ lint: format
1515

1616

1717
test:
18-
poetry run pytest tests/
18+
poetry run pytest tests/ -rA
1919

2020

2121
requirements:

examples/source/simple_source/example.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import uuid
22
from datetime import datetime
3+
import logging
34

45
from pynumaflow.shared.asynciter import NonBlockingIterator
56
from pynumaflow.sourcer import (
@@ -12,21 +13,29 @@
1213
get_default_partitions,
1314
Sourcer,
1415
SourceAsyncServer,
16+
NackRequest,
1517
)
1618

19+
logging.basicConfig(
20+
level=logging.INFO,
21+
format="%(asctime)s %(levelname)-8s %(message)s",
22+
datefmt="%Y-%m-%d %H:%M:%S",
23+
)
24+
logger = logging.getLogger(__name__)
25+
1726

1827
class AsyncSource(Sourcer):
1928
"""
2029
AsyncSource is a class for User Defined Source implementation.
2130
"""
2231

2332
def __init__(self):
24-
"""
25-
to_ack_set: Set to maintain a track of the offsets yet to be acknowledged
26-
read_idx : the offset idx till where the messages have been read
27-
"""
28-
self.to_ack_set = set()
29-
self.read_idx = 0
33+
# The offset idx till where the messages have been read
34+
self.read_idx: int = 0
35+
# Set to maintain a track of the offsets yet to be acknowledged
36+
self.to_ack_set: set[int] = set()
37+
# Set to maintain a track of the offsets that have been negatively acknowledged
38+
self.nacked: set[int] = set()
3039

3140
async def read_handler(self, datum: ReadRequest, output: NonBlockingIterator):
3241
"""
@@ -38,25 +47,42 @@ async def read_handler(self, datum: ReadRequest, output: NonBlockingIterator):
3847
return
3948

4049
for x in range(datum.num_records):
50+
# If there are any nacked offsets, re-deliver them
51+
if self.nacked:
52+
idx = self.nacked.pop()
53+
else:
54+
idx = self.read_idx
55+
self.read_idx += 1
4156
headers = {"x-txn-id": str(uuid.uuid4())}
4257
await output.put(
4358
Message(
4459
payload=str(self.read_idx).encode(),
45-
offset=Offset.offset_with_default_partition_id(str(self.read_idx).encode()),
60+
offset=Offset.offset_with_default_partition_id(str(idx).encode()),
4661
event_time=datetime.now(),
4762
headers=headers,
4863
)
4964
)
50-
self.to_ack_set.add(str(self.read_idx))
51-
self.read_idx += 1
65+
self.to_ack_set.add(idx)
5266

5367
async def ack_handler(self, ack_request: AckRequest):
5468
"""
5569
The ack handler is used acknowledge the offsets that have been read, and remove them
5670
from the to_ack_set
5771
"""
5872
for req in ack_request.offsets:
59-
self.to_ack_set.remove(str(req.offset, "utf-8"))
73+
offset = int(req.offset)
74+
self.to_ack_set.remove(offset)
75+
76+
async def nack_handler(self, ack_request: NackRequest):
77+
"""
78+
Add the offsets that have been negatively acknowledged to the nacked set
79+
"""
80+
81+
for req in ack_request.offsets:
82+
offset = int(req.offset)
83+
self.to_ack_set.remove(offset)
84+
self.nacked.add(offset)
85+
logger.info("Negatively acknowledged offsets: %s", self.nacked)
6086

6187
async def pending_handler(self) -> PendingResponse:
6288
"""
@@ -74,4 +100,5 @@ async def partitions_handler(self) -> PartitionsResponse:
74100
if __name__ == "__main__":
75101
ud_source = AsyncSource()
76102
grpc_server = SourceAsyncServer(ud_source)
103+
logger.info("Starting grpc server")
77104
grpc_server.start()

pynumaflow/proto/sourcer/source.proto

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ service Source {
2121
// Clients sends n requests and expects n responses.
2222
rpc AckFn(stream AckRequest) returns (stream AckResponse);
2323

24+
// NackFn negatively acknowledges a batch of offsets. Invoked during a critical error in the monovertex or pipeline.
25+
// Unlike AckFn its not a streaming rpc because this is only invoked when there is a critical error (error path).
26+
rpc NackFn(NackRequest) returns (NackResponse);
27+
2428
// PendingFn returns the number of pending records at the user defined source.
2529
rpc PendingFn(google.protobuf.Empty) returns (PendingResponse);
2630

@@ -139,6 +143,24 @@ message AckResponse {
139143
optional Handshake handshake = 2;
140144
}
141145

146+
message NackRequest {
147+
message Request {
148+
// Required field holding the offset to be nacked
149+
repeated Offset offsets = 1;
150+
}
151+
// Required field holding the request. The list will be ordered and will have the same order as the original Read response.
152+
Request request = 1;
153+
}
154+
155+
message NackResponse {
156+
message Result {
157+
// Required field indicating the nack request is successful.
158+
google.protobuf.Empty success = 1;
159+
}
160+
// Required field holding the result.
161+
Result result = 1;
162+
}
163+
142164
/*
143165
* ReadyResponse is the health check result for user defined source.
144166
*/

pynumaflow/proto/sourcer/source_pb2.py

Lines changed: 23 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pynumaflow/proto/sourcer/source_pb2.pyi

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,28 @@ class AckResponse(_message.Message):
111111
handshake: Handshake
112112
def __init__(self, result: _Optional[_Union[AckResponse.Result, _Mapping]] = ..., handshake: _Optional[_Union[Handshake, _Mapping]] = ...) -> None: ...
113113

114+
class NackRequest(_message.Message):
115+
__slots__ = ("request",)
116+
class Request(_message.Message):
117+
__slots__ = ("offsets",)
118+
OFFSETS_FIELD_NUMBER: _ClassVar[int]
119+
offsets: _containers.RepeatedCompositeFieldContainer[Offset]
120+
def __init__(self, offsets: _Optional[_Iterable[_Union[Offset, _Mapping]]] = ...) -> None: ...
121+
REQUEST_FIELD_NUMBER: _ClassVar[int]
122+
request: NackRequest.Request
123+
def __init__(self, request: _Optional[_Union[NackRequest.Request, _Mapping]] = ...) -> None: ...
124+
125+
class NackResponse(_message.Message):
126+
__slots__ = ("result",)
127+
class Result(_message.Message):
128+
__slots__ = ("success",)
129+
SUCCESS_FIELD_NUMBER: _ClassVar[int]
130+
success: _empty_pb2.Empty
131+
def __init__(self, success: _Optional[_Union[_empty_pb2.Empty, _Mapping]] = ...) -> None: ...
132+
RESULT_FIELD_NUMBER: _ClassVar[int]
133+
result: NackResponse.Result
134+
def __init__(self, result: _Optional[_Union[NackResponse.Result, _Mapping]] = ...) -> None: ...
135+
114136
class ReadyResponse(_message.Message):
115137
__slots__ = ("ready",)
116138
READY_FIELD_NUMBER: _ClassVar[int]

pynumaflow/proto/sourcer/source_pb2_grpc.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ def __init__(self, channel):
4545
request_serializer=source__pb2.AckRequest.SerializeToString,
4646
response_deserializer=source__pb2.AckResponse.FromString,
4747
_registered_method=True)
48+
self.NackFn = channel.unary_unary(
49+
'/source.v1.Source/NackFn',
50+
request_serializer=source__pb2.NackRequest.SerializeToString,
51+
response_deserializer=source__pb2.NackResponse.FromString,
52+
_registered_method=True)
4853
self.PendingFn = channel.unary_unary(
4954
'/source.v1.Source/PendingFn',
5055
request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
@@ -88,6 +93,14 @@ def AckFn(self, request_iterator, context):
8893
context.set_details('Method not implemented!')
8994
raise NotImplementedError('Method not implemented!')
9095

96+
def NackFn(self, request, context):
97+
"""NackFn negatively acknowledges a batch of offsets. Invoked during a critical error in the monovertex or pipeline.
98+
Unlike AckFn its not a streaming rpc because this is only invoked when there is a critical error (error path).
99+
"""
100+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
101+
context.set_details('Method not implemented!')
102+
raise NotImplementedError('Method not implemented!')
103+
91104
def PendingFn(self, request, context):
92105
"""PendingFn returns the number of pending records at the user defined source.
93106
"""
@@ -122,6 +135,11 @@ def add_SourceServicer_to_server(servicer, server):
122135
request_deserializer=source__pb2.AckRequest.FromString,
123136
response_serializer=source__pb2.AckResponse.SerializeToString,
124137
),
138+
'NackFn': grpc.unary_unary_rpc_method_handler(
139+
servicer.NackFn,
140+
request_deserializer=source__pb2.NackRequest.FromString,
141+
response_serializer=source__pb2.NackResponse.SerializeToString,
142+
),
125143
'PendingFn': grpc.unary_unary_rpc_method_handler(
126144
servicer.PendingFn,
127145
request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
@@ -202,6 +220,33 @@ def AckFn(request_iterator,
202220
metadata,
203221
_registered_method=True)
204222

223+
@staticmethod
224+
def NackFn(request,
225+
target,
226+
options=(),
227+
channel_credentials=None,
228+
call_credentials=None,
229+
insecure=False,
230+
compression=None,
231+
wait_for_ready=None,
232+
timeout=None,
233+
metadata=None):
234+
return grpc.experimental.unary_unary(
235+
request,
236+
target,
237+
'/source.v1.Source/NackFn',
238+
source__pb2.NackRequest.SerializeToString,
239+
source__pb2.NackResponse.FromString,
240+
options,
241+
channel_credentials,
242+
insecure,
243+
call_credentials,
244+
compression,
245+
wait_for_ready,
246+
timeout,
247+
metadata,
248+
_registered_method=True)
249+
205250
@staticmethod
206251
def PendingFn(request,
207252
target,

pynumaflow/sourcer/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
ReadRequest,
44
PendingResponse,
55
AckRequest,
6+
NackRequest,
67
Offset,
78
PartitionsResponse,
89
get_default_partitions,
910
Sourcer,
11+
SourceCallable,
1012
)
1113
from pynumaflow.sourcer.async_server import SourceAsyncServer
1214

@@ -15,9 +17,11 @@
1517
"ReadRequest",
1618
"PendingResponse",
1719
"AckRequest",
20+
"NackRequest",
1821
"Offset",
1922
"PartitionsResponse",
2023
"get_default_partitions",
2124
"Sourcer",
2225
"SourceAsyncServer",
26+
"SourceCallable",
2327
]

0 commit comments

Comments
 (0)