Skip to content

Commit a35190b

Browse files
committed
Keep direct activity calls outside payload transport
1 parent bad91c1 commit a35190b

6 files changed

Lines changed: 42 additions & 19 deletions

File tree

‎azure-functions-durable/CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ client bindings, and invocation logging context is preserved.
2323
Worker payload-size and storage errors retain their original error details.
2424
Activity wrappers preserve the application's source directory for
2525
`context.function_directory`, including when payload storage is not configured.
26+
Direct activity calls retain their Python return values and do not access payload
27+
storage; externalization applies only to host binding invocations.
2628

2729
## v2.0.0rc1
2830

‎azure-functions-durable/azure/durable_functions/internal/compat/activity.py‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
deexternalize_payload_async,
4141
externalize_activity_output,
4242
externalize_activity_output_async,
43-
get_payload_store,
4443
)
4544
from .orchestration_context import accepts_two_positional_args
4645

@@ -188,17 +187,19 @@ def decode(value: str) -> Any:
188187
return df_loads(value)
189188
except json.JSONDecodeError:
190189
return value
190+
except Exception as error:
191+
raise ValueError('activity trigger input must be a string or a '
192+
f'valid json serializable ({value})') from error
191193

192194
wrapper: Callable[..., Any]
193195
if inspect.iscoroutinefunction(fn):
194196
@wraps(fn)
195197
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
196-
if get_payload_store() is None:
197-
return await fn(*args, **kwargs)
198198
bound = signature.bind(*args, **kwargs)
199199
value = bound.arguments.get(input_name)
200-
if isinstance(value, ActivityPayload):
201-
bound.arguments[input_name] = decode(await deexternalize_payload_async(value.value))
200+
if not isinstance(value, ActivityPayload):
201+
return await fn(*args, **kwargs)
202+
bound.arguments[input_name] = decode(await deexternalize_payload_async(value.value))
202203
result = await fn(*bound.args, **bound.kwargs)
203204
if result is None:
204205
return None
@@ -209,12 +210,11 @@ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
209210
else:
210211
@wraps(fn)
211212
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
212-
if get_payload_store() is None:
213-
return fn(*args, **kwargs)
214213
bound = signature.bind(*args, **kwargs)
215214
value = bound.arguments.get(input_name)
216-
if isinstance(value, ActivityPayload):
217-
bound.arguments[input_name] = decode(deexternalize_payload(value.value))
215+
if not isinstance(value, ActivityPayload):
216+
return fn(*args, **kwargs)
217+
bound.arguments[input_name] = decode(deexternalize_payload(value.value))
218218
result = fn(*bound.args, **bound.kwargs)
219219
if result is None:
220220
return None

‎azure-functions-durable/azure/durable_functions/internal/converters.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def decode(cls, data: meta.Datum, *,
139139
if data_type in ['string', 'json']:
140140
value = data.value
141141
store = get_transport_payload_store()
142-
if store is not None and store.is_known_token(value):
142+
if store is not None:
143143
return ActivityPayload(value)
144144
try:
145145
result = df_loads(value)

‎tests/azure-functions-durable/test_activity_adapter_compat.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,29 @@ async def native_activity(context, payload):
205205
store.upload_async.assert_awaited_once()
206206

207207

208+
@pytest.mark.parametrize("user_async", [False, True])
209+
@pytest.mark.parametrize("value", [None, "small", {"small": True}])
210+
async def test_host_activity_externalizes_large_output_from_inline_input(
211+
monkeypatch, payload_store_factory, user_async, value):
212+
store = payload_store_factory()
213+
monkeypatch.setattr(payloads, "_payload_store", store)
214+
output = {"large": "x" * 200}
215+
216+
def activity(payload):
217+
assert payload == value
218+
return output
219+
220+
async def async_activity(payload):
221+
return activity(payload)
222+
223+
wrapper = wrap_activity_payloads(async_activity if user_async else activity, "payload")
224+
decoded = ActivityTriggerConverter.decode(
225+
meta.Datum(type="json", value=json.dumps(value)), trigger_metadata=None)
226+
result = await wrapper(decoded) if user_async else wrapper(decoded)
227+
encoded = ActivityTriggerConverter.encode(result, expected_type=None)
228+
assert json.loads(store.download(json.loads(encoded.value))) == output
229+
230+
208231
def test_sync_activity_uses_sync_storage_on_the_calling_thread(monkeypatch, payload_store_factory):
209232
store = payload_store_factory()
210233
monkeypatch.setattr(payloads, "_payload_store", store)

‎tests/azure-functions-durable/test_converters.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636

3737

3838
def _encode_activity(value):
39-
result = wrap_activity_payloads(lambda payload: payload, "payload")(value)
39+
host_input = ActivityTriggerConverter.decode(
40+
meta.Datum(type="json", value="null"), trigger_metadata=None)
41+
result = wrap_activity_payloads(lambda payload: value, "payload")(host_input)
4042
return ActivityTriggerConverter.encode(result, expected_type=None)
4143

4244

‎tests/azure-functions-durable/test_decorator_compat.py‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import pytest
1010
from unittest.mock import AsyncMock, MagicMock, Mock, patch
1111
from azure.durable_functions.internal import payloads
12-
from azure.durable_functions.internal.converters import ActivityTriggerConverter
1312
from azure.durable_functions.constants import (
1413
ACTIVITY_TRIGGER,
1514
DURABLE_CLIENT,
@@ -95,8 +94,8 @@ def my_activity(ctx, payload):
9594

9695
@pytest.mark.parametrize("configured", [False, True])
9796
@pytest.mark.parametrize("user_async", [False, True])
98-
async def test_activity_trigger_preserves_direct_calls_and_skips_inline_storage(
99-
monkeypatch, payload_store_factory, configured, user_async):
97+
@pytest.mark.parametrize("value", [None, "hello", {"large": "x" * 200}])
98+
async def test_direct_activity_calls_skip_storage(monkeypatch, payload_store_factory, configured, user_async, value):
10099
monkeypatch.setattr(payloads, "_payload_store", None)
101100
app = df.DFApp()
102101

@@ -117,12 +116,9 @@ async def async_activity(payload):
117116
function = registered.build().get_user_function()
118117
assert inspect.iscoroutinefunction(function) == user_async
119118
assert list(inspect.signature(function).parameters) == ["payload"]
120-
result = await function("hello") if user_async else function("hello")
119+
result = await registered(value) if user_async else registered(value)
121120
assert not inspect.isawaitable(result)
122-
if configured:
123-
assert ActivityTriggerConverter.encode(result, expected_type=None).value == '"hello"'
124-
else:
125-
assert result == "hello"
121+
assert result is value
126122
store.download.assert_not_called()
127123
store.upload.assert_not_called()
128124
store.download_async.assert_not_called()

0 commit comments

Comments
 (0)