fix: make shared jenv thread-safe
A new copy is created for each request, but cache is shared.
This commit is contained in:
parent
2c2ec13874
commit
829062b1e3
2 changed files with 40 additions and 8 deletions
|
|
@ -2,6 +2,7 @@ import types
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests import IntegrationTestCase
|
from frappe.tests import IntegrationTestCase
|
||||||
|
from frappe.utils.jinja import get_jenv
|
||||||
from frappe.utils.safe_exec import ServerScriptNotEnabled, get_safe_globals, safe_exec
|
from frappe.utils.safe_exec import ServerScriptNotEnabled, get_safe_globals, safe_exec
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -127,3 +128,17 @@ class TestSafeExec(IntegrationTestCase):
|
||||||
class TestNoSafeExec(IntegrationTestCase):
|
class TestNoSafeExec(IntegrationTestCase):
|
||||||
def test_safe_exec_disabled_by_default(self):
|
def test_safe_exec_disabled_by_default(self):
|
||||||
self.assertRaises(ServerScriptNotEnabled, safe_exec, "pass")
|
self.assertRaises(ServerScriptNotEnabled, safe_exec, "pass")
|
||||||
|
|
||||||
|
|
||||||
|
class TestJinjaGlobals(IntegrationTestCase):
|
||||||
|
def test_jenv_thread_safety(self):
|
||||||
|
first = get_jenv()
|
||||||
|
# reinit to create a new local ctx, this "simulates" two request running in two diff
|
||||||
|
# thread.
|
||||||
|
frappe.init(frappe.local.site, force=True)
|
||||||
|
second = get_jenv()
|
||||||
|
self.assertIsNot(first, second)
|
||||||
|
self.assertIsNot(first.globals, second.globals)
|
||||||
|
self.assertIsNot(first.filters, second.filters)
|
||||||
|
self.assertIsNot(first.globals["frappe"], second.globals["frappe"])
|
||||||
|
self.assertIsNot(first.globals["frappe"]["form_dict"], second.globals["frappe"]["form_dict"])
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,39 @@ def get_jenv():
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils.safe_exec import get_safe_globals
|
from frappe.utils.safe_exec import get_safe_globals
|
||||||
|
|
||||||
jenv = _get_jenv()
|
if jenv := getattr(frappe.local, "jenv", None):
|
||||||
jenv.globals.update(get_safe_globals()) # TODO: This isn't thread safe.
|
return jenv
|
||||||
|
|
||||||
|
default_jenv = _get_jenv()
|
||||||
|
jenv = default_jenv.overlay()
|
||||||
|
# XXX: This is safe to share between requests, the only reason why we are overlaying jenv is to
|
||||||
|
# reuse cache but still have request specific jenv object.
|
||||||
|
if not frappe._dev_server:
|
||||||
|
jenv.cache = default_jenv.cache
|
||||||
|
|
||||||
|
jenv.globals = default_jenv.globals.copy()
|
||||||
|
jenv.filters = default_jenv.filters.copy()
|
||||||
|
|
||||||
|
jenv.globals.update(get_safe_globals())
|
||||||
|
methods, filters = get_jinja_hooks()
|
||||||
|
jenv.globals.update(methods or {})
|
||||||
|
jenv.filters.update(filters or {})
|
||||||
|
|
||||||
frappe.local.jenv = jenv
|
frappe.local.jenv = jenv
|
||||||
|
|
||||||
return jenv
|
return jenv
|
||||||
|
|
||||||
|
|
||||||
@site_cache(ttl=10 * 60, maxsize=4)
|
@site_cache(ttl=10 * 60, maxsize=4)
|
||||||
def _get_jenv():
|
def _get_jenv():
|
||||||
|
# XXX: DO NOT use any thread/request specific data in this function!
|
||||||
|
# Some functionality like `get_safe_globals` appears safe but internally uses request local
|
||||||
|
# data.
|
||||||
|
|
||||||
from jinja2 import DebugUndefined
|
from jinja2 import DebugUndefined
|
||||||
from jinja2.sandbox import SandboxedEnvironment
|
from jinja2.sandbox import SandboxedEnvironment
|
||||||
|
|
||||||
from frappe.utils.safe_exec import UNSAFE_ATTRIBUTES, get_safe_globals
|
from frappe.utils.safe_exec import UNSAFE_ATTRIBUTES
|
||||||
|
|
||||||
UNSAFE_ATTRIBUTES = UNSAFE_ATTRIBUTES - {"format", "format_map"}
|
UNSAFE_ATTRIBUTES = UNSAFE_ATTRIBUTES - {"format", "format_map"}
|
||||||
|
|
||||||
|
|
@ -31,13 +52,9 @@ def _get_jenv():
|
||||||
return super().is_safe_attribute(obj, attr, *args, **kwargs)
|
return super().is_safe_attribute(obj, attr, *args, **kwargs)
|
||||||
|
|
||||||
# frappe will be loaded last, so app templates will get precedence
|
# frappe will be loaded last, so app templates will get precedence
|
||||||
jenv = FrappeSandboxedEnvironment(loader=get_jloader(), undefined=DebugUndefined)
|
jenv = FrappeSandboxedEnvironment(loader=get_jloader(), undefined=DebugUndefined, cache_size=32)
|
||||||
set_filters(jenv)
|
set_filters(jenv)
|
||||||
|
|
||||||
methods, filters = get_jinja_hooks()
|
|
||||||
jenv.globals.update(methods or {})
|
|
||||||
jenv.filters.update(filters or {})
|
|
||||||
|
|
||||||
return jenv
|
return jenv
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue