Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8283660: Convert com/sun/jndi/ldap/AbstractLdapNamingEnumeration.java finalizer to Cleaner #8311

Open
wants to merge 29 commits into
base: master
Choose a base branch
from

Conversation

bchristi-git
Copy link
Member

@bchristi-git bchristi-git commented Apr 20, 2022

Please review this change to replace the finalizer in AbstractLdapNamingEnumeration with a Cleaner.

The pieces of state required for cleanup (LdapCtx homeCtx, LdapResult res, and LdapClient enumClnt) are moved to a static inner class . From there, the change is fairly mechanical.

Details of note:

  1. Some operations need to change the state values (the update() method is probably the most interesting).
  2. Subclasses need to access homeCtx; I added a homeCtx() method to read homeCtx from the superclass's state.

The test case is based on a copy of com/sun/jndi/ldap/blits/AddTests/AddNewEntry.java. A more minimal test case might be possible, but this was done for expediency.

The test only confirms that the new Cleaner use does not keep the object reachable. It only tests LdapSearchEnumeration (not LdapNamingEnumeration or LdapBindingEnumeration, though all are subclasses of AbstractLdapNamingEnumeration).

Thanks.


Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue

Issue

  • JDK-8283660: Convert com/sun/jndi/ldap/AbstractLdapNamingEnumeration.java finalizer to Cleaner

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk pull/8311/head:pull/8311
$ git checkout pull/8311

Update a local copy of the PR:
$ git checkout pull/8311
$ git pull https://git.openjdk.org/jdk pull/8311/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 8311

View PR using the GUI difftool:
$ git pr show -t 8311

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/8311.diff

@bridgekeeper
Copy link

@bridgekeeper bridgekeeper bot commented Apr 20, 2022

👋 Welcome back bchristi! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

@openjdk openjdk bot commented Apr 20, 2022

@bchristi-git The following label will be automatically applied to this pull request:

  • core-libs

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command.

@openjdk openjdk bot added the core-libs label Apr 20, 2022
@openjdk openjdk bot added the rfr label Apr 20, 2022
@mlbridge
Copy link

@mlbridge mlbridge bot commented Apr 20, 2022

