github-actions[bot] commented on code in PR #69255:
URL: https://github.com/apache/doris/pull/69255#discussion_r4396320091


##########
be/src/udf/python/python_server.py:
##########
@@ -958,63 +937,130 @@ def load(self) -> AdaptivePythonUDF:
         return AdaptivePythonUDF(self.python_udf_meta, func)
 
 
+class UDFModuleAliasLoader(importlib.abc.Loader):
+    """Makes "pkg" the same module object as 
"__doris_pyudf_<function_id>.pkg"."""
+
+    def __init__(self, udf_module_name: str):
+        self.udf_module_name = udf_module_name
+        self.udf_module_spec = None
+
+    def create_module(self, spec) -> ModuleType:
+        module = importlib.import_module(self.udf_module_name)
+        self.udf_module_spec = module.__spec__
+        return module
+
+    def exec_module(self, module: ModuleType) -> None:
+        # module_from_spec() replaced the spec; keep it consistent with 
__name__.
+        module.__spec__ = self.udf_module_spec
+
+
+class UDFModuleFinder(importlib.abc.MetaPathFinder):
+    """
+    Loads user modules under the root package "__doris_pyudf_<function_id>", 
so a
+    user module named like a server module (e.g. types, json) does not resolve 
to
+    the server module. The name depends only on the function id, which keeps
+    pickled UDAF state classes loadable in every worker.
+
+    Absolute imports in user code look in the UDF directory first, e.g.
+    "from json import helper" in the user package json loads
+    "__doris_pyudf_<function_id>.json.helper".
+    """
+
+    def __init__(self, function_id: int, location: str):
+        self.root_package = self.root_package_name(function_id)
+        self.root_package_prefix = self.root_package + "."
+        self.location = location
+        self.original_import = builtins.__import__
+        # Top-level name -> whether the UDF directory provides it.
+        self.udf_top_level_module_cache: Dict[str, bool] = {}
+        # importlib.import_module bypasses builtins.__import__, so user code 
gets
+        # an importlib whose import_module adds the prefix.
+        self.udf_importlib = ModuleType("importlib")
+        self.udf_importlib.__dict__.update(vars(importlib))
+        self.udf_importlib.import_module = self.import_module_for_udf_code
+        self.udf_importlib.__getattr__ = lambda name: getattr(importlib, name)
+
+    @staticmethod
+    def root_package_name(function_id: int) -> str:
+        return f"__doris_pyudf_{function_id}"
+
+    def install(self) -> None:
+        sys.meta_path.insert(0, self)
+        builtins.__import__ = self.import_for_udf_code
+
+    def find_spec(self, fullname, path=None, target=None):
+        if fullname == self.root_package:
+            spec = importlib.machinery.ModuleSpec(fullname, None, 
is_package=True)
+            spec.submodule_search_locations = [self.location]
+            return spec
+
+        if fullname.startswith(self.root_package_prefix):
+            return None
+
+        parent_name = fullname.rpartition(".")[0]
+        if parent_name:
+            # Only children of a user package, so "json.decoder" stays 
standard.
+            parent = sys.modules.get(parent_name)
+            if getattr(parent, "__name__", None) != self.root_package_prefix + 
parent_name:
+                return None
+            if importlib.machinery.PathFinder.find_spec(
+                self.root_package_prefix + fullname, path
+            ) is None:
+                return None
+        elif not self.is_udf_top_level_module(fullname):
+            return None
+
+        return importlib.machinery.ModuleSpec(
+            fullname, UDFModuleAliasLoader(self.root_package_prefix + fullname)
+        )
+
+    def is_udf_top_level_module(self, name: str) -> bool:
+        is_udf_module = self.udf_top_level_module_cache.get(name)
+        if is_udf_module is None:
+            is_udf_module = (
+                importlib.machinery.PathFinder.find_spec(name, 
[self.location]) is not None
+            )
+            self.udf_top_level_module_cache[name] = is_udf_module
+        return is_udf_module
+
+    def add_root_package_prefix(self, name: str) -> str:
+        if self.is_udf_top_level_module(name.partition(".")[0]):
+            return self.root_package_prefix + name
+        return name
+
+    def import_for_udf_code(self, name, globals=None, locals=None, 
fromlist=(), level=0):
+        """builtins.__import__ that adds the prefix to absolute imports in 
user code."""
+        if (
+            level != 0
+            or globals is None
+            or not globals.get("__name__", 
"").startswith(self.root_package_prefix)
+        ):
+            return self.original_import(name, globals, locals, fromlist, level)
+
+        udf_module_name = self.add_root_package_prefix(name)

Review Comment:
   [P2] Preserve UDAF state compatibility across rolling BE upgrades
   
   This namespace also becomes the module name stored in pickled aggregate 
