spulec / freezegun

Let your Python tests travel through time
Apache License 2.0
4.04k stars 265 forks source link

"IndexError: list index out of range" when accessing ignore_lists or freeze_factories from thread #345

Open alex-kowalczyk opened 4 years ago

alex-kowalczyk commented 4 years ago

When fake_time or similar patched thing is somehow passed (eg via queue) to some thread, it might be later called there. In the meanwhile the patching might have been removed (e.g main thread exited with freeze_time() block). This resuls in ignore_lists or freeze_factories being empty.

This bugs hits logging background transports heavily.

Quick and dirty solution would be to check for presence of the element [-1] before accessing it.

This results in an exception like these:


    self.run()
  File "/opt/python3.7/lib/python3.7/threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
  File "/env/lib/python3.7/site-packages/google/cloud/logging/handlers/transports/background_thread.py", line 141, in _thread_main
    max_latency=self._max_latency,
  File "/env/lib/python3.7/site-packages/google/cloud/logging/handlers/transports/background_thread.py", line 67, in _get_many
    elapsed = time.time() - start
  File "/env/lib/python3.7/site-packages/freezegun/api.py", line 173, in fake_time
    if _should_use_real_time():
  File "/env/lib/python3.7/site-packages/freezegun/api.py", line 151, in _should_use_real_time
    if not ignore_lists[-1]:
IndexError: list index out of range" ```

Or: 
``` File "/usr/lib/python3.7/socketserver.py", line 650, in process_request_thread
    self.finish_request(request, client_address)
  File "/usr/lib/python3.7/socketserver.py", line 360, in finish_request
    self.RequestHandlerClass(request, client_address, self)
  File "/usr/lib/python3.7/socketserver.py", line 720, in __init__
    self.handle()
  File "site-packages/django/core/servers/basehttp.py", line 155, in handle
    handler.run(self.server.get_app())
  File "/usr/lib/python3.7/wsgiref/handlers.py", line 148, in run
    self.close()
  File "/usr/lib/python3.7/wsgiref/simple_server.py", line 35, in close
    self.status.split(' ',1)[0], self.bytes_sent
  File "/usr/lib/python3.7/http/server.py", line 544, in log_request
    self.requestline, str(code), str(size))
  File "site-packages/django/core/servers/basehttp.py", line 99, in log_message
    'server_time': self.log_date_time_string(),
  File "/usr/lib/python3.7/http/server.py", line 594, in log_date_time_string
    now = time.time()
  File "site-packages/freezegun/api.py", line 175, in fake_time
    current_time = get_current_time()
  File "site-packages/freezegun/api.py", line 169, in get_current_time```
spulec commented 4 years ago

Can you confirm which version of freezegun you are using?

By default, moto should be not running when it sees threading in the stacktrace: https://github.com/spulec/freezegun/commit/270e3cae8216878d4c83d3d39001e37ff457649d

See https://github.com/spulec/freezegun/issues/307 for why that is also an issue.

alex-kowalczyk commented 4 years ago

0.3.12 , so it includes the commit. The issue is not with ignoring threading itself. The problem occurs when the thread still runs and uses time functions (eg time.time()) after the main thread has been unfrozen (exited with block). time.time() checks if it should be ignored while the ignore_lists is already removed.

spulec commented 4 years ago

Ah, I see.

If we just check for the presence of ignore_lists[-1] before accessing it, I believe the code will still break later on. We could return True is ignore_lists is empty. Is that the behavior we want though? As a user, would you expect this thread to be using the real time or the fake time at this point?

alex-kowalczyk commented 4 years ago

Ideally the threads spawned in frozen-time should be frozen, to make this predictable.

lig commented 4 years ago

Any workaround for this? It really hurts.

deeTEEcee commented 3 years ago

I hit the same issue and after upgrading to 1.1.0, it was fixed.

PhilMarsh commented 7 months ago

This is a simple multi-threading bug caused by not locking around accesses of the global freezegun.api.ignore_lists. For example:

It doesn't actually matter if we've correctly configured to ignore the affected module. The attempt to check for ignored modules is failing before we can make a decision.


One option is to add locking around the most of _should_use_real_time() and the .append() and .pop() calls. As long as we're only freezing time for testing purposes, any lock contention should be negligible.

Another option is to fix this locklessly with an EAFP approach. At the top of _should_use_real_time(), try to grab ignore_lists[-1]. If it raises and IndexError, then we're not in a frozen context, so return True. Else, hold onto the value we just got and use it for the rest of the function. This would look something like:

def _should_use_real_time():
    ...
    try:
        modules_to_ignore = ignore_lists[-1]
    except IndexError:
        # Means stop() has already been called, so we can now return the real time
        return True

    for ...:
        ...
        if ... module_name.startswith(modules_to_ignore):
            return True
    ...

This avoid a lock, but the modules_to_ignore value could become stale while using it. Again, this shouldn't be a big deal if we're only testing.


Also, this problem exists for all of the mutable globals: https://github.com/spulec/freezegun/blob/1af533984e118f5fe58d3264a52c904737df7d77/freezegun/api.py#L60-L63

Some of these look like they need a larger refactor to make threadsafe.