/* This class maintains the pieces of state that need (or are needed for)
* cleanup, which happens by calling cleanup(), or is done by the Cleaner.
*/
private static class CleaningAction implements Runnable {
Copy link
Contributor

@RogerRiggs RogerRiggs Apr 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This nested class might be more aptly named EnumCtx since it is used during the enumeration process.

The mention of the cleanup() method in the nested class javadoc is a bit ambiguous, it seems there should be a cleanup() method in the class, but it is in AbstractLdapNamingEnumeration.

The state field might also be renamed enumCtx.

this.homeCtx = homeCtx;
homeCtx.incEnumCount();
enumClnt = homeCtx.clnt; // remember
this.state.homeCtx.incEnumCount();
Copy link
Contributor

@RogerRiggs RogerRiggs Apr 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readability might be improved by using the new homeCtx() method instead of state.homeCtx in this file.
It would then be consistent with how subclasses access it.
It depends which style you prefer to be more consistent with.

Copy link
Member

@dfuch dfuch left a comment

I also agree with Roger's suggestions.

@Override
public void run() {
if (enumClnt != null) {
enumClnt.clearSearchReply(res, homeCtx.reqCtls);
Copy link
Member

@dfuch dfuch Apr 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit strange to see that there is no guard here to verify that homeCtx != null, when line 76 implies that it might. My reading is that homeCtxt is not supposed to be null when enumClnt is not null. That could be explained in a comment, or alternatively asserted just before line 73 (assert homeCtx != null;)

Copy link
Member Author

@bchristi-git bchristi-git Apr 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is strange -- that code came from the finalizer. I will add an assert.

Copy link
Member Author

@bchristi-git bchristi-git May 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears that the update() method can set 'homeCtx' for 'ne' to null while leaving 'enumClnt' non-null (~L410).
Perhaps here, the clearSearchReply() call should only happen if both are non-null.

}
}

private CleaningAction state;
Copy link
Member

@dfuch dfuch Apr 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if state should be final?

Copy link
Member Author

@bchristi-git bchristi-git Apr 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me. cleanable can be final, too.

@bchristi-git
Copy link
Member Author

@bchristi-git bchristi-git commented May 27, 2022

AbstractLdapEnumeration's mutable state brings the possibility of threading issues between the program and cleaner threads. I've added some threading-related changes to the fix. See my comment in the bug report for additional background.

Since synchronization may now happen on the cleaner thread, I've changed AbstractLdapEnumeration to use its own Cleaner instance instead of the shared cleaner, for added safety. There have been deadlocks in ldap cleanup in the past.

The added finally blocks led to a lot of indentation changes. The "hide whitespace" option might help with viewing the changes.

try {
LdapResult newRes = homeCtx().getSearchReply(enumCtx.enumClnt, enumCtx.res);
enumCtx.setRes(newRes);
if (enumCtx.res == null) {
Copy link
Contributor

@RogerRiggs RogerRiggs Jun 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks odd, setting the value using synchronized, but reading it without.

Copy link
Member Author

@bchristi-git bchristi-git Jun 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added getters to EnumCtx, and a comment explaining why only setters are synchronized.

@mlbridge
Copy link

@mlbridge mlbridge bot commented Jun 2, 2022

Mailing list message from Hans Boehm on core-libs-dev:

On Wed, Jun 1, 2022 at 2:47 PM Roger Riggs <rriggs at openjdk.java.net> wrote:

...

214: } finally {
215: // Ensure Cleaner does not run until after this method
completes
216: Reference.reachabilityFence(this);

I don't think there is any benefit to the `try{} finally {fence}`.
The reachabilityFence has no executable code. Its only purpose is to keep
the reference in scope alive.

That's an interesting general question.

I agree that this is true with the right implementation assumptions, which
might conceivably be warranted here. But if there is a possibility that the
block throws after a point at which you need this to ensure reachability,
then I don't think this is spec-correct without the try-finally. Consider

tmp = a.field;
use(field); // Cleaner associated with a invalidates field
if (moon_phase() == FULL) throw(...);
Reference.reachabilityFence(a);

Consider the full moon case. The reachabilityFence spec says: "the
referenced object is not reclaimable by garbage collection at least until
after the invocation of this method." This method is not invoked, so there
is no guarantee, and hence this may fail.

And indeed, a compiler could conceivably rewrite this to

if (moon_phase() != FULL) {
tmp = a.field;
use(field); // Cleaner associated with a invalidates field
Reference.reachabilityFence(a);
} else {
tmp = a.field;
use(field); // Cleaner associated with a invalidates field <--
potential crash
throw(...);
}

in which case this might, on very rare occasions, actually fail in the
throwing case, since the reference a may not be kept in the else clause.

Hans

@RogerRiggs
Copy link
Contributor

@RogerRiggs RogerRiggs commented Jun 2, 2022

Hans, thank for the detailed example. I had not fully considered the flow of control in the throwing case.

Copy link
Contributor

@RogerRiggs RogerRiggs left a comment

trivial comments.
The commented out printf/println's should be removed before committing.

homeCtx.incEnumCount();
enumClnt = homeCtx.clnt; // remember
this.enumCtx.homeCtx.incEnumCount();
this.cleanable = LDAP_CLEANER.register(this, enumCtx);
Copy link
Contributor

@RogerRiggs RogerRiggs Jun 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use enumCtx.getHomeCts() above.
Use this.enumCtx in call to register.
For consistency.

@openjdk
Copy link

@openjdk openjdk bot commented Jun 7, 2022

@bchristi-git This change is no longer ready for integration - check the PR body for details.

@openjdk openjdk bot added the ready label Jun 7, 2022
@bchristi-git
Copy link
Member Author

@bchristi-git bchristi-git commented Jun 7, 2022

The commented out printf/println's should be removed before committing.

Do you mean the pre-existing printlns in LdapSearchEnumeration.java?

@RogerRiggs
Copy link
Contributor

@RogerRiggs RogerRiggs commented Jun 7, 2022

The commented out printf/println's should be removed before committing.

Do you mean the pre-existing printlns in LdapSearchEnumeration.java?
Usually, I would mean any that were added for this issue.
The changes in indentation (as presented by Github) mislead me.
Keep them if they were there before.

@bridgekeeper
Copy link

@bridgekeeper bridgekeeper bot commented Jul 5, 2022

@bchristi-git This pull request has been inactive for more than 4 weeks and will be automatically closed if another 4 weeks passes without any activity. To avoid this, simply add a new comment to the pull request. Feel free to ask for assistance if you need help with progressing this pull request towards integration!

@openjdk openjdk bot removed ready rfr labels Jul 20, 2022
@bchristi-git
Copy link
Member Author

@bchristi-git bchristi-git commented Jul 21, 2022

I've updated how the reachability and memory visibility issues (per comment) are addressed:

Additional reachability fences

Unless it's immediately obvious that no cleanable state is accessed during the course of a method's execution, all methods should include a reachabilityFence to ensure that cleanable state is not cleaned up while a method is still running. This applies to any class that uses a Cleaner (or a finalizer, really).

VarHandle.fullFence to ensure memory visibility, instead of synchronized methods
This makes for simpler code, better reflects our intentions, and covers cases where a field of a volatile object is modified (volatility of an object reference does not extend to its fields).

Unless it's immediately obvious that no cleanable state is modified during the course of the method's execution, all methods should include a fullFence to ensure changes made on the main/program thread are seen by the cleanup thread. This applies to any class that uses a Cleaner (or a finalizer, really) where the state to be cleaned can be mutated during the course of execution.

Other possibilities for triggering the necessary volatile operations could be:

  • empty synchronized blocks
  • adding some "junk" variable to EnumCtx, along with volatile reads/writes of the variable where needed

One could perhaps use deep code tracing to determine that cleanable state is not accessed/written during the course of a method's execution, and omit fences based on that knowledge. However I believe that leaves too large a burden on future maintainers and reviewers to remember to re-perform the code tracing and re-confirm non-use of cleanable state, even when changing possibly "far away" code.

Copy link
Contributor

@RogerRiggs RogerRiggs left a comment

LGTM
(Except for a TODO and some tabs that should be spaces)
(Probably there's a higher level lesson to be learned about designing this kind of interaction with a server and the teardown needed).

@openjdk openjdk bot added the rfr label Jul 22, 2022
// are reachability fences to ensure that the registered object remains
// reachable.
// TODO: Is anything else needed so that this constructor
// "happens-before" the cleaning action ?
Copy link
Member

@stuart-marks stuart-marks Jul 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the resolution of this TODO is to modify the specification of Cleaner to require that:

  1. the object being registered is guaranteed reachable until after registration has completed; and
  2. a "memory consistency effects" clause should also be added to Cleaner.register. For an example, see the class specification of CopyOnWriteArrayList.

It may be that reachabilityFences are already in the right place in Cleaner. Great. But last time I looked it doesn't have any memory visibility operations. I'd suggest opening a bug on Cleaner for the spec change and accumulating notes there.

@stuart-marks
Copy link
Member

@stuart-marks stuart-marks commented Jul 22, 2022

I think the main outstanding issues here are as follows.

First, are we convinced that it's necessary to add a try/finally block with a memory fence and a reachability fence in pretty much every mutator method? I think the answer is Yes, but we ought to have this ratified by memory model and GC experts.

A second issue is whether VarHandle::fullFence is the right memory fence operation. I'm not quite convinced that it is, but I'm not sure what the right alternative is either.

Finally, we probably also need approval from the maintainers of this code. :-)

@stuart-marks
Copy link
Member

@stuart-marks stuart-marks commented Jul 26, 2022

Hans Boehm wrote:

I also have concerns about the use of fullFence here. I believe it should be the case that reachabilityFence guarantees visibility of memory operations program-ordered before the reachabilityFence(p) to the Cleaner associated with p. Mutator-collector synchronization should normally ensure that. On rereading, it also appears to me that the current reachabilityFence() documentation does not make that as clear as it should.

It appears to me that this is addressing an instance of the problem for which I suggested a more general, though also not completely elegant, solution in https://docs.google.com/document/d/1yMC4VzZobMQTrVAraMy7xBdWPCJ0p7G5r_43yaqsj-s/edit?usp=sharing .

Hi Hans, thanks for looking at this. In the prior discussions on reachabilityFence and premature finalization, I don't recall seeing any mention of memory model issues. To my mind the discussions here are the first mention of them. (Of course it's possible I could have missed something.) The memory model is quite subtle and it's difficult to be sure of anything. However, as time goes on we've become more confident that there IS an issue here with the memory model that needs to be addressed.

Then there is a the question of what to do about it. One possibility is to add some memory ordering semantics into reachabilityFence(p), as you suggest. As convenient as it might seem, it's not obvious to me that we want reachability fused with memory order. At least we should discuss them separately before deciding what to do.

This PR seems to be on a long journey :-) so let me describe some of the background and where I think this ought to go.

First, like most PRs, this started off as an effort to make some code changes. In this case it's part of Brent's (@bchristi-git) effort to convert finalizers in the JDK to Cleaners. This is related to discovering coding patterns for how to do this effectively. For example, the entire object's state is available to the finalizer. But in order to convert this to a Cleaner, the state available to the cleaning action needs to be refactored into a separate object from the "outer" object. The EnumCtx nested class serves this role here.

Second, we're trying to address the reachability issues. You (Hans) have been writing and speaking about this for some time, mostly in the context of finalization. We in the JDK group haven't prioritized fixing this issue with respect to finalization (since it's old and deprecated and nobody should be using it, yeah right). Now that we're converting things to use Cleaner, which has the same issues, we're forced to confront them. Our working position is that there needs to be a reachabilityFence within a try/finally block in the "right" places; determining the definition of "right" is one of the goals here.

The third issue is memory ordering. For finalization-based objects, the JLS specifies a happens-before edge between the constructor and the finalizer. (I think this works for objects whose finalizable state is fully initialized in the constructor. But it doesn't work for objects, like this one, whose finalizable state is mutable throughout the lifetime of the object.) There is nothing specified for memory ordering edges for Cleaner or java.lang.ref references at all that I can see. Given the lack of such specifications, we're faced with using the right low-level memory ordering mechanisms to get the memory order we require. We're using VarHandle::fullFence as kind of a placeholder for this. (We've also considered empty synchronized blocks, writes/reads to "junk" variables created expressly for this purpose, and other VarHandle fence operations, but none seem obviously better. I'm sure there are other alternatives we haven't considered.) I'd welcome discussion of the proper alternative.

The fourth issue is, do we really want every programmer who uses Cleaner to have to figure out all this reachabilityFence and VarHandle fence stuff? Of course not. It would be nice to have some higher-level construct (such as a language change like the "Reachability Revisited" proposal), or possibly to create some library APIs to facilitate this. At a minimum, I think we need to adjust various specifications like Cleaner and java.lang.ref to address memory ordering issues. There is certainly more that needs to be done though.

The difficulty with trying to come up with language changes or library APIs is that I don't think we understand this problem well enough to define what those mechanisms are actually supposed to do. So before we get to that point, I think we should see attempt to write down a correct solution using the existing reachability and memory ordering mechanisms. That's what Brent has done here. We should review and iterate on this and identify the places where the specs need to change, and arrive at a point where the we believe the code, even if ugly and inconvenient, is correct under that spec.

Maybe at that point we can contemplate a higher-level approach such as a language mechanism or a library API (or both), but I don't think we're quite there yet. Or maybe some alternative path forward might emerge.

// overridden to access the cleanable state without the proper fences.

// Ensure writes are visible to the Cleaner thread
VarHandle.fullFence();
Copy link
Contributor

@plevart plevart Jul 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think any memory fences are needed here. Reachability fence is enough. Cleaner runs in its own thread by polling enqued PhantomReference(s) from the ReferenceQueue. There is a happens-before edge between ReferenceHandler thread enqueue-ing "pending" PhantomReferences and Cleaner thread dequeue-ing them. There is a happens-before edge between GC thread clearing a PhantomReference and linking it into a "pending" list and ReferenceHandler thread unlinking it from the "pending" list and enque-ing it into a ReferenceQueue. There is a happens-before edge before a call to Reference.reachabilityFence(referent) and a GC thread discovering a phantom-reachable referent and clearing the PhantomReference and linking it into a "pending" list.
So there you have a happens-before chain which makes all writes before a call to eference.reachabilityFence(this) visible to a Cleaner task that is initialized to observe reachabillity of this....

// Same situation as nextElement() - see comment above

// Ensure writes are visible to the Cleaner thread
VarHandle.fullFence();
Copy link
Contributor

@plevart plevart Jul 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above. Memory fences are not needed.

}
} finally {
// Ensure writes are visible to the Cleaner thread
VarHandle.fullFence();
Copy link
Contributor

@plevart plevart Jul 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The memory fence is not needed (as explained above), but...

return (AbstractLdapNamingEnumeration<? extends NameClassPair>)refCtx.list(listArg);
} finally {
// Ensure writes are visible to the Cleaner thread
VarHandle.fullFence();
Copy link
Contributor

@plevart plevart Jul 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for memory fence

return sr;
} finally {
// Ensure writes are visible to the Cleaner thread
VarHandle.fullFence();
Copy link
Contributor

@plevart plevart Jul 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

memory fence is not needed.

searchArgs.name, searchArgs.filter, searchArgs.cons);
} finally {
// Ensure writes are visible to the Cleaner thread
VarHandle.fullFence();
Copy link
Contributor

@plevart plevart Jul 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for memory fence.

@Override
public void run() {
// Ensure changes on the main/program thread happens-before cleanup
VarHandle.fullFence();
Copy link
Contributor

@plevart plevart Jul 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for memory fence as explained in various finally blocks...

if (homeCtx != null) {
homeCtx.decEnumCount();
homeCtx = null;
}
Copy link
Contributor

@plevart plevart Jul 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaner's task (run() method) is guaranteed to be called at most once. So there's no need for above "idempotent" logic. Unless some of those fields are optional when the instance is constructed....

@plevart
Copy link
Contributor

@plevart plevart commented Jul 30, 2022

Hans Boehm wrote:

I also have concerns about the use of fullFence here. I believe it should be the case that reachabilityFence guarantees visibility of memory operations program-ordered before the reachabilityFence(p) to the Cleaner associated with p. Mutator-collector synchronization should normally ensure that. On rereading, it also appears to me that the current reachabilityFence() documentation does not make that as clear as it should.

It appears to me that this is addressing an instance of the problem for which I suggested a more general, though also not completely elegant, solution in https://docs.google.com/document/d/1yMC4VzZobMQTrVAraMy7xBdWPCJ0p7G5r_43yaqsj-s/edit?usp=sharing .

Hi Hans, thanks for looking at this. In the prior discussions on reachabilityFence and premature finalization, I don't recall seeing any mention of memory model issues. To my mind the discussions here are the first mention of them. (Of course it's possible I could have missed something.) The memory model is quite subtle and it's difficult to be sure of anything. However, as time goes on we've become more confident that there IS an issue here with the memory model that needs to be addressed.

Then there is a the question of what to do about it. One possibility is to add some memory ordering semantics into reachabilityFence(p), as you suggest. As convenient as it might seem, it's not obvious to me that we want reachability fused with memory order. At least we should discuss them separately before deciding what to do.

This PR seems to be on a long journey :-) so let me describe some of the background and where I think this ought to go.

