Skip to content

Commit 669ff0a

Browse files
authored
Merge branch 'master' into PYTHON-5081
2 parents 6827f71 + b922868 commit 669ff0a

File tree

12 files changed

+1210
-57
lines changed

12 files changed

+1210
-57
lines changed

.evergreen/generated_configs/tasks.yml

Lines changed: 540 additions & 0 deletions
Large diffs are not rendered by default.

.evergreen/generated_configs/variants.yml

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -817,97 +817,110 @@ buildvariants:
817817
PYTHON_BINARY: /opt/python/3.13/bin/python3
818818

819819
# Ocsp tests
820-
- name: ocsp-rhel8-v4.4-python3.9
820+
- name: ocsp-rhel8-v4.2-python3.9
821821
tasks:
822822
- name: .ocsp
823-
display_name: OCSP RHEL8 v4.4 Python3.9
823+
display_name: OCSP RHEL8 v4.2 Python3.9
824824
run_on:
825825
- rhel87-small
826826
batchtime: 20160
827827
expansions:
828828
AUTH: noauth
829829
SSL: ssl
830830
TOPOLOGY: server
831-
VERSION: "4.4"
831+
VERSION: "4.2"
832832
PYTHON_BINARY: /opt/python/3.9/bin/python3
833-
- name: ocsp-rhel8-v5.0-python3.10
833+
- name: ocsp-rhel8-v4.4-python3.10
834834
tasks:
835835
- name: .ocsp
836-
display_name: OCSP RHEL8 v5.0 Python3.10
836+
display_name: OCSP RHEL8 v4.4 Python3.10
837837
run_on:
838838
- rhel87-small
839839
batchtime: 20160
840840
expansions:
841841
AUTH: noauth
842842
SSL: ssl
843843
TOPOLOGY: server
844-
VERSION: "5.0"
844+
VERSION: "4.4"
845845
PYTHON_BINARY: /opt/python/3.10/bin/python3
846-
- name: ocsp-rhel8-v6.0-python3.11
846+
- name: ocsp-rhel8-v5.0-python3.11
847847
tasks:
848848
- name: .ocsp
849-
display_name: OCSP RHEL8 v6.0 Python3.11
849+
display_name: OCSP RHEL8 v5.0 Python3.11
850850
run_on:
851851
- rhel87-small
852852
batchtime: 20160
853853
expansions:
854854
AUTH: noauth
855855
SSL: ssl
856856
TOPOLOGY: server
857-
VERSION: "6.0"
857+
VERSION: "5.0"
858858
PYTHON_BINARY: /opt/python/3.11/bin/python3
859-
- name: ocsp-rhel8-v7.0-python3.12
859+
- name: ocsp-rhel8-v6.0-python3.12
860860
tasks:
861861
- name: .ocsp
862-
display_name: OCSP RHEL8 v7.0 Python3.12
862+
display_name: OCSP RHEL8 v6.0 Python3.12
863863
run_on:
864864
- rhel87-small
865865
batchtime: 20160
866866
expansions:
867867
AUTH: noauth
868868
SSL: ssl
869869
TOPOLOGY: server
870-
VERSION: "7.0"
870+
VERSION: "6.0"
871871
PYTHON_BINARY: /opt/python/3.12/bin/python3
872-
- name: ocsp-rhel8-v8.0-python3.13
872+
- name: ocsp-rhel8-v7.0-python3.13
873873
tasks:
874874
- name: .ocsp
875-
display_name: OCSP RHEL8 v8.0 Python3.13
875+
display_name: OCSP RHEL8 v7.0 Python3.13
876876
run_on:
877877
- rhel87-small
878878
batchtime: 20160
879879
expansions:
880880
AUTH: noauth
881881
SSL: ssl
882882
TOPOLOGY: server
883-
VERSION: "8.0"
883+
VERSION: "7.0"
884884
PYTHON_BINARY: /opt/python/3.13/bin/python3
885-
- name: ocsp-rhel8-rapid-pypy3.10
885+
- name: ocsp-rhel8-v8.0-pypy3.10
886886
tasks:
887887
- name: .ocsp
888-
display_name: OCSP RHEL8 rapid PyPy3.10
888+
display_name: OCSP RHEL8 v8.0 PyPy3.10
889889
run_on:
890890
- rhel87-small
891891
batchtime: 20160
892892
expansions:
893893
AUTH: noauth
894894
SSL: ssl
895895
TOPOLOGY: server
896-
VERSION: rapid
896+
VERSION: "8.0"
897897
PYTHON_BINARY: /opt/python/pypy3.10/bin/python3
898-
- name: ocsp-rhel8-latest-python3.9
898+
- name: ocsp-rhel8-rapid-python3.9
899899
tasks:
900900
- name: .ocsp
901-
display_name: OCSP RHEL8 latest Python3.9
901+
display_name: OCSP RHEL8 rapid Python3.9
902902
run_on:
903903
- rhel87-small
904904
batchtime: 20160
905905
expansions:
906906
AUTH: noauth
907907
SSL: ssl
908908
TOPOLOGY: server
909-
VERSION: latest
909+
VERSION: rapid
910910
PYTHON_BINARY: /opt/python/3.9/bin/python3
911+
- name: ocsp-rhel8-latest-python3.10
912+
tasks:
913+
- name: .ocsp
914+
display_name: OCSP RHEL8 latest Python3.10
915+
run_on:
916+
- rhel87-small
917+
batchtime: 20160
918+
expansions:
919+
AUTH: noauth
920+
SSL: ssl
921+
TOPOLOGY: server
922+
VERSION: latest
923+
PYTHON_BINARY: /opt/python/3.10/bin/python3
911924
- name: ocsp-win64-v4.4-python3.9
912925
tasks:
913926
- name: .ocsp-rsa !.ocsp-staple
@@ -1338,6 +1351,7 @@ buildvariants:
13381351
- name: storage-inmemory-rhel8-python3.9
13391352
tasks:
13401353
- name: .standalone .noauth .nossl .4.0 .sync_async
1354+
- name: .standalone .noauth .nossl .4.2 .sync_async
13411355
- name: .standalone .noauth .nossl .4.4 .sync_async
13421356
- name: .standalone .noauth .nossl .5.0 .sync_async
13431357
- name: .standalone .noauth .nossl .6.0 .sync_async

