Skip to content

PYTHON-6207 [Spike] back bson encode and decode with Rust and run the full BSON performance suite - #3170

Draft
blink1073 wants to merge 8 commits into
mongodb:mainfrom
blink1073:pymongo-py-bson-crate
Draft

blink1073 wants to merge 8 commits into
mongodb:mainfrom
blink1073:pymongo-py-bson-crate

Conversation

@blink1073

@blink1073 blink1073 commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

PYTHON-6207

Summary

Adds a Rust implementation of the BSON codec as a drop-in replacement for the C extension. On the perf host it is faster than C on all six benchmarks, from 1.02x on FullDecoding to 1.71x on encoding.

Motivation

The C extensions hold the repo's highest memory-safety and reviewability risk. This ticket is the first step of the rust-shared-core program (PYTHON-6162): prove the codec can move to Rust with byte-exact parity and no performance regression before any larger migration.

Changes

  • Added a Rust implementation of the BSON codec under rust/bson, differential-tested against the C extension.
  • Added a benchmark selector that switches the performance suite between the C and Rust codecs.
  • Added an Evergreen task that builds both codecs on the perf host and publishes the comparison.

Testing

  • Differential suite against the C extension: 124 passed.
  • BSON micro-benchmarks: 6 passed per codec.
  • Full comparison protocol run on the perf host: task results.

Throughput, megabytes per second, perf host (October 31 run):

Benchmark C MB/s Rust MB/s Rust/C
FlatEncoding 508.3 870.7 1.71x
DeepEncoding 229.6 391.8 1.71x
FullEncoding 287.8 331.6 1.15x
DeepDecoding 188.7 267.0 1.42x
FlatDecoding 455.7 515.5 1.13x
FullDecoding 143.3 145.6 1.02x

Rust is at parity or better on every benchmark. The run also captured DeepDecoding profiles for both codecs as task artifacts.

Copilot AI balanced review requested due to automatic review settings November 2, 2026 14:50
@blink1073
blink1073 requested a review from a team as a code owner November 2, 2026 14:50
@blink1073
blink1073 requested a review from aclark4life November 2, 2026 14:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical defects risk undefined behavior when decoding writable buffers and BSON length overflow from unchecked cumulative growth, alongside codec compatibility and CI regressions.

9 open issues
What changed in this PR

Adds an experimental Rust-backed BSON codec alongside PyMongo’s C extension, with differential tests and benchmark tooling to evaluate compatibility and throughput before broader migration.

Changes:

  • Implements six BSON codec entry points using PyO3, including codec options and BSON-specific types.
  • Adds differential tests, benchmark fixtures, and a BSON_IMPL selector.
  • Adds Evergreen comparison runs, optional decoding profiles, and artifact publishing.
File Description
tools/​compare_bson_perf.py Formats C/Rust throughput comparisons.
test/​performance/​data/​extended_bson/​full_bson.json Adds mixed-type benchmark data.
test/​performance/​data/​extended_bson/​flat_bson.json Adds flat-document benchmark data.
test/​performance/​data/​extended_bson/​deep_bson.json Adds nested-document benchmark data.
test/​performance/​conftest.py Activates codec selection before benchmarks.
test/​performance/​bson_impl_select.py Selects and rebinds codec entry points.
rust/​bson/​tests/​test_rbson.py Adds differential compatibility tests.
rust/​bson/​src/​types.rs Caches Python types used by the codec.
rust/​bson/​src/​options.rs Parses codec options and type markers.
rust/​bson/​src/​lib.rs Exposes Python entry points and buffer handling.
rust/​bson/​src/​errors.rs Constructs BSON exceptions.
rust/​bson/​src/​encode.rs Implements BSON encoding.
rust/​bson/​src/​decode.rs Implements BSON decoding and field-name caching.
rust/​bson/​src/​datetime_conv.rs Converts datetime fields and epoch milliseconds.
rust/​bson/​src/​buffer.rs Provides encoding buffer operations.
rust/​bson/​pyproject.toml Defines Python packaging with maturin.
rust/​bson/​Cargo.toml Defines Rust dependencies and release settings.
rust/​bson/​Cargo.lock Locks Rust dependencies.
rust/​bson/​build.rs Enables Python-version compilation flags.
AGENTS.md Documents the experimental codec workflow.
.pre-commit-config.yaml Allows Rust terminology in spell checks.
.gitignore Excludes build and comparison outputs.
.evergreen/​scripts/​test_generate_config.py Tests artifact and profiling configuration.
.evergreen/​scripts/​generate_config.py Defines comparison tasks, variants, and uploads.
.evergreen/​scripts/​generate_config_utils.py Changes the shared performance host assignment.
.evergreen/​scripts/​bson_codec_compare.sh Builds, benchmarks, compares, and profiles codecs.
.evergreen/​generated_configs/​variants.yml Adds the comparison variant and updates hosts.
.evergreen/​generated_configs/​tasks.yml Adds the generated comparison task.
.evergreen/​generated_configs/​functions.yml Adds generated artifact-upload commands.

