Skip to content

Adding Close selector to prevent FD/FIFO leaks #3707

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

Closed
wants to merge 12 commits into from

Conversation

shvo123
Copy link
Contributor

@shvo123 shvo123 commented Jan 2, 2022

following File Descriptor leak issue #3705, we added slector.close() and this leak stopped. Closing/destroying ChannelOutputStream object does not close the selector therefore it retains redundant pipes/FD that cen be seen using lsof command or ls /proc/

following File Descriptor leak issue spring-projects#3705, we added slector.close() and this leak stopped. Closing/destroying ChannelOutputStream object does not close the selector therefore it retains redundant pipes/FD that cen be seen using lsof command or ls /proc/
set selector to null after closing it
@@ -662,6 +662,8 @@ protected synchronized void doWrite(ByteBuffer buffer) throws IOException {
TcpNioConnection.this.socketChannel.write(buffer);
remaining = buffer.remaining();
}
selector.close();
selector = null;
Copy link
Member

Choose a reason for hiding this comment

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

And why do we need to close selector after each write?
You said yourself in the PR description:

Closing/destroying ChannelOutputStream object does not close the selector

I don't see that you close a ChannelOutputStream in this write operation, so I believe it is done somehow externally. Therefore that ChannelOutputStream.close() is called from there.
So, it still looks suspicious to always close a selector in the end of write, but not once when ChannelOutputStream is closed.

What do I miss, please?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK I see your point it is better to move selector.close() to ChannelOutputStream.close()
And I would like to ask you if you feel comfortable with:
ChannelOutputStream.close() eventually calls doClose() that eventually calls super.close() that has the following code:
for (TcpSender sender : this.senders) { sender.removeDeadConnection(this);

This removeDeadConnection does nothing it has empty implementation, I feel (maybe wrong) that it may cause Out Of Memory in one of the maps in my case in TcpSendingMessageHandler in map connections.

Copy link
Member

Choose a reason for hiding this comment

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

I think that is not related to what we are talking here about.
I guess @garyrussell is looking into that via #3701 .

Let's concentrate here on the proper selector closing!

Make sure selector is closed to prevent FD leaks.
Close ChannelOutputStream and its selector to prevent leaks
@shvo123
Copy link
Contributor Author

shvo123 commented Jan 3, 2022

Hi Artem, I added to doClose() close try { this.channelOutputStream.close(); } catch (@SuppressWarnings(UNUSED) Exception e) { }
not sure it is required what do you think ?

} catch (IOException e) {
// do nothing
}
}
doClose();
Copy link
Member

Choose a reason for hiding this comment

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

I don't think such an implementation is OK.
This doClose() is exactly that one in the outer class, so this close() is going to be called again.
And so on in recursive manner.

I'm not sure what was an original intention with such a delegation, but we need to be sure that we don't do a recursion.

Could you, please, double check the solution?
Also the catch block must be on a new line.
Be sure run \gradlew clean :spring-integration-ip:check before pushing the change to the PR.

Copy link
Contributor

@garyrussell garyrussell Jan 3, 2022

Choose a reason for hiding this comment

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

I am not sure what's going on, but GitHub is not rendering the changes from all 5 commits; I can only see this change.

It looks like a bug that we never closed the output stream; we would have hit the stack overflow problem if we did; day one issue, I think.

I believe we can just remove that doClose() call here.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, and since the output close() is never called from the outside, it is safe to have it call it from connection close.
And yes: still keep a selector.close() 😄

Make sure selector is closed to prevent FD/pipes leak
Copy link
Member

@artembilan artembilan left a comment

Choose a reason for hiding this comment

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

The current code is probably OK so far in terms of context responsibility.
But I still wonder how and when this ChannelOutputStream.close() is called?

Your previous version was very close to the truth. We only needed to figure out what to do with the recursion.

I've just ran the whole test suite for ip module and no one calls this ChannelOutputStream.close(). Therefore your current fix does not bring us a desired final solution for original OOM issue...

/CC @garyrussell

@garyrussell
Copy link
Contributor

Full diff now:

diff --git a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioConnection.java b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioConnection.java
index 1d1844c08f..4c529ba166 100644
--- a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioConnection.java
+++ b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioConnection.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -139,6 +139,11 @@ public class TcpNioConnection extends TcpConnectionSupport {
 		}
 		catch (@SuppressWarnings(UNUSED) Exception e) {
 		}
