Skip to content

Commit

Permalink
PYTHON-694 Test mod_wsgi sub interpreters (#1327)
Browse files Browse the repository at this point in the history
Test mod_wsgi sub interpreters and embedded mode.
Use unique collection name for each mod_wsgi interpreter.
Test encoding/decoding all bson types.
  • Loading branch information
ShaneHarvey authored Jul 27, 2023
1 parent c259dde commit eed4a55
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 101 deletions.
28 changes: 27 additions & 1 deletion .evergreen/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,9 @@ functions:
script: |
set -o xtrace
${PREPARE_SHELL}
PYTHON_BINARY=${PYTHON_BINARY} MOD_WSGI_VERSION=${MOD_WSGI_VERSION} PROJECT_DIRECTORY=${PROJECT_DIRECTORY} bash ${PROJECT_DIRECTORY}/.evergreen/run-mod-wsgi-tests.sh
PYTHON_BINARY=${PYTHON_BINARY} MOD_WSGI_VERSION=${MOD_WSGI_VERSION} \
MOD_WSGI_EMBEDDED=${MOD_WSGI_EMBEDDED} PROJECT_DIRECTORY=${PROJECT_DIRECTORY} \
bash ${PROJECT_DIRECTORY}/.evergreen/run-mod-wsgi-tests.sh
"run mockupdb tests":
- command: shell.exec
Expand Down Expand Up @@ -1677,6 +1679,28 @@ tasks:
TOPOLOGY: "replica_set"
- func: "run mod_wsgi tests"

- name: "mod-wsgi-embedded-mode-standalone"
tags: ["mod_wsgi"]
commands:
- func: "bootstrap mongo-orchestration"
vars:
VERSION: "latest"
TOPOLOGY: "server"
- func: "run mod_wsgi tests"
vars:
MOD_WSGI_EMBEDDED: "1"

- name: "mod-wsgi-embedded-mode-replica-set"
tags: ["mod_wsgi"]
commands:
- func: "bootstrap mongo-orchestration"
vars:
VERSION: "latest"
TOPOLOGY: "replica_set"
- func: "run mod_wsgi tests"
vars:
MOD_WSGI_EMBEDDED: "1"

- name: "no-server"
tags: ["no-server"]
commands:
Expand Down Expand Up @@ -3088,6 +3112,8 @@ buildvariants:
tasks:
- name: "mod-wsgi-standalone"
- name: "mod-wsgi-replica-set"
- name: "mod-wsgi-embedded-mode-standalone"
- name: "mod-wsgi-embedded-mode-replica-set"

- matrix_name: "mockupdb-tests"
matrix_spec:
Expand Down
31 changes: 18 additions & 13 deletions .evergreen/run-mod-wsgi-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,30 @@ fi

PYTHON_VERSION=$(${PYTHON_BINARY} -c "import sys; sys.stdout.write('.'.join(str(val) for val in sys.version_info[:2]))")

# Ensure the C extensions are installed.
${PYTHON_BINARY} setup.py build_ext -i

export MOD_WSGI_SO=/opt/python/mod_wsgi/python_version/$PYTHON_VERSION/mod_wsgi_version/$MOD_WSGI_VERSION/mod_wsgi.so
export PYTHONHOME=/opt/python/$PYTHON_VERSION
# If MOD_WSGI_EMBEDDED is set use the default embedded mode behavior instead
# of daemon mode (WSGIDaemonProcess).
if [ -n "$MOD_WSGI_EMBEDDED" ]; then
export MOD_WSGI_CONF=mod_wsgi_test_embedded.conf
else
export MOD_WSGI_CONF=mod_wsgi_test.conf
fi

cd ..
$APACHE -k start -f ${PROJECT_DIRECTORY}/test/mod_wsgi_test/${APACHE_CONFIG}
trap '$APACHE -k stop -f ${PROJECT_DIRECTORY}/test/mod_wsgi_test/${APACHE_CONFIG}' EXIT HUP

set +e
wget -t 1 -T 10 -O - "http://localhost:8080${PROJECT_DIRECTORY}"
STATUS=$?
set -e

# Debug
cat error_log

if [ $STATUS != 0 ]; then
exit $STATUS
fi
wget -t 1 -T 10 -O - "http://localhost:8080/interpreter1${PROJECT_DIRECTORY}" || (cat error_log && exit 1)
wget -t 1 -T 10 -O - "http://localhost:8080/interpreter2${PROJECT_DIRECTORY}" || (cat error_log && exit 1)

${PYTHON_BINARY} ${PROJECT_DIRECTORY}/test/mod_wsgi_test/test_client.py -n 25000 -t 100 parallel http://localhost:8080${PROJECT_DIRECTORY}
${PYTHON_BINARY} ${PROJECT_DIRECTORY}/test/mod_wsgi_test/test_client.py -n 25000 -t 100 parallel \
http://localhost:8080/interpreter1${PROJECT_DIRECTORY} http://localhost:8080/interpreter2${PROJECT_DIRECTORY} || \
(tail -n 100 error_log && exit 1)

${PYTHON_BINARY} ${PROJECT_DIRECTORY}/test/mod_wsgi_test/test_client.py -n 25000 serial http://localhost:8080${PROJECT_DIRECTORY}
${PYTHON_BINARY} ${PROJECT_DIRECTORY}/test/mod_wsgi_test/test_client.py -n 25000 serial \
http://localhost:8080/interpreter1${PROJECT_DIRECTORY} http://localhost:8080/interpreter2${PROJECT_DIRECTORY} || \
(tail -n 100 error_log && exit 1)
16 changes: 11 additions & 5 deletions test/mod_wsgi_test/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Test Matrix

PyMongo should be tested with several versions of mod_wsgi and a selection
of Python versions. Each combination of mod_wsgi and Python version should
be tested with a standalone and a replica set. ``mod_wsgi_test.wsgi``
be tested with a standalone and a replica set. ``mod_wsgi_test.py``
detects if the deployment is a replica set and connects to the whole set.

Setup
Expand Down Expand Up @@ -74,31 +74,37 @@ Run the test
Run the included ``test_client.py`` script::

python test/mod_wsgi_test/test_client.py -n 2500 -t 100 parallel \
http://localhost/${WORKSPACE}
http://localhost/interpreter1${WORKSPACE} http://localhost/interpreter2${WORKSPACE}

...where the "n" argument is the total number of requests to make to Apache,
and "t" specifies the number of threads. ``WORKSPACE`` is the location of
the PyMongo checkout.
the PyMongo checkout. Note that multiple URLs are passed, each one corresponds
to a different sub interpreter.

Run this script again with different arguments to make serial requests::

python test/mod_wsgi_test/test_client.py -n 25000 serial \
http://localhost/${WORKSPACE}
http://localhost/interpreter1${WORKSPACE} http://localhost/interpreter2${WORKSPACE}

The ``test_client.py`` script merely makes HTTP requests to Apache. Its
exit code is non-zero if any of its requests fails, for example with an
HTTP 500.

The core of the test is in the WSGI script, ``mod_wsgi_test.wsgi``.
The core of the test is in the WSGI script, ``mod_wsgi_test.py``.
This script inserts some documents into MongoDB at startup, then queries
documents for each HTTP request.

If PyMongo is leaking connections and "n" is much greater than the ulimit,
the test will fail when PyMongo exhausts its file descriptors.

The script also encodes and decodes all BSON types to ensure that
multiple sub interpreters in the same process are supported. This tests
the workaround added in `PYTHON-569 <https://jira.mongodb.org/browse/PYTHON-569>`_.

Automation
----------

At MongoDB, Inc. we use a continuous integration job that tests each
combination in the matrix. The job starts up Apache, starts a single server
or replica set, and runs ``test_client.py`` with the proper arguments.
See `run-mod-wsgi-tests.sh <https://github.com/mongodb/mongo-python-driver/blob/master/.evergreen/run-mod-wsgi-tests.sh>`_
2 changes: 1 addition & 1 deletion test/mod_wsgi_test/apache22amazon.conf
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ CustomLog ${PWD}/access_log combined
Allow from All
</Directory>

Include ${PROJECT_DIRECTORY}/test/mod_wsgi_test/mod_wsgi_test.conf
Include ${PROJECT_DIRECTORY}/test/mod_wsgi_test/${MOD_WSGI_CONF}
2 changes: 1 addition & 1 deletion test/mod_wsgi_test/apache22ubuntu1204.conf
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ CustomLog ${PWD}/access_log combined
Allow from All
</Directory>

Include ${PROJECT_DIRECTORY}/test/mod_wsgi_test/mod_wsgi_test.conf
Include ${PROJECT_DIRECTORY}/test/mod_wsgi_test/${MOD_WSGI_CONF}
2 changes: 1 addition & 1 deletion test/mod_wsgi_test/apache24ubuntu161404.conf
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ CustomLog ${PWD}/access_log combined
Require all granted
</Directory>

Include ${PROJECT_DIRECTORY}/test/mod_wsgi_test/mod_wsgi_test.conf
Include ${PROJECT_DIRECTORY}/test/mod_wsgi_test/${MOD_WSGI_CONF}
16 changes: 6 additions & 10 deletions test/mod_wsgi_test/mod_wsgi_test.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2012-2015 MongoDB, Inc.
# Copyright 2012-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -20,17 +20,13 @@ LoadModule wsgi_module ${MOD_WSGI_SO}
WSGISocketPrefix /tmp/

<VirtualHost *>

ServerName localhost

WSGIDaemonProcess mod_wsgi_test processes=1 threads=15 display-name=mod_wsgi_test

WSGIProcessGroup mod_wsgi_test

# Mount the script twice so that multiple interpreters are used.
# For the convenience of unittests, rather than hard-code the location of
# mod_wsgi_test.wsgi, include it in the URL, so
# http://localhost/location-of-pymongo-checkout will work:

WSGIScriptAliasMatch ^/(.+) $1/test/mod_wsgi_test/mod_wsgi_test.wsgi

# mod_wsgi_test.py, include it in the URL, so
# http://localhost/interpreter1/location-of-pymongo-checkout will work:
WSGIScriptAliasMatch ^/interpreter1/(.+) $1/test/mod_wsgi_test/mod_wsgi_test.py
WSGIScriptAliasMatch ^/interpreter2/(.+) $1/test/mod_wsgi_test/mod_wsgi_test.py
</VirtualHost>
110 changes: 110 additions & 0 deletions test/mod_wsgi_test/mod_wsgi_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Copyright 2012-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Minimal test of PyMongo in a WSGI application, see bug PYTHON-353
"""

import datetime
import os
import re
import sys
import uuid

this_path = os.path.dirname(os.path.join(os.getcwd(), __file__))

# Location of PyMongo checkout
repository_path = os.path.normpath(os.path.join(this_path, "..", ".."))
sys.path.insert(0, repository_path)

import bson
import pymongo
from bson.binary import STANDARD, Binary
from bson.code import Code
from bson.codec_options import CodecOptions
from bson.datetime_ms import DatetimeConversion, DatetimeMS
from bson.dbref import DBRef
from bson.objectid import ObjectId
from bson.regex import Regex
from pymongo.mongo_client import MongoClient

# Ensure the C extensions are installed.
assert bson.has_c()
assert pymongo.has_c()

OPTS: "CodecOptions[dict]" = CodecOptions(
uuid_representation=STANDARD, datetime_conversion=DatetimeConversion.DATETIME_AUTO
)
client: "MongoClient[dict]" = MongoClient()
# Use a unique collection name for each process:
coll_name = f"test-{uuid.uuid4()}"
collection = client.test.get_collection(coll_name, codec_options=OPTS)
ndocs = 20
collection.drop()
doc = {
"int32": 2 << 15,
"int64": 2 << 50,
"null": None,
"bool": True,
"float": 1.5,
"str": "string",
"list": [1, 2, 3],
"dict": {"a": 1, "b": 2, "c": 3},
"datetime": datetime.datetime.fromtimestamp(1690328577.446),
"datetime_ms_out_of_range": DatetimeMS(-2 << 60),
"regex_native": re.compile("regex*"),
"regex_pymongo": Regex("regex*"),
"binary": Binary(b"bytes", 128),
"oid": ObjectId(),
"dbref": DBRef("test", 1),
"code": Code("function(){ return true; }"),
"code_w_scope": Code("return function(){ return x; }", scope={"x": False}),
"bytes": b"bytes",
"uuid": uuid.uuid4(),
}
collection.insert_many([dict(i=i, **doc) for i in range(ndocs)])
client.close() # Discard main thread's request socket.
client = MongoClient()
collection = client.test.get_collection(coll_name, codec_options=OPTS)

try:
from mod_wsgi import version as mod_wsgi_version # type: ignore[import]
except:
mod_wsgi_version = None


def application(environ, start_response):
results = list(collection.find().batch_size(10))
assert len(results) == ndocs, f"n_actual={len(results)} n_expected={ndocs}"
# Test encoding and decoding works (for sub interpreter support).
decoded = bson.decode(bson.encode(doc, codec_options=OPTS), codec_options=OPTS)
for key, value in doc.items():
# Native regex objects are decoded as bson Regex.
if isinstance(value, re.Pattern):
value = Regex.from_native(value)
assert decoded[key] == value, f"failed on doc[{key!r}]: {decoded[key]!r} != {value!r}"
assert isinstance(
decoded[key], type(value)
), f"failed on doc[{key}]: {decoded[key]!r} is not an instance of {type(value)}"

output = (
f" python {sys.version}, mod_wsgi {mod_wsgi_version},"
f" pymongo {pymongo.version},"
f' mod_wsgi.process_group = {environ["mod_wsgi.process_group"]!r}'
f' mod_wsgi.application_group = {environ["mod_wsgi.application_group"]!r}'
f' wsgi.multithread = {environ["wsgi.multithread"]!r}'
"\n"
)
response_headers = [("Content-Length", str(len(output)))]
start_response("200 OK", response_headers)
return [output.encode("ascii")]
53 changes: 0 additions & 53 deletions test/mod_wsgi_test/mod_wsgi_test.wsgi

This file was deleted.

30 changes: 30 additions & 0 deletions test/mod_wsgi_test/mod_wsgi_test_embedded.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright 2023-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Minimal test of PyMongo in an *Embedded mode* WSGI application.

LoadModule wsgi_module ${MOD_WSGI_SO}

# Avoid permissions issues
WSGISocketPrefix /tmp/

<VirtualHost *>
ServerName localhost
# Mount the script twice so that multiple interpreters are used.
# For the convenience of unittests, rather than hard-code the location of
# mod_wsgi_test.py, include it in the URL, so
# http://localhost/interpreter1/location-of-pymongo-checkout will work:
WSGIScriptAliasMatch ^/interpreter1/(.+) $1/test/mod_wsgi_test/mod_wsgi_test.py
WSGIScriptAliasMatch ^/interpreter2/(.+) $1/test/mod_wsgi_test/mod_wsgi_test.py
</VirtualHost>
Loading

0 comments on commit eed4a55

Please sign in to comment.