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

Fix memory leak in tomcat application server #1701

Closed
wants to merge 5 commits into from

Conversation

rPraml
Copy link
Contributor

@rPraml rPraml commented May 12, 2019

This PR relates to that post
https://groups.google.com/forum/#!topic/ebean/pfrnwNlahOU
(the post seems to be corrupt in the google forum)

It is a first draft, how to solve the memory/transaction leak problem. I have to test that in my environment, if the problem is gone now.
@rbygrave maybe you can give me some feedback of this implementation

@@ -325,7 +325,7 @@ public Object getUserObject(String name) {

@Override
public void depth(int diff) {
transaction.depth();
transaction.depth(diff);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this is an error, that I have found while debugging

Copy link
Member

Choose a reason for hiding this comment

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

Logged as: #1705

protected synchronized TransactionMap initialValue() {
return new TransactionMap();
}
};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here are several misconcepts, that prevents an application server to unload ebean/webappclassloader

  1. the bracket initalization constructs an anonymous class, which holds a reference to our classloader
  2. the initalValue is also a class from ebean, which holds also a reference to the classloader.

If such a reference is stuck in an other thread, the GC cannot remove the classloader on redeploy.
A java.util.HashMap can be cleared by the leakDetector, so that all references can go away.

scopeManager.set(t);
return t;
SpiTransaction transaction = createTransaction(false, -1);
return wrapAsScopedTransaction(transaction);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

To get erverything working, I had to put only scopedTransaction in the DefaultTransactionScopeManager, respectively to the ThreadLocal.
When a non scoped transaction is put to the ThreadLocal, it will not get removed. It will only get inactive, so that it can be replaced the next run.
Do you think the additional overhead we will get here, is a problem?

Customer customer = Ebean.getReference(Customer.class, 42424242L);
Order order = new Order();
order.setCustomer(customer);
Ebean.save(order);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

these tests were all wrong! They have to save "order" and not "customer"

// we also use only maps from java.util, as these object may not trigger a
// class leaking issue in an application server if the maps are cleared by the
// leak detector
private static final ThreadLocal<Map<String, SpiTransaction>> local = new ThreadLocal<>();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I deleted TransactionMap and replaced it with a HashMap, which can be cleared on shutdown.

What were your intentions for using the serverName as key?
I think it would be better, to hold a ThreadLocal per server.
This could easily passed as "TransactionHolder" from the serverConfig here:
https://github.com/ebean-orm/ebean/blob/master/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java#L503

Copy link
Member

Choose a reason for hiding this comment

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

I deleted TransactionMap and replaced it with a HashMap

To get a PR in this area we need to focus on the minimal change and not try to include refactor/tidy up changes until we have the core change lined up and agreed.

/**
* The Transaction leak detection.
*/
private TransactionLeakDetection transactionLeakDetection = TransactionLeakDetection.COUNTER;

Choose a reason for hiding this comment

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

IMHO should be OFF by default. Leak detection is something you enable in a debug or development environment, not in production. Default settings should be sensible default settings for production.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good argument, there are currently 3 modes

  • OFF = really off
  • COUNTER = count how many transactions are started and stopped. Sum should be 0 on shutdown. Maintaining a counter should not add any noticeable overhead.
  • DETAIL = record details for debugging (add

So I thought COUNTER would be a good default value, because it would notify, if there are any detected leaks, but I can also change thhis to OFF.

Choose a reason for hiding this comment

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

Sure, but always think at scale. LongAdder maintains an internal array to avoid locking and contention. The size of the array reflects the amount of parallel count operations, which roughly represents the count of parallel transactions. So while it does not cost you a lot of performance, it will cost you memory. Given that transaction management is pretty crucial I would expect that most apps will not have transaction issues at all in production. So I think I would still prefer OFF as a default.

Also as a second note: If you assume that transaction leaks should be found during development and we are talking about web applications, then IMHO it would be preferable to have a leak output (if detected) per request or shortly after a request and not when undeploying the app. Could very well be, that during development, you simply hard restart the server instead of undeploying the app. So something like a servlet filter (or similar for other types of servers) might be more appropriate.
A different approach could also be to let the developer activate leak detection and then also let the developer specify a timeout after which the developer thinks a leak must had happened. That approach is for example used by HikariCP, a pretty good JDBC connection pool. The timeout you choose would be a little more than the slowest transaction in your app.

Copy link
Member

Choose a reason for hiding this comment

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

Unfortunately I don't want the TransactionLeakDetection in here at all.

That is, we already have this as part of Ebean's connection pool - that is the much better place to have it and I don't understand why that is part of this PR. It means this PR can't be merged as it is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rbygrave I agree and I'm also not very happy with the changes, as they touch one of the guts of ebean and I cannot test if it will work properly with spring/jta/....

The main issue was, that every thread is left with an inactive transaction object that were stuck in the ThreadLocal. And this will prevent tomcat from removing the web-app-classloader

This is also what the TransactionLeakDetector does: it detects stuck inactive transactions in the threadlocal object(s), so I tried to fix the root cause, so that this will not happen any more.

As workaround I can write some servlet filter, as from @jnehlmeier suggessted, which gurantees, that the ThreadLocal is removed after each web request (and may notify of leaked active transactions)

So I need a solution for my Classloader leak, because the old implementation left inactive transactions in the ThreadLocal (writing a servlet filter would be possible, but I see this as workaround and would like a clean implementation in ebean)

What do you suggest, how we should continue here?

Copy link
Member

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

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

There are three possible solutions for the class loader leak:

  1. A servlet filter that cleans ThreadLocals containing Ebean classes
  2. Don't put Ebean classes into ThreadLocals but only JDK classes
  3. Extract TransactionMap storage and have a ThreadLocal based implementation for classic applications and a request attribute based implementation (might require a Filter as well to get access to the request) when using Ebean in a server environment.

Wondering if anyone has used Ebean with async servlets yet. Because in that environment different threads can execute the same request but the transaction might life for the entire request. So storing transaction information in ThreadLocal is problematic in that case (the thread that starts processing the request and starts the transaction must not be the thread that finishes the request processing and wants to commit the transaction).

Copy link
Member

Choose a reason for hiding this comment

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

every thread is left with an inactive transaction object that were stuck in the ThreadLocal.

I think the goal is that under normal/good operation with each transaction committed or rolledback then DefaultTransactionThreadLocal is "ok for garbage collection".

Now, I believe we think there is a problem here because ...

  private static final ThreadLocal<TransactionMap> local = new ThreadLocal<TransactionMap>() {
    @Override
    protected synchronized TransactionMap initialValue() {
      return new TransactionMap();
    }
  };

... the above means there is anonymous class than references TransactionMap which is a class loaded by the same classloader as the anonymous class. This results in DefaultTransactionThreadLocal not being garbage collected even when TransactionMap has been removed by the commit() or rollback().

Is that the thinking?

Choose a reason for hiding this comment

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

@rbygrave you will have a leak if the ThreadLocal holds an instance of TransactionMap (that map can be empty or not, does not matter). So whenever you call local.get() without local.remove() / local.set(null) you have a problem.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks @jnehlmeier - that is what I was thinking.
This PR replaces TransactionMap with HashMap - https://github.com/ebean-orm/ebean/pull/1704/files (a cut down version of your change @rPraml )

@rbygrave
Copy link
Member

The second commit has changes that can not be merged. Do you want to redo this ?

@@ -810,14 +810,7 @@ public Transaction beginTransaction(TxScope txScope) {
public Transaction beginTransaction(TxIsolation isolation) {
// start an explicit transaction
SpiTransaction t = transactionManager.createTransaction(true, isolation.getLevel());
try {
Copy link
Member

Choose a reason for hiding this comment

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

Why? This was here for a reason etc.

// we also use only maps from java.util, as these object may not trigger a
// class leaking issue in an application server if the maps are cleared by the
// leak detector
private static final ThreadLocal<Map<String, SpiTransaction>> local = new ThreadLocal<>();
Copy link
Member

Choose a reason for hiding this comment

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

I deleted TransactionMap and replaced it with a HashMap

To get a PR in this area we need to focus on the minimal change and not try to include refactor/tidy up changes until we have the core change lined up and agreed.

private static TransactionMap.State getState(String serverName) {
return local.get().getStateWithCreate(serverName);
public static void shutdown() {
leakDetector.detectLeaks();
Copy link
Member

Choose a reason for hiding this comment

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

shutdown() depending on the leak detection that I'm not sure I'd want ... can this not work without leakDetector etc?

rbygrave added a commit that referenced this pull request May 16, 2019
rbygrave added a commit that referenced this pull request May 17, 2019
…nThreadLocal

- Use ThreadLocal.withInitial to always have a map.
- Remove the calls to local.remove(); when the map is empty (with the expectation that the thread local will GC with an empty map.
rbygrave added a commit that referenced this pull request May 17, 2019
#1701 (v2) - Replace TransactionMap with HashMap in DefaultTransactionThreadLocal
@rPraml
Copy link
Contributor Author

rPraml commented May 18, 2019

This is obsolete now, all relevant changes to fix the memory leak are included in #1711, #1714 and #1715. I also tested these changes in my application-server and the memory leak is gone now.

Ebean clears the threadLocal now in all known cases, so the autonatic leak detector is not included in the other PRs.

(Transaction leak detection and prevention, if really required, has to be done in application code, for example in a ServletFilter - See https://github.com/ebean-orm/ebean/blob/master/src/test/java/io/ebean/BaseTestCase.java#L43 to get an idea)

Closing without merge...

@rPraml rPraml closed this May 18, 2019
@rPraml rPraml deleted the fix-memory-leak branch November 22, 2019 13:40
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.

3 participants