Review effort: Balanced

Comment thread rust/bson/src/buffer.rs Resolved
Comment thread rust/bson/src/lib.rs Resolved
Comment thread rust/bson/src/decode.rs Resolved
Comment thread rust/bson/src/encode.rs Resolved
Comment thread rust/bson/src/encode.rs Resolved
Comment thread rust/bson/src/lib.rs Resolved
@blink1073
blink1073 marked this pull request as draft November 2, 2026 15:00
@blink1073 blink1073 changed the title PYTHON-6207 back bson encode and decode with Rust and run the full BSON performance suite PYTHON-6207 [Spike] back bson encode and decode with Rust and run the full BSON performance suite Nov 2, 2026
@blink1073
blink1073 removed the request for review from aclark4life November 2, 2026 15:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread rust/bson/src/lib.rs Resolved
Comment thread rust/bson/src/decode.rs Resolved
Comment thread rust/bson/src/encode.rs Resolved
Comment thread rust/bson/src/encode.rs Resolved
Comment thread rust/bson/src/options.rs Resolved
Comment thread rust/bson/tests/test_rbson.py Resolved
…f flag, fixed-size OID/Decimal128 payloads, handler conversion errors, parity suite in compare task

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Malformed UTF-8 field names can trigger a custom error handler that re-enters decoding while the key-cache mutex is held, deadlocking the process.

No open issues 🎉

7 previously missed issues

In code that hasn't changed since last review

High severity Bypass UTF-8 cache for invalid input

rust/​bson/​src/​decode.rs:1028

The strict error handler can be replaced with codecs.register_error("strict", handler), and PyUnicode_DecodeUTF8 invokes that Python handler even when its errors argument is NULL. For a malformed short field name, a handler that calls BSON decoding again therefore re-enters decode_key_cached while its non-reentrant mutex is held, deadlocking the process. Bypass the cache for invalid UTF-8 before taking the lock; this also prevents caching handler-dependent replacements as though they were strict decoding results.

Medium severity Mark Evergreen task as patch-only

.evergreen/​scripts/​generate_config.py:1211

Omitting batchtime does not make an Evergreen variant patch-only, and this task has no patch_only setting. The Rust build and four benchmark passes can therefore run on normal commits despite the stated patch-only intent. Set patch_only=True on this task and regenerate the Evergreen configuration.

Medium severity Restrict DBRef shortcut to exact dicts

rust/​bson/​src/​decode.rs:312

The no-dollar-key shortcut is only valid for an exact dict. A supported custom document_class can add DBRef keys in its constructor or rewrite ref/id to $ref/$id in __setitem__. The C codec checks the resulting mapping and returns a DBRef, but this gate skips the hook and returns the custom mapping instead. Please restrict this shortcut to the exact-dict case and add a differential regression test for a mapping that rewrites keys.

