2222import com .uber .cadence .converter .JsonDataConverter ;
2323import java .io .IOException ;
2424import java .lang .reflect .Type ;
25- import java .nio .charset .StandardCharsets ;
2625import java .security .MessageDigest ;
2726import java .security .NoSuchAlgorithmException ;
2827
3938 *
4039 * <ul>
4140 * <li>{@code 0x00 || json} — payload is small enough to inline.
42- * <li>{@code 0x01 || jsonEnvelope} — payload was offloaded; the envelope JSON has the form
43- * {@code {"__s3_ref ":"<bucket>/<sha256hex>"}}.
41+ * <li>{@code 0x01 || jsonEnvelope} — payload was offloaded; the envelope JSON has the form {@code
42+ * {"s3Ref ":"<bucket>/<sha256hex>"}}.
4443 * </ul>
4544 *
4645 * <p>Keys are derived from the SHA-256 of the payload so {@code toData} is idempotent across
4746 * Cadence workflow replays. Using a fresh UUID per call would write a new orphaned blob on every
4847 * replay because the SDK calls {@code toData} again each time the workflow re-executes from the
49- * top. If the workflow needs to control the key (e.g. to encode routing metadata), generate it
50- * with {@code Workflow.sideEffect} and pass it alongside the payload instead.
48+ * top. If the workflow needs to control the key (e.g. to encode routing metadata), generate it with
49+ * {@code Workflow.sideEffect} and pass it alongside the payload instead.
5150 */
5251/*
5352 * =============================================================================
@@ -104,12 +103,31 @@ public final class S3OffloadDataConverter implements DataConverter {
104103 private final String bucket ;
105104 private final int thresholdBytes ;
106105
106+ static final class BlobReference {
107+ public String s3Ref ;
108+
109+ public BlobReference () {}
110+
111+ BlobReference (String s3Ref ) {
112+ this .s3Ref = s3Ref ;
113+ }
114+ }
115+
107116 /**
108117 * @param store the BlobStore backend (use {@link LocalFsBlobStore} for zero-config demo).
109118 * @param bucket logical bucket / prefix name embedded in the reference key.
110119 * @param thresholdBytes max inline payload size; larger payloads are offloaded.
111120 */
112121 public S3OffloadDataConverter (BlobStore store , String bucket , int thresholdBytes ) {
122+ if (store == null ) {
123+ throw new IllegalArgumentException ("store must not be null" );
124+ }
125+ if (bucket == null || bucket .trim ().isEmpty ()) {
126+ throw new IllegalArgumentException ("bucket must not be null or empty" );
127+ }
128+ if (thresholdBytes < 0 ) {
129+ throw new IllegalArgumentException ("thresholdBytes must not be negative" );
130+ }
113131 this .store = store ;
114132 this .bucket = bucket ;
115133 this .thresholdBytes = thresholdBytes ;
@@ -140,8 +158,7 @@ public byte[] toData(Object... values) throws DataConverterException {
140158 "Failed to offload payload to blob store (key=" + key + ")" , e );
141159 }
142160
143- String envelope = "{\" __s3_ref\" :\" " + key + "\" }" ;
144- byte [] envBytes = envelope .getBytes (StandardCharsets .UTF_8 );
161+ byte [] envBytes = delegate .toData (new BlobReference (key ));
145162 byte [] result = new byte [1 + envBytes .length ];
146163 result [0 ] = OFFLOAD_PREFIX ;
147164 System .arraycopy (envBytes , 0 , result , 1 , envBytes .length );
@@ -173,7 +190,7 @@ private byte[] unwrap(byte[] content) throws DataConverterException {
173190 case INLINE_PREFIX :
174191 return body ;
175192 case OFFLOAD_PREFIX :
176- String key = extractS3Ref (new String ( body , StandardCharsets . UTF_8 ) );
193+ String key = extractS3Ref (body );
177194 try {
178195 return store .get (key );
179196 } catch (IOException e ) {
@@ -186,24 +203,13 @@ private byte[] unwrap(byte[] content) throws DataConverterException {
186203 }
187204 }
188205
189- /**
190- * Extracts the value of {@code __s3_ref} from the envelope JSON without bringing in a JSON
191- * parser. The envelope is produced by this class, so the format is fixed and trivially parseable.
192- */
193- private static String extractS3Ref (String envelopeJson ) throws DataConverterException {
194- String marker = "\" __s3_ref\" :\" " ;
195- int start = envelopeJson .indexOf (marker );
196- if (start < 0 ) {
197- throw new DataConverterException (
198- "s3 offload: envelope missing __s3_ref field: " + envelopeJson , null );
199- }
200- start += marker .length ();
201- int end = envelopeJson .indexOf ('"' , start );
202- if (end < 0 ) {
203- throw new DataConverterException (
204- "s3 offload: envelope __s3_ref field is unterminated: " + envelopeJson , null );
206+ private static String extractS3Ref (byte [] envelopeJson ) throws DataConverterException {
207+ BlobReference reference =
208+ delegate .fromData (envelopeJson , BlobReference .class , BlobReference .class );
209+ if (reference == null || reference .s3Ref == null || reference .s3Ref .isEmpty ()) {
210+ throw new DataConverterException ("s3 offload: envelope missing s3Ref field" , null );
205211 }
206- return envelopeJson . substring ( start , end ) ;
212+ return reference . s3Ref ;
207213 }
208214
209215 private static String sha256Hex (byte [] data ) throws DataConverterException {
0 commit comments