+		try {
+			this.channelOutputStream.close();
+		}
+		catch (@SuppressWarnings(UNUSED) Exception e) {
+		}
 		super.close();
 	}
 
@@ -620,6 +625,13 @@ public class TcpNioConnection extends TcpConnectionSupport {
 
 		@Override
 		public void close() {
+			if (selector != null) {
+				try {
+					selector.close();
+				} catch (IOException e) {
+					// do nothing
+				}
+			}
 			doClose();
 		}
 
@@ -663,7 +675,6 @@ public class TcpNioConnection extends TcpConnectionSupport {
 				remaining = buffer.remaining();
 			}
 		}
-
 	}
 
 	/**

Not sure why GH is messed up.

} catch (IOException e) {
// do nothing
}
}
doClose();
Copy link
Member

Choose a reason for hiding this comment

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

Yes, and since the output close() is never called from the outside, it is safe to have it call it from connection close.
And yes: still keep a selector.close() 😄

Make sure OutputStream is closed to avoid FD leak
@shvo123
Copy link
Contributor Author

shvo123 commented Jan 3, 2022

See last commit. I made sure no recursive call would take place.

@shvo123
Copy link
Contributor Author

shvo123 commented Jan 3, 2022

I will try tomorrow to add unit test for ChannelOutputStream.close()

try {
// in order to prevent endless recursive calls from channelOutputStream.close() to doClose() that would call channelOutputStream.close() etc...
if (this.channelOutputStream != null) {
((OutputStream) this.channelOutputStream).close();
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand how does your code help?
The this.channelOutputStream is never null according TcpNioConnection and you cannot set it to null (and you don't) because it is final.

And it is pointless to cast it to OutputStream since it is a super of the ChannelOutputStream.

Our idea was really to call this close here, but remove that doClose() from the ChannelOutputStream.

At the moment I'm very confused since the solution does not reflect your comments and does not address our discussion.

Please, elaborate what am I still missing?

Thanks

Copy link
Contributor Author

@shvo123 shvo123 Jan 4, 2022

Choose a reason for hiding this comment

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

I took a look again in the code the source of the trouble is that doClose() does not call ChannelOutputStream.close(), and that the ChannelOutputStream.close() itself is erronous:
that is:
@Override public void close() { doClose(); }

doClose is not part of class ChannelOutputStream and what it does has nothing to do with ChannelOutputStream, it nor handle the selector that is private member of ChannelOutputStream neither closes the OutputStream itself.
So I think that the best approach would be to rewrite the ChannelOutputStream.close() to:
@Override public void close() { if (this.selector != null) { try { this.selector.close(); } catch (IOException e) { // do nothing } } super.close(); }
This is more similar to ChannelInputStream.close()
In addition I added ChannelOutputStream.close() to doClose().

In addition I took the liberty to write real impelmentation of ChannelOutputStream.flush()`.

Attached diff from the base source.

10x,
diff.txt

David

following File Descriptor leak issue spring-projects#3705, we added slector.close() and this leak stopped. Closing/destroying ChannelOutputStream object does not close the selector therefore it retains redundant pipes/FD that cen be seen using lsof command or ls /proc/
prevent FD leak by closing ChannelOutputStream and its selector. Rewriting doClose, ChannelOutputStream.close() and ChannelOutputStream.flush()
style correction
}
}
try {
super.close();
Copy link
Member

Choose a reason for hiding this comment

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

No reason to call super: it is just an empty method.
That's why we didn't delegate to it originally anyway.

}

@Override
public void flush() {
try {
super.flush();
Copy link
Member

Choose a reason for hiding this comment

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

No reason to call super: it is just an empty method.

I think we can just remove this method altogether.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no problem will not override flush at all

try {
this.selector.close();
}
catch (@SuppressWarnings(UNUSED) Exception e) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think it is this method responsibility to suppress an exception.
Let it be done in the TcpNioConnection.close() instead!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok

Copy link
Member

Choose a reason for hiding this comment

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

I thought that we agreed in the previous discussion that this catch is going to be removed...

@@ -663,7 +684,6 @@ protected synchronized void doWrite(ByteBuffer buffer) throws IOException {
remaining = buffer.remaining();
}
}

Copy link
Member

Choose a reason for hiding this comment

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

This is wrong: every member (even last method) of the class has to be surrounded with blank lines.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not sure where this empty line is missing

Copy link
Member

Choose a reason for hiding this comment

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

Just bring it back and push!
It is not a problem though: we can clean it up on merge.
Just letting you to know what is our code style: https://github.com/spring-projects/spring-integration/wiki/Spring-Integration-Framework-Code-Style !

diff.txt Outdated
@@ -0,0 +1,51 @@
diff --git a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioConnection.java b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioConnection.java
Copy link
Member

Choose a reason for hiding this comment

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

This file must not be here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

how can i remove it?

Copy link
Member

Choose a reason for hiding this comment

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

It must be somehow in your local project copy.
So, just remove it and push commit.

Removing flush, ChannelOutputStream.close() throws exception
try {
this.selector.close();
}
catch (@SuppressWarnings(UNUSED) Exception e) {
Copy link
Member

Choose a reason for hiding this comment

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

I thought that we agreed in the previous discussion that this catch is going to be removed...

@shvo123
Copy link
Contributor Author

shvo123 commented Jan 4, 2022

Quoting Artem, "I thought that we agreed in the previous discussion that this catch is going to be removed... "
I thought we are talking about super.close(), and I removed its catch clause accordingly. do not mind to remove catch clause of selector as well.
let me know about missing empty line location and then I would push it.

Remove redundant catch clause and redundant file
Copy link
Member

@artembilan artembilan left a comment

Choose a reason for hiding this comment

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

It is OK with me so far.

Would you mind to add your name to the @author list of the affected class?
Also: any chances to cover this functionality with with some simple unit test?
I believe something like mocking with verification that ChannelOutputStream.close() is called when we call TcpNioConnection.close() should be enough.
Plus, confirm, please, that this change really fixes OOM problem for your project.

@shvo123
Copy link
Contributor Author

shvo123 commented Jan 4, 2022

Would you mind to add your name to the @author list of the affected class?
like that?

Also: any chances to cover this functionality with with some simple unit test?
I believe something like mocking with verification that ChannelOutputStream.close() is called when we call TcpNioConnection.close() should be enough.
Would try to add it to TcpNioConnectionWriteTests is it suitable?

Plus, confirm, please, that this change really fixes OOM problem for your project.

@artembilan
Copy link
Member

Just add a new line @author David Herschler Shvo after all those.
You don't change @since because it is not a new class.

Yes, that TcpNioConnectionWriteTests looks like a good candidate indeed.

Probably something like ccf.getConnection() and call its close() should be enough and then you use TestUtils.getProperty(connection, 'channelOutputStream.selector', Selector.class) and check its status via isOpen(). Must be false. But not before this fix.

… failed

Make sure internal output stream would close even if selector.close() failed by putting the outputStream.close(0 in finally clause
Copy link
Member

@artembilan artembilan left a comment

Choose a reason for hiding this comment

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

I think that's it.
Taking this locally for final review and clean up.
Will try to add some testing as well...

@shvo123
Copy link
Contributor Author

shvo123 commented Jan 5, 2022

10x

@artembilan
Copy link
Member

Merged as and cherry-picked to 5.4.x & 5.3.x as 0b38b8d.

Sorry I couldn't figured out a simple test for this Selector use-case, so decided to merge it as is since the fix is fully straightforward with simple close() delegation which was missed before.

@shvo123 , thank you for contribution; looking forward for more!

@artembilan artembilan closed this Jan 5, 2022
@shvo123
Copy link
Contributor Author

shvo123 commented Jan 6, 2022 via email

@artembilan
Copy link
Member

Hi @shvo123 !

is to write a big buffer of 260K

Yes, I tried to do that, but it blocks when it tries to be selected.
Probably I miss something on the server side, because I believe the channel has to be released somehow for the selector.

If you can come up with some unit-test on the matter, I would appreciate your contribution.

Thanks

@shvo123
Copy link
Contributor Author

shvo123 commented Jan 20, 2022

Hi Artem,
We tested the corrections made by Gary and they are OK.
The fix to close the selectors is also OK.

Can you tell me when formal release would be released so we can download it from maven repository instead of using our customized Spring integration.
10x,
David

@garyrussell
Copy link
Contributor

5.5.8 (and the other supported branches) were released this week https://github.com/spring-projects/spring-integration/releases

@shvo123
Copy link
Contributor Author

shvo123 commented Jan 20, 2022 via email

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