.evergreen/scripts/generate_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
# Globals
2727
##############
2828

29-
ALL_VERSIONS = ["4.0", "4.4", "5.0", "6.0", "7.0", "8.0", "rapid", "latest"]
29+
ALL_VERSIONS = ["4.0", "4.2", "4.4", "5.0", "6.0", "7.0", "8.0", "rapid", "latest"]
3030
CPYTHONS = ["3.9", "3.10", "3.11", "3.12", "3.13"]
3131
PYPYS = ["pypy3.10"]
3232
ALL_PYTHONS = CPYTHONS + PYPYS

.github/workflows/dist.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,9 @@ jobs:
3535
# https://github.com/github/feedback/discussions/7835#discussioncomment-1769026
3636
buildplat:
3737
- [ubuntu-20.04, "manylinux_x86_64", "cp3*-manylinux_x86_64"]
38-
- [ubuntu-24.04-arm, "manylinux_aarch64", "cp3*-manylinux_aarch64"]
39-
# Disabled pending PYTHON-5058
40-
# - [ubuntu-24.04, "manylinux_ppc64le", "cp3*-manylinux_ppc64le"]
41-
# - [ubuntu-24.04, "manylinux_s390x", "cp3*-manylinux_s390x"]
38+
- [ubuntu-20.04, "manylinux_aarch64", "cp3*-manylinux_aarch64"]
39+
- [ubuntu-20.04, "manylinux_ppc64le", "cp3*-manylinux_ppc64le"]
40+
- [ubuntu-20.04, "manylinux_s390x", "cp3*-manylinux_s390x"]
4241
- [ubuntu-20.04, "manylinux_i686", "cp3*-manylinux_i686"]
4342
- [windows-2019, "win_amd6", "cp3*-win_amd64"]
4443
- [windows-2019, "win32", "cp3*-win32"]
@@ -63,6 +62,10 @@ jobs:
6362
if: runner.os == 'Linux'
6463
uses: docker/setup-qemu-action@v3
6564
with:
65+
# setup-qemu-action by default uses `tonistiigi/binfmt:latest` image,
66+
# which is out of date. This causes seg faults during build.
67+
# Here we manually fix the version.
68+
image: tonistiigi/binfmt:qemu-v8.1.5
6669
platforms: all
6770

