|
| 1 | +# |
| 2 | +# Copyright (C) 2025 sits developers. |
| 3 | +# |
| 4 | +# This program is free software; you can redistribute it and/or modify it |
| 5 | +# under the terms of the GNU General Public License as published by |
| 6 | +# the Free Software Foundation; either version 2 of the License, or |
| 7 | +# (at your option) any later version. |
| 8 | +# |
| 9 | +# This program is distributed in the hope that it will be useful, |
| 10 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | +# GNU General Public License for more details. |
| 13 | +# |
| 14 | +# You should have received a copy of the GNU General Public License |
| 15 | +# along with this program; if not, see <https://www.gnu.org/licenses/>. |
| 16 | +# |
| 17 | + |
| 18 | +"""Arrow interoperability between R and Python. |
| 19 | +
|
| 20 | +``pysits`` moves data between R and Python as Arrow IPC streams. That puts two |
| 21 | +independent builds of the libarrow C++ library in a single process: the one |
| 22 | +bundled in the R ``arrow`` package and the one bundled in ``pyarrow``. Their |
| 23 | +symbols collapse into a single namespace, so a call made through ``pyarrow`` |
| 24 | +can be served by R's libarrow. |
| 25 | +
|
| 26 | +Each build keeps a private ``mimalloc`` heap. When the two are mixed, a buffer |
| 27 | +allocated by one library is read back as zeroed memory by the other, and every |
| 28 | +transfer is silently corrupted instead of failing. Both builds also support the |
| 29 | +system allocator, which is shared, so selecting it keeps them interoperable. |
| 30 | +
|
| 31 | +The allocator is chosen by libarrow the first time it initializes, so importing |
| 32 | +this module sets ``ARROW_DEFAULT_MEMORY_POOL``. It is imported before anything |
| 33 | +that pulls in ``pyarrow`` or the R ``arrow`` package. |
| 34 | +""" |
| 35 | + |
| 36 | +import os |
| 37 | + |
| 38 | +# |
| 39 | +# Allocator selected to keep R libarrow and pyarrow interoperable |
| 40 | +# |
| 41 | +ARROW_MEMORY_POOL_ENVVAR = "ARROW_DEFAULT_MEMORY_POOL" |
| 42 | +ARROW_MEMORY_POOL = "system" |
| 43 | + |
| 44 | +# Set on import: libarrow reads this once, when it initializes. A value already |
| 45 | +# present in the environment is left alone so users can override it. |
| 46 | +os.environ.setdefault(ARROW_MEMORY_POOL_ENVVAR, ARROW_MEMORY_POOL) |
| 47 | + |
| 48 | +# |
| 49 | +# Values transferred to R and back to verify the round-trip is lossless. A |
| 50 | +# mismatched pair of libarrow builds reads them back as zeros. |
| 51 | +# |
| 52 | +PROBE_COLUMN = "pysits_arrow_probe" |
| 53 | +PROBE_VALUES = [0.5, 1.5, 2.5] |
| 54 | + |
| 55 | + |
| 56 | +def check_arrow_memory_pool() -> None: |
| 57 | + """Refuse to load R ``arrow`` package under an unshared allocator. |
| 58 | +
|
| 59 | + Called before the R ``arrow`` package is loaded. Corruption appears only |
| 60 | + once both libarrow builds are in use, and is silent when it does, so the |
| 61 | + configuration is rejected up front rather than probed for afterwards. |
| 62 | +
|
| 63 | + Raises: |
| 64 | + RuntimeError: If the selected allocator is not the shared one. |
| 65 | + """ |
| 66 | + selected = os.environ.get(ARROW_MEMORY_POOL_ENVVAR) |
| 67 | + |
| 68 | + if selected == ARROW_MEMORY_POOL: |
| 69 | + return |
| 70 | + |
| 71 | + raise RuntimeError( |
| 72 | + f"{ARROW_MEMORY_POOL_ENVVAR} is set to '{selected}', but pysits " |
| 73 | + f"requires '{ARROW_MEMORY_POOL}'.\n\n" |
| 74 | + "The R `arrow` package and `pyarrow` each bundle their own build of " |
| 75 | + "the libarrow C++ library. Loaded together, they must use the system " |
| 76 | + "allocator to share buffers. With any other allocator, data sent " |
| 77 | + "between R and Python is silently replaced by zeros.\n\n" |
| 78 | + f"Unset {ARROW_MEMORY_POOL_ENVVAR}, or set it before starting Python:\n\n" |
| 79 | + f" export {ARROW_MEMORY_POOL_ENVVAR}={ARROW_MEMORY_POOL}" |
| 80 | + ) |
| 81 | + |
| 82 | + |
| 83 | +def arrow_interop_error(observed: list) -> RuntimeError: |
| 84 | + """Build the error raised when an Arrow round-trip loses data. |
| 85 | +
|
| 86 | + Args: |
| 87 | + observed (list): Values read back from R for `PROBE_VALUES`. |
| 88 | +
|
| 89 | + Returns: |
| 90 | + RuntimeError: Error describing the cause and how to resolve it. |
| 91 | + """ |
| 92 | + import pyarrow as pa |
| 93 | + |
| 94 | + pool = pa.default_memory_pool().backend_name |
| 95 | + |
| 96 | + return RuntimeError( |
| 97 | + "Data sent to R is coming back corrupted, so pysits cannot run.\n\n" |
| 98 | + f"Sent {PROBE_VALUES}, received {observed}.\n\n" |
| 99 | + "The R `arrow` package and `pyarrow` each bundle their own build of " |
| 100 | + "the libarrow C++ library. Loaded together, they must use the system " |
| 101 | + f"allocator to share buffers, but pyarrow is using '{pool}'.\n\n" |
| 102 | + "This happens when pyarrow is initialized before pysits with a " |
| 103 | + "different allocator. Either import pysits before pyarrow, or set the " |
| 104 | + "environment variable before starting Python:\n\n" |
| 105 | + f" export {ARROW_MEMORY_POOL_ENVVAR}={ARROW_MEMORY_POOL}" |
| 106 | + ) |
0 commit comments