python-eel / Eel

A little Python library for making simple Electron-like HTML/JS GUI apps
MIT License
6.38k stars 583 forks source link

Memory leak: sometimes the return ws message arrives before the callback is registered, leaving it registered forever. #540

Open nosachamos opened 2 years ago

nosachamos commented 2 years ago

Eel version 1.40

Describe the bug

When a call is made from Python to JS, the return in the python side is a function that must be called to be obtain the result. If a callback is provided, that callback is placed in the _call_return_callbacks dictionary. Then, then the return websocket message returns from the JS side, that callback is found on this dict and it is removed and invoked. All good.

The problem is that between the call msg being sent, and the callback be registered, the websocket return message may arrive. This happens from time to time since its all running locally and is pretty fast, and when this happens the callback is registered after the fact and stays there forever.

Essentially, this code runs first:

    ...
    elif 'return' in message:
        call_id = message['return']
        if call_id in _call_return_callbacks:     # <===  This can happen very fast, before the call back is registered
            callback, error_callback = _call_return_callbacks.pop(call_id)
            if message['status'] == 'ok':
                callback(message['value'])
            elif message['status'] == 'error' and error_callback is not None:
                error_callback(message['error'], message['stack'])
        else:
            _call_return_values[call_id] = message['value'] 

Then this one:

def _call_return(call):
    global _js_result_timeout
    call_id = call['call']

    def return_func(callback=None, error_callback=None):
        if callback is not None:
            _call_return_callbacks[call_id] = (callback, error_callback)    # <<==== registered too late
        else:
            for w in range(_js_result_timeout):
                if call_id in _call_return_values:
                    return _call_return_values.pop(call_id)
                sleep(0.001)

    return return_func

As a result, both the return value is left in _call_return_values and the callback is left in the _call_return_callbacks dict indefinitely.

To Reproduce Do lots of very quick calls from Python to JS, and see that the number of keys in the two mentioned dicts increases steadily over time. Place some prints in the two locations I mentioned and see that they happen out of order sometimes.

nosachamos commented 2 years ago

Here is an example for illustration:

image

After several thousand calls, three thousand were left in the those call backs and return values:

image

In my system it happens 1 to 2 times per thousand calls from Python to JS, but since I do a lot of calls:

image

... my program dies overnight.