Skip to content

Commit b9168c1

Browse files
authored
💥 refactor: payloads/memo size validation (#1640)
1 parent 41f67aa commit b9168c1

20 files changed

Lines changed: 287 additions & 798 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ to include examples, links to docs, or any other relevant information.
3030

3131
### Breaking Changes
3232

33+
- Payload size limits have moved from `DataConverter` to `Client.connect`. Pass
34+
`payload_limits=PayloadLimitsConfig(...)` (now exported from
35+
`temporalio.client`) instead of setting `payload_limits` on `DataConverter`.
36+
Config fields were renamed to `payloads_warn_size` and `memo_warn_size`, and
37+
the deprecated `PayloadSizeWarning` was removed.
38+
3339
### Fixed
3440

3541
### Security

temporalio/bridge/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ class ClientConfig:
8282
http_connect_proxy_config: ClientHttpConnectProxyConfig | None
8383
dns_load_balancing_config: ClientDnsLoadBalancingConfig | None
8484
grpc_compression: str
85+
payloads_warn_size: int
86+
memo_warn_size: int
8587

8688

8789
@dataclass

temporalio/bridge/sdk-core

Submodule sdk-core updated 75 files

temporalio/bridge/src/client.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ pub struct ClientConfig {
3838
http_connect_proxy_config: Option<ClientHttpConnectProxyConfig>,
3939
dns_load_balancing_config: Option<ClientDnsLoadBalancingConfig>,
4040
grpc_compression: String,
41+
payloads_warn_size: u64,
42+
memo_warn_size: u64,
4143
}
4244

4345
#[derive(FromPyObject)]
@@ -268,6 +270,10 @@ impl ClientConfig {
268270
.maybe_http_connect_proxy(self.http_connect_proxy_config.map(Into::into))
269271
.dns_load_balancing(dns_load_balancing)
270272
.grpc_compression(grpc_compression_from_str(&self.grpc_compression)?)
273+
.payload_limits(temporalio_client::PayloadLimitsOptions {
274+
payloads_warn_size: self.payloads_warn_size,
275+
memo_warn_size: self.memo_warn_size,
276+
})
271277
.headers(ascii_headers)
272278
.binary_headers(binary_headers)
273279
.maybe_api_key(self.api_key)

temporalio/bridge/src/worker.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ pub struct WorkerConfig {
6363
nexus_task_poller_behavior: PollerBehavior,
6464
plugins: Vec<String>,
6565
storage_drivers: HashSet<String>,
66+
disable_payload_error_limit: bool,
6667
}
6768

6869
#[derive(FromPyObject)]
@@ -774,6 +775,7 @@ fn convert_worker_config(
774775
.map(|r#type| StorageDriverInfo { r#type })
775776
.collect::<HashSet<_>>(),
776777
)
778+
.disable_payload_error_limit(conf.disable_payload_error_limit)
777779
.build()
778780
.map_err(|err| PyValueError::new_err(format!("Invalid worker config: {err}")))
779781
}
@@ -827,11 +829,13 @@ fn convert_tuner_holder(
827829
}
828830

829831
Ok(temporalio_sdk_core::TunerHolderOptions::builder()
830-
.maybe_resource_based_options(first.map(|first| {
831-
temporalio_sdk_core::ResourceBasedSlotsOptions::builder()
832-
.target_mem_usage(first.target_memory_usage)
833-
.target_cpu_usage(first.target_cpu_usage)
834-
.build()
832+
.maybe_resource_based_config(first.map(|first| {
833+
temporalio_sdk_core::ResourceBasedTunerConfig::Options(
834+
temporalio_sdk_core::ResourceBasedSlotsOptions::builder()
835+
.target_mem_usage(first.target_memory_usage)
836+
.target_cpu_usage(first.target_cpu_usage)
837+
.build(),
838+
)
835839
}))
836840
.workflow_slot_options(convert_slot_supplier(
837841
holder.workflow_slot_supplier,
@@ -899,10 +903,11 @@ fn convert_versioning_strategy(
899903
use_worker_versioning: options.use_worker_versioning,
900904
default_versioning_behavior: if options.use_worker_versioning {
901905
Some(
902-
options
903-
.default_versioning_behavior
904-
.try_into()
905-
.unwrap_or_default(),
906+
temporalio_common::protos::temporal::api::enums::v1::VersioningBehavior::try_from(
907+
options.default_versioning_behavior,
908+
)
909+
.unwrap_or_default()
910+
.into(),
906911
)
907912
} else {
908913
None

temporalio/bridge/worker.py

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class WorkerConfig:
5858
nexus_task_poller_behavior: PollerBehavior
5959
plugins: Sequence[str]
6060
storage_drivers: set[str]
61+
disable_payload_error_limit: bool
6162

6263

6364
@dataclass
@@ -284,10 +285,8 @@ class _Visitor(VisitorFunctions):
284285
def __init__(
285286
self,
286287
f: Callable[[Sequence[Payload]], Awaitable[list[Payload]]],
287-
visit_system_nexus_envelope: Callable[[Payload], Awaitable[None]] | None = None,
288288
):
289289
self._f = f
290-
self._visit_system_nexus_envelope = visit_system_nexus_envelope
291290

292291
async def visit_payload(self, payload: Payload) -> None:
293292
new_payload = (await self._f([payload]))[0]
@@ -303,10 +302,6 @@ async def visit_payloads(self, payloads: PayloadSequence) -> None:
303302
del payloads[:]
304303
payloads.extend(new_payloads)
305304

306-
async def visit_system_nexus_envelope(self, payload: Payload) -> None:
307-
if self._visit_system_nexus_envelope is not None:
308-
await self._visit_system_nexus_envelope(payload)
309-
310305

311306
async def decode_activation(
312307
activation: temporalio.bridge.proto.workflow_activation.WorkflowActivation,
@@ -348,39 +343,22 @@ async def encode_completion(
348343
Returns:
349344
Metrics from any external storage store operations that occurred.
350345
"""
351-
352-
async def _validate_system_nexus_envelope(payload: Payload) -> None:
353-
data_converter._validate_payload_limits([payload])
354-
355346
await CommandAwarePayloadVisitor(
356347
skip_search_attributes=True,
357348
skip_headers=not encode_headers,
358349
).visit(
359-
_Visitor(
360-
data_converter._encode_payload_sequence,
361-
visit_system_nexus_envelope=_validate_system_nexus_envelope,
362-
),
350+
_Visitor(data_converter._encode_payload_sequence),
363351
completion,
364352
)
365353

366-
async def _store_and_validate(
367-
payloads: Sequence[Payload],
368-
) -> list[Payload]:
369-
stored = await data_converter._external_store_payload_sequence(payloads)
370-
data_converter._validate_payload_limits(stored)
371-
return stored
372-
373354
metrics = temporalio.converter._extstore.StorageOperationMetrics()
374355
with metrics.track():
375356
await CommandAwarePayloadVisitor(
376357
skip_search_attributes=True,
377358
skip_headers=not encode_headers,
378359
concurrency_limit=storage_concurrency_limit,
379360
).visit(
380-
_Visitor(
381-
_store_and_validate,
382-
visit_system_nexus_envelope=_validate_system_nexus_envelope,
383-
),
361+
_Visitor(data_converter._external_store_payload_sequence),
384362
completion,
385363
)
386364

temporalio/client/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
GrpcCompression,
2020
HttpConnectProxyConfig,
2121
KeepAliveConfig,
22+
PayloadLimitsConfig,
2223
RetryConfig,
2324
RPCError,
2425
RPCStatusCode,
@@ -359,6 +360,7 @@
359360
"GrpcCompression",
360361
"HttpConnectProxyConfig",
361362
"KeepAliveConfig",
363+
"PayloadLimitsConfig",
362364
"RetryConfig",
363365
"RPCError",
364366
"RPCStatusCode",

temporalio/client/_client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
GrpcCompression,
3636
HttpConnectProxyConfig,
3737
KeepAliveConfig,
38+
PayloadLimitsConfig,
3839
RetryConfig,
3940
ServiceClient,
4041
TLSConfig,
@@ -154,6 +155,7 @@ async def connect(
154155
http_connect_proxy_config: HttpConnectProxyConfig | None = None,
155156
dns_load_balancing_config: DnsLoadBalancingConfig | None = None,
156157
grpc_compression: GrpcCompression = GrpcCompression.GZIP,
158+
payload_limits: PayloadLimitsConfig = PayloadLimitsConfig(),
157159
header_codec_behavior: HeaderCodecBehavior = HeaderCodecBehavior.NO_CODEC,
158160
) -> Self:
159161
"""Connect to a Temporal server.
@@ -217,6 +219,8 @@ async def connect(
217219
grpc_compression: Transport-level gRPC compression for the client
218220
connection. Default is gzip. Set to
219221
:py:attr:`GrpcCompression.NONE` to disable compression.
222+
payload_limits: Warning thresholds for outbound payload/memo sizes. Over-threshold
223+
fields are logged but still sent. Set a threshold to 0 to disable it.
220224
header_codec_behavior: Encoding behavior for headers sent by the client.
221225
"""
222226
connect_config = temporalio.service.ConnectConfig(
@@ -232,6 +236,7 @@ async def connect(
232236
http_connect_proxy_config=http_connect_proxy_config,
233237
dns_load_balancing_config=dns_load_balancing_config,
234238
grpc_compression=grpc_compression,
239+
payload_limits=payload_limits,
235240
)
236241

237242
def make_lambda(
@@ -3049,6 +3054,7 @@ class ClientConnectConfig(TypedDict, total=False):
30493054
http_connect_proxy_config: HttpConnectProxyConfig | None
30503055
dns_load_balancing_config: DnsLoadBalancingConfig | None
30513056
grpc_compression: GrpcCompression
3057+
payload_limits: PayloadLimitsConfig
30523058
header_codec_behavior: HeaderCodecBehavior
30533059

30543060

temporalio/converter/__init__.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,6 @@
3535
PayloadConverter,
3636
value_to_type,
3737
)
38-
from temporalio.converter._payload_limits import (
39-
PayloadLimitsConfig,
40-
PayloadSizeWarning,
41-
)
4238
from temporalio.converter._search_attributes import (
4339
decode_search_attributes,
4440
decode_typed_search_attributes,
@@ -80,8 +76,6 @@
8076
"JSONTypeConverterUnhandled",
8177
"PayloadCodec",
8278
"PayloadConverter",
83-
"PayloadLimitsConfig",
84-
"PayloadSizeWarning",
8579
"SerializationContext",
8680
"WithSerializationContext",
8781
"WorkflowSerializationContext",

temporalio/converter/_data_converter.py

Lines changed: 0 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
import dataclasses
6-
import warnings
76
from collections.abc import Mapping, Sequence
87
from dataclasses import dataclass
98
from logging import getLogger
@@ -30,12 +29,6 @@
3029
from temporalio.converter._payload_converter import (
3130
PayloadConverter,
3231
)
33-
from temporalio.converter._payload_limits import (
34-
PayloadLimitsConfig,
35-
PayloadSizeWarning,
36-
_PayloadSizeError,
37-
_ServerPayloadErrorLimits,
38-
)
3932
from temporalio.converter._serialization_context import (
4033
SerializationContext,
4134
WithSerializationContext,
@@ -86,9 +79,6 @@ class DataConverter(WithSerializationContext):
8679
failure_converter: FailureConverter = dataclasses.field(init=False)
8780
"""Failure converter created from the :py:attr:`failure_converter_class`."""
8881

89-
payload_limits: PayloadLimitsConfig = PayloadLimitsConfig()
90-
"""Settings for payload size limits."""
91-
9282
external_storage: ExternalStorage | None = None
9383
"""Options for external storage. If None, external storage is disabled.
9484
@@ -99,9 +89,6 @@ class DataConverter(WithSerializationContext):
9989
default: ClassVar[DataConverter]
10090
"""Singleton default data converter."""
10191

102-
_payload_error_limits: _ServerPayloadErrorLimits | None = None
103-
"""Server-reported limits for payloads."""
104-
10592
def __post_init__(self) -> None: # noqa: D105
10693
object.__setattr__(self, "payload_converter", self.payload_converter_class())
10794
object.__setattr__(self, "failure_converter", self.failure_converter_class())
@@ -124,7 +111,6 @@ async def encode(
124111
payloads = self.payload_converter.to_payloads(values)
125112
payloads = await self._encode_payload_sequence(payloads)
126113
payloads = await self._external_store_payload_sequence(payloads)
127-
self._validate_payload_limits(payloads)
128114
return payloads
129115

130116
async def decode(
@@ -230,11 +216,6 @@ def _with_contexts(
230216
"""Return an instance with both serialization and store contexts applied."""
231217
return self.with_context(serialization_ctx)._with_store_context(store_ctx)
232218

233-
def _with_payload_error_limits(
234-
self, limits: _ServerPayloadErrorLimits | None
235-
) -> DataConverter:
236-
return dataclasses.replace(self, _payload_error_limits=limits)
237-
238219
async def _decode_memo(
239220
self,
240221
source: temporalio.api.common.v1.Memo,
@@ -273,16 +254,6 @@ async def _encode_memo_existing(
273254
if not isinstance(v, temporalio.api.common.v1.Payload):
274255
payload = (await self.encode([v]))[0]
275256
memo.fields[k].CopyFrom(payload)
276-
# Memos have their field payloads validated all together in one unit
277-
DataConverter._validate_limits(
278-
list(memo.fields.values()),
279-
self._payload_error_limits.memo_size_error
280-
if self._payload_error_limits
281-
else None,
282-
"[TMPRL1103] Attempted to upload memo with size that exceeded the error limit.",
283-
self.payload_limits.memo_size_warning,
284-
"[TMPRL1103] Attempted to upload memo with size that exceeded the warning limit.",
285-
)
286257

287258
async def _transform_outbound_payload(
288259
self, payload: temporalio.api.common.v1.Payload
@@ -291,7 +262,6 @@ async def _transform_outbound_payload(
291262
payload = (await self.payload_codec.encode([payload]))[0]
292263
if self.external_storage:
293264
payload = await self.external_storage._store_payload(payload)
294-
self._validate_payload_limits([payload])
295265
return payload
296266

297267
async def _transform_outbound_payloads(
@@ -301,7 +271,6 @@ async def _transform_outbound_payloads(
301271
await self.payload_codec.encode_wrapper(payloads)
302272
if self.external_storage:
303273
await self.external_storage._store_payloads(payloads)
304-
self._validate_payload_limits(payloads.payloads)
305274

306275
async def _transform_inbound_payload(
307276
self, payload: temporalio.api.common.v1.Payload
@@ -376,42 +345,6 @@ async def _decode_payload_sequence(
376345
def _decode_payload_has_effect(self) -> bool:
377346
return self.payload_codec is not None or self.external_storage is not None
378347

379-
def _validate_payload_limits(
380-
self,
381-
payloads: Sequence[temporalio.api.common.v1.Payload],
382-
):
383-
DataConverter._validate_limits(
384-
payloads,
385-
self._payload_error_limits.payload_size_error
386-
if self._payload_error_limits
387-
else None,
388-
"[TMPRL1103] Attempted to upload payloads with size that exceeded the error limit.",
389-
self.payload_limits.payload_size_warning,
390-
"[TMPRL1103] Attempted to upload payloads with size that exceeded the warning limit.",
391-
)
392-
393-
@staticmethod
394-
def _validate_limits(
395-
payloads: Sequence[temporalio.api.common.v1.Payload],
396-
error_limit: int | None,
397-
error_message: str,
398-
warning_limit: int,
399-
warning_message: str,
400-
):
401-
total_size = sum(payload.ByteSize() for payload in payloads)
402-
403-
if error_limit and error_limit > 0 and total_size > error_limit:
404-
raise _PayloadSizeError(
405-
f"{error_message} Size: {total_size} bytes, Limit: {error_limit} bytes"
406-
)
407-
408-
if warning_limit > 0 and total_size > warning_limit:
409-
# TODO: Use a context aware logger to log extra information about workflow/activity/etc
410-
warnings.warn(
411-
f"{warning_message} Size: {total_size} bytes, Limit: {warning_limit} bytes",
412-
PayloadSizeWarning,
413-
)
414-
415348

416349
def default() -> DataConverter:
417350
"""Default data converter.

0 commit comments

Comments
 (0)