Publish MethodStructure native/code state safely (volatile) - #549
Publish MethodStructure native/code state safely (volatile)#549lagergren wants to merge 2 commits into
Conversation
|
I'm curious about this: We shouldn't have multiple threads whacking away on a MethodStructure. So it was never intended to be thread-safe. I'm going to suggest Gene review this so because he has more context for this area; the problem may be "upstream" from this. |
|
Even if we pretend it is never going to happen, it has very much proven to me working on these things that you should write code that is thread safe anyway because you never know under which forms it will run or how it will be appropriated. This is ZERO COST and will case ZERO OVERHEAD, and I strongly encourage it since there have been several real issues with mutable fields where actually multiple threads CAN run them and I add reproducers as such . Eitherway - you do recognize it is an "both fields at once" write / read - so it is just good form to code it like that, and my intellij even recognizes this and suggests it, even though it is not a specific problem that shows up in the problem view. I have bumped into this, and I am not sure how, or if my Draft of the Tool API implementation POC does something unsafe, but I would always code these as volatiles. If it runs only in one thread it is free, if it runs in several it is zero cost given what we do and how often we do it and directly safe. Let's just merge it to avoid another future headache? |
|
Fair challenge, and you're right about two things — but I think the factual premise doesn't hold on current master, so let me put the evidence up rather than argue from principle. "We shouldn't have multiple threads whacking away on a MethodStructure." Agreed as intent. But the native flag is already written on a lazy, runtime-reachable path, not at link time. Writer — if (fRebase && fHasNoCode && !fNative) {
fNative = true;
method.markNative(); // writes m_fNative on a SHARED MethodStructure
pool.invalidateTypeInfos(id.getNamespace());
}That sits inside Reader — MethodStructure method = body.getMethodStructure();
if (method != null && method.isNative()) { // reads m_fNativeAlso lazy, also on the dispatch path via Both sides run on the XVM service pool — So the failure needs no deliberate parallelism, just two services: service A first-touches type T, enters the TypeInfo build, and flips Where I think you're right, and it's the more important point: "the problem may be upstream." It is. The real defect is that TypeInfo construction mutates shared, interned I'd propose taking the volatile now as a stopgap (it converts an undiagnosable intermittent crash into correct behaviour) with the upstream question tracked separately, rather than blocking on the larger fix. Happy to go the other way if Gene would rather fix it at the source in one go. One correction to my own earlier framing: I called this zero-overhead, and that is not strictly accurate. A volatile read is a plain load on x86, but on ARM it lowers to |
|
Following up with the case for taking the 1. The cost is measurable, and I overstated it in my own correction above. I implied
So the real figure is one acquire-load per invocation on a path that allocates. Not literally zero, but far below measurement noise. 2. The "never intended to be concurrent" prior has a poor record in this exact subsystem. Three fields in the same metadata layer were designed single-threaded and turned out not to be: the implicit-identity cache written from concurrent service threads, delegation synthesis publishing half-built method code, and the TypeInfo placeholder identity race — the last of which describes concurrent TypeInfo builds on one pool as routine. I don't read that as carelessness; I read it as the metadata layer being reachable from more places than any one design intent anticipated. 3. Fixing the upstream writer doesn't retire the field's exposure. 4. The outcomes are asymmetric. If I'm wrong about the race, we paid an acquire-load per invocation. If the single-threaded assumption is wrong anywhere, the symptom is an intermittent 5. It is also a declaration of intent. These are lazily-initialised fields read by code that doesn't own them. 6. It is trivially reversible, and cheap to reverse in the right direction. If the upstream work genuinely establishes single-threaded ownership, deleting a keyword is a one-line, reviewable change with the reasoning recorded in the field comment. Adding it after a visibility bug ships costs a debugging cycle nobody can reproduce. And it competes with nothing: no lock, no API, no structure that the real fix would have to unwind — it is the smallest possible placeholder. To be explicit about where I've landed: I'm not arguing this is the right fix. You're correct that the design flaw is TypeInfo construction mutating shared interned |
|
So to summarize - the ConstantPool is deeply unsafe for several reasons, especially if you run something while compiling, but not only. This will get my stability way up when I test other things and it is general a good thing to do. There will be negligible overhead for reading and writing this pair as volatiles given the frequency It is in zero hot paths |
|
That's a lot of text to get through 😳 I think the biggest problem here is more fundamental: These structures should not be being read by one thread when another thread is modifying them. That's the project I'm working on related to the "bundle" effort, aka cpurdy/immut_xstructs, which disallows changes unless you've explicitly said "I need a mutable copy of this file structure" which either loads it from disk as mutable or takes a copy of a read-only one. |
|
Correction to my previous comment: this PR is unchanged — still the minimal two-word The design worth considering, on branch Two volatiles do not fully deliver what this PR's own field comment claims. The comment says the flag and the public void markNative() {
setAbstract(false);
resetRuntimeInfo(); // m_code = null; m_fNative = false; <-- intermediate
m_fNative = true; // <-- settled
m_fTransient = true;
}Between those statements the object is observably The branch holds both values in one immutable record behind one volatile reference: private record CodeState(boolean isNative, Code code) { … }
private transient volatile CodeState m_codeState = CodeState.NONE;Readers get an atomic snapshot; So there are three options, and it is your call:
One unrelated thing the branch surfaced, in if (getName().equals("compare") && getIdentityConstant().getNamespace().getName().equals("Const")) {
int q= 0;
}An empty |
e3fd405 to
820039b
Compare
|
Removed the debug residue from if (getName().equals("compare") && getIdentityConstant().getNamespace().getName().equals("Const")) {
int q= 0;
}An empty Worth naming what it actually is, though, because the shape recurs: that is trace-level logging, implemented by hand. Someone wanted "tell me when This is exactly the gap a pervasive logging framework fills with zero overhead on disabled log levels. Happy to help. |
m_fNative and m_code are written by one thread and read by others with no happens-before edge between them, and they are read TOGETHER: getOps() -> ensureCode() tests isNative() before touching m_code. A racing reader can therefore pair a stale native=false with a null m_code and take the wrong branch - which getOps() turns into 'has no code' IllegalStateException - or observe a partially constructed Code through the plain reference. Both fields are now volatile. That is the whole change: two modifiers, behaviour- neutral, no locking added and no hot path altered. MethodStructureVisibilityTest pins the contract by reflection so the edge cannot be silently removed later. It is deliberately a REGRESSION PIN, not a race reproduction: the defect is provable from the JMM but not deterministically reproducible, because markNative() runs at link time and the window does not reliably overlap. Verified the pin fails if either modifier is dropped. Gated: xdk:installDist then :javatools:test :javatools_utils:test, sequentially.
An empty if-block testing for Const.compare with an unused local, left over
from a breakpoint hook:
if (getName().equals("compare") && ...getName().equals("Const")) {
int q= 0;
}
It sits in the method this PR is about, so it goes with it.
a00c7a6 to
164f21c
Compare
Makes
MethodStructure.m_fNativeandm_codevolatile. Two modifiers, plus a test that pins them.Why this is needed
These two fields are written by one thread and read by others, with no happens-before edge between them — and, crucially, they are read together:
Meanwhile
markNative()performs a multi-step transition over that same state:With both fields non-volatile there is nothing ordering those writes against those reads, so a racing reader can:
native == false(not yet visible) together with am_codethat is stillnull, take the non-native branch, and fail.getOps()turns that into a hardIllegalStateException("... has no code").Code— the plain writem_code = new Code(this)can become visible before the object's own initialisation does.Code. Benign today, but it means "oneCodeper method" is not actually guaranteed.Why it matters now rather than in principle
It is not reachable in a strictly single-threaded link-then-run sequence, which is why it has not bitten in the CLI. It becomes reachable the moment a second thread touches a
MethodStructurewhile another marks it native or forces its code — concurrent compilation, a warm/resident runtime serving repeated work, an embedding host, or a JIT/interpreter mix. Those are all directions the project is actively moving in.The failure mode is the expensive kind: intermittent, timing-dependent, and non-reproducible — an
IllegalStateExceptionfromgetOps()in a run that worked yesterday, with a stack that points at the reader and says nothing about the writer. Cheap to prevent, extremely tedious to diagnose.Why
volatileand nothing moreThis is deliberately the minimal fix:
ensureCode(). That is a real change to a frequently-called method, so it is deliberately left out of this PR; the duplicate objects are each complete and correct, so the remaining exposure is waste rather than corruption.The test
MethodStructureVisibilityTestasserts by reflection that both fields arevolatile.It is a regression pin, not a race reproduction, and the PR should be read that way. The defect is provable from the Java Memory Model but not deterministically reproducible:
markNative()runs at link time, so the racing window does not reliably overlap without instrumentation. What the test does guarantee is that the edge cannot be silently removed later by someone dropping a modifier — which is exactly how this kind of fix usually regresses. Verified that it fails if eithervolatileis removed.Related
Same family as #548 (a diagnostic accessor NPE'ing on an ambient thread-local) and #547 (an unsynchronized scan cache): shared mutable state whose ownership and publication were left implicit. All three were surfaced by driving the compiler and runtime from ordinary Java threads rather than from the CLI's single-threaded path.