Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion .github/workflows/pr-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,67 @@ jobs:
| sed -u 's/^/resource-monitor out-of-memory /' ) &
trap 'kill %1 %2 2>/dev/null || true' EXIT

# Fail fast if the target dies mid-run. If a server crashes, every
# remaining test otherwise blocks on connection timeouts against a
# dead target one by one, and the job burns the full 6h runner limit
# before failing. This watchdog polls the profile's containers and, if
# any expected service stops running, terminates the test run so the
# job fails in about a minute instead of hours.
PROFILE='${{ matrix.target.profile }}'
watch_target() {
set +e
local pid=$1 expected running low=0
expected=$(docker compose -f dev/compose.yaml --profile "$PROFILE" config --services | grep -c .)
# Sanity-check the probe once. The stack is already up (compose
# --wait), so every service should read as running; if it does not,
# the ps query is unreliable on this runner and the watchdog steps
# aside rather than risk a false abort.
if [ "$(docker compose -f dev/compose.yaml --profile "$PROFILE" ps --status running -q 2>/dev/null | grep -c .)" -lt "$expected" ]; then
echo "resource-monitor target-watchdog disabled: could not read a healthy baseline"
return
fi
while kill -0 "$pid" 2>/dev/null; do
running=$(docker compose -f dev/compose.yaml --profile "$PROFILE" ps --status running -q 2>/dev/null | grep -c .)
# Require two consecutive low readings before aborting. A single
# low count can be a transient docker-CLI hiccup on this
# resource-heavy runner (a failed query reads as 0); a real crash
# stays low, so demanding two polls in a row rules out a false
# abort while adding only one poll interval to a genuine one.
if [ "$running" -lt "$expected" ]; then
low=$((low + 1))
if [ "$low" -ge 2 ]; then
echo "::error::A $PROFILE target container exited mid-run (likely a server crash). Aborting the test run so the job fails fast instead of timing out every remaining test against a dead target."
docker compose -f dev/compose.yaml --profile "$PROFILE" ps -a
kill -TERM "$pid" 2>/dev/null; sleep 10; kill -KILL "$pid" 2>/dev/null
return
fi
else
low=0
fi
sleep 15
done
}

# --timeout caps any single hung test as a backstop independent of the
# watchdog: a wedged query fails its own test (default signal method,
# as the crash-test job uses) instead of stalling a worker forever.
pytest documentdb_tests/compatibility/tests \
--connection-string "${{ matrix.target.connection_string }}" \
--engine-name "${{ matrix.target.engine }}" \
-n auto \
-v \
--timeout=120 \
--json-report --json-report-file=${{ github.workspace }}/.test-results/${{ matrix.target.name }}-report.json \
--junitxml=${{ github.workspace }}/.test-results/${{ matrix.target.name }}-results.xml
--junitxml=${{ github.workspace }}/.test-results/${{ matrix.target.name }}-results.xml &
pytest_pid=$!
watch_target "$pytest_pid" &
watch_pid=$!
set +e
wait "$pytest_pid"
status=$?
set -e
kill "$watch_pid" 2>/dev/null || true
exit "$status"

- name: Dump container logs
if: always()
Expand Down
31 changes: 31 additions & 0 deletions dev/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,36 @@
# cache keeps the combined footprint within the VM. There is intentionally no
# per-container mem_limit: that caps total process memory (not just cache) and a
# transient working-set spike then kills mongod even when the VM has room.
#
# Open files: each mongod raises its open-file limit (ulimits.nofile). The suite
# creates and drops tens of thousands of collections in a run, and WiredTiger
# holds an open file descriptor per table (collection + indexes) in its handle
# cache, closing idle ones only lazily. On the replica set a dropped collection
# also becomes a drop-pending ident whose files linger until the drop timestamp
# is majority-committed and swept, so descriptors accumulate faster than they
# are reclaimed. WiredTiger also keeps a file handle open per table it has
# touched and closes idle ones only very lazily, so the descriptor count tracks
# the number of collections created over the run, not the number live at once.
# The replica set peaks near 60k open files partway through the suite; when that
# reaches the ceiling mongod runs out of descriptors mid-run, which surfaces
# inside WiredTiger as a WT_PANIC and crashes the server (EMFILE -> fassert).
# 64000 was not enough headroom (the suite crashed at ~31k collections against
# it), so this is set to 1048576 -- the limit MongoDB's own packaging ships and
# well above the suite's peak. Standalone reclaims dropped files immediately so
# it never approaches the ceiling, but the limit is raised on both for parity.

