Skip to content

Commit e574ce6

Browse files
committed
PYTHON-5683: Spike: Investigate using Rust for Extension Modules
- Implement comprehensive Rust BSON encoder/decoder - Add Evergreen CI configuration and test scripts - Add GitHub Actions workflow for Rust testing - Add runtime selection via PYMONGO_USE_RUST environment variable - Add performance benchmarking suite - Update build system to support Rust extension - Add documentation for Rust extension usage and testing"
1 parent 3667638 commit e574ce6

40 files changed

+4664
-30
lines changed

.evergreen/generated_configs/functions.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ functions:
111111
- LOAD_BALANCER
112112
- LOCAL_ATLAS
113113
- NO_EXT
114+
- PYMONGO_BUILD_RUST
115+
- PYMONGO_USE_RUST
114116
type: test
115117
- command: expansions.update
116118
params:
@@ -152,6 +154,8 @@ functions:
152154
- IS_WIN32
153155
- REQUIRE_FIPS
154156
- TEST_MIN_DEPS
157+
- PYMONGO_BUILD_RUST
158+
- PYMONGO_USE_RUST
155159
type: test
156160
- command: subprocess.exec
157161
params:

.evergreen/generated_configs/tasks.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2554,6 +2554,21 @@ tasks:
25542554
- func: attach benchmark test results
25552555
- func: send dashboard data
25562556
tags: [perf]
2557+
- name: perf-8.0-standalone-ssl-rust
2558+
commands:
2559+
- func: run server
2560+
vars:
2561+
VERSION: v8.0-perf
2562+
SSL: ssl
2563+
- func: run tests
2564+
vars:
2565+
TEST_NAME: perf
2566+
SUB_TEST_NAME: rust
2567+
PYMONGO_BUILD_RUST: "1"
2568+
PYMONGO_USE_RUST: "1"
2569+
- func: attach benchmark test results
2570+
- func: send dashboard data
2571+
tags: [perf]
25572572
- name: perf-8.0-standalone
25582573
commands:
25592574
- func: run server
@@ -2580,6 +2595,21 @@ tasks:
25802595
- func: attach benchmark test results
25812596
- func: send dashboard data
25822597
tags: [perf]
2598+
- name: perf-8.0-standalone-rust
2599+
commands:
2600+
- func: run server
2601+
vars:
2602+
VERSION: v8.0-perf
2603+
SSL: nossl
2604+
- func: run tests
2605+
vars:
2606+
TEST_NAME: perf
2607+
SUB_TEST_NAME: rust
2608+
PYMONGO_BUILD_RUST: "1"
2609+
PYMONGO_USE_RUST: "1"
2610+
- func: attach benchmark test results
2611+
- func: send dashboard data
2612+
tags: [perf]
25832613

25842614
# Search index tests
25852615
- name: test-search-index-helpers

.evergreen/generated_configs/variants.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,40 @@ buildvariants:
477477
expansions:
478478
SUB_TEST_NAME: pyopenssl
479479

480+
# Rust tests
481+
- name: test-with-rust-extension
482+
tasks:
483+
- name: .test-standard .server-latest .pr
484+
display_name: Test with Rust Extension
485+
run_on:
486+
- rhel87-small
487+
expansions:
488+
PYMONGO_BUILD_RUST: "1"
489+
PYMONGO_USE_RUST: "1"
490+
tags: [rust, pr]
491+
- name: test-with-rust-extension---macos-arm64
492+
tasks:
493+
- name: .test-standard .server-latest !.pr
494+
display_name: Test with Rust Extension - macOS ARM64
495+
run_on:
496+
- macos-14-arm64
497+
batchtime: 10080
498+
expansions:
499+
PYMONGO_BUILD_RUST: "1"
500+
PYMONGO_USE_RUST: "1"
501+
tags: [rust]
502+
- name: test-with-rust-extension---windows
503+
tasks:
504+
- name: .test-standard .server-latest !.pr
505+
display_name: Test with Rust Extension - Windows
506+
run_on:
507+
- windows-64-vsMulti-small
508+
batchtime: 10080
509+
expansions:
510+
PYMONGO_BUILD_RUST: "1"
511+
PYMONGO_USE_RUST: "1"
512+
tags: [rust]
513+
480514
# Search index tests
481515
- name: search-index-helpers-rhel8
482516
tasks:

