Skip to content

Commit 2884c47

Browse files
author
Manan Rajotia
committed
Multithreading robustness and defensive checks added - PR comments addressal
Signed-off-by: Manan Rajotia <rajotia@amazon.com>
1 parent 213493d commit 2884c47

2 files changed

Lines changed: 129 additions & 3 deletions

File tree

data-prepper-plugins/aws-lambda/src/main/java/org/opensearch/dataprepper/plugins/lambda/common/StreamingLambdaHandler.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,11 @@ public CompletableFuture<List<Record<Event>>> invokeWithStreaming(Buffer inputBu
8888
try {
8989
// DefaultPayloadChunk should have payload() method
9090
byte[] chunkBytes = chunk.payload().asByteArray();
91-
responseStream.write(chunkBytes);
91+
// Synchronize access to ByteArrayOutputStream as it's not thread-safe
92+
// AWS SDK may deliver chunks on different threads
93+
synchronized (responseStream) {
94+
responseStream.write(chunkBytes);
95+
}
9296
LOG.debug("Received chunk of size: {} bytes", chunkBytes.length);
9397
} catch (IOException e) {
9498
LOG.error("Error writing chunk to response stream", e);
@@ -173,6 +177,11 @@ private List<Record<Event>> applyResponseHandling(
173177
* All chunks from the streaming response are merged into one Event,
174178
* matching the original input event count.
175179
*
180+
* <p>The reconstructed event retains the original event's EventHandle, enabling proper end-to-end
181+
* acknowledgement tracking. Chunks are treated as transport-level fragments (due to Lambda's
182+
* response size limits) and are not tracked separately in the acknowledgement system - only the
183+
* final reconstructed event is acknowledged downstream.</p>
184+
*
176185
* @param parsedRecords All chunks parsed from the streaming response
177186
* @param inputBuffer Original input buffer
178187
* @return List containing the reconstructed document(s)
@@ -193,8 +202,21 @@ private List<Record<Event>> reconstructDocument(
193202
return parsedRecords;
194203
}
195204

196-
// For now, merge all chunks into the first original record
197-
// This handles the common case: 1 input event → multiple chunks → 1 reconstructed event
205+
// Defensive check: reconstruct-document requires exactly 1 event per buffer
206+
// This should be enforced by validation in LambdaProcessor, but we check here to prevent silent failures
207+
if (originalRecords.size() != 1) {
208+
String errorMsg = String.format(
209+
"reconstruct-document mode requires exactly 1 event per buffer, found %d events. " +
210+
"This should have been prevented by configuration validation. " +
211+
"Please ensure batch.threshold.event_count is set to 1.",
212+
originalRecords.size()
213+
);
214+
LOG.error(errorMsg);
215+
throw new IllegalStateException(errorMsg);
216+
}
217+
218+
// Merge all chunks into the single original record
219+
// This handles: 1 input event → multiple chunks → 1 reconstructed event
198220
Event reconstructedEvent = originalRecords.get(0).getData();
199221

200222
// Merge all parsed chunks into the reconstructed event

data-prepper-plugins/aws-lambda/src/test/java/org/opensearch/dataprepper/plugins/lambda/common/StreamingLambdaHandlerTest.java

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,15 @@
2929
import software.amazon.awssdk.services.lambda.model.InvokeWithResponseStreamRequest;
3030
import software.amazon.awssdk.services.lambda.model.InvokeWithResponseStreamResponseHandler;
3131

32+
import java.io.ByteArrayOutputStream;
3233
import java.util.List;
3334
import java.util.concurrent.CompletableFuture;
35+
import java.util.concurrent.CountDownLatch;
36+
import java.util.concurrent.ExecutorService;
37+
import java.util.concurrent.Executors;
38+
import java.util.concurrent.TimeUnit;
3439

40+
import static org.junit.jupiter.api.Assertions.assertEquals;
3541
import static org.junit.jupiter.api.Assertions.assertNotNull;
3642
import static org.junit.jupiter.api.Assertions.assertTrue;
3743
import static org.mockito.ArgumentMatchers.any;
@@ -211,4 +217,102 @@ void testDocumentReconstruction_WithReconstructDisabled_ReturnsChunksSeparately(
211217
// Then - without reconstruction, chunks remain separate
212218
assertNotNull(result);
213219
}
220+
221+
@Test
222+
void testChunkWriting_WithConcurrentThreads_MaintainsDataIntegrity() throws InterruptedException {
223+
// This test verifies that ByteArrayOutputStream writes are thread-safe
224+
// Simulates the scenario where AWS SDK delivers chunks on different threads
225+
226+
int numThreads = 10;
227+
int chunksPerThread = 10;
228+
int bytesPerChunk = 100;
229+
230+
ByteArrayOutputStream testStream = new ByteArrayOutputStream();
231+
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
232+
233+
CountDownLatch startLatch = new CountDownLatch(1);
234+
CountDownLatch doneLatch = new CountDownLatch(numThreads * chunksPerThread);
235+
236+
// Track which chunks were written for verification
237+
java.util.Set<String> writtenChunks = java.util.concurrent.ConcurrentHashMap.newKeySet();
238+
239+
// Each thread writes multiple chunks concurrently
240+
for (int t = 0; t < numThreads; t++) {
241+
final int threadId = t;
242+
for (int c = 0; c < chunksPerThread; c++) {
243+
final int chunkId = c;
244+
executor.submit(() -> {
245+
try {
246+
startLatch.await(); // Synchronize start for maximum concurrency
247+
248+
// Create a chunk with unique identifiable pattern
249+
byte[] chunk = new byte[bytesPerChunk];
250+
// Fill chunk with a repeating pattern: [threadId, chunkId, threadId, chunkId, ...]
251+
for (int i = 0; i < bytesPerChunk; i++) {
252+
chunk[i] = (i % 2 == 0) ? (byte) threadId : (byte) chunkId;
253+
}
254+
255+
// Track this chunk for later verification
256+
writtenChunks.add(threadId + "-" + chunkId);
257+
258+
// Simulate the synchronized write that should happen in StreamingLambdaHandler
259+
synchronized (testStream) {
260+
testStream.write(chunk);
261+
}
262+
263+
doneLatch.countDown();
264+
} catch (Exception e) {
265+
// Fail the test if exception occurs
266+
throw new RuntimeException("Chunk write failed", e);
267+
}
268+
});
269+
}
270+
}
271+
272+
// Start all threads simultaneously
273+
startLatch.countDown();
274+
275+
// Wait for all writes to complete
276+
boolean completed = doneLatch.await(10, TimeUnit.SECONDS);
277+
assertTrue(completed, "All chunk writes should complete within timeout");
278+
279+
executor.shutdown();
280+
boolean terminated = executor.awaitTermination(5, TimeUnit.SECONDS);
281+
assertTrue(terminated, "Executor should terminate");
282+
283+
// Verify 1: Total size should equal expected total bytes (no data loss)
284+
int expectedSize = numThreads * chunksPerThread * bytesPerChunk;
285+
assertEquals(expectedSize, testStream.size(),
286+
"Total bytes written should match expected size (no data loss)");
287+
288+
// Verify 2: All chunks were written (no missing chunks)
289+
assertEquals(numThreads * chunksPerThread, writtenChunks.size(),
290+
"All chunks should have been written");
291+
292+
// Verify 3: Chunk integrity - each chunk should be complete and not interleaved
293+
byte[] allBytes = testStream.toByteArray();
294+
int chunkCount = allBytes.length / bytesPerChunk;
295+
296+
for (int i = 0; i < chunkCount; i++) {
297+
int offset = i * bytesPerChunk;
298+
299+
// Extract this chunk
300+
byte[] extractedChunk = new byte[bytesPerChunk];
301+
System.arraycopy(allBytes, offset, extractedChunk, 0, bytesPerChunk);
302+
303+
// Verify chunk has consistent pattern (not interleaved)
304+
// First byte should be threadId, alternating with chunkId
305+
byte threadId = extractedChunk[0];
306+
byte chunkId = extractedChunk[1];
307+
308+
for (int j = 0; j < bytesPerChunk; j++) {
309+
byte expected = (j % 2 == 0) ? threadId : chunkId;
310+
assertEquals(expected, extractedChunk[j],
311+
String.format("Chunk %d byte %d has wrong value - chunk may be corrupted/interleaved", i, j));
312+
}
313+
}
314+
315+
// If we reach here, all chunks maintained their integrity - no interleaving occurred
316+
// This proves the synchronized block prevents data corruption
317+
}
214318
}

0 commit comments

Comments
 (0)