xiangfu0 commented on code in PR #19825:
URL: https://github.com/apache/pinot/pull/19825#discussion_r4244083976


##########
pinot-core/src/main/java/org/apache/pinot/core/data/table/IndexedTable.java:
##########
@@ -108,6 +109,29 @@ public boolean upsert(Record record) {
     return upsert(new Key(keyValues), record);
   }
 
+  /// Upserts an already-extracted result into a table with exactly one 
aggregation function.
+  /// The table may retain the key and result; callers must not modify them 
after this call.
+  /// A null result follows the aggregation function's existing merge 
semantics.
+  public boolean upsertSingleAggregationResult(Key key, @Nullable Object 
result) {

Review Comment:
   [pre-existing, surfaced by this PR's new entry point] The new 
`upsertSingleAggregationResult` routes into `addOrUpdateRecord` for 
`DeterministicConcurrentIndexedTable`, whose `_lookupMap` is a 
`ConcurrentSkipListMap`. Unlike `ConcurrentHashMap`, `CSLM.compute` invokes the 
remapping function without any per-key lock and commits via CAS on the value 
reference. Since `updateRecord`/`updateSingleAggregationRecord` mutate the 
existing `Record`'s values array in place and return the same reference, the 
CAS trivially succeeds for every racer: two combine threads merging the same 
group key can both read `values[i]` (e.g. count 5), both write 6, and one 
segment's contribution is silently lost. Trigger: 
`accurateGroupByWithoutOrderBy` query option with a multi-threaded combine. Not 
introduced by this PR (the old `upsert(Key, Record)` path has the same race), 
but worth fixing since this table's whole purpose is accuracy; the in-place 
merge needs a lock or a copy-on-write record for CSLM.



##########
pinot-core/src/main/java/org/apache/pinot/core/data/table/IndexedTable.java:
##########
@@ -108,6 +109,29 @@ public boolean upsert(Record record) {
     return upsert(new Key(keyValues), record);
   }
 
+  /// Upserts an already-extracted result into a table with exactly one 
aggregation function.
+  /// The table may retain the key and result; callers must not modify them 
after this call.
+  /// A null result follows the aggregation function's existing merge 
semantics.
+  public boolean upsertSingleAggregationResult(Key key, @Nullable Object 
result) {
+    assert _aggregationFunctions.length == 1;

Review Comment:
   The single-aggregation precondition is enforced only by `assert` (here and 
in the `ConcurrentIndexedTable` override), which vanishes under the production 
default `-da`. If a future caller invokes this on a table with N>1 aggregation 
functions, `createSingleAggregationRecord` silently builds a record of length 
`_numKeyColumns + 1` that is shorter than the table's schema: a later merge via 
`updateRecord` throws `ArrayIndexOutOfBoundsException` far from the cause, or 
serialization reads a missing column (silently wrong results). A cheap hard 
check fails fast at the call boundary:
   
   ```suggestion
       Preconditions.checkState(_aggregationFunctions.length == 1,
           "upsertSingleAggregationResult requires exactly one aggregation 
function");
   ```



##########
pinot-core/src/main/java/org/apache/pinot/core/data/table/ConcurrentIndexedTable.java:
##########
@@ -48,6 +50,26 @@ public boolean upsert(Key key, Record record) {
     return true;
   }
 
+  @Override
+  public boolean upsertSingleAggregationResult(Key key, @Nullable Object 
result) {
+    assert _aggregationFunctions.length == 1;
+    if (_hasOrderBy) {
+      return super.upsertSingleAggregationResult(key, result);
+    }
+    BiFunction<Key, Record, Record> mergeResult = (k, record) -> record == null
+        ? createSingleAggregationRecord(k, result)
+        : updateSingleAggregationRecord(record, result);
+    if (_noMoreNewRecords.get()) {

Review Comment:
   This block re-implements `upsertWithoutOrderBy`'s admission control (the 
`_noMoreNewRecords` pre-check, the `computeIfPresent` vs `compute` split, and 
the `size() >= _resultSize` flag transition) as a parallel copy. Any future fix 
to the cap logic (changing `>=`, counting dropped groups for a trim metric, 
fixing the size-check race) now has to be made twice, or single-agg and 
multi-agg queries silently admit different groups. Consider a shared helper 
taking the `BiFunction` (e.g. `upsertWithoutOrderBy(Key, BiFunction<Key, 
Record, Record>)`) that both paths call. Related: a subclass overriding 
`upsertWithoutOrderBy` to customize unordered admission (the pattern 
`UnboundedConcurrentIndexedTable` establishes for ordered upserts) would be 
silently bypassed on the single-agg path.



##########
pinot-core/src/main/java/org/apache/pinot/core/data/table/IndexedTable.java:
##########
@@ -108,6 +109,29 @@ public boolean upsert(Record record) {
     return upsert(new Key(keyValues), record);
   }
 
+  /// Upserts an already-extracted result into a table with exactly one 
aggregation function.
+  /// The table may retain the key and result; callers must not modify them 
after this call.
+  /// A null result follows the aggregation function's existing merge 
semantics.
+  public boolean upsertSingleAggregationResult(Key key, @Nullable Object 
result) {
+    assert _aggregationFunctions.length == 1;
+    return upsert(key, createSingleAggregationRecord(key, result));
+  }
+
+  protected Record createSingleAggregationRecord(Key key, @Nullable Object 
result) {
+    Object[] values = Arrays.copyOf(key.getValues(), _numKeyColumns + 1);
+    values[_numKeyColumns] = result;
+    return new Record(values);
+  }
+
+  protected Record updateSingleAggregationRecord(Record record, @Nullable 
Object result) {

Review Comment:
   `updateSingleAggregationRecord` duplicates `updateRecord`'s merge dispatch 
(`_hasFinalInput ? mergeFinalResult : merge`, same `_numKeyColumns` indexing, 
same `Comparable` casts) specialized to one column. If the merge contract 
changes (null handling, a new input mode, layout change), both methods must 
change together or the single-agg and multi-agg paths merge differently. 
Extracting a shared per-column helper, e.g. `Object mergeValue(int funcIdx, 
Object existing, Object input)`, used by both removes the drift risk.



##########
pinot-core/src/main/java/org/apache/pinot/core/data/table/IndexedTable.java:
##########
@@ -108,6 +109,29 @@ public boolean upsert(Record record) {
     return upsert(new Key(keyValues), record);
   }
 
+  /// Upserts an already-extracted result into a table with exactly one 
aggregation function.
+  /// The table may retain the key and result; callers must not modify them 
after this call.
+  /// A null result follows the aggregation function's existing merge 
semantics.
+  public boolean upsertSingleAggregationResult(Key key, @Nullable Object 
result) {
+    assert _aggregationFunctions.length == 1;
+    return upsert(key, createSingleAggregationRecord(key, result));

Review Comment:
   The deferred-allocation benefit only reaches 
`UnboundedConcurrentIndexedTable`. For a no-order-by combine, 
`GroupByUtils.getTrimDisabledIndexedTable` can also return `SimpleIndexedTable` 
(numThreads == 1) and `DeterministicConcurrentIndexedTable`, which fall into 
this base implementation that still does `Arrays.copyOf` + `new Record` per 
group key per segment -- exactly the allocation the PR removes, now plus an 
extra call. `HashMap` and `ConcurrentSkipListMap` both support `compute`, so 
the lazy create/merge could live in base-class helpers (mirroring 
`addOrUpdateRecord`/`updateExistingRecord`) and cover all three table types.



##########
pinot-core/src/main/java/org/apache/pinot/core/operator/combine/GroupByCombineOperator.java:
##########
@@ -77,6 +78,8 @@ public GroupByCombineOperator(List<Operator> operators, 
QueryContext queryContex
     assert _queryContext.getGroupByExpressions() != null;
     _numKeyColumns = _queryContext.getNumGroupByKeyColumns();
     _numColumns = _numKeyColumns + _numAggregationFunctions;
+    _useSingleAggregationResult = _numAggregationFunctions == 1

Review Comment:
   This caller-side gate duplicates dispatch the table already owns: 
`ConcurrentIndexedTable.upsertSingleAggregationResult` falls back to the 
generic path when `_hasOrderBy`, and the base implementation is 
behavior-identical to the eager loop for any single-aggregation query. As a 
result the `_hasOrderBy` fallback branch in the override is production-dead 
(reachable only from the new test), and "when is the scalar path valid" is now 
encoded in two places that can drift (e.g. a second call site such as the 
data-table reducer, or a trim-semantics change in the table). Consider either 
reducing this to `_numAggregationFunctions == 1` and letting the table 
dispatch, or documenting why the caller must exclude order-by/grouping sets.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to