Skip to content

Commit be014e3

Browse files
committed
feat: end to end wiring for per message nack
1 parent 030ded1 commit be014e3

38 files changed

Lines changed: 407 additions & 129 deletions

packages/pynumaflow/pynumaflow/_constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
STREAM_EOF = "EOF"
5555
DELIMITER = ":"
5656
DROP = "U+005C__DROP__"
57+
NACK = "U+005C__NACK__"
5758

5859
_PROCESS_COUNT = os.cpu_count()
5960
# Cap max value to 16
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
5+
from pynumaflow.proto.common import nack_options_pb2
6+
7+
8+
@dataclass
9+
class NackOptions:
10+
"""Per-message redelivery options for a nack."""
11+
12+
delay: int | None = None
13+
max_deliveries: int | None = None
14+
reason: str | None = None
15+
16+
def _to_proto(self) -> nack_options_pb2.NackOptions:
17+
return nack_options_pb2.NackOptions(
18+
reason=self.reason,
19+
max_deliveries=self.max_deliveries,
20+
delay=self.delay,
21+
)
22+
23+
24+
def _nack_options_to_proto(
25+
opts: NackOptions | None,
26+
) -> nack_options_pb2.NackOptions | None:
27+
if opts is None:
28+
return None
29+
return opts._to_proto()
30+
31+
32+
def _nack_options_from_proto(
33+
proto: nack_options_pb2.NackOptions,
34+
) -> NackOptions:
35+
return NackOptions(
36+
delay=proto.delay if proto.HasField("delay") else None,
37+
max_deliveries=proto.max_deliveries if proto.HasField("max_deliveries") else None,
38+
reason=proto.reason if proto.HasField("reason") else None,
39+
)

packages/pynumaflow/pynumaflow/batchmapper/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
BatchResponse,
99
)
1010
from pynumaflow.batchmapper.async_server import BatchMapAsyncServer
11+
from pynumaflow._nack import NackOptions
1112

1213
__all__ = [
1314
"Message",
@@ -17,4 +18,5 @@
1718
"BatchMapper",
1819
"BatchResponses",
1920
"BatchResponse",
21+
"NackOptions",
2022
]

packages/pynumaflow/pynumaflow/batchmapper/_dtypes.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from typing import TypeAlias, TypeVar
66
from collections.abc import AsyncIterable, Callable
77

8-
from pynumaflow._constants import DROP
8+
from pynumaflow._constants import DROP, NACK
9+
from pynumaflow._nack import NackOptions
910
from pynumaflow._validate import _validate_message_fields
1011

1112
M = TypeVar("M", bound="Message")
@@ -27,6 +28,7 @@ class Message:
2728
_value: bytes
2829
_keys: list[str]
2930
_tags: list[str]
31+
_nack_options: NackOptions | None
3032

3133
def __init__(self, value: bytes, keys: list[str] | None = None, tags: list[str] | None = None):
3234
"""
@@ -36,12 +38,23 @@ def __init__(self, value: bytes, keys: list[str] | None = None, tags: list[str]
3638
self._keys = keys or []
3739
self._tags = tags or []
3840
self._value = value or b""
41+
self._nack_options = None
3942

4043
# returns the Message Object which will be dropped
4144
@classmethod
4245
def to_drop(cls: type[M]) -> M:
4346
return cls(b"", None, [DROP])
4447

48+
@classmethod
49+
def to_nack(cls: type[M], opts: NackOptions | None = None) -> M:
50+
m = cls(b"", None, [NACK])
51+
m._nack_options = opts
52+
return m
53+
54+
@property
55+
def nack_options(self) -> NackOptions | None:
56+
return self._nack_options
57+
4558
@property
4659
def value(self) -> bytes:
4760
return self._value

packages/pynumaflow/pynumaflow/batchmapper/servicer/async_servicer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from pynumaflow.shared.server import update_context_err
1111
from pynumaflow.types import NumaflowServicerContext
1212
from pynumaflow._constants import _LOGGER, STREAM_EOF, ERR_UDF_EXCEPTION_STRING
13+
from pynumaflow._nack import _nack_options_to_proto
1314

1415

1516
class AsyncBatchMapServicer(map_pb2_grpc.MapServicer):
@@ -80,7 +81,7 @@ async def MapFn(
8081
for msg in batch_response.messages:
8182
single_req_resp.append(
8283
map_pb2.MapResponse.Result(
83-
keys=msg.keys, value=msg.value, tags=msg.tags
84+
keys=msg.keys, value=msg.value, tags=msg.tags, nack_options=_nack_options_to_proto(msg.nack_options)
8485
)
8586
)
8687
# send the response for a given ID back to the stream

packages/pynumaflow/pynumaflow/mapper/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from pynumaflow.mapper._dtypes import Message, Messages, Datum, DROP, Mapper
66
from pynumaflow._metadata import UserMetadata, SystemMetadata
7+
from pynumaflow._nack import NackOptions
78

89
__all__ = [
910
"Message",
@@ -16,4 +17,5 @@
1617
"MapMultiprocServer",
1718
"UserMetadata",
1819
"SystemMetadata",
20+
"NackOptions",
1921
]

packages/pynumaflow/pynumaflow/mapper/_dtypes.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from collections.abc import Callable
77
from warnings import warn
88

9-
from pynumaflow._constants import DROP
9+
from pynumaflow._constants import DROP, NACK
10+
from pynumaflow._nack import NackOptions
1011
from pynumaflow._metadata import UserMetadata, SystemMetadata
1112
from pynumaflow._validate import _validate_message_fields
1213

@@ -30,6 +31,7 @@ class Message:
3031
_keys: list[str]
3132
_tags: list[str]
3233
_user_metadata: UserMetadata
34+
_nack_options: NackOptions | None
3335

3436
def __init__(
3537
self,
@@ -46,12 +48,23 @@ def __init__(
4648
self._tags = tags or []
4749
self._value = value or b""
4850
self._user_metadata = user_metadata or UserMetadata()
51+
self._nack_options = None
4952

5053
# returns the Message Object which will be dropped
5154
@classmethod
5255
def to_drop(cls: type[M]) -> M:
5356
return cls(b"", None, [DROP])
5457

58+
@classmethod
59+
def to_nack(cls: type[M], opts: NackOptions | None = None) -> M:
60+
m = cls(b"", None, [NACK])
61+
m._nack_options = opts
62+
return m
63+
64+
@property
65+
def nack_options(self) -> NackOptions | None:
66+
return self._nack_options
67+
5568
@property
5669
def value(self) -> bytes:
5770
return self._value

packages/pynumaflow/pynumaflow/mapper/_servicer/_async_servicer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pynumaflow._constants import _LOGGER, STREAM_EOF, ERR_UDF_EXCEPTION_STRING
88
from pynumaflow.mapper._dtypes import MapAsyncCallable, Datum, MapError, Message, Messages
99
from pynumaflow._metadata import _user_and_system_metadata_from_proto
10+
from pynumaflow._nack import _nack_options_to_proto
1011
from pynumaflow.proto.mapper import map_pb2, map_pb2_grpc
1112
from pynumaflow.shared.server import update_context_err
1213
from pynumaflow.types import NumaflowServicerContext
@@ -143,6 +144,7 @@ async def _invoke_map(self, req: map_pb2.MapRequest, result_queue: NonBlockingIt
143144
value=msg.value,
144145
tags=msg.tags,
145146
metadata=msg.user_metadata._to_proto(),
147+
nack_options=_nack_options_to_proto(msg.nack_options),
146148
)
147149
)
148150
await result_queue.put(map_pb2.MapResponse(results=datums, id=req.id))

packages/pynumaflow/pynumaflow/mapper/_servicer/_sync_servicer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from pynumaflow._constants import NUM_THREADS_DEFAULT, STREAM_EOF, _LOGGER, ERR_UDF_EXCEPTION_STRING
1111
from pynumaflow.mapper._dtypes import MapSyncCallable, Datum, MapError
12+
from pynumaflow._nack import _nack_options_to_proto
1213
from pynumaflow.proto.mapper import map_pb2, map_pb2_grpc
1314
from pynumaflow.shared.synciter import SyncIterator
1415
from pynumaflow.types import NumaflowServicerContext
@@ -155,6 +156,7 @@ def _invoke_map(
155156
value=resp.value,
156157
tags=resp.tags,
157158
metadata=resp.user_metadata._to_proto(),
159+
nack_options=_nack_options_to_proto(resp.nack_options),
158160
)
159161
)
160162
result_queue.put(map_pb2.MapResponse(results=results, id=request.id))

packages/pynumaflow/pynumaflow/mapstreamer/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from pynumaflow.mapstreamer._dtypes import Message, Messages, Datum, MapStreamer
44
from pynumaflow.mapstreamer.async_server import MapStreamAsyncServer
5+
from pynumaflow._nack import NackOptions
56

67
__all__ = [
78
"Message",
@@ -10,4 +11,5 @@
1011
"DROP",
1112
"MapStreamAsyncServer",
1213
"MapStreamer",
14+
"NackOptions",
1315
]

0 commit comments

Comments
 (0)