services:
# mongo-standalone: a single standalone server.
mongo-standalone:
image: mongo:8.2.4
profiles: ["mongo-standalone", "all"]
command: ["--wiredTigerCacheSizeGB", "1.5"]
# Raise the open-file limit above the container default so the suite's
# collection churn cannot exhaust mongod's descriptors (see header note).
ulimits:
nofile:
soft: 1048576
hard: 1048576
ports:
- "27017:27017"
healthcheck:
Expand Down Expand Up @@ -80,6 +103,14 @@ services:
- "skipAuthenticationToMongot=true"
- "--setParameter"
- "skipAuthenticationToSearchIndexManagementServer=true"
# Raise the open-file limit well above the peak. This matters most here: the
# replica set defers dropped-collection files as drop-pending idents, so
# descriptors accumulate under churn and exhaust a lower limit mid-run,
# crashing mongod with a WT_PANIC (see header note).
ulimits:
nofile:
soft: 1048576
hard: 1048576
ports:
- "27018:27017"
healthcheck:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,12 @@ def test_let_nested_combinations(collection, test):
# ---------------------------------------------------------------------------
def test_let_two_lets_same_projection(collection):
"""Test two separate $let expressions in same projection with same variable name."""
collection.insert_one({})
result = execute_command(
collection,
{
"aggregate": 1,
"aggregate": collection.name,
"pipeline": [
{"$documents": [{}]},
{
"$project": {
"_id": 0,
Expand Down Expand Up @@ -248,12 +248,12 @@ def test_let_across_multiple_documents(collection):

def test_let_error_cross_let_variable_ref(collection):
"""Test $let where variable defined in one $let is referenced in sibling $let."""
collection.insert_one({})
result = execute_command(
collection,
{
"aggregate": 1,
"aggregate": collection.name,
"pipeline": [
{"$documents": [{}]},
{
"$project": {
"_id": 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ def execute_project(collection, project):
"""
Execute a projection with literal input values.

Evaluates the projection against a single empty document. The document is
inserted into the collection and the pipeline runs over that collection,
rather than synthesizing the row with a ``$documents`` stage. This keeps the
helper free of any dependency on ``$documents`` support while producing the
same single-row input the projection sees.

Note: the inserted document carries an auto-generated ``_id``, so the input
row is ``{_id: <ObjectId>}`` rather than the field-less row ``$documents:
[{}]`` produces. Literal expressions and references to any other (missing)
field behave identically, and the output projection excludes ``_id``; but an
expression that reads ``$_id``, ``$$ROOT``, or ``$$CURRENT`` sees that id and
will diverge. Callers that need a truly field-less input (e.g. ``$$ROOT`` must
be ``{}``) or exactly one row over a pre-populated collection must shape their
own pipeline instead of using this helper.

Args:
collection: MongoDB collection object
project: Fields to project. Do not include _id; the function always
Expand All @@ -51,12 +66,12 @@ def execute_project(collection, project):
>>> execute_project(collection, {"sum": {"$add": [1, 2]}})
# Returns result with {"sum": 3} in firstBatch
"""
collection.insert_one({})
return execute_command(
collection,
{
"aggregate": 1,
"aggregate": collection.name,
"pipeline": [
{"$documents": [{}]},
{"$project": {**materialize(project), "_id": 0}},
],
"cursor": {},
Expand Down Expand Up @@ -100,10 +115,24 @@ def execute_project_with_insert(collection, document, project):

def execute_expression(collection, expression):
"""
Execute an aggregation expression using $documents stage.

Evaluates an expression against an empty document using the $documents
stage. Useful for testing expressions with literal values.
Execute an aggregation expression against a single empty document.

Evaluates an expression against an empty document. The document is inserted
into the collection and the pipeline runs over that collection, rather than
synthesizing the row with a ``$documents`` stage. This keeps the helper free
of any dependency on ``$documents`` support while producing the same
single-row input the expression is evaluated against. Useful for testing
expressions with literal values; references to fields other than ``_id``
resolve to missing, just as they would against a ``$documents: [{}]`` row.

Note: the inserted document carries an auto-generated ``_id``, so the input
row is ``{_id: <ObjectId>}`` rather than the field-less row ``$documents:
[{}]`` produces. Literal expressions and references to any other (missing)
field are unaffected, and the output projection excludes ``_id``; but an
expression that reads ``$_id``, ``$$ROOT``, or ``$$CURRENT`` sees that id and
will diverge. Callers that need a truly field-less input (e.g. ``$$ROOT`` must
be ``{}``) or exactly one row over a pre-populated collection must shape their
own pipeline instead of using this helper.

Args:
collection: MongoDB collection object
Expand All @@ -117,12 +146,12 @@ def execute_expression(collection, expression):
>>> execute_expression(collection, {"$add": [1, 2]})
# Returns result with {"result": 3} in firstBatch
"""
collection.insert_one({})
return execute_command(
collection,
{
"aggregate": 1,
"aggregate": collection.name,
"pipeline": [
{"$documents": [{}]},
{"$project": {"_id": 0, "result": expression}},
],
"cursor": {},
Expand Down
Loading
Loading