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

Close all and counts #13

Merged
merged 5 commits into from
Apr 28, 2024
Merged

Conversation

justmobilize
Copy link
Collaborator

@justmobilize justmobilize commented Apr 25, 2024

  • Added to properties: open_sockets and freeable_open_sockets as informational.
  • Added connection_manager_close_all which will close out all open sockets to free resources

fixes #7

@@ -24,14 +24,38 @@

# get request session
requests = adafruit_requests.Session(pool, ssl_context)
connection_manager = adafruit_connection_manager.get_connection_manager(pool)
Copy link
Collaborator Author

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

adafruit_connection_manager.py Show resolved Hide resolved
@justmobilize justmobilize marked this pull request as ready for review April 25, 2024 21:53
@justmobilize
Copy link
Collaborator Author

@RetiredWizard is this what you are looking for? Instead of calling:

adafruit_connection_manager._global_socketpool.clear()

you would call:

adafruit_connection_manager.connection_manager_close_all()

@RetiredWizard
Copy link

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. 😁

@justmobilize
Copy link
Collaborator Author

This also specifically closes any open sockets and frees up as much as it can.

@justmobilize
Copy link
Collaborator Author

@anecdata would enjoy your feedback here. And if you think there's more extra info we could give to the advanced developer

@justmobilize
Copy link
Collaborator Author

@DJDevon3, I updated the example to show counts (made public methods for getting them) for using with. Thoughts?

@justmobilize
Copy link
Collaborator Author

@RetiredWizard let me know what your testing finds!

@RetiredWizard
Copy link

I'll try to set up some testing later tonight....

@RetiredWizard
Copy link

RetiredWizard commented Apr 27, 2024

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:

Connected: False
3
10.0.0.197
Connected: False
3
10.0.0.197
esp32_ready: False
Traceback (most recent call last):
  File "<stdin>", line 51, in <module>
  File "adafruit_requests.py", line 591, in get
  File "adafruit_requests.py", line 525, in request
  File "/lib/adafruit_connection_manager.py", line 266, in get_socket
  File "adafruit_esp32spi/adafruit_esp32spi_socket.py", line 44, in getaddrinfo
  File "adafruit_esp32spi/adafruit_esp32spi.py", line 666, in get_host_by_name
  File "adafruit_esp32spi/adafruit_esp32spi.py", line 340, in _send_command_get_response
  File "adafruit_esp32spi/adafruit_esp32spi.py", line 247, in _send_command
  File "adafruit_esp32spi/adafruit_esp32spi.py", line 200, in _wait_for_ready
ValueError: Object has been deinitialized and can no longer be used. Create a new object.

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)

@RetiredWizard
Copy link

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 adafruit_connection_manager._global_socketpool.clear() method.

@justmobilize
Copy link
Collaborator Author

Hmmm, so it seems like we may need 2 options:

  1. Close everything but keep the radio, pool and SSL active
  2. Close everything AND kill and kill the tracked radio, pool and SSL.

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?

@RetiredWizard
Copy link

RetiredWizard commented Apr 27, 2024

I'm still just scratching at the surface, but the old method adafruit_connection_manager._global_socketpool.clear() worked. I'm wondering if the _free_sockets method is closing the sockets but retaining the socket data so when a closed socket is reopened it grabs all the old objects.

        """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....

@RetiredWizard
Copy link

RetiredWizard commented Apr 27, 2024

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...

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.

@justmobilize
Copy link
Collaborator Author

@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

@justmobilize
Copy link
Collaborator Author

@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.

@RetiredWizard
Copy link

RetiredWizard commented Apr 27, 2024

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

@justmobilize
Copy link
Collaborator Author

@RetiredWizard awesome! Response.close() just does two things:

  1. Closes the socket
  2. Sets the socket to None

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!

@justmobilize
Copy link
Collaborator Author

@dhalbert would love a review when you have a chance!

Copy link
Contributor

@dhalbert dhalbert left a 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.

adafruit_connection_manager.py Outdated Show resolved Hide resolved
adafruit_connection_manager.py Outdated Show resolved Hide resolved
@justmobilize
Copy link
Collaborator Author

@dhalbert updated per our conversation!

Copy link
Contributor

@dhalbert dhalbert left a 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.

adafruit_connection_manager.py Outdated Show resolved Hide resolved
@dhalbert
Copy link
Contributor

@RetiredWizard we'd appreciate your comments as well.

Copy link

@RetiredWizard RetiredWizard left a 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.

addr_info, host, port, timeout, is_ssl, ssl_context
)
if isinstance(result, Exception):
raise RuntimeError(f"Error connecting socket: {result}") from result
Copy link

@RetiredWizard RetiredWizard Apr 27, 2024

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.

Copy link
Collaborator Author

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

Copy link
Collaborator Author

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()

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 😁

Copy link
Collaborator Author

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?

Copy link
Contributor

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
Copy link
Collaborator Author

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

@justmobilize
Copy link
Collaborator Author

@dhalbert and @RetiredWizard ready for the last (I hope) review!

@RetiredWizard
Copy link

Looks good to me...

@justmobilize
Copy link
Collaborator Author

@RetiredWizard does this close out #14 as well?

@RetiredWizard
Copy link

Yes is does, thanks!

@justmobilize
Copy link
Collaborator Author

@dhalbert what are the next steps?

@dhalbert dhalbert merged commit 1531496 into adafruit:main Apr 28, 2024
1 check passed
@justmobilize justmobilize deleted the close-all-and-counts branch April 28, 2024 04:30
adafruit-adabot added a commit to adafruit/Adafruit_CircuitPython_Bundle that referenced this pull request Apr 28, 2024
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
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.

Add socket/sessions cleanup method to API
3 participants