6871
- name: Install cibuildwheel

.github/workflows/release-python.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jobs:
3737
pre-publish:
3838
environment: release
3939
runs-on: ubuntu-latest
40+
if: github.repository_owner == 'mongodb' || github.event_name == 'workflow_dispatch'
4041
permissions:
4142
id-token: write
4243
contents: write

CONTRIBUTING.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,11 @@ To prevent the `synchro` hook from accidentally overwriting code, it first check
261261
of a file is changing and not its async counterpart, and will fail.
262262
In the unlikely scenario that you want to override this behavior, first export `OVERRIDE_SYNCHRO_CHECK=1`.
263263

264+
Sometimes, the `synchro` hook will fail and introduce changes many previously unmodified files. This is due to static
265+
Python errors, such as missing imports, incorrect syntax, or other fatal typos. To resolve these issues,
266+
run `pre-commit run --all-files --hook-stage manual ruff` and fix all reported errors before running the `synchro`
267+
hook again.
268+
264269
## Converting a test to async
265270
The `tools/convert_test_to_async.py` script takes in an existing synchronous test file and outputs a
266271
partially-converted asynchronous version of the same name to the `test/asynchronous` directory.
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# Copyright 2015-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+
"""Test AsyncMongoClient's mongos load balancing using a mock."""
16+
from __future__ import annotations
17+
18+
import asyncio
19+
import sys
20+
import threading
21+
from test.asynchronous.helpers import ConcurrentRunner
22+
23+
from pymongo.operations import _Op
24+
25+
sys.path[0:0] = [""]
26+
27+
from test.asynchronous import AsyncMockClientTest, async_client_context, connected, unittest
28+
from test.asynchronous.pymongo_mocks import AsyncMockClient
29+
from test.utils import async_wait_until
30+
31+
from pymongo.errors import AutoReconnect, InvalidOperation
32+
from pymongo.server_selectors import writable_server_selector
33+
from pymongo.topology_description import TOPOLOGY_TYPE
34+
35+
_IS_SYNC = False
36+
37+
38+
class SimpleOp(ConcurrentRunner):
39+
def __init__(self, client):
40+
super().__init__()
41+
self.client = client
42+
self.passed = False
43+
44+
async def run(self):
45+
await self.client.db.command("ping")
46+
self.passed = True # No exception raised.
47+
48+
49+
async def do_simple_op(client, ntasks):
50+
tasks = [SimpleOp(client) for _ in range(ntasks)]
51+
for t in tasks:
52+
await t.start()
53+
54+
for t in tasks:
55+
await t.join()
56+
57+
for t in tasks:
58+
assert t.passed
59+
60+
61+
async def writable_addresses(topology):
62+
return {
63+
server.description.address
64+
for server in await topology.select_servers(writable_server_selector, _Op.TEST)
65+
}
66+
67+
68+
class TestMongosLoadBalancing(AsyncMockClientTest):
69+
@async_client_context.require_connection
70+
@async_client_context.require_no_load_balancer
71+
async def asyncSetUp(self):
72+
await super().asyncSetUp()
73+
74+
def mock_client(self, **kwargs):
75+
mock_client = AsyncMockClient(
76+
standalones=[],
77+
members=[],
78+
mongoses=["a:1", "b:2", "c:3"],
79+
host="a:1,b:2,c:3",
80+
connect=False,
81+
**kwargs,
82+
)
83+
self.addAsyncCleanup(mock_client.aclose)
84+
85+
# Latencies in seconds.
86+
mock_client.mock_rtts["a:1"] = 0.020
87+
mock_client.mock_rtts["b:2"] = 0.025
88+
mock_client.mock_rtts["c:3"] = 0.045
89+
return mock_client
90+
91+
async def test_lazy_connect(self):
92+
# While connected() ensures we can trigger connection from the main
93+
# thread and wait for the monitors, this test triggers connection from
94+
# several threads at once to check for data races.
95+
nthreads = 10
96+
client = self.mock_client()
97+
self.assertEqual(0, len(client.nodes))
98+
99+
# Trigger initial connection.
100+
await do_simple_op(client, nthreads)
101+
await async_wait_until(lambda: len(client.nodes) == 3, "connect to all mongoses")
102+
103+
async def test_failover(self):
104+
ntasks = 10
105+
client = await connected(self.mock_client(localThresholdMS=0.001))
106+
await async_wait_until(lambda: len(client.nodes) == 3, "connect to all mongoses")
107+
108+
# Our chosen mongos goes down.
109+
client.kill_host("a:1")
110+
111+
# Trigger failover to higher-latency nodes. AutoReconnect should be
112+
# raised at most once in each thread.
113+
passed = []
114+
115+
async def f():
116+
try:
117+
await client.db.command("ping")
118+
except AutoReconnect:
119+
# Second attempt succeeds.
120+
await client.db.command("ping")
121+
122+
passed.append(True)
123+
124+
tasks = [ConcurrentRunner(target=f) for _ in range(ntasks)]
125+
for t in tasks:
126+
await t.start()
127+
128+
for t in tasks:
129+
await t.join()
130+
131+
self.assertEqual(ntasks, len(passed))
132+
133+
# Down host removed from list.
134+
self.assertEqual(2, len(client.nodes))
135+
136+
async def test_local_threshold(self):
137+
client = await connected(self.mock_client(localThresholdMS=30))
138+
self.assertEqual(30, client.options.local_threshold_ms)
139+
await async_wait_until(lambda: len(client.nodes) == 3, "connect to all mongoses")
140+
topology = client._topology
141+
142+
# All are within a 30-ms latency window, see self.mock_client().
143+
self.assertEqual({("a", 1), ("b", 2), ("c", 3)}, await writable_addresses(topology))
144+
145+
# No error
146+
await client.admin.command("ping")
147+
148+
client = await connected(self.mock_client(localThresholdMS=0))
149+
self.assertEqual(0, client.options.local_threshold_ms)
150+
# No error
151+
await client.db.command("ping")
152+
# Our chosen mongos goes down.
153+
client.kill_host("{}:{}".format(*next(iter(client.nodes))))
154+
try:
155+
await client.db.command("ping")
156+
except:
157+
pass
158+
159+
# We eventually connect to a new mongos.
160+
async def connect_to_new_mongos():
161+
try:
162+
return await client.db.command("ping")
163+
except AutoReconnect:
164+
pass
165+
166+
await async_wait_until(connect_to_new_mongos, "connect to a new mongos")
167+
168+
async def test_load_balancing(self):
169+
# Although the server selection JSON tests already prove that
170+
# select_servers works for sharded topologies, here we do an end-to-end
171+
# test of discovering servers' round trip times and configuring
172+
# localThresholdMS.
173+
client = await connected(self.mock_client())
174+
await async_wait_until(lambda: len(client.nodes) == 3, "connect to all mongoses")
175+
176+
# Prohibited for topology type Sharded.
177+
with self.assertRaises(InvalidOperation):
178+
await client.address
179+
180+
topology = client._topology
181+
self.assertEqual(TOPOLOGY_TYPE.Sharded, topology.description.topology_type)
182+
183+
# a and b are within the 15-ms latency window, see self.mock_client().
184+
self.assertEqual({("a", 1), ("b", 2)}, await writable_addresses(topology))
185+
186+
client.mock_rtts["a:1"] = 0.045
187+
188+
# Discover only b is within latency window.
189+
async def predicate():
190+
return {("b", 2)} == await writable_addresses(topology)
191+
192+
await async_wait_until(
193+
predicate,
194+
'discover server "a" is too far',
195+
)
196+
197+
198+
if __name__ == "__main__":
199+
unittest.main()

0 commit comments

Comments
 (0)