Medium severity Propagate top-level TypeDecoder exceptions

rust/​bson/​src/​decode.rs:620

A registered TypeDecoder that raises ValueError for a top-level scalar produces InvalidBSON with Rust, while C preserves ValueError, breaking callers that handle their decoder's exceptions. Match C's exception boundary: wrap parsing and construction failures before invoking the converter, and propagate its failure without another wrapper at that level.

Medium severity Preserve mapping lookups for dict subclasses

rust/​bson/​src/​decode.rs:654

cast::<PyDict>() also accepts dict subclasses, so this fast path bypasses their __getitem__ overrides. The C DBRef hook uses mapping lookups even for those subclasses. For example, a custom document_class that normalizes the $ref value on lookup produces a different DBRef.collection with Rust because this code reads the unnormalized stored value. Please use the raw-storage fast path only for an exact dict and preserve mapping lookups for subclasses.

Medium severity Propagate exceptions outside the Exception hierarchy

rust/​bson/​src/​decode.rs:1038

This wraps every PyErr, including KeyboardInterrupt and SystemExit, as InvalidBSON. For example, a registered TypeDecoder raising KeyboardInterrupt is turned into a data error instead of interrupting the operation. The C reference explicitly wraps only subclasses of Exception; preserve exceptions outside that hierarchy before performing the conversion.

Medium severity Accept signed-byte buffers in BSON decoding

rust/​bson/​src/​lib.rs:97

PyBuffer::<u8>::get rejects signed-byte buffers such as memoryview(encoded).cast("b") and array.array("b", encoded), although the C codec accepts them, so valid BSON fails to decode. Use a shared format-independent buffer acquisition helper that preserves contiguity, item-size, ownership, release and snapshot checks. The same issue affects raw-document decoding at rust/bson/src/lib.rs:72 and raw-document encoding at rust/bson/src/encode.rs:711.

7 resolved since last review

Review effort: Balanced

…ion boundary, signed-byte buffers, patch-only compare variant

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The raw-byte shortcut misses decoded $ref and $id keys produced by a registered strict handler, causing Rust to return a dict where the C decoder returns a DBRef.

1 open issue

Review effort: Balanced

Comment thread rust/bson/src/decode.rs Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The callback-induced panic risk, benchmark-selection inconsistency, incomplete Rust clamping coverage, and large-document copying overhead require human assessment of their combined impact.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Rust-selected benchmarks can silently measure the C codec, while allocation failures during decoding can abort Python and dictionary mutation during encoding can trigger Rust panics.

6 open issues

Review effort: Balanced

# and hold a direct reference; (module, attribute) pairs to rebind.
_CAPTURING_MODULES = (
("bson.raw_bson", "_raw_to_dict"),
("pymongo.message", "_dict_to_bson"),
Comment thread rust/bson/src/decode.rs
.str()
.and_then(|m| m.to_cow().map(|c| c.into_owned()))
.unwrap_or_default();
bson_error(py, "InvalidBSON", &message)
Comment thread rust/bson/src/encode.rs
// Mappings are iterated by key with per-key lookup: no items() method
// is required (Mapping.register'd classes may not provide one).
if let Ok(dict) = mapping.cast::<PyDict>() {
for (key, value) in dict.iter() {
Comment thread rust/bson/src/lib.rs
let buffer = RawBuffer::get(object)?;
with_get_buffer_checks(&buffer)?;
// Copy while the GIL is held and before any callback runs.
let data = buffer.as_slice().to_vec();
result = assert_same_round_trip({"a": datetime.datetime(2026, 10, 20, 14, 30, 0)}, opts=opts)
assert isinstance(result["a"], datetime.datetime)
# Out-of-range values clamp to datetime.min/max.
result = decode_c(encode_c({"a": DatetimeMS(-(2**62))}, opts=opts), opts)
from __future__ import annotations

# Importing triggers codec selection (side effect; see bson_impl_select.py).
from test.performance import bson_impl_select

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants