4

If I have a structure in an async webserver like

import contextvars
...
my_context_var = contextvars.ContextVar("var")

@app.route("/foo")  # decorator from webserver
async def some_web_endpoint():
    local_ctx_var = my_context_var.set(params.get("bar"))  # app sets params
    await some_function_that_can_raise()
    local_ctx_var.reset()

Will it leak memory if I don't wrap the ContextVar in a finally: block and some_function_that_can_raise() raises an Exception?
(without such a case, .reset() would never be called)

    try:
        await some_function_that_can_raise()
    finally:
        local_ctx_var.reset()

.. or is it safe to assume the value will be destroyed when the request scope ends?
The async example in the upstream docs doesn't actually bother .reset()-ing it at all!
In such a case, .reset() is redundant as it happens right before the context is cleaned up anyways.


To add some more context (ha), I'm recently learning about ContextVars and I assume the second is the case.

local_ctx_var is the only name which refers to the Token (from .set()), and as the name is deleted when the request scope ends, the local value should become candidate for garbage collection, preventing a potential leak and making .reset() unnecessary for short-lived scopes (hooray)

..but I'm not absolutely certain, and while there's some very extremely helpful information on the subject, it muddles the mixture slightly

ti7
  • 16,375
  • 6
  • 40
  • 68
  • I'm a little surprised `contextvars.Token` doesn't support the context manager protocol - I would have expected the standard idiom for this to be `with my_context_var.set(params.get("bar")): ...`. – user2357112 Apr 12 '21 at 18:42
  • @user2357112supportsMonica Me too - for example [loguru's `.contextualize`](https://loguru.readthedocs.io/en/stable/api/logger.html#loguru._logger.Logger.contextualize) makes use of `ContextVar`s in such a way! Maybe it would make a good PEP? ..though that it's not seems like a good indicator that any leftovers will eventually be cleaned up without `.reset()` – ti7 Apr 12 '21 at 19:12

1 Answers1

4

Yes - the previous value of the context_var is kept in the token object in this case. There is this rather similar question, where one of the answers run a simple benchmark to assert that calling context_var.set() multiple times and discarding the return value does not consume memory, when compared to, say, create a new string and keeping a reference to it.

Given the benchmark, I made some further experimentation and concluded there is no leak - in fact, in code like the above, calling reset is indeed redundant - it is useful if you'd have to restore the previous value inside a loop construct for some reason.

The new var is set, on top of the last saved context, the value set in the current version of the context is simply discarded along the way: the only references to it are the one left in the tokens, if any. In ohtther words: what preserves the previous values in a "stack like" way are calls to contextvars.run and contextvars.copy_context only, not Contextvar.set.

jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • Excellent - this mostly confirms my suspicions, but also seems unfinished? I'll see if I can bring a good async test, which is what I'm really interested in beyond what's in that post, but I imagine it'll behave similarly (ContextVar becomes a candidate for GC after request finishes along with its other deleted names) – ti7 Apr 16 '21 at 18:32
  • Great. I have a personal project I started a little before contextvars was launched - I use another approach to keeping the contexts (I keep contextualized values in the execution frame locals vars) - it is not published, but the main class there `ContextLocals` works like a charm: https://github.com/jsbueno/extracontext/blob/master/tests/test_async.py - if you find that useful, do not hesitate to create issues so we can do a usable release. – jsbueno Apr 16 '21 at 19:12