This is an automated email from the ASF dual-hosted git repository.
paleolimbot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git
The following commit(s) were added to refs/heads/main by this push:
new 2c8810a8 feat(rust/sedona): default memory limit to 75% of physical
memory with fair pool (#687)
2c8810a8 is described below
commit 2c8810a838e5bcacf2294e7b5bd973f8267781e6
Author: Kristin Cowalcijk <[email protected]>
AuthorDate: Thu Mar 5 03:46:09 2026 +0800
feat(rust/sedona): default memory limit to 75% of physical memory with fair
pool (#687)
---
Cargo.lock | 94 +++++++++++++++++++++
Cargo.toml | 1 +
docs/memory-management.ipynb | 54 ++++++------
docs/memory-management.md | 44 +++++-----
python/sedonadb/python/sedonadb/_options.py | 35 ++++----
rust/sedona/Cargo.toml | 1 +
rust/sedona/src/context.rs | 9 +-
rust/sedona/src/context_builder.rs | 122 ++++++++++++++++++++++++----
sedona-cli/src/main.rs | 35 ++++++--
9 files changed, 311 insertions(+), 84 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index ac91274b..6cbb4d3e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3925,6 +3925,15 @@ dependencies = [
"minimal-lexical",
]
+[[package]]
+name = "ntapi"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
+dependencies = [
+ "winapi",
+]
+
[[package]]
name = "num-bigint"
version = "0.4.6"
@@ -3992,6 +4001,25 @@ dependencies = [
"syn 2.0.114",
]
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "objc2-io-kit"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a"
+dependencies = [
+ "libc",
+ "objc2-core-foundation",
+]
+
[[package]]
name = "object"
version = "0.32.2"
@@ -5113,6 +5141,7 @@ dependencies = [
"sedona-tg",
"serde",
"serde_json",
+ "sysinfo",
"tempfile",
"tokio",
"url",
@@ -6022,6 +6051,20 @@ dependencies = [
"syn 2.0.114",
]
+[[package]]
+name = "sysinfo"
+version = "0.38.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d03c61d2a49c649a15c407338afe7accafde9dac869995dccb73e5f7ef7d9034"
+dependencies = [
+ "libc",
+ "memchr",
+ "ntapi",
+ "objc2-core-foundation",
+ "objc2-io-kit",
+ "windows",
+]
+
[[package]]
name = "tar"
version = "0.4.44"
@@ -6694,6 +6737,27 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+[[package]]
+name = "windows"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
+dependencies = [
+ "windows-collections",
+ "windows-core",
+ "windows-future",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
+dependencies = [
+ "windows-core",
+]
+
[[package]]
name = "windows-core"
version = "0.62.2"
@@ -6707,6 +6771,17 @@ dependencies = [
"windows-strings",
]
+[[package]]
+name = "windows-future"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
+dependencies = [
+ "windows-core",
+ "windows-link",
+ "windows-threading",
+]
+
[[package]]
name = "windows-implement"
version = "0.60.2"
@@ -6735,6 +6810,16 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+[[package]]
+name = "windows-numerics"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
+dependencies = [
+ "windows-core",
+ "windows-link",
+]
+
[[package]]
name = "windows-result"
version = "0.4.1"
@@ -6822,6 +6907,15 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
+[[package]]
+name = "windows-threading"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
+dependencies = [
+ "windows-link",
+]
+
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
diff --git a/Cargo.toml b/Cargo.toml
index abe66917..df7f8757 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -118,6 +118,7 @@ rand = "0.10"
regex = "1.12"
rstest = "0.26.1"
serde = { version = "1" }
+sysinfo = "0.38"
serde_json = { version = "1" }
serde_with = { version = "1" }
tempfile = { version = "3"}
diff --git a/docs/memory-management.ipynb b/docs/memory-management.ipynb
index 0c338d4c..75379559 100644
--- a/docs/memory-management.ipynb
+++ b/docs/memory-management.ipynb
@@ -26,7 +26,9 @@
"\n",
"# Memory Management and Spilling\n",
"\n",
- "SedonaDB supports memory-limited execution with automatic spill-to-disk,
allowing you to process datasets that are larger than available memory. When a
memory limit is configured, operators that exceed their memory budget
automatically spill intermediate data to temporary files on disk and read them
back as needed."
+ "SedonaDB uses memory-limited execution with automatic spill-to-disk out
of the box. By default, the memory limit is set to **75% of the system's
physical memory** and memory is managed by a **fair** pool. When operators
exceed their memory budget they automatically spill intermediate data to
temporary files on disk and read them back as needed.\n",
+ "\n",
+ "This means SedonaDB works well for large datasets without any
configuration. The sections below explain how to tune the defaults when needed."
]
},
{
@@ -36,12 +38,12 @@
"source": [
"## Configuring Memory Limits\n",
"\n",
- "Set `memory_limit` on the context options to cap the total memory
available for query execution. The limit accepts an integer (bytes) or a
human-readable string such as `\"4gb\"`, `\"512m\"`, or `\"1.5g\"`."
+ "By default, SedonaDB limits query execution memory to **75% of the
system's physical memory**. You can override this by setting `memory_limit` on
the context options before running your first query. The limit accepts an
integer (bytes) or a human-readable string such as `\"4gb\"`, `\"512m\"`, or
`\"1.5g\"`."
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 22,
"id": "d1f99fcf",
"metadata": {},
"outputs": [],
@@ -57,7 +59,14 @@
"id": "1fdc73aa",
"metadata": {},
"source": [
- "Without a memory limit, SedonaDB uses an unbounded memory pool and
operators can use as much memory as needed (until the process hits system
limits). In this mode, operators typically won't spill to disk because there is
no memory budget to enforce.\n",
+ "To disable the memory limit entirely and use an unbounded memory pool,
set `memory_limit` to `\"unlimited\"`:\n",
+ "\n",
+ "```python\n",
+ "sd = sedona.db.connect()\n",
+ "sd.options.memory_limit = \"unlimited\"\n",
+ "```\n",
+ "\n",
+ "In unbounded mode, operators can use as much memory as needed (until the
process hits system limits) and typically won't spill to disk because there is
no memory budget to enforce.\n",
"\n",
"> **Note:** All runtime options (`memory_limit`, `memory_pool_type`,
`temp_dir`, `unspillable_reserve_ratio`) must be set before the internal
context is initialized. The internal context is created on the first call to
`sd.sql(...)` (including `SET` statements) or any read method (for example,
`sd.read_parquet(...)`) -- not when you call `.execute()` on the returned
DataFrame. Once the internal context is created, these runtime options become
read-only."
]
@@ -71,15 +80,15 @@
"\n",
"The `memory_pool_type` option controls how the memory budget is
distributed among concurrent operators. Two pool types are available:\n",
"\n",
- "- **`\"greedy\"`** -- Grants memory reservations on a
first-come-first-served basis. This is the default when no pool type is
specified. Simple, but can lead to memory reservation failures under pressure
-- one consumer may exhaust the pool before others get a chance to reserve
memory.\n",
- "- **`\"fair\"` (recommended)** -- Distributes memory fairly among
spillable consumers and reserves a fraction of the pool for unspillable
consumers. More stable under memory pressure and significantly less likely to
cause reservation failures, at the cost of slightly lower utilization of the
total reserved memory.\n",
+ "- **`\"fair\"` (default)** -- Distributes memory fairly among spillable
consumers and reserves a fraction of the pool for unspillable consumers. Stable
under memory pressure and significantly less likely to cause reservation
failures.\n",
+ "- **`\"greedy\"`** -- Grants memory reservations on a
first-come-first-served basis. Simpler, but can lead to memory reservation
failures under pressure -- one consumer may exhaust the pool before others get
a chance to reserve memory.\n",
"\n",
- "We recommend using `\"fair\"` whenever a memory limit is configured."
+ "You only need to set `memory_pool_type` if you want to switch to the
greedy pool:"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 23,
"id": "b1dff726",
"metadata": {},
"outputs": [],
@@ -88,7 +97,7 @@
"\n",
"sd = sedona.db.connect()\n",
"sd.options.memory_limit = \"4gb\"\n",
- "sd.options.memory_pool_type = \"fair\""
+ "sd.options.memory_pool_type = \"greedy\""
]
},
{
@@ -96,7 +105,7 @@
"id": "bd4c0a76",
"metadata": {},
"source": [
- "> **Note:** `memory_pool_type` only takes effect when `memory_limit` is
set."
+ "> **Note:** `memory_pool_type` only takes effect when a memory limit is
active (i.e., `memory_limit` is not set to `\"unlimited\"`)."
]
},
{
@@ -111,7 +120,7 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 24,
"id": "dc0718cf",
"metadata": {},
"outputs": [],
@@ -120,7 +129,6 @@
"\n",
"sd = sedona.db.connect()\n",
"sd.options.memory_limit = \"8gb\"\n",
- "sd.options.memory_pool_type = \"fair\"\n",
"sd.options.unspillable_reserve_ratio = 0.3 # reserve 30% for unspillable
consumers"
]
},
@@ -136,7 +144,7 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 25,
"id": "c8d7a5c9",
"metadata": {},
"outputs": [],
@@ -144,8 +152,6 @@
"import sedona.db\n",
"\n",
"sd = sedona.db.connect()\n",
- "sd.options.memory_limit = \"4gb\"\n",
- "sd.options.memory_pool_type = \"fair\"\n",
"sd.options.temp_dir = \"/mnt/fast-ssd/sedona-spill\""
]
},
@@ -154,14 +160,14 @@
"id": "5d318b8f",
"metadata": {},
"source": [
- "## Example: Spatial Join with Limited Memory\n",
+ "## Example: Spatial Join with Memory Management\n",
"\n",
- "This example performs a spatial join between Natural Earth cities
(points) and Natural Earth countries (polygons) using `ST_Contains`. Spatial
joins are one of the most common workloads that benefit from memory limits and
spill-to-disk."
+ "This example performs a spatial join between Natural Earth cities
(points) and Natural Earth countries (polygons) using `ST_Contains`. 4GB memory
limit and fair pool are used. We also override `temp_dir` to control where
spill files are written."
]
},
{
"cell_type": "code",
- "execution_count": 21,
+ "execution_count": null,
"id": "1ed77d58",
"metadata": {},
"outputs": [
@@ -201,12 +207,12 @@
"\n",
"sd = sedona.db.connect()\n",
"\n",
- "# Configure runtime options before any sd.sql(...) or sd.read_* call.\n",
+ "# Optionally override runtime options before any sd.sql(...) or sd.read_*
call.\n",
"sd.options.memory_limit = \"4gb\"\n",
"sd.options.memory_pool_type = \"fair\"\n",
- "sd.options.unspillable_reserve_ratio = 0.2\n",
"sd.options.temp_dir = \"/tmp/sedona-spill\"\n",
"\n",
+ "# Call sd.sql(...) or sd.read_* to trigger the creation of the context
with the above options.\n",
"cities = sd.read_parquet(\n",
"
\"https://raw.githubusercontent.com/geoarrow/geoarrow-data/v0.2.0/natural-earth/files/natural-earth_cities_geo.parquet\"\n",
")\n",
@@ -236,7 +242,7 @@
"source": [
"## Operators Supporting Memory Limits\n",
"\n",
- "When a memory limit is configured, the following operators automatically
spill intermediate data to disk when they exceed their memory budget.\n",
+ "With the default memory limit active, the following operators
automatically spill intermediate data to disk when they exceed their memory
budget.\n",
"\n",
"In practice, this means memory limits and spilling can apply to both
SedonaDB's spatial operators and DataFusion's general-purpose operators used by
common SQL constructs.\n",
"\n",
@@ -286,8 +292,6 @@
"import sedona.db\n",
"\n",
"sd = sedona.db.connect()\n",
- "sd.options.memory_limit = \"4gb\"\n",
- "sd.options.memory_pool_type = \"fair\"\n",
"\n",
"# Enable LZ4 compression for spill files.\n",
"sd.sql(\"SET datafusion.execution.spill_compression =
'lz4_frame'\").execute()"
@@ -300,7 +304,7 @@
"source": [
"### Maximum temporary directory size\n",
"\n",
- "DataFusion limits the total size of temporary spill files to prevent
unbounded disk usage. The default limit is **100 G**. If your workload needs to
spill more data than this, increase the limit."
+ "DataFusion limits the total size of temporary spill files to prevent
unbounded disk usage. The default limit is **100G**. If your workload needs to
spill more data than this, increase the limit."
]
},
{
@@ -313,8 +317,6 @@
"import sedona.db\n",
"\n",
"sd = sedona.db.connect()\n",
- "sd.options.memory_limit = \"4gb\"\n",
- "sd.options.memory_pool_type = \"fair\"\n",
"\n",
"# Increase the spill directory size limit to 500 GB.\n",
"sd.sql(\"SET datafusion.runtime.max_temp_directory_size =
'500G'\").execute()"
diff --git a/docs/memory-management.md b/docs/memory-management.md
index 0318e807..2acbabfb 100644
--- a/docs/memory-management.md
+++ b/docs/memory-management.md
@@ -19,11 +19,13 @@
# Memory Management and Spilling
-SedonaDB supports memory-limited execution with automatic spill-to-disk,
allowing you to process datasets that are larger than available memory. When a
memory limit is configured, operators that exceed their memory budget
automatically spill intermediate data to temporary files on disk and read them
back as needed.
+SedonaDB uses memory-limited execution with automatic spill-to-disk out of the
box. By default, the memory limit is set to **75% of the system's physical
memory** and memory is managed by a **fair** pool. When operators exceed their
memory budget they automatically spill intermediate data to temporary files on
disk and read them back as needed.
+
+This means SedonaDB works well for large datasets without any configuration.
The sections below explain how to tune the defaults when needed.
## Configuring Memory Limits
-Set `memory_limit` on the context options to cap the total memory available
for query execution. The limit accepts an integer (bytes) or a human-readable
string such as `"4gb"`, `"512m"`, or `"1.5g"`.
+By default, SedonaDB limits query execution memory to **75% of the system's
physical memory**. You can override this by setting `memory_limit` on the
context options before running your first query. The limit accepts an integer
(bytes) or a human-readable string such as `"4gb"`, `"512m"`, or `"1.5g"`.
```python
@@ -33,7 +35,14 @@ sd = sedona.db.connect()
sd.options.memory_limit = "4gb"
```
-Without a memory limit, SedonaDB uses an unbounded memory pool and operators
can use as much memory as needed (until the process hits system limits). In
this mode, operators typically won't spill to disk because there is no memory
budget to enforce.
+To disable the memory limit entirely and use an unbounded memory pool, set
`memory_limit` to `"unlimited"`:
+
+```python
+sd = sedona.db.connect()
+sd.options.memory_limit = "unlimited"
+```
+
+In unbounded mode, operators can use as much memory as needed (until the
process hits system limits) and typically won't spill to disk because there is
no memory budget to enforce.
> **Note:** All runtime options (`memory_limit`, `memory_pool_type`,
> `temp_dir`, `unspillable_reserve_ratio`) must be set before the internal
> context is initialized. The internal context is created on the first call to
> `sd.sql(...)` (including `SET` statements) or any read method (for example,
> `sd.read_parquet(...)`) -- not when you call `.execute()` on the returned
> DataFrame. Once the internal context is created, these runtime options
> become read-only.
@@ -41,10 +50,10 @@ Without a memory limit, SedonaDB uses an unbounded memory
pool and operators can
The `memory_pool_type` option controls how the memory budget is distributed
among concurrent operators. Two pool types are available:
-- **`"greedy"`** -- Grants memory reservations on a first-come-first-served
basis. This is the default when no pool type is specified. Simple, but can lead
to memory reservation failures under pressure -- one consumer may exhaust the
pool before others get a chance to reserve memory.
-- **`"fair"` (recommended)** -- Distributes memory fairly among spillable
consumers and reserves a fraction of the pool for unspillable consumers. More
stable under memory pressure and significantly less likely to cause reservation
failures, at the cost of slightly lower utilization of the total reserved
memory.
+- **`"fair"` (default)** -- Distributes memory fairly among spillable
consumers and reserves a fraction of the pool for unspillable consumers. Stable
under memory pressure and significantly less likely to cause reservation
failures.
+- **`"greedy"`** -- Grants memory reservations on a first-come-first-served
basis. Simpler, but can lead to memory reservation failures under pressure --
one consumer may exhaust the pool before others get a chance to reserve memory.
-We recommend using `"fair"` whenever a memory limit is configured.
+You only need to set `memory_pool_type` if you want to switch to the greedy
pool:
```python
@@ -52,10 +61,10 @@ import sedona.db
sd = sedona.db.connect()
sd.options.memory_limit = "4gb"
-sd.options.memory_pool_type = "fair"
+sd.options.memory_pool_type = "greedy"
```
-> **Note:** `memory_pool_type` only takes effect when `memory_limit` is set.
+> **Note:** `memory_pool_type` only takes effect when a memory limit is active
(i.e., `memory_limit` is not set to `"unlimited"`).
### Unspillable reserve ratio
@@ -67,7 +76,6 @@ import sedona.db
sd = sedona.db.connect()
sd.options.memory_limit = "8gb"
-sd.options.memory_pool_type = "fair"
sd.options.unspillable_reserve_ratio = 0.3 # reserve 30% for unspillable
consumers
```
@@ -80,14 +88,12 @@ By default, DataFusion uses the system temporary directory
for spill files. You
import sedona.db
sd = sedona.db.connect()
-sd.options.memory_limit = "4gb"
-sd.options.memory_pool_type = "fair"
sd.options.temp_dir = "/mnt/fast-ssd/sedona-spill"
```
-## Example: Spatial Join with Limited Memory
+## Example: Spatial Join with Memory Management
-This example performs a spatial join between Natural Earth cities (points) and
Natural Earth countries (polygons) using `ST_Contains`. Spatial joins are one
of the most common workloads that benefit from memory limits and spill-to-disk.
+This example performs a spatial join between Natural Earth cities (points) and
Natural Earth countries (polygons) using `ST_Contains`. 4GB memory limit and
fair pool are used. We also override `temp_dir` to control where spill files
are written.
```python
@@ -95,12 +101,12 @@ import sedona.db
sd = sedona.db.connect()
-# Configure runtime options before any sd.sql(...) or sd.read_* call.
+# Optionally override runtime options before any sd.sql(...) or sd.read_* call.
sd.options.memory_limit = "4gb"
sd.options.memory_pool_type = "fair"
-sd.options.unspillable_reserve_ratio = 0.2
sd.options.temp_dir = "/tmp/sedona-spill"
+# Call sd.sql(...) or sd.read_* to trigger the creation of the context with
the above options.
cities = sd.read_parquet(
"https://raw.githubusercontent.com/geoarrow/geoarrow-data/v0.2.0/natural-earth/files/natural-earth_cities_geo.parquet"
)
@@ -151,7 +157,7 @@ sd.sql(
## Operators Supporting Memory Limits
-When a memory limit is configured, the following operators automatically spill
intermediate data to disk when they exceed their memory budget.
+With the default memory limit active, the following operators automatically
spill intermediate data to disk when they exceed their memory budget.
In practice, this means memory limits and spilling can apply to both
SedonaDB's spatial operators and DataFusion's general-purpose operators used by
common SQL constructs.
@@ -183,8 +189,6 @@ By default, data is written to spill files uncompressed.
Enabling compression re
import sedona.db
sd = sedona.db.connect()
-sd.options.memory_limit = "4gb"
-sd.options.memory_pool_type = "fair"
# Enable LZ4 compression for spill files.
sd.sql("SET datafusion.execution.spill_compression = 'lz4_frame'").execute()
@@ -192,15 +196,13 @@ sd.sql("SET datafusion.execution.spill_compression =
'lz4_frame'").execute()
### Maximum temporary directory size
-DataFusion limits the total size of temporary spill files to prevent unbounded
disk usage. The default limit is **100 G**. If your workload needs to spill
more data than this, increase the limit.
+DataFusion limits the total size of temporary spill files to prevent unbounded
disk usage. The default limit is **100G**. If your workload needs to spill more
data than this, increase the limit.
```python
import sedona.db
sd = sedona.db.connect()
-sd.options.memory_limit = "4gb"
-sd.options.memory_pool_type = "fair"
# Increase the spill directory size limit to 500 GB.
sd.sql("SET datafusion.runtime.max_temp_directory_size = '500G'").execute()
diff --git a/python/sedonadb/python/sedonadb/_options.py
b/python/sedonadb/python/sedonadb/_options.py
index 5da51077..3e0ab9e1 100644
--- a/python/sedonadb/python/sedonadb/_options.py
+++ b/python/sedonadb/python/sedonadb/_options.py
@@ -37,17 +37,20 @@ class Options:
created will raise a `RuntimeError`:
- `memory_limit`: Maximum memory for execution, in bytes or as a
- human-readable string (e.g., `"4gb"`, `"512m"`).
+ human-readable string (e.g., `"4gb"`, `"512m"`). Set to
+ `"unlimited"` to disable the memory limit. Defaults to 75% of
+ system physical memory.
- `temp_dir`: Directory for temporary/spill files.
- `memory_pool_type`: Memory pool type (`"greedy"` or `"fair"`).
+ Defaults to `"fair"`.
- `unspillable_reserve_ratio`: Fraction of memory reserved for
unspillable consumers (only applies to the `"fair"` pool type).
Examples:
>>> sd = sedona.db.connect()
- >>> sd.options.memory_limit = "4gb"
- >>> sd.options.memory_pool_type = "fair"
+ >>> sd.options.memory_limit = "4gb" # override default (75%
of RAM)
+ >>> sd.options.memory_pool_type = "greedy" # override default (fair)
>>> sd.options.temp_dir = "/tmp/sedona-spill"
>>> sd.options.interactive = True
>>> sd.sql("SELECT 1 as one")
@@ -67,7 +70,7 @@ class Options:
# Runtime options (must be set before first query)
self._memory_limit = None
self._temp_dir = None
- self._memory_pool_type = "greedy"
+ self._memory_pool_type = None
self._unspillable_reserve_ratio = None
# Set to True once the internal context is created; after this,
@@ -126,9 +129,9 @@ class Options:
"""Maximum memory for query execution.
Accepts an integer (bytes) or a human-readable string such as
- `"4gb"`, `"512m"`, or `"1.5g"`. When set, a bounded memory pool is
- created to enforce this limit. Without a memory limit, DataFusion's
- default unbounded pool is used.
+ `"4gb"`, `"512m"`, or `"1.5g"`. Set to `"unlimited"` to disable
+ the memory limit entirely. When `None`, the Rust-side default
+ (75% of system physical memory) is used.
Must be set before the first query is executed.
@@ -137,6 +140,7 @@ class Options:
>>> sd = sedona.db.connect()
>>> sd.options.memory_limit = "4gb"
>>> sd.options.memory_limit = 4 * 1024 * 1024 * 1024 # equivalent
+ >>> sd.options.memory_limit = "unlimited" # disable memory limit
"""
return self._memory_limit
@@ -175,26 +179,27 @@ class Options:
)
@property
- def memory_pool_type(self) -> str:
+ def memory_pool_type(self) -> Optional[str]:
"""Memory pool type: `"greedy"` or `"fair"`.
- - `"greedy"`: A simple pool that grants reservations on a
- first-come-first-served basis. This is the default.
- `"fair"`: A pool that fairly distributes memory among spillable
consumers and reserves a fraction for unspillable consumers
- (configured via `unspillable_reserve_ratio`).
+ (configured via `unspillable_reserve_ratio`). This is the default.
+ - `"greedy"`: A simple pool that grants reservations on a
+ first-come-first-served basis.
- Only takes effect when `memory_limit` is set.
+ When `None`, the Rust-side default (`"fair"`) is used.
+ Only takes effect when a memory limit is active.
Must be set before the first query is executed.
"""
return self._memory_pool_type
@memory_pool_type.setter
- def memory_pool_type(self, value: Literal["greedy", "fair"]) -> None:
+ def memory_pool_type(self, value: "Optional[Literal['greedy', 'fair']]")
-> None:
self._check_runtime_mutable("memory_pool_type")
- if value not in ("greedy", "fair"):
+ if value is not None and value not in ("greedy", "fair"):
raise ValueError(
- f"memory_pool_type must be 'greedy' or 'fair', got '{value}'"
+ f"memory_pool_type must be 'greedy', 'fair', or None, got
'{value}'"
)
self._memory_pool_type = value
diff --git a/rust/sedona/Cargo.toml b/rust/sedona/Cargo.toml
index eefc5b93..0c6fb4c9 100644
--- a/rust/sedona/Cargo.toml
+++ b/rust/sedona/Cargo.toml
@@ -85,5 +85,6 @@ sedona-testing = { workspace = true }
sedona-tg = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
+sysinfo = { workspace = true }
tokio = { workspace = true }
url = { workspace = true }
diff --git a/rust/sedona/src/context.rs b/rust/sedona/src/context.rs
index ee70d598..92940bd7 100644
--- a/rust/sedona/src/context.rs
+++ b/rust/sedona/src/context.rs
@@ -871,8 +871,13 @@ mod tests {
.expect("SedonaOptions not found");
assert!(opts.spatial_join.spilled_batch_in_memory_size_threshold >= 10
* 1024 * 1024);
- // Specify no memory limit, spilled batch threshold should be
unlimited (0 is for unlimited)
- let ctx = SedonaContextBuilder::new().build().await.unwrap();
+ // Explicitly disable the memory limit; spilled batch threshold should
be unlimited
+ // (0 means unlimited)
+ let ctx = SedonaContextBuilder::new()
+ .without_memory_limit()
+ .build()
+ .await
+ .unwrap();
let state = ctx.ctx.state();
let opts = state
.config_options()
diff --git a/rust/sedona/src/context_builder.rs
b/rust/sedona/src/context_builder.rs
index ad6b79c8..9341b358 100644
--- a/rust/sedona/src/context_builder.rs
+++ b/rust/sedona/src/context_builder.rs
@@ -33,6 +33,18 @@ use crate::{
size_parser,
};
+/// The fraction of total physical memory to use as the default memory limit.
+const DEFAULT_MEMORY_FRACTION: f64 = 0.75;
+
+/// Compute the default memory limit as 75% of total physical memory.
+fn default_memory_limit() -> usize {
+ let mut sys = sysinfo::System::new();
+ sys.refresh_memory();
+ // `System::total_memory()` returns bytes since sysinfo 0.23+.
+ let total = sys.total_memory() as f64;
+ (total * DEFAULT_MEMORY_FRACTION) as usize
+}
+
/// Builder for constructing a [`SedonaContext`] with configurable runtime
/// environment settings.
///
@@ -40,6 +52,10 @@ use crate::{
/// and runtime environments so that the same logic can be reused across the
/// CLI, Python bindings, ADBC driver, and any future entry points.
///
+/// By default, the builder uses 75% of the system's physical memory as the
+/// memory limit and a fair memory pool. Use
[`without_memory_limit`](Self::without_memory_limit)
+/// or pass `"unlimited"` as the `memory_limit` option to disable the limit.
+///
/// # Examples
///
/// ```rust,no_run
@@ -47,12 +63,24 @@ use crate::{
/// use sedona::context_builder::SedonaContextBuilder;
/// use sedona::pool_type::PoolType;
///
+/// // Uses defaults: 75% of physical memory, fair pool
+/// let ctx = SedonaContextBuilder::new()
+/// .build()
+/// .await?;
+///
+/// // Override with explicit memory limit
/// let ctx = SedonaContextBuilder::new()
/// .with_memory_limit(4 * 1024 * 1024 * 1024)
/// .with_pool_type(PoolType::Fair)
/// .with_temp_dir("/tmp/sedona-spill".to_string())
/// .build()
/// .await?;
+///
+/// // Disable memory limit entirely
+/// let ctx = SedonaContextBuilder::new()
+/// .without_memory_limit()
+/// .build()
+/// .await?;
/// # Ok(())
/// # }
/// ```
@@ -69,6 +97,11 @@ use crate::{
/// opts.insert("memory_pool_type".to_string(), "fair".to_string());
///
/// let ctx = SedonaContextBuilder::from_options(&opts)?.build().await?;
+///
+/// // Use "unlimited" to disable memory limit
+/// let mut opts = HashMap::new();
+/// opts.insert("memory_limit".to_string(), "unlimited".to_string());
+/// let ctx = SedonaContextBuilder::from_options(&opts)?.build().await?;
/// # Ok(())
/// # }
/// ```
@@ -89,15 +122,15 @@ impl SedonaContextBuilder {
/// Create a new builder with default settings.
///
/// Defaults:
- /// - `memory_limit`: `None` (no limit, uses DataFusion's default
unbounded pool)
- /// - `pool_type`: `PoolType::Greedy`
+ /// - `memory_limit`: 75% of total physical memory
+ /// - `pool_type`: `PoolType::Fair`
/// - `unspillable_reserve_ratio`: `0.2`
/// - `temp_dir`: `None` (uses DataFusion's default temp directory)
pub fn new() -> Self {
Self {
- memory_limit: None,
+ memory_limit: Some(default_memory_limit()),
temp_dir: None,
- pool_type: PoolType::Greedy,
+ pool_type: PoolType::Fair,
unspillable_reserve_ratio: DEFAULT_UNSPILLABLE_RESERVE_RATIO,
}
}
@@ -107,8 +140,9 @@ impl SedonaContextBuilder {
/// Recognized keys:
/// - `"memory_limit"`: Memory limit as a human-readable size string
/// (e.g., `"4gb"`, `"512m"`, `"1.5g"`) or plain bytes (e.g.,
- /// `"4294967296"`). See [`size_parser::parse_size_string`] for
- /// supported suffixes.
+ /// `"4294967296"`). Use `"unlimited"` to disable the memory limit
+ /// entirely. See [`size_parser::parse_size_string`] for supported
+ /// suffixes.
/// - `"temp_dir"`: Path for temporary/spill files
/// - `"memory_pool_type"`: `"greedy"` or `"fair"`
/// - `"unspillable_reserve_ratio"`: Float between 0.0 and 1.0
@@ -118,8 +152,12 @@ impl SedonaContextBuilder {
let mut builder = Self::new();
if let Some(memory_limit) = options.get("memory_limit") {
- let limit = size_parser::parse_size_string(memory_limit)?;
- builder = builder.with_memory_limit(limit);
+ if memory_limit.eq_ignore_ascii_case("unlimited") {
+ builder = builder.without_memory_limit();
+ } else {
+ let limit = size_parser::parse_size_string(memory_limit)?;
+ builder = builder.with_memory_limit(limit);
+ }
}
if let Some(temp_dir) = options.get("temp_dir") {
@@ -154,6 +192,15 @@ impl SedonaContextBuilder {
self
}
+ /// Remove the memory limit.
+ ///
+ /// This disables the default memory pool and uses DataFusion's
+ /// unbounded memory pool instead.
+ pub fn without_memory_limit(mut self) -> Self {
+ self.memory_limit = None;
+ self
+ }
+
/// Set the directory for temporary/spill files.
pub fn with_temp_dir(mut self, temp_dir: String) -> Self {
self.temp_dir = Some(temp_dir);
@@ -239,9 +286,12 @@ mod tests {
#[test]
fn test_default_builder() {
let builder = SedonaContextBuilder::new();
- assert!(builder.memory_limit.is_none());
+ // Default memory limit should be 75% of physical memory
+ let expected_limit = default_memory_limit();
+ assert_eq!(builder.memory_limit, Some(expected_limit));
+ assert!(builder.memory_limit.unwrap() > 0);
assert!(builder.temp_dir.is_none());
- assert_eq!(builder.pool_type, PoolType::Greedy);
+ assert_eq!(builder.pool_type, PoolType::Fair);
assert!(
(builder.unspillable_reserve_ratio -
DEFAULT_UNSPILLABLE_RESERVE_RATIO).abs()
< f64::EPSILON
@@ -262,6 +312,12 @@ mod tests {
assert!((builder.unspillable_reserve_ratio - 0.3).abs() <
f64::EPSILON);
}
+ #[test]
+ fn test_without_memory_limit() {
+ let builder = SedonaContextBuilder::new().without_memory_limit();
+ assert!(builder.memory_limit.is_none());
+ }
+
#[test]
fn test_invalid_unspillable_reserve_ratio() {
let result =
SedonaContextBuilder::new().with_unspillable_reserve_ratio(-0.1);
@@ -297,9 +353,29 @@ mod tests {
fn test_from_options_empty() {
let opts = HashMap::new();
let builder = SedonaContextBuilder::from_options(&opts).unwrap();
- assert!(builder.memory_limit.is_none());
+ // Empty options should use defaults (75% memory, Fair pool)
+ assert!(builder.memory_limit.is_some());
assert!(builder.temp_dir.is_none());
- assert_eq!(builder.pool_type, PoolType::Greedy);
+ assert_eq!(builder.pool_type, PoolType::Fair);
+ }
+
+ #[test]
+ fn test_from_options_unlimited() {
+ let mut opts = HashMap::new();
+ opts.insert("memory_limit".to_string(), "unlimited".to_string());
+ let builder = SedonaContextBuilder::from_options(&opts).unwrap();
+ assert!(builder.memory_limit.is_none());
+
+ // Case insensitive
+ let mut opts = HashMap::new();
+ opts.insert("memory_limit".to_string(), "Unlimited".to_string());
+ let builder = SedonaContextBuilder::from_options(&opts).unwrap();
+ assert!(builder.memory_limit.is_none());
+
+ let mut opts = HashMap::new();
+ opts.insert("memory_limit".to_string(), "UNLIMITED".to_string());
+ let builder = SedonaContextBuilder::from_options(&opts).unwrap();
+ assert!(builder.memory_limit.is_none());
}
#[test]
@@ -349,12 +425,13 @@ mod tests {
let mut opts = HashMap::new();
opts.insert("unknown_key".to_string(), "value".to_string());
let builder = SedonaContextBuilder::from_options(&opts).unwrap();
- assert!(builder.memory_limit.is_none());
+ // Default memory limit should still be set
+ assert!(builder.memory_limit.is_some());
}
#[test]
fn test_build_runtime_env_no_memory_limit() {
- let builder = SedonaContextBuilder::new();
+ let builder = SedonaContextBuilder::new().without_memory_limit();
let result = builder.build_runtime_env();
assert!(result.is_ok());
}
@@ -379,6 +456,14 @@ mod tests {
assert!(result.is_ok());
}
+ #[test]
+ fn test_build_runtime_env_default() {
+ // Default builder should build successfully with 75% memory + fair
pool
+ let builder = SedonaContextBuilder::new();
+ let result = builder.build_runtime_env();
+ assert!(result.is_ok());
+ }
+
#[tokio::test]
async fn test_build_context_default() {
let ctx = SedonaContextBuilder::new().build().await;
@@ -396,4 +481,13 @@ mod tests {
.await;
assert!(ctx.is_ok());
}
+
+ #[tokio::test]
+ async fn test_build_context_without_memory_limit() {
+ let ctx = SedonaContextBuilder::new()
+ .without_memory_limit()
+ .build()
+ .await;
+ assert!(ctx.is_ok());
+ }
}
diff --git a/sedona-cli/src/main.rs b/sedona-cli/src/main.rs
index 6dd315ee..082e68a1 100644
--- a/sedona-cli/src/main.rs
+++ b/sedona-cli/src/main.rs
@@ -66,15 +66,15 @@ struct Args {
#[clap(
short = 'm',
long,
- help = "The memory pool limitation (e.g. '10g'), default to None (no
limit)",
- value_parser(extract_memory_pool_size)
+ help = "The memory pool limitation (e.g. '10g'), default to 75% of
physical memory. Use 'unlimited' to disable",
+ value_parser(parse_memory_limit)
)]
- memory_limit: Option<usize>,
+ memory_limit: Option<MemoryLimitArg>,
#[clap(
long,
help = "Specify the memory pool type 'greedy' or 'fair'",
- default_value_t = PoolType::Greedy
+ default_value_t = PoolType::Fair
)]
mem_pool_type: PoolType,
@@ -140,6 +140,15 @@ enum FunctionListFormat {
Json,
}
+/// Parsed representation of the `--memory-limit` CLI argument.
+#[derive(Debug, Clone, PartialEq)]
+enum MemoryLimitArg {
+ /// Disable the memory limit entirely.
+ Unlimited,
+ /// Use an explicit byte limit.
+ Limit(usize),
+}
+
#[tokio::main]
/// Calls [`main_inner`], then handles printing errors and returning the
correct exit code
pub async fn main() -> ExitCode {
@@ -190,8 +199,14 @@ async fn main_inner() -> Result<()> {
let mut builder = SedonaContextBuilder::new()
.with_pool_type(args.mem_pool_type.clone())
.with_unspillable_reserve_ratio(args.unspillable_reserve_ratio)?;
- if let Some(memory_limit) = args.memory_limit {
- builder = builder.with_memory_limit(memory_limit);
+ match args.memory_limit {
+ Some(MemoryLimitArg::Unlimited) => {
+ builder = builder.without_memory_limit();
+ }
+ Some(MemoryLimitArg::Limit(limit)) => {
+ builder = builder.with_memory_limit(limit);
+ }
+ None => {}
}
let ctx = builder.build().await?;
@@ -252,6 +267,14 @@ pub fn extract_memory_pool_size(size: &str) ->
Result<usize, String> {
sedona::size_parser::parse_size_string(size).map_err(|e| e.to_string())
}
+fn parse_memory_limit(s: &str) -> Result<MemoryLimitArg, String> {
+ if s.eq_ignore_ascii_case("unlimited") {
+ Ok(MemoryLimitArg::Unlimited)
+ } else {
+ extract_memory_pool_size(s).map(MemoryLimitArg::Limit)
+ }
+}
+
fn validate_unspillable_reserve_ratio(s: &str) -> Result<f64, String> {
let value: f64 = s
.parse()