Skip to content

Commit eed4a55

Browse files
authored
PYTHON-694 Test mod_wsgi sub interpreters (mongodb#1327)
Test mod_wsgi sub interpreters and embedded mode. Use unique collection name for each mod_wsgi interpreter. Test encoding/decoding all bson types.
1 parent c259dde commit eed4a55

11 files changed

+224
-101
lines changed

.evergreen/config.yml

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,9 @@ functions:
347347
script: |
348348
set -o xtrace
349349
${PREPARE_SHELL}
350-
PYTHON_BINARY=${PYTHON_BINARY} MOD_WSGI_VERSION=${MOD_WSGI_VERSION} PROJECT_DIRECTORY=${PROJECT_DIRECTORY} bash ${PROJECT_DIRECTORY}/.evergreen/run-mod-wsgi-tests.sh
350+
PYTHON_BINARY=${PYTHON_BINARY} MOD_WSGI_VERSION=${MOD_WSGI_VERSION} \
351+
MOD_WSGI_EMBEDDED=${MOD_WSGI_EMBEDDED} PROJECT_DIRECTORY=${PROJECT_DIRECTORY} \
352+
bash ${PROJECT_DIRECTORY}/.evergreen/run-mod-wsgi-tests.sh
351353
352354
"run mockupdb tests":
353355
- command: shell.exec
@@ -1677,6 +1679,28 @@ tasks:
16771679
TOPOLOGY: "replica_set"
16781680
- func: "run mod_wsgi tests"
16791681

1682+
- name: "mod-wsgi-embedded-mode-standalone"
1683+
tags: ["mod_wsgi"]
1684+
commands:
1685+
- func: "bootstrap mongo-orchestration"
1686+
vars:
1687+
VERSION: "latest"
1688+
TOPOLOGY: "server"
1689+
- func: "run mod_wsgi tests"
1690+
vars:
1691+
MOD_WSGI_EMBEDDED: "1"
1692+
1693+
- name: "mod-wsgi-embedded-mode-replica-set"
1694+
tags: ["mod_wsgi"]
1695+
commands:
1696+
- func: "bootstrap mongo-orchestration"
1697+
vars:
1698+
VERSION: "latest"
1699+
TOPOLOGY: "replica_set"
1700+
- func: "run mod_wsgi tests"
1701+
vars:
1702+
MOD_WSGI_EMBEDDED: "1"
1703+
16801704
- name: "no-server"
16811705
tags: ["no-server"]
16821706
commands:
@@ -3088,6 +3112,8 @@ buildvariants:
30883112
tasks:
30893113
- name: "mod-wsgi-standalone"
30903114
- name: "mod-wsgi-replica-set"
3115+
- name: "mod-wsgi-embedded-mode-standalone"
3116+
- name: "mod-wsgi-embedded-mode-replica-set"
30913117

30923118
- matrix_name: "mockupdb-tests"
30933119
matrix_spec:

.evergreen/run-mod-wsgi-tests.sh

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,30 @@ fi
1818

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

21+
# Ensure the C extensions are installed.
22+
${PYTHON_BINARY} setup.py build_ext -i
23+
2124
export MOD_WSGI_SO=/opt/python/mod_wsgi/python_version/$PYTHON_VERSION/mod_wsgi_version/$MOD_WSGI_VERSION/mod_wsgi.so
2225
export PYTHONHOME=/opt/python/$PYTHON_VERSION
26+
# If MOD_WSGI_EMBEDDED is set use the default embedded mode behavior instead
27+
# of daemon mode (WSGIDaemonProcess).
28+
if [ -n "$MOD_WSGI_EMBEDDED" ]; then
29+
export MOD_WSGI_CONF=mod_wsgi_test_embedded.conf
30+
else
31+
export MOD_WSGI_CONF=mod_wsgi_test.conf
32+
fi
2333

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

28-
set +e
29-
wget -t 1 -T 10 -O - "http://localhost:8080${PROJECT_DIRECTORY}"
30-
STATUS=$?
31-
set -e
32-
33-
# Debug
34-
cat error_log
35-
36-
if [ $STATUS != 0 ]; then
37-
exit $STATUS
38-
fi
38+
wget -t 1 -T 10 -O - "http://localhost:8080/interpreter1${PROJECT_DIRECTORY}" || (cat error_log && exit 1)
39+
wget -t 1 -T 10 -O - "http://localhost:8080/interpreter2${PROJECT_DIRECTORY}" || (cat error_log && exit 1)
3940

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

42-
${PYTHON_BINARY} ${PROJECT_DIRECTORY}/test/mod_wsgi_test/test_client.py -n 25000 serial http://localhost:8080${PROJECT_DIRECTORY}
45+
${PYTHON_BINARY} ${PROJECT_DIRECTORY}/test/mod_wsgi_test/test_client.py -n 25000 serial \
46+
http://localhost:8080/interpreter1${PROJECT_DIRECTORY} http://localhost:8080/interpreter2${PROJECT_DIRECTORY} || \
47+
(tail -n 100 error_log && exit 1)

test/mod_wsgi_test/README.rst

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Test Matrix
1515

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

2121
Setup
@@ -74,31 +74,37 @@ Run the test
7474
Run the included ``test_client.py`` script::
7575

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

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

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

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

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

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

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

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

102107
At MongoDB, Inc. we use a continuous integration job that tests each
103108
combination in the matrix. The job starts up Apache, starts a single server
104109
or replica set, and runs ``test_client.py`` with the proper arguments.
110+
See `run-mod-wsgi-tests.sh <https://github.com/mongodb/mongo-python-driver/blob/master/.evergreen/run-mod-wsgi-tests.sh>`_

test/mod_wsgi_test/apache22amazon.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ CustomLog ${PWD}/access_log combined
3131
Allow from All
3232
</Directory>
3333

34-
Include ${PROJECT_DIRECTORY}/test/mod_wsgi_test/mod_wsgi_test.conf
34+
Include ${PROJECT_DIRECTORY}/test/mod_wsgi_test/${MOD_WSGI_CONF}

test/mod_wsgi_test/apache22ubuntu1204.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ CustomLog ${PWD}/access_log combined
2626
Allow from All
2727
</Directory>
2828

29-
Include ${PROJECT_DIRECTORY}/test/mod_wsgi_test/mod_wsgi_test.conf
29+
Include ${PROJECT_DIRECTORY}/test/mod_wsgi_test/${MOD_WSGI_CONF}

test/mod_wsgi_test/apache24ubuntu161404.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ CustomLog ${PWD}/access_log combined
2525
Require all granted
2626
</Directory>
2727

28-
Include ${PROJECT_DIRECTORY}/test/mod_wsgi_test/mod_wsgi_test.conf
28+
Include ${PROJECT_DIRECTORY}/test/mod_wsgi_test/${MOD_WSGI_CONF}

test/mod_wsgi_test/mod_wsgi_test.conf

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2012-2015 MongoDB, Inc.
1+
# Copyright 2012-present MongoDB, Inc.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -20,17 +20,13 @@ LoadModule wsgi_module ${MOD_WSGI_SO}
2020
WSGISocketPrefix /tmp/
2121

2222
<VirtualHost *>
23-
2423
ServerName localhost
25-
2624
WSGIDaemonProcess mod_wsgi_test processes=1 threads=15 display-name=mod_wsgi_test
27-
2825
WSGIProcessGroup mod_wsgi_test
29-
26+
# Mount the script twice so that multiple interpreters are used.
3027
# For the convenience of unittests, rather than hard-code the location of
31-
# mod_wsgi_test.wsgi, include it in the URL, so
32-
# http://localhost/location-of-pymongo-checkout will work:
33-
34-
WSGIScriptAliasMatch ^/(.+) $1/test/mod_wsgi_test/mod_wsgi_test.wsgi
35-
28+
# mod_wsgi_test.py, include it in the URL, so
29+
# http://localhost/interpreter1/location-of-pymongo-checkout will work:
30+
WSGIScriptAliasMatch ^/interpreter1/(.+) $1/test/mod_wsgi_test/mod_wsgi_test.py
31+
WSGIScriptAliasMatch ^/interpreter2/(.+) $1/test/mod_wsgi_test/mod_wsgi_test.py
3632
</VirtualHost>

test/mod_wsgi_test/mod_wsgi_test.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Copyright 2012-present MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Minimal test of PyMongo in a WSGI application, see bug PYTHON-353
16+
"""
17+
18+
import datetime
19+
import os
20+
import re
21+
import sys
22+
import uuid
23+
24+
this_path = os.path.dirname(os.path.join(os.getcwd(), __file__))
25+
26+
# Location of PyMongo checkout
27+
repository_path = os.path.normpath(os.path.join(this_path, "..", ".."))
28+
sys.path.insert(0, repository_path)
29+
30+
import bson
31+
import pymongo
32+
from bson.binary import STANDARD, Binary
33+
from bson.code import Code
34+
from bson.codec_options import CodecOptions
35+
from bson.datetime_ms import DatetimeConversion, DatetimeMS
36+
from bson.dbref import DBRef
37+
from bson.objectid import ObjectId
38+
from bson.regex import Regex
39+
from pymongo.mongo_client import MongoClient
40+
41+
# Ensure the C extensions are installed.
42+
assert bson.has_c()
43+
assert pymongo.has_c()
44+
45+
OPTS: "CodecOptions[dict]" = CodecOptions(
46+
uuid_representation=STANDARD, datetime_conversion=DatetimeConversion.DATETIME_AUTO
47+
)
48+
client: "MongoClient[dict]" = MongoClient()
49+
# Use a unique collection name for each process:
50+
coll_name = f"test-{uuid.uuid4()}"
51+
collection = client.test.get_collection(coll_name, codec_options=OPTS)
52+
ndocs = 20
53+
collection.drop()
54+
doc = {
55+
"int32": 2 << 15,
56+
"int64": 2 << 50,
57+
"null": None,
58+
"bool": True,
59+
"float": 1.5,
60+
"str": "string",
61+
"list": [1, 2, 3],
62+
"dict": {"a": 1, "b": 2, "c": 3},
63+
"datetime": datetime.datetime.fromtimestamp(1690328577.446),
64+
"datetime_ms_out_of_range": DatetimeMS(-2 << 60),
65+
"regex_native": re.compile("regex*"),
66+
"regex_pymongo": Regex("regex*"),
67+
"binary": Binary(b"bytes", 128),
68+
"oid": ObjectId(),
69+
"dbref": DBRef("test", 1),
70+
"code": Code("function(){ return true; }"),
71+
"code_w_scope": Code("return function(){ return x; }", scope={"x": False}),
72+
"bytes": b"bytes",
73+
"uuid": uuid.uuid4(),
74+
}
75+
collection.insert_many([dict(i=i, **doc) for i in range(ndocs)])
76+
client.close() # Discard main thread's request socket.
77+
client = MongoClient()
78+
collection = client.test.get_collection(coll_name, codec_options=OPTS)
79+
80+
try:
81+
from mod_wsgi import version as mod_wsgi_version # type: ignore[import]
82+
except:
83+
mod_wsgi_version = None
84+
85+
86+
def application(environ, start_response):
87+
results = list(collection.find().batch_size(10))
88+
assert len(results) == ndocs, f"n_actual={len(results)} n_expected={ndocs}"
89+
# Test encoding and decoding works (for sub interpreter support).
90+
decoded = bson.decode(bson.encode(doc, codec_options=OPTS), codec_options=OPTS)
91+
for key, value in doc.items():
92+
# Native regex objects are decoded as bson Regex.
93+
if isinstance(value, re.Pattern):
94+
value = Regex.from_native(value)
95+
assert decoded[key] == value, f"failed on doc[{key!r}]: {decoded[key]!r} != {value!r}"
96+
assert isinstance(
97+
decoded[key], type(value)
98+
), f"failed on doc[{key}]: {decoded[key]!r} is not an instance of {type(value)}"
99+
100+
output = (
101+
f" python {sys.version}, mod_wsgi {mod_wsgi_version},"
102+
f" pymongo {pymongo.version},"
103+
f' mod_wsgi.process_group = {environ["mod_wsgi.process_group"]!r}'
104+
f' mod_wsgi.application_group = {environ["mod_wsgi.application_group"]!r}'
105+
f' wsgi.multithread = {environ["wsgi.multithread"]!r}'
106+
"\n"
107+
)
108+
response_headers = [("Content-Length", str(len(output)))]
109+
start_response("200 OK", response_headers)
110+
return [output.encode("ascii")]

test/mod_wsgi_test/mod_wsgi_test.wsgi

Lines changed: 0 additions & 53 deletions
This file was deleted.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright 2023-present MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# Minimal test of PyMongo in an *Embedded mode* WSGI application.
16+
17+
LoadModule wsgi_module ${MOD_WSGI_SO}
18+
19+
# Avoid permissions issues
20+
WSGISocketPrefix /tmp/
21+
22+
<VirtualHost *>
23+
ServerName localhost
24+
# Mount the script twice so that multiple interpreters are used.
25+
# For the convenience of unittests, rather than hard-code the location of
26+
# mod_wsgi_test.py, include it in the URL, so
27+
# http://localhost/interpreter1/location-of-pymongo-checkout will work:
28+
WSGIScriptAliasMatch ^/interpreter1/(.+) $1/test/mod_wsgi_test/mod_wsgi_test.py
29+
WSGIScriptAliasMatch ^/interpreter2/(.+) $1/test/mod_wsgi_test/mod_wsgi_test.py
30+
</VirtualHost>

0 commit comments

Comments
 (0)