-
Notifications
You must be signed in to change notification settings - Fork 5
Close all and counts #13
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
Conversation
@@ -24,14 +24,38 @@ | |||
|
|||
# get request session | |||
requests = adafruit_requests.Session(pool, ssl_context) | |||
connection_manager = adafruit_connection_manager.get_connection_manager(pool) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The output of this is so:
----------------------------------------
Nothing yet opened
Open Sockets: 0
Freeable Open Sockets: 0
----------------------------------------
Fetching from http://wifitest.adafruit.com/testwifi/index.html in a context handler
Text Response This is a test of Adafruit WiFi!
If you can read this, its working :)
----------------------------------------
1 request, opened and freed
Open Sockets: 1
Freeable Open Sockets: 1
----------------------------------------
Fetching from http://wifitest.adafruit.com/testwifi/index.html not in a context handler
----------------------------------------
1 request, opened but not freed
Open Sockets: 1
Freeable Open Sockets: 0
----------------------------------------
Closing everything in the pool
----------------------------------------
Everything closed
Open Sockets: 0
Freeable Open Sockets: 0
@RetiredWizard is this what you are looking for? Instead of calling:
you would call:
|
The new call feels much better than utilizing the private function call. I'll try it with my code when I get a chance although I'm sure you tested against my test code. 😁 |
This also specifically closes any open sockets and frees up as much as it can. |
@anecdata would enjoy your feedback here. And if you think there's more |
@DJDevon3, I updated the example to show counts (made public methods for getting them) for using |
@RetiredWizard let me know what your testing finds! |
I'll try to set up some testing later tonight.... |
So I've been poking around trying to figure out where this is coming from but after changing my reset code block as follows: adafruit_connection_manager.connection_manager_close_all()
radio.disconnect()
radio = None
esp32_cs.deinit()
esp32_ready.deinit()
esp32_reset.deinit()
spi.deinit()
requests = None
pool = None The second attempt to make a connection/request results in the following:
I've traced it to the esp32_ready pin. It appears that adafruit_esp32spi is attempting to access the original esp32_ready object that was deinit in the reset block rather than the new esp32_ready object created in the second connect/request block. I'm trying to figure out how the original instance is being retained but don't really know how connection manager is working well enough yet. The line causing the error is the last line of the test code snippet: response = requests.get(text_url,headers=None,timeout=15000) |
Actually, given the debug print statements in my test code, I'm thinking that I was having a similar issue with esp32_ready not being a valid object for the second requests.get call when I originally found the issue and before finding the |
Hmmm, so it seems like we may need 2 options:
I think the hard part on 2 is that if you haven't fully released everything and still have one of those objects referenced somewhere that it will cause problems... @dhalbert do you have thoughts? Should CM assume you have de-inited everything if you call this? And if you didn't, create new pools SSL contexts and everything? |
I'm still just scratching at the surface, but the old method """Get a new socket and connect"""
if session_id:
session_id = str(session_id)
key = (host, port, proto, session_id)
if key in self._open_sockets:
socket = self._open_sockets[key]
if self._available_socket[socket]:
self._available_socket[socket] = False
return socket This code from get_socket looks to me like if I try and open a new socket for the same URL it will simply reopen the old socket since the _free_sockets method didn't actually clear the data, only flag the socket as being closed/free. Edit: Maybe that's the distinction between 1 and 2 you were talking about above.... |
I don't think close/deinit gets used very often, if someone does call a close, I think it's reasonable to expect them to re-initialize the associated resources if they need to reopen. I guess I don't do enough WiFi to understand what advantage there would be in retaining the radio, pool and SSL after a close. |
@RetiredWizard so I think adding an option to release references to the radio, pool and SSL make sense. My worry would be that people do it without realizing what is happening and thus keep creating new things that aren't fully de-inited and use up all the memory on having 100 pools... I'll add another optional param and let you know when it's ready for re-test |
@RetiredWizard I added another option. This should work: adafruit_connection_manager.connection_manager_close_all(release_references=True) This will close sockets and clear out: adafruit_connection_manager._global_connection_managers
adafruit_connection_manager._global_ssl_contexts
adafruit_connection_manager._global_socketpools So as long as you've fully release the objects with you: radio.disconnect()
radio = None
esp32_cs.deinit()
esp32_ready.deinit()
esp32_reset.deinit()
spi.deinit() before trying to add it again, you should be good. |
Seems to work great 😁 Thanks! I did add the following block before calling the cloase_all: if self.response:
self.response.close()
self.response = None But that's probably not connection_manager's responsibility. Maybe a note in the API documentation to close out any returned requests before shutting down the connection_manager. but then that may be getting too application specific for API docs.... Edit: I tested on an ESP32SPI (Arduino Nano RP2040 Connect), PicoW and an ESP32S3 |
@RetiredWizard awesome!
So yes, if you pass a response around and the socket has been closed and not read, you'll get errors. But if one is intentionally trying to close everything, hopefully they would guard against that. Thank you for all your testing! |
@dhalbert would love a review when you have a chance! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, several questions and suggestions.
@dhalbert updated per our conversation! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the extended conversation! One suggestion. I think it is clearer now, and would be interested in comments from others as well.
@RetiredWizard we'd appreciate your comments as well. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked through the code and it looks good to me, but I don't really have a strong understanding of how pools/sockets work so I'm not sure how much value that is for you. I did make a suggestion regarding the retry of the _get_connected_socket call but it's not critical and would probably only have any value for esp32SPI boards.
adafruit_connection_manager.py
Outdated
addr_info, host, port, timeout, is_ssl, ssl_context | ||
) | ||
if isinstance(result, Exception): | ||
raise RuntimeError(f"Error connecting socket: {result}") from result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block is a lot cleaner. I still worry about tossing the first error though, in my experience the first error is often the most important. What do you think about saving the result of the first _get_connect_socket call if it's an exception and then if the second call also returns an exception, printing the result before actually raising the error. Something like:
if isinstance(result, Exception):
if result_sav is not None:
print(f"Error connecting socket: {sys.exception(result_sav)}\nTrying again")
raise RuntimeError(f"Error connecting socket: {result}") from result
This fakes the timing of events a bit since both errors would be displayed after both attempts fail, but has the advantage of keeping the output clean if the second attempt succeeds.
Edit: Actually if the results is an exception at the end then the first attempt must have failed so you wouldn't need to do the result_sav is not None
test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@RetiredWizard this is tough. I wouldn't want a core library printing. I think with a little thought, we could flag a debug mode that prints them all (maybe with a connection_manager_set_debug(socket)
. I also have a fear on too much bloat...
What about:
last_result = ""
result = self._get_connected_socket(
addr_info, host, port, timeout, is_ssl, ssl_context
)
if isinstance(result, Exception):
# Got an error, if there are any available sockets, free them and try again
if self.available_socket_count:
last_result = f", first error: {result}"
self._free_sockets()
result = self._get_connected_socket(
addr_info, host, port, timeout, is_ssl, ssl_context
)
if isinstance(result, Exception):
raise RuntimeError(f"Error connecting socket: {result}{last_result}") from result
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would update this test:
def test_get_socket_runtime_error_ties_again_only_once():
mock_pool = mocket.MocketPool()
mock_socket_1 = mocket.Mocket()
mock_socket_2 = mocket.Mocket()
mock_pool.socket.side_effect = [
mock_socket_1,
RuntimeError("error 1"),
RuntimeError("error 2"),
RuntimeError("error 3"),
mock_socket_2,
]
free_sockets_mock = mock.Mock()
connection_manager = adafruit_connection_manager.ConnectionManager(mock_pool)
connection_manager._free_sockets = free_sockets_mock
# get a socket and then mark as free
socket = connection_manager.get_socket(mocket.MOCK_HOST_1, 80, "http:")
assert socket == mock_socket_1
connection_manager.free_socket(socket)
free_sockets_mock.assert_not_called()
# try to get a socket that returns a RuntimeError twice
with pytest.raises(RuntimeError) as context:
connection_manager.get_socket(mocket.MOCK_HOST_2, 80, "http:")
assert "Error connecting socket: error 2, first error: error 1" in str(context)
free_sockets_mock.assert_called_once()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea If that does what it looks like it does, that's great. I also see the argument to drop for the sake of bloat though. Some of the WiFi boards have really thin available memory and a few bytes can make a big difference. I'll let you make the call 😁
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dhalbert are you good with this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seems ok. You can save just save the exception (first_exception
) and then only make a formatted string in the second if
.
Yes, reducing code size is good wherever you can do it for frozen libraries.
@@ -64,7 +64,7 @@ def connect(self, address: Tuple[str, int]) -> None: | |||
try: | |||
return self._socket.connect(address, self._mode) | |||
except RuntimeError as error: | |||
raise OSError(errno.ENOMEM) from error | |||
raise OSError(errno.ENOMEM, str(error)) from error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for ESP32SPI, a little more information
@dhalbert and @RetiredWizard ready for the last (I hope) review! |
Looks good to me... |
@RetiredWizard does this close out #14 as well? |
Yes is does, thanks! |
@dhalbert what are the next steps? |
Updating https://github.com/adafruit/Adafruit_CircuitPython_Wiznet5k to 6.0.0 from 5.2.1: > Merge pull request adafruit/Adafruit_CircuitPython_Wiznet5k#150 from pinkavaj/pi-fix-sock-exit > Merge pull request adafruit/Adafruit_CircuitPython_Wiznet5k#160 from justmobilize/remove-wsgiserver Updating https://github.com/adafruit/Adafruit_CircuitPython_ConnectionManager to 1.2.0 from 1.1.0: > Merge pull request adafruit/Adafruit_CircuitPython_ConnectionManager#13 from justmobilize/close-all-and-counts Updating https://github.com/adafruit/Adafruit_CircuitPython_Requests to 3.2.6 from 3.2.5: > Merge pull request adafruit/Adafruit_CircuitPython_Requests#188 from justmobilize/with-updates Updating https://github.com/adafruit/Adafruit_CircuitPython_Bundle/circuitpython_library_list.md to NA from NA: > Updated download stats for the libraries
open_sockets
andfreeable_open_sockets
as informational.connection_manager_close_all
which will close out all open sockets to free resourcesfixes #7