Will a ContextVar leak memory in async logic if not reset after Exception?

Viewed 709

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

1 Answers

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.

Related