|
6 | 6 | import pytest |
7 | 7 |
|
8 | 8 | from temporalio.api.common.v1 import Payload |
| 9 | +from temporalio.api.sdk.v1.external_storage_pb2 import ExternalStorageReference |
9 | 10 | from temporalio.converter import ( |
10 | 11 | DataConverter, |
11 | 12 | ExternalStorage, |
|
16 | 17 | StorageDriverRetrieveContext, |
17 | 18 | StorageDriverStoreContext, |
18 | 19 | ) |
19 | | -from temporalio.converter._extstore import _StorageReference |
| 20 | +from temporalio.converter._extstore import _REFERENCE_ENCODING, _StorageReference |
| 21 | +from temporalio.converter._payload_converter import JSONProtoPayloadConverter |
20 | 22 | from temporalio.exceptions import ApplicationError |
21 | 23 |
|
| 24 | +_legacy_ref_converter = JSONPlainPayloadConverter(encoding=_REFERENCE_ENCODING.decode()) |
| 25 | + |
| 26 | + |
| 27 | +def _make_legacy_payload( |
| 28 | + driver_name: str, claim_data: dict[str, str], size_bytes: int |
| 29 | +) -> Payload: |
| 30 | + """Build a reference payload in the legacy ``json/external-storage-reference`` format.""" |
| 31 | + ref = _StorageReference( |
| 32 | + driver_name=driver_name, |
| 33 | + driver_claim=StorageDriverClaim(claim_data=claim_data), |
| 34 | + ) |
| 35 | + payload = _legacy_ref_converter.to_payload(ref) |
| 36 | + assert payload is not None |
| 37 | + payload.external_payloads.add().size_bytes = size_bytes |
| 38 | + return payload |
| 39 | + |
22 | 40 |
|
23 | 41 | class InMemoryTestDriver(StorageDriver): |
24 | 42 | """In-memory storage driver for testing.""" |
@@ -115,33 +133,27 @@ async def test_extstore_encode_decode(self): |
115 | 133 | assert driver._retrieve_calls == 1 |
116 | 134 |
|
117 | 135 | async def test_extstore_reference_structure(self): |
118 | | - """Test that external storage creates proper reference structure.""" |
| 136 | + """Externalized payloads are written as ExternalStorageReference proto (json/protobuf encoding).""" |
119 | 137 | converter = DataConverter( |
120 | 138 | external_storage=ExternalStorage( |
121 | 139 | drivers=[InMemoryTestDriver("test-driver")], |
122 | 140 | payload_size_threshold=50, |
123 | 141 | ) |
124 | 142 | ) |
125 | 143 |
|
126 | | - # Create large payload |
127 | 144 | large_value = "x" * 100 |
128 | 145 | encoded = await converter.encode([large_value]) |
129 | 146 |
|
130 | | - # Verify reference structure |
131 | 147 | reference_payload = encoded[0] |
132 | 148 | assert len(reference_payload.external_payloads) > 0 |
| 149 | + assert reference_payload.metadata.get("encoding") == b"json/protobuf" |
133 | 150 |
|
134 | | - # The payload should contain a serialized _ExternalStorageReference |
135 | | - # Deserialize it to verify structure using the same encoding |
136 | | - claim_converter = JSONPlainPayloadConverter( |
137 | | - encoding="json/external-storage-reference" |
| 151 | + reference = JSONProtoPayloadConverter().from_payload( |
| 152 | + reference_payload, ExternalStorageReference |
138 | 153 | ) |
139 | | - reference = claim_converter.from_payload(reference_payload, _StorageReference) |
140 | | - |
141 | | - assert isinstance(reference, _StorageReference) |
142 | | - assert "test-driver" == reference.driver_name |
143 | | - assert isinstance(reference.driver_claim, StorageDriverClaim) |
144 | | - assert "key" in reference.driver_claim.claim_data |
| 154 | + assert isinstance(reference, ExternalStorageReference) |
| 155 | + assert reference.driver_name == "test-driver" |
| 156 | + assert "key" in reference.claim_data |
145 | 157 |
|
146 | 158 | async def test_extstore_composite_conditional(self): |
147 | 159 | """Test using multiple drivers based on size.""" |
@@ -482,9 +494,10 @@ async def test_selector_always_first_driver_handles_all_stores(self): |
482 | 494 | assert second._store_calls == 0 |
483 | 495 |
|
484 | 496 | # The reference in history names the first driver. |
485 | | - ref = JSONPlainPayloadConverter( |
486 | | - encoding="json/external-storage-reference" |
487 | | - ).from_payload(encoded[0], _StorageReference) |
| 497 | + ref = JSONProtoPayloadConverter().from_payload( |
| 498 | + encoded[0], ExternalStorageReference |
| 499 | + ) |
| 500 | + assert isinstance(ref, ExternalStorageReference) |
488 | 501 | assert ref.driver_name == "driver-first" |
489 | 502 |
|
490 | 503 | # Retrieval also goes to the first driver. |
@@ -694,5 +707,99 @@ def test_negative_payload_size_threshold_raises(self, threshold: int): |
694 | 707 | ) |
695 | 708 |
|
696 | 709 |
|
| 710 | +class TestBackwardCompat: |
| 711 | + """Tests that the retrieval path handles the legacy ``json/external-storage-reference`` |
| 712 | + format for in-flight workflows written before the ExternalStorageReference proto.""" |
| 713 | + |
| 714 | + async def test_legacy_format_single_payload_decode(self): |
| 715 | + """A single payload in the legacy reference format is retrieved correctly.""" |
| 716 | + driver = InMemoryTestDriver() |
| 717 | + |
| 718 | + inner_payload = (await DataConverter().encode(["x" * 200]))[0] |
| 719 | + stored_key = "payload-0" |
| 720 | + driver._storage[stored_key] = inner_payload.SerializeToString() |
| 721 | + |
| 722 | + legacy_payload = _make_legacy_payload( |
| 723 | + driver_name=driver.name(), |
| 724 | + claim_data={"key": stored_key}, |
| 725 | + size_bytes=inner_payload.ByteSize(), |
| 726 | + ) |
| 727 | + |
| 728 | + converter = DataConverter( |
| 729 | + external_storage=ExternalStorage( |
| 730 | + drivers=[driver], |
| 731 | + payload_size_threshold=100, |
| 732 | + ) |
| 733 | + ) |
| 734 | + decoded = await converter.decode([legacy_payload], [str]) |
| 735 | + assert decoded[0] == "x" * 200 |
| 736 | + assert driver._retrieve_calls == 1 |
| 737 | + |
| 738 | + async def test_legacy_and_new_format_mixed_batch_decode(self): |
| 739 | + """A batch containing legacy-format, new proto-format, and inline payloads |
| 740 | + all decode correctly in a single call.""" |
| 741 | + driver = InMemoryTestDriver() |
| 742 | + converter = DataConverter( |
| 743 | + external_storage=ExternalStorage( |
| 744 | + drivers=[driver], |
| 745 | + payload_size_threshold=50, |
| 746 | + ) |
| 747 | + ) |
| 748 | + |
| 749 | + new_value = "new-format-value" * 20 |
| 750 | + inline_value = "small" |
| 751 | + encoded = await converter.encode([new_value, inline_value]) |
| 752 | + new_format_payload = encoded[0] |
| 753 | + inline_payload = encoded[1] |
| 754 | + assert driver._store_calls == 1 |
| 755 | + |
| 756 | + legacy_value = "legacy-format-value" * 20 |
| 757 | + legacy_inner = (await DataConverter().encode([legacy_value]))[0] |
| 758 | + stored_key = f"payload-{len(driver._storage)}" |
| 759 | + driver._storage[stored_key] = legacy_inner.SerializeToString() |
| 760 | + legacy_payload = _make_legacy_payload( |
| 761 | + driver_name=driver.name(), |
| 762 | + claim_data={"key": stored_key}, |
| 763 | + size_bytes=legacy_inner.ByteSize(), |
| 764 | + ) |
| 765 | + |
| 766 | + decoded = await converter.decode( |
| 767 | + [legacy_payload, new_format_payload, inline_payload], [str, str, str] |
| 768 | + ) |
| 769 | + assert decoded[0] == legacy_value |
| 770 | + assert decoded[1] == new_value |
| 771 | + assert decoded[2] == inline_value |
| 772 | + # Both external payloads share the same driver and are batched into one retrieve call. |
| 773 | + assert driver._retrieve_calls == 1 |
| 774 | + |
| 775 | + async def test_new_format_encode_round_trips(self): |
| 776 | + """Payloads written with the new ExternalStorageReference format round-trip |
| 777 | + correctly and carry the expected proto encoding.""" |
| 778 | + driver = InMemoryTestDriver() |
| 779 | + converter = DataConverter( |
| 780 | + external_storage=ExternalStorage( |
| 781 | + drivers=[driver], |
| 782 | + payload_size_threshold=50, |
| 783 | + ) |
| 784 | + ) |
| 785 | + |
| 786 | + value = "round-trip-value" * 20 |
| 787 | + encoded = await converter.encode([value]) |
| 788 | + ref_payload = encoded[0] |
| 789 | + |
| 790 | + assert ref_payload.metadata.get("encoding") == b"json/protobuf" |
| 791 | + assert len(ref_payload.external_payloads) > 0 |
| 792 | + |
| 793 | + ref = JSONProtoPayloadConverter().from_payload( |
| 794 | + ref_payload, ExternalStorageReference |
| 795 | + ) |
| 796 | + assert isinstance(ref, ExternalStorageReference) |
| 797 | + assert ref.driver_name == driver.name() |
| 798 | + assert "key" in ref.claim_data |
| 799 | + |
| 800 | + decoded = await converter.decode(encoded, [str]) |
| 801 | + assert decoded[0] == value |
| 802 | + |
| 803 | + |
697 | 804 | if __name__ == "__main__": |
698 | 805 | pytest.main([__file__, "-v"]) |
0 commit comments