apache / apache/fory

[Java] Multiple Fory instances cannot be constructed on Java 8 (Non-ASCII characters in meta string)

Closed
#4,017 1 comment 0 reactions 0 assignees View on GitHub
bug
Dominant language
Java
Stars
4.5k
Forks
443
Avg merge
5h 59m
Merged PRs (30d)
77

Description

### Search before asking

- [x] I had searched in the [issues](https://github.com/apache/fory/issues) and found no similar issues.

### Version

```
Fory 1.6.1 and 1.7.0 (latest). Fails on Java 8, passes on Java 21.
fory-core declares maven.compiler.source/target = 8, so Java 8 is a supported target.

Failing JDK : 1.8.0_491-b10, HotSpot 64-Bit Server VM 25.491-b10
Passing JDK : Temurin 21.0.11+10-LTS
OS : macOS 15 (Darwin 24.6.0), arm64
```

### Component(s)

Java

### Minimal reproduce step

[fory-multi-instance-repro.zip](https://github.com/user-attachments/files/31758009/fory-multi-instance-repro.zip)

On Java 8, constructing a **second** `Fory` instance in the same JVM fails, once an earlier
instance has serialized an object graph large enough to trigger JIT codegen. The failure comes
from `ClassResolver.initialize()` while registering Fory's **own built-in types** — the string
it fails to encode is the package or type name of a Fory internal class, which is ASCII by
construction.

A single `Fory` instance never fails, at any payload size (checked to ~3.3 MB / 150k objects),
so this is not a serialization-size problem. Once it throws, every later `Fory` construction in
that JVM fails the same way.

The practical impact is on any application holding more than one `Fory` — two library modules
that each keep a `static` instance, or one instance per classloader in a container. The second
can become permanently un-constructible depending on what the first happened to serialize.

The attached `fory-multi-instance-repro.zip` is a single self-contained file, no dependencies
beyond `fory-core`:

unzip fory-multi-instance-repro.zip && cd fory-multi-instance-repro
curl -sO https://repo1.maven.org/maven2/org/apache/fory/fory-core/1.7.0/fory-core-1.7.0.jar
javac -cp fory-core-1.7.0.jar ForyMultiInstanceRepro.java
java -cp fory-core-1.7.0.jar:. ForyMultiInstanceRepro

Optional args: `[items] [instances] [builderConfig]`, defaults `800 4 copy+num`.

Some configurations are probabilistic, so run it a few times to see a rate:

for i in $(seq 1 10); do java -cp fory-core-1.7.0.jar:. ForyMultiInstanceRepro; done

### What did you expect to see?

OK items=800 instances=4 bytes=194341

### What did you see instead?

```
10 of 10 runs fail on Java 8; 0 of 10 on Java 21.

FAIL at instance 2 of 4 (items=800, bytes=194341): java.lang.IllegalArgumentException: Non-ASCII characters in meta string are not allowed

java.lang.IllegalArgumentException: Non-ASCII characters in meta string are not allowed
at org.apache.fory.meta.MetaStringEncoder.encodeBinary(MetaStringEncoder.java:92)
at org.apache.fory.resolver.SharedRegistry.getEncodedMetaString(SharedRegistry.java:337)
at org.apache.fory.resolver.SharedRegistry.getPackageEncodedMetaString(SharedRegistry.java:309)
at org.apache.fory.resolver.TypeInfo.(TypeInfo.java:106)
at org.apache.fory.resolver.ClassResolver.registerInternalImpl(ClassResolver.java:728)
at org.apache.fory.resolver.ClassResolver.registerInternal(ClassResolver.java:716)
at org.apache.fory.resolver.ClassResolver.initialize(ClassResolver.java:273)
at org.apache.fory.Fory.(Fory.java:155)
at org.apache.fory.config.ForyBuilder.newFory(ForyBuilder.java:789)
at org.apache.fory.ThreadLocalFory.newFory(ThreadLocalFory.java:67)

Variations across runs of the same command, all with the above root cause:

- it also fails via getTypeNameEncodedMetaString (SharedRegistry.java:317), so either meta
string can be the one that fails;
- on 1.7.0 it is sometimes surfaced wrapped, as
org.apache.fory.exception.SerializationException: java.lang.RuntimeException: Create sequential serializer failed
(7/10 raw, 3/10 wrapped in one sample of 10);
- the failing instance index is usually 2, occasionally 3.

Results, 10 runs per cell:

fory items instances Java 8 Java 21
1.6.1 800 4 10/10 fail 0/10
1.7.0 800 4 10/10 fail 0/10
1.6.1 800 1 0/10 0/10
```

### Anything Else?

Conditions, each verified by removing it:

1. Java 8. Not reproducible on Java 21, including at 8 instances and 4x the payload.
2. More than one Fory instance constructed in the JVM.
3. An earlier serialization large enough to trigger JIT codegen. Built-in types alone never
trigger it. A flat HashMap of 6000 entries does NOT reproduce; several
distinct generic shapes (Map, Map, Set) do. It is not a simple
size threshold — the same shape reproduces at 2000 entries per field but not at 10000.
4. withRefTracking(true).

The failure rate tracks how many builder methods are called. With withLanguage +
requireClassRegistration(false) + withRefTracking(true) alone it reproduces intermittently;
adding further builder calls raises the rate (8 runs each, Java 8; reproduce each row with the
third arg, e.g. `... ForyMultiInstanceRepro 800 4 min`):

builder config arg fail rate
minimal (above) min 2/8
+ withRefCopy(true) copy 2/8
+ serializeEnumByName(true) enum 6/8
+ withNumberCompressed(false) num 7/8
+ withRefCopy(true) + withNumberCompressed(false) copy+num 8/8

serializeEnumByName(true) raises the rate although the payload contains no enums, and
withRefCopy(true) affects only copy(), which is never called here. That suggests the trigger is
sensitive to the number of action-recording builder methods replayed in ForyBuilder.factory(),
rather than to any single option's semantics.

withCodegen(false) and withAsyncCompilation(true) each reduce the rate without eliminating it,
so neither is a workaround.

Ruled out:

- A shared SharedRegistry instance. Fory.(builder, classLoader, sharedRegistry) does
new SharedRegistry() when the argument is null, so separate instances do not share one.
- The isLatin/ASCII range gap. StringEncodingUtils.isLatin(char[]) accepts chars <= 255 while
the LOWER_SPECIAL encoders reject > 127, but encodeBinary(String, Encoding[]) correctly
falls back to UTF-8 — verified with "café", "aÿ", "aĀ", "a中" on both JDKs.
The gap applies only to the forced-encoding overload, which is what this path uses.
- The multi-release jar. PlatformStringUtils and MemoryBuffer are overridden only under
META-INF/versions/25/, so Java 8 and Java 21 both load the base classes.

Possibly related:

- #3828 — deserialization depending on Fory instance state (different symptom, same theme of
cross-instance coupling).
- #3546 — introduced caching of serializers and TypeInfo in SharedRegistry.
- #2070 — a prior Java 8 vs 8+ divergence in the same internal registration path.

Suggested hardening, independent of root cause:
MetaStringEncoder.encodeBinary(String, Encoding) throws when the forced encoding cannot
represent the string, while the Encoding[] overload falls back to UTF-8. Making the
forced-encoding path fall back the same way would downgrade this from an error that permanently
prevents Fory construction in the JVM to correct (if slightly larger) output.

### Are you willing to submit a PR?

- [ ] I'm willing to submit a PR!

Contributor guide

Open the contributing guide

Research direction

Run the attached ForyMultiInstanceRepro on Java 8 and Java 21, then inspect MetaStringEncoder.encodeBinary and the SharedRegistry calls at lines 309, 317, and 337. Trace how ClassResolver.initialize registers built-in types across multiple Fory instances; done means repeated construction succeeds on Java 8 without the Non-ASCII meta-string failure while preserving the existing encoding behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.