First, like most PRs, this started off as an effort to make some code changes. In this case it's part of Brent's (@bchristi-git) effort to convert finalizers in the JDK to Cleaners. This is related to discovering coding patterns for how to do this effectively. For example, the entire object's state is available to the finalizer. But in order to convert this to a Cleaner, the state available to the cleaning action needs to be refactored into a separate object from the "outer" object. The EnumCtx nested class serves this role here.

Second, we're trying to address the reachability issues. You (Hans) have been writing and speaking about this for some time, mostly in the context of finalization. We in the JDK group haven't prioritized fixing this issue with respect to finalization (since it's old and deprecated and nobody should be using it, yeah right). Now that we're converting things to use Cleaner, which has the same issues, we're forced to confront them. Our working position is that there needs to be a reachabilityFence within a try/finally block in the "right" places; determining the definition of "right" is one of the goals here.

The third issue is memory ordering. For finalization-based objects, the JLS specifies a happens-before edge between the constructor and the finalizer. (I think this works for objects whose finalizable state is fully initialized in the constructor. But it doesn't work for objects, like this one, whose finalizable state is mutable throughout the lifetime of the object.) There is nothing specified for memory ordering edges for Cleaner or java.lang.ref references at all that I can see. Given the lack of such specifications, we're faced with using the right low-level memory ordering mechanisms to get the memory order we require. We're using VarHandle::fullFence as kind of a placeholder for this. (We've also considered empty synchronized blocks, writes/reads to "junk" variables created expressly for this purpose, and other VarHandle fence operations, but none seem obviously better. I'm sure there are other alternatives we haven't considered.) I'd welcome discussion of the proper alternative.

The fourth issue is, do we really want every programmer who uses Cleaner to have to figure out all this reachabilityFence and VarHandle fence stuff? Of course not. It would be nice to have some higher-level construct (such as a language change like the "Reachability Revisited" proposal), or possibly to create some library APIs to facilitate this. At a minimum, I think we need to adjust various specifications like Cleaner and java.lang.ref to address memory ordering issues. There is certainly more that needs to be done though.

The difficulty with trying to come up with language changes or library APIs is that I don't think we understand this problem well enough to define what those mechanisms are actually supposed to do. So before we get to that point, I think we should see attempt to write down a correct solution using the existing reachability and memory ordering mechanisms. That's what Brent has done here. We should review and iterate on this and identify the places where the specs need to change, and arrive at a point where the we believe the code, even if ugly and inconvenient, is correct under that spec.

Maybe at that point we can contemplate a higher-level approach such as a language mechanism or a library API (or both), but I don't think we're quite there yet. Or maybe some alternative path forward might emerge.

Hi,

Sorry for not reading up to this message before starting reviewing the changes... As I was trying to explain in the comments, I don't think there is any issue about memory ordering here. As Cleaner is implemented currently in OpenJDK, there a 5 threads involved in a typical scenario of a cleanable object:
T1 - initializing thread that registers a Cleaner action
T2 - some thread that accesses/mutates object state including parts that are accessed/cleaned up by Cleaner action and guards against premature Cleaner action execution with reachability fence
T3 - GC thread that discovers a phantom-reachable referent of a PhantomReference that is tracked by the Cleaner and clears the PhantomReference and links it into a "pending" list.
T4 - a ReferenceHandler thread that waits for "pending" Reference(s) and enqueues them to ReferenceQueue(s)
T5 - a single Cleaner thread that dequeues PhantomReference(s) from the Cleaner's ReferenceQueue and executes cleanup actions

There is a synchronization action between T1 and T5, so there is no doubt that writes in the construrctor of a tracked object that typically preced Cleaner registration are visible to Cleaner action.

There is a synchronization action between T3 and T4 and there is a synchronization action between T4 and T5 (both can be verified in code). The only question here remaining is whether there is synchronization between threads that mutate object state and GC thread that discovers phantom-reachable referents..

I'm not qualified to answer that question. Especially with the advent of new fully concurrent GC algorithms such as ZGC and Shenandoah. So perhaps this is a question for GC experts.

@plevart
Copy link
Contributor

@plevart plevart commented Aug 1, 2022

...while waiting for a GC expert to confirm that writes, in program order preceding a call to reachability fence, are visibile to GC thread that discovers a phantom reachable referent, here is an example which illustrates that some writes at least "must" be visible for GC to function properly:

var ar = new Object[1];
var e = new Object();

ar[0] = e;
Reference.reachabilityFence(e);

// GC kicks-in at this point

var e2 = ar[0];
// use e2

A write to an array slot ar[0] must be visible to a GC thread when it searches for root(s) after the mutator thread's call to reachability fence. If it was not, it could miss that fact that object 'e' is still reachable. So GC must do something so that at least writes to reference variables are visible. If it does not distinguish reference writes from primitive writes, then it does the same for all writes.

@stuart-marks
Copy link
Member

@stuart-marks stuart-marks commented Aug 3, 2022

Hi Peter, thanks for contributing to this. I think you're observing that as a practical matter, the implementations of GC and of various libraries such as reference queues must see a consistent view of memory in order for anything to work. This seems right. However, I'm concerned about what the specification says, or ought to say.

Hans seems to think that the JLS assertions about finalization also apply to reference processing. I'm not so sure... that seems to me to be a rather generous interpretation. On the face of it, I cannot find anything explicit in the specifications that supports it. It certainly seems reasonable (to me) that reachabilityFence(x) ought to HB the corresponding reference being dequeued. If so I would like the specification to say that. (The fact that there is a GC thread that enqueues the reference is part of the implementation, which is opaque to the specification.) It may also be the that any point where the referent is reachable HB its corresponding reference is dequeued. Clearly the GC implementation needs to make sure this is the case; the questions in my mind are whether, where, and how this should be specified!

It may be that the VarHandle fences aren't necessary. However, if they end up driving the right updates to the specifications, they will have served their purpose.

Setting this aside, it does seem like all uses of a cleanable object need to have a try/finally statement, with at least an RF in the finally clause. Is there any evidence that shows that this construct isn't needed?

@mlbridge
Copy link

@mlbridge mlbridge bot commented Aug 5, 2022

Mailing list message from Hans Boehm on core-libs-dev:

I also have concerns about the use of fullFence here. I believe it should
be the case that reachabilityFence guarantees visibility of memory
operations program-ordered before the reachabilityFence(p) to the Cleaner
associated with p. Mutator-collector synchronization should normally ensure
that. On rereading, it also appears to me that the current
reachabilityFence() documentation does not make that as clear as it should.

It appears to me that this is addressing an instance of the problem for
which I suggested a more general, though also not completely elegant,
solution in
https://docs.google.com/document/d/1yMC4VzZobMQTrVAraMy7xBdWPCJ0p7G5r_43yaqsj-s/edit?usp=sharing
.

On Fri, Jul 22, 2022 at 3:27 PM Stuart Marks <smarks at openjdk.org> wrote:

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/core-libs-dev/attachments/20220724/fcf8d713/attachment.htm>

@mlbridge
Copy link

@mlbridge mlbridge bot commented Aug 5, 2022

Mailing list message from Hans Boehm on core-libs-dev:

On Mon, Jul 25, 2022 at 10:30 PM Stuart Marks <smarks at openjdk.org> wrote:
...

Hans Boehm wrote:

I also have concerns about the use of fullFence here. I believe it

should be the case that reachabilityFence guarantees visibility

of memory operations program-ordered before the reachabilityFence(p) to

the Cleaner associated with p. Mutator-collector

synchronization should normally ensure that. On rereading, it also

appears to me that the current reachabilityFence() documentation does not
make that as clear as it should.

It appears to me that this is addressing an instance of the problem for

which I suggested a more general, though also not

completely elegant, solution in

https://docs.google.com/document/d/1yMC4VzZobMQTrVAraMy7xBdWPCJ0p7G5r_43yaqsj-s/edit?usp=sharing
.

Hi Hans, thanks for looking at this. In the prior discussions on

reachabilityFence and premature finalization, I don't recall seeing any
mention of

memory model issues. To my mind the discussions here are the first

mention of them. (Of course it's possible I could have missed something.)

The memory model is quite subtle and it's difficult to be sure of

anything. However, as time goes on we've become more confident that there
_IS_

an issue here with the memory model that needs to be addressed.

Then there is a the question of what to do about it. One possibility is

to add some memory ordering semantics into reachabilityFence(p), as you
suggest.

As convenient as it might seem, it's not obvious to me that we want

reachability fused with memory order. At least we should discuss them
separately

before deciding what to do.

This PR seems to be on a long journey :-) so let me describe some of the

background and where I think this ought to go.

First, like most PRs, this started off as an effort to make some code

changes. In this case it's part of Brent's (@bchristi-git) effort to
convert finalizers

in the JDK to Cleaners. This is related to discovering coding patterns

for how to do this effectively. For example, the entire object's state is
available

to the finalizer. But in order to convert this to a Cleaner, the state

available to the cleaning action needs to be refactored into a separate
object from the

"outer" object. The `EnumCtx` nested class serves this role here.

Second, we're trying to address the reachability issues. You (Hans) have

been writing and speaking about this for some time, mostly in the context of

finalization. We in the JDK group haven't prioritized fixing this issue

with respect to finalization (since it's old and deprecated and nobody
should be

using it, yeah right). Now that we're converting things to use Cleaner,

which has the same issues, we're forced to confront them. Our working
position

is that there needs to be a reachabilityFence within a try/finally block

in the "right" places; determining the definition of "right" is one of the
goals here.
Fully agreed. reachabilityFence use is really orthogonal to the mechanism
used for the cleanup action. It also exists for plain References.
Technically,
you don't even need ReferenceQueues to trigger the problem; there are
probably cases in which WeakReferences are polled such that you need
reachabilityFence calls.

The third issue is memory ordering. For finalization-based objects, the

JLS specifies a happens-before edge between the constructor and the

finalizer. (I think this works for objects whose finalizable state is

fully initialized in the constructor. But it doesn't work for objects, like
this one,

whose finalizable state is mutable throughout the lifetime of the object.)

My recollection is that was added as a very minimal guarantee, that was not
intended to suffice in general. Even if the state is fully initialized
in the constructor, things will usually still break if the final method
call that uses the state does not happen before the execution of the
Cleaner.

There is nothing specified for memory ordering edges for Cleaner or
java.lang.ref references at all that I can see. Given the lack of such

specifications, we're faced with using the right low-level memory ordering

mechanisms to get the memory order we require. We're using

VarHandle::fullFence as kind of a placeholder for this. (We've also
considered

empty synchronized blocks, writes/reads to "junk" variables created

expressly for this purpose, and other VarHandle fence operations,

but none seem obviously better. I'm sure there are other alternatives we

haven't considered.) I'd welcome discussion of the proper alternative.
I think the intent was that JLS 12.6.2 still applies, though that section
was always stated in terms of finalizers. And in the absence of
reachabilityFence, it's rather weak anyway.

The fourth issue is, do we really want every programmer who uses Cleaner

to have to figure out all this reachabilityFence and VarHandle fence stuff?

Of course not. It would be nice to have some higher-level construct (such

as a language change like the "Reachability Revisited" proposal), or
possibly

to create some library APIs to facilitate this. At a minimum, I think we

need to adjust various specifications like Cleaner and java.lang.ref to

address memory ordering issues. There is certainly more that needs to be

done though.
I clearly agree.

The difficulty with trying to come up with language changes or library

APIs is that I don't think we understand this problem well enough to define

what those mechanisms are actually supposed to do. So before we get to

that point, I think we should see attempt to write down a correct solution

using the existing reachability and memory ordering mechanisms. That's

what Brent has done here. We should review and iterate on this and identify

the places where the specs need to change, and arrive at a point where

the we believe the code, even if ugly and inconvenient, is correct under
that spec.
I'm not convinced that's feasible without some spec clarification.
Otherwise I would agree.

The reachabilityFence spec currently says: "the referenced object is not
reclaimable by garbage collection at least until after the invocation of
this method."

In my opinion, the only meaningful interpretation of this is that the
reachabilityFence call happens before any Reference to the argument object
is enqueued.
Preventing "garbage collection" per se isn't ever a goal. (And in the case
of finalizers, it happens much later than finalization,
so it's not the right notion anyway.) And the only sense of "after" that
makes sense in such contexts is the inverse of the happens-before relation.
In retrospect, this interpretation is definitely a stretch, and the spec
should be much clearer. And I suspect that if we had a clearer statement to
this
effect we might largely be able to get rid of 12.6.2, which would be a huge
win.

It sounds like you're applying an alternative reading that largely ignores
the clause I quoted, and simply goes by "Ensures that the object referenced
by
the given reference remains strongly reachable". Presumably this implies,
via the Reference spec, that References are not enqueued until later. Even
there, it's unclear to me what "later" means, except in terms of
happens-before.

AFAICT, implementations actually do comply with my reading, though an
OpenJDK expert should confirm. We won't enqueue a Reference to x, where
x was last referenced by reachabilityFence(x) in thread T, unless there is
an intervening GC that result in T at a safe-point past the
reachabilityFence.
The safepoint synchronization must ensure that the safepoint happens-before
the GC decision to enqueue, which will then happen-before the actual
enqueuing. Enforcing this ordering is cheap, and probably necessary for GC
correctness anyway. So in implementation terms, I think my interpretation
is safe.

In contrast, putting in explicit fullFences is clearly horribly expensive,
much more so than a reachabilityFences. And they only make sense if they
occur both at the use
sites and in the Cleaner. And without my "happens-before" interpretation,
we technically don't know that use site one happens before the one in the
Cleaner,
so I don't think we have a guarantee they help, except by an assumption
that also implies they're not needed.

Maybe at that point we can contemplate a higher-level approach such as a

language mechanism or a library API (or both), but I don't think we're
quite there yet.

Or maybe some alternative path forward might emerge.

We share that hope. None of the current options look beautiful to me
either. But I think it would also be useful to agree that reachabilityFence
implies memory ordering.
Even if we find a better mechanism for most case, I expect that
reachabilityFence will still make sense in corner cases.

Hans
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/core-libs-dev/attachments/20220726/e034be9a/attachment-0001.htm>

@mlbridge
Copy link

@mlbridge mlbridge bot commented Aug 5, 2022

Mailing list message from Hans Boehm on core-libs-dev:

On Tue, Aug 2, 2022 at 7:43 PM Stuart Marks <smarks at openjdk.org> wrote:

...
Setting this aside, it does seem like all uses of a cleanable object need
to have a try/finally statement, with at least an RF in the finally clause.
Is there any evidence that shows that this construct isn't needed?

I think the reachabilityFence is clearly needed with current
implementations, though their omission is hard to test for, since the
failures tend to only occur with unlikely schedules. But those schedules
clearly exist. And fixing implementations to avoid that by unconditionally
preserving references is problematic, especially given that bytecode tends
to hide scopes. (Back during Java memory model discussions, there was also
an argument that this would cause us to pay everywhere for a rarely used
feature. I believe the "rarely used" part less and less. On the other hand,
I also now tend to view a percent or two performance difference as more
significant than back then, so maybe that overall argument still applies.)

I would also guess that the try-finally is required by some current
optimizations. But that's more based on an argument that such optimizations
exist, so somebody must have implemented them, rather than anything more
concrete.

Hans

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/core-libs-dev/attachments/20220802/49ce514e/attachment.htm>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core-libs rfr
5 participants