.evergreen/scripts/generate_config.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -958,18 +958,24 @@ def create_search_index_tasks():
958958

959959
def create_perf_tasks():
960960
tasks = []
961-
for version, ssl, sync in product(["8.0"], ["ssl", "nossl"], ["sync", "async"]):
961+
for version, ssl, sync in product(["8.0"], ["ssl", "nossl"], ["sync", "async", "rust"]):
962962
vars = dict(VERSION=f"v{version}-perf", SSL=ssl)
963963
server_func = FunctionCall(func="run server", vars=vars)
964-
vars = dict(TEST_NAME="perf", SUB_TEST_NAME=sync)
965-
test_func = FunctionCall(func="run tests", vars=vars)
964+
test_vars = dict(TEST_NAME="perf", SUB_TEST_NAME=sync)
965+
# Enable Rust for rust perf tests
966+
if sync == "rust":
967+
test_vars["PYMONGO_BUILD_RUST"] = "1"
968+
test_vars["PYMONGO_USE_RUST"] = "1"
969+
test_func = FunctionCall(func="run tests", vars=test_vars)
966970
attach_func = FunctionCall(func="attach benchmark test results")
967971
send_func = FunctionCall(func="send dashboard data")
968972
task_name = f"perf-{version}-standalone"
969973
if ssl == "ssl":
970974
task_name += "-ssl"
971975
if sync == "async":
972976
task_name += "-async"
977+
elif sync == "rust":
978+
task_name += "-rust"
973979
tags = ["perf"]
974980
commands = [server_func, test_func, attach_func, send_func]
975981
tasks.append(EvgTask(name=task_name, tags=tags, commands=commands))
@@ -1189,6 +1195,8 @@ def create_run_server_func():
11891195
"LOAD_BALANCER",
11901196
"LOCAL_ATLAS",
11911197
"NO_EXT",
1198+
"PYMONGO_BUILD_RUST",
1199+
"PYMONGO_USE_RUST",
11921200
]
11931201
args = [".evergreen/just.sh", "run-server", "${TEST_NAME}"]
11941202
sub_cmd = get_subprocess_exec(include_expansions_in_env=includes, args=args)
@@ -1222,6 +1230,8 @@ def create_run_tests_func():
12221230
"IS_WIN32",
12231231
"REQUIRE_FIPS",
12241232
"TEST_MIN_DEPS",
1233+
"PYMONGO_BUILD_RUST",
1234+
"PYMONGO_USE_RUST",
12251235
]
12261236
args = [".evergreen/just.sh", "setup-tests", "${TEST_NAME}", "${SUB_TEST_NAME}"]
12271237
setup_cmd = get_subprocess_exec(include_expansions_in_env=includes, args=args)
@@ -1283,6 +1293,55 @@ def create_send_dashboard_data_func():
12831293
return "send dashboard data", cmds
12841294

12851295

1296+
def create_rust_variants():
1297+
"""Create build variants that test with Rust extension alongside C extension."""
1298+
variants = []
1299+
1300+
# Test Rust on Linux (primary platform) - runs on PRs
1301+
# Run standard tests with Rust enabled (both sync and async)
1302+
variant = create_variant(
1303+
[".test-standard .server-latest .pr"],
1304+
"Test with Rust Extension",
1305+
host=DEFAULT_HOST,
1306+
tags=["rust", "pr"],
1307+
expansions=dict(
1308+
PYMONGO_BUILD_RUST="1",
1309+
PYMONGO_USE_RUST="1",
1310+
),
1311+
)
1312+
variants.append(variant)
1313+
1314+
# Test on macOS ARM64 (important for M1/M2 Macs)
1315+
variant = create_variant(
1316+
[".test-standard .server-latest !.pr"],
1317+
"Test with Rust Extension - macOS ARM64",
1318+
host=HOSTS["macos-arm64"],
1319+
tags=["rust"],
1320+
batchtime=BATCHTIME_WEEK,
1321+
expansions=dict(
1322+
PYMONGO_BUILD_RUST="1",
1323+
PYMONGO_USE_RUST="1",
1324+
),
1325+
)
1326+
variants.append(variant)
1327+
1328+
# Test on Windows (important for cross-platform compatibility)
1329+
variant = create_variant(
1330+
[".test-standard .server-latest !.pr"],
1331+
"Test with Rust Extension - Windows",
1332+
host=HOSTS["win64"],
1333+
tags=["rust"],
1334+
batchtime=BATCHTIME_WEEK,
1335+
expansions=dict(
1336+
PYMONGO_BUILD_RUST="1",
1337+
PYMONGO_USE_RUST="1",
1338+
),
1339+
)
1340+
variants.append(variant)
1341+
1342+
return variants
1343+
1344+
12861345
mod = sys.modules[__name__]
12871346
write_variants_to_file(mod)
12881347
write_tasks_to_file(mod)

.evergreen/scripts/install-dependencies.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ fi
3030

3131
# Ensure just is installed.
3232
if ! command -v just &>/dev/null; then
33-
uv tool install rust-just
33+
uv tool install rust-just || uv tool install --force rust-just
3434
fi
3535

3636
popd > /dev/null

.evergreen/scripts/install-rust.sh

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/bin/bash
2+
# Install Rust toolchain for building the Rust BSON extension.
3+
set -eu
4+
5+
echo "Installing Rust toolchain..."
6+
7+
# Check if Rust is already installed
8+
if command -v cargo &> /dev/null; then
9+
echo "Rust is already installed:"
10+
rustc --version
11+
cargo --version
12+
echo "Updating Rust toolchain..."
13+
rustup update stable
14+
else
15+
echo "Rust not found. Installing Rust..."
16+
17+
# Install Rust using rustup
18+
if [ "Windows_NT" = "${OS:-}" ]; then
19+
# Windows installation
20+
curl --proto '=https' --tlsv1.2 -sSf https://win.rustup.rs/x86_64 -o rustup-init.exe
21+
./rustup-init.exe -y --default-toolchain stable
22+
rm rustup-init.exe
23+
24+
# Add to PATH for current session
25+
export PATH="$HOME/.cargo/bin:$PATH"
26+
else
27+
# Unix-like installation (Linux, macOS)
28+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
29+
30+
# Source cargo env
31+
source "$HOME/.cargo/env"
32+
fi
33+
34+
echo "Rust installation complete:"
35+
rustc --version
36+
cargo --version
37+
fi
38+
39+
# Install maturin if not already installed
40+
if ! command -v maturin &> /dev/null; then
41+
echo "Installing maturin..."
42+
cargo install maturin
43+
echo "maturin installation complete:"
44+
maturin --version
45+
else
46+
echo "maturin is already installed:"
47+
maturin --version
48+
fi
49+
50+
echo "Rust toolchain setup complete."

.evergreen/scripts/run_tests.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,16 @@ def run() -> None:
151151
if os.environ.get("PYMONGOCRYPT_LIB"):
152152
handle_pymongocrypt()
153153

