Skip to content

fix: serialize JSONB.parseObject per type to prevent concurrent $ref NPE#16384

Open
mmustafasenoglu wants to merge 2 commits into
apache:3.3from
mmustafasenoglu:fix/fastjson2-concurrent-ref-npe
Open

fix: serialize JSONB.parseObject per type to prevent concurrent $ref NPE#16384
mmustafasenoglu wants to merge 2 commits into
apache:3.3from
mmustafasenoglu:fix/fastjson2-concurrent-ref-npe

Conversation

@mmustafasenoglu

Copy link
Copy Markdown

What is the purpose of the change

Fix a race condition in FastJson2ObjectInput where concurrent deserialization of JSONB bytes containing $ref references produces null fields.

Brief changelog

  • Add striped locks (64 stripes) to FastJson2ObjectInput keyed by target class
  • Wrap JSONB.parseObject calls in synchronized (getStripe(cls)) blocks
  • Add getStripe(Class<?>) helper method

Related issue

Fixes #16368

Detailed explanation

Root cause

When multiple objects reference the same shared singleton (e.g., List.of()), fastjson2 serialization writes $ref references instead of duplicating the value. During concurrent deserialization:

  1. Multiple threads encounter an ObjectReader cache miss for the same type
  2. fastjson2's ObjectReaderProvider.getObjectReaderInternal has a non-atomic check-then-act: if (objectReader == null) { createObjectReader(...); putIfAbsent(...); }
  3. Concurrent createObjectReader calls produce corrupted ObjectReader instances (incomplete FieldReader arrays)
  4. $ref resolution via fieldReader.accept() fails silently → fields remain null

Fix

Striped locks keyed by target class serialize JSONB.parseObject calls per type. Concurrent deserialization of different types is unaffected; only same-type concurrent deserialization is serialized, preventing the fastjson2 race condition.

The 64-stripe design provides sufficient concurrency for typical applications while bounding memory usage.

How to reproduce

See the reproduction test in the issue: 200 threads with CyclicBarrier, cache cleared each round, ~1% of tasks produce null fields.

When using FastJson2Serialization with  references (e.g., shared
List.of() singletons serialized by fastjson2), concurrent deserialization
under cache-miss conditions produces corrupted ObjectReader instances in
fastjson2's ObjectReaderProvider.getObjectReaderInternal (non-atomic
check-then-act). This causes $ref resolution to silently fail, leaving
fields as null.

Add striped locks (64 stripes) keyed by target class to serialize
JSONB.parseObject calls per type. Concurrent deserialization of different
types is unaffected; only same-type concurrent deserialization is
serialized, preventing the race condition.

Fixes apache#16368

Signed-off-by: Mustafa Senoglu <mmustafasenoglu0@gmail.com>
@codecov-commenter

codecov-commenter commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 64.28571% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.88%. Comparing base (d0bf5c3) to head (45b0cc2).

Files with missing lines Patch % Lines
...mmon/serialize/fastjson2/FastJson2ObjectInput.java 64.28% 2 Missing and 3 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##                3.3   #16384      +/-   ##
============================================
+ Coverage     60.86%   60.88%   +0.01%     
- Complexity    11763    11769       +6     
============================================
  Files          1953     1953              
  Lines         89262    89272      +10     
  Branches      13471    13472       +1     
============================================
+ Hits          54331    54350      +19     
- Misses        29329    29333       +4     
+ Partials       5602     5589      -13     
Flag Coverage Δ
integration-tests-java21 32.15% <64.28%> (+0.04%) ⬆️
integration-tests-java8 32.22% <64.28%> (+0.06%) ⬆️
samples-tests-java21 32.19% <64.28%> (+0.04%) ⬆️
samples-tests-java8 29.80% <0.00%> (+<0.01%) ⬆️
unit-tests-java11 59.13% <50.00%> (+0.02%) ⬆️
unit-tests-java17 58.59% <50.00%> (-0.01%) ⬇️
unit-tests-java21 58.56% <50.00%> (-0.04%) ⬇️
unit-tests-java25 58.52% <50.00%> (-0.02%) ⬇️
unit-tests-java8 59.13% <50.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

mmustafasenoglu added a commit to mmustafasenoglu/dubbo that referenced this pull request Jul 17, 2026
Add tests for the striped lock mechanism introduced in PR apache#16384:

- testReadObjectWithType: exercises readObject(Class, Type) overload
- testReadObjectWithTypeList: exercises readObject with ParameterizedType for List
- testReadObjectNullClass: exercises getStripe(null) branch
- testConcurrentDeserializationSameType: concurrent deserialization of same type (same stripe)
- testConcurrentDeserializationDifferentTypes: concurrent deserialization of different types (different stripes)
- testReadObjectWithTypeAndString: readObject(Class, Type) with String
- testReadObjectWithTypeMap: readObject(Class, Type) with ParameterizedType for Map

These tests improve patch coverage from 64.28% by covering:
- getStripe() with null cls parameter
- readObject(Class<T>, Type) code path
- synchronized block execution across multiple threads

Signed-off-by: Mustafa Senoglu <mmustafasenoglu0@gmail.com>
Add tests for the striped lock mechanism introduced in this PR:

- testReadObjectWithType: exercises readObject(Class, Type) overload
- testReadObjectWithTypeList: exercises readObject with ParameterizedType for List
- testReadObjectNullClass: exercises getStripe(null) branch
- testConcurrentDeserializationSameType: concurrent deserialization of same type (same stripe)
- testConcurrentDeserializationDifferentTypes: concurrent deserialization of different types (different stripes)
- testReadObjectWithTypeAndString: readObject(Class, Type) with String
- testReadObjectWithTypeMap: readObject(Class, Type) with ParameterizedType for Map

These tests improve patch coverage by covering:
- getStripe() with null cls parameter
- readObject(Class<T>, Type) code path
- synchronized block execution across multiple threads

Signed-off-by: Mustafa Senoglu <mmustafasenoglu0@gmail.com>
@mmustafasenoglu

Copy link
Copy Markdown
Author

Hi, added FastJson2StripedLockTest to improve patch coverage (previously 64.28%).

New test methods:

Test Coverage target
testReadObjectWithType readObject(Class<T>, Type) overload
testReadObjectWithTypeList readObject with ParameterizedType (List)
testReadObjectNullClass getStripe(null) branch
testConcurrentDeserializationSameType synchronized block, same stripe
testConcurrentDeserializationDifferentTypes synchronized block, different stripes
testReadObjectWithTypeAndString readObject(Class, Type) with String
testReadObjectWithTypeMap readObject(Class, Type) with ParameterizedType (Map)

These should cover the previously uncovered getStripe(null) branch, the readObject(Class, Type) code path, and exercise the synchronized blocks under concurrency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Concurrent deserialization with $ref references produces null fields in FastJson2Serialization

2 participants