Skip to content

Introduce ObjectCleaner and use it in FastThreadLocal to ensure FastThreadLocal.onRemoval(...) is called#7463

Closed
normanmaurer wants to merge 1 commit into4.1from
cleanup_phantom
Closed

Introduce ObjectCleaner and use it in FastThreadLocal to ensure FastThreadLocal.onRemoval(...) is called#7463
normanmaurer wants to merge 1 commit into4.1from
cleanup_phantom

Conversation

@normanmaurer
Copy link
Copy Markdown
Member

@normanmaurer normanmaurer commented Dec 1, 2017

Motivation:

There is no guarantee that FastThreadLocal.onRemoval(...) is called if the FastThreadLocal is used by "non" FastThreacLocalThreads. This can lead to all sort of problems, like for example memory leaks as direct memory is not correctly cleaned up etc.

Beside this we use ThreadDeathWatcher to check if we need to release buffers back to the pool when thread local caches are collected. In the past ThreadDeathWatcher was used which will need to "wakeup" every second to check if the registered Threads are still alive. If we can ensure FastThreadLocal.onRemoval(...) is called we do not need this anymore.

Modifications:

- Introduce ObjectCleaner and use it to ensure FastThreadLocal.onRemoval(...) is always called when a Thread is collected.
- Deprecate ThreadDeathWatcher
- Add unit tests.

Result:

Consistent way of cleanup FastThreadLocals when a Thread is collected.

@normanmaurer
Copy link
Copy Markdown
Member Author

There is still some TODO to fix ... Just wanted to show the idea

@johnou
Copy link
Copy Markdown
Contributor

johnou commented Dec 1, 2017

@normanmaurer could the same approach be used with Recycler instead of finalize?

@normanmaurer
Copy link
Copy Markdown
Member Author

@johnou not sure what you refer to with finalize.... any pointers?

@johnou
Copy link
Copy Markdown
Contributor

johnou commented Dec 1, 2017

@normanmaurer

io.netty.util.Recycler.WeakOrderQueue#finalize

        @Override
        protected void finalize() throws Throwable {
            try {
                super.finalize();
            } finally {
                // We need to reclaim all space that was reserved by this WeakOrderQueue so we not run out of space in
                // the stack. This is needed as we not have a good life-time control over the queue as it is used in a
                // WeakHashMap which will drop it at any time.
                Link link = head;
                while (link != null) {
                    reclaimSpace(LINK_CAPACITY);
                    link = link.next;
                }
            }
        }

@normanmaurer
Copy link
Copy Markdown
Member Author

normanmaurer commented Dec 1, 2017 via email

Copy link
Copy Markdown
Member

@ejona86 ejona86 left a comment

Choose a reason for hiding this comment

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

I don't see how this would avoid ClassLoader leaks. In fact, it seems strictly worse.

As I mentioned in #7446 (comment), I think there is a fundamental issue with cleaning anything retained from a static via a background-based cleaner.