state. Take an existing ZIP where `entry.py` imports `State` from 
`state_helper.py` and returns it from `aggregate_state`. Previously the helper 
remained registered as `state_helper`, so old workers could exchange these 
states. On an upgraded BE it is imported as `__doris_pyudf_<id>.state_helper`; 
`UDAFStateManager.serialize()` pickles that name, and 
`AggregatePythonUDAFData::write/read/merge()` forwards the bytes unchanged. 
During the supported BE-first rolling upgrade, a merge fragment on an older BE 
calls its unchanged `pickle.loads()`, which [imports the recorded module 
name](https://github.com/python/cpython/blob/3.10/Lib/pickle.py#L1447-L1459), 
cannot find this new namespace, and fails with `ModuleNotFoundError`. Keep 
serialized user-class references readable by pre-upgrade workers, or prevent 
incompatible state exchanges, and cover a mixed-version custom-state case.



##########
be/src/udf/python/python_server.py:
##########
@@ -958,63 +937,130 @@ def load(self) -> AdaptivePythonUDF:
         return AdaptivePythonUDF(self.python_udf_meta, func)
 
 
+class UDFModuleAliasLoader(importlib.abc.Loader):
+    """Makes "pkg" the same module object as 
"__doris_pyudf_<function_id>.pkg"."""
+
+    def __init__(self, udf_module_name: str):
+        self.udf_module_name = udf_module_name
+        self.udf_module_spec = None
+
+    def create_module(self, spec) -> ModuleType:
+        module = importlib.import_module(self.udf_module_name)
+        self.udf_module_spec = module.__spec__
+        return module
+
+    def exec_module(self, module: ModuleType) -> None:
+        # module_from_spec() replaced the spec; keep it consistent with 
__name__.
+        module.__spec__ = self.udf_module_spec
+
+
+class UDFModuleFinder(importlib.abc.MetaPathFinder):
+    """
+    Loads user modules under the root package "__doris_pyudf_<function_id>", 
so a
+    user module named like a server module (e.g. types, json) does not resolve 
to
+    the server module. The name depends only on the function id, which keeps
+    pickled UDAF state classes loadable in every worker.
+
+    Absolute imports in user code look in the UDF directory first, e.g.
+    "from json import helper" in the user package json loads
+    "__doris_pyudf_<function_id>.json.helper".
+    """
+
+    def __init__(self, function_id: int, location: str):
+        self.root_package = self.root_package_name(function_id)
+        self.root_package_prefix = self.root_package + "."
+        self.location = location
+        self.original_import = builtins.__import__
+        # Top-level name -> whether the UDF directory provides it.
+        self.udf_top_level_module_cache: Dict[str, bool] = {}
+        # importlib.import_module bypasses builtins.__import__, so user code 
gets
+        # an importlib whose import_module adds the prefix.
+        self.udf_importlib = ModuleType("importlib")
+        self.udf_importlib.__dict__.update(vars(importlib))
+        self.udf_importlib.import_module = self.import_module_for_udf_code
+        self.udf_importlib.__getattr__ = lambda name: getattr(importlib, name)
+
+    @staticmethod
+    def root_package_name(function_id: int) -> str:
+        return f"__doris_pyudf_{function_id}"
+
+    def install(self) -> None:
+        sys.meta_path.insert(0, self)
+        builtins.__import__ = self.import_for_udf_code
+
+    def find_spec(self, fullname, path=None, target=None):
+        if fullname == self.root_package:
+            spec = importlib.machinery.ModuleSpec(fullname, None, 
is_package=True)
+            spec.submodule_search_locations = [self.location]
+            return spec
+
+        if fullname.startswith(self.root_package_prefix):
+            return None
+
+        parent_name = fullname.rpartition(".")[0]
+        if parent_name:
+            # Only children of a user package, so "json.decoder" stays 
standard.
+            parent = sys.modules.get(parent_name)
+            if getattr(parent, "__name__", None) != self.root_package_prefix + 
parent_name:
+                return None
+            if importlib.machinery.PathFinder.find_spec(
+                self.root_package_prefix + fullname, path
+            ) is None:
+                return None
+        elif not self.is_udf_top_level_module(fullname):
+            return None
+
+        return importlib.machinery.ModuleSpec(
+            fullname, UDFModuleAliasLoader(self.root_package_prefix + fullname)

Review Comment:
   [P2] Preserve resource loading through module aliases
   
   For an uploaded ZIP containing `mypkg/__init__.py`, `mypkg/udf.py` and 
`mypkg/data.txt`, an initializer such as `DATA = pkgutil.get_data("mypkg", 
"data.txt").decode()` now fails before `mypkg.udf.evaluate` can load. The 
package initially exists only under `__doris_pyudf_<id>.mypkg`, so `pkgutil`'s 
bare-name `find_spec` gets this alias spec. `UDFModuleAliasLoader` has no 
`get_data`; [CPython returns `None` 
immediately](https://github.com/python/cpython/blob/3.10/Lib/pkgutil.py#L569-L574),
 and `.decode()` raises `AttributeError`. Restoring the real spec in 
`exec_module` happens after that capability check. At the merge base, the 
ordinary package was already in `sys.modules` during its initializer and its 
`SourceFileLoader` read the resource. Preserve the underlying loader's resource 
support and package metadata in aliases, and cover resource access during 
package initialization.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to