Skip to content

Commit 8f922d1

Browse files
authored
[vpj] counting records produced per partition as part of push and adding it to the EOP message
1 parent b519953 commit 8f922d1

26 files changed

Lines changed: 751 additions & 14 deletions

File tree

clients/venice-push-job/src/main/java/com/linkedin/venice/hadoop/VenicePushJob.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -917,10 +917,18 @@ public void run() {
917917
runJobWithKillDetection();
918918

919919
if (!pushJobSetting.suppressEndOfPushMessage) {
920+
Map<Integer, Long> partitionRecordCounts = getPerPartitionRecordCounts();
920921
if (pushJobSetting.sendControlMessagesDirectly) {
921-
getVeniceWriter(pushJobSetting).broadcastEndOfPush(Collections.emptyMap());
922+
getVeniceWriter(pushJobSetting).broadcastEndOfPush(Collections.emptyMap(), partitionRecordCounts);
922923
} else {
923-
controllerClient.writeEndOfPush(pushJobSetting.storeName, pushJobSetting.version);
924+
ControllerResponse eopResponse = controllerClient
925+
.writeEndOfPush(pushJobSetting.storeName, pushJobSetting.version, partitionRecordCounts);
926+
if (eopResponse.isError()) {
927+
throw new VeniceException(
928+
"Failed to write End-of-Push for topic: "
929+
+ Version.composeKafkaTopic(pushJobSetting.storeName, pushJobSetting.version) + ": "
930+
+ eopResponse.getError());
931+
}
924932
}
925933
}
926934
}
@@ -1752,6 +1760,15 @@ void updatePushJobDetailsWithCheckpoint(PushJobCheckpoints checkpoint) {
17521760
pushJobDetails.pushJobLatestCheckpoint = checkpoint.getValue();
17531761
}
17541762

1763+
private Map<Integer, Long> getPerPartitionRecordCounts() {
1764+
String topicName = Version.composeKafkaTopic(pushJobSetting.storeName, pushJobSetting.version);
1765+
if (dataWriterComputeJob == null || dataWriterComputeJob.getTaskTracker() == null) {
1766+
LOGGER.warn("Cannot retrieve per-partition record counts for topic: {}: no task tracker available", topicName);
1767+
return Collections.emptyMap();
1768+
}
1769+
return dataWriterComputeJob.getTaskTracker().getPerPartitionRecordCounts();
1770+
}
1771+
17551772
private void updatePushJobDetailsWithDataWriterTracker() {
17561773
if (dataWriterComputeJob == null) {
17571774
LOGGER.info("No running job found. Skip updating push job details.");

clients/venice-push-job/src/main/java/com/linkedin/venice/hadoop/task/datawriter/AbstractPartitionWriter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,11 @@ public int getValueSchemaId() {
267267
* This doesn't need to be atomic since {@link #processValuesForKey(byte[], Iterator, DataWriterTaskTracker)} will be called sequentially.
268268
*/
269269
private long messageSent = 0;
270+
271+
protected long getMessageSent() {
272+
return messageSent;
273+
}
274+
270275
private final AtomicLong messageCompleted = new AtomicLong();
271276
private final AtomicLong messageErrored = new AtomicLong();
272277
private long timeOfLastReduceFunctionEndInNS = 0;

clients/venice-push-job/src/main/java/com/linkedin/venice/hadoop/task/datawriter/DataWriterTaskTracker.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.linkedin.venice.hadoop.task.datawriter;
22

33
import com.linkedin.venice.hadoop.task.TaskTracker;
4+
import java.util.Collections;
5+
import java.util.Map;
46

57

68
/**
@@ -132,4 +134,14 @@ default long getTotalPutOrDeleteRecordsCount() {
132134
default long getIncrementalPushThrottledTimeMs() {
133135
return 0;
134136
}
137+
138+
/**
139+
* Returns per-partition record counts collected during the data writer job.
140+
* For the Spark path, these are collected via {@code collect()} on the DAG output
141+
*
142+
* @return Map of partition ID to record count, or empty map if not available.
143+
*/
144+
default Map<Integer, Long> getPerPartitionRecordCounts() {
145+
return Collections.emptyMap();
146+
}
135147
}

clients/venice-push-job/src/main/java/com/linkedin/venice/spark/SparkConstants.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class SparkConstants {
1818

1919
// Internal column names, hence begins with "_"
2020
public static final String PARTITION_COLUMN_NAME = "__partition__";
21+
public static final String RECORD_COUNT_COLUMN_NAME = "__record_count__";
2122
public static final String SCHEMA_ID_COLUMN_NAME = "__schema_id__";
2223
public static final String RMD_VERSION_ID_COLUMN_NAME = "__replication_metadata_version_id__";
2324
public static final String OFFSET_COLUMN_NAME = "__offset__";
@@ -29,6 +30,10 @@ public class SparkConstants {
2930
new StructField(VALUE_COLUMN_NAME, BinaryType, true, Metadata.empty()),
3031
new StructField(RMD_COLUMN_NAME, BinaryType, true, Metadata.empty()) });
3132

33+
public static final StructType PARTITION_RECORD_COUNT_SCHEMA = new StructType(
34+
new StructField[] { new StructField(PARTITION_COLUMN_NAME, IntegerType, false, Metadata.empty()),
35+
new StructField(RECORD_COUNT_COLUMN_NAME, LongType, false, Metadata.empty()) });
36+
3237
public static final StructType DEFAULT_SCHEMA_WITH_PARTITION = new StructType(
3338
new StructField[] { new StructField(KEY_COLUMN_NAME, BinaryType, false, Metadata.empty()),
3439
new StructField(VALUE_COLUMN_NAME, BinaryType, true, Metadata.empty()),

clients/venice-push-job/src/main/java/com/linkedin/venice/spark/datawriter/jobs/AbstractDataWriterSparkJob.java

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static com.linkedin.venice.spark.SparkConstants.OFFSET;
2121
import static com.linkedin.venice.spark.SparkConstants.OFFSET_COLUMN_NAME;
2222
import static com.linkedin.venice.spark.SparkConstants.PARTITION_COLUMN_NAME;
23+
import static com.linkedin.venice.spark.SparkConstants.PARTITION_RECORD_COUNT_SCHEMA;
2324
import static com.linkedin.venice.spark.SparkConstants.RAW_PUBSUB_INPUT_TABLE_SCHEMA;
2425
import static com.linkedin.venice.spark.SparkConstants.REPLICATION_METADATA_PAYLOAD;
2526
import static com.linkedin.venice.spark.SparkConstants.RMD_COLUMN_NAME;
@@ -113,9 +114,11 @@
113114
import java.util.Arrays;
114115
import java.util.Collections;
115116
import java.util.Comparator;
117+
import java.util.HashMap;
116118
import java.util.HashSet;
117119
import java.util.Iterator;
118120
import java.util.List;
121+
import java.util.Map;
119122
import java.util.Optional;
120123
import java.util.Properties;
121124
import java.util.Set;
@@ -844,7 +847,7 @@ public void runComputeJob() {
844847
validateRmdSchema(pushJobSetting);
845848

846849
ExpressionEncoder<Row> rowEncoder = RowEncoder.apply(DEFAULT_SCHEMA);
847-
ExpressionEncoder<Row> rowEncoderWithPartition = RowEncoder.apply(DEFAULT_SCHEMA_WITH_PARTITION);
850+
ExpressionEncoder<Row> partitionRecordCountEncoder = RowEncoder.apply(PARTITION_RECORD_COUNT_SCHEMA);
848851
int numOutputPartitions = pushJobSetting.partitionCount;
849852

850853
Properties jobProps = new Properties();
@@ -913,11 +916,30 @@ public void runComputeJob() {
913916
} finally {
914917
kafkaWriteMetrics.timeNs.add(System.nanoTime() - startNs);
915918
}
916-
}, rowEncoderWithPartition);
917-
918-
// For VPJ, we don't care about the output from the DAG. ".count()" is an action that will trigger execution of
919-
// the DAG to completion and will not copy all the rows to the driver to be more memory efficient.
920-
dataFrame.count();
919+
}, partitionRecordCountEncoder);
920+
921+
/*
922+
* collect() returns exactly one (partitionId, recordCount) row per Spark partition. With
923+
* speculative execution, if the original task and a speculative attempt both finish at the
924+
* same time, Spark's TaskScheduler accepts the result from whichever one successfully
925+
* communicates completion first and immediately kills the other; the duplicate's work is
926+
* discarded to prevent duplicate data processing. So no two rows in the collected list will
927+
* ever share the same partition id, and we can populate the map directly via put().
928+
*
929+
* The collected data volume is bounded by numPartitions (e.g. 10K partitions = ~160KB) so
930+
* collectAsList() is safe.
931+
*/
932+
String topicName = pushJobSetting.topic;
933+
List<Row> partitionCountRows = dataFrame.collectAsList();
934+
Map<Integer, Long> perPartitionRecordCounts = new HashMap<>(partitionCountRows.size());
935+
for (Row row: partitionCountRows) {
936+
perPartitionRecordCounts.put(row.getInt(0), row.getLong(1));
937+
}
938+
taskTracker.setPerPartitionRecordCounts(perPartitionRecordCounts);
939+
LOGGER.info(
940+
"Collected per-partition record counts for topic: {} ({} partitions)",
941+
topicName,
942+
perPartitionRecordCounts.size());
921943
} finally {
922944
// No matter what, always log the final accumulator values
923945
logAccumulatorValues();

clients/venice-push-job/src/main/java/com/linkedin/venice/spark/datawriter/task/SparkDataWriterTaskTracker.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package com.linkedin.venice.spark.datawriter.task;
22

33
import com.linkedin.venice.hadoop.task.datawriter.DataWriterTaskTracker;
4+
import java.util.Collections;
5+
import java.util.HashMap;
6+
import java.util.Map;
47

58

69
/**
710
* This class is used to track the metrics for the Spark Data Writer task.
811
*/
912
public class SparkDataWriterTaskTracker implements DataWriterTaskTracker {
1013
private final DataWriterAccumulators accumulators;
14+
private Map<Integer, Long> perPartitionRecordCounts = Collections.emptyMap();
1115

1216
public SparkDataWriterTaskTracker(DataWriterAccumulators accumulators) {
1317
this.accumulators = accumulators;
@@ -172,4 +176,18 @@ public long getRepushTtlFilterCount() {
172176
public long getIncrementalPushThrottledTimeMs() {
173177
return accumulators.incrementalPushThrottleTimeCounter.value();
174178
}
179+
180+
/**
181+
* Sets the per-partition record counts collected from the Spark DAG output via {@code collect()}.
182+
*/
183+
public void setPerPartitionRecordCounts(Map<Integer, Long> counts) {
184+
this.perPartitionRecordCounts = (counts == null || counts.isEmpty())
185+
? Collections.emptyMap()
186+
: Collections.unmodifiableMap(new HashMap<>(counts));
187+
}
188+
189+
@Override
190+
public Map<Integer, Long> getPerPartitionRecordCounts() {
191+
return perPartitionRecordCounts;
192+
}
175193
}

clients/venice-push-job/src/main/java/com/linkedin/venice/spark/datawriter/writer/SparkPartitionWriter.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,11 @@ void processRows(Iterator<Row> rows) {
8585
super.processValuesForKey(key, valueRecordsForKey.iterator(), dataWriterTaskTracker);
8686
}
8787
}
88+
89+
/**
90+
* @return The number of records sent to PubSub by this partition writer.
91+
*/
92+
long getRecordCount() {
93+
return getMessageSent();
94+
}
8895
}

clients/venice-push-job/src/main/java/com/linkedin/venice/spark/datawriter/writer/SparkPartitionWriterFactory.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package com.linkedin.venice.spark.datawriter.writer;
22

33
import com.linkedin.venice.spark.datawriter.task.DataWriterAccumulators;
4+
import java.util.Collections;
45
import java.util.Iterator;
56
import java.util.Properties;
7+
import org.apache.spark.TaskContext;
68
import org.apache.spark.api.java.function.MapPartitionsFunction;
79
import org.apache.spark.broadcast.Broadcast;
810
import org.apache.spark.sql.Row;
11+
import org.apache.spark.sql.RowFactory;
912

1013

1114
public class SparkPartitionWriterFactory implements MapPartitionsFunction<Row, Row> {
@@ -20,9 +23,12 @@ public SparkPartitionWriterFactory(Broadcast<Properties> jobProps, DataWriterAcc
2023

2124
@Override
2225
public Iterator<Row> call(Iterator<Row> rows) throws Exception {
26+
long recordCount;
2327
try (SparkPartitionWriter partitionWriter = new SparkPartitionWriter(jobProps.getValue(), accumulators)) {
2428
partitionWriter.processRows(rows);
29+
recordCount = partitionWriter.getRecordCount();
2530
}
26-
return rows;
31+
int partitionId = TaskContext.get().partitionId();
32+
return Collections.singletonList(RowFactory.create(partitionId, recordCount)).iterator();
2733
}
2834
}

clients/venice-push-job/src/test/java/com/linkedin/venice/hadoop/TestVenicePushJobCheckpoints.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,8 @@ private void configureControllerClientMock(
746746
ControllerResponse controllerResponse = mock(ControllerResponse.class);
747747
when(controllerResponse.isError()).thenReturn(false);
748748
when(controllerClient.sendPushJobDetails(anyString(), anyInt(), any(byte[].class))).thenReturn(controllerResponse);
749+
when(controllerClient.writeEndOfPush(anyString(), anyInt())).thenReturn(controllerResponse);
750+
when(controllerClient.writeEndOfPush(anyString(), anyInt(), any())).thenReturn(controllerResponse);
749751
}
750752

751753
private void configureClusterDiscoverControllerClient(ControllerClient controllerClient) {

clients/venice-push-job/src/test/java/com/linkedin/venice/hadoop/VenicePushJobTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,7 @@ private ControllerClient getClient(Consumer<StoreInfo> storeInfo, boolean applyF
809809

810810
ControllerResponse response = new ControllerResponse();
811811
doReturn(response).when(client).writeEndOfPush(anyString(), anyInt());
812+
doReturn(response).when(client).writeEndOfPush(anyString(), anyInt(), any());
812813
doReturn(response).when(client).sendPushJobDetails(anyString(), anyInt(), any(byte[].class));
813814
return client;
814815
}

0 commit comments

Comments
 (0)