diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/RetinaPayloadBuilder.java b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/RetinaPayloadBuilder.java index a2ab248..4e2d79b 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/RetinaPayloadBuilder.java +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/RetinaPayloadBuilder.java @@ -19,6 +19,7 @@ import io.pixelsdb.pixels.sink.event.RowChangeEvent; import io.pixelsdb.pixels.sink.exception.SinkException; +import java.util.ArrayList; import java.util.List; /** @@ -30,6 +31,41 @@ private RetinaPayloadBuilder() { } + /** + * Builds table updates while preserving the source operation order. + * + *

Retina processes the operation fields inside one {@code TableUpdateData} + * in its own fixed order. Therefore, only adjacent events with the same + * operation may share a payload. The order of the returned payloads matches + * the order of {@code events}.

+ */ + public static List buildTableUpdateDataList( + String tableName, + long timestamp, + List events) throws SinkException + { + if (events == null || events.isEmpty()) + { + throw new IllegalArgumentException("events is empty"); + } + + List updates = new ArrayList<>(); + int groupStart = 0; + for (int i = 1; i <= events.size(); i++) + { + if (i == events.size() || events.get(i).getOp() != events.get(groupStart).getOp()) + { + updates.add(buildTableUpdateData( + tableName, timestamp, events.subList(groupStart, i))); + groupStart = i; + } + } + return updates; + } + + /** + * Builds one table update from a non-empty, homogeneous operation group. + */ public static RetinaProto.TableUpdateData buildTableUpdateData( String tableName, long timestamp, @@ -41,6 +77,14 @@ public static RetinaProto.TableUpdateData buildTableUpdateData( } RowChangeEvent firstEvent = events.get(0); + for (RowChangeEvent event : events) + { + if (event.getOp() != firstEvent.getOp()) + { + throw new IllegalArgumentException( + "TableUpdateData cannot contain different operation types"); + } + } RetinaProto.TableUpdateData.Builder builder = RetinaProto.TableUpdateData.newBuilder() .setTimestamp(timestamp) .setPrimaryIndexId(firstEvent.getTableMetadata().getPrimaryIndexKeyId()) diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableCrossTxWriter.java b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableCrossTxWriter.java index ce1a535..1c90b06 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableCrossTxWriter.java +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableCrossTxWriter.java @@ -72,17 +72,9 @@ public void flush(List batch) { if (smallBatch != null && !smallBatch.isEmpty()) { - RetinaProto.TableUpdateData update = - buildTableUpdateDataFromBatch(txId, smallBatch); - if (update == null) - { - continue; - } - tableUpdateData.add(update); - tableUpdateCount.add(smallBatch.size()); + addTransactionBatch(txId, smallBatch, tableUpdateData, + txIds, fullTableName, tableUpdateCount); } - txIds.add(currTxId); - fullTableName.add(event.getFullTableName()); txId = currTxId; smallBatch = new LinkedList<>(); } @@ -91,13 +83,8 @@ public void flush(List batch) if (smallBatch != null) { - RetinaProto.TableUpdateData update = - buildTableUpdateDataFromBatch(txId, smallBatch); - if (update != null) - { - tableUpdateData.add(update); - tableUpdateCount.add(smallBatch.size()); - } + addTransactionBatch(txId, smallBatch, tableUpdateData, + txIds, fullTableName, tableUpdateCount); } // flushRateLimiter.acquire(batch.size()); @@ -149,6 +136,26 @@ public void flush(List batch) } } + private void addTransactionBatch( + String txId, + List transactionBatch, + List tableUpdateData, + List txIds, + List fullTableNames, + List tableUpdateCount) + { + List updates = + buildTableUpdateDataFromBatch(txId, transactionBatch); + if (updates.isEmpty()) + { + return; + } + tableUpdateData.addAll(updates); + txIds.add(txId); + fullTableNames.add(transactionBatch.get(0).getFullTableName()); + tableUpdateCount.add(transactionBatch.size()); + } + private void failCtxs(List txIds) { for (String writeTxId : txIds) @@ -187,13 +194,13 @@ private void updateCtxCounters(List txIds, List fullTableName, L writeLock.unlock(); } - protected RetinaProto.TableUpdateData buildTableUpdateDataFromBatch( + protected List buildTableUpdateDataFromBatch( String txId, List smallBatch) { SinkContext sinkContext = SinkContextManager.getInstance().getSinkContext(txId); if (sinkContext == null) { - return null; + return List.of(); } try { @@ -212,7 +219,7 @@ protected RetinaProto.TableUpdateData buildTableUpdateDataFromBatch( } try { - return RetinaPayloadBuilder.buildTableUpdateData( + return RetinaPayloadBuilder.buildTableUpdateDataList( tableName, sinkContext.getTimestamp(), smallBatch); } catch (SinkException e) { diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableSingleRecordWriter.java b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableSingleRecordWriter.java index 39391fb..3d2c6a2 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableSingleRecordWriter.java +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableSingleRecordWriter.java @@ -52,12 +52,9 @@ public void flush(List batch) { List tableUpdateData = new LinkedList<>(); // Timestamp + IndexKey binding happen inside RetinaPayloadBuilder. - RetinaProto.TableUpdateData update = + List updates = buildTableUpdateDataFromBatch(pixelsTransContext, batch); - if (update != null) - { - tableUpdateData.add(update); - } + tableUpdateData.addAll(updates); // flushRateLimiter.acquire(batch.size()); long txStartTime = System.currentTimeMillis(); @@ -98,12 +95,12 @@ public void flush(List batch) } } - protected RetinaProto.TableUpdateData buildTableUpdateDataFromBatch( + protected List buildTableUpdateDataFromBatch( TransContext transContext, List smallBatch) { try { - return RetinaPayloadBuilder.buildTableUpdateData( + return RetinaPayloadBuilder.buildTableUpdateDataList( tableName, transContext.getTimestamp(), smallBatch); } catch (SinkException e) { diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableSingleTxWriter.java b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableSingleTxWriter.java index e8cef3e..0aa47ab 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableSingleTxWriter.java +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableSingleTxWriter.java @@ -82,8 +82,8 @@ public void flush(List batchToFlush) RowChangeEvent event1 = batch.get(0); try { - List tableUpdateData = List.of( - RetinaPayloadBuilder.buildTableUpdateData(tableName, 0L, batch)); + List tableUpdateData = + RetinaPayloadBuilder.buildTableUpdateDataList(tableName, 0L, batch); delegate.writeTrans(event1.getSchemaName(), tableUpdateData); sinkContext.updateCounter(fullTableName, batch.size()); // ---- Outside lock: build proto and write ---- diff --git a/src/test/java/io/pixelsdb/pixels/sink/writer/retina/RetinaPayloadBuilderTest.java b/src/test/java/io/pixelsdb/pixels/sink/writer/retina/RetinaPayloadBuilderTest.java index d13a25b..28d0d07 100644 --- a/src/test/java/io/pixelsdb/pixels/sink/writer/retina/RetinaPayloadBuilderTest.java +++ b/src/test/java/io/pixelsdb/pixels/sink/writer/retina/RetinaPayloadBuilderTest.java @@ -29,6 +29,7 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; class RetinaPayloadBuilderTest { @@ -51,43 +52,87 @@ void shouldBuildCompleteUpdateRecordRequest() throws Exception RowChangeEvent delete = event( SinkProto.OperationType.DELETE, row("4", "delete"), null, metadata); - RetinaProto.TableUpdateData tableUpdate = - RetinaPayloadBuilder.buildTableUpdateData( + List tableUpdates = + RetinaPayloadBuilder.buildTableUpdateDataList( TABLE_NAME, TIMESTAMP, List.of(insert, snapshot, update, delete)); RetinaProto.UpdateRecordRequest request = RetinaPayloadBuilder.buildUpdateRecordRequest( - "fixed-token", SCHEMA_NAME, 23, List.of(tableUpdate)); + "fixed-token", SCHEMA_NAME, 23, tableUpdates); assertEquals("fixed-token", request.getHeader().getToken()); assertEquals(SCHEMA_NAME, request.getSchemaName()); assertEquals(23, request.getVirtualNodeId()); - assertEquals(List.of(tableUpdate), request.getTableUpdateDataList()); - assertEquals(TABLE_NAME, tableUpdate.getTableName()); - assertEquals(INDEX_ID, tableUpdate.getPrimaryIndexId()); - assertEquals(TIMESTAMP, tableUpdate.getTimestamp()); + assertEquals(tableUpdates, request.getTableUpdateDataList()); + assertEquals(4, tableUpdates.size()); + for (RetinaProto.TableUpdateData tableUpdate : tableUpdates) + { + assertEquals(TABLE_NAME, tableUpdate.getTableName()); + assertEquals(INDEX_ID, tableUpdate.getPrimaryIndexId()); + assertEquals(TIMESTAMP, tableUpdate.getTimestamp()); + } + + assertEquals(1, tableUpdates.get(0).getInsertDataCount()); + assertRow(tableUpdates.get(0).getInsertData(0).getIndexKeys(0), + tableUpdates.get(0).getInsertData(0).getColValuesList(), "1", "insert"); + assertEquals(List.of(false, false), tableUpdates.get(0).getInsertData(0).getIsNullList()); + assertEquals(1, tableUpdates.get(1).getInsertDataCount()); + assertRow(tableUpdates.get(1).getInsertData(0).getIndexKeys(0), + tableUpdates.get(1).getInsertData(0).getColValuesList(), "2", "snapshot"); + assertEquals(List.of(false, false), tableUpdates.get(1).getInsertData(0).getIsNullList()); + + assertEquals(1, tableUpdates.get(2).getUpdateDataCount()); + assertRow(tableUpdates.get(2).getUpdateData(0).getIndexKeys(0), + tableUpdates.get(2).getUpdateData(0).getColValuesList(), "3", "after"); + assertEquals(List.of(false, false), tableUpdates.get(2).getUpdateData(0).getIsNullList()); - assertEquals(2, tableUpdate.getInsertDataCount()); - assertRow(tableUpdate.getInsertData(0).getIndexKeys(0), - tableUpdate.getInsertData(0).getColValuesList(), "1", "insert"); - assertEquals(List.of(false, false), tableUpdate.getInsertData(0).getIsNullList()); - assertRow(tableUpdate.getInsertData(1).getIndexKeys(0), - tableUpdate.getInsertData(1).getColValuesList(), "2", "snapshot"); - assertEquals(List.of(false, false), tableUpdate.getInsertData(1).getIsNullList()); - - assertEquals(1, tableUpdate.getUpdateDataCount()); - assertRow(tableUpdate.getUpdateData(0).getIndexKeys(0), - tableUpdate.getUpdateData(0).getColValuesList(), "3", "after"); - assertEquals(List.of(false, false), tableUpdate.getUpdateData(0).getIsNullList()); - - assertEquals(1, tableUpdate.getDeleteDataCount()); - assertEquals(bytes("4"), tableUpdate.getDeleteData(0).getIndexKeys(0).getKey()); - assertEquals(INDEX_ID, tableUpdate.getDeleteData(0).getIndexKeys(0).getIndexId()); - assertEquals(TABLE_ID, tableUpdate.getDeleteData(0).getIndexKeys(0).getTableId()); - assertEquals(TIMESTAMP, tableUpdate.getDeleteData(0).getIndexKeys(0).getTimestamp()); + assertEquals(1, tableUpdates.get(3).getDeleteDataCount()); + assertEquals(bytes("4"), tableUpdates.get(3).getDeleteData(0).getIndexKeys(0).getKey()); + assertEquals(INDEX_ID, tableUpdates.get(3).getDeleteData(0).getIndexKeys(0).getIndexId()); + assertEquals(TABLE_ID, tableUpdates.get(3).getDeleteData(0).getIndexKeys(0).getTableId()); + assertEquals(TIMESTAMP, tableUpdates.get(3).getDeleteData(0).getIndexKeys(0).getTimestamp()); assertEquals(request, RetinaProto.UpdateRecordRequest.parseFrom(request.toByteArray())); } + @Test + void shouldGroupOnlyAdjacentEventsWithTheSameOperation() throws Exception + { + TableMetadata metadata = tableMetadata(); + List events = List.of( + event(SinkProto.OperationType.INSERT, null, row("1", "first"), metadata), + event(SinkProto.OperationType.INSERT, null, row("2", "second"), metadata), + event(SinkProto.OperationType.DELETE, row("3", "third"), null, metadata), + event(SinkProto.OperationType.DELETE, row("4", "fourth"), null, metadata), + event(SinkProto.OperationType.INSERT, null, row("5", "fifth"), metadata)); + + List updates = + RetinaPayloadBuilder.buildTableUpdateDataList(TABLE_NAME, TIMESTAMP, events); + + assertEquals(3, updates.size()); + assertEquals(2, updates.get(0).getInsertDataCount()); + assertEquals(2, updates.get(1).getDeleteDataCount()); + assertEquals(1, updates.get(2).getInsertDataCount()); + assertEquals(bytes("1"), updates.get(0).getInsertData(0).getIndexKeys(0).getKey()); + assertEquals(bytes("2"), updates.get(0).getInsertData(1).getIndexKeys(0).getKey()); + assertEquals(bytes("3"), updates.get(1).getDeleteData(0).getIndexKeys(0).getKey()); + assertEquals(bytes("4"), updates.get(1).getDeleteData(1).getIndexKeys(0).getKey()); + assertEquals(bytes("5"), updates.get(2).getInsertData(0).getIndexKeys(0).getKey()); + } + + @Test + void shouldRejectMixedOperationsInOneTableUpdateData() throws Exception + { + TableMetadata metadata = tableMetadata(); + RowChangeEvent insert = event( + SinkProto.OperationType.INSERT, null, row("1", "insert"), metadata); + RowChangeEvent delete = event( + SinkProto.OperationType.DELETE, row("1", "delete"), null, metadata); + + assertThrows(IllegalArgumentException.class, + () -> RetinaPayloadBuilder.buildTableUpdateData( + TABLE_NAME, TIMESTAMP, List.of(insert, delete))); + } + @Test void shouldPropagateIsNullAlongsideColValues() throws Exception { @@ -111,16 +156,16 @@ void shouldPropagateIsNullAlongsideColValues() throws Exception .build(), metadata); - RetinaProto.TableUpdateData tableUpdate = - RetinaPayloadBuilder.buildTableUpdateData( + List tableUpdates = + RetinaPayloadBuilder.buildTableUpdateDataList( TABLE_NAME, TIMESTAMP, List.of(withNull, emptyString)); - assertEquals(List.of(false, true), tableUpdate.getInsertData(0).getIsNullList()); - assertEquals(bytes("5"), tableUpdate.getInsertData(0).getColValues(0)); - assertEquals(ByteString.EMPTY, tableUpdate.getInsertData(0).getColValues(1)); + assertEquals(List.of(false, true), tableUpdates.get(0).getInsertData(0).getIsNullList()); + assertEquals(bytes("5"), tableUpdates.get(0).getInsertData(0).getColValues(0)); + assertEquals(ByteString.EMPTY, tableUpdates.get(0).getInsertData(0).getColValues(1)); - assertEquals(List.of(false, false), tableUpdate.getUpdateData(0).getIsNullList()); - assertEquals(ByteString.EMPTY, tableUpdate.getUpdateData(0).getColValues(1)); + assertEquals(List.of(false, false), tableUpdates.get(1).getUpdateData(0).getIsNullList()); + assertEquals(ByteString.EMPTY, tableUpdates.get(1).getUpdateData(0).getColValues(1)); } private static void assertRow(