xiangfu0 commented on code in PR #19223: URL: https://github.com/apache/pinot/pull/19223#discussion_r3771082053
########## pinot-common/src/main/java/org/apache/pinot/common/function/ForyJsonPathExtractor.java: ########## @@ -0,0 +1,439 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.pinot.common.function; + +import com.fasterxml.jackson.core.StreamReadConstraints; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.annotation.Nullable; +import org.apache.fory.json.ForyJson; +import org.apache.fory.json.codec.JsonValueCodec; +import org.apache.fory.json.reader.JsonReader; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Utf16JsonReader; +import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/// Shared Fory JSON parser for opt-in JSON-path implementations. +/// +/// Streaming extraction uses one single-state parser per worker thread to avoid shared-pool contention. Initialization +/// or runtime linkage failures permanently disable the optional path, allowing callers to fall back to +/// Jackson/Jayway. Jackson's default scalar token limits are enforced while walking the document, and inputs outside +/// Fory's safe nesting depth fall back to the reference parser. +public final class ForyJsonPathExtractor { + private static final Logger LOGGER = LoggerFactory.getLogger(ForyJsonPathExtractor.class); + private static final StreamReadConstraints JACKSON_CONSTRAINTS = StreamReadConstraints.defaults(); + private static final ThreadLocal<PathContext> PATH_CONTEXT = ThreadLocal.withInitial(PathContext::new); + private static final AtomicBoolean UNAVAILABLE_WARNING_LOGGED = new AtomicBoolean(); + + private ForyJsonPathExtractor() { + } + + private static final class Holder { + private static volatile boolean _streamingAvailable; + private static final ThreadLocal<ForyJson> STREAMING_PARSER = ThreadLocal.withInitial(() -> { + if (!_streamingAvailable) { + throw new IllegalStateException("Fory JSON is unavailable"); + } + ForyJson parser = buildStreamingParser(); + if (parser == null) { + _streamingAvailable = false; + throw new IllegalStateException("Fory JSON is unavailable"); + } + return parser; + }); + + static { + ForyJson parser = buildStreamingParser(); + _streamingAvailable = parser != null; + if (parser != null) { + STREAMING_PARSER.set(parser); + } + } + + private Holder() { + } + + @Nullable + private static ForyJson buildStreamingParser() { + try { + return ForyJson.builder().withCodegen(false).withAsyncCompilation(false).withConcurrencyLevel(1) + .registerCodec(PathResult.class, PathCodec.INSTANCE).build(); + } catch (RuntimeException | LinkageError e) { + logUnavailable(e); + return null; + } + } + } + + /// Returns whether the optional Fory runtime initialized successfully. + public static boolean isAvailable() { + return Holder._streamingAvailable; + } + + /// Extracts a simple path with Fory's streaming reader without materializing the complete JSON tree. + /// + /// Unrelated values are still fully consumed so malformed input and duplicate-key last-wins behavior match the + /// reference parser. Jackson's nesting, field-name, string, and number limits are checked while scanning. Callers + /// should retry with the reference parser when this method throws. + @Nullable + public static Object extract(String json, SimpleJsonPath path) { + if ((JACKSON_CONSTRAINTS.hasMaxDocumentLength() + && json.length() > JACKSON_CONSTRAINTS.getMaxDocumentLength()) + || (JACKSON_CONSTRAINTS.hasMaxTokenCount() && requiresJacksonFallback(json))) { + throw new IllegalArgumentException("JSON document requires Jackson constraint validation"); + } Review Comment: Fixed in f0f1036c99. The dead lexical prescan is removed. Document length is checked up front; nesting, field-name, string, and number limits are enforced during the streaming walk; configured maxTokenCount is counted in that same walk. The focused configured-token-limit child-JVM test passes. ########## pinot-common/src/main/java/org/apache/pinot/common/function/scalar/JsonFunctions.java: ########## @@ -129,6 +130,25 @@ private static Object fastJsonPath(Object object, String jsonPath, boolean useBi return jsonPath(object, jsonPath); } + /// Resolves a simple path over a JSON string with Fory's streaming reader. Complex paths, already-parsed inputs, + /// container results, and Fory extraction failures use the existing Jayway implementation so the experimental + /// functions below retain the current behavior outside Fory's supported envelope. + @Nullable + private static Object foryJsonPath(Object object, String jsonPath) { + SimpleJsonPath simpleJsonPath = SimpleJsonPath.compile(jsonPath); + if (!(object instanceof String) || simpleJsonPath == null) { + return jsonPath(object, jsonPath); + } Review Comment: Fixed in f0f1036c99. foryJsonPath now checks object instanceof String before compiling SimpleJsonPath, so Map/List and other non-String inputs go directly to the existing Jayway path without the extra compile. ########## pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkJsonExtractScalarQuery.java: ########## @@ -0,0 +1,218 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.pinot.perf; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.apache.pinot.common.request.context.ExpressionContext; +import org.apache.pinot.common.request.context.LiteralContext; +import org.apache.pinot.core.common.BlockValSet; +import org.apache.pinot.core.operator.ColumnContext; +import org.apache.pinot.core.operator.blocks.ValueBlock; +import org.apache.pinot.core.operator.transform.TransformResultMetadata; +import org.apache.pinot.core.operator.transform.function.BaseTransformFunction; +import org.apache.pinot.core.operator.transform.function.JsonExtractScalarTransformFunction; +import org.apache.pinot.core.operator.transform.function.LiteralTransformFunction; +import org.apache.pinot.core.operator.transform.function.TransformFunction; +import org.apache.pinot.spi.data.FieldSpec.DataType; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OperationsPerInvocation; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + + +/// Measures the actual query-side `jsonExtractScalar*` ValueBlock loop. Each invocation processes a 128-row block; +/// JMH normalizes throughput and allocation to one row via [OperationsPerInvocation]. Input projection is represented +/// by a pre-materialized String array so the comparison isolates JSON extraction, type coercion, and result writing. +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(1) +@Warmup(iterations = 3, time = 2) +@Measurement(iterations = 5, time = 3) +@State(Scope.Thread) +public class BenchmarkJsonExtractScalarQuery { + private static final int BLOCK_ROWS = 128; + private static final TransformResultMetadata STRING_METADATA = + new TransformResultMetadata(DataType.STRING, true, false); + private static final String BASE_JSON = "{" + + "\"earlyMetric\":17," + + "\"user\":{\"id\":\"u-19283\",\"country\":\"US\",\"tier\":\"gold\",\"age\":41}," + + "\"event\":{\"name\":\"checkout\",\"cart\":[{\"sku\":\"A1\",\"qty\":2,\"price\":19.99}," + + "{\"sku\":\"B7\",\"qty\":1,\"price\":129.0},{\"sku\":\"C3\",\"qty\":4,\"price\":3.5}]," + + "\"currency\":\"USD\",\"coupon\":null}," + + "\"device\":{\"os\":\"macOS\",\"version\":\"14.5\",\"browser\":\"Chrome\"," + + "\"screen\":{\"width\":1728,\"height\":1117}}," + + "\"geo\":{\"city\":\"San Francisco\",\"region\":\"CA\",\"country\":\"US\"," + + "\"coordinates\":[-122.4194,37.7749]}," + + "\"attributes\":{\"campaign\":\"summer-sale\",\"referrer\":\"search\",\"experiment\":\"checkout-v2\"}," + + "\"flags\":[\"returning\",\"subscribed\",\"beta\"]," + + "\"lateMetric\":19} "; + + @Param({"early", "late"}) + private String _fieldPosition; + + @Param({"700", "8192", "65536"}) + private int _documentBytes; + + private ValueBlock _valueBlock; + private JsonExtractScalarTransformFunction _jayway; + private JsonExtractScalarTransformFunction _fast; + private JsonExtractScalarTransformFunction _firstMatch; + private JsonExtractScalarTransformFunction _fory; + + @Setup + public void setUp() { + String json = buildJson(_documentBytes); + String[] jsonRows = new String[BLOCK_ROWS]; + Arrays.fill(jsonRows, json); + TransformFunction input = new StringArrayTransformFunction(jsonRows); + String path = "early".equals(_fieldPosition) ? "$.earlyMetric" : "$.lateMetric"; + List<TransformFunction> arguments = List.of(input, literal(path), literal("LONG")); + + _valueBlock = new FixedValueBlock(BLOCK_ROWS); + _jayway = new JsonExtractScalarTransformFunction(); + _fast = new JsonExtractScalarTransformFunction.Fast(); + _firstMatch = new JsonExtractScalarTransformFunction.FirstMatch(); + _fory = new JsonExtractScalarTransformFunction.Fory(); + for (JsonExtractScalarTransformFunction function : List.of(_jayway, _fast, _firstMatch, _fory)) { + function.init(arguments, Map.<String, ColumnContext>of(), false); + long expected = "early".equals(_fieldPosition) ? 17L : 19L; + long[] values = function.transformToLongValuesSV(_valueBlock); + if (values.length < BLOCK_ROWS || values[0] != expected || values[BLOCK_ROWS - 1] != expected) { + throw new IllegalStateException(function.getName() + " produced an unexpected query result"); + } + } + } + + private static LiteralTransformFunction literal(String value) { + return new LiteralTransformFunction(new LiteralContext(DataType.STRING, value)); + } + + private static String buildJson(int targetBytes) { + if (BASE_JSON.length() >= targetBytes) { + return BASE_JSON; + } + String marker = "\"lateMetric\":"; + int markerOffset = BASE_JSON.indexOf(marker); + String paddingPrefix = "\"padding\":\""; + String paddingSuffix = "\","; + int paddingLength = targetBytes - BASE_JSON.length() - paddingPrefix.length() - paddingSuffix.length(); + return BASE_JSON.substring(0, markerOffset) + paddingPrefix + "x".repeat(paddingLength) + paddingSuffix + + BASE_JSON.substring(markerOffset); Review Comment: Fixed in f0f1036c99. buildJson now returns the base document when the prefix/suffix leave no positive padding length, avoiding a negative String.repeat argument. ########## pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunction.java: ########## @@ -72,6 +74,10 @@ /// keys to the first non-null occurrence and does not validate malformed content after the resolved value. Use it /// only for well-formed, duplicate-free JSON. `Fast` scans the full root value and retains Jayway's last-key-wins /// and malformed-document behavior; see [FastJsonPathExtractor] for one documented unaddressed-value edge case. +/// `jsonExtractScalarFory` is experimental and must be selected explicitly. It accelerates simple paths over +/// `STRING` input for scalar result types other than `STRING` and `BIG_DECIMAL`. `BYTES` input, complex paths, +/// containers / array result types, precision-sensitive results, deeply nested documents, and Fory failures use +/// Jayway. Its name, supported envelope, and implementation can change while the integration is evaluated. Review Comment: Resolved in f0f1036c99 with explicit initialization-time eligibility. Fory is eligible only for simple paths, STRING input, single-value output, and result types other than STRING/JSON/BIG_DECIMAL. The Javadoc now states this exact envelope, and the eligibility matrix test covers it. ########## pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunction.java: ########## @@ -802,7 +817,7 @@ private <T> IntFunction<T> getResultExtractor(ValueBlock valueBlock, ParseContex if (_jsonFieldTransformFunction.getResultMetadata().getDataType() == DataType.BYTES) { byte[][] jsonBytes = _jsonFieldTransformFunction.transformToBytesValuesSV(valueBlock); IntFunction<T> jaywayExtractor = i -> parseContext.parseUtf8(jsonBytes[i]).read(_jsonPath); - if (_simpleJsonPath == null) { + if (_simpleJsonPath == null || useBigDecimal || _extractionMode == ExtractionMode.FORY) { Review Comment: Fixed in f0f1036c99. The BYTES branch no longer gates Fast/FirstMatch on useBigDecimal; only Fory uses Jayway for BYTES. I added a non-vacuous regression using a precise addressed decimal plus a hostile unaddressed exponent that Jayway rejects but Fast/FirstMatch skip. The 246-case focused suite passes. ########## pinot-common/src/main/java/org/apache/pinot/common/function/ForyJsonPathExtractor.java: ########## @@ -0,0 +1,439 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.pinot.common.function; + +import com.fasterxml.jackson.core.StreamReadConstraints; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.annotation.Nullable; +import org.apache.fory.json.ForyJson; +import org.apache.fory.json.codec.JsonValueCodec; +import org.apache.fory.json.reader.JsonReader; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Utf16JsonReader; +import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/// Shared Fory JSON parser for opt-in JSON-path implementations. +/// +/// Streaming extraction uses one single-state parser per worker thread to avoid shared-pool contention. Initialization +/// or runtime linkage failures permanently disable the optional path, allowing callers to fall back to +/// Jackson/Jayway. Jackson's default scalar token limits are enforced while walking the document, and inputs outside +/// Fory's safe nesting depth fall back to the reference parser. +public final class ForyJsonPathExtractor { + private static final Logger LOGGER = LoggerFactory.getLogger(ForyJsonPathExtractor.class); + private static final StreamReadConstraints JACKSON_CONSTRAINTS = StreamReadConstraints.defaults(); + private static final ThreadLocal<PathContext> PATH_CONTEXT = ThreadLocal.withInitial(PathContext::new); + private static final AtomicBoolean UNAVAILABLE_WARNING_LOGGED = new AtomicBoolean(); + + private ForyJsonPathExtractor() { + } + + private static final class Holder { + private static volatile boolean _streamingAvailable; + private static final ThreadLocal<ForyJson> STREAMING_PARSER = ThreadLocal.withInitial(() -> { + if (!_streamingAvailable) { + throw new IllegalStateException("Fory JSON is unavailable"); + } + ForyJson parser = buildStreamingParser(); + if (parser == null) { + _streamingAvailable = false; + throw new IllegalStateException("Fory JSON is unavailable"); + } + return parser; + }); + + static { + ForyJson parser = buildStreamingParser(); + _streamingAvailable = parser != null; + if (parser != null) { + STREAMING_PARSER.set(parser); + } + } + + private Holder() { + } + + @Nullable + private static ForyJson buildStreamingParser() { + try { + return ForyJson.builder().withCodegen(false).withAsyncCompilation(false).withConcurrencyLevel(1) + .registerCodec(PathResult.class, PathCodec.INSTANCE).build(); + } catch (RuntimeException | LinkageError e) { + logUnavailable(e); + return null; + } + } + } + + /// Returns whether the optional Fory runtime initialized successfully. + public static boolean isAvailable() { + return Holder._streamingAvailable; + } + + /// Extracts a simple path with Fory's streaming reader without materializing the complete JSON tree. + /// + /// Unrelated values are still fully consumed so malformed input and duplicate-key last-wins behavior match the + /// reference parser. Jackson's nesting, field-name, string, and number limits are checked while scanning. Callers + /// should retry with the reference parser when this method throws. + @Nullable + public static Object extract(String json, SimpleJsonPath path) { + if ((JACKSON_CONSTRAINTS.hasMaxDocumentLength() + && json.length() > JACKSON_CONSTRAINTS.getMaxDocumentLength()) + || (JACKSON_CONSTRAINTS.hasMaxTokenCount() && requiresJacksonFallback(json))) { + throw new IllegalArgumentException("JSON document requires Jackson constraint validation"); + } + if (!Holder._streamingAvailable) { + throw new IllegalStateException("Fory JSON is unavailable"); + } + ForyJson parser = Holder.STREAMING_PARSER.get(); + PathContext context = PATH_CONTEXT.get(); + if (context._active) { + throw new IllegalStateException("Fory JSON path extraction is not reentrant"); + } + context._active = true; + context._path = path; + context._result = null; + try { + parser.fromJson(json, PathResult.class); + return context._result; + } catch (LinkageError e) { + disable(); + logUnavailable(e); + throw new IllegalStateException("Fory JSON became unavailable", e); + } finally { + context._path = null; + context._result = null; + context._active = false; + } + } + + private static void disable() { + Holder._streamingAvailable = false; + Holder.STREAMING_PARSER.remove(); + PATH_CONTEXT.remove(); + } + + private static void logUnavailable(Throwable cause) { + if (UNAVAILABLE_WARNING_LOGGED.compareAndSet(false, true)) { + LOGGER.warn("Experimental Fory JSON support is unavailable; falling back to Jackson/Jayway", cause); + } + } + + private static Object readPath(JsonReader reader, SimpleJsonPath path, int depth) { + String key = path.getKey(depth); + if (key != null) { + return readObjectPath(reader, path, depth, key); + } + return readArrayPath(reader, path, depth, path.getIndex(depth)); + } + + @Nullable + private static Object readObjectPath(JsonReader reader, SimpleJsonPath path, int depth, String expectedKey) { + if (reader.peekToken() != '{') { + skipValue(reader); + return null; + } + reader.enterDepth(); + try { + reader.expect('{'); + if (reader.consume('}')) { + return null; + } + Object result = null; + boolean more; + do { + String fieldName = reader.readFieldName(); + if (fieldName.length() > JACKSON_CONSTRAINTS.getMaxNameLength()) { + throw new IllegalArgumentException("JSON field name exceeds Jackson's configured limit"); + } + reader.expect(':'); + if (expectedKey.equals(fieldName)) { + result = depth + 1 == path.length() ? readScalar(reader) : readPath(reader, path, depth + 1); + } else { + skipValue(reader); + } + more = reader.consumeCommaOrEndObject(); + } while (more); + return result; + } finally { + reader.exitDepth(); + } + } + + @Nullable + private static Object readArrayPath(JsonReader reader, SimpleJsonPath path, int depth, int expectedIndex) { + if (reader.peekToken() != '[') { + skipValue(reader); + return null; + } + reader.enterDepth(); + try { + reader.expect('['); + if (reader.consume(']')) { + return null; + } + Object result = null; + int index = 0; + boolean more; + do { + if (index == expectedIndex) { + result = depth + 1 == path.length() ? readScalar(reader) : readPath(reader, path, depth + 1); + } else { + skipValue(reader); + } + index++; + more = reader.consumeCommaOrEndArray(); + } while (more); + return result; + } finally { + reader.exitDepth(); + } + } + + @Nullable + private static Object readScalar(JsonReader reader) { + char token = reader.peekToken(); + if (token == '"') { + String value = reader.readString(); + if (value.length() > JACKSON_CONSTRAINTS.getMaxStringLength()) { + throw new IllegalArgumentException("JSON string exceeds Jackson's configured limit"); + } + return value; + } + if (token == 't' || token == 'f') { + return reader.readBoolean(); + } + if (token == 'n') { + reader.readNull(); + return null; + } + if (token == '{' || token == '[') { + // Query scalar coercion has observable error/default behavior for containers. Let Jayway produce the exact + // reference value rather than materializing a Fory container on this uncommon path. + throw new IllegalArgumentException("Container result requires reference JSON parsing"); Review Comment: Fixed in f0f1036c99. Container leaves are fully consumed and return a private identity sentinel instead of throwing per row; callers then use Jayway for exact container/coercion behavior. Duplicate container/scalar order and malformed-tail tests cover the sentinel path. At T32, object/array Fory allocation dropped from about 2.9/2.8 KiB to 2.2/1.8 KiB per op. ########## pinot-common/src/main/java/org/apache/pinot/common/function/ForyJsonPathExtractor.java: ########## @@ -0,0 +1,439 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.pinot.common.function; + +import com.fasterxml.jackson.core.StreamReadConstraints; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.annotation.Nullable; +import org.apache.fory.json.ForyJson; +import org.apache.fory.json.codec.JsonValueCodec; +import org.apache.fory.json.reader.JsonReader; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Utf16JsonReader; +import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/// Shared Fory JSON parser for opt-in JSON-path implementations. +/// +/// Streaming extraction uses one single-state parser per worker thread to avoid shared-pool contention. Initialization +/// or runtime linkage failures permanently disable the optional path, allowing callers to fall back to +/// Jackson/Jayway. Jackson's default scalar token limits are enforced while walking the document, and inputs outside +/// Fory's safe nesting depth fall back to the reference parser. +public final class ForyJsonPathExtractor { + private static final Logger LOGGER = LoggerFactory.getLogger(ForyJsonPathExtractor.class); + private static final StreamReadConstraints JACKSON_CONSTRAINTS = StreamReadConstraints.defaults(); + private static final ThreadLocal<PathContext> PATH_CONTEXT = ThreadLocal.withInitial(PathContext::new); + private static final AtomicBoolean UNAVAILABLE_WARNING_LOGGED = new AtomicBoolean(); + + private ForyJsonPathExtractor() { + } + + private static final class Holder { + private static volatile boolean _streamingAvailable; + private static final ThreadLocal<ForyJson> STREAMING_PARSER = ThreadLocal.withInitial(() -> { Review Comment: Fixed in f0f1036c99. The per-caller ThreadLocal<ForyJson> is replaced by one process-wide, thread-safe Fory facade with Fory's fixed default state pool; only the small path/result context remains thread-local and is cleared after each call. T32 ValueBlock Fory remains 1.15-1.32x Fast at 700 B and 3.62-4.13x at 64 KiB. The pool is bounded, though steady-state JMH does not measure its retained heap. -- 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]