Thread cleanupThread = new Thread(new Runnable() {
@Override
public void run() {
for (;;) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So this runs forever? Isn't that guaranteed to be a classloader leak via this Runnable?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@ejona86 yes... I will fix this soon :)

Copy link
Copy Markdown
Member

@ejona86 ejona86 left a comment

Choose a reason for hiding this comment

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

This seems fine to me, other than the ThreadCleaner looping forever. And actually, starting a thread from a static initializer also seems a bit worrisome.

}
return cache;
}
// No caching for non FastThreadLocalThreads.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This comment looks out-of-date. Remove?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

s/htraded/threaded/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: s/its/it's/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: there's a double space

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

s/arleady/already/

@ejona86
Copy link
Copy Markdown
Member

ejona86 commented Dec 12, 2017

I guess the failure case is the ThreadLocal instance could be thrown away, and let the storage evaporate (since Thread uses WeakReference for the mapping), but the Thread itself remain running.

So, this turned out to be semi-wrong. Or rather, if you leave a Thread running, "you're gonna have a bad time." I realized that even using finalize() you can't necessarily clean up resources if the Thread lives because ThreadLocal is only lazily cleaned up in the ThreadLocalMap. Any values held are strong references, and so they might be retained for the rest of the Thread's lifetime.

So I'm not trying to solve that any more :-). And that makes multiple solutions we've considered solvable. The only thing we need to be wary of is clearing the Thread.contextClassLoader.

@normanmaurer normanmaurer changed the title Introduce AutomaticCleanupReference and use it PooledByteBufAllocator… Introduce ThreadCleaner and use it in PooledByteBufAllocator to cleanup ThreadLocal caches. Dec 12, 2017
@@ -440,14 +441,26 @@ protected synchronized PoolThreadCache initialValue() {
if (useCacheForAllThreads || current instanceof FastThreadLocalThread) {
// If our FastThreadLocalThread will call FastThreadLocal.removeAll() we not need to use
// the ThreadDeathWatcher to release memory from the PoolThreadCache once the Thread dies.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

update javadoc?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

+1 ... Let me do this


// We store the Thread in a WeakReference as otherwise we may be the only ones that still hold a strong
// Reference to the Thread itself after it died.
final WeakReference<Thread> threadRef;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should this be in the same pull request?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I did it in the same as I noticed it here because the Thread was never GCed. Maybe I should do it in a separate PR tho..

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@johnou #7499 ... Even added a test-case :)

* Allows a way to register some {@link Runnable} that will executed once there are no references to the {@link Thread}
* anymore. This typically happens once the {@link Thread} dies / completes.
*/
public final class ThreadCleaner {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

no phantom in the end? :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

nope... WeakReference was good enough.

@carl-mastrangelo
Copy link
Copy Markdown
Member

carl-mastrangelo commented Dec 13, 2017

Out of curiosity: why not spawn the cleaner thread when the original thread goes away, rather than when it is first attached? That would keep the number of active threads down.

Also, why not do the cleanup in a finalize() method? This is actually garbage collection, so it seems reasonable to do it there and not spawn another thread.

@normanmaurer
Copy link
Copy Markdown
Member Author

@carl-mastrangelo answers / comments below....

Out of curiosity: why not spawn the cleaner thread when the original thread goes away, rather than when it is first attached? That would keep the number of active threads down.

How would you detect that the original Thread goes away ? Also I think its not so bad to have one extra thread that is doing the cleanup for everything. I will most likely use it in other places as well at some point.

Also, why not do the cleanup in a finalize() method? This is actually garbage collection, so it seems reasonable to do it there and not spawn another thread.

I actually had a PR (#7446) with a finalize() at some point but @ejona86 asked me to not do this as finalize() is marked as @deprecated in Java9 and so should not be used anymore.

@johnou
Copy link
Copy Markdown
Contributor

johnou commented Dec 13, 2017

@carl-mastrangelo Object.finalize is deprecated, the recommended replacements are PhantomReference and the new Cleaner class.

@normanmaurer
Copy link
Copy Markdown
Member Author

@johnou @ejona86 @carl-mastrangelo PTAL again as I changed the approach a bit in a followup commit that I pushed. So the "real problem" why we needed all of this is because of the user uses a non FastThreadLocalThread there is no guarantee that we will call FastThreadLocal.removeAll() at all once the Thread goes away. The cleanup of the PoolThreadCache is done in FastThreadLocal.onRemove(...) that will be triggered by the removeAll().

Because of this I now moved the usage of ThreadCleaner.register(...) into the FastThreadLocal itself which has the affect that we are guaranteed that onRemoval(...) is always called (even if we use a non FastThreadLocal. I think this is a more general fix to the problem and also fixes a possible memory leak that could happen NativeDatagramPacketArray if non FastThreadLocal threads are used to access it as onRemoval would never been called before for these:

https://github.com/netty/netty/blob/4.1/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeDatagramPacketArray.java#L43

WDYT ?

}
}

private static void removeAll(InternalThreadLocalMap threadLocalMap) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a reason this was split out into its own function?

I had expected the Runnable passed to ThreadCleaner would use it, but it uses remove(...) instead. It's a bit hard to determine what's the difference between the two methods.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Actually sorry it should use removeAll and I messed up :(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Sorry what I wanted to say was that we not need to factor out removeAll and can just call remove... will fix it.

@ejona86
Copy link
Copy Markdown
Member

ejona86 commented Dec 13, 2017

Because of this I now moved the usage of ThreadCleaner.register(...) into the FastThreadLocal itself which has the affect that we are guaranteed that onRemoval(...) is always called (even if we use a non FastThreadLocal

It also means that you'll use the ThreadCleaner even if nothing stored in the FastThreadLocal needs cleaning. Normally that'd be rare, but if caching is disabled for non-FastThreadLocalThreads, then it'd be more frequent. Not sure it matters at all.

@normanmaurer
Copy link
Copy Markdown
Member Author

@ejona86 yeah I agree... That said I think it may be still the preferred way to fix it and ensure onRemoval is always called and so can depend on it... WDYT ?

*/
@UnstableApi
public static boolean willCleanupFastThreadLocals(Thread thread) {
return thread instanceof FastThreadLocalThread && ((FastThreadLocalThread) thread)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

might be clearer to break on &&

// This will hold a reference to the ThreadCleanerReference which will be removed once we called cleanup()
private static final Set<ThreadCleanerReference> LIVE_SET = new ConcurrentSet<ThreadCleanerReference>();
private static final ReferenceQueue<Object> REFERENCE_QUEUE = new ReferenceQueue<Object>();
private static final AtomicBoolean CLEANER_RUNNING = new AtomicBoolean(false);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: caps are usually for constants.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think CAPS are for things that are static final so I think this is fine. Sure the content can be changed but this is ok IMHO.

cleanupThread.setName("ThreadCleanerReaper");

// This Thread is not a daemon as it will die once all references to the registered Threads will go away.
cleanupThread.setDaemon(false);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Daemon is for not blocking shutdown. In a shutdown scenario, how important is it to clean up refs?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

actually I think we could also just use a non-daemon thread as the thread will go away once there is no references anymore.

@ejona86
Copy link
Copy Markdown
Member

ejona86 commented Dec 15, 2017

That said I think it may be still the preferred way to fix it and ensure onRemoval is always called and so can depend on it... WDYT ?

I'm fine with it. It might be unnecessary in some cases, but doesn't seem like it hurts much and does remove a type of bug-prone behavior.

@normanmaurer normanmaurer force-pushed the cleanup_phantom branch 2 times, most recently from a232792 to 18eedc6 Compare December 15, 2017 14:37
@normanmaurer
Copy link
Copy Markdown
Member Author

@carl-mastrangelo PTAL again

@normanmaurer normanmaurer requested a review from trustin December 18, 2017 08:42
Copy link
Copy Markdown
Member

@Scottmitch Scottmitch left a comment

Choose a reason for hiding this comment

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

few questions ... GC sensitive tests make me 😢 but not sure of a better alternative.

LIVE_SET.add(reference);

// Check if there is already
if (CLEANER_RUNNING.compareAndSet(false, true)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we should able to avoid the CAS operation most of the time by doing a read first because I assume should be true more often than not?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

again I think we should not care as this should not happen very frequently and the CAS is the thing that I would be concerned last about (as the other stuff will be the "expensive")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

fair enough ... I guess even if folks are doing get/remove this may not buy much.

* This should only be used if there are no other ways to execute some cleanup once the {@link Thread} dies as
* it is not a cheap way to handle the cleanup.
*/
public static void register(Thread thread, Runnable cleanupTask) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this class is internal ... consider:

  1. exposing a method where the user can directly create a ThreadCleanerReference to avoid allocating a Runnable and ThreadCleanerReference
  2. Make the class package private and you can use the same technique as above to reduce object allocation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I am not concerned about this as this should be something that is done very frequently and I think this design is cleaner.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Agreed the current APIs are nicer. However it is internal and doesn't need to be generic right? We could potentially save 1 object allocation per thread local. Since it is internal we can always change it later but maybe nice to keep thread local overhead down if we don't have to be generic.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@Scottmitch as discussed offline I will merge it as it is for now and we can address this if we find out its an issue later on. The current design is nicer :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

sgtm ... internal == change later if necessary

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

exactly 👍

}
}
if (interrupted) {
// As we catched the InterruptedException above we should mark the Thread as interrupted.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

catched -> caught

@normanmaurer
Copy link
Copy Markdown
Member Author

@Scottmitch yeah I think how we test here is the best we can do for now...

Copy link
Copy Markdown
Member

@Scottmitch Scottmitch left a comment

Choose a reason for hiding this comment

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

1 suggestion (#7463 (comment)) but lgtm

@carl-mastrangelo
Copy link
Copy Markdown
Member

cc: @zpencer

…hreadLocal.onRemoval(...) is called

Motivation:

There is no guarantee that FastThreadLocal.onRemoval(...) is called if the FastThreadLocal is used by "non" FastThreacLocalThreads. This can lead to all sort of problems, like for example memory leaks as direct memory is not correctly cleaned up etc.

Beside this we use ThreadDeathWatcher to check if we need to release buffers back to the pool when thread local caches are collected. In the past ThreadDeathWatcher was used which will need to "wakeup" every second to check if the registered Threads are still alive. If we can ensure FastThreadLocal.onRemoval(...) is called we do not need this anymore.

Modifications:

- Introduce ObjectCleaner and use it to ensure FastThreadLocal.onRemoval(...) is always called when a Thread is collected.
- Deprecate ThreadDeathWatcher
- Add unit tests.

Result:

Consistent way of cleanup FastThreadLocals when a Thread is collected.
@normanmaurer normanmaurer changed the title Introduce ThreadCleaner and use it in PooledByteBufAllocator to cleanup ThreadLocal caches. Introduce ObjectCleaner and use it in FastThreadLocal to ensure FastThreadLocal.onRemoval(...) is called Dec 20, 2017
@normanmaurer
Copy link
Copy Markdown
Member Author

@mosesn you may be interested as well.

@normanmaurer
Copy link
Copy Markdown
Member Author

Cherry-picked into 4.1 (e329ca1) and 4.0 (00ae627)

@normanmaurer normanmaurer deleted the cleanup_phantom branch December 21, 2017 06:39
Runnable runnable = new Runnable() {
@Override
public void run() {
assertEquals(Thread.currentThread().getName(), threadLocal.get());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A bit late to this PR, but what happens if code sets a FastThreadLocal and the thread exits before it's read? It looks like the cleaner code is only registered on the get path.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@zpencer good point. Let me fix this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

6 participants