xiangfu0 commented on code in PR #18406:
URL: https://github.com/apache/pinot/pull/18406#discussion_r3201306396
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java:
##########
@@ -2111,6 +2115,372 @@ private static void overwriteConfig(JsonNode oldCfg,
JsonNode newCfg) {
}
}
+ // Top-level keys in {@code tableIndexConfig} that look like per-column
collections (string-array of column
+ // names) but must NOT be touched by the consuming-override scrub. Anything
else of string-array shape is treated
+ // as a per-column list and the overridden column name is stripped from it.
Inverting the policy this way
+ // (deny-list rather than allow-list) keeps the scrub correct as new
per-column index config keys are added to
+ // {@link IndexingConfig} — there is no hand-curated list to drift out of
sync. Today every string-array in
+ // {@link IndexingConfig} IS a per-column list (`invertedIndexColumns`,
`noDictionaryColumns`,
+ // `bloomFilterColumns`, `rangeIndexColumns`, `jsonIndexColumns`,
`onHeapDictionaryColumns`,
+ // `varLengthDictionaryColumns`), so this is correct. **Important:** any
future non-per-column string-array field
+ // added to {@link IndexingConfig} MUST be added to this exclusion set,
otherwise the scrub will silently strip
+ // entries from it whenever an overridden column name happens to match.
+ //
+ // - sortedColumn is structurally tied to the segment layout; a separate
validate-time check rejects overrides
+ // on sorted columns.
+ // - starTreeIndexConfigs is a list of objects (not column names); leave it
alone.
+ // - tierOverwrites is the tier-overlay JSON, handled by a separate
transform.
+ private static final Set<String> CONSUMING_OVERRIDE_SCRUB_EXCLUDED_KEYS =
Set.of(
+ "sortedColumn",
+ "starTreeIndexConfigs",
+ "tierOverwrites");
+
+ /// Returns a [TableConfig] with each [FieldConfig#getConsumingOverride()]
merged into its parent
+ /// [FieldConfig]. The result is intended for building the realtime *mutable
consuming* segment only.
+ /// The committed/immutable segment and all loaded immutable segments use
the un-overridden table config — that is
+ /// the persisted shape on disk. A typical use is keeping the table in raw
encoding for storage efficiency while
+ /// giving the consuming segment a dictionary + inverted index for fast
filtering during the consuming window.
+ ///
+ /// Supported override keys: `encodingType`, `indexes`. Other [FieldConfig]
fields are intentionally not
+ /// overridable — see [#ALLOWED_CONSUMING_OVERRIDE_KEYS] for the rationale.
+ ///
+ /// Merge semantics: every supported field present in `consumingOverride`
replaces the parent field wholesale.
+ /// For `indexes` the entire JSON subtree is replaced rather than merged
key-by-key.
+ ///
+ /// For each overridden column, the column is also scrubbed from legacy
per-column lists in `tableIndexConfig`
+ /// (e.g. `invertedIndexColumns`, `rangeIndexColumns`, ...) so that those
lists do not contradict the merged
+ /// [FieldConfig].
+ ///
+ /// If no [FieldConfig] declares a consuming override, the original
[TableConfig] is returned unchanged.
+ ///
+ /// @param tableConfig original table config; not mutated
+ /// @return a new table config with overrides merged, or the original config
when no overrides are configured
+ public static TableConfig applyConsumingOverrides(TableConfig tableConfig) {
+ if (!hasConsumingOverride(tableConfig)) {
+ return tableConfig;
+ }
+ // Enforce the same invariants that {@link #validateConsumingOverrides}
enforces, so a hand-edited or
+ // older-controller-written TableConfig can't reach the merge with an
unsupported override and silently produce
+ // a wrong-shape consuming segment. We re-check here (rather than relying
solely on validate-time) because the
+ // merge runs on every server at consuming-segment build time and may see
stale configs.
+ enforceConsumingOverrideInvariants(tableConfig);
+ try {
+ // Deep-copy the JSON tree before mutating it. {@code toJsonNode} may
alias subtrees on JsonNode-typed fields
+ // of FieldConfig (notably {@code _indexes}, {@code _tierOverwrites},
{@code _consumingOverride}); mutating
+ // those in place would silently corrupt the cached TableConfig that
other threads are concurrently reading.
+ JsonNode tblCfgJson = tableConfig.toJsonNode().deepCopy();
+ JsonNode fieldCfgListJson =
tblCfgJson.get(TableConfig.FIELD_CONFIG_LIST_KEY);
+ // hasConsumingOverride already proved at least one FieldConfig has a
non-empty override, so the JSON view
+ // must surface the field config list as an array. If not, the
TableConfig (de)serialization layer is
+ // inconsistent — fail loudly rather than silently shipping the
un-overridden shape.
+ Preconditions.checkState(fieldCfgListJson != null &&
fieldCfgListJson.isArray(),
+ "fieldConfigList missing from JSON view of table %s despite
hasConsumingOverride==true",
+ tableConfig.getTableName());
+ JsonNode tblIdxCfgJson = tblCfgJson.get(TableConfig.INDEXING_CONFIG_KEY);
+ Set<String> overriddenColumns = new HashSet<>();
+ Iterator<JsonNode> fieldCfgItr = fieldCfgListJson.elements();
+ while (fieldCfgItr.hasNext()) {
+ JsonNode fieldCfgJson = fieldCfgItr.next();
+ JsonNode overrideJson =
fieldCfgJson.get(TableConfig.CONSUMING_OVERRIDE_KEY);
+ if (overrideJson == null || !overrideJson.isObject() ||
overrideJson.isEmpty()) {
+ continue;
+ }
+ JsonNode nameNode = fieldCfgJson.get("name");
+ // FieldConfig's @JsonCreator already guarantees name is non-null, but
we re-parsed via toJsonNode so guard
+ // defensively. A missing name is a bug — fail loudly rather than
silently dropping the override.
+ Preconditions.checkState(nameNode != null && nameNode.isTextual(),
+ "FieldConfig with consumingOverride is missing a textual 'name'
field in table: %s",
+ tableConfig.getTableName());
+ overriddenColumns.add(nameNode.asText());
+ overwriteConfig(fieldCfgJson, overrideJson);
+ // Remove the override marker so downstream consumers see only the
merged shape.
+ ((ObjectNode) fieldCfgJson).remove(TableConfig.CONSUMING_OVERRIDE_KEY);
+ }
+ Preconditions.checkState(!overriddenColumns.isEmpty(),
+ "no overridden columns produced from JSON view of table %s despite
hasConsumingOverride==true",
+ tableConfig.getTableName());
+ if (tblIdxCfgJson != null && tblIdxCfgJson.isObject()) {
+ scrubOverriddenColumnsFromTableIndexConfig((ObjectNode) tblIdxCfgJson,
overriddenColumns);
+ }
+ return JsonUtils.jsonNodeToObject(tblCfgJson, TableConfig.class);
+ } catch (IOException e) {
+ // Surface override parse/deserialization failures (e.g. unknown keys,
type mismatches) loudly so the bad
+ // config is rejected at validate time rather than silently falling back
to the un-overridden shape — that
+ // would build the consuming segment with the wrong layout and the user
would get no signal.
+ throw new IllegalStateException(
+ "Failed to apply consuming overrides for table: " +
tableConfig.getTableName(), e);
+ }
+ }
+
+ /// For each top-level entry in `tableIndexConfig` that holds an array of
column-name strings (legacy
+ /// per-column lists like `invertedIndexColumns`, `noDictionaryColumns`,
etc.), strip any name in
+ /// `overriddenColumns`. Entries whose key is in
[#CONSUMING_OVERRIDE_SCRUB_EXCLUDED_KEYS], that are not arrays,
+ /// or that contain non-string elements, are left alone.
+ ///
+ /// Object-shaped values (e.g. `segmentPartitionConfig.columnPartitionMap`,
feature configs) are intentionally
+ /// NOT scrubbed: the heuristic "drop any object key matching an overridden
column name" would silently drop
+ /// unrelated feature-config keys whose names happen to collide with a
column name. Validation already rejects
+ /// overrides on partition columns up front.
+ private static void scrubOverriddenColumnsFromTableIndexConfig(ObjectNode
tblIdxCfgObj,
+ Set<String> overriddenColumns) {
+ Iterator<Map.Entry<String, JsonNode>> entries =
tblIdxCfgObj.properties().iterator();
+ while (entries.hasNext()) {
+ Map.Entry<String, JsonNode> entry = entries.next();
+ String key = entry.getKey();
+ if (CONSUMING_OVERRIDE_SCRUB_EXCLUDED_KEYS.contains(key)) {
+ continue;
+ }
+ JsonNode value = entry.getValue();
+ if (value == null || !value.isArray() || value.isEmpty()) {
Review Comment:
This scrub only handles array-valued `tableIndexConfig` entries, but
`ForwardIndexType` still treats `noDictionaryConfig.keySet()` as the RAW
signal. That means a table using `noDictionaryConfig` for the persisted RAW
column cannot opt into `consumingOverride.encodingType=DICTIONARY`: the merged
config still carries the map entry and
`validateIndexingConfigAndFieldConfigList` will reject it as a
DICTIONARY-vs-noDictionary conflict. Please scrub `noDictionaryConfig` for
overridden columns too, otherwise the documented RAW-on-disk /
DICTIONARY-on-consuming use case only works for `noDictionaryColumns`.
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java:
##########
@@ -2111,6 +2115,372 @@ private static void overwriteConfig(JsonNode oldCfg,
JsonNode newCfg) {
}
}
+ // Top-level keys in {@code tableIndexConfig} that look like per-column
collections (string-array of column
+ // names) but must NOT be touched by the consuming-override scrub. Anything
else of string-array shape is treated
+ // as a per-column list and the overridden column name is stripped from it.
Inverting the policy this way
+ // (deny-list rather than allow-list) keeps the scrub correct as new
per-column index config keys are added to
+ // {@link IndexingConfig} — there is no hand-curated list to drift out of
sync. Today every string-array in
+ // {@link IndexingConfig} IS a per-column list (`invertedIndexColumns`,
`noDictionaryColumns`,
+ // `bloomFilterColumns`, `rangeIndexColumns`, `jsonIndexColumns`,
`onHeapDictionaryColumns`,
+ // `varLengthDictionaryColumns`), so this is correct. **Important:** any
future non-per-column string-array field
+ // added to {@link IndexingConfig} MUST be added to this exclusion set,
otherwise the scrub will silently strip
+ // entries from it whenever an overridden column name happens to match.
+ //
+ // - sortedColumn is structurally tied to the segment layout; a separate
validate-time check rejects overrides
+ // on sorted columns.
+ // - starTreeIndexConfigs is a list of objects (not column names); leave it
alone.
+ // - tierOverwrites is the tier-overlay JSON, handled by a separate
transform.
+ private static final Set<String> CONSUMING_OVERRIDE_SCRUB_EXCLUDED_KEYS =
Set.of(
+ "sortedColumn",
+ "starTreeIndexConfigs",
+ "tierOverwrites");
+
+ /// Returns a [TableConfig] with each [FieldConfig#getConsumingOverride()]
merged into its parent
+ /// [FieldConfig]. The result is intended for building the realtime *mutable
consuming* segment only.
+ /// The committed/immutable segment and all loaded immutable segments use
the un-overridden table config — that is
+ /// the persisted shape on disk. A typical use is keeping the table in raw
encoding for storage efficiency while
+ /// giving the consuming segment a dictionary + inverted index for fast
filtering during the consuming window.
+ ///
+ /// Supported override keys: `encodingType`, `indexes`. Other [FieldConfig]
fields are intentionally not
+ /// overridable — see [#ALLOWED_CONSUMING_OVERRIDE_KEYS] for the rationale.
+ ///
+ /// Merge semantics: every supported field present in `consumingOverride`
replaces the parent field wholesale.
+ /// For `indexes` the entire JSON subtree is replaced rather than merged
key-by-key.
+ ///
+ /// For each overridden column, the column is also scrubbed from legacy
per-column lists in `tableIndexConfig`
+ /// (e.g. `invertedIndexColumns`, `rangeIndexColumns`, ...) so that those
lists do not contradict the merged
+ /// [FieldConfig].
+ ///
+ /// If no [FieldConfig] declares a consuming override, the original
[TableConfig] is returned unchanged.
+ ///
+ /// @param tableConfig original table config; not mutated
+ /// @return a new table config with overrides merged, or the original config
when no overrides are configured
+ public static TableConfig applyConsumingOverrides(TableConfig tableConfig) {
+ if (!hasConsumingOverride(tableConfig)) {
+ return tableConfig;
+ }
+ // Enforce the same invariants that {@link #validateConsumingOverrides}
enforces, so a hand-edited or
+ // older-controller-written TableConfig can't reach the merge with an
unsupported override and silently produce
+ // a wrong-shape consuming segment. We re-check here (rather than relying
solely on validate-time) because the
+ // merge runs on every server at consuming-segment build time and may see
stale configs.
+ enforceConsumingOverrideInvariants(tableConfig);
+ try {
+ // Deep-copy the JSON tree before mutating it. {@code toJsonNode} may
alias subtrees on JsonNode-typed fields
+ // of FieldConfig (notably {@code _indexes}, {@code _tierOverwrites},
{@code _consumingOverride}); mutating
+ // those in place would silently corrupt the cached TableConfig that
other threads are concurrently reading.
+ JsonNode tblCfgJson = tableConfig.toJsonNode().deepCopy();
+ JsonNode fieldCfgListJson =
tblCfgJson.get(TableConfig.FIELD_CONFIG_LIST_KEY);
+ // hasConsumingOverride already proved at least one FieldConfig has a
non-empty override, so the JSON view
+ // must surface the field config list as an array. If not, the
TableConfig (de)serialization layer is
+ // inconsistent — fail loudly rather than silently shipping the
un-overridden shape.
+ Preconditions.checkState(fieldCfgListJson != null &&
fieldCfgListJson.isArray(),
+ "fieldConfigList missing from JSON view of table %s despite
hasConsumingOverride==true",
+ tableConfig.getTableName());
+ JsonNode tblIdxCfgJson = tblCfgJson.get(TableConfig.INDEXING_CONFIG_KEY);
+ Set<String> overriddenColumns = new HashSet<>();
+ Iterator<JsonNode> fieldCfgItr = fieldCfgListJson.elements();
+ while (fieldCfgItr.hasNext()) {
+ JsonNode fieldCfgJson = fieldCfgItr.next();
+ JsonNode overrideJson =
fieldCfgJson.get(TableConfig.CONSUMING_OVERRIDE_KEY);
+ if (overrideJson == null || !overrideJson.isObject() ||
overrideJson.isEmpty()) {
+ continue;
+ }
+ JsonNode nameNode = fieldCfgJson.get("name");
+ // FieldConfig's @JsonCreator already guarantees name is non-null, but
we re-parsed via toJsonNode so guard
+ // defensively. A missing name is a bug — fail loudly rather than
silently dropping the override.
+ Preconditions.checkState(nameNode != null && nameNode.isTextual(),
+ "FieldConfig with consumingOverride is missing a textual 'name'
field in table: %s",
+ tableConfig.getTableName());
+ overriddenColumns.add(nameNode.asText());
+ overwriteConfig(fieldCfgJson, overrideJson);
+ // Remove the override marker so downstream consumers see only the
merged shape.
+ ((ObjectNode) fieldCfgJson).remove(TableConfig.CONSUMING_OVERRIDE_KEY);
+ }
+ Preconditions.checkState(!overriddenColumns.isEmpty(),
+ "no overridden columns produced from JSON view of table %s despite
hasConsumingOverride==true",
+ tableConfig.getTableName());
+ if (tblIdxCfgJson != null && tblIdxCfgJson.isObject()) {
+ scrubOverriddenColumnsFromTableIndexConfig((ObjectNode) tblIdxCfgJson,
overriddenColumns);
+ }
+ return JsonUtils.jsonNodeToObject(tblCfgJson, TableConfig.class);
+ } catch (IOException e) {
+ // Surface override parse/deserialization failures (e.g. unknown keys,
type mismatches) loudly so the bad
+ // config is rejected at validate time rather than silently falling back
to the un-overridden shape — that
+ // would build the consuming segment with the wrong layout and the user
would get no signal.
+ throw new IllegalStateException(
+ "Failed to apply consuming overrides for table: " +
tableConfig.getTableName(), e);
+ }
+ }
+
+ /// For each top-level entry in `tableIndexConfig` that holds an array of
column-name strings (legacy
+ /// per-column lists like `invertedIndexColumns`, `noDictionaryColumns`,
etc.), strip any name in
+ /// `overriddenColumns`. Entries whose key is in
[#CONSUMING_OVERRIDE_SCRUB_EXCLUDED_KEYS], that are not arrays,
+ /// or that contain non-string elements, are left alone.
+ ///
+ /// Object-shaped values (e.g. `segmentPartitionConfig.columnPartitionMap`,
feature configs) are intentionally
+ /// NOT scrubbed: the heuristic "drop any object key matching an overridden
column name" would silently drop
+ /// unrelated feature-config keys whose names happen to collide with a
column name. Validation already rejects
+ /// overrides on partition columns up front.
+ private static void scrubOverriddenColumnsFromTableIndexConfig(ObjectNode
tblIdxCfgObj,
+ Set<String> overriddenColumns) {
+ Iterator<Map.Entry<String, JsonNode>> entries =
tblIdxCfgObj.properties().iterator();
+ while (entries.hasNext()) {
+ Map.Entry<String, JsonNode> entry = entries.next();
+ String key = entry.getKey();
+ if (CONSUMING_OVERRIDE_SCRUB_EXCLUDED_KEYS.contains(key)) {
+ continue;
+ }
+ JsonNode value = entry.getValue();
+ if (value == null || !value.isArray() || value.isEmpty()) {
+ continue;
+ }
+ ArrayNode arr = (ArrayNode) value;
+ for (JsonNode element : arr) {
+ if (!element.isTextual()) {
+ // Non-string-array entry — not a per-column list; leave alone.
+ arr = null;
+ break;
+ }
+ }
+ if (arr == null) {
+ continue;
+ }
+ List<JsonNode> kept = new ArrayList<>(arr.size());
+ for (JsonNode element : arr) {
+ if (!overriddenColumns.contains(element.asText())) {
+ kept.add(element);
+ }
+ }
+ if (kept.size() != arr.size()) {
+ ArrayNode replacement = JsonUtils.newArrayNode();
+ for (JsonNode element : kept) {
+ replacement.add(element);
+ }
+ tblIdxCfgObj.set(key, replacement);
+ }
+ }
+ }
+
+ /// Builds the [RealtimeSegmentConfig.Builder] used to construct a mutable
consuming segment, applying any
+ /// [FieldConfig#getConsumingOverride()] present on the table config. When
no override is configured, the
+ /// un-modified [IndexLoadingConfig]-driven builder is returned so existing
code paths (tier overwrites,
+ /// instance-level mutations on `IndexLoadingConfig`) flow through
unchanged. If the override merge throws (a
+ /// misconfiguration that slipped past validate), the call falls back to the
`IndexLoadingConfig`-driven builder,
+ /// logs the error, and invokes `onFallback` so the caller can emit a metric
/ surface the degradation. The
+ /// fallback-applied builder produces a consuming segment matching the
persisted shape (no override) for the
+ /// full lifetime of that consuming segment — there is no auto-retry;
operators must reload the table after
+ /// fixing the override config to pick up the corrected shape on the next
consuming-segment build.
+ ///
+ /// **IndexLoadingConfig contract on the override path:** the helper
rebuilds a fresh [IndexLoadingConfig] from
+ /// `(indexLoadingConfig.getInstanceDataManagerConfig(), mergedTableConfig,
schemaCopy)` and re-applies the
+ /// `segmentTier`. Any other in-place mutation the caller made on the
supplied `indexLoadingConfig` (segment
+ /// version, star-tree configs, etc.) is **not** carried over on the
override path. Callers that need such
+ /// mutations preserved must either set them on the returned Builder after
this call, or extend this helper.
+ ///
+ /// **Schema clone:** the override path defensively deep-copies the schema
via JSON round-trip because the
+ /// override-path `IndexLoadingConfig` constructor invokes
`TimestampIndexUtils.applyTimestampIndex` which
+ /// mutates the schema under a lock keyed to the *new* (per-call)
TableConfig instance. The non-override path
+ /// constructs `IndexLoadingConfig` once at table-data-manager-load time
(not per-segment-build), so the lock
+ /// is not racing per call there. The clone is therefore needed only on the
override path. TODO: fix
+ /// TimestampIndexUtils to not mutate its inputs so this clone can be
removed.
+ ///
+ /// @param tableConfig source table config; not mutated
+ /// @param schema schema in scope; deep-copied before being passed to
the override-path IndexLoadingConfig
+ /// @param indexLoadingConfig fallback / no-override path; on the override
path only `instanceDataManagerConfig`
+ /// and `segmentTier` are read (see contract above)
+ /// @param logger logger to emit error on fallback
+ /// @param onFallback optional callback invoked when the override merge
fails and the helper falls back to the
+ /// persisted shape; intended for the caller to bump a
metric (e.g.
+ /// [ServerMeter#CONSUMING_OVERRIDE_FALLBACK]) tagged
with its own table-name context. May
+ /// be {@code null} to skip metric emission (e.g. in unit
tests).
+ /// @return a Builder ready for the caller to chain `.set...` calls and
`.build()`
+ public static RealtimeSegmentConfig.Builder
buildConsumingSegmentConfigBuilder(TableConfig tableConfig,
+ Schema schema, IndexLoadingConfig indexLoadingConfig, Logger logger,
@Nullable Runnable onFallback) {
+ if (hasConsumingOverride(tableConfig)) {
+ try {
+ TableConfig consumingTableConfig =
applyConsumingOverrides(tableConfig);
+ Schema schemaCopy;
+ try {
+ schemaCopy = JsonUtils.jsonNodeToObject(schema.toJsonObject(),
Schema.class);
+ } catch (IOException ioe) {
+ throw new IllegalStateException("Failed to clone schema for
consumingOverride path on table: "
+ + tableConfig.getTableName(), ioe);
+ }
+ IndexLoadingConfig consumingIlc = new
IndexLoadingConfig(indexLoadingConfig.getInstanceDataManagerConfig(),
+ consumingTableConfig, schemaCopy);
+ /// `segmentVersion` and `readMode` are already derived by the
constructor from the same
+ /// `instanceDataManagerConfig` +
`consumingTableConfig.indexingConfig` inputs the source ILC used, so they
+ /// don't need to be copied across. Tier overlay, however, is set by
the caller post-construction (it is
+ /// not in either input), so we re-apply it explicitly.
+ String segmentTier = indexLoadingConfig.getSegmentTier();
+ if (segmentTier != null) {
+ consumingIlc.setSegmentTier(segmentTier);
+ }
+ return new RealtimeSegmentConfig.Builder(consumingIlc);
+ } catch (RuntimeException e) {
+ /// Include the override snippet so the consuming-segment build log
line is self-sufficient for triage —
+ /// operators reading the log do not need to also fetch the table
config from ZK to see which override
+ /// failed. The metric (CONSUMING_OVERRIDE_FALLBACK) is the alerting
hook; this log is the diagnostic.
+ logger.error("Failed to apply consumingOverride for table: {}
(override snippet: {}); falling back to "
+ + "persisted shape — consuming segment will run with the
un-overridden shape for its full lifetime",
+ tableConfig.getTableName(),
summarizeConsumingOverrides(tableConfig), e);
+ if (onFallback != null) {
+ onFallback.run();
+ }
+ }
+ }
+ return new RealtimeSegmentConfig.Builder(indexLoadingConfig);
+ }
+
+ /// Returns a `column → override` short summary for the failure log.
+ private static String summarizeConsumingOverrides(TableConfig tableConfig) {
+ List<FieldConfig> fieldConfigs = tableConfig.getFieldConfigList();
+ if (fieldConfigs == null) {
+ return "{}";
+ }
+ StringBuilder sb = new StringBuilder("{");
+ boolean first = true;
+ for (FieldConfig fieldConfig : fieldConfigs) {
+ JsonNode override = fieldConfig.getConsumingOverride();
+ if (override == null || !override.isObject() || override.isEmpty()) {
+ continue;
+ }
+ if (!first) {
+ sb.append(", ");
+ }
+ sb.append(fieldConfig.getName()).append('=').append(override);
+ first = false;
+ }
+ sb.append('}');
+ return sb.toString();
+ }
+
+ public static boolean hasConsumingOverride(TableConfig tableConfig) {
+ List<FieldConfig> fieldConfigList = tableConfig.getFieldConfigList();
+ if (fieldConfigList == null) {
+ return false;
+ }
+ for (FieldConfig fieldConfig : fieldConfigList) {
+ JsonNode overrideJson = fieldConfig.getConsumingOverride();
+ if (overrideJson != null && overrideJson.isObject() &&
!overrideJson.isEmpty()) {
Review Comment:
Non-object `consumingOverride` values are silently treated as "no override"
here. For example, `consumingOverride: []` or `consumingOverride: "foo"` passes
validation, then the realtime path takes the default builder with no error,
log, or metric. Since this is a user-facing config and the rest of the patch is
trying to fail fast on bad override shapes, please reject any non-null
`consumingOverride` that is not an object instead of letting
`hasConsumingOverride()` filter it out.
--
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]