154+
# Check if Rust extension is being used
155+
if os.environ.get("PYMONGO_USE_RUST") or os.environ.get("PYMONGO_BUILD_RUST"):
156+
try:
157+
import bson
158+
159+
LOGGER.info(f"BSON implementation: {bson.get_bson_implementation()}")
160+
LOGGER.info(f"Has Rust: {bson.has_rust()}, Has C: {bson.has_c()}")
161+
except Exception as e:
162+
LOGGER.warning(f"Could not check BSON implementation: {e}")
163+
154164
LOGGER.info(f"Test setup:\n{AUTH=}\n{SSL=}\n{UV_ARGS=}\n{TEST_ARGS=}")
155165

156166
# Record the start time for a perf test.

.evergreen/scripts/setup-dev-env.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ bash $HERE/install-dependencies.sh
2222
# Handle the value for UV_PYTHON.
2323
. $HERE/setup-uv-python.sh
2424

25+
# Show Rust toolchain status for debugging
26+
echo "Rust toolchain: $(rustc --version 2>/dev/null || echo 'not found')"
27+
echo "Cargo: $(cargo --version 2>/dev/null || echo 'not found')"
28+
echo "Maturin: $(maturin --version 2>/dev/null || echo 'not found')"
29+
2530
# Only run the next part if not running on CI.
2631
if [ -z "${CI:-}" ]; then
2732
# Add the default install path to the path if needed.

.evergreen/scripts/setup-tests.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ set -eu
1313
# MONGODB_API_VERSION The mongodb api version to use in tests.
1414
# MONGODB_URI If non-empty, use as the MONGODB_URI in tests.
1515
# USE_ACTIVE_VENV If non-empty, use the active virtual environment.
16+
# PYMONGO_BUILD_RUST If non-empty, build and test with Rust extension.
17+
# PYMONGO_USE_RUST If non-empty, use the Rust extension for tests.
1618

1719
SCRIPT_DIR=$(dirname ${BASH_SOURCE:-$0})
1820

@@ -21,6 +23,12 @@ if [ -f $SCRIPT_DIR/env.sh ]; then
2123
source $SCRIPT_DIR/env.sh
2224
fi
2325

26+
# Install Rust toolchain if building Rust extension
27+
if [ -n "${PYMONGO_BUILD_RUST:-}" ]; then
28+
echo "PYMONGO_BUILD_RUST is set, installing Rust toolchain..."
29+
bash $SCRIPT_DIR/install-rust.sh
30+
fi
31+
2432
echo "Setting up tests with args \"$*\"..."
2533
uv run ${USE_ACTIVE_VENV:+--active} "$SCRIPT_DIR/setup_tests.py" "$@"
2634
echo "Setting up tests with args \"$*\"... done."

.evergreen/scripts/setup_tests.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
"UV_PYTHON",
3333
"REQUIRE_FIPS",
3434
"IS_WIN32",
35+
"PYMONGO_USE_RUST",
36+
"PYMONGO_BUILD_RUST",
3537
]
3638

3739
# Map the test name to test extra.
@@ -447,7 +449,7 @@ def handle_test_env() -> None:
447449

448450
# PYTHON-4769 Run perf_test.py directly otherwise pytest's test collection negatively
449451
# affects the benchmark results.
450-
if sub_test_name == "sync":
452+
if sub_test_name == "sync" or sub_test_name == "rust":
451453
TEST_ARGS = f"test/performance/perf_test.py {TEST_ARGS}"
452454
else:
453455
TEST_ARGS = f"test/performance/async_perf_test.py {TEST_ARGS}"
@@ -471,6 +473,10 @@ def handle_test_env() -> None:
471473
if TEST_SUITE:
472474
TEST_ARGS = f"-m {TEST_SUITE} {TEST_ARGS}"
473475

476+
# For test_bson, run the specific test file
477+
if test_name == "test_bson":
478+
TEST_ARGS = f"test/test_bson.py {TEST_ARGS}"
479+
474480
write_env("TEST_ARGS", TEST_ARGS)
475481
write_env("UV_ARGS", " ".join(UV_ARGS))
476482

0 commit comments

Comments
 (0)