Compare commits
55 Commits
spec-resyn
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a8e34c726 | ||
|
|
552b7bf47b | ||
|
|
a50550535d | ||
|
|
0adf6df131 | ||
|
|
f145c7db94 | ||
|
|
b6bac45c7e | ||
|
|
8dc7efade2 | ||
|
|
f4219bdca2 | ||
|
|
900d9c7910 | ||
|
|
575d75f4d3 | ||
|
|
c30eff1291 | ||
|
|
e67931dff7 | ||
|
|
64edd22d73 | ||
|
|
b3f1c4befb | ||
|
|
ab44a21b46 | ||
|
|
a13842f351 | ||
|
|
8363bf60ad | ||
|
|
5406febcd9 | ||
|
|
3491c08ef6 | ||
|
|
912ef337f9 | ||
|
|
b4e2c03a92 | ||
|
|
f31ba09713 | ||
|
|
5da91837d4 | ||
|
|
35e51a50f3 | ||
|
|
f41dd5c08b | ||
|
|
49e7a052e2 | ||
|
|
a2b0cd85e3 | ||
|
|
e1751ff253 | ||
|
|
ee20ef52ec | ||
|
|
08b806fd87 | ||
|
|
db4db928d3 | ||
|
|
ee851ba974 | ||
|
|
ce416a0944 | ||
|
|
daba50c797 | ||
|
|
c3428789fb | ||
|
|
ec9d95413c | ||
|
|
13085ff679 | ||
|
|
80c3ff2aee | ||
|
|
3d89d9faca | ||
|
|
b6cc22ffdd | ||
|
|
f303125cee | ||
|
|
38da6c3f9a | ||
|
|
926541fa4d | ||
|
|
f533157981 | ||
|
|
e028fe2a38 | ||
|
|
469a32a9dd | ||
|
|
84814b2a72 | ||
|
|
908102d776 | ||
|
|
edd0e0698f | ||
|
|
cbd82e75e7 | ||
|
|
6923641626 | ||
|
|
b60d266ad7 | ||
|
|
36676384bd | ||
|
|
0441761872 | ||
|
|
fdb6a3291f |
4
.codecov.yml
Normal file
4
.codecov.yml
Normal file
@ -0,0 +1,4 @@
|
||||
# do not notify until at least 100 builds have been uploaded from the CI pipeline
|
||||
# you can also set after_n_builds on comments independently
|
||||
comment:
|
||||
after_n_builds: 100
|
||||
@ -250,6 +250,7 @@ functions:
|
||||
working_dir: src
|
||||
include_expansions_in_env:
|
||||
- TOOLCHAIN_VERSION
|
||||
- COVERAGE
|
||||
type: test
|
||||
|
||||
# Upload coverage codecov
|
||||
@ -268,6 +269,8 @@ functions:
|
||||
- github_pr_number
|
||||
- github_pr_head_branch
|
||||
- github_author
|
||||
- requester
|
||||
- branch_name
|
||||
type: test
|
||||
|
||||
# Upload coverage
|
||||
|
||||
@ -75,7 +75,7 @@ tasks:
|
||||
SUB_TEST_NAME: session-creds
|
||||
TOOLCHAIN_VERSION: 3.14t
|
||||
tags: [auth-aws, auth-aws-session-creds, free-threaded]
|
||||
- name: test-auth-aws-rapid-web-identity-python3.14
|
||||
- name: test-auth-aws-rapid-web-identity-python3.14-cov
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
@ -87,7 +87,8 @@ tasks:
|
||||
TEST_NAME: auth_aws
|
||||
SUB_TEST_NAME: web-identity
|
||||
TOOLCHAIN_VERSION: "3.14"
|
||||
tags: [auth-aws, auth-aws-web-identity]
|
||||
COVERAGE: "1"
|
||||
tags: [auth-aws, auth-aws-web-identity, pr]
|
||||
- name: test-auth-aws-rapid-web-identity-session-name-python3.14
|
||||
commands:
|
||||
- func: run server
|
||||
@ -904,7 +905,7 @@ tasks:
|
||||
- ocsp-ecdsa
|
||||
- rapid
|
||||
- ocsp-staple
|
||||
- name: test-ocsp-ecdsa-valid-cert-server-staples-latest-python3.14
|
||||
- name: test-ocsp-ecdsa-valid-cert-server-staples-latest-python3.14-cov
|
||||
commands:
|
||||
- func: run tests
|
||||
vars:
|
||||
@ -913,11 +914,13 @@ tasks:
|
||||
TEST_NAME: ocsp
|
||||
TOOLCHAIN_VERSION: "3.14"
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
tags:
|
||||
- ocsp
|
||||
- ocsp-ecdsa
|
||||
- latest
|
||||
- ocsp-staple
|
||||
- pr
|
||||
- name: test-ocsp-ecdsa-invalid-cert-server-staples-v4.4-python3.10-min-deps
|
||||
commands:
|
||||
- func: run tests
|
||||
@ -1928,7 +1931,7 @@ tasks:
|
||||
- ocsp-rsa
|
||||
- rapid
|
||||
- ocsp-staple
|
||||
- name: test-ocsp-rsa-valid-cert-server-staples-latest-python3.14
|
||||
- name: test-ocsp-rsa-valid-cert-server-staples-latest-python3.14-cov
|
||||
commands:
|
||||
- func: run tests
|
||||
vars:
|
||||
@ -1937,11 +1940,13 @@ tasks:
|
||||
TEST_NAME: ocsp
|
||||
TOOLCHAIN_VERSION: "3.14"
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
tags:
|
||||
- ocsp
|
||||
- ocsp-rsa
|
||||
- latest
|
||||
- ocsp-staple
|
||||
- pr
|
||||
- name: test-ocsp-rsa-invalid-cert-server-staples-v4.4-python3.10-min-deps
|
||||
commands:
|
||||
- func: run tests
|
||||
@ -2615,20 +2620,18 @@ tasks:
|
||||
- replica_set-auth-nossl
|
||||
- async
|
||||
- free-threaded
|
||||
- name: test-server-version-python3.13-sync-auth-nossl-replica-set-cov
|
||||
- name: test-server-version-python3.13-sync-auth-nossl-replica-set
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: nossl
|
||||
TOPOLOGY: replica_set
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: nossl
|
||||
TOPOLOGY: replica_set
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.13"
|
||||
TEST_NAME: default_sync
|
||||
tags:
|
||||
@ -2636,20 +2639,18 @@ tasks:
|
||||
- python-3.13
|
||||
- replica_set-auth-nossl
|
||||
- sync
|
||||
- name: test-server-version-python3.12-async-auth-ssl-replica-set-cov
|
||||
- name: test-server-version-python3.12-async-auth-ssl-replica-set
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: replica_set
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: replica_set
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.12"
|
||||
TEST_NAME: default_async
|
||||
tags:
|
||||
@ -2657,20 +2658,18 @@ tasks:
|
||||
- python-3.12
|
||||
- replica_set-auth-ssl
|
||||
- async
|
||||
- name: test-server-version-python3.11-sync-auth-ssl-replica-set-cov
|
||||
- name: test-server-version-python3.11-sync-auth-ssl-replica-set
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: replica_set
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: replica_set
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.11"
|
||||
TEST_NAME: default_sync
|
||||
tags:
|
||||
@ -2743,20 +2742,18 @@ tasks:
|
||||
- python-pypy3.11
|
||||
- replica_set-noauth-ssl
|
||||
- async
|
||||
- name: test-server-version-python3.14-sync-noauth-ssl-replica-set-cov
|
||||
- name: test-server-version-python3.14-sync-noauth-ssl-replica-set
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: replica_set
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: replica_set
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.14"
|
||||
TEST_NAME: default_sync
|
||||
tags:
|
||||
@ -2764,20 +2761,18 @@ tasks:
|
||||
- python-3.14
|
||||
- replica_set-noauth-ssl
|
||||
- sync
|
||||
- name: test-server-version-python3.14-async-auth-nossl-sharded-cluster-cov
|
||||
- name: test-server-version-python3.14-async-auth-nossl-sharded-cluster
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: nossl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: nossl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.14"
|
||||
TEST_NAME: default_async
|
||||
tags:
|
||||
@ -2829,20 +2824,18 @@ tasks:
|
||||
- sharded_cluster-auth-ssl
|
||||
- async
|
||||
- pr
|
||||
- name: test-server-version-python3.11-async-auth-ssl-sharded-cluster-cov
|
||||
- name: test-server-version-python3.11-async-auth-ssl-sharded-cluster
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.11"
|
||||
TEST_NAME: default_async
|
||||
tags:
|
||||
@ -2850,20 +2843,18 @@ tasks:
|
||||
- python-3.11
|
||||
- sharded_cluster-auth-ssl
|
||||
- async
|
||||
- name: test-server-version-python3.12-async-auth-ssl-sharded-cluster-cov
|
||||
- name: test-server-version-python3.12-async-auth-ssl-sharded-cluster
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.12"
|
||||
TEST_NAME: default_async
|
||||
tags:
|
||||
@ -2871,20 +2862,18 @@ tasks:
|
||||
- python-3.12
|
||||
- sharded_cluster-auth-ssl
|
||||
- async
|
||||
- name: test-server-version-python3.13-async-auth-ssl-sharded-cluster-cov
|
||||
- name: test-server-version-python3.13-async-auth-ssl-sharded-cluster
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.13"
|
||||
TEST_NAME: default_async
|
||||
tags:
|
||||
@ -2892,20 +2881,18 @@ tasks:
|
||||
- python-3.13
|
||||
- sharded_cluster-auth-ssl
|
||||
- async
|
||||
- name: test-server-version-python3.14-async-auth-ssl-sharded-cluster-cov
|
||||
- name: test-server-version-python3.14-async-auth-ssl-sharded-cluster
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.14"
|
||||
TEST_NAME: default_async
|
||||
tags:
|
||||
@ -2976,20 +2963,18 @@ tasks:
|
||||
- sharded_cluster-auth-ssl
|
||||
- sync
|
||||
- pr
|
||||
- name: test-server-version-python3.11-sync-auth-ssl-sharded-cluster-cov
|
||||
- name: test-server-version-python3.11-sync-auth-ssl-sharded-cluster
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.11"
|
||||
TEST_NAME: default_sync
|
||||
tags:
|
||||
@ -2997,20 +2982,18 @@ tasks:
|
||||
- python-3.11
|
||||
- sharded_cluster-auth-ssl
|
||||
- sync
|
||||
- name: test-server-version-python3.12-sync-auth-ssl-sharded-cluster-cov
|
||||
- name: test-server-version-python3.12-sync-auth-ssl-sharded-cluster
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.12"
|
||||
TEST_NAME: default_sync
|
||||
tags:
|
||||
@ -3018,20 +3001,18 @@ tasks:
|
||||
- python-3.12
|
||||
- sharded_cluster-auth-ssl
|
||||
- sync
|
||||
- name: test-server-version-python3.13-sync-auth-ssl-sharded-cluster-cov
|
||||
- name: test-server-version-python3.13-sync-auth-ssl-sharded-cluster
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.13"
|
||||
TEST_NAME: default_sync
|
||||
tags:
|
||||
@ -3039,20 +3020,18 @@ tasks:
|
||||
- python-3.13
|
||||
- sharded_cluster-auth-ssl
|
||||
- sync
|
||||
- name: test-server-version-python3.14-sync-auth-ssl-sharded-cluster-cov
|
||||
- name: test-server-version-python3.14-sync-auth-ssl-sharded-cluster
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.14"
|
||||
TEST_NAME: default_sync
|
||||
tags:
|
||||
@ -3099,20 +3078,18 @@ tasks:
|
||||
- python-pypy3.11
|
||||
- sharded_cluster-auth-ssl
|
||||
- sync
|
||||
- name: test-server-version-python3.12-async-noauth-nossl-sharded-cluster-cov
|
||||
- name: test-server-version-python3.12-async-noauth-nossl-sharded-cluster
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: noauth
|
||||
SSL: nossl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: noauth
|
||||
SSL: nossl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.12"
|
||||
TEST_NAME: default_async
|
||||
tags:
|
||||
@ -3120,20 +3097,18 @@ tasks:
|
||||
- python-3.12
|
||||
- sharded_cluster-noauth-nossl
|
||||
- async
|
||||
- name: test-server-version-python3.11-sync-noauth-nossl-sharded-cluster-cov
|
||||
- name: test-server-version-python3.11-sync-noauth-nossl-sharded-cluster
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: noauth
|
||||
SSL: nossl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: noauth
|
||||
SSL: nossl
|
||||
TOPOLOGY: sharded_cluster
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.11"
|
||||
TEST_NAME: default_sync
|
||||
tags:
|
||||
@ -3141,7 +3116,7 @@ tasks:
|
||||
- python-3.11
|
||||
- sharded_cluster-noauth-nossl
|
||||
- sync
|
||||
- name: test-server-version-python3.10-async-noauth-ssl-sharded-cluster-min-deps-cov
|
||||
- name: test-server-version-python3.10-async-noauth-ssl-sharded-cluster-min-deps
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
@ -3149,14 +3124,12 @@ tasks:
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
TEST_MIN_DEPS: "1"
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
TEST_MIN_DEPS: "1"
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.10"
|
||||
TEST_NAME: default_async
|
||||
tags:
|
||||
@ -3183,20 +3156,18 @@ tasks:
|
||||
- python-pypy3.11
|
||||
- sharded_cluster-noauth-ssl
|
||||
- sync
|
||||
- name: test-server-version-python3.13-async-auth-nossl-standalone-cov
|
||||
- name: test-server-version-python3.13-async-auth-nossl-standalone
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: nossl
|
||||
TOPOLOGY: standalone
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: nossl
|
||||
TOPOLOGY: standalone
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.13"
|
||||
TEST_NAME: default_async
|
||||
tags:
|
||||
@ -3204,20 +3175,18 @@ tasks:
|
||||
- python-3.13
|
||||
- standalone-auth-nossl
|
||||
- async
|
||||
- name: test-server-version-python3.12-sync-auth-nossl-standalone-cov
|
||||
- name: test-server-version-python3.12-sync-auth-nossl-standalone
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: nossl
|
||||
TOPOLOGY: standalone
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: nossl
|
||||
TOPOLOGY: standalone
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.12"
|
||||
TEST_NAME: default_sync
|
||||
tags:
|
||||
@ -3225,20 +3194,18 @@ tasks:
|
||||
- python-3.12
|
||||
- standalone-auth-nossl
|
||||
- sync
|
||||
- name: test-server-version-python3.11-async-auth-ssl-standalone-cov
|
||||
- name: test-server-version-python3.11-async-auth-ssl-standalone
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: standalone
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: standalone
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.11"
|
||||
TEST_NAME: default_async
|
||||
tags:
|
||||
@ -3246,7 +3213,7 @@ tasks:
|
||||
- python-3.11
|
||||
- standalone-auth-ssl
|
||||
- async
|
||||
- name: test-server-version-python3.10-sync-auth-ssl-standalone-min-deps-cov
|
||||
- name: test-server-version-python3.10-sync-auth-ssl-standalone-min-deps
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
@ -3254,14 +3221,12 @@ tasks:
|
||||
SSL: ssl
|
||||
TOPOLOGY: standalone
|
||||
TEST_MIN_DEPS: "1"
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: standalone
|
||||
TEST_MIN_DEPS: "1"
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.10"
|
||||
TEST_NAME: default_sync
|
||||
tags:
|
||||
@ -3293,18 +3258,20 @@ tasks:
|
||||
- standalone-noauth-nossl
|
||||
- async
|
||||
- pr
|
||||
- name: test-server-version-pypy3.11-sync-noauth-nossl-standalone
|
||||
- name: test-server-version-pypy3.11-sync-noauth-nossl-standalone-cov
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: noauth
|
||||
SSL: nossl
|
||||
TOPOLOGY: standalone
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: noauth
|
||||
SSL: nossl
|
||||
TOPOLOGY: standalone
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: pypy3.11
|
||||
TEST_NAME: default_sync
|
||||
tags:
|
||||
@ -3313,20 +3280,18 @@ tasks:
|
||||
- standalone-noauth-nossl
|
||||
- sync
|
||||
- pr
|
||||
- name: test-server-version-python3.14-async-noauth-ssl-standalone-cov
|
||||
- name: test-server-version-python3.14-async-noauth-ssl-standalone
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: standalone
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: standalone
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.14"
|
||||
TEST_NAME: default_async
|
||||
tags:
|
||||
@ -4082,7 +4047,7 @@ tasks:
|
||||
- standalone-noauth-nossl
|
||||
- async
|
||||
- pypy
|
||||
- name: test-standard-latest-python3.12-async-noauth-ssl-replica-set
|
||||
- name: test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
@ -4090,12 +4055,14 @@ tasks:
|
||||
SSL: ssl
|
||||
TOPOLOGY: replica_set
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: replica_set
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.12"
|
||||
TEST_NAME: default_async
|
||||
tags:
|
||||
@ -4128,7 +4095,7 @@ tasks:
|
||||
- replica_set-noauth-ssl
|
||||
- async
|
||||
- pypy
|
||||
- name: test-standard-latest-python3.13-async-auth-ssl-sharded-cluster
|
||||
- name: test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
@ -4136,12 +4103,14 @@ tasks:
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.13"
|
||||
TEST_NAME: default_async
|
||||
tags:
|
||||
@ -4151,7 +4120,7 @@ tasks:
|
||||
- sharded_cluster-auth-ssl
|
||||
- async
|
||||
- pr
|
||||
- name: test-standard-latest-python3.11-async-noauth-nossl-standalone
|
||||
- name: test-standard-latest-python3.11-async-noauth-nossl-standalone-cov
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
@ -4159,12 +4128,14 @@ tasks:
|
||||
SSL: nossl
|
||||
TOPOLOGY: standalone
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: noauth
|
||||
SSL: nossl
|
||||
TOPOLOGY: standalone
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.11"
|
||||
TEST_NAME: default_async
|
||||
tags:
|
||||
@ -4174,7 +4145,7 @@ tasks:
|
||||
- standalone-noauth-nossl
|
||||
- async
|
||||
- pr
|
||||
- name: test-standard-latest-python3.14-async-noauth-nossl-standalone
|
||||
- name: test-standard-latest-python3.14-async-noauth-nossl-standalone-cov
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
@ -4182,12 +4153,14 @@ tasks:
|
||||
SSL: nossl
|
||||
TOPOLOGY: standalone
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: noauth
|
||||
SSL: nossl
|
||||
TOPOLOGY: standalone
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.14"
|
||||
TEST_NAME: default_async
|
||||
tags:
|
||||
@ -4829,7 +4802,7 @@ tasks:
|
||||
- python-3.13
|
||||
- standalone-noauth-nossl
|
||||
- noauth
|
||||
- name: test-non-standard-latest-python3.14t-noauth-ssl-replica-set
|
||||
- name: test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
@ -4837,12 +4810,14 @@ tasks:
|
||||
SSL: ssl
|
||||
TOPOLOGY: replica_set
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: replica_set
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: 3.14t
|
||||
tags:
|
||||
- test-non-standard
|
||||
@ -4874,7 +4849,7 @@ tasks:
|
||||
- replica_set-noauth-ssl
|
||||
- noauth
|
||||
- pypy
|
||||
- name: test-non-standard-latest-python3.14-auth-ssl-sharded-cluster
|
||||
- name: test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
@ -4882,12 +4857,14 @@ tasks:
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.14"
|
||||
tags:
|
||||
- test-non-standard
|
||||
@ -4896,7 +4873,7 @@ tasks:
|
||||
- sharded_cluster-auth-ssl
|
||||
- auth
|
||||
- pr
|
||||
- name: test-non-standard-latest-python3.13-noauth-nossl-standalone
|
||||
- name: test-non-standard-latest-python3.13-noauth-nossl-standalone-cov
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
@ -4904,12 +4881,14 @@ tasks:
|
||||
SSL: nossl
|
||||
TOPOLOGY: standalone
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: noauth
|
||||
SSL: nossl
|
||||
TOPOLOGY: standalone
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.13"
|
||||
tags:
|
||||
- test-non-standard
|
||||
@ -5007,7 +4986,7 @@ tasks:
|
||||
- pypy
|
||||
|
||||
# Test numpy tests
|
||||
- name: test-numpy-python3.10
|
||||
- name: test-numpy-python3.10-python3.10
|
||||
commands:
|
||||
- func: test numpy
|
||||
vars:
|
||||
@ -5017,16 +4996,18 @@ tasks:
|
||||
- vector
|
||||
- python-3.10
|
||||
- test-numpy
|
||||
- name: test-numpy-python3.14
|
||||
- name: test-numpy-python3.14-python3.14-cov
|
||||
commands:
|
||||
- func: test numpy
|
||||
vars:
|
||||
TOOLCHAIN_VERSION: "3.14"
|
||||
COVERAGE: "1"
|
||||
tags:
|
||||
- binary
|
||||
- vector
|
||||
- python-3.14
|
||||
- test-numpy
|
||||
- pr
|
||||
|
||||
# Test standard auth tests
|
||||
- name: test-standard-auth-v4.2-python3.10-auth-ssl-sharded-cluster-min-deps
|
||||
@ -5290,7 +5271,7 @@ tasks:
|
||||
- sharded_cluster-auth-ssl
|
||||
- auth
|
||||
- pypy
|
||||
- name: test-standard-auth-latest-python3.11-auth-ssl-sharded-cluster
|
||||
- name: test-standard-auth-latest-python3.11-auth-ssl-sharded-cluster-cov
|
||||
commands:
|
||||
- func: run server
|
||||
vars:
|
||||
@ -5298,12 +5279,14 @@ tasks:
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
- func: run tests
|
||||
vars:
|
||||
AUTH: auth
|
||||
SSL: ssl
|
||||
TOPOLOGY: sharded_cluster
|
||||
VERSION: latest
|
||||
COVERAGE: "1"
|
||||
TOOLCHAIN_VERSION: "3.11"
|
||||
tags:
|
||||
- test-standard-auth
|
||||
|
||||
@ -368,7 +368,6 @@ buildvariants:
|
||||
run_on:
|
||||
- rhel87-small
|
||||
expansions:
|
||||
COVERAGE: "1"
|
||||
NO_EXT: "1"
|
||||
|
||||
# No server tests
|
||||
@ -420,6 +419,8 @@ buildvariants:
|
||||
run_on:
|
||||
- ubuntu2204-small
|
||||
batchtime: 1440
|
||||
expansions:
|
||||
COVERAGE: "1"
|
||||
tags: [pr]
|
||||
- name: auth-oidc-macos
|
||||
tasks:
|
||||
@ -614,6 +615,7 @@ buildvariants:
|
||||
- name: test-win64
|
||||
tasks:
|
||||
- name: .test-standard !.pypy
|
||||
- name: .test-no-orchestration !.pypy
|
||||
display_name: "* Test Win64"
|
||||
run_on:
|
||||
- windows-2022-latest-small
|
||||
|
||||
@ -94,6 +94,9 @@ do
|
||||
change-streams|change_streams)
|
||||
cpjson change-streams/tests/ change_streams/
|
||||
;;
|
||||
client-backpressure|client_backpressure)
|
||||
cpjson client-backpressure/tests client-backpressure
|
||||
;;
|
||||
client-side-encryption|csfle|fle)
|
||||
cpjson client-side-encryption/tests/ client-side-encryption/spec
|
||||
cpjson client-side-encryption/corpus/ client-side-encryption/corpus
|
||||
|
||||
@ -38,6 +38,7 @@ trap "cleanup_tests" SIGINT ERR
|
||||
|
||||
# Start the test runner.
|
||||
echo "Running tests with UV_PYTHON=${UV_PYTHON:-}..."
|
||||
echo "UV_ARGS=${UV_ARGS}"
|
||||
uv run ${UV_ARGS} --reinstall-package pymongo .evergreen/scripts/run_tests.py "$@"
|
||||
echo "Running tests with UV_PYTHON=${UV_PYTHON:-}... done."
|
||||
|
||||
|
||||
@ -97,6 +97,8 @@ def create_standard_nonlinux_variants() -> list[BuildVariant]:
|
||||
tasks = [
|
||||
f".test-standard !.pypy .server-{version}" for version in get_versions_from("6.0")
|
||||
]
|
||||
if host_name == "win64":
|
||||
tasks.append(".test-no-orchestration !.pypy")
|
||||
host = HOSTS[host_name]
|
||||
tags = ["standard-non-linux"]
|
||||
expansions = dict()
|
||||
@ -318,7 +320,7 @@ def create_green_framework_variants():
|
||||
def create_no_c_ext_variants():
|
||||
host = DEFAULT_HOST
|
||||
tasks = [".test-standard"]
|
||||
expansions = dict(COVERAGE="1")
|
||||
expansions = dict()
|
||||
handle_c_ext(C_EXTS[0], expansions)
|
||||
display_name = get_variant_name("No C Ext", host)
|
||||
return [create_variant(tasks, display_name, host=host, expansions=expansions)]
|
||||
@ -344,8 +346,12 @@ def create_test_numpy_tasks():
|
||||
tasks = []
|
||||
for python in MIN_MAX_PYTHON:
|
||||
tags = ["binary", "vector", f"python-{python}", "test-numpy"]
|
||||
task_name = get_task_name("test-numpy", python=python)
|
||||
test_func = FunctionCall(func="test numpy", vars=dict(TOOLCHAIN_VERSION=python))
|
||||
vars = dict(TOOLCHAIN_VERSION=python)
|
||||
if python == MIN_MAX_PYTHON[-1]:
|
||||
tags.append("pr")
|
||||
vars["COVERAGE"] = "1"
|
||||
task_name = get_task_name("test-numpy", python=python, **vars)
|
||||
test_func = FunctionCall(func="test numpy", vars=vars)
|
||||
tasks.append(EvgTask(name=task_name, tags=tags, commands=[test_func]))
|
||||
return tasks
|
||||
|
||||
@ -397,6 +403,7 @@ def create_oidc_auth_variants():
|
||||
tags=["pr"],
|
||||
host=host,
|
||||
batchtime=BATCHTIME_DAY,
|
||||
expansions=dict(COVERAGE="1"),
|
||||
)
|
||||
)
|
||||
return variants
|
||||
@ -596,7 +603,7 @@ def create_server_version_tasks():
|
||||
expansions["TEST_MIN_DEPS"] = "1"
|
||||
if "t" in python:
|
||||
tags.append("free-threaded")
|
||||
if python not in PYPYS and "t" not in python:
|
||||
if "pr" in tags:
|
||||
expansions["COVERAGE"] = "1"
|
||||
name = get_task_name(
|
||||
"test-server-version",
|
||||
@ -661,6 +668,8 @@ def create_test_non_standard_tasks():
|
||||
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology, VERSION=version)
|
||||
if python == ALL_PYTHONS[0]:
|
||||
expansions["TEST_MIN_DEPS"] = "1"
|
||||
elif pr:
|
||||
expansions["COVERAGE"] = "1"
|
||||
name = get_task_name("test-non-standard", python=python, **expansions)
|
||||
server_func = FunctionCall(func="run server", vars=expansions)
|
||||
test_vars = expansions.copy()
|
||||
@ -703,6 +712,8 @@ def create_test_standard_auth_tasks():
|
||||
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology, VERSION=version)
|
||||
if python == ALL_PYTHONS[0]:
|
||||
expansions["TEST_MIN_DEPS"] = "1"
|
||||
elif pr:
|
||||
expansions["COVERAGE"] = "1"
|
||||
name = get_task_name("test-standard-auth", python=python, **expansions)
|
||||
server_func = FunctionCall(func="run server", vars=expansions)
|
||||
test_vars = expansions.copy()
|
||||
@ -741,6 +752,8 @@ def create_standard_tasks():
|
||||
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology, VERSION=version)
|
||||
if python == ALL_PYTHONS[0]:
|
||||
expansions["TEST_MIN_DEPS"] = "1"
|
||||
elif pr:
|
||||
expansions["COVERAGE"] = "1"
|
||||
name = get_task_name("test-standard", python=python, sync=sync, **expansions)
|
||||
server_func = FunctionCall(func="run server", vars=expansions)
|
||||
test_vars = expansions.copy()
|
||||
@ -810,8 +823,11 @@ def create_aws_tasks():
|
||||
if "t" in python:
|
||||
tags.append("free-threaded")
|
||||
test_vars = dict(TEST_NAME="auth_aws", SUB_TEST_NAME=test_type, TOOLCHAIN_VERSION=python)
|
||||
if python == ALL_PYTHONS[0]:
|
||||
if python == MIN_MAX_PYTHON[0]:
|
||||
test_vars["TEST_MIN_DEPS"] = "1"
|
||||
elif python == MIN_MAX_PYTHON[-1]:
|
||||
tags.append("pr")
|
||||
test_vars["COVERAGE"] = "1"
|
||||
name = get_task_name(f"{base_name}-{test_type}", **test_vars)
|
||||
test_func = FunctionCall(func="run tests", vars=test_vars)
|
||||
funcs = [server_func, assume_func, test_func]
|
||||
@ -849,11 +865,11 @@ def create_oidc_tasks():
|
||||
tasks = []
|
||||
for sub_test in ["default", "azure", "gcp", "eks", "aks", "gke"]:
|
||||
vars = dict(TEST_NAME="auth_oidc", SUB_TEST_NAME=sub_test)
|
||||
test_func = FunctionCall(func="run tests", vars=vars)
|
||||
task_name = f"test-auth-oidc-{sub_test}"
|
||||
tags = ["auth_oidc"]
|
||||
if sub_test != "default":
|
||||
tags.append("auth_oidc_remote")
|
||||
test_func = FunctionCall(func="run tests", vars=vars)
|
||||
task_name = get_task_name(f"test-auth-oidc-{sub_test}", **vars)
|
||||
tasks.append(EvgTask(name=task_name, tags=tags, commands=[test_func]))
|
||||
|
||||
return tasks
|
||||
@ -903,14 +919,14 @@ def _create_ocsp_tasks(algo, variant, server_type, base_task_name):
|
||||
)
|
||||
if python == ALL_PYTHONS[0]:
|
||||
vars["TEST_MIN_DEPS"] = "1"
|
||||
test_func = FunctionCall(func="run tests", vars=vars)
|
||||
|
||||
tags = ["ocsp", f"ocsp-{algo}", version]
|
||||
if "disableStapling" not in variant:
|
||||
tags.append("ocsp-staple")
|
||||
if algo == "valid-cert-server-staples" and version == "latest":
|
||||
if base_task_name == "valid-cert-server-staples" and version == "latest":
|
||||
tags.append("pr")
|
||||
|
||||
if "TEST_MIN_DEPS" not in vars:
|
||||
vars["COVERAGE"] = "1"
|
||||
test_func = FunctionCall(func="run tests", vars=vars)
|
||||
task_name = get_task_name(f"test-ocsp-{algo}-{base_task_name}", **vars)
|
||||
tasks.append(EvgTask(name=task_name, tags=tags, commands=[test_func]))
|
||||
|
||||
@ -1087,6 +1103,8 @@ def create_upload_coverage_codecov_func():
|
||||
"github_pr_number",
|
||||
"github_pr_head_branch",
|
||||
"github_author",
|
||||
"requester",
|
||||
"branch_name",
|
||||
]
|
||||
args = [
|
||||
".evergreen/scripts/upload-codecov.sh",
|
||||
@ -1228,7 +1246,7 @@ def create_run_tests_func():
|
||||
|
||||
|
||||
def create_test_numpy_func():
|
||||
includes = ["TOOLCHAIN_VERSION"]
|
||||
includes = ["TOOLCHAIN_VERSION", "COVERAGE"]
|
||||
test_cmd = get_subprocess_exec(
|
||||
include_expansions_in_env=includes, args=[".evergreen/just.sh", "test-numpy"]
|
||||
)
|
||||
|
||||
@ -7,6 +7,8 @@ import subprocess
|
||||
from argparse import Namespace
|
||||
from subprocess import CalledProcessError
|
||||
|
||||
JIRA_FILTER = "https://jira.mongodb.org/issues/?jql=labels%20%3D%20automated-sync%20AND%20status%20!%3D%20Closed"
|
||||
|
||||
|
||||
def resync_specs(directory: pathlib.Path, errored: dict[str, str]) -> None:
|
||||
"""Actually sync the specs"""
|
||||
@ -117,6 +119,7 @@ def write_summary(errored: dict[str, str], new: list[str], filename: str | None)
|
||||
pr_body += "\n -".join(new)
|
||||
pr_body += "\n"
|
||||
if pr_body != "":
|
||||
pr_body = f"Jira tickets: {JIRA_FILTER}\n\n" + pr_body
|
||||
if filename is None:
|
||||
print(f"\n{pr_body}")
|
||||
else:
|
||||
|
||||
@ -12,7 +12,7 @@ def set_env(name: str, value: Any = "1") -> None:
|
||||
|
||||
def start_server():
|
||||
opts, extra_opts = get_test_options(
|
||||
"Run a MongoDB server. All given flags will be passed to run-orchestration.sh in DRIVERS_TOOLS.",
|
||||
"Run a MongoDB server. All given flags will be passed to run-mongodb.sh in DRIVERS_TOOLS.",
|
||||
require_sub_test_name=False,
|
||||
allow_extra_opts=True,
|
||||
)
|
||||
@ -51,7 +51,7 @@ def start_server():
|
||||
elif opts.quiet:
|
||||
extra_opts.append("-q")
|
||||
|
||||
cmd = ["bash", f"{DRIVERS_TOOLS}/.evergreen/run-orchestration.sh", *extra_opts]
|
||||
cmd = ["bash", f"{DRIVERS_TOOLS}/.evergreen/run-mongodb.sh", "start", *extra_opts]
|
||||
run_command(cmd, cwd=DRIVERS_TOOLS)
|
||||
|
||||
|
||||
|
||||
@ -4,7 +4,9 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@ -202,6 +204,16 @@ def run() -> None:
|
||||
if os.environ.get("DEBUG_LOG"):
|
||||
TEST_ARGS.extend(f"-o log_cli_level={logging.DEBUG}".split())
|
||||
|
||||
if os.environ.get("COVERAGE"):
|
||||
binary = sys.executable.replace(os.sep, "/")
|
||||
cmd = f"{binary} -m coverage run -m pytest {' '.join(TEST_ARGS)} {' '.join(sys.argv[1:])}"
|
||||
result = subprocess.run(shlex.split(cmd), check=False) # noqa: S603
|
||||
cmd = f"{binary} -m coverage report"
|
||||
subprocess.run(shlex.split(cmd), check=False) # noqa: S603
|
||||
if result.returncode != 0:
|
||||
print(result.stderr)
|
||||
sys.exit(result.returncode)
|
||||
|
||||
# Run local tests.
|
||||
ret = pytest.main(TEST_ARGS + sys.argv[1:])
|
||||
if ret != 0:
|
||||
|
||||
@ -153,6 +153,10 @@ def handle_test_env() -> None:
|
||||
# Start compiling the args we'll pass to uv.
|
||||
UV_ARGS = ["--extra test --no-group dev"]
|
||||
|
||||
# If USE_ACTIVE_VENV is set, add --active to UV_ARGS so run-tests.sh uses the active venv.
|
||||
if is_set("USE_ACTIVE_VENV"):
|
||||
UV_ARGS.append("--active")
|
||||
|
||||
test_title = test_name
|
||||
if sub_test_name:
|
||||
test_title += f" {sub_test_name}"
|
||||
@ -324,7 +328,8 @@ def handle_test_env() -> None:
|
||||
version = os.environ.get("VERSION", "latest")
|
||||
cmd = [
|
||||
"bash",
|
||||
f"{DRIVERS_TOOLS}/.evergreen/run-orchestration.sh",
|
||||
f"{DRIVERS_TOOLS}/.evergreen/run-mongodb.sh",
|
||||
"start",
|
||||
"--ssl",
|
||||
"--version",
|
||||
version,
|
||||
@ -431,6 +436,9 @@ def handle_test_env() -> None:
|
||||
# We do not want the default client_context to be initialized.
|
||||
write_env("DISABLE_CONTEXT")
|
||||
|
||||
if test_name == "numpy":
|
||||
UV_ARGS.append("--with numpy")
|
||||
|
||||
if test_name == "perf":
|
||||
data_dir = ROOT / "specifications/source/benchmarking/data"
|
||||
if not data_dir.exists():
|
||||
@ -458,12 +466,14 @@ def handle_test_env() -> None:
|
||||
# Keep in sync with combine-coverage.sh.
|
||||
# coverage >=5 is needed for relative_files=true.
|
||||
UV_ARGS.append("--group coverage")
|
||||
TEST_ARGS = f"{TEST_ARGS} --cov"
|
||||
write_env("COVERAGE")
|
||||
|
||||
if opts.green_framework:
|
||||
framework = opts.green_framework or os.environ["GREEN_FRAMEWORK"]
|
||||
UV_ARGS.append(f"--group {framework}")
|
||||
if framework == "gevent" and opts.test_min_deps:
|
||||
# PYTHON-5729. This can be removed when the min supported gevent is moved to 25.9.1.
|
||||
UV_ARGS.append('--with "setuptools==81.0"')
|
||||
|
||||
else:
|
||||
TEST_ARGS = f"-v --durations=5 {TEST_ARGS}"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# Stop a server that was started using run-orchestration.sh in DRIVERS_TOOLS.
|
||||
# Stop a server that was started using run-mongodb.sh in DRIVERS_TOOLS.
|
||||
set -eu
|
||||
|
||||
HERE=$(dirname ${BASH_SOURCE:-$0})
|
||||
@ -11,4 +11,4 @@ if [ -f $HERE/env.sh ]; then
|
||||
source $HERE/env.sh
|
||||
fi
|
||||
|
||||
bash ${DRIVERS_TOOLS}/.evergreen/stop-orchestration.sh
|
||||
bash ${DRIVERS_TOOLS}/.evergreen/run-mongodb.sh stop
|
||||
|
||||
@ -8,35 +8,50 @@ ROOT=$(dirname "$(dirname $HERE)")
|
||||
|
||||
pushd $ROOT > /dev/null
|
||||
export FNAME=coverage.xml
|
||||
|
||||
if [ -z "${github_pr_number:-}" ]; then
|
||||
echo "This is not a PR, not running codecov"
|
||||
exit 0
|
||||
fi
|
||||
REQUESTER=${requester:-}
|
||||
|
||||
if [ ! -f ".coverage" ]; then
|
||||
echo "There are no XML test results, not running codecov"
|
||||
echo "There are no coverage results, not running codecov"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Uploading..."
|
||||
printf 'pr: %s\n' "$github_pr_number"
|
||||
if [[ "${REQUESTER}" == "github_pr" || "${REQUESTER}" == "commit" ]]; then
|
||||
echo "Uploading codecov for $REQUESTER..."
|
||||
else
|
||||
echo "Error: requester must be 'github_pr' or 'commit', got '${REQUESTER}'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'sha: %s\n' "$github_commit"
|
||||
printf 'branch: %s:%s\n' "$github_author" "$github_pr_head_branch"
|
||||
printf 'flag: %s-%s\n' "$build_variant" "$task_name"
|
||||
printf 'file: %s\n' "$FNAME"
|
||||
uv tool run --with "coverage[toml]" coverage xml
|
||||
uv tool run --from codecov-cli codecovcli upload-process \
|
||||
--report-type coverage \
|
||||
--disable-search \
|
||||
--fail-on-error \
|
||||
--git-service github \
|
||||
--token ${CODECOV_TOKEN} \
|
||||
--pr ${github_pr_number} \
|
||||
--sha ${github_commit} \
|
||||
--branch "${github_author}:${github_pr_head_branch}" \
|
||||
--flag "${build_variant}-${task_name}" \
|
||||
--file $FNAME
|
||||
echo "Uploading...done."
|
||||
|
||||
codecov_args=(
|
||||
upload-process
|
||||
--report-type coverage
|
||||
--disable-search
|
||||
--fail-on-error
|
||||
--git-service github
|
||||
--token "${CODECOV_TOKEN}"
|
||||
--sha "${github_commit}"
|
||||
--flag "${build_variant}-${task_name}"
|
||||
--file "${FNAME}"
|
||||
)
|
||||
|
||||
if [ -n "${github_pr_number:-}" ]; then
|
||||
printf 'branch: %s:%s\n' "$github_author" "$github_pr_head_branch"
|
||||
printf 'pr: %s\n' "$github_pr_number"
|
||||
uv tool run --from codecov-cli codecovcli \
|
||||
"${codecov_args[@]}" \
|
||||
--pr "${github_pr_number}" \
|
||||
--branch "${github_author}:${github_pr_head_branch}"
|
||||
else
|
||||
printf 'branch: %s\n' "$branch_name"
|
||||
uv tool run --from codecov-cli codecovcli \
|
||||
"${codecov_args[@]}" \
|
||||
--branch "${branch_name}"
|
||||
fi
|
||||
echo "Uploading codecov for $REQUESTER... done."
|
||||
|
||||
popd > /dev/null
|
||||
|
||||
@ -44,6 +44,7 @@ TEST_SUITE_MAP = {
|
||||
"mockupdb": "mockupdb",
|
||||
"ocsp": "ocsp",
|
||||
"perf": "perf",
|
||||
"numpy": "",
|
||||
}
|
||||
|
||||
# Tests that require a sub test suite.
|
||||
@ -51,7 +52,7 @@ SUB_TEST_REQUIRED = ["auth_aws", "auth_oidc", "kms", "mod_wsgi", "perf"]
|
||||
|
||||
EXTRA_TESTS = ["mod_wsgi", "aws_lambda", "doctest"]
|
||||
|
||||
# Tests that do not use run-orchestration directly.
|
||||
# Tests that do not use run-mongodb directly.
|
||||
NO_RUN_ORCHESTRATION = [
|
||||
"auth_oidc",
|
||||
"atlas_connect",
|
||||
|
||||
@ -1,64 +0,0 @@
|
||||
diff --git a/test/load_balancer/cursors.json b/test/load_balancer/cursors.json
|
||||
index 43e4fbb4f..4e2a55fd4 100644
|
||||
--- a/test/load_balancer/cursors.json
|
||||
+++ b/test/load_balancer/cursors.json
|
||||
@@ -376,7 +376,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
+ "description": "pinned connections are not returned after an network error during getMore",
|
||||
- "description": "pinned connections are returned after an network error during getMore",
|
||||
"operations": [
|
||||
{
|
||||
"name": "failPoint",
|
||||
@@ -440,7 +440,7 @@
|
||||
"object": "testRunner",
|
||||
"arguments": {
|
||||
"client": "client0",
|
||||
+ "connections": 1
|
||||
- "connections": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -659,7 +659,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
+ "description": "pinned connections are not returned to the pool after a non-network error on getMore",
|
||||
- "description": "pinned connections are returned to the pool after a non-network error on getMore",
|
||||
"operations": [
|
||||
{
|
||||
"name": "failPoint",
|
||||
@@ -715,7 +715,7 @@
|
||||
"object": "testRunner",
|
||||
"arguments": {
|
||||
"client": "client0",
|
||||
+ "connections": 1
|
||||
- "connections": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
diff --git a/test/load_balancer/sdam-error-handling.json b/test/load_balancer/sdam-error-handling.json
|
||||
index 63aabc04d..462fa0aac 100644
|
||||
--- a/test/load_balancer/sdam-error-handling.json
|
||||
+++ b/test/load_balancer/sdam-error-handling.json
|
||||
@@ -366,6 +366,9 @@
|
||||
{
|
||||
"connectionCreatedEvent": {}
|
||||
},
|
||||
+ {
|
||||
+ "poolClearedEvent": {}
|
||||
+ },
|
||||
{
|
||||
"connectionClosedEvent": {
|
||||
"reason": "error"
|
||||
@@ -378,9 +375,6 @@
|
||||
"connectionCheckOutFailedEvent": {
|
||||
"reason": "connectionError"
|
||||
}
|
||||
- },
|
||||
- {
|
||||
- "poolClearedEvent": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
diff --git a/test/discovery_and_monitoring/unified/serverMonitoringMode.json b/test/discovery_and_monitoring/unified/serverMonitoringMode.json
|
||||
index e44fad1b..4b492f7d 100644
|
||||
--- a/test/discovery_and_monitoring/unified/serverMonitoringMode.json
|
||||
+++ b/test/discovery_and_monitoring/unified/serverMonitoringMode.json
|
||||
@@ -5,7 +5,8 @@
|
||||
{
|
||||
"topologies": [
|
||||
"single",
|
||||
- "sharded"
|
||||
+ "sharded",
|
||||
+ "sharded-replicaset"
|
||||
],
|
||||
"serverless": "forbid"
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
diff --git a/test/server_selection_logging/replica-set.json b/test/server_selection_logging/replica-set.json
|
||||
index 830b1ea51..5eba784bf 100644
|
||||
--- a/test/server_selection_logging/replica-set.json
|
||||
+++ b/test/server_selection_logging/replica-set.json
|
||||
@@ -184,7 +184,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
- "level": "debug",
|
||||
+ "level": "info",
|
||||
"component": "serverSelection",
|
||||
"data": {
|
||||
"message": "Waiting for suitable server to become available",
|
||||
diff --git a/test/server_selection_logging/standalone.json b/test/server_selection_logging/standalone.json
|
||||
index 830b1ea51..5eba784bf 100644
|
||||
--- a/test/server_selection_logging/standalone.json
|
||||
+++ b/test/server_selection_logging/standalone.json
|
||||
@@ -191,7 +191,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
- "level": "debug",
|
||||
+ "level": "info",
|
||||
"component": "serverSelection",
|
||||
"data": {
|
||||
"message": "Waiting for suitable server to become available",
|
||||
diff --git a/test/server_selection_logging/sharded.json b/test/server_selection_logging/sharded.json
|
||||
index 830b1ea51..5eba784bf 100644
|
||||
--- a/test/server_selection_logging/sharded.json
|
||||
+++ b/test/server_selection_logging/sharded.json
|
||||
@@ -193,7 +193,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
- "level": "debug",
|
||||
+ "level": "info",
|
||||
"component": "serverSelection",
|
||||
"data": {
|
||||
"message": "Waiting for suitable server to become available",
|
||||
diff --git a/test/server_selection_logging/sharded.json b/test/server_selection_logging/operation-id.json
|
||||
index 830b1ea51..5eba784bf 100644
|
||||
--- a/test/server_selection_logging/operation-id.json
|
||||
+++ b/test/server_selection_logging/operation-id.json
|
||||
@@ -197,7 +197,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
- "level": "debug",
|
||||
+ "level": "info",
|
||||
"component": "serverSelection",
|
||||
"data": {
|
||||
"message": "Waiting for suitable server to become available",
|
||||
@@ -383,7 +383,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
- "level": "debug",
|
||||
+ "level": "info",
|
||||
"component": "serverSelection",
|
||||
"data": {
|
||||
"message": "Waiting for suitable server to become available",
|
||||
@ -1,31 +0,0 @@
|
||||
diff --git a/test/discovery_and_monitoring/errors/error_handling_handshake.json b/test/discovery_and_monitoring/errors/error_handling_handshake.json
|
||||
index 56ca7d113..bf83f46f6 100644
|
||||
--- a/test/discovery_and_monitoring/errors/error_handling_handshake.json
|
||||
+++ b/test/discovery_and_monitoring/errors/error_handling_handshake.json
|
||||
@@ -97,14 +97,22 @@
|
||||
"outcome": {
|
||||
"servers": {
|
||||
"a:27017": {
|
||||
- "type": "Unknown",
|
||||
- "topologyVersion": null,
|
||||
+ "type": "RSPrimary",
|
||||
+ "setName": "rs",
|
||||
+ "topologyVersion": {
|
||||
+ "processId": {
|
||||
+ "$oid": "000000000000000000000001"
|
||||
+ },
|
||||
+ "counter": {
|
||||
+ "$numberLong": "1"
|
||||
+ }
|
||||
+ },
|
||||
"pool": {
|
||||
- "generation": 1
|
||||
+ "generation": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
- "topologyType": "ReplicaSetNoPrimary",
|
||||
+ "topologyType": "ReplicaSetWithPrimary",
|
||||
"logicalSessionTimeoutMinutes": null,
|
||||
"setName": "rs"
|
||||
}
|
||||
460
.evergreen/spec-patch/PYTHON-5759.patch
Normal file
460
.evergreen/spec-patch/PYTHON-5759.patch
Normal file
@ -0,0 +1,460 @@
|
||||
diff --git a/test/client-side-encryption/spec/unified/accessToken-azure.json b/test/client-side-encryption/spec/unified/accessToken-azure.json
|
||||
new file mode 100644
|
||||
index 00000000..510d8795
|
||||
--- /dev/null
|
||||
+++ b/test/client-side-encryption/spec/unified/accessToken-azure.json
|
||||
@@ -0,0 +1,186 @@
|
||||
+{
|
||||
+ "description": "accessToken-azure",
|
||||
+ "schemaVersion": "1.28",
|
||||
+ "runOnRequirements": [
|
||||
+ {
|
||||
+ "minServerVersion": "4.1.10",
|
||||
+ "csfle": {
|
||||
+ "minLibmongocryptVersion": "1.6.0"
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "createEntities": [
|
||||
+ {
|
||||
+ "client": {
|
||||
+ "id": "client",
|
||||
+ "autoEncryptOpts": {
|
||||
+ "keyVaultNamespace": "keyvault.datakeys",
|
||||
+ "kmsProviders": {
|
||||
+ "azure": {
|
||||
+ "accessToken": {
|
||||
+ "$$placeholder": 1
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "database": {
|
||||
+ "id": "db",
|
||||
+ "client": "client",
|
||||
+ "databaseName": "db"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "collection": {
|
||||
+ "id": "coll",
|
||||
+ "database": "db",
|
||||
+ "collectionName": "coll"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "clientEncryption": {
|
||||
+ "id": "clientEncryption",
|
||||
+ "clientEncryptionOpts": {
|
||||
+ "keyVaultClient": "client",
|
||||
+ "keyVaultNamespace": "keyvault.datakeys",
|
||||
+ "kmsProviders": {
|
||||
+ "azure": {
|
||||
+ "accessToken": {
|
||||
+ "$$placeholder": 1
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "initialData": [
|
||||
+ {
|
||||
+ "databaseName": "db",
|
||||
+ "collectionName": "coll",
|
||||
+ "documents": [],
|
||||
+ "createOptions": {
|
||||
+ "validator": {
|
||||
+ "$jsonSchema": {
|
||||
+ "properties": {
|
||||
+ "secret": {
|
||||
+ "encrypt": {
|
||||
+ "keyId": [
|
||||
+ {
|
||||
+ "$binary": {
|
||||
+ "base64": "AZURE+AAAAAAAAAAAAAAAA==",
|
||||
+ "subType": "04"
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "bsonType": "string",
|
||||
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ "bsonType": "object"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "databaseName": "keyvault",
|
||||
+ "collectionName": "datakeys",
|
||||
+ "documents": [
|
||||
+ {
|
||||
+ "_id": {
|
||||
+ "$binary": {
|
||||
+ "base64": "AZURE+AAAAAAAAAAAAAAAA==",
|
||||
+ "subType": "04"
|
||||
+ }
|
||||
+ },
|
||||
+ "keyAltNames": [
|
||||
+ "my-key"
|
||||
+ ],
|
||||
+ "keyMaterial": {
|
||||
+ "$binary": {
|
||||
+ "base64": "n+HWZ0ZSVOYA3cvQgP7inN4JSXfOH85IngmeQxRpQHjCCcqT3IFqEWNlrsVHiz3AELimHhX4HKqOLWMUeSIT6emUDDoQX9BAv8DR1+E1w4nGs/NyEneac78EYFkK3JysrFDOgl2ypCCTKAypkn9CkAx1if4cfgQE93LW4kczcyHdGiH36CIxrCDGv1UzAvERN5Qa47DVwsM6a+hWsF2AAAJVnF0wYLLJU07TuRHdMrrphPWXZsFgyV+lRqJ7DDpReKNO8nMPLV/mHqHBHGPGQiRdb9NoJo8CvokGz4+KE8oLwzKf6V24dtwZmRkrsDV4iOhvROAzz+Euo1ypSkL3mw==",
|
||||
+ "subType": "00"
|
||||
+ }
|
||||
+ },
|
||||
+ "creationDate": {
|
||||
+ "$date": {
|
||||
+ "$numberLong": "1552949630483"
|
||||
+ }
|
||||
+ },
|
||||
+ "updateDate": {
|
||||
+ "$date": {
|
||||
+ "$numberLong": "1552949630483"
|
||||
+ }
|
||||
+ },
|
||||
+ "status": {
|
||||
+ "$numberInt": "0"
|
||||
+ },
|
||||
+ "masterKey": {
|
||||
+ "provider": "azure",
|
||||
+ "keyVaultEndpoint": "key-vault-csfle.vault.azure.net",
|
||||
+ "keyName": "key-name-csfle"
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ],
|
||||
+ "tests": [
|
||||
+ {
|
||||
+ "description": "Auto encrypt using access token Azure credentials",
|
||||
+ "operations": [
|
||||
+ {
|
||||
+ "name": "insertOne",
|
||||
+ "arguments": {
|
||||
+ "document": {
|
||||
+ "_id": 1,
|
||||
+ "secret": "string0"
|
||||
+ }
|
||||
+ },
|
||||
+ "object": "coll"
|
||||
+ }
|
||||
+ ],
|
||||
+ "outcome": [
|
||||
+ {
|
||||
+ "documents": [
|
||||
+ {
|
||||
+ "_id": 1,
|
||||
+ "secret": {
|
||||
+ "$binary": {
|
||||
+ "base64": "AQGVERPgAAAAAAAAAAAAAAAC5DbBSwPwfSlBrDtRuglvNvCXD1KzDuCKY2P+4bRFtHDjpTOE2XuytPAUaAbXf1orsPq59PVZmsbTZbt2CB8qaQ==",
|
||||
+ "subType": "06"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "collectionName": "coll",
|
||||
+ "databaseName": "db"
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "description": "Explicit encrypt using access token Azure credentials",
|
||||
+ "operations": [
|
||||
+ {
|
||||
+ "name": "encrypt",
|
||||
+ "object": "clientEncryption",
|
||||
+ "arguments": {
|
||||
+ "value": "string0",
|
||||
+ "opts": {
|
||||
+ "keyAltName": "my-key",
|
||||
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
|
||||
+ }
|
||||
+ },
|
||||
+ "expectResult": {
|
||||
+ "$binary": {
|
||||
+ "base64": "AQGVERPgAAAAAAAAAAAAAAAC5DbBSwPwfSlBrDtRuglvNvCXD1KzDuCKY2P+4bRFtHDjpTOE2XuytPAUaAbXf1orsPq59PVZmsbTZbt2CB8qaQ==",
|
||||
+ "subType": "06"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ]
|
||||
+}
|
||||
diff --git a/test/client-side-encryption/spec/unified/accessToken-gcp.json b/test/client-side-encryption/spec/unified/accessToken-gcp.json
|
||||
new file mode 100644
|
||||
index 00000000..f5cf8914
|
||||
--- /dev/null
|
||||
+++ b/test/client-side-encryption/spec/unified/accessToken-gcp.json
|
||||
@@ -0,0 +1,188 @@
|
||||
+{
|
||||
+ "description": "accessToken-gcp",
|
||||
+ "schemaVersion": "1.28",
|
||||
+ "runOnRequirements": [
|
||||
+ {
|
||||
+ "minServerVersion": "4.1.10",
|
||||
+ "csfle": {
|
||||
+ "minLibmongocryptVersion": "1.6.0"
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "createEntities": [
|
||||
+ {
|
||||
+ "client": {
|
||||
+ "id": "client",
|
||||
+ "autoEncryptOpts": {
|
||||
+ "keyVaultNamespace": "keyvault.datakeys",
|
||||
+ "kmsProviders": {
|
||||
+ "gcp": {
|
||||
+ "accessToken": {
|
||||
+ "$$placeholder": 1
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "database": {
|
||||
+ "id": "db",
|
||||
+ "client": "client",
|
||||
+ "databaseName": "db"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "collection": {
|
||||
+ "id": "coll",
|
||||
+ "database": "db",
|
||||
+ "collectionName": "coll"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "clientEncryption": {
|
||||
+ "id": "clientEncryption",
|
||||
+ "clientEncryptionOpts": {
|
||||
+ "keyVaultClient": "client",
|
||||
+ "keyVaultNamespace": "keyvault.datakeys",
|
||||
+ "kmsProviders": {
|
||||
+ "gcp": {
|
||||
+ "accessToken": {
|
||||
+ "$$placeholder": 1
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "initialData": [
|
||||
+ {
|
||||
+ "databaseName": "db",
|
||||
+ "collectionName": "coll",
|
||||
+ "documents": [],
|
||||
+ "createOptions": {
|
||||
+ "validator": {
|
||||
+ "$jsonSchema": {
|
||||
+ "properties": {
|
||||
+ "secret": {
|
||||
+ "encrypt": {
|
||||
+ "keyId": [
|
||||
+ {
|
||||
+ "$binary": {
|
||||
+ "base64": "GCP+AAAAAAAAAAAAAAAAAA==",
|
||||
+ "subType": "04"
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "bsonType": "string",
|
||||
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ "bsonType": "object"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "databaseName": "keyvault",
|
||||
+ "collectionName": "datakeys",
|
||||
+ "documents": [
|
||||
+ {
|
||||
+ "_id": {
|
||||
+ "$binary": {
|
||||
+ "base64": "GCP+AAAAAAAAAAAAAAAAAA==",
|
||||
+ "subType": "04"
|
||||
+ }
|
||||
+ },
|
||||
+ "keyAltNames": [
|
||||
+ "my-key"
|
||||
+ ],
|
||||
+ "keyMaterial": {
|
||||
+ "$binary": {
|
||||
+ "base64": "CiQAIgLj0WyktnB4dfYHo5SLZ41K4ASQrjJUaSzl5vvVH0G12G0SiQEAjlV8XPlbnHDEDFbdTO4QIe8ER2/172U1ouLazG0ysDtFFIlSvWX5ZnZUrRMmp/R2aJkzLXEt/zf8Mn4Lfm+itnjgo5R9K4pmPNvvPKNZX5C16lrPT+aA+rd+zXFSmlMg3i5jnxvTdLHhg3G7Q/Uv1ZIJskKt95bzLoe0tUVzRWMYXLIEcohnQg==",
|
||||
+ "subType": "00"
|
||||
+ }
|
||||
+ },
|
||||
+ "creationDate": {
|
||||
+ "$date": {
|
||||
+ "$numberLong": "1552949630483"
|
||||
+ }
|
||||
+ },
|
||||
+ "updateDate": {
|
||||
+ "$date": {
|
||||
+ "$numberLong": "1552949630483"
|
||||
+ }
|
||||
+ },
|
||||
+ "status": {
|
||||
+ "$numberInt": "0"
|
||||
+ },
|
||||
+ "masterKey": {
|
||||
+ "provider": "gcp",
|
||||
+ "projectId": "devprod-drivers",
|
||||
+ "location": "global",
|
||||
+ "keyRing": "key-ring-csfle",
|
||||
+ "keyName": "key-name-csfle"
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ],
|
||||
+ "tests": [
|
||||
+ {
|
||||
+ "description": "Auto encrypt using access token GCP credentials",
|
||||
+ "operations": [
|
||||
+ {
|
||||
+ "name": "insertOne",
|
||||
+ "arguments": {
|
||||
+ "document": {
|
||||
+ "_id": 1,
|
||||
+ "secret": "string0"
|
||||
+ }
|
||||
+ },
|
||||
+ "object": "coll"
|
||||
+ }
|
||||
+ ],
|
||||
+ "outcome": [
|
||||
+ {
|
||||
+ "documents": [
|
||||
+ {
|
||||
+ "_id": 1,
|
||||
+ "secret": {
|
||||
+ "$binary": {
|
||||
+ "base64": "ARgj/gAAAAAAAAAAAAAAAAACwFd+Y5Ojw45GUXNvbcIpN9YkRdoHDHkR4kssdn0tIMKlDQOLFkWFY9X07IRlXsxPD8DcTiKnl6XINK28vhcGlg==",
|
||||
+ "subType": "06"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "collectionName": "coll",
|
||||
+ "databaseName": "db"
|
||||
+ }
|
||||
+ ]
|
||||
+ },
|
||||
+ {
|
||||
+ "description": "Explicit encrypt using access token GCP credentials",
|
||||
+ "operations": [
|
||||
+ {
|
||||
+ "name": "encrypt",
|
||||
+ "object": "clientEncryption",
|
||||
+ "arguments": {
|
||||
+ "value": "string0",
|
||||
+ "opts": {
|
||||
+ "keyAltName": "my-key",
|
||||
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
|
||||
+ }
|
||||
+ },
|
||||
+ "expectResult": {
|
||||
+ "$binary": {
|
||||
+ "base64": "ARgj/gAAAAAAAAAAAAAAAAACwFd+Y5Ojw45GUXNvbcIpN9YkRdoHDHkR4kssdn0tIMKlDQOLFkWFY9X07IRlXsxPD8DcTiKnl6XINK28vhcGlg==",
|
||||
+ "subType": "06"
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }
|
||||
+ ]
|
||||
+}
|
||||
diff --git a/test/unified-test-format/invalid/clientEncryptionOpts-kmsProviders-azure-accessToken-type.json b/test/unified-test-format/invalid/clientEncryptionOpts-kmsProviders-azure-accessToken-type.json
|
||||
new file mode 100644
|
||||
index 00000000..8fe5c150
|
||||
--- /dev/null
|
||||
+++ b/test/unified-test-format/invalid/clientEncryptionOpts-kmsProviders-azure-accessToken-type.json
|
||||
@@ -0,0 +1,31 @@
|
||||
+{
|
||||
+ "description": "clientEncryptionOpts-kmsProviders-azure-accessToken-type",
|
||||
+ "schemaVersion": "1.28",
|
||||
+ "createEntities": [
|
||||
+ {
|
||||
+ "client": {
|
||||
+ "id": "client0"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "clientEncryption": {
|
||||
+ "id": "clientEncryption0",
|
||||
+ "clientEncryptionOpts": {
|
||||
+ "keyVaultClient": "client0",
|
||||
+ "keyVaultNamespace": "keyvault.datakeys",
|
||||
+ "kmsProviders": {
|
||||
+ "azure": {
|
||||
+ "accessToken": 0
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "tests": [
|
||||
+ {
|
||||
+ "description": "",
|
||||
+ "operations": []
|
||||
+ }
|
||||
+ ]
|
||||
+}
|
||||
diff --git a/test/unified-test-format/invalid/clientEncryptionOpts-kmsProviders-gcp-accessToken-type.json b/test/unified-test-format/invalid/clientEncryptionOpts-kmsProviders-gcp-accessToken-type.json
|
||||
new file mode 100644
|
||||
index 00000000..2284e26c
|
||||
--- /dev/null
|
||||
+++ b/test/unified-test-format/invalid/clientEncryptionOpts-kmsProviders-gcp-accessToken-type.json
|
||||
@@ -0,0 +1,31 @@
|
||||
+{
|
||||
+ "description": "clientEncryptionOpts-kmsProviders-gcp-accessToken-type",
|
||||
+ "schemaVersion": "1.28",
|
||||
+ "createEntities": [
|
||||
+ {
|
||||
+ "client": {
|
||||
+ "id": "client0"
|
||||
+ }
|
||||
+ },
|
||||
+ {
|
||||
+ "clientEncryption": {
|
||||
+ "id": "clientEncryption0",
|
||||
+ "clientEncryptionOpts": {
|
||||
+ "keyVaultClient": "client0",
|
||||
+ "keyVaultNamespace": "keyvault.datakeys",
|
||||
+ "kmsProviders": {
|
||||
+ "gcp": {
|
||||
+ "accessToken": 0
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ ],
|
||||
+ "tests": [
|
||||
+ {
|
||||
+ "description": "",
|
||||
+ "operations": []
|
||||
+ }
|
||||
+ ]
|
||||
+}
|
||||
44
.github/copilot-instructions.md
vendored
Normal file
44
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
When reviewing code, focus on:
|
||||
|
||||
## Security Critical Issues
|
||||
- Check for hardcoded secrets, API keys, or credentials.
|
||||
- Check for instances of potential method call injection, dynamic code execution, symbol injection or other code injection vulnerabilities.
|
||||
|
||||
## Performance Red Flags
|
||||
- Spot inefficient loops and algorithmic issues.
|
||||
- Check for memory leaks and resource cleanup.
|
||||
|
||||
## Code Quality Essentials
|
||||
- Methods should be focused and appropriately sized. If a method is doing too much, suggest refactorings to split it up.
|
||||
- Use clear, descriptive naming conventions.
|
||||
- Avoid encapsulation violations and ensure proper separation of concerns.
|
||||
- All public classes, modules, and methods should have clear documentation in Sphinx format.
|
||||
|
||||
## PyMongo-specific Concerns
|
||||
- Do not review files within `pymongo/synchronous` or files in `test/` that also have a file of the same name in `test/asynchronous` unless the reviewed changes include a `_IS_SYNC` statement. PyMongo generates these files from `pymongo/asynchronous` and `test/asynchronous` using `tools/synchro.py`.
|
||||
- All asynchronous functions must not call any blocking I/O.
|
||||
|
||||
## Review Style
|
||||
- Be specific and actionable in feedback.
|
||||
- Explain the "why" behind recommendations.
|
||||
- Acknowledge good patterns when you see them.
|
||||
- Ask clarifying questions when code intent is unclear.
|
||||
|
||||
Always prioritize security vulnerabilities and performance issues that could impact users.
|
||||
|
||||
Always suggest changes to improve readability and testability. For example, this suggestion seeks to make the code more readable, reusable, and testable:
|
||||
|
||||
```python
|
||||
# Instead of:
|
||||
if user.email and "@" in user.email and len(user.email) > 5:
|
||||
submit_button.enabled = True
|
||||
else:
|
||||
submit_button.enabled = False
|
||||
|
||||
# Consider:
|
||||
def valid_email(email):
|
||||
return email and "@" in email and len(email) > 5
|
||||
|
||||
|
||||
submit_button.enabled = valid_email(user.email)
|
||||
```
|
||||
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
@ -6,8 +6,8 @@ If you are an external contributor and there is no JIRA ticket associated with y
|
||||
for the PR title. A MongoDB employee will create a JIRA ticket and edit the name and links as appropriate.
|
||||
|
||||
Note on AI Contributions:
|
||||
We do not accept pull requests that are primarily or substantially generated by AI tools (ChatGPT, Copilot, etc.).
|
||||
All contributions must be written and understood by human contributors.
|
||||
We only accept pull requests that are authored and submitted by human contributors who fully understand the changes they are proposing.
|
||||
All contributions must be written and understood by human contributors. Please read about our policy in our contributing guide.
|
||||
-->
|
||||
[JIRA TICKET]
|
||||
|
||||
|
||||
10
.github/workflows/dist.yml
vendored
10
.github/workflows/dist.yml
vendored
@ -61,7 +61,7 @@ jobs:
|
||||
|
||||
- name: Set up QEMU
|
||||
if: runner.os == 'Linux'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
with:
|
||||
# setup-qemu-action by default uses `tonistiigi/binfmt:latest` image,
|
||||
# which is out of date. This causes seg faults during build.
|
||||
@ -92,7 +92,7 @@ jobs:
|
||||
# Free-threading builds:
|
||||
ls wheelhouse/*cp314t*.whl
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: wheel-${{ matrix.buildplat[1] }}
|
||||
path: ./wheelhouse/*.whl
|
||||
@ -125,7 +125,7 @@ jobs:
|
||||
cd ..
|
||||
python -c "from pymongo import has_c; assert has_c()"
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: "sdist"
|
||||
path: ./dist/*.tar.gz
|
||||
@ -136,13 +136,13 @@ jobs:
|
||||
name: Download Wheels
|
||||
steps:
|
||||
- name: Download all workflow run artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
- name: Flatten directory
|
||||
working-directory: .
|
||||
run: |
|
||||
find . -mindepth 2 -type f -exec mv {} . \;
|
||||
find . -type d -empty -delete
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: all-dist-${{ github.run_id }}
|
||||
path: "./*"
|
||||
|
||||
2
.github/workflows/release-python.yml
vendored
2
.github/workflows/release-python.yml
vendored
@ -75,7 +75,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: all-dist-${{ github.run_id }}
|
||||
path: dist/
|
||||
|
||||
2
.github/workflows/sbom.yml
vendored
2
.github/workflows/sbom.yml
vendored
@ -67,7 +67,7 @@ jobs:
|
||||
run: rm -rf .venv .venv-sbom sbom-requirements.txt
|
||||
|
||||
- name: Upload SBOM artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: sbom
|
||||
path: sbom.json
|
||||
|
||||
22
.github/workflows/test-python.yml
vendored
22
.github/workflows/test-python.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.10"
|
||||
@ -68,7 +68,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@ -90,7 +90,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.10"
|
||||
@ -118,7 +118,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.10"
|
||||
@ -143,7 +143,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.10"
|
||||
@ -162,7 +162,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.10"
|
||||
@ -184,7 +184,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "${{matrix.python}}"
|
||||
@ -205,7 +205,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: "3.10"
|
||||
@ -245,7 +245,7 @@ jobs:
|
||||
run: |
|
||||
pip install build
|
||||
python -m build --sdist
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: "sdist"
|
||||
path: dist/*.tar.gz
|
||||
@ -257,7 +257,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Download sdist
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: sdist/
|
||||
- name: Unpack SDist
|
||||
@ -295,7 +295,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||
with:
|
||||
python-version: "3.9"
|
||||
- id: setup-mongodb
|
||||
|
||||
2
.github/workflows/zizmor.yml
vendored
2
.github/workflows/zizmor.yml
vendored
@ -18,4 +18,4 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@135698455da5c3b3e55f73f4419e481ab68cdd95 # v0.4.1
|
||||
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -43,3 +43,4 @@ test/lambda/*.json
|
||||
xunit-results/
|
||||
coverage.xml
|
||||
server.log
|
||||
.coverage
|
||||
|
||||
@ -85,49 +85,53 @@ likelihood for getting review sooner shoots up.
|
||||
- `versionadded:: 3.11`
|
||||
- `versionchanged:: 3.5`
|
||||
|
||||
**Pull Request Template Breakdown**
|
||||
### AI-Generated Contributions Policy
|
||||
|
||||
- **Github PR Title**
|
||||
#### Our Stance
|
||||
|
||||
- The PR Title format should always be
|
||||
`[JIRA-ID] : Jira Title or Blurb Summary`.
|
||||
We only accept pull requests that are authored and submitted by human contributors who fully understand the changes they are proposing. Pull requests that are not clearly owned and understood by a human contributor may be closed. **All contributions must be submitted, reviewed, and understood by human contributors.**
|
||||
|
||||
- **JIRA LINK**
|
||||
##### Why This Policy Exists
|
||||
|
||||
- Convenient link to the associated JIRA ticket.
|
||||
At MongoDB, we understand the power and prevalence of AI tools in software development. With that being said, many MongoDB libraries are foundational tools used in production systems worldwide. The nature of these libraries requires:
|
||||
|
||||
- **Summary**
|
||||
- **Deep domain expertise**: MongoDB's wire protocol, BSON specification, connection pooling, authentication mechanisms, and concurrency patterns require an understanding that AI alone cannot substantiate.
|
||||
|
||||
- Small blurb on why this is needed. The JIRA task should have
|
||||
the more in-depth description, but this should still, at a
|
||||
high level, give anyone looking an understanding of why the
|
||||
PR has been checked in.
|
||||
- **Long-term maintainability**: Contributors need to be able to explain *why* code is written a certain way, explain design decisions, and be available to iterate on their contributions.
|
||||
|
||||
- **Changes in this PR**
|
||||
- **Security responsibility**: Authentication, credential handling, and TLS implementation cannot be left to probabilistic code generation.
|
||||
|
||||
- The explicit code changes that this PR is introducing. This
|
||||
should be more specific than just the task name. (Unless the
|
||||
task name is very clear).
|
||||
##### What This Means for Contributors
|
||||
|
||||
- **Test Plan**
|
||||
**Required:**
|
||||
|
||||
- Everything needs a test description. Describe what you did
|
||||
to validate your changes actually worked; if you did
|
||||
nothing, then document you did not test it. Aim to make
|
||||
these steps reproducible by other engineers, specifically
|
||||
with your primary reviewer in mind.
|
||||
- Full understanding of every line of code you submit
|
||||
- Ability to explain and defend your implementation choices
|
||||
- Willingness to iterate and maintain your contributions
|
||||
|
||||
- **Screenshots**
|
||||
**Encouraged:**
|
||||
|
||||
- Any images that provide more context to the PR. Usually,
|
||||
these just coincide with the test plan.
|
||||
- Using AI assistants as learning tools to understand concepts
|
||||
- IDE autocomplete features that suggest standard patterns
|
||||
- AI help for brainstorming approaches (but write the code yourself)
|
||||
- Writing code using AI tools, reviewing each line and revising code as necessary.
|
||||
|
||||
- **Callouts or follow-up items**
|
||||
**Not allowed:**
|
||||
|
||||
- This is a good place for identifying "to-dos" that you've
|
||||
placed in the code (Must have an accompanying JIRA Ticket).
|
||||
- Potential bugs that you are unsure how to test in the code.
|
||||
- Opinions you want to receive about your code.
|
||||
- Submitting PRs generated solely by AI tools
|
||||
- Copy-pasting AI-generated code without full understanding
|
||||
|
||||
##### Disclosure
|
||||
|
||||
If you used AI assistance in any way during your contribution, please disclose what the AI assistant was used for in your PR description. We would love to know what tools developers have found useful in iterating in their day to day.
|
||||
|
||||
##### Questions?
|
||||
|
||||
If you're unsure whether your contribution complies with this policy, please ask for guidance within the scope of the PR and clarify any uncertainty. We're happy to guide contributors toward successful contributions.
|
||||
|
||||
---
|
||||
|
||||
*This policy helps us maintain the reliability, security, and trustworthiness that production applications depend on. Thank you for understanding and for contributing thoughtfully to PyMongo.*
|
||||
|
||||
## Running Linters
|
||||
|
||||
@ -197,7 +201,7 @@ the pages will re-render and the browser will automatically refresh.
|
||||
version of Python, set `UV_PYTHON` before running `just install`.
|
||||
- Ensure you have started the appropriate Mongo Server(s). You can run `just run-server` with optional args
|
||||
to set up the server. All given options will be passed to
|
||||
[`run-orchestration.sh`](https://github.com/mongodb-labs/drivers-evergreen-tools/blob/master/.evergreen/run-orchestration.sh). Run `$DRIVERS_TOOLS/.evergreen/run-orchestration.sh -h`
|
||||
[`run-mongodb.sh`](https://github.com/mongodb-labs/drivers-evergreen-tools/blob/master/.evergreen/run-mongodb.sh). Run `$DRIVERS_TOOLS/.evergreen/run-mongodb.sh start -h`
|
||||
for a full list of options.
|
||||
- Run `just test` or `pytest` to run all of the tests.
|
||||
- Append `test/<mod_name>.py::<class_name>::<test_name>` to run
|
||||
@ -205,6 +209,7 @@ the pages will re-render and the browser will automatically refresh.
|
||||
and the `<class_name>` to test a full module. For example:
|
||||
`just test test/test_change_stream.py::TestUnifiedChangeStreamsErrors::test_change_stream_errors_on_ElectionInProgress`.
|
||||
- Use the `-k` argument to select tests by pattern.
|
||||
- Run `just test-coverage` to run tests with coverage and display a report. After running tests with coverage, use `just coverage-html` to generate an HTML report in `htmlcov/index.html`.
|
||||
|
||||
|
||||
## Running tests that require secrets, services, or other configuration
|
||||
@ -396,7 +401,7 @@ To run any of the test suites with minimum supported dependencies, pass `--test-
|
||||
|
||||
- If adding new tests files that should only be run for that test suite, add a pytest marker to the file and add
|
||||
to the list of pytest markers in `pyproject.toml`. Then add the test suite to the `TEST_SUITE_MAP` in `.evergreen/scripts/utils.py`. If for some reason it is not a pytest-runnable test, add it to the list of `EXTRA_TESTS` instead.
|
||||
- If the test uses Atlas or otherwise doesn't use `run-orchestration.sh`, add it to the `NO_RUN_ORCHESTRATION` list in
|
||||
- If the test uses Atlas or otherwise doesn't use `run-mongodb.sh`, add it to the `NO_RUN_ORCHESTRATION` list in
|
||||
`.evergreen/scripts/utils.py`.
|
||||
- If there is something special required to run the local server or there is an extra flag that should always be set
|
||||
like `AUTH`, add that logic to `.evergreen/scripts/run_server.py`.
|
||||
@ -500,13 +505,20 @@ python3 ./.evergreen/scripts/resync-all-specs.py
|
||||
|
||||
Follow the [Python Driver Release Process Wiki](https://wiki.corp.mongodb.com/display/DRIVERS/Python+Driver+Release+Process).
|
||||
|
||||
## Asyncio considerations
|
||||
## Project Structure and Asyncio Considerations
|
||||
|
||||
PyMongo adds asyncio capability by modifying the source files in `*/asynchronous` to `*/synchronous` using
|
||||
[unasync](https://github.com/python-trio/unasync/) and some custom transforms.
|
||||
This section describes the layout of the `pymongo/` package.
|
||||
|
||||
Where possible, edit the code in `*/asynchronous/*.py` and not the synchronous files.
|
||||
You can run `pre-commit run --all-files synchro` before running tests if you are testing synchronous code.
|
||||
Within `pymongo/`, the code is further divided into the `pymongo/asynchronous` and `pymongo/synchronous` subdirectories.
|
||||
Files in `pymongo/synchronous` are generated from `pymongo/asynchronous` using the `synchro` pre-commit hook, which uses [unasync](https://github.com/python-trio/unasync/) and some custom transforms.
|
||||
|
||||
As a result, **all modifications** within `pymongo` must be made in either the top-level `pymongo` directory when they have to exhibit differing behavior between sync and async contexts or the `pymongo/asynchronous` directory, not `pymongo/synchronous`.
|
||||
Any changes made directly to files in the `pymongo/synchronous` directory will be overwritten by the `synchro` hook when it is run, which happens automatically on commit.
|
||||
|
||||
Some top-level files (e.g. `pymongo/collection.py`) are re-export files for existing import compatibility and should not be modified directly.
|
||||
The other top-level files (e.g. `pymongo/network_layer.py`, `pymongo/pool_shared.py`) contain either shared code used in both the asynchronous and synchronous APIs, or code that is very different between the two APIs and therefore cannot be generated from the async version using `synchro`.
|
||||
|
||||
Run `pre-commit run --all-files synchro` before running tests to generate the latest version of the synchronous code.
|
||||
|
||||
To prevent the `synchro` hook from accidentally overwriting code, it first checks to see whether a sync version
|
||||
of a file is changing and not its async counterpart, and will fail.
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
[](https://pypi.org/project/pymongo)
|
||||
[](https://pepy.tech/project/pymongo)
|
||||
[](http://pymongo.readthedocs.io/en/stable/api?badge=stable)
|
||||
[](https://codecov.io/gh/mongodb/mongo-python-driver)
|
||||
|
||||
## About
|
||||
|
||||
@ -215,4 +216,4 @@ pip install -e ".[test]"
|
||||
pytest
|
||||
```
|
||||
|
||||
For more advanced testing scenarios, see the [contributing guide](./CONTRIBUTING.md#running-tests-locally).
|
||||
For more advanced testing scenarios, see the [contributing guide](https://github.com/mongodb/mongo-python-driver/blob/master/CONTRIBUTING.md#running-tests-locally).
|
||||
|
||||
@ -109,6 +109,7 @@ struct module_state {
|
||||
#define DATETIME_CLAMP 2
|
||||
#define DATETIME_MS 3
|
||||
#define DATETIME_AUTO 4
|
||||
#define PYTHON_3_12 0x030C0000
|
||||
|
||||
/* Converts integer to its string representation in decimal notation. */
|
||||
extern int cbson_long_long_to_str(long long num, char* str, size_t size) {
|
||||
@ -249,6 +250,67 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
||||
*/
|
||||
static int write_raw_doc(buffer_t buffer, PyObject* raw, PyObject* _raw);
|
||||
|
||||
#if PY_VERSION_HEX >= PYTHON_3_12
|
||||
/* Transfer traceback from old_exc to new_exc.
|
||||
* Steals reference to old_exc. */
|
||||
static PyObject* _transfer_traceback(PyObject *old_exc, PyObject *new_exc) {
|
||||
PyObject *tb = PyException_GetTraceback(old_exc);
|
||||
if (tb) {
|
||||
PyException_SetTraceback(new_exc, tb);
|
||||
Py_DECREF(tb);
|
||||
}
|
||||
Py_DECREF(old_exc);
|
||||
return new_exc;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Rewrap the current exception as InvalidBSON(str(e)) if it is not already an InvalidBSON error. */
|
||||
static void _rewrap_as_invalid_bson(void) {
|
||||
#if PY_VERSION_HEX >= PYTHON_3_12
|
||||
PyObject *exc = PyErr_GetRaisedException();
|
||||
if (exc && PyErr_GivenExceptionMatches(exc, PyExc_Exception)) {
|
||||
PyObject *InvalidBSON = _error("InvalidBSON");
|
||||
if (InvalidBSON) {
|
||||
if (!PyErr_GivenExceptionMatches(exc, InvalidBSON)) {
|
||||
PyObject *err_msg = PyObject_Str(exc);
|
||||
if (err_msg) {
|
||||
PyObject *new_exc = PyObject_CallOneArg(InvalidBSON, err_msg);
|
||||
if (new_exc) {
|
||||
exc = _transfer_traceback(exc, new_exc);
|
||||
}
|
||||
}
|
||||
Py_XDECREF(err_msg);
|
||||
}
|
||||
Py_DECREF(InvalidBSON);
|
||||
}
|
||||
}
|
||||
/* Steals reference to exc. */
|
||||
PyErr_SetRaisedException(exc);
|
||||
#else
|
||||
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
|
||||
PyObject *InvalidBSON = NULL;
|
||||
PyErr_Fetch(&etype, &evalue, &etrace);
|
||||
if (PyErr_GivenExceptionMatches(etype, PyExc_Exception)) {
|
||||
InvalidBSON = _error("InvalidBSON");
|
||||
if (InvalidBSON) {
|
||||
if (!PyErr_GivenExceptionMatches(etype, InvalidBSON)) {
|
||||
Py_DECREF(etype);
|
||||
etype = InvalidBSON;
|
||||
if (evalue) {
|
||||
PyObject *msg = PyObject_Str(evalue);
|
||||
Py_DECREF(evalue);
|
||||
evalue = msg;
|
||||
}
|
||||
PyErr_NormalizeException(&etype, &evalue, &etrace);
|
||||
} else {
|
||||
Py_DECREF(InvalidBSON);
|
||||
}
|
||||
}
|
||||
}
|
||||
PyErr_Restore(etype, evalue, etrace);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Date stuff */
|
||||
static PyObject* datetime_from_millis(long long millis) {
|
||||
/* To encode a datetime instance like datetime(9999, 12, 31, 23, 59, 59, 999999)
|
||||
@ -294,34 +356,57 @@ static PyObject* datetime_from_millis(long long millis) {
|
||||
timeinfo.tm_sec,
|
||||
microseconds);
|
||||
if(!datetime) {
|
||||
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
|
||||
#if PY_VERSION_HEX >= PYTHON_3_12
|
||||
PyObject *exc = PyErr_GetRaisedException();
|
||||
|
||||
/*
|
||||
* Calling _error clears the error state, so fetch it first.
|
||||
*/
|
||||
PyErr_Fetch(&etype, &evalue, &etrace);
|
||||
|
||||
/* Only add addition error message on ValueError exceptions. */
|
||||
if (PyErr_GivenExceptionMatches(etype, PyExc_ValueError)) {
|
||||
if (evalue) {
|
||||
PyObject* err_msg = PyObject_Str(evalue);
|
||||
/* Only add additional error message on ValueError exceptions. */
|
||||
if (exc && PyErr_GivenExceptionMatches(exc, PyExc_ValueError)) {
|
||||
PyObject* err_msg = PyObject_Str(exc);
|
||||
if (err_msg) {
|
||||
PyObject* appendage = PyUnicode_FromString(" (Consider Using CodecOptions(datetime_conversion=DATETIME_AUTO) or MongoClient(datetime_conversion='DATETIME_AUTO')). See: https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/dates-and-times/#handling-out-of-range-datetimes");
|
||||
if (appendage) {
|
||||
PyObject* msg = PyUnicode_Concat(err_msg, appendage);
|
||||
if (msg) {
|
||||
Py_DECREF(evalue);
|
||||
evalue = msg;
|
||||
PyObject* new_exc = PyObject_CallOneArg(PyExc_ValueError, msg);
|
||||
if (new_exc) {
|
||||
exc = _transfer_traceback(exc, new_exc);
|
||||
}
|
||||
Py_DECREF(msg);
|
||||
}
|
||||
}
|
||||
Py_XDECREF(appendage);
|
||||
}
|
||||
Py_XDECREF(err_msg);
|
||||
}
|
||||
PyErr_NormalizeException(&etype, &evalue, &etrace);
|
||||
}
|
||||
/* Steals references to args. */
|
||||
PyErr_Restore(etype, evalue, etrace);
|
||||
/* Steals reference to exc. */
|
||||
PyErr_SetRaisedException(exc);
|
||||
#else
|
||||
/* Calling _error clears the error state, so fetch it first.*/
|
||||
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
|
||||
PyErr_Fetch(&etype, &evalue, &etrace);
|
||||
|
||||
/* Only add additional error message on ValueError exceptions. */
|
||||
if (PyErr_GivenExceptionMatches(etype, PyExc_ValueError)) {
|
||||
if (evalue) {
|
||||
PyObject* err_msg = PyObject_Str(evalue);
|
||||
if (err_msg) {
|
||||
PyObject* appendage = PyUnicode_FromString(" (Consider Using CodecOptions(datetime_conversion=DATETIME_AUTO) or MongoClient(datetime_conversion='DATETIME_AUTO')). See: https://www.mongodb.com/docs/languages/python/pymongo-driver/current/data-formats/dates-and-times/#handling-out-of-range-datetimes");
|
||||
if (appendage) {
|
||||
PyObject* msg = PyUnicode_Concat(err_msg, appendage);
|
||||
if (msg) {
|
||||
Py_DECREF(evalue);
|
||||
evalue = msg;
|
||||
}
|
||||
}
|
||||
Py_XDECREF(appendage);
|
||||
}
|
||||
Py_XDECREF(err_msg);
|
||||
}
|
||||
PyErr_NormalizeException(&etype, &evalue, &etrace);
|
||||
}
|
||||
/* Steals references to args. */
|
||||
PyErr_Restore(etype, evalue, etrace);
|
||||
#endif
|
||||
}
|
||||
return datetime;
|
||||
}
|
||||
@ -356,7 +441,8 @@ static PyObject* datetime_ms_from_millis(PyObject* self, long long millis){
|
||||
if (!(ll_millis = PyLong_FromLongLong(millis))){
|
||||
return NULL;
|
||||
}
|
||||
dt = PyObject_CallFunctionObjArgs(state->DatetimeMS, ll_millis, NULL);
|
||||
PyObject* args[1] = {ll_millis};
|
||||
dt = PyObject_Vectorcall(state->DatetimeMS, args, 1, NULL);
|
||||
Py_DECREF(ll_millis);
|
||||
return dt;
|
||||
}
|
||||
@ -401,7 +487,9 @@ static PyObject* decode_datetime(PyObject* self, long long millis, const codec_o
|
||||
int64_t min_millis_offset = 0;
|
||||
int64_t max_millis_offset = 0;
|
||||
if (options->tz_aware && options->tzinfo && options->tzinfo != Py_None) {
|
||||
PyObject* utcoffset = PyObject_CallMethodObjArgs(options->tzinfo, state->_utcoffset_str, state->min_datetime, NULL);
|
||||
PyObject* utcoffset_args[2] = {options->tzinfo, state->min_datetime};
|
||||
PyObject* utcoffset = PyObject_VectorcallMethod(
|
||||
state->_utcoffset_str, utcoffset_args, 2, NULL);
|
||||
if (utcoffset == NULL) {
|
||||
return 0;
|
||||
}
|
||||
@ -420,7 +508,9 @@ static PyObject* decode_datetime(PyObject* self, long long millis, const codec_o
|
||||
(PyDateTime_DELTA_GET_MICROSECONDS(utcoffset) / 1000);
|
||||
}
|
||||
Py_DECREF(utcoffset);
|
||||
utcoffset = PyObject_CallMethodObjArgs(options->tzinfo, state->_utcoffset_str, state->max_datetime, NULL);
|
||||
utcoffset_args[1] = state->max_datetime;
|
||||
utcoffset = PyObject_VectorcallMethod(
|
||||
state->_utcoffset_str, utcoffset_args, 2, NULL);
|
||||
if (utcoffset == NULL) {
|
||||
return 0;
|
||||
}
|
||||
@ -481,7 +571,9 @@ static PyObject* decode_datetime(PyObject* self, long long millis, const codec_o
|
||||
|
||||
/* convert to local time */
|
||||
if (options->tzinfo != Py_None) {
|
||||
PyObject* temp = PyObject_CallMethodObjArgs(value, state->_astimezone_str, options->tzinfo, NULL);
|
||||
PyObject* astimezone_args[2] = {value, options->tzinfo};
|
||||
PyObject* temp = PyObject_VectorcallMethod(
|
||||
state->_astimezone_str, astimezone_args, 2, NULL);
|
||||
Py_DECREF(value);
|
||||
value = temp;
|
||||
}
|
||||
@ -688,7 +780,8 @@ static int _load_python_objects(PyObject* module) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
compiled = PyObject_CallFunction(re_compile, "O", empty_string);
|
||||
PyObject* compile_args[1] = {empty_string};
|
||||
compiled = PyObject_Vectorcall(re_compile, compile_args, 1, NULL);
|
||||
Py_DECREF(re_compile);
|
||||
if (compiled == NULL) {
|
||||
state->REType = NULL;
|
||||
@ -711,13 +804,19 @@ static long _type_marker(PyObject* object, PyObject* _type_marker_str) {
|
||||
PyObject* type_marker = NULL;
|
||||
long type = 0;
|
||||
|
||||
if (PyObject_HasAttr(object, _type_marker_str)) {
|
||||
type_marker = PyObject_GetAttr(object, _type_marker_str);
|
||||
if (type_marker == NULL) {
|
||||
#if PY_VERSION_HEX >= 0x030D0000
|
||||
// 3.13
|
||||
if (PyObject_GetOptionalAttr(object, _type_marker_str, &type_marker) == -1) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
# else
|
||||
if (PyObject_HasAttr(object, _type_marker_str)) {
|
||||
type_marker = PyObject_GetAttr(object, _type_marker_str);
|
||||
if (type_marker == NULL) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
/*
|
||||
* Python objects with broken __getattr__ implementations could return
|
||||
* arbitrary types for a call to PyObject_GetAttrString. For example
|
||||
@ -814,6 +913,7 @@ int convert_codec_options(PyObject* self, PyObject* options_obj, codec_options_t
|
||||
}
|
||||
|
||||
options->is_raw_bson = (101 == type_marker);
|
||||
options->is_dict_class = (options->document_class == (PyObject*)&PyDict_Type);
|
||||
options->options_obj = options_obj;
|
||||
|
||||
Py_INCREF(options->options_obj);
|
||||
@ -1013,10 +1113,20 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
||||
}
|
||||
/*
|
||||
* Use _type_marker attribute instead of PyObject_IsInstance for better perf.
|
||||
*
|
||||
* Skip _type_marker lookup for common built-in types
|
||||
* that we know don't have a _type_marker attribute. This avoids the overhead
|
||||
* of PyObject_HasAttr/PyObject_GetAttr calls for the most common cases.
|
||||
*/
|
||||
type = _type_marker(value, state->_type_marker_str);
|
||||
if (type < 0) {
|
||||
return 0;
|
||||
if (PyUnicode_CheckExact(value) || PyLong_CheckExact(value) || PyFloat_CheckExact(value) ||
|
||||
PyBool_Check(value) || PyDict_CheckExact(value) || PyList_CheckExact(value) ||
|
||||
PyTuple_CheckExact(value) || PyBytes_CheckExact(value) || value == Py_None) {
|
||||
type = 0;
|
||||
} else {
|
||||
type = _type_marker(value, state->_type_marker_str);
|
||||
if (type < 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
@ -1227,7 +1337,9 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
||||
case 100:
|
||||
{
|
||||
/* DBRef */
|
||||
PyObject* as_doc = PyObject_CallMethodObjArgs(value, state->_as_doc_str, NULL);
|
||||
PyObject* as_doc_args[1] = {value};
|
||||
PyObject* as_doc = PyObject_VectorcallMethod(
|
||||
state->_as_doc_str, as_doc_args, 1, NULL);
|
||||
if (!as_doc) {
|
||||
return 0;
|
||||
}
|
||||
@ -1383,7 +1495,9 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
||||
return write_unicode(buffer, value);
|
||||
} else if (PyDateTime_Check(value)) {
|
||||
long long millis;
|
||||
PyObject* utcoffset = PyObject_CallMethodObjArgs(value, state->_utcoffset_str , NULL);
|
||||
PyObject* utcoffset_args[1] = {value};
|
||||
PyObject* utcoffset = PyObject_VectorcallMethod(
|
||||
state->_utcoffset_str, utcoffset_args, 1, NULL);
|
||||
if (utcoffset == NULL)
|
||||
return 0;
|
||||
if (utcoffset != Py_None) {
|
||||
@ -1422,7 +1536,9 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
||||
if (!(uuid_rep_obj = PyLong_FromLong(options->uuid_rep))) {
|
||||
return 0;
|
||||
}
|
||||
binary_value = PyObject_CallMethodObjArgs(state->Binary, state->_from_uuid_str, value, uuid_rep_obj, NULL);
|
||||
PyObject* from_uuid_args[3] = {state->Binary, value, uuid_rep_obj};
|
||||
binary_value = PyObject_VectorcallMethod(
|
||||
state->_from_uuid_str, from_uuid_args, 3, NULL);
|
||||
Py_DECREF(uuid_rep_obj);
|
||||
|
||||
if (binary_value == NULL) {
|
||||
@ -1452,7 +1568,8 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
||||
if (converter != NULL) {
|
||||
/* Transform types that have a registered converter.
|
||||
* A new reference is created upon transformation. */
|
||||
new_value = PyObject_CallFunctionObjArgs(converter, value, NULL);
|
||||
PyObject* converter_args[1] = {value};
|
||||
new_value = PyObject_Vectorcall(converter, converter_args, 1, NULL);
|
||||
if (new_value == NULL) {
|
||||
return 0;
|
||||
}
|
||||
@ -1466,8 +1583,9 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
|
||||
/* Try the fallback encoder if one is provided and we have not already
|
||||
* attempted to use the fallback encoder. */
|
||||
if (!in_fallback_call && options->type_registry.has_fallback_encoder) {
|
||||
new_value = PyObject_CallFunctionObjArgs(
|
||||
options->type_registry.fallback_encoder, value, NULL);
|
||||
PyObject* fallback_args[1] = {value};
|
||||
new_value = PyObject_Vectorcall(
|
||||
options->type_registry.fallback_encoder, fallback_args, 1, NULL);
|
||||
if (new_value == NULL) {
|
||||
// propagate any exception raised by the callback
|
||||
return 0;
|
||||
@ -1648,6 +1766,46 @@ fail:
|
||||
/* Update Invalid Document error to include doc as a property.
|
||||
*/
|
||||
void handle_invalid_doc_error(PyObject* dict) {
|
||||
#if PY_VERSION_HEX >= PYTHON_3_12
|
||||
PyObject *exc = PyErr_GetRaisedException();
|
||||
PyObject *msg = NULL, *new_msg = NULL;
|
||||
PyObject *InvalidDocument = NULL;
|
||||
|
||||
if (exc == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
InvalidDocument = _error("InvalidDocument");
|
||||
if (InvalidDocument == NULL) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (PyErr_GivenExceptionMatches(exc, InvalidDocument)) {
|
||||
msg = PyObject_Str(exc);
|
||||
if (msg) {
|
||||
const char *msg_utf8 = PyUnicode_AsUTF8(msg);
|
||||
if (msg_utf8 == NULL) {
|
||||
goto cleanup;
|
||||
}
|
||||
new_msg = PyUnicode_FromFormat("Invalid document: %s", msg_utf8);
|
||||
if (new_msg == NULL) {
|
||||
goto cleanup;
|
||||
}
|
||||
/* Add doc to the error instance as a property. */
|
||||
PyObject* exc_args[2] = {new_msg, dict};
|
||||
PyObject* new_exc = PyObject_Vectorcall(InvalidDocument, exc_args, 2, NULL);
|
||||
if (new_exc) {
|
||||
exc = _transfer_traceback(exc, new_exc);
|
||||
}
|
||||
}
|
||||
}
|
||||
cleanup:
|
||||
/* Steals reference to exc. */
|
||||
PyErr_SetRaisedException(exc);
|
||||
Py_XDECREF(msg);
|
||||
Py_XDECREF(InvalidDocument);
|
||||
Py_XDECREF(new_msg);
|
||||
#else
|
||||
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
|
||||
PyObject *msg = NULL, *new_msg = NULL, *new_evalue = NULL;
|
||||
PyErr_Fetch(&etype, &evalue, &etrace);
|
||||
@ -1668,7 +1826,8 @@ void handle_invalid_doc_error(PyObject* dict) {
|
||||
goto cleanup;
|
||||
}
|
||||
// Add doc to the error instance as a property.
|
||||
new_evalue = PyObject_CallFunctionObjArgs(InvalidDocument, new_msg, dict, NULL);
|
||||
PyObject* exc_args[2] = {new_msg, dict};
|
||||
new_evalue = PyObject_Vectorcall(InvalidDocument, exc_args, 2, NULL);
|
||||
Py_DECREF(evalue);
|
||||
Py_DECREF(etype);
|
||||
etype = InvalidDocument;
|
||||
@ -1689,6 +1848,7 @@ cleanup:
|
||||
Py_XDECREF(InvalidDocument);
|
||||
Py_XDECREF(new_evalue);
|
||||
Py_XDECREF(new_msg);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@ -1944,7 +2104,8 @@ static PyObject *_dbref_hook(PyObject* self, PyObject* value) {
|
||||
PyMapping_DelItem(value, state->_dollar_db_str);
|
||||
}
|
||||
|
||||
ret = PyObject_CallFunctionObjArgs(state->DBRef, ref, id, database, value, NULL);
|
||||
PyObject* dbref_args[4] = {ref, id, database, value};
|
||||
ret = PyObject_Vectorcall(state->DBRef, dbref_args, 4, NULL);
|
||||
Py_DECREF(value);
|
||||
} else {
|
||||
ret = value;
|
||||
@ -2120,7 +2281,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
}
|
||||
memcpy(&length, buffer + *position, 4);
|
||||
length = BSON_UINT32_FROM_LE(length);
|
||||
if (max < length) {
|
||||
if (max - 5 < length) { // Account for 5-byte header. max >= 5 guaranteed above
|
||||
goto invalid;
|
||||
}
|
||||
|
||||
@ -2160,7 +2321,13 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
goto uuiderror;
|
||||
}
|
||||
|
||||
binary_value = PyObject_CallFunction(state->Binary, "(Oi)", data, subtype);
|
||||
PyObject* subtype_obj = PyLong_FromLong(subtype);
|
||||
if (!subtype_obj) {
|
||||
goto uuiderror;
|
||||
}
|
||||
PyObject* binary_args[2] = {data, subtype_obj};
|
||||
binary_value = PyObject_Vectorcall(state->Binary, binary_args, 2, NULL);
|
||||
Py_DECREF(subtype_obj);
|
||||
if (binary_value == NULL) {
|
||||
goto uuiderror;
|
||||
}
|
||||
@ -2175,7 +2342,9 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
if (!uuid_rep_obj) {
|
||||
goto uuiderror;
|
||||
}
|
||||
value = PyObject_CallMethodObjArgs(binary_value, state->_as_uuid_str, uuid_rep_obj, NULL);
|
||||
PyObject* as_uuid_args[2] = {binary_value, uuid_rep_obj};
|
||||
value = PyObject_VectorcallMethod(
|
||||
state->_as_uuid_str, as_uuid_args, 2, NULL);
|
||||
Py_DECREF(uuid_rep_obj);
|
||||
}
|
||||
|
||||
@ -2194,7 +2363,8 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
Py_DECREF(data);
|
||||
goto invalid;
|
||||
}
|
||||
value = PyObject_CallFunctionObjArgs(state->Binary, data, st, NULL);
|
||||
PyObject* binary_args[2] = {data, st};
|
||||
value = PyObject_Vectorcall(state->Binary, binary_args, 2, NULL);
|
||||
Py_DECREF(st);
|
||||
Py_DECREF(data);
|
||||
if (!value) {
|
||||
@ -2215,7 +2385,13 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
if (max < 12) {
|
||||
goto invalid;
|
||||
}
|
||||
value = PyObject_CallFunction(state->ObjectId, "y#", buffer + *position, (Py_ssize_t)12);
|
||||
PyObject* oid_bytes = PyBytes_FromStringAndSize(buffer + *position, 12);
|
||||
if (!oid_bytes) {
|
||||
goto invalid;
|
||||
}
|
||||
PyObject* oid_args[1] = {oid_bytes};
|
||||
value = PyObject_Vectorcall(state->ObjectId, oid_args, 1, NULL);
|
||||
Py_DECREF(oid_bytes);
|
||||
*position += 12;
|
||||
break;
|
||||
}
|
||||
@ -2294,7 +2470,14 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
}
|
||||
*position += (unsigned)flags_length + 1;
|
||||
|
||||
value = PyObject_CallFunction(state->Regex, "Oi", pattern, flags);
|
||||
PyObject* flags_obj = PyLong_FromLong(flags);
|
||||
if (!flags_obj) {
|
||||
Py_DECREF(pattern);
|
||||
goto invalid;
|
||||
}
|
||||
PyObject* regex_args[2] = {pattern, flags_obj};
|
||||
value = PyObject_Vectorcall(state->Regex, regex_args, 2, NULL);
|
||||
Py_DECREF(flags_obj);
|
||||
Py_DECREF(pattern);
|
||||
break;
|
||||
}
|
||||
@ -2327,13 +2510,21 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
}
|
||||
*position += coll_length;
|
||||
|
||||
id = PyObject_CallFunction(state->ObjectId, "y#", buffer + *position, (Py_ssize_t)12);
|
||||
PyObject* oid_bytes = PyBytes_FromStringAndSize(buffer + *position, 12);
|
||||
if (!oid_bytes) {
|
||||
Py_DECREF(collection);
|
||||
goto invalid;
|
||||
}
|
||||
PyObject* oid_args[1] = {oid_bytes};
|
||||
id = PyObject_Vectorcall(state->ObjectId, oid_args, 1, NULL);
|
||||
Py_DECREF(oid_bytes);
|
||||
if (!id) {
|
||||
Py_DECREF(collection);
|
||||
goto invalid;
|
||||
}
|
||||
*position += 12;
|
||||
value = PyObject_CallFunctionObjArgs(state->DBRef, collection, id, NULL);
|
||||
PyObject* dbref_args[2] = {collection, id};
|
||||
value = PyObject_Vectorcall(state->DBRef, dbref_args, 2, NULL);
|
||||
Py_DECREF(collection);
|
||||
Py_DECREF(id);
|
||||
break;
|
||||
@ -2363,7 +2554,8 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
goto invalid;
|
||||
}
|
||||
*position += value_length;
|
||||
value = PyObject_CallFunctionObjArgs(state->Code, code, NULL, NULL);
|
||||
PyObject* code_args[1] = {code};
|
||||
value = PyObject_Vectorcall(state->Code, code_args, 1, NULL);
|
||||
Py_DECREF(code);
|
||||
break;
|
||||
}
|
||||
@ -2429,7 +2621,8 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
}
|
||||
*position += scope_size;
|
||||
|
||||
value = PyObject_CallFunctionObjArgs(state->Code, code, scope, NULL);
|
||||
PyObject* code_scope_args[2] = {code, scope};
|
||||
value = PyObject_Vectorcall(state->Code, code_scope_args, 2, NULL);
|
||||
Py_DECREF(code);
|
||||
Py_DECREF(scope);
|
||||
break;
|
||||
@ -2459,7 +2652,19 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
memcpy(&time, buffer + *position + 4, 4);
|
||||
inc = BSON_UINT32_FROM_LE(inc);
|
||||
time = BSON_UINT32_FROM_LE(time);
|
||||
value = PyObject_CallFunction(state->Timestamp, "II", time, inc);
|
||||
PyObject* time_obj = PyLong_FromUnsignedLong(time);
|
||||
if (!time_obj) {
|
||||
goto invalid;
|
||||
}
|
||||
PyObject* inc_obj = PyLong_FromUnsignedLong(inc);
|
||||
if (!inc_obj) {
|
||||
Py_DECREF(time_obj);
|
||||
goto invalid;
|
||||
}
|
||||
PyObject* ts_args[2] = {time_obj, inc_obj};
|
||||
value = PyObject_Vectorcall(state->Timestamp, ts_args, 2, NULL);
|
||||
Py_DECREF(time_obj);
|
||||
Py_DECREF(inc_obj);
|
||||
*position += 8;
|
||||
break;
|
||||
}
|
||||
@ -2471,7 +2676,13 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
}
|
||||
memcpy(&ll, buffer + *position, 8);
|
||||
ll = (int64_t)BSON_UINT64_FROM_LE(ll);
|
||||
value = PyObject_CallFunction(state->BSONInt64, "L", ll);
|
||||
PyObject* ll_obj = PyLong_FromLongLong(ll);
|
||||
if (!ll_obj) {
|
||||
goto invalid;
|
||||
}
|
||||
PyObject* int64_args[1] = {ll_obj};
|
||||
value = PyObject_Vectorcall(state->BSONInt64, int64_args, 1, NULL);
|
||||
Py_DECREF(ll_obj);
|
||||
*position += 8;
|
||||
break;
|
||||
}
|
||||
@ -2484,19 +2695,21 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
if (!_bytes_obj) {
|
||||
goto invalid;
|
||||
}
|
||||
value = PyObject_CallMethodObjArgs(state->Decimal128, state->_from_bid_str, _bytes_obj, NULL);
|
||||
PyObject* dec128_args[2] = {state->Decimal128, _bytes_obj};
|
||||
value = PyObject_VectorcallMethod(
|
||||
state->_from_bid_str, dec128_args, 2, NULL);
|
||||
Py_DECREF(_bytes_obj);
|
||||
*position += 16;
|
||||
break;
|
||||
}
|
||||
case 255:
|
||||
{
|
||||
value = PyObject_CallFunctionObjArgs(state->MinKey, NULL);
|
||||
value = PyObject_Vectorcall(state->MinKey, NULL, 0, NULL);
|
||||
break;
|
||||
}
|
||||
case 127:
|
||||
{
|
||||
value = PyObject_CallFunctionObjArgs(state->MaxKey, NULL);
|
||||
value = PyObject_Vectorcall(state->MaxKey, NULL, 0, NULL);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@ -2548,7 +2761,8 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
}
|
||||
converter = PyDict_GetItem(options->type_registry.decoder_map, value_type);
|
||||
if (converter != NULL) {
|
||||
PyObject* new_value = PyObject_CallFunctionObjArgs(converter, value, NULL);
|
||||
PyObject* converter_args[1] = {value};
|
||||
PyObject* new_value = PyObject_Vectorcall(converter, converter_args, 1, NULL);
|
||||
Py_DECREF(value_type);
|
||||
Py_DECREF(value);
|
||||
return new_value;
|
||||
@ -2566,42 +2780,7 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
|
||||
* Wrap any non-InvalidBSON errors in InvalidBSON.
|
||||
*/
|
||||
if (PyErr_Occurred()) {
|
||||
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
|
||||
PyObject *InvalidBSON = NULL;
|
||||
|
||||
/*
|
||||
* Calling _error clears the error state, so fetch it first.
|
||||
*/
|
||||
PyErr_Fetch(&etype, &evalue, &etrace);
|
||||
|
||||
/* Dont reraise anything but PyExc_Exceptions as InvalidBSON. */
|
||||
if (PyErr_GivenExceptionMatches(etype, PyExc_Exception)) {
|
||||
InvalidBSON = _error("InvalidBSON");
|
||||
if (InvalidBSON) {
|
||||
if (!PyErr_GivenExceptionMatches(etype, InvalidBSON)) {
|
||||
/*
|
||||
* Raise InvalidBSON(str(e)).
|
||||
*/
|
||||
Py_DECREF(etype);
|
||||
etype = InvalidBSON;
|
||||
|
||||
if (evalue) {
|
||||
PyObject *msg = PyObject_Str(evalue);
|
||||
Py_DECREF(evalue);
|
||||
evalue = msg;
|
||||
}
|
||||
PyErr_NormalizeException(&etype, &evalue, &etrace);
|
||||
} else {
|
||||
/*
|
||||
* The current exception matches InvalidBSON, so we don't
|
||||
* need this reference after all.
|
||||
*/
|
||||
Py_DECREF(InvalidBSON);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Steals references to args. */
|
||||
PyErr_Restore(etype, evalue, etrace);
|
||||
_rewrap_as_invalid_bson();
|
||||
} else {
|
||||
PyObject *InvalidBSON = _error("InvalidBSON");
|
||||
if (InvalidBSON) {
|
||||
@ -2639,25 +2818,7 @@ static int _element_to_dict(PyObject* self, const char* string,
|
||||
if (!*name) {
|
||||
/* If NULL is returned then wrap the UnicodeDecodeError
|
||||
in an InvalidBSON error */
|
||||
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
|
||||
PyObject *InvalidBSON = NULL;
|
||||
|
||||
PyErr_Fetch(&etype, &evalue, &etrace);
|
||||
if (PyErr_GivenExceptionMatches(etype, PyExc_Exception)) {
|
||||
InvalidBSON = _error("InvalidBSON");
|
||||
if (InvalidBSON) {
|
||||
Py_DECREF(etype);
|
||||
etype = InvalidBSON;
|
||||
|
||||
if (evalue) {
|
||||
PyObject *msg = PyObject_Str(evalue);
|
||||
Py_DECREF(evalue);
|
||||
evalue = msg;
|
||||
}
|
||||
PyErr_NormalizeException(&etype, &evalue, &etrace);
|
||||
}
|
||||
}
|
||||
PyErr_Restore(etype, evalue, etrace);
|
||||
_rewrap_as_invalid_bson();
|
||||
return -1;
|
||||
}
|
||||
position += (unsigned)name_length + 1;
|
||||
@ -2716,11 +2877,20 @@ static PyObject* _elements_to_dict(PyObject* self, const char* string,
|
||||
unsigned max,
|
||||
const codec_options_t* options) {
|
||||
unsigned position = 0;
|
||||
PyObject* dict = PyObject_CallObject(options->document_class, NULL);
|
||||
PyObject* dict;
|
||||
int raw_array = 0;
|
||||
|
||||
/* Use PyDict_New() directly when document_class is dict.
|
||||
* This avoids the overhead of PyObject_CallObject() for the common case. */
|
||||
if (options->is_dict_class) {
|
||||
dict = PyDict_New();
|
||||
} else {
|
||||
dict = PyObject_CallObject(options->document_class, NULL);
|
||||
}
|
||||
if (!dict) {
|
||||
return NULL;
|
||||
}
|
||||
int raw_array = 0;
|
||||
|
||||
while (position < max) {
|
||||
PyObject* name = NULL;
|
||||
PyObject* value = NULL;
|
||||
@ -2735,7 +2905,24 @@ static PyObject* _elements_to_dict(PyObject* self, const char* string,
|
||||
position = (unsigned)new_position;
|
||||
}
|
||||
|
||||
PyObject_SetItem(dict, name, value);
|
||||
/* Use PyDict_SetItem() when document_class is dict.
|
||||
* PyDict_SetItem() is faster than PyObject_SetItem() because it
|
||||
* avoids method lookup overhead. */
|
||||
if (options->is_dict_class) {
|
||||
if (PyDict_SetItem(dict, name, value) < 0) {
|
||||
Py_DECREF(name);
|
||||
Py_DECREF(value);
|
||||
Py_DECREF(dict);
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
if (PyObject_SetItem(dict, name, value) < 0) {
|
||||
Py_DECREF(name);
|
||||
Py_DECREF(value);
|
||||
Py_DECREF(dict);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
Py_DECREF(name);
|
||||
Py_DECREF(value);
|
||||
}
|
||||
@ -2747,9 +2934,14 @@ static PyObject* elements_to_dict(PyObject* self, const char* string,
|
||||
const codec_options_t* options) {
|
||||
PyObject* result;
|
||||
if (options->is_raw_bson) {
|
||||
return PyObject_CallFunction(
|
||||
options->document_class, "y#O",
|
||||
string, max, options->options_obj);
|
||||
PyObject* bson_bytes = PyBytes_FromStringAndSize(string, max);
|
||||
if (!bson_bytes) {
|
||||
return NULL;
|
||||
}
|
||||
PyObject* raw_args[2] = {bson_bytes, options->options_obj};
|
||||
result = PyObject_Vectorcall(options->document_class, raw_args, 2, NULL);
|
||||
Py_DECREF(bson_bytes);
|
||||
return result;
|
||||
}
|
||||
if (Py_EnterRecursiveCall(" while decoding a BSON document"))
|
||||
return NULL;
|
||||
|
||||
@ -72,6 +72,7 @@ typedef struct codec_options_t {
|
||||
unsigned char datetime_conversion;
|
||||
PyObject* options_obj;
|
||||
unsigned char is_raw_bson;
|
||||
unsigned char is_dict_class;
|
||||
} codec_options_t;
|
||||
|
||||
/* C API functions */
|
||||
|
||||
16
bson/son.py
16
bson/son.py
@ -22,6 +22,7 @@ from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
import warnings
|
||||
from collections.abc import Mapping as _Mapping
|
||||
from typing import (
|
||||
Any,
|
||||
@ -99,13 +100,28 @@ class SON(Dict[_Key, _Value]):
|
||||
yield from self.__keys
|
||||
|
||||
def has_key(self, key: _Key) -> bool:
|
||||
warnings.warn(
|
||||
"SON.has_key() is deprecated, use the in operator instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return key in self.__keys
|
||||
|
||||
def iterkeys(self) -> Iterator[_Key]:
|
||||
warnings.warn(
|
||||
"SON.iterkeys() is deprecated, use the keys() method instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.__iter__()
|
||||
|
||||
# fourth level uses definitions from lower levels
|
||||
def itervalues(self) -> Iterator[_Value]:
|
||||
warnings.warn(
|
||||
"SON.itervalues() is deprecated, use the values() method instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
for _, v in self.items():
|
||||
yield v
|
||||
|
||||
|
||||
@ -1,6 +1,22 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Changes in Version 4.17.0 (2026/04/20)
|
||||
--------------------------------------
|
||||
|
||||
PyMongo 4.17 brings a number of changes including:
|
||||
|
||||
- ``has_key``, ``iterkeys`` and ``itervalues`` in :class:`bson.son.SON` have
|
||||
been deprecated and will be removed in PyMongo 5.0. These methods were
|
||||
deprecated in favor of the standard dictionary containment operator ``in``
|
||||
and the ``keys()`` and ``values()`` methods, respectively.
|
||||
- Added the :meth:`~pymongo.asynchronous.client_session.AsyncClientSession.bind` and :meth:`~pymongo.client_session.ClientSession.bind` methods
|
||||
that allow users to bind a session to all database operations within the scope of a context manager instead of having to explicitly pass the session to each individual operation.
|
||||
See the `Transactions docs <https://www.mongodb.com/docs/languages/python/pymongo-driver/current/crud/transactions/#methods>`_ for examples and more information.
|
||||
- Added support for MongoDB's Intelligent Workload Management (IWM) and ingress connection rate limiting features.
|
||||
The driver now gracefully handles write-blocking scenarios and optimizes connection establishment during high-load conditions to maintain application availability.
|
||||
See the `IWM <https://www.mongodb.com/docs/atlas/intelligent-workload-management>`_ or `Overload Errors <https://www.mongodb.com/docs/atlas/overload-errors/?interface=driver&language=python>`_ docs for more information.
|
||||
|
||||
Changes in Version 4.16.0 (2026/01/07)
|
||||
--------------------------------------
|
||||
|
||||
|
||||
47
justfile
47
justfile
@ -16,61 +16,78 @@ default:
|
||||
resync:
|
||||
@uv sync --quiet
|
||||
|
||||
# Set up the development environment
|
||||
install:
|
||||
bash .evergreen/scripts/setup-dev-env.sh
|
||||
|
||||
# Build the HTML documentation
|
||||
[group('docs')]
|
||||
docs: && resync
|
||||
{{docs_run}} sphinx-build -W -b html doc {{doc_build}}/html
|
||||
|
||||
# Serve the docs locally with live-reload
|
||||
[group('docs')]
|
||||
docs-serve: && resync
|
||||
{{docs_run}} sphinx-autobuild -W -b html doc --watch ./pymongo --watch ./bson --watch ./gridfs {{doc_build}}/serve
|
||||
|
||||
# Check documentation hyperlinks for broken URLs
|
||||
[group('docs')]
|
||||
docs-linkcheck: && resync
|
||||
{{docs_run}} sphinx-build -E -b linkcheck doc {{doc_build}}/linkcheck
|
||||
|
||||
# Run mypy and pyright
|
||||
[group('typing')]
|
||||
typing: && resync
|
||||
just typing-mypy
|
||||
just typing-pyright
|
||||
|
||||
# Run mypy against the library source and test suite
|
||||
[group('typing')]
|
||||
typing-mypy: && resync
|
||||
{{typing_run}} python -m mypy {{mypy_args}} bson gridfs tools pymongo
|
||||
{{typing_run}} python -m mypy {{mypy_args}} --config-file mypy_test.ini test
|
||||
{{typing_run}} python -m mypy {{mypy_args}} test/test_typing.py test/test_typing_strict.py
|
||||
|
||||
# Run pyright against the typing test files
|
||||
[group('typing')]
|
||||
typing-pyright: && resync
|
||||
{{typing_run}} python -m pyright test/test_typing.py test/test_typing_strict.py
|
||||
{{typing_run}} python -m pyright -p strict_pyrightconfig.json test/test_typing_strict.py
|
||||
|
||||
# Run all pre-commit hooks across the repository
|
||||
[group('lint')]
|
||||
lint *args="": && resync
|
||||
uvx pre-commit run --all-files {{args}}
|
||||
|
||||
# Run shellcheck, doc8, and slotscheck
|
||||
[group('lint')]
|
||||
lint-manual *args="": && resync
|
||||
uvx pre-commit run --all-files --hook-stage manual {{args}}
|
||||
|
||||
# Run pytest (e.g. just test test/test_uri_parser.py)
|
||||
[group('test')]
|
||||
test *args="-v --durations=5 --maxfail=10": && resync
|
||||
uv run --extra test python -m pytest {{args}}
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
uv run ${USE_ACTIVE_VENV:+--active} --extra test python -m pytest {{args}}
|
||||
|
||||
# Run the BSON test suite with numpy
|
||||
[group('test')]
|
||||
test-numpy: && resync
|
||||
uv run --extra test --with numpy python -m pytest test/test_bson.py
|
||||
test-numpy *args="": && resync
|
||||
just setup-tests numpy {{args}}
|
||||
just run-tests test/test_bson.py
|
||||
|
||||
# Run tests via the Evergreen test runner script
|
||||
[group('test')]
|
||||
run-tests *args: && resync
|
||||
bash ./.evergreen/run-tests.sh {{args}}
|
||||
|
||||
# Set up the test environment (auth, TLS, etc.)
|
||||
[group('test')]
|
||||
setup-tests *args="":
|
||||
bash .evergreen/scripts/setup-tests.sh {{args}}
|
||||
|
||||
# Tear down resources created by setup-tests
|
||||
[group('test')]
|
||||
teardown-tests:
|
||||
bash .evergreen/scripts/teardown-tests.sh
|
||||
@ -79,6 +96,30 @@ teardown-tests:
|
||||
integration-tests:
|
||||
bash integration_tests/run.sh
|
||||
|
||||
# Run the full test suite with coverage
|
||||
[group('test')]
|
||||
test-coverage *args="":
|
||||
just setup-tests --cov
|
||||
just run-tests {{args}}
|
||||
|
||||
# Print the coverage summary to the terminal
|
||||
[group('coverage')]
|
||||
coverage-report:
|
||||
uv tool run --with "coverage[toml]" coverage report
|
||||
|
||||
# Generate an HTML coverage report in htmlcov/
|
||||
[group('coverage')]
|
||||
coverage-html:
|
||||
uv tool run --with "coverage[toml]" coverage html
|
||||
@echo "Coverage report generated in htmlcov/index.html"
|
||||
|
||||
# Generate an XML coverage report at coverage.xml
|
||||
[group('coverage')]
|
||||
coverage-xml:
|
||||
uv tool run --with "coverage[toml]" coverage xml
|
||||
@echo "Coverage report generated in coverage.xml"
|
||||
|
||||
# Start a MongoDB server via drivers-evergreen-tools
|
||||
[group('server')]
|
||||
run-server *args="":
|
||||
bash .evergreen/scripts/run-server.sh {{args}}
|
||||
|
||||
@ -17,6 +17,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
def _get_azure_response(
|
||||
@ -29,7 +30,7 @@ def _get_azure_response(
|
||||
url += "?api-version=2018-02-01"
|
||||
url += f"&resource={resource}"
|
||||
if client_id:
|
||||
url += f"&client_id={client_id}"
|
||||
url += f"&client_id={quote(client_id)}"
|
||||
headers = {"Metadata": "true", "Accept": "application/json"}
|
||||
request = Request(url, headers=headers) # noqa: S310
|
||||
try:
|
||||
|
||||
@ -18,7 +18,7 @@ from __future__ import annotations
|
||||
import re
|
||||
from typing import List, Tuple, Union
|
||||
|
||||
__version__ = "4.17.0.dev0"
|
||||
__version__ = "4.18.0.dev0"
|
||||
|
||||
|
||||
def get_version_tuple(version: str) -> Tuple[Union[int, str], ...]:
|
||||
|
||||
@ -59,6 +59,7 @@ from pymongo.errors import (
|
||||
InvalidOperation,
|
||||
NotPrimaryError,
|
||||
OperationFailure,
|
||||
PyMongoError,
|
||||
WaitQueueTimeoutError,
|
||||
)
|
||||
from pymongo.helpers_shared import _RETRYABLE_ERROR_CODES
|
||||
@ -563,9 +564,17 @@ class _AsyncClientBulk:
|
||||
error, ConnectionFailure
|
||||
) and not isinstance(error, (NotPrimaryError, WaitQueueTimeoutError))
|
||||
|
||||
retryable_label_error = isinstance(
|
||||
error, PyMongoError
|
||||
) and error.has_error_label("RetryableError")
|
||||
|
||||
# Synthesize the full bulk result without modifying the
|
||||
# current one because this write operation may be retried.
|
||||
if retryable and (retryable_top_level_error or retryable_network_error):
|
||||
if retryable and (
|
||||
retryable_top_level_error
|
||||
or retryable_network_error
|
||||
or retryable_label_error
|
||||
):
|
||||
full = copy.deepcopy(full_result)
|
||||
_merge_command(self.ops, self.idx_offset, full, result)
|
||||
_throw_client_bulk_write_exception(full, self.verbose_results)
|
||||
|
||||
@ -135,10 +135,13 @@ Classes
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import random
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import Mapping as _Mapping
|
||||
from contextvars import ContextVar, Token
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@ -161,7 +164,9 @@ from pymongo.asynchronous.cursor_base import _ConnectionManager
|
||||
from pymongo.errors import (
|
||||
ConfigurationError,
|
||||
ConnectionFailure,
|
||||
ExecutionTimeout,
|
||||
InvalidOperation,
|
||||
NetworkTimeout,
|
||||
OperationFailure,
|
||||
PyMongoError,
|
||||
WTimeoutError,
|
||||
@ -181,6 +186,28 @@ if TYPE_CHECKING:
|
||||
|
||||
_IS_SYNC = False
|
||||
|
||||
_SESSION: ContextVar[Optional[AsyncClientSession]] = ContextVar("SESSION", default=None)
|
||||
|
||||
|
||||
class _AsyncBoundSessionContext:
|
||||
"""Context manager returned by AsyncClientSession.bind() that manages bound state."""
|
||||
|
||||
def __init__(self, session: AsyncClientSession, end_session: bool) -> None:
|
||||
self._session = session
|
||||
self._session_token: Optional[Token[AsyncClientSession]] = None
|
||||
self._end_session = end_session
|
||||
|
||||
async def __aenter__(self) -> AsyncClientSession:
|
||||
self._session_token = _SESSION.set(self._session) # type: ignore[assignment]
|
||||
return self._session
|
||||
|
||||
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
if self._session_token:
|
||||
_SESSION.reset(self._session_token) # type: ignore[arg-type]
|
||||
self._session_token = None
|
||||
if self._end_session:
|
||||
await self._session.end_session()
|
||||
|
||||
|
||||
class SessionOptions:
|
||||
"""Options for a new :class:`AsyncClientSession`.
|
||||
@ -404,6 +431,7 @@ class _Transaction:
|
||||
self.recovery_token = None
|
||||
self.attempt = 0
|
||||
self.client = client
|
||||
self.has_completed_command = False
|
||||
|
||||
def active(self) -> bool:
|
||||
return self.state in (_TxnState.STARTING, _TxnState.IN_PROGRESS)
|
||||
@ -411,6 +439,9 @@ class _Transaction:
|
||||
def starting(self) -> bool:
|
||||
return self.state == _TxnState.STARTING
|
||||
|
||||
def set_starting(self) -> None:
|
||||
self.state = _TxnState.STARTING
|
||||
|
||||
@property
|
||||
def pinned_conn(self) -> Optional[AsyncConnection]:
|
||||
if self.active() and self.conn_mgr:
|
||||
@ -436,6 +467,7 @@ class _Transaction:
|
||||
self.sharded = False
|
||||
self.recovery_token = None
|
||||
self.attempt = 0
|
||||
self.has_completed_command = False
|
||||
|
||||
def __del__(self) -> None:
|
||||
if self.conn_mgr:
|
||||
@ -470,11 +502,29 @@ _UNKNOWN_COMMIT_ERROR_CODES: frozenset = _RETRYABLE_ERROR_CODES | frozenset( #
|
||||
# This limit is non-configurable and was chosen to be twice the 60 second
|
||||
# default value of MongoDB's `transactionLifetimeLimitSeconds` parameter.
|
||||
_WITH_TRANSACTION_RETRY_TIME_LIMIT = 120
|
||||
_BACKOFF_MAX = 0.500 # 500ms max backoff
|
||||
_BACKOFF_INITIAL = 0.005 # 5ms initial backoff
|
||||
|
||||
|
||||
def _within_time_limit(start_time: float) -> bool:
|
||||
def _within_time_limit(start_time: float, backoff: float = 0) -> bool:
|
||||
"""Are we within the with_transaction retry limit?"""
|
||||
return time.monotonic() - start_time < _WITH_TRANSACTION_RETRY_TIME_LIMIT
|
||||
remaining = _csot.remaining()
|
||||
if remaining is not None and remaining <= 0:
|
||||
return False
|
||||
return time.monotonic() + backoff - start_time < _WITH_TRANSACTION_RETRY_TIME_LIMIT
|
||||
|
||||
|
||||
def _make_timeout_error(error: BaseException) -> PyMongoError:
|
||||
"""Convert error to a NetworkTimeout or ExecutionTimeout as appropriate."""
|
||||
if _csot.remaining() is not None:
|
||||
timeout_error: PyMongoError = ExecutionTimeout(
|
||||
str(error), 50, {"ok": 0, "errmsg": str(error), "code": 50}
|
||||
)
|
||||
else:
|
||||
timeout_error = NetworkTimeout(str(error))
|
||||
if isinstance(error, PyMongoError):
|
||||
timeout_error._error_labels = error._error_labels.copy()
|
||||
return timeout_error
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
@ -547,6 +597,24 @@ class AsyncClientSession:
|
||||
if self._server_session is None:
|
||||
raise InvalidOperation("Cannot use ended session")
|
||||
|
||||
def bind(self, end_session: bool = True) -> _AsyncBoundSessionContext:
|
||||
"""Bind this session so it is implicitly passed to all database operations within the returned context.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async with client.start_session() as s:
|
||||
async with s.bind():
|
||||
# session=s is passed implicitly
|
||||
await client.db.collection.insert_one({"x": 1})
|
||||
|
||||
:param end_session: Whether to end the session on exiting the returned context. Defaults to True.
|
||||
If set to False, :meth:`~pymongo.asynchronous.client_session.AsyncClientSession.end_session()` must be called
|
||||
once the session is no longer used.
|
||||
|
||||
.. versionadded:: 4.17
|
||||
"""
|
||||
return _AsyncBoundSessionContext(self, end_session)
|
||||
|
||||
async def __aenter__(self) -> AsyncClientSession:
|
||||
return self
|
||||
|
||||
@ -703,7 +771,17 @@ class AsyncClientSession:
|
||||
https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/transactions-convenient-api.md#handling-errors-inside-the-callback
|
||||
"""
|
||||
start_time = time.monotonic()
|
||||
retry = 0
|
||||
last_error: Optional[BaseException] = None
|
||||
while True:
|
||||
if retry: # Implement exponential backoff on retry.
|
||||
jitter = random.random() # noqa: S311
|
||||
backoff = jitter * min(_BACKOFF_INITIAL * (1.5**retry), _BACKOFF_MAX)
|
||||
if not _within_time_limit(start_time, backoff):
|
||||
assert last_error is not None
|
||||
raise _make_timeout_error(last_error) from last_error
|
||||
await asyncio.sleep(backoff)
|
||||
retry += 1
|
||||
await self.start_transaction(
|
||||
read_concern, write_concern, read_preference, max_commit_time_ms
|
||||
)
|
||||
@ -711,15 +789,16 @@ class AsyncClientSession:
|
||||
ret = await callback(self)
|
||||
# Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
|
||||
except BaseException as exc:
|
||||
last_error = exc
|
||||
if self.in_transaction:
|
||||
await self.abort_transaction()
|
||||
if (
|
||||
isinstance(exc, PyMongoError)
|
||||
and exc.has_error_label("TransientTransactionError")
|
||||
and _within_time_limit(start_time)
|
||||
if isinstance(exc, PyMongoError) and exc.has_error_label(
|
||||
"TransientTransactionError"
|
||||
):
|
||||
# Retry the entire transaction.
|
||||
continue
|
||||
if _within_time_limit(start_time):
|
||||
# Retry the entire transaction.
|
||||
continue
|
||||
raise _make_timeout_error(last_error) from exc
|
||||
raise
|
||||
|
||||
if not self.in_transaction:
|
||||
@ -730,17 +809,18 @@ class AsyncClientSession:
|
||||
try:
|
||||
await self.commit_transaction()
|
||||
except PyMongoError as exc:
|
||||
if (
|
||||
exc.has_error_label("UnknownTransactionCommitResult")
|
||||
and _within_time_limit(start_time)
|
||||
and not _max_time_expired_error(exc)
|
||||
):
|
||||
last_error = exc
|
||||
if exc.has_error_label(
|
||||
"UnknownTransactionCommitResult"
|
||||
) and not _max_time_expired_error(exc):
|
||||
if not _within_time_limit(start_time):
|
||||
raise _make_timeout_error(last_error) from exc
|
||||
# Retry the commit.
|
||||
continue
|
||||
|
||||
if exc.has_error_label("TransientTransactionError") and _within_time_limit(
|
||||
start_time
|
||||
):
|
||||
if exc.has_error_label("TransientTransactionError"):
|
||||
if not _within_time_limit(start_time):
|
||||
raise _make_timeout_error(last_error) from exc
|
||||
# Retry the entire transaction.
|
||||
break
|
||||
raise
|
||||
@ -1021,7 +1101,11 @@ class AsyncClientSession:
|
||||
read_preference: _ServerMode,
|
||||
conn: AsyncConnection,
|
||||
) -> None:
|
||||
if not conn.supports_sessions:
|
||||
# getMores must be sent with a session if the cursor was opened with one
|
||||
operation = next(iter(command))
|
||||
if not conn.supports_sessions and (
|
||||
isinstance(self._server_session, _EmptyServerSession) or operation != "getMore"
|
||||
):
|
||||
if not self._implicit:
|
||||
raise ConfigurationError("Sessions are not supported by this MongoDB deployment")
|
||||
return
|
||||
|
||||
@ -20,7 +20,6 @@ from collections import abc
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AsyncContextManager,
|
||||
Callable,
|
||||
Coroutine,
|
||||
Generic,
|
||||
@ -571,11 +570,6 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
await change_stream._initialize_cursor()
|
||||
return change_stream
|
||||
|
||||
async def _conn_for_writes(
|
||||
self, session: Optional[AsyncClientSession], operation: str
|
||||
) -> AsyncContextManager[AsyncConnection]:
|
||||
return await self._database.client._conn_for_writes(session, operation)
|
||||
|
||||
async def _command(
|
||||
self,
|
||||
conn: AsyncConnection,
|
||||
@ -652,7 +646,10 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
if "size" in options:
|
||||
options["size"] = float(options["size"])
|
||||
cmd.update(options)
|
||||
async with await self._conn_for_writes(session, operation=_Op.CREATE) as conn:
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession], conn: AsyncConnection, _retryable_write: bool
|
||||
) -> None:
|
||||
if qev2_required and conn.max_wire_version < 21:
|
||||
raise ConfigurationError(
|
||||
"Driver support of Queryable Encryption is incompatible with server. "
|
||||
@ -669,6 +666,8 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
session=session,
|
||||
)
|
||||
|
||||
await self.database.client._retryable_write(False, inner, session, _Op.CREATE)
|
||||
|
||||
async def _create(
|
||||
self,
|
||||
options: MutableMapping[str, Any],
|
||||
@ -2240,7 +2239,10 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
command (like maxTimeMS) can be passed as keyword arguments.
|
||||
"""
|
||||
names = []
|
||||
async with await self._conn_for_writes(session, operation=_Op.CREATE_INDEXES) as conn:
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession], conn: AsyncConnection, _retryable_write: bool
|
||||
) -> list[str]:
|
||||
supports_quorum = conn.max_wire_version >= 9
|
||||
|
||||
def gen_indexes() -> Iterator[Mapping[str, Any]]:
|
||||
@ -2269,7 +2271,11 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
write_concern=self._write_concern_for(session),
|
||||
session=session,
|
||||
)
|
||||
return names
|
||||
return names
|
||||
|
||||
return await self.database.client._retryable_write(
|
||||
False, inner, session, _Op.CREATE_INDEXES
|
||||
)
|
||||
|
||||
async def create_index(
|
||||
self,
|
||||
@ -2422,7 +2428,6 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
kwargs["comment"] = comment
|
||||
await self._drop_index("*", session=session, **kwargs)
|
||||
|
||||
@_csot.apply
|
||||
async def drop_index(
|
||||
self,
|
||||
index_or_name: _IndexKeyHint,
|
||||
@ -2490,7 +2495,10 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
cmd.update(kwargs)
|
||||
if comment is not None:
|
||||
cmd["comment"] = comment
|
||||
async with await self._conn_for_writes(session, operation=_Op.DROP_INDEXES) as conn:
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession], conn: AsyncConnection, _retryable_write: bool
|
||||
) -> None:
|
||||
await self._command(
|
||||
conn,
|
||||
cmd,
|
||||
@ -2500,6 +2508,8 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
session=session,
|
||||
)
|
||||
|
||||
await self.database.client._retryable_write(False, inner, session, _Op.DROP_INDEXES)
|
||||
|
||||
async def list_indexes(
|
||||
self,
|
||||
session: Optional[AsyncClientSession] = None,
|
||||
@ -2763,17 +2773,22 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
cmd = {"createSearchIndexes": self.name, "indexes": list(gen_indexes())}
|
||||
cmd.update(kwargs)
|
||||
|
||||
async with await self._conn_for_writes(
|
||||
session, operation=_Op.CREATE_SEARCH_INDEXES
|
||||
) as conn:
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession], conn: AsyncConnection, _retryable_write: bool
|
||||
) -> list[str]:
|
||||
resp = await self._command(
|
||||
conn,
|
||||
cmd,
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
codec_options=_UNICODE_REPLACE_CODEC_OPTIONS,
|
||||
session=session,
|
||||
)
|
||||
return [index["name"] for index in resp["indexesCreated"]]
|
||||
|
||||
return await self.database.client._retryable_write(
|
||||
False, inner, session, _Op.CREATE_SEARCH_INDEXES
|
||||
)
|
||||
|
||||
async def drop_search_index(
|
||||
self,
|
||||
name: str,
|
||||
@ -2799,15 +2814,21 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
cmd.update(kwargs)
|
||||
if comment is not None:
|
||||
cmd["comment"] = comment
|
||||
async with await self._conn_for_writes(session, operation=_Op.DROP_SEARCH_INDEXES) as conn:
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession], conn: AsyncConnection, _retryable_write: bool
|
||||
) -> None:
|
||||
await self._command(
|
||||
conn,
|
||||
cmd,
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
allowable_errors=["ns not found", 26],
|
||||
codec_options=_UNICODE_REPLACE_CODEC_OPTIONS,
|
||||
session=session,
|
||||
)
|
||||
|
||||
await self.database.client._retryable_write(False, inner, session, _Op.DROP_SEARCH_INDEXES)
|
||||
|
||||
async def update_search_index(
|
||||
self,
|
||||
name: str,
|
||||
@ -2835,15 +2856,21 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
cmd.update(kwargs)
|
||||
if comment is not None:
|
||||
cmd["comment"] = comment
|
||||
async with await self._conn_for_writes(session, operation=_Op.UPDATE_SEARCH_INDEX) as conn:
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession], conn: AsyncConnection, _retryable_write: bool
|
||||
) -> None:
|
||||
await self._command(
|
||||
conn,
|
||||
cmd,
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
allowable_errors=["ns not found", 26],
|
||||
codec_options=_UNICODE_REPLACE_CODEC_OPTIONS,
|
||||
session=session,
|
||||
)
|
||||
|
||||
await self.database.client._retryable_write(False, inner, session, _Op.UPDATE_SEARCH_INDEX)
|
||||
|
||||
async def options(
|
||||
self,
|
||||
session: Optional[AsyncClientSession] = None,
|
||||
@ -2918,6 +2945,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
session,
|
||||
retryable=not cmd._performs_write,
|
||||
operation=_Op.AGGREGATE,
|
||||
is_aggregate_write=cmd._performs_write,
|
||||
)
|
||||
|
||||
async def aggregate(
|
||||
@ -3123,17 +3151,21 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
|
||||
if comment is not None:
|
||||
cmd["comment"] = comment
|
||||
write_concern = self._write_concern_for_cmd(cmd, session)
|
||||
client = self._database.client
|
||||
|
||||
async with await self._conn_for_writes(session, operation=_Op.RENAME) as conn:
|
||||
async with self._database.client._tmp_session(session) as s:
|
||||
return await conn.command(
|
||||
"admin",
|
||||
cmd,
|
||||
write_concern=write_concern,
|
||||
parse_write_concern_error=True,
|
||||
session=s,
|
||||
client=self._database.client,
|
||||
)
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession], conn: AsyncConnection, _retryable_write: bool
|
||||
) -> MutableMapping[str, Any]:
|
||||
return await conn.command(
|
||||
"admin",
|
||||
cmd,
|
||||
write_concern=write_concern,
|
||||
parse_write_concern_error=True,
|
||||
session=session,
|
||||
client=client,
|
||||
)
|
||||
|
||||
return await client._retryable_write(False, inner, session, _Op.RENAME)
|
||||
|
||||
async def distinct(
|
||||
self,
|
||||
|
||||
@ -931,14 +931,15 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
|
||||
if read_preference is None:
|
||||
read_preference = (session and session._txn_read_preference()) or ReadPreference.PRIMARY
|
||||
async with await self._client._conn_for_reads(
|
||||
read_preference, session, operation=command_name
|
||||
) as (
|
||||
connection,
|
||||
read_preference,
|
||||
):
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession],
|
||||
_server: Server,
|
||||
conn: AsyncConnection,
|
||||
read_preference: _ServerMode,
|
||||
) -> Union[dict[str, Any], _CodecDocumentType]:
|
||||
return await self._command(
|
||||
connection,
|
||||
conn,
|
||||
command,
|
||||
value,
|
||||
check,
|
||||
@ -949,6 +950,10 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return await self._client._retryable_read(
|
||||
inner, read_preference, session, command_name, None, False, is_run_command=True
|
||||
)
|
||||
|
||||
@_csot.apply
|
||||
async def cursor_command(
|
||||
self,
|
||||
@ -1016,17 +1021,17 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
|
||||
async with self._client._tmp_session(session) as tmp_session:
|
||||
opts = codec_options or DEFAULT_CODEC_OPTIONS
|
||||
|
||||
if read_preference is None:
|
||||
read_preference = (
|
||||
tmp_session and tmp_session._txn_read_preference()
|
||||
) or ReadPreference.PRIMARY
|
||||
async with await self._client._conn_for_reads(
|
||||
read_preference, tmp_session, command_name
|
||||
) as (
|
||||
conn,
|
||||
read_preference,
|
||||
):
|
||||
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession],
|
||||
_server: Server,
|
||||
conn: AsyncConnection,
|
||||
read_preference: _ServerMode,
|
||||
) -> AsyncCommandCursor[_DocumentType]:
|
||||
response = await self._command(
|
||||
conn,
|
||||
command,
|
||||
@ -1035,7 +1040,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
None,
|
||||
read_preference,
|
||||
opts,
|
||||
session=tmp_session,
|
||||
session=session,
|
||||
**kwargs,
|
||||
)
|
||||
coll = self.get_collection("$cmd", read_preference=read_preference)
|
||||
@ -1045,7 +1050,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
response["cursor"],
|
||||
conn.address,
|
||||
max_await_time_ms=max_await_time_ms,
|
||||
session=tmp_session,
|
||||
session=session,
|
||||
comment=comment,
|
||||
)
|
||||
await cmd_cursor._maybe_pin_connection(conn)
|
||||
@ -1053,6 +1058,10 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
else:
|
||||
raise InvalidOperation("Command does not return a cursor.")
|
||||
|
||||
return await self.client._retryable_read(
|
||||
inner, read_preference, tmp_session, command_name, None, False
|
||||
)
|
||||
|
||||
async def _retryable_read_command(
|
||||
self,
|
||||
command: Union[str, MutableMapping[str, Any]],
|
||||
@ -1254,9 +1263,11 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
if comment is not None:
|
||||
command["comment"] = comment
|
||||
|
||||
async with await self._client._conn_for_writes(session, operation=_Op.DROP) as connection:
|
||||
async def inner(
|
||||
session: Optional[AsyncClientSession], conn: AsyncConnection, _retryable_write: bool
|
||||
) -> dict[str, Any]:
|
||||
return await self._command(
|
||||
connection,
|
||||
conn,
|
||||
command,
|
||||
allowable_errors=["ns not found", 26],
|
||||
write_concern=self._write_concern_for(session),
|
||||
@ -1264,6 +1275,8 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
|
||||
session=session,
|
||||
)
|
||||
|
||||
return await self.client._retryable_write(False, inner, session, _Op.DROP)
|
||||
|
||||
@_csot.apply
|
||||
async def drop_collection(
|
||||
self,
|
||||
|
||||
@ -17,8 +17,11 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import builtins
|
||||
import functools
|
||||
import random
|
||||
import socket
|
||||
import sys
|
||||
import time as time # noqa: PLC0414 # needed in sync version
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
@ -26,6 +29,8 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
from pymongo import _csot
|
||||
from pymongo.common import MAX_ADAPTIVE_RETRIES
|
||||
from pymongo.errors import (
|
||||
OperationFailure,
|
||||
)
|
||||
@ -38,6 +43,7 @@ F = TypeVar("F", bound=Callable[..., Any])
|
||||
|
||||
|
||||
def _handle_reauth(func: F) -> F:
|
||||
@functools.wraps(func)
|
||||
async def inner(*args: Any, **kwargs: Any) -> Any:
|
||||
no_reauth = kwargs.pop("no_reauth", False)
|
||||
from pymongo.asynchronous.pool import AsyncConnection
|
||||
@ -70,6 +76,46 @@ def _handle_reauth(func: F) -> F:
|
||||
return cast(F, inner)
|
||||
|
||||
|
||||
_BACKOFF_INITIAL = 0.1
|
||||
_BACKOFF_MAX = 10
|
||||
|
||||
|
||||
def _backoff(
|
||||
attempt: int, initial_delay: float = _BACKOFF_INITIAL, max_delay: float = _BACKOFF_MAX
|
||||
) -> float:
|
||||
jitter = random.random() # noqa: S311
|
||||
return jitter * min(initial_delay * (2**attempt), max_delay)
|
||||
|
||||
|
||||
class _RetryPolicy:
|
||||
"""A retry limiter that performs exponential backoff with jitter."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attempts: int = MAX_ADAPTIVE_RETRIES,
|
||||
backoff_initial: float = _BACKOFF_INITIAL,
|
||||
backoff_max: float = _BACKOFF_MAX,
|
||||
):
|
||||
self.attempts = attempts
|
||||
self.backoff_initial = backoff_initial
|
||||
self.backoff_max = backoff_max
|
||||
|
||||
def backoff(self, attempt: int) -> float:
|
||||
"""Return the backoff duration for the given attempt."""
|
||||
return _backoff(max(0, attempt - 1), self.backoff_initial, self.backoff_max)
|
||||
|
||||
async def should_retry(self, attempt: int, delay: float) -> bool:
|
||||
"""Return if we have retry attempts remaining and the next backoff would not exceed a timeout."""
|
||||
if attempt > self.attempts:
|
||||
return False
|
||||
|
||||
if _csot.get_timeout():
|
||||
if time.monotonic() + delay > _csot.get_deadline():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _getaddrinfo(
|
||||
host: Any, port: Any, **kwargs: Any
|
||||
) -> list[
|
||||
|
||||
@ -35,6 +35,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import contextlib
|
||||
import os
|
||||
import time as time # noqa: PLC0414 # needed in sync version
|
||||
import warnings
|
||||
import weakref
|
||||
from collections import defaultdict
|
||||
@ -65,8 +66,11 @@ from pymongo import _csot, common, helpers_shared, periodic_executor
|
||||
from pymongo.asynchronous import client_session, database, uri_parser
|
||||
from pymongo.asynchronous.change_stream import AsyncChangeStream, AsyncClusterChangeStream
|
||||
from pymongo.asynchronous.client_bulk import _AsyncClientBulk
|
||||
from pymongo.asynchronous.client_session import _EmptyServerSession
|
||||
from pymongo.asynchronous.client_session import _SESSION, _EmptyServerSession
|
||||
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
|
||||
from pymongo.asynchronous.helpers import (
|
||||
_RetryPolicy,
|
||||
)
|
||||
from pymongo.asynchronous.settings import TopologySettings
|
||||
from pymongo.asynchronous.topology import Topology, _ErrorContext
|
||||
from pymongo.client_options import ClientOptions
|
||||
@ -610,8 +614,18 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
client to use Stable API. See `versioned API <https://www.mongodb.com/docs/manual/reference/stable-api/#what-is-the-stable-api--and-should-you-use-it->`_ for
|
||||
details.
|
||||
|
||||
| **Overload retry options:**
|
||||
|
||||
- `max_adaptive_retries`: (int) How many retries to allow for overload errors. Defaults to ``2``.
|
||||
- `enable_overload_retargeting`: (boolean) Whether overload retargeting is enabled for this client.
|
||||
If enabled, server overload errors will cause retry attempts to select a server that has not yet returned an overload error, if possible.
|
||||
Defaults to ``False``.
|
||||
|
||||
.. seealso:: The MongoDB documentation on `connections <https://dochub.mongodb.org/core/connections>`_.
|
||||
|
||||
.. versionchanged:: 4.17
|
||||
Added the ``max_adaptive_retries`` and ``enable_overload_retargeting`` URI and keyword arguments.
|
||||
|
||||
.. versionchanged:: 4.5
|
||||
Added the ``serverMonitoringMode`` keyword argument.
|
||||
|
||||
@ -879,11 +893,14 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
self._options.read_concern,
|
||||
)
|
||||
|
||||
self._retry_policy = _RetryPolicy(attempts=self._options.max_adaptive_retries)
|
||||
|
||||
self._init_based_on_options(self._seeds, srv_max_hosts, srv_service_name)
|
||||
|
||||
self._opened = False
|
||||
self._closed = False
|
||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
|
||||
if not is_srv:
|
||||
self._init_background()
|
||||
|
||||
@ -1408,7 +1425,8 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
def _ensure_session(
|
||||
self, session: Optional[AsyncClientSession] = None
|
||||
) -> Optional[AsyncClientSession]:
|
||||
"""If provided session is None, lend a temporary session."""
|
||||
"""If provided session and bound session are None, lend a temporary session."""
|
||||
session = session or self._get_bound_session()
|
||||
if session:
|
||||
return session
|
||||
|
||||
@ -1990,6 +2008,8 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
read_pref: Optional[_ServerMode] = None,
|
||||
retryable: bool = False,
|
||||
operation_id: Optional[int] = None,
|
||||
is_run_command: bool = False,
|
||||
is_aggregate_write: bool = False,
|
||||
) -> T:
|
||||
"""Internal retryable helper for all client transactions.
|
||||
|
||||
@ -2001,6 +2021,8 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
:param address: Server Address, defaults to None
|
||||
:param read_pref: Topology of read operation, defaults to None
|
||||
:param retryable: If the operation should be retried once, defaults to None
|
||||
:param is_run_command: If this is a runCommand operation, defaults to False
|
||||
:param is_aggregate_write: If this is a aggregate operation with a write, defaults to False.
|
||||
|
||||
:return: Output of the calling func()
|
||||
"""
|
||||
@ -2015,6 +2037,8 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
address=address,
|
||||
retryable=retryable,
|
||||
operation_id=operation_id,
|
||||
is_run_command=is_run_command,
|
||||
is_aggregate_write=is_aggregate_write,
|
||||
).run()
|
||||
|
||||
async def _retryable_read(
|
||||
@ -2026,6 +2050,8 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
address: Optional[_Address] = None,
|
||||
retryable: bool = True,
|
||||
operation_id: Optional[int] = None,
|
||||
is_run_command: bool = False,
|
||||
is_aggregate_write: bool = False,
|
||||
) -> T:
|
||||
"""Execute an operation with consecutive retries if possible
|
||||
|
||||
@ -2041,6 +2067,8 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
:param address: Optional address when sending a message, defaults to None
|
||||
:param retryable: if we should attempt retries
|
||||
(may not always be supported even if supplied), defaults to False
|
||||
:param is_run_command: If this is a runCommand operation, defaults to False.
|
||||
:param is_aggregate_write: If this is a aggregate operation with a write, defaults to False.
|
||||
"""
|
||||
|
||||
# Ensure that the client supports retrying on reads and there is no session in
|
||||
@ -2059,6 +2087,8 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
read_pref=read_pref,
|
||||
retryable=retryable,
|
||||
operation_id=operation_id,
|
||||
is_run_command=is_run_command,
|
||||
is_aggregate_write=is_aggregate_write,
|
||||
)
|
||||
|
||||
async def _retryable_write(
|
||||
@ -2267,11 +2297,14 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
self, session: Optional[client_session.AsyncClientSession]
|
||||
) -> AsyncGenerator[Optional[client_session.AsyncClientSession], None]:
|
||||
"""If provided session is None, lend a temporary session."""
|
||||
if session is not None:
|
||||
if not isinstance(session, client_session.AsyncClientSession):
|
||||
raise ValueError(
|
||||
f"'session' argument must be an AsyncClientSession or None, not {type(session)}"
|
||||
)
|
||||
if session is not None and not isinstance(session, client_session.AsyncClientSession):
|
||||
raise ValueError(
|
||||
f"'session' argument must be an AsyncClientSession or None, not {type(session)}"
|
||||
)
|
||||
|
||||
# Check for a bound session. If one exists, treat it as an explicitly passed session.
|
||||
session = session or self._get_bound_session()
|
||||
if session:
|
||||
# Don't call end_session.
|
||||
yield session
|
||||
return
|
||||
@ -2301,6 +2334,18 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
if session is not None:
|
||||
session._process_response(reply)
|
||||
|
||||
def _get_bound_session(self) -> Optional[AsyncClientSession]:
|
||||
bound_session = _SESSION.get()
|
||||
if bound_session:
|
||||
if bound_session.client is self:
|
||||
return bound_session
|
||||
else:
|
||||
raise InvalidOperation(
|
||||
"Only the client that created the bound session can perform operations within its context block. See <PLACEHOLDER> for more information."
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
async def server_info(
|
||||
self, session: Optional[client_session.AsyncClientSession] = None
|
||||
) -> dict[str, Any]:
|
||||
@ -2438,15 +2483,13 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
f"name_or_database must be an instance of str or a AsyncDatabase, not {type(name)}"
|
||||
)
|
||||
|
||||
async with await self._conn_for_writes(session, operation=_Op.DROP_DATABASE) as conn:
|
||||
await self[name]._command(
|
||||
conn,
|
||||
{"dropDatabase": 1, "comment": comment},
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
write_concern=self._write_concern_for(session),
|
||||
parse_write_concern_error=True,
|
||||
session=session,
|
||||
)
|
||||
await self[name].command(
|
||||
{"dropDatabase": 1, "comment": comment},
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
write_concern=self._write_concern_for(session),
|
||||
parse_write_concern_error=True,
|
||||
session=session,
|
||||
)
|
||||
|
||||
@_csot.apply
|
||||
async def bulk_write(
|
||||
@ -2730,12 +2773,15 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
address: Optional[_Address] = None,
|
||||
retryable: bool = False,
|
||||
operation_id: Optional[int] = None,
|
||||
is_run_command: bool = False,
|
||||
is_aggregate_write: bool = False,
|
||||
):
|
||||
self._last_error: Optional[Exception] = None
|
||||
self._retrying = False
|
||||
self._multiple_retries = _csot.get_timeout() is not None
|
||||
self._always_retryable = False
|
||||
self._max_retries = float("inf") if _csot.get_timeout() is not None else 1
|
||||
self._client = mongo_client
|
||||
|
||||
self._retry_policy = mongo_client._retry_policy
|
||||
self._func = func
|
||||
self._bulk = bulk
|
||||
self._session = session
|
||||
@ -2751,6 +2797,8 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
self._operation = operation
|
||||
self._operation_id = operation_id
|
||||
self._attempt_number = 0
|
||||
self._is_run_command = is_run_command
|
||||
self._is_aggregate_write = is_aggregate_write
|
||||
|
||||
async def run(self) -> T:
|
||||
"""Runs the supplied func() and attempts a retry
|
||||
@ -2770,7 +2818,13 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
while True:
|
||||
self._check_last_error(check_csot=True)
|
||||
try:
|
||||
return await self._read() if self._is_read else await self._write()
|
||||
res = await self._read() if self._is_read else await self._write()
|
||||
# Track whether the transaction has completed a command.
|
||||
# If we need to apply backpressure to the first command,
|
||||
# we will need to revert back to starting state.
|
||||
if self._session is not None and self._session.in_transaction:
|
||||
self._session._transaction.has_completed_command = True
|
||||
return res
|
||||
except ServerSelectionTimeoutError:
|
||||
# The application may think the write was never attempted
|
||||
# if we raise ServerSelectionTimeoutError on the retry
|
||||
@ -2781,37 +2835,80 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
# most likely be a waste of time.
|
||||
raise
|
||||
except PyMongoError as exc:
|
||||
always_retryable = False
|
||||
overloaded = False
|
||||
exc_to_check = exc
|
||||
|
||||
if self._is_run_command and not (
|
||||
self._client.options.retry_reads and self._client.options.retry_writes
|
||||
):
|
||||
raise
|
||||
if self._is_aggregate_write and not self._client.options.retry_writes:
|
||||
raise
|
||||
|
||||
# Execute specialized catch on read
|
||||
if self._is_read:
|
||||
if isinstance(exc, (ConnectionFailure, OperationFailure)):
|
||||
# ConnectionFailures do not supply a code property
|
||||
exc_code = getattr(exc, "code", None)
|
||||
if self._is_not_eligible_for_retry() or (
|
||||
isinstance(exc, OperationFailure)
|
||||
and exc_code not in helpers_shared._RETRYABLE_ERROR_CODES
|
||||
overloaded = exc.has_error_label("SystemOverloadedError")
|
||||
if overloaded:
|
||||
self._max_retries = self._client.options.max_adaptive_retries
|
||||
always_retryable = exc.has_error_label("RetryableError") and overloaded
|
||||
if not self._client.options.retry_reads or (
|
||||
not always_retryable
|
||||
and (
|
||||
self._is_not_eligible_for_retry()
|
||||
or (
|
||||
isinstance(exc, OperationFailure)
|
||||
and exc_code not in helpers_shared._RETRYABLE_ERROR_CODES
|
||||
)
|
||||
)
|
||||
):
|
||||
raise
|
||||
self._retrying = True
|
||||
self._last_error = exc
|
||||
self._attempt_number += 1
|
||||
|
||||
# Revert back to starting state if we're in a transaction but haven't completed the first
|
||||
# command.
|
||||
if (
|
||||
overloaded
|
||||
and self._session is not None
|
||||
and self._session.in_transaction
|
||||
):
|
||||
transaction = self._session._transaction
|
||||
if not transaction.has_completed_command:
|
||||
transaction.set_starting()
|
||||
transaction.attempt = 0
|
||||
else:
|
||||
raise
|
||||
|
||||
# Specialized catch on write operation
|
||||
if not self._is_read:
|
||||
if not self._retryable:
|
||||
if isinstance(exc, ClientBulkWriteException) and isinstance(
|
||||
exc.error, PyMongoError
|
||||
):
|
||||
exc_to_check = exc.error
|
||||
retryable_write_label = exc_to_check.has_error_label("RetryableWriteError")
|
||||
overloaded = exc_to_check.has_error_label("SystemOverloadedError")
|
||||
if overloaded:
|
||||
self._max_retries = self._client.options.max_adaptive_retries
|
||||
always_retryable = exc_to_check.has_error_label("RetryableError") and overloaded
|
||||
|
||||
# Always retry abortTransaction and commitTransaction up to once
|
||||
if self._operation not in ["abortTransaction", "commitTransaction"] and (
|
||||
not self._client.options.retry_writes
|
||||
or not (self._retryable or always_retryable)
|
||||
):
|
||||
raise
|
||||
if isinstance(exc, ClientBulkWriteException) and exc.error:
|
||||
retryable_write_error_exc = isinstance(
|
||||
exc.error, PyMongoError
|
||||
) and exc.error.has_error_label("RetryableWriteError")
|
||||
else:
|
||||
retryable_write_error_exc = exc.has_error_label("RetryableWriteError")
|
||||
if retryable_write_error_exc:
|
||||
if retryable_write_label or always_retryable:
|
||||
assert self._session
|
||||
await self._session._unpin()
|
||||
if not retryable_write_error_exc or self._is_not_eligible_for_retry():
|
||||
if exc.has_error_label("NoWritesPerformed") and self._last_error:
|
||||
if not always_retryable and (
|
||||
not retryable_write_label or self._is_not_eligible_for_retry()
|
||||
):
|
||||
if exc_to_check.has_error_label("NoWritesPerformed") and self._last_error:
|
||||
raise self._last_error from exc
|
||||
else:
|
||||
raise
|
||||
@ -2820,17 +2917,39 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
self._bulk.retrying = True
|
||||
else:
|
||||
self._retrying = True
|
||||
if not exc.has_error_label("NoWritesPerformed"):
|
||||
if not exc_to_check.has_error_label("NoWritesPerformed"):
|
||||
self._last_error = exc
|
||||
if self._last_error is None:
|
||||
self._last_error = exc
|
||||
# Revert back to starting state if we're in a transaction but haven't completed the first
|
||||
# command.
|
||||
if overloaded and self._session is not None and self._session.in_transaction:
|
||||
transaction = self._session._transaction
|
||||
if not transaction.has_completed_command:
|
||||
transaction.set_starting()
|
||||
transaction.attempt = 0
|
||||
|
||||
if self._server is not None:
|
||||
if self._server is not None and (
|
||||
self._client.topology_description.topology_type_name == "Sharded"
|
||||
or (overloaded and self._client.options.enable_overload_retargeting)
|
||||
):
|
||||
self._deprioritized_servers.append(self._server)
|
||||
|
||||
self._always_retryable = always_retryable
|
||||
if overloaded:
|
||||
delay = self._retry_policy.backoff(self._attempt_number)
|
||||
if not await self._retry_policy.should_retry(self._attempt_number, delay):
|
||||
if exc_to_check.has_error_label("NoWritesPerformed") and self._last_error:
|
||||
raise self._last_error from exc
|
||||
else:
|
||||
raise
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
def _is_not_eligible_for_retry(self) -> bool:
|
||||
"""Checks if the exchange is not eligible for retry"""
|
||||
return not self._retryable or (self._is_retrying() and not self._multiple_retries)
|
||||
return not self._retryable or (
|
||||
self._is_retrying() and self._attempt_number >= self._max_retries
|
||||
)
|
||||
|
||||
def _is_retrying(self) -> bool:
|
||||
"""Checks if the exchange is currently undergoing a retry"""
|
||||
@ -2889,7 +3008,7 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
and conn.supports_sessions
|
||||
)
|
||||
is_mongos = conn.is_mongos
|
||||
if not sessions_supported:
|
||||
if not self._always_retryable and not sessions_supported:
|
||||
# A retry is not possible because this server does
|
||||
# not support sessions raise the last error.
|
||||
self._check_last_error()
|
||||
@ -2921,7 +3040,7 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
conn,
|
||||
read_pref,
|
||||
):
|
||||
if self._retrying and not self._retryable:
|
||||
if self._retrying and not self._retryable and not self._always_retryable:
|
||||
self._check_last_error()
|
||||
if self._retrying:
|
||||
_debug_log(
|
||||
|
||||
@ -19,6 +19,8 @@ import collections
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
import time
|
||||
import weakref
|
||||
@ -52,10 +54,12 @@ from pymongo.errors import ( # type:ignore[attr-defined]
|
||||
DocumentTooLarge,
|
||||
ExecutionTimeout,
|
||||
InvalidOperation,
|
||||
NetworkTimeout,
|
||||
NotPrimaryError,
|
||||
OperationFailure,
|
||||
PyMongoError,
|
||||
WaitQueueTimeoutError,
|
||||
_CertificateError,
|
||||
)
|
||||
from pymongo.hello import Hello, HelloCompat
|
||||
from pymongo.helpers_shared import _get_timeout_details, format_timeout_details
|
||||
@ -250,6 +254,7 @@ class AsyncConnection:
|
||||
cmd = self.hello_cmd()
|
||||
performing_handshake = not self.performed_handshake
|
||||
awaitable = False
|
||||
cmd["backpressure"] = True
|
||||
if performing_handshake:
|
||||
self.performed_handshake = True
|
||||
cmd["client"] = self.opts.metadata
|
||||
@ -752,14 +757,10 @@ class Pool:
|
||||
# Enforces: maxConnecting
|
||||
# Also used for: clearing the wait queue
|
||||
self._max_connecting_cond = _async_create_condition(self.lock)
|
||||
self._max_connecting = self.opts.max_connecting
|
||||
self._pending = 0
|
||||
self._max_connecting = self.opts.max_connecting
|
||||
self._client_id = client_id
|
||||
if self.enabled_for_cmap:
|
||||
assert self.opts._event_listeners is not None
|
||||
self.opts._event_listeners.publish_pool_created(
|
||||
self.address, self.opts.non_default_options
|
||||
)
|
||||
# Log before publishing event to prevent potential listener preemption in tests
|
||||
if self.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
_CONNECTION_LOGGER,
|
||||
@ -769,6 +770,11 @@ class Pool:
|
||||
serverPort=self.address[1],
|
||||
**self.opts.non_default_options,
|
||||
)
|
||||
if self.enabled_for_cmap:
|
||||
assert self.opts._event_listeners is not None
|
||||
self.opts._event_listeners.publish_pool_created(
|
||||
self.address, self.opts.non_default_options
|
||||
)
|
||||
# Similar to active_sockets but includes threads in the wait queue.
|
||||
self.operation_count: int = 0
|
||||
# Retain references to pinned connections to prevent the CPython GC
|
||||
@ -783,9 +789,6 @@ class Pool:
|
||||
async with self.lock:
|
||||
if self.state != PoolState.READY:
|
||||
self.state = PoolState.READY
|
||||
if self.enabled_for_cmap:
|
||||
assert self.opts._event_listeners is not None
|
||||
self.opts._event_listeners.publish_pool_ready(self.address)
|
||||
if self.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
_CONNECTION_LOGGER,
|
||||
@ -794,6 +797,9 @@ class Pool:
|
||||
serverHost=self.address[0],
|
||||
serverPort=self.address[1],
|
||||
)
|
||||
if self.enabled_for_cmap:
|
||||
assert self.opts._event_listeners is not None
|
||||
self.opts._event_listeners.publish_pool_ready(self.address)
|
||||
|
||||
@property
|
||||
def closed(self) -> bool:
|
||||
@ -854,9 +860,6 @@ class Pool:
|
||||
else:
|
||||
for conn in sockets:
|
||||
await conn.close_conn(ConnectionClosedReason.POOL_CLOSED)
|
||||
if self.enabled_for_cmap:
|
||||
assert listeners is not None
|
||||
listeners.publish_pool_closed(self.address)
|
||||
if self.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
_CONNECTION_LOGGER,
|
||||
@ -865,15 +868,11 @@ class Pool:
|
||||
serverHost=self.address[0],
|
||||
serverPort=self.address[1],
|
||||
)
|
||||
if self.enabled_for_cmap:
|
||||
assert listeners is not None
|
||||
listeners.publish_pool_closed(self.address)
|
||||
else:
|
||||
if old_state != PoolState.PAUSED:
|
||||
if self.enabled_for_cmap:
|
||||
assert listeners is not None
|
||||
listeners.publish_pool_cleared(
|
||||
self.address,
|
||||
service_id=service_id,
|
||||
interrupt_connections=interrupt_connections,
|
||||
)
|
||||
if self.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
_CONNECTION_LOGGER,
|
||||
@ -883,6 +882,13 @@ class Pool:
|
||||
serverPort=self.address[1],
|
||||
serviceId=service_id,
|
||||
)
|
||||
if self.enabled_for_cmap:
|
||||
assert listeners is not None
|
||||
listeners.publish_pool_cleared(
|
||||
self.address,
|
||||
service_id=service_id,
|
||||
interrupt_connections=interrupt_connections,
|
||||
)
|
||||
if not _IS_SYNC:
|
||||
await asyncio.gather(
|
||||
*[conn.close_conn(ConnectionClosedReason.STALE) for conn in sockets], # type: ignore[func-returns-value]
|
||||
@ -986,6 +992,21 @@ class Pool:
|
||||
self.requests -= 1
|
||||
self.size_cond.notify()
|
||||
|
||||
def _handle_connection_error(self, error: BaseException) -> None:
|
||||
# Handle system overload condition for non-sdam pools.
|
||||
# Look for errors of type AutoReconnect and add error labels if appropriate.
|
||||
if self.is_sdam or type(error) not in (AutoReconnect, NetworkTimeout):
|
||||
return
|
||||
assert isinstance(error, AutoReconnect) # Appease type checker.
|
||||
# If the original error was a DNS, certificate, or SSL error, ignore it.
|
||||
if isinstance(error.__cause__, (_CertificateError, SSLErrors, socket.gaierror)):
|
||||
# End of file errors are excluded, because the server may have disconnected
|
||||
# during the handshake.
|
||||
if not isinstance(error.__cause__, (ssl.SSLEOFError, ssl.SSLZeroReturnError)):
|
||||
return
|
||||
error._add_error_label("SystemOverloadedError")
|
||||
error._add_error_label("RetryableError")
|
||||
|
||||
async def connect(self, handler: Optional[_MongoClientErrorHandler] = None) -> AsyncConnection:
|
||||
"""Connect to Mongo and return a new AsyncConnection.
|
||||
|
||||
@ -1037,10 +1058,10 @@ class Pool:
|
||||
reason=_verbose_connection_error_reason(ConnectionClosedReason.ERROR),
|
||||
error=ConnectionClosedReason.ERROR,
|
||||
)
|
||||
self._handle_connection_error(error)
|
||||
if isinstance(error, (IOError, OSError, *SSLErrors)):
|
||||
details = _get_timeout_details(self.opts)
|
||||
_raise_connection_failure(self.address, error, timeout_details=details)
|
||||
|
||||
raise
|
||||
|
||||
conn = AsyncConnection(networking_interface, self, self.address, conn_id, self.is_sdam) # type: ignore[arg-type]
|
||||
@ -1049,18 +1070,22 @@ class Pool:
|
||||
self.active_contexts.discard(tmp_context)
|
||||
if tmp_context.cancelled:
|
||||
conn.cancel_context.cancel()
|
||||
completed_hello = False
|
||||
try:
|
||||
if not self.is_sdam:
|
||||
await conn.hello()
|
||||
completed_hello = True
|
||||
self.is_writable = conn.is_writable
|
||||
if handler:
|
||||
handler.contribute_socket(conn, completed_handshake=False)
|
||||
|
||||
await conn.authenticate()
|
||||
# Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
|
||||
except BaseException:
|
||||
except BaseException as e:
|
||||
async with self.lock:
|
||||
self.active_contexts.discard(conn.cancel_context)
|
||||
if not completed_hello:
|
||||
self._handle_connection_error(e)
|
||||
await conn.close_conn(ConnectionClosedReason.ERROR)
|
||||
raise
|
||||
|
||||
@ -1389,8 +1414,8 @@ class Pool:
|
||||
:class:`~pymongo.errors.AutoReconnect` exceptions on server
|
||||
hiccups, etc. We only check if the socket was closed by an external
|
||||
error if it has been > 1 second since the socket was checked into the
|
||||
pool, to keep performance reasonable - we can't avoid AutoReconnects
|
||||
completely anyway.
|
||||
pool to keep performance reasonable -
|
||||
we can't avoid AutoReconnects completely anyway.
|
||||
"""
|
||||
idle_time_seconds = conn.idle_time_seconds()
|
||||
# If socket is idle, open a new one.
|
||||
@ -1401,8 +1426,9 @@ class Pool:
|
||||
await conn.close_conn(ConnectionClosedReason.IDLE)
|
||||
return True
|
||||
|
||||
if self._check_interval_seconds is not None and (
|
||||
self._check_interval_seconds == 0 or idle_time_seconds > self._check_interval_seconds
|
||||
check_interval_seconds = self._check_interval_seconds
|
||||
if check_interval_seconds is not None and (
|
||||
check_interval_seconds == 0 or idle_time_seconds > check_interval_seconds
|
||||
):
|
||||
if conn.conn_closed():
|
||||
await conn.close_conn(ConnectionClosedReason.ERROR)
|
||||
|
||||
@ -913,7 +913,9 @@ class Topology:
|
||||
# Clear the pool.
|
||||
await server.reset(service_id)
|
||||
elif isinstance(error, ConnectionFailure):
|
||||
if isinstance(error, WaitQueueTimeoutError):
|
||||
if isinstance(error, WaitQueueTimeoutError) or (
|
||||
error.has_error_label("SystemOverloadedError")
|
||||
):
|
||||
return
|
||||
# "Client MUST replace the server's description with type Unknown
|
||||
# ... MUST NOT request an immediate check of the server."
|
||||
|
||||
@ -235,6 +235,16 @@ class ClientOptions:
|
||||
self.__server_monitoring_mode = options.get(
|
||||
"servermonitoringmode", common.SERVER_MONITORING_MODE
|
||||
)
|
||||
self.__max_adaptive_retries = (
|
||||
options.get("max_adaptive_retries", common.MAX_ADAPTIVE_RETRIES)
|
||||
if "max_adaptive_retries" in options
|
||||
else options.get("maxadaptiveretries", common.MAX_ADAPTIVE_RETRIES)
|
||||
)
|
||||
self.__enable_overload_retargeting = (
|
||||
options.get("enable_overload_retargeting", common.ENABLE_OVERLOAD_RETARGETING)
|
||||
if "enable_overload_retargeting" in options
|
||||
else options.get("enableoverloadretargeting", common.ENABLE_OVERLOAD_RETARGETING)
|
||||
)
|
||||
|
||||
@property
|
||||
def _options(self) -> Mapping[str, Any]:
|
||||
@ -346,3 +356,19 @@ class ClientOptions:
|
||||
.. versionadded:: 4.5
|
||||
"""
|
||||
return self.__server_monitoring_mode
|
||||
|
||||
@property
|
||||
def max_adaptive_retries(self) -> int:
|
||||
"""The configured maxAdaptiveRetries option.
|
||||
|
||||
.. versionadded:: 4.17
|
||||
"""
|
||||
return self.__max_adaptive_retries
|
||||
|
||||
@property
|
||||
def enable_overload_retargeting(self) -> bool:
|
||||
"""The configured enableOverloadRetargeting option.
|
||||
|
||||
.. versionadded:: 4.17
|
||||
"""
|
||||
return self.__enable_overload_retargeting
|
||||
|
||||
@ -140,6 +140,12 @@ SRV_SERVICE_NAME = "mongodb"
|
||||
# Default value for serverMonitoringMode
|
||||
SERVER_MONITORING_MODE = "auto" # poll/stream/auto
|
||||
|
||||
# Default value for max adaptive retries
|
||||
MAX_ADAPTIVE_RETRIES = 2
|
||||
|
||||
# Default value for enableOverloadRetargeting
|
||||
ENABLE_OVERLOAD_RETARGETING = False
|
||||
|
||||
# Auth mechanism properties that must raise an error instead of warning if they invalidate.
|
||||
_MECH_PROP_MUST_RAISE = ["CANONICALIZE_HOST_NAME"]
|
||||
|
||||
@ -233,13 +239,6 @@ def validate_readable(option: str, value: Any) -> Optional[str]:
|
||||
return value
|
||||
|
||||
|
||||
def validate_positive_integer_or_none(option: str, value: Any) -> Optional[int]:
|
||||
"""Validate that 'value' is a positive integer or None."""
|
||||
if value is None:
|
||||
return value
|
||||
return validate_positive_integer(option, value)
|
||||
|
||||
|
||||
def validate_non_negative_integer_or_none(option: str, value: Any) -> Optional[int]:
|
||||
"""Validate that 'value' is a positive integer or 0 or None."""
|
||||
if value is None:
|
||||
@ -261,20 +260,6 @@ def validate_string_or_none(option: str, value: Any) -> Optional[str]:
|
||||
return validate_string(option, value)
|
||||
|
||||
|
||||
def validate_int_or_basestring(option: str, value: Any) -> Union[int, str]:
|
||||
"""Validates that 'value' is an integer or string."""
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
elif isinstance(value, str):
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return value
|
||||
raise TypeError(
|
||||
f"Wrong type for {option}, value must be an integer or a string, not {type(value)}"
|
||||
)
|
||||
|
||||
|
||||
def validate_non_negative_int_or_basestring(option: Any, value: Any) -> Union[int, str]:
|
||||
"""Validates that 'value' is an integer or string."""
|
||||
if isinstance(value, int):
|
||||
@ -738,6 +723,8 @@ URI_OPTIONS_VALIDATOR_MAP: dict[str, Callable[[Any, Any], Any]] = {
|
||||
"srvmaxhosts": validate_non_negative_integer,
|
||||
"timeoutms": validate_timeoutms,
|
||||
"servermonitoringmode": validate_server_monitoring_mode,
|
||||
"maxadaptiveretries": validate_non_negative_integer,
|
||||
"enableoverloadretargeting": validate_boolean_or_string,
|
||||
}
|
||||
|
||||
# Dictionary where keys are the names of URI options specific to pymongo,
|
||||
@ -771,6 +758,8 @@ KW_VALIDATORS: dict[str, Callable[[Any, Any], Any]] = {
|
||||
"server_selector": validate_is_callable_or_none,
|
||||
"auto_encryption_opts": validate_auto_encryption_opts_or_none,
|
||||
"authoidcallowedhosts": validate_list,
|
||||
"max_adaptive_retries": validate_non_negative_integer,
|
||||
"enable_overload_retargeting": validate_boolean_or_string,
|
||||
}
|
||||
|
||||
# Dictionary where keys are any URI option name, and values are the
|
||||
@ -817,16 +806,6 @@ TIMEOUT_OPTIONS: list[str] = [
|
||||
"waitqueuetimeoutms",
|
||||
]
|
||||
|
||||
_AUTH_OPTIONS = frozenset(["authmechanismproperties"])
|
||||
|
||||
|
||||
def validate_auth_option(option: str, value: Any) -> tuple[str, Any]:
|
||||
"""Validate optional authentication parameters."""
|
||||
lower, value = validate(option, value)
|
||||
if lower not in _AUTH_OPTIONS:
|
||||
raise ConfigurationError(f"Unknown option: {option}. Must be in {_AUTH_OPTIONS}")
|
||||
return option, value
|
||||
|
||||
|
||||
def _get_validator(
|
||||
key: str, validators: dict[str, Callable[[Any, Any], Any]], normed_key: Optional[str] = None
|
||||
|
||||
@ -357,7 +357,7 @@ class SSLContext:
|
||||
try:
|
||||
for storename in ("CA", "ROOT"):
|
||||
self._load_wincerts(storename)
|
||||
except PermissionError:
|
||||
except Exception:
|
||||
# Fall back to certifi
|
||||
self._load_certifi()
|
||||
elif _sys.platform == "darwin":
|
||||
|
||||
@ -59,6 +59,7 @@ from pymongo.errors import (
|
||||
InvalidOperation,
|
||||
NotPrimaryError,
|
||||
OperationFailure,
|
||||
PyMongoError,
|
||||
WaitQueueTimeoutError,
|
||||
)
|
||||
from pymongo.helpers_shared import _RETRYABLE_ERROR_CODES
|
||||
@ -561,9 +562,17 @@ class _ClientBulk:
|
||||
error, ConnectionFailure
|
||||
) and not isinstance(error, (NotPrimaryError, WaitQueueTimeoutError))
|
||||
|
||||
retryable_label_error = isinstance(
|
||||
error, PyMongoError
|
||||
) and error.has_error_label("RetryableError")
|
||||
|
||||
# Synthesize the full bulk result without modifying the
|
||||
# current one because this write operation may be retried.
|
||||
if retryable and (retryable_top_level_error or retryable_network_error):
|
||||
if retryable and (
|
||||
retryable_top_level_error
|
||||
or retryable_network_error
|
||||
or retryable_label_error
|
||||
):
|
||||
full = copy.deepcopy(full_result)
|
||||
_merge_command(self.ops, self.idx_offset, full, result)
|
||||
_throw_client_bulk_write_exception(full, self.verbose_results)
|
||||
|
||||
@ -136,9 +136,11 @@ Classes
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import random
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import Mapping as _Mapping
|
||||
from contextvars import ContextVar, Token
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@ -159,7 +161,9 @@ from pymongo import _csot
|
||||
from pymongo.errors import (
|
||||
ConfigurationError,
|
||||
ConnectionFailure,
|
||||
ExecutionTimeout,
|
||||
InvalidOperation,
|
||||
NetworkTimeout,
|
||||
OperationFailure,
|
||||
PyMongoError,
|
||||
WTimeoutError,
|
||||
@ -180,6 +184,28 @@ if TYPE_CHECKING:
|
||||
|
||||
_IS_SYNC = True
|
||||
|
||||
_SESSION: ContextVar[Optional[ClientSession]] = ContextVar("SESSION", default=None)
|
||||
|
||||
|
||||
class _BoundSessionContext:
|
||||
"""Context manager returned by ClientSession.bind() that manages bound state."""
|
||||
|
||||
def __init__(self, session: ClientSession, end_session: bool) -> None:
|
||||
self._session = session
|
||||
self._session_token: Optional[Token[ClientSession]] = None
|
||||
self._end_session = end_session
|
||||
|
||||
def __enter__(self) -> ClientSession:
|
||||
self._session_token = _SESSION.set(self._session) # type: ignore[assignment]
|
||||
return self._session
|
||||
|
||||
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
if self._session_token:
|
||||
_SESSION.reset(self._session_token) # type: ignore[arg-type]
|
||||
self._session_token = None
|
||||
if self._end_session:
|
||||
self._session.end_session()
|
||||
|
||||
|
||||
class SessionOptions:
|
||||
"""Options for a new :class:`ClientSession`.
|
||||
@ -403,6 +429,7 @@ class _Transaction:
|
||||
self.recovery_token = None
|
||||
self.attempt = 0
|
||||
self.client = client
|
||||
self.has_completed_command = False
|
||||
|
||||
def active(self) -> bool:
|
||||
return self.state in (_TxnState.STARTING, _TxnState.IN_PROGRESS)
|
||||
@ -410,6 +437,9 @@ class _Transaction:
|
||||
def starting(self) -> bool:
|
||||
return self.state == _TxnState.STARTING
|
||||
|
||||
def set_starting(self) -> None:
|
||||
self.state = _TxnState.STARTING
|
||||
|
||||
@property
|
||||
def pinned_conn(self) -> Optional[Connection]:
|
||||
if self.active() and self.conn_mgr:
|
||||
@ -435,6 +465,7 @@ class _Transaction:
|
||||
self.sharded = False
|
||||
self.recovery_token = None
|
||||
self.attempt = 0
|
||||
self.has_completed_command = False
|
||||
|
||||
def __del__(self) -> None:
|
||||
if self.conn_mgr:
|
||||
@ -469,11 +500,29 @@ _UNKNOWN_COMMIT_ERROR_CODES: frozenset = _RETRYABLE_ERROR_CODES | frozenset( #
|
||||
# This limit is non-configurable and was chosen to be twice the 60 second
|
||||
# default value of MongoDB's `transactionLifetimeLimitSeconds` parameter.
|
||||
_WITH_TRANSACTION_RETRY_TIME_LIMIT = 120
|
||||
_BACKOFF_MAX = 0.500 # 500ms max backoff
|
||||
_BACKOFF_INITIAL = 0.005 # 5ms initial backoff
|
||||
|
||||
|
||||
def _within_time_limit(start_time: float) -> bool:
|
||||
def _within_time_limit(start_time: float, backoff: float = 0) -> bool:
|
||||
"""Are we within the with_transaction retry limit?"""
|
||||
return time.monotonic() - start_time < _WITH_TRANSACTION_RETRY_TIME_LIMIT
|
||||
remaining = _csot.remaining()
|
||||
if remaining is not None and remaining <= 0:
|
||||
return False
|
||||
return time.monotonic() + backoff - start_time < _WITH_TRANSACTION_RETRY_TIME_LIMIT
|
||||
|
||||
|
||||
def _make_timeout_error(error: BaseException) -> PyMongoError:
|
||||
"""Convert error to a NetworkTimeout or ExecutionTimeout as appropriate."""
|
||||
if _csot.remaining() is not None:
|
||||
timeout_error: PyMongoError = ExecutionTimeout(
|
||||
str(error), 50, {"ok": 0, "errmsg": str(error), "code": 50}
|
||||
)
|
||||
else:
|
||||
timeout_error = NetworkTimeout(str(error))
|
||||
if isinstance(error, PyMongoError):
|
||||
timeout_error._error_labels = error._error_labels.copy()
|
||||
return timeout_error
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
@ -546,6 +595,24 @@ class ClientSession:
|
||||
if self._server_session is None:
|
||||
raise InvalidOperation("Cannot use ended session")
|
||||
|
||||
def bind(self, end_session: bool = True) -> _BoundSessionContext:
|
||||
"""Bind this session so it is implicitly passed to all database operations within the returned context.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with client.start_session() as s:
|
||||
with s.bind():
|
||||
# session=s is passed implicitly
|
||||
client.db.collection.insert_one({"x": 1})
|
||||
|
||||
:param end_session: Whether to end the session on exiting the returned context. Defaults to True.
|
||||
If set to False, :meth:`~pymongo.client_session.ClientSession.end_session()` must be called
|
||||
once the session is no longer used.
|
||||
|
||||
.. versionadded:: 4.17
|
||||
"""
|
||||
return _BoundSessionContext(self, end_session)
|
||||
|
||||
def __enter__(self) -> ClientSession:
|
||||
return self
|
||||
|
||||
@ -702,21 +769,32 @@ class ClientSession:
|
||||
https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/transactions-convenient-api.md#handling-errors-inside-the-callback
|
||||
"""
|
||||
start_time = time.monotonic()
|
||||
retry = 0
|
||||
last_error: Optional[BaseException] = None
|
||||
while True:
|
||||
if retry: # Implement exponential backoff on retry.
|
||||
jitter = random.random() # noqa: S311
|
||||
backoff = jitter * min(_BACKOFF_INITIAL * (1.5**retry), _BACKOFF_MAX)
|
||||
if not _within_time_limit(start_time, backoff):
|
||||
assert last_error is not None
|
||||
raise _make_timeout_error(last_error) from last_error
|
||||
time.sleep(backoff)
|
||||
retry += 1
|
||||
self.start_transaction(read_concern, write_concern, read_preference, max_commit_time_ms)
|
||||
try:
|
||||
ret = callback(self)
|
||||
# Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
|
||||
except BaseException as exc:
|
||||
last_error = exc
|
||||
if self.in_transaction:
|
||||
self.abort_transaction()
|
||||
if (
|
||||
isinstance(exc, PyMongoError)
|
||||
and exc.has_error_label("TransientTransactionError")
|
||||
and _within_time_limit(start_time)
|
||||
if isinstance(exc, PyMongoError) and exc.has_error_label(
|
||||
"TransientTransactionError"
|
||||
):
|
||||
# Retry the entire transaction.
|
||||
continue
|
||||
if _within_time_limit(start_time):
|
||||
# Retry the entire transaction.
|
||||
continue
|
||||
raise _make_timeout_error(last_error) from exc
|
||||
raise
|
||||
|
||||
if not self.in_transaction:
|
||||
@ -727,17 +805,18 @@ class ClientSession:
|
||||
try:
|
||||
self.commit_transaction()
|
||||
except PyMongoError as exc:
|
||||
if (
|
||||
exc.has_error_label("UnknownTransactionCommitResult")
|
||||
and _within_time_limit(start_time)
|
||||
and not _max_time_expired_error(exc)
|
||||
):
|
||||
last_error = exc
|
||||
if exc.has_error_label(
|
||||
"UnknownTransactionCommitResult"
|
||||
) and not _max_time_expired_error(exc):
|
||||
if not _within_time_limit(start_time):
|
||||
raise _make_timeout_error(last_error) from exc
|
||||
# Retry the commit.
|
||||
continue
|
||||
|
||||
if exc.has_error_label("TransientTransactionError") and _within_time_limit(
|
||||
start_time
|
||||
):
|
||||
if exc.has_error_label("TransientTransactionError"):
|
||||
if not _within_time_limit(start_time):
|
||||
raise _make_timeout_error(last_error) from exc
|
||||
# Retry the entire transaction.
|
||||
break
|
||||
raise
|
||||
@ -1018,7 +1097,11 @@ class ClientSession:
|
||||
read_preference: _ServerMode,
|
||||
conn: Connection,
|
||||
) -> None:
|
||||
if not conn.supports_sessions:
|
||||
# getMores must be sent with a session if the cursor was opened with one
|
||||
operation = next(iter(command))
|
||||
if not conn.supports_sessions and (
|
||||
isinstance(self._server_session, _EmptyServerSession) or operation != "getMore"
|
||||
):
|
||||
if not self._implicit:
|
||||
raise ConfigurationError("Sessions are not supported by this MongoDB deployment")
|
||||
return
|
||||
|
||||
@ -21,7 +21,6 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
ContextManager,
|
||||
Generic,
|
||||
Iterable,
|
||||
Iterator,
|
||||
@ -572,11 +571,6 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
change_stream._initialize_cursor()
|
||||
return change_stream
|
||||
|
||||
def _conn_for_writes(
|
||||
self, session: Optional[ClientSession], operation: str
|
||||
) -> ContextManager[Connection]:
|
||||
return self._database.client._conn_for_writes(session, operation)
|
||||
|
||||
def _command(
|
||||
self,
|
||||
conn: Connection,
|
||||
@ -653,7 +647,10 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
if "size" in options:
|
||||
options["size"] = float(options["size"])
|
||||
cmd.update(options)
|
||||
with self._conn_for_writes(session, operation=_Op.CREATE) as conn:
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession], conn: Connection, _retryable_write: bool
|
||||
) -> None:
|
||||
if qev2_required and conn.max_wire_version < 21:
|
||||
raise ConfigurationError(
|
||||
"Driver support of Queryable Encryption is incompatible with server. "
|
||||
@ -670,6 +667,8 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
session=session,
|
||||
)
|
||||
|
||||
self.database.client._retryable_write(False, inner, session, _Op.CREATE)
|
||||
|
||||
def _create(
|
||||
self,
|
||||
options: MutableMapping[str, Any],
|
||||
@ -2237,7 +2236,10 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
command (like maxTimeMS) can be passed as keyword arguments.
|
||||
"""
|
||||
names = []
|
||||
with self._conn_for_writes(session, operation=_Op.CREATE_INDEXES) as conn:
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession], conn: Connection, _retryable_write: bool
|
||||
) -> list[str]:
|
||||
supports_quorum = conn.max_wire_version >= 9
|
||||
|
||||
def gen_indexes() -> Iterator[Mapping[str, Any]]:
|
||||
@ -2266,7 +2268,9 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
write_concern=self._write_concern_for(session),
|
||||
session=session,
|
||||
)
|
||||
return names
|
||||
return names
|
||||
|
||||
return self.database.client._retryable_write(False, inner, session, _Op.CREATE_INDEXES)
|
||||
|
||||
def create_index(
|
||||
self,
|
||||
@ -2419,7 +2423,6 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
kwargs["comment"] = comment
|
||||
self._drop_index("*", session=session, **kwargs)
|
||||
|
||||
@_csot.apply
|
||||
def drop_index(
|
||||
self,
|
||||
index_or_name: _IndexKeyHint,
|
||||
@ -2487,7 +2490,10 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
cmd.update(kwargs)
|
||||
if comment is not None:
|
||||
cmd["comment"] = comment
|
||||
with self._conn_for_writes(session, operation=_Op.DROP_INDEXES) as conn:
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession], conn: Connection, _retryable_write: bool
|
||||
) -> None:
|
||||
self._command(
|
||||
conn,
|
||||
cmd,
|
||||
@ -2497,6 +2503,8 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
session=session,
|
||||
)
|
||||
|
||||
self.database.client._retryable_write(False, inner, session, _Op.DROP_INDEXES)
|
||||
|
||||
def list_indexes(
|
||||
self,
|
||||
session: Optional[ClientSession] = None,
|
||||
@ -2760,15 +2768,22 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
cmd = {"createSearchIndexes": self.name, "indexes": list(gen_indexes())}
|
||||
cmd.update(kwargs)
|
||||
|
||||
with self._conn_for_writes(session, operation=_Op.CREATE_SEARCH_INDEXES) as conn:
|
||||
def inner(
|
||||
session: Optional[ClientSession], conn: Connection, _retryable_write: bool
|
||||
) -> list[str]:
|
||||
resp = self._command(
|
||||
conn,
|
||||
cmd,
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
codec_options=_UNICODE_REPLACE_CODEC_OPTIONS,
|
||||
session=session,
|
||||
)
|
||||
return [index["name"] for index in resp["indexesCreated"]]
|
||||
|
||||
return self.database.client._retryable_write(
|
||||
False, inner, session, _Op.CREATE_SEARCH_INDEXES
|
||||
)
|
||||
|
||||
def drop_search_index(
|
||||
self,
|
||||
name: str,
|
||||
@ -2794,15 +2809,21 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
cmd.update(kwargs)
|
||||
if comment is not None:
|
||||
cmd["comment"] = comment
|
||||
with self._conn_for_writes(session, operation=_Op.DROP_SEARCH_INDEXES) as conn:
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession], conn: Connection, _retryable_write: bool
|
||||
) -> None:
|
||||
self._command(
|
||||
conn,
|
||||
cmd,
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
allowable_errors=["ns not found", 26],
|
||||
codec_options=_UNICODE_REPLACE_CODEC_OPTIONS,
|
||||
session=session,
|
||||
)
|
||||
|
||||
self.database.client._retryable_write(False, inner, session, _Op.DROP_SEARCH_INDEXES)
|
||||
|
||||
def update_search_index(
|
||||
self,
|
||||
name: str,
|
||||
@ -2830,15 +2851,21 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
cmd.update(kwargs)
|
||||
if comment is not None:
|
||||
cmd["comment"] = comment
|
||||
with self._conn_for_writes(session, operation=_Op.UPDATE_SEARCH_INDEX) as conn:
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession], conn: Connection, _retryable_write: bool
|
||||
) -> None:
|
||||
self._command(
|
||||
conn,
|
||||
cmd,
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
allowable_errors=["ns not found", 26],
|
||||
codec_options=_UNICODE_REPLACE_CODEC_OPTIONS,
|
||||
session=session,
|
||||
)
|
||||
|
||||
self.database.client._retryable_write(False, inner, session, _Op.UPDATE_SEARCH_INDEX)
|
||||
|
||||
def options(
|
||||
self,
|
||||
session: Optional[ClientSession] = None,
|
||||
@ -2911,6 +2938,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
session,
|
||||
retryable=not cmd._performs_write,
|
||||
operation=_Op.AGGREGATE,
|
||||
is_aggregate_write=cmd._performs_write,
|
||||
)
|
||||
|
||||
def aggregate(
|
||||
@ -3116,17 +3144,21 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
|
||||
if comment is not None:
|
||||
cmd["comment"] = comment
|
||||
write_concern = self._write_concern_for_cmd(cmd, session)
|
||||
client = self._database.client
|
||||
|
||||
with self._conn_for_writes(session, operation=_Op.RENAME) as conn:
|
||||
with self._database.client._tmp_session(session) as s:
|
||||
return conn.command(
|
||||
"admin",
|
||||
cmd,
|
||||
write_concern=write_concern,
|
||||
parse_write_concern_error=True,
|
||||
session=s,
|
||||
client=self._database.client,
|
||||
)
|
||||
def inner(
|
||||
session: Optional[ClientSession], conn: Connection, _retryable_write: bool
|
||||
) -> MutableMapping[str, Any]:
|
||||
return conn.command(
|
||||
"admin",
|
||||
cmd,
|
||||
write_concern=write_concern,
|
||||
parse_write_concern_error=True,
|
||||
session=session,
|
||||
client=client,
|
||||
)
|
||||
|
||||
return client._retryable_write(False, inner, session, _Op.RENAME)
|
||||
|
||||
def distinct(
|
||||
self,
|
||||
|
||||
@ -931,12 +931,15 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
|
||||
if read_preference is None:
|
||||
read_preference = (session and session._txn_read_preference()) or ReadPreference.PRIMARY
|
||||
with self._client._conn_for_reads(read_preference, session, operation=command_name) as (
|
||||
connection,
|
||||
read_preference,
|
||||
):
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession],
|
||||
_server: Server,
|
||||
conn: Connection,
|
||||
read_preference: _ServerMode,
|
||||
) -> Union[dict[str, Any], _CodecDocumentType]:
|
||||
return self._command(
|
||||
connection,
|
||||
conn,
|
||||
command,
|
||||
value,
|
||||
check,
|
||||
@ -947,6 +950,10 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return self._client._retryable_read(
|
||||
inner, read_preference, session, command_name, None, False, is_run_command=True
|
||||
)
|
||||
|
||||
@_csot.apply
|
||||
def cursor_command(
|
||||
self,
|
||||
@ -1014,15 +1021,17 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
|
||||
with self._client._tmp_session(session) as tmp_session:
|
||||
opts = codec_options or DEFAULT_CODEC_OPTIONS
|
||||
|
||||
if read_preference is None:
|
||||
read_preference = (
|
||||
tmp_session and tmp_session._txn_read_preference()
|
||||
) or ReadPreference.PRIMARY
|
||||
with self._client._conn_for_reads(read_preference, tmp_session, command_name) as (
|
||||
conn,
|
||||
read_preference,
|
||||
):
|
||||
|
||||
def inner(
|
||||
session: Optional[ClientSession],
|
||||
_server: Server,
|
||||
conn: Connection,
|
||||
read_preference: _ServerMode,
|
||||
) -> CommandCursor[_DocumentType]:
|
||||
response = self._command(
|
||||
conn,
|
||||
command,
|
||||
@ -1031,7 +1040,7 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
None,
|
||||
read_preference,
|
||||
opts,
|
||||
session=tmp_session,
|
||||
session=session,
|
||||
**kwargs,
|
||||
)
|
||||
coll = self.get_collection("$cmd", read_preference=read_preference)
|
||||
@ -1041,7 +1050,7 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
response["cursor"],
|
||||
conn.address,
|
||||
max_await_time_ms=max_await_time_ms,
|
||||
session=tmp_session,
|
||||
session=session,
|
||||
comment=comment,
|
||||
)
|
||||
cmd_cursor._maybe_pin_connection(conn)
|
||||
@ -1049,6 +1058,10 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
else:
|
||||
raise InvalidOperation("Command does not return a cursor.")
|
||||
|
||||
return self.client._retryable_read(
|
||||
inner, read_preference, tmp_session, command_name, None, False
|
||||
)
|
||||
|
||||
def _retryable_read_command(
|
||||
self,
|
||||
command: Union[str, MutableMapping[str, Any]],
|
||||
@ -1247,9 +1260,11 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
if comment is not None:
|
||||
command["comment"] = comment
|
||||
|
||||
with self._client._conn_for_writes(session, operation=_Op.DROP) as connection:
|
||||
def inner(
|
||||
session: Optional[ClientSession], conn: Connection, _retryable_write: bool
|
||||
) -> dict[str, Any]:
|
||||
return self._command(
|
||||
connection,
|
||||
conn,
|
||||
command,
|
||||
allowable_errors=["ns not found", 26],
|
||||
write_concern=self._write_concern_for(session),
|
||||
@ -1257,6 +1272,8 @@ class Database(common.BaseObject, Generic[_DocumentType]):
|
||||
session=session,
|
||||
)
|
||||
|
||||
return self.client._retryable_write(False, inner, session, _Op.DROP)
|
||||
|
||||
@_csot.apply
|
||||
def drop_collection(
|
||||
self,
|
||||
|
||||
@ -17,8 +17,11 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import builtins
|
||||
import functools
|
||||
import random
|
||||
import socket
|
||||
import sys
|
||||
import time as time # noqa: PLC0414 # needed in sync version
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
@ -26,6 +29,8 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
from pymongo import _csot
|
||||
from pymongo.common import MAX_ADAPTIVE_RETRIES
|
||||
from pymongo.errors import (
|
||||
OperationFailure,
|
||||
)
|
||||
@ -38,6 +43,7 @@ F = TypeVar("F", bound=Callable[..., Any])
|
||||
|
||||
|
||||
def _handle_reauth(func: F) -> F:
|
||||
@functools.wraps(func)
|
||||
def inner(*args: Any, **kwargs: Any) -> Any:
|
||||
no_reauth = kwargs.pop("no_reauth", False)
|
||||
from pymongo.message import _BulkWriteContext
|
||||
@ -70,6 +76,46 @@ def _handle_reauth(func: F) -> F:
|
||||
return cast(F, inner)
|
||||
|
||||
|
||||
_BACKOFF_INITIAL = 0.1
|
||||
_BACKOFF_MAX = 10
|
||||
|
||||
|
||||
def _backoff(
|
||||
attempt: int, initial_delay: float = _BACKOFF_INITIAL, max_delay: float = _BACKOFF_MAX
|
||||
) -> float:
|
||||
jitter = random.random() # noqa: S311
|
||||
return jitter * min(initial_delay * (2**attempt), max_delay)
|
||||
|
||||
|
||||
class _RetryPolicy:
|
||||
"""A retry limiter that performs exponential backoff with jitter."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attempts: int = MAX_ADAPTIVE_RETRIES,
|
||||
backoff_initial: float = _BACKOFF_INITIAL,
|
||||
backoff_max: float = _BACKOFF_MAX,
|
||||
):
|
||||
self.attempts = attempts
|
||||
self.backoff_initial = backoff_initial
|
||||
self.backoff_max = backoff_max
|
||||
|
||||
def backoff(self, attempt: int) -> float:
|
||||
"""Return the backoff duration for the given attempt."""
|
||||
return _backoff(max(0, attempt - 1), self.backoff_initial, self.backoff_max)
|
||||
|
||||
def should_retry(self, attempt: int, delay: float) -> bool:
|
||||
"""Return if we have retry attempts remaining and the next backoff would not exceed a timeout."""
|
||||
if attempt > self.attempts:
|
||||
return False
|
||||
|
||||
if _csot.get_timeout():
|
||||
if time.monotonic() + delay > _csot.get_deadline():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _getaddrinfo(
|
||||
host: Any, port: Any, **kwargs: Any
|
||||
) -> list[
|
||||
|
||||
@ -35,6 +35,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import contextlib
|
||||
import os
|
||||
import time as time # noqa: PLC0414 # needed in sync version
|
||||
import warnings
|
||||
import weakref
|
||||
from collections import defaultdict
|
||||
@ -108,8 +109,11 @@ from pymongo.server_type import SERVER_TYPE
|
||||
from pymongo.synchronous import client_session, database, uri_parser
|
||||
from pymongo.synchronous.change_stream import ChangeStream, ClusterChangeStream
|
||||
from pymongo.synchronous.client_bulk import _ClientBulk
|
||||
from pymongo.synchronous.client_session import _EmptyServerSession
|
||||
from pymongo.synchronous.client_session import _SESSION, _EmptyServerSession
|
||||
from pymongo.synchronous.command_cursor import CommandCursor
|
||||
from pymongo.synchronous.helpers import (
|
||||
_RetryPolicy,
|
||||
)
|
||||
from pymongo.synchronous.settings import TopologySettings
|
||||
from pymongo.synchronous.topology import Topology, _ErrorContext
|
||||
from pymongo.topology_description import TOPOLOGY_TYPE, TopologyDescription
|
||||
@ -610,8 +614,18 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
client to use Stable API. See `versioned API <https://www.mongodb.com/docs/manual/reference/stable-api/#what-is-the-stable-api--and-should-you-use-it->`_ for
|
||||
details.
|
||||
|
||||
| **Overload retry options:**
|
||||
|
||||
- `max_adaptive_retries`: (int) How many retries to allow for overload errors. Defaults to ``2``.
|
||||
- `enable_overload_retargeting`: (boolean) Whether overload retargeting is enabled for this client.
|
||||
If enabled, server overload errors will cause retry attempts to select a server that has not yet returned an overload error, if possible.
|
||||
Defaults to ``False``.
|
||||
|
||||
.. seealso:: The MongoDB documentation on `connections <https://dochub.mongodb.org/core/connections>`_.
|
||||
|
||||
.. versionchanged:: 4.17
|
||||
Added the ``max_adaptive_retries`` and ``enable_overload_retargeting`` URI and keyword arguments.
|
||||
|
||||
.. versionchanged:: 4.5
|
||||
Added the ``serverMonitoringMode`` keyword argument.
|
||||
|
||||
@ -879,11 +893,14 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
self._options.read_concern,
|
||||
)
|
||||
|
||||
self._retry_policy = _RetryPolicy(attempts=self._options.max_adaptive_retries)
|
||||
|
||||
self._init_based_on_options(self._seeds, srv_max_hosts, srv_service_name)
|
||||
|
||||
self._opened = False
|
||||
self._closed = False
|
||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
|
||||
if not is_srv:
|
||||
self._init_background()
|
||||
|
||||
@ -1406,7 +1423,8 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
)
|
||||
|
||||
def _ensure_session(self, session: Optional[ClientSession] = None) -> Optional[ClientSession]:
|
||||
"""If provided session is None, lend a temporary session."""
|
||||
"""If provided session and bound session are None, lend a temporary session."""
|
||||
session = session or self._get_bound_session()
|
||||
if session:
|
||||
return session
|
||||
|
||||
@ -1986,6 +2004,8 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
read_pref: Optional[_ServerMode] = None,
|
||||
retryable: bool = False,
|
||||
operation_id: Optional[int] = None,
|
||||
is_run_command: bool = False,
|
||||
is_aggregate_write: bool = False,
|
||||
) -> T:
|
||||
"""Internal retryable helper for all client transactions.
|
||||
|
||||
@ -1997,6 +2017,8 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
:param address: Server Address, defaults to None
|
||||
:param read_pref: Topology of read operation, defaults to None
|
||||
:param retryable: If the operation should be retried once, defaults to None
|
||||
:param is_run_command: If this is a runCommand operation, defaults to False
|
||||
:param is_aggregate_write: If this is a aggregate operation with a write, defaults to False.
|
||||
|
||||
:return: Output of the calling func()
|
||||
"""
|
||||
@ -2011,6 +2033,8 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
address=address,
|
||||
retryable=retryable,
|
||||
operation_id=operation_id,
|
||||
is_run_command=is_run_command,
|
||||
is_aggregate_write=is_aggregate_write,
|
||||
).run()
|
||||
|
||||
def _retryable_read(
|
||||
@ -2022,6 +2046,8 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
address: Optional[_Address] = None,
|
||||
retryable: bool = True,
|
||||
operation_id: Optional[int] = None,
|
||||
is_run_command: bool = False,
|
||||
is_aggregate_write: bool = False,
|
||||
) -> T:
|
||||
"""Execute an operation with consecutive retries if possible
|
||||
|
||||
@ -2037,6 +2063,8 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
:param address: Optional address when sending a message, defaults to None
|
||||
:param retryable: if we should attempt retries
|
||||
(may not always be supported even if supplied), defaults to False
|
||||
:param is_run_command: If this is a runCommand operation, defaults to False.
|
||||
:param is_aggregate_write: If this is a aggregate operation with a write, defaults to False.
|
||||
"""
|
||||
|
||||
# Ensure that the client supports retrying on reads and there is no session in
|
||||
@ -2055,6 +2083,8 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
read_pref=read_pref,
|
||||
retryable=retryable,
|
||||
operation_id=operation_id,
|
||||
is_run_command=is_run_command,
|
||||
is_aggregate_write=is_aggregate_write,
|
||||
)
|
||||
|
||||
def _retryable_write(
|
||||
@ -2263,11 +2293,14 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
self, session: Optional[client_session.ClientSession]
|
||||
) -> Generator[Optional[client_session.ClientSession], None]:
|
||||
"""If provided session is None, lend a temporary session."""
|
||||
if session is not None:
|
||||
if not isinstance(session, client_session.ClientSession):
|
||||
raise ValueError(
|
||||
f"'session' argument must be a ClientSession or None, not {type(session)}"
|
||||
)
|
||||
if session is not None and not isinstance(session, client_session.ClientSession):
|
||||
raise ValueError(
|
||||
f"'session' argument must be a ClientSession or None, not {type(session)}"
|
||||
)
|
||||
|
||||
# Check for a bound session. If one exists, treat it as an explicitly passed session.
|
||||
session = session or self._get_bound_session()
|
||||
if session:
|
||||
# Don't call end_session.
|
||||
yield session
|
||||
return
|
||||
@ -2295,6 +2328,18 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
if session is not None:
|
||||
session._process_response(reply)
|
||||
|
||||
def _get_bound_session(self) -> Optional[ClientSession]:
|
||||
bound_session = _SESSION.get()
|
||||
if bound_session:
|
||||
if bound_session.client is self:
|
||||
return bound_session
|
||||
else:
|
||||
raise InvalidOperation(
|
||||
"Only the client that created the bound session can perform operations within its context block. See <PLACEHOLDER> for more information."
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
def server_info(self, session: Optional[client_session.ClientSession] = None) -> dict[str, Any]:
|
||||
"""Get information about the MongoDB server we're connected to.
|
||||
|
||||
@ -2428,15 +2473,13 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
f"name_or_database must be an instance of str or a Database, not {type(name)}"
|
||||
)
|
||||
|
||||
with self._conn_for_writes(session, operation=_Op.DROP_DATABASE) as conn:
|
||||
self[name]._command(
|
||||
conn,
|
||||
{"dropDatabase": 1, "comment": comment},
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
write_concern=self._write_concern_for(session),
|
||||
parse_write_concern_error=True,
|
||||
session=session,
|
||||
)
|
||||
self[name].command(
|
||||
{"dropDatabase": 1, "comment": comment},
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
write_concern=self._write_concern_for(session),
|
||||
parse_write_concern_error=True,
|
||||
session=session,
|
||||
)
|
||||
|
||||
@_csot.apply
|
||||
def bulk_write(
|
||||
@ -2720,12 +2763,15 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
address: Optional[_Address] = None,
|
||||
retryable: bool = False,
|
||||
operation_id: Optional[int] = None,
|
||||
is_run_command: bool = False,
|
||||
is_aggregate_write: bool = False,
|
||||
):
|
||||
self._last_error: Optional[Exception] = None
|
||||
self._retrying = False
|
||||
self._multiple_retries = _csot.get_timeout() is not None
|
||||
self._always_retryable = False
|
||||
self._max_retries = float("inf") if _csot.get_timeout() is not None else 1
|
||||
self._client = mongo_client
|
||||
|
||||
self._retry_policy = mongo_client._retry_policy
|
||||
self._func = func
|
||||
self._bulk = bulk
|
||||
self._session = session
|
||||
@ -2741,6 +2787,8 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
self._operation = operation
|
||||
self._operation_id = operation_id
|
||||
self._attempt_number = 0
|
||||
self._is_run_command = is_run_command
|
||||
self._is_aggregate_write = is_aggregate_write
|
||||
|
||||
def run(self) -> T:
|
||||
"""Runs the supplied func() and attempts a retry
|
||||
@ -2760,7 +2808,13 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
while True:
|
||||
self._check_last_error(check_csot=True)
|
||||
try:
|
||||
return self._read() if self._is_read else self._write()
|
||||
res = self._read() if self._is_read else self._write()
|
||||
# Track whether the transaction has completed a command.
|
||||
# If we need to apply backpressure to the first command,
|
||||
# we will need to revert back to starting state.
|
||||
if self._session is not None and self._session.in_transaction:
|
||||
self._session._transaction.has_completed_command = True
|
||||
return res
|
||||
except ServerSelectionTimeoutError:
|
||||
# The application may think the write was never attempted
|
||||
# if we raise ServerSelectionTimeoutError on the retry
|
||||
@ -2771,37 +2825,80 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
# most likely be a waste of time.
|
||||
raise
|
||||
except PyMongoError as exc:
|
||||
always_retryable = False
|
||||
overloaded = False
|
||||
exc_to_check = exc
|
||||
|
||||
if self._is_run_command and not (
|
||||
self._client.options.retry_reads and self._client.options.retry_writes
|
||||
):
|
||||
raise
|
||||
if self._is_aggregate_write and not self._client.options.retry_writes:
|
||||
raise
|
||||
|
||||
# Execute specialized catch on read
|
||||
if self._is_read:
|
||||
if isinstance(exc, (ConnectionFailure, OperationFailure)):
|
||||
# ConnectionFailures do not supply a code property
|
||||
exc_code = getattr(exc, "code", None)
|
||||
if self._is_not_eligible_for_retry() or (
|
||||
isinstance(exc, OperationFailure)
|
||||
and exc_code not in helpers_shared._RETRYABLE_ERROR_CODES
|
||||
overloaded = exc.has_error_label("SystemOverloadedError")
|
||||
if overloaded:
|
||||
self._max_retries = self._client.options.max_adaptive_retries
|
||||
always_retryable = exc.has_error_label("RetryableError") and overloaded
|
||||
if not self._client.options.retry_reads or (
|
||||
not always_retryable
|
||||
and (
|
||||
self._is_not_eligible_for_retry()
|
||||
or (
|
||||
isinstance(exc, OperationFailure)
|
||||
and exc_code not in helpers_shared._RETRYABLE_ERROR_CODES
|
||||
)
|
||||
)
|
||||
):
|
||||
raise
|
||||
self._retrying = True
|
||||
self._last_error = exc
|
||||
self._attempt_number += 1
|
||||
|
||||
# Revert back to starting state if we're in a transaction but haven't completed the first
|
||||
# command.
|
||||
if (
|
||||
overloaded
|
||||
and self._session is not None
|
||||
and self._session.in_transaction
|
||||
):
|
||||
transaction = self._session._transaction
|
||||
if not transaction.has_completed_command:
|
||||
transaction.set_starting()
|
||||
transaction.attempt = 0
|
||||
else:
|
||||
raise
|
||||
|
||||
# Specialized catch on write operation
|
||||
if not self._is_read:
|
||||
if not self._retryable:
|
||||
if isinstance(exc, ClientBulkWriteException) and isinstance(
|
||||
exc.error, PyMongoError
|
||||
):
|
||||
exc_to_check = exc.error
|
||||
retryable_write_label = exc_to_check.has_error_label("RetryableWriteError")
|
||||
overloaded = exc_to_check.has_error_label("SystemOverloadedError")
|
||||
if overloaded:
|
||||
self._max_retries = self._client.options.max_adaptive_retries
|
||||
always_retryable = exc_to_check.has_error_label("RetryableError") and overloaded
|
||||
|
||||
# Always retry abortTransaction and commitTransaction up to once
|
||||
if self._operation not in ["abortTransaction", "commitTransaction"] and (
|
||||
not self._client.options.retry_writes
|
||||
or not (self._retryable or always_retryable)
|
||||
):
|
||||
raise
|
||||
if isinstance(exc, ClientBulkWriteException) and exc.error:
|
||||
retryable_write_error_exc = isinstance(
|
||||
exc.error, PyMongoError
|
||||
) and exc.error.has_error_label("RetryableWriteError")
|
||||
else:
|
||||
retryable_write_error_exc = exc.has_error_label("RetryableWriteError")
|
||||
if retryable_write_error_exc:
|
||||
if retryable_write_label or always_retryable:
|
||||
assert self._session
|
||||
self._session._unpin()
|
||||
if not retryable_write_error_exc or self._is_not_eligible_for_retry():
|
||||
if exc.has_error_label("NoWritesPerformed") and self._last_error:
|
||||
if not always_retryable and (
|
||||
not retryable_write_label or self._is_not_eligible_for_retry()
|
||||
):
|
||||
if exc_to_check.has_error_label("NoWritesPerformed") and self._last_error:
|
||||
raise self._last_error from exc
|
||||
else:
|
||||
raise
|
||||
@ -2810,17 +2907,39 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
self._bulk.retrying = True
|
||||
else:
|
||||
self._retrying = True
|
||||
if not exc.has_error_label("NoWritesPerformed"):
|
||||
if not exc_to_check.has_error_label("NoWritesPerformed"):
|
||||
self._last_error = exc
|
||||
if self._last_error is None:
|
||||
self._last_error = exc
|
||||
# Revert back to starting state if we're in a transaction but haven't completed the first
|
||||
# command.
|
||||
if overloaded and self._session is not None and self._session.in_transaction:
|
||||
transaction = self._session._transaction
|
||||
if not transaction.has_completed_command:
|
||||
transaction.set_starting()
|
||||
transaction.attempt = 0
|
||||
|
||||
if self._server is not None:
|
||||
if self._server is not None and (
|
||||
self._client.topology_description.topology_type_name == "Sharded"
|
||||
or (overloaded and self._client.options.enable_overload_retargeting)
|
||||
):
|
||||
self._deprioritized_servers.append(self._server)
|
||||
|
||||
self._always_retryable = always_retryable
|
||||
if overloaded:
|
||||
delay = self._retry_policy.backoff(self._attempt_number)
|
||||
if not self._retry_policy.should_retry(self._attempt_number, delay):
|
||||
if exc_to_check.has_error_label("NoWritesPerformed") and self._last_error:
|
||||
raise self._last_error from exc
|
||||
else:
|
||||
raise
|
||||
time.sleep(delay)
|
||||
|
||||
def _is_not_eligible_for_retry(self) -> bool:
|
||||
"""Checks if the exchange is not eligible for retry"""
|
||||
return not self._retryable or (self._is_retrying() and not self._multiple_retries)
|
||||
return not self._retryable or (
|
||||
self._is_retrying() and self._attempt_number >= self._max_retries
|
||||
)
|
||||
|
||||
def _is_retrying(self) -> bool:
|
||||
"""Checks if the exchange is currently undergoing a retry"""
|
||||
@ -2879,7 +2998,7 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
and conn.supports_sessions
|
||||
)
|
||||
is_mongos = conn.is_mongos
|
||||
if not sessions_supported:
|
||||
if not self._always_retryable and not sessions_supported:
|
||||
# A retry is not possible because this server does
|
||||
# not support sessions raise the last error.
|
||||
self._check_last_error()
|
||||
@ -2911,7 +3030,7 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
conn,
|
||||
read_pref,
|
||||
):
|
||||
if self._retrying and not self._retryable:
|
||||
if self._retrying and not self._retryable and not self._always_retryable:
|
||||
self._check_last_error()
|
||||
if self._retrying:
|
||||
_debug_log(
|
||||
|
||||
@ -19,6 +19,8 @@ import collections
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
import time
|
||||
import weakref
|
||||
@ -49,10 +51,12 @@ from pymongo.errors import ( # type:ignore[attr-defined]
|
||||
DocumentTooLarge,
|
||||
ExecutionTimeout,
|
||||
InvalidOperation,
|
||||
NetworkTimeout,
|
||||
NotPrimaryError,
|
||||
OperationFailure,
|
||||
PyMongoError,
|
||||
WaitQueueTimeoutError,
|
||||
_CertificateError,
|
||||
)
|
||||
from pymongo.hello import Hello, HelloCompat
|
||||
from pymongo.helpers_shared import _get_timeout_details, format_timeout_details
|
||||
@ -250,6 +254,7 @@ class Connection:
|
||||
cmd = self.hello_cmd()
|
||||
performing_handshake = not self.performed_handshake
|
||||
awaitable = False
|
||||
cmd["backpressure"] = True
|
||||
if performing_handshake:
|
||||
self.performed_handshake = True
|
||||
cmd["client"] = self.opts.metadata
|
||||
@ -750,14 +755,10 @@ class Pool:
|
||||
# Enforces: maxConnecting
|
||||
# Also used for: clearing the wait queue
|
||||
self._max_connecting_cond = _create_condition(self.lock)
|
||||
self._max_connecting = self.opts.max_connecting
|
||||
self._pending = 0
|
||||
self._max_connecting = self.opts.max_connecting
|
||||
self._client_id = client_id
|
||||
if self.enabled_for_cmap:
|
||||
assert self.opts._event_listeners is not None
|
||||
self.opts._event_listeners.publish_pool_created(
|
||||
self.address, self.opts.non_default_options
|
||||
)
|
||||
# Log before publishing event to prevent potential listener preemption in tests
|
||||
if self.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
_CONNECTION_LOGGER,
|
||||
@ -767,6 +768,11 @@ class Pool:
|
||||
serverPort=self.address[1],
|
||||
**self.opts.non_default_options,
|
||||
)
|
||||
if self.enabled_for_cmap:
|
||||
assert self.opts._event_listeners is not None
|
||||
self.opts._event_listeners.publish_pool_created(
|
||||
self.address, self.opts.non_default_options
|
||||
)
|
||||
# Similar to active_sockets but includes threads in the wait queue.
|
||||
self.operation_count: int = 0
|
||||
# Retain references to pinned connections to prevent the CPython GC
|
||||
@ -781,9 +787,6 @@ class Pool:
|
||||
with self.lock:
|
||||
if self.state != PoolState.READY:
|
||||
self.state = PoolState.READY
|
||||
if self.enabled_for_cmap:
|
||||
assert self.opts._event_listeners is not None
|
||||
self.opts._event_listeners.publish_pool_ready(self.address)
|
||||
if self.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
_CONNECTION_LOGGER,
|
||||
@ -792,6 +795,9 @@ class Pool:
|
||||
serverHost=self.address[0],
|
||||
serverPort=self.address[1],
|
||||
)
|
||||
if self.enabled_for_cmap:
|
||||
assert self.opts._event_listeners is not None
|
||||
self.opts._event_listeners.publish_pool_ready(self.address)
|
||||
|
||||
@property
|
||||
def closed(self) -> bool:
|
||||
@ -852,9 +858,6 @@ class Pool:
|
||||
else:
|
||||
for conn in sockets:
|
||||
conn.close_conn(ConnectionClosedReason.POOL_CLOSED)
|
||||
if self.enabled_for_cmap:
|
||||
assert listeners is not None
|
||||
listeners.publish_pool_closed(self.address)
|
||||
if self.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
_CONNECTION_LOGGER,
|
||||
@ -863,15 +866,11 @@ class Pool:
|
||||
serverHost=self.address[0],
|
||||
serverPort=self.address[1],
|
||||
)
|
||||
if self.enabled_for_cmap:
|
||||
assert listeners is not None
|
||||
listeners.publish_pool_closed(self.address)
|
||||
else:
|
||||
if old_state != PoolState.PAUSED:
|
||||
if self.enabled_for_cmap:
|
||||
assert listeners is not None
|
||||
listeners.publish_pool_cleared(
|
||||
self.address,
|
||||
service_id=service_id,
|
||||
interrupt_connections=interrupt_connections,
|
||||
)
|
||||
if self.enabled_for_logging and _CONNECTION_LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_debug_log(
|
||||
_CONNECTION_LOGGER,
|
||||
@ -881,6 +880,13 @@ class Pool:
|
||||
serverPort=self.address[1],
|
||||
serviceId=service_id,
|
||||
)
|
||||
if self.enabled_for_cmap:
|
||||
assert listeners is not None
|
||||
listeners.publish_pool_cleared(
|
||||
self.address,
|
||||
service_id=service_id,
|
||||
interrupt_connections=interrupt_connections,
|
||||
)
|
||||
if not _IS_SYNC:
|
||||
asyncio.gather(
|
||||
*[conn.close_conn(ConnectionClosedReason.STALE) for conn in sockets], # type: ignore[func-returns-value]
|
||||
@ -982,6 +988,21 @@ class Pool:
|
||||
self.requests -= 1
|
||||
self.size_cond.notify()
|
||||
|
||||
def _handle_connection_error(self, error: BaseException) -> None:
|
||||
# Handle system overload condition for non-sdam pools.
|
||||
# Look for errors of type AutoReconnect and add error labels if appropriate.
|
||||
if self.is_sdam or type(error) not in (AutoReconnect, NetworkTimeout):
|
||||
return
|
||||
assert isinstance(error, AutoReconnect) # Appease type checker.
|
||||
# If the original error was a DNS, certificate, or SSL error, ignore it.
|
||||
if isinstance(error.__cause__, (_CertificateError, SSLErrors, socket.gaierror)):
|
||||
# End of file errors are excluded, because the server may have disconnected
|
||||
# during the handshake.
|
||||
if not isinstance(error.__cause__, (ssl.SSLEOFError, ssl.SSLZeroReturnError)):
|
||||
return
|
||||
error._add_error_label("SystemOverloadedError")
|
||||
error._add_error_label("RetryableError")
|
||||
|
||||
def connect(self, handler: Optional[_MongoClientErrorHandler] = None) -> Connection:
|
||||
"""Connect to Mongo and return a new Connection.
|
||||
|
||||
@ -1033,10 +1054,10 @@ class Pool:
|
||||
reason=_verbose_connection_error_reason(ConnectionClosedReason.ERROR),
|
||||
error=ConnectionClosedReason.ERROR,
|
||||
)
|
||||
self._handle_connection_error(error)
|
||||
if isinstance(error, (IOError, OSError, *SSLErrors)):
|
||||
details = _get_timeout_details(self.opts)
|
||||
_raise_connection_failure(self.address, error, timeout_details=details)
|
||||
|
||||
raise
|
||||
|
||||
conn = Connection(networking_interface, self, self.address, conn_id, self.is_sdam) # type: ignore[arg-type]
|
||||
@ -1045,18 +1066,22 @@ class Pool:
|
||||
self.active_contexts.discard(tmp_context)
|
||||
if tmp_context.cancelled:
|
||||
conn.cancel_context.cancel()
|
||||
completed_hello = False
|
||||
try:
|
||||
if not self.is_sdam:
|
||||
conn.hello()
|
||||
completed_hello = True
|
||||
self.is_writable = conn.is_writable
|
||||
if handler:
|
||||
handler.contribute_socket(conn, completed_handshake=False)
|
||||
|
||||
conn.authenticate()
|
||||
# Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
|
||||
except BaseException:
|
||||
except BaseException as e:
|
||||
with self.lock:
|
||||
self.active_contexts.discard(conn.cancel_context)
|
||||
if not completed_hello:
|
||||
self._handle_connection_error(e)
|
||||
conn.close_conn(ConnectionClosedReason.ERROR)
|
||||
raise
|
||||
|
||||
@ -1385,8 +1410,8 @@ class Pool:
|
||||
:class:`~pymongo.errors.AutoReconnect` exceptions on server
|
||||
hiccups, etc. We only check if the socket was closed by an external
|
||||
error if it has been > 1 second since the socket was checked into the
|
||||
pool, to keep performance reasonable - we can't avoid AutoReconnects
|
||||
completely anyway.
|
||||
pool to keep performance reasonable -
|
||||
we can't avoid AutoReconnects completely anyway.
|
||||
"""
|
||||
idle_time_seconds = conn.idle_time_seconds()
|
||||
# If socket is idle, open a new one.
|
||||
@ -1397,8 +1422,9 @@ class Pool:
|
||||
conn.close_conn(ConnectionClosedReason.IDLE)
|
||||
return True
|
||||
|
||||
if self._check_interval_seconds is not None and (
|
||||
self._check_interval_seconds == 0 or idle_time_seconds > self._check_interval_seconds
|
||||
check_interval_seconds = self._check_interval_seconds
|
||||
if check_interval_seconds is not None and (
|
||||
check_interval_seconds == 0 or idle_time_seconds > check_interval_seconds
|
||||
):
|
||||
if conn.conn_closed():
|
||||
conn.close_conn(ConnectionClosedReason.ERROR)
|
||||
|
||||
@ -911,7 +911,9 @@ class Topology:
|
||||
# Clear the pool.
|
||||
server.reset(service_id)
|
||||
elif isinstance(error, ConnectionFailure):
|
||||
if isinstance(error, WaitQueueTimeoutError):
|
||||
if isinstance(error, WaitQueueTimeoutError) or (
|
||||
error.has_error_label("SystemOverloadedError")
|
||||
):
|
||||
return
|
||||
# "Client MUST replace the server's description with type Unknown
|
||||
# ... MUST NOT request an immediate check of the server."
|
||||
|
||||
@ -51,7 +51,6 @@ dev = []
|
||||
pip = ["pip>=20.2"]
|
||||
gevent = ["gevent>=21.12"]
|
||||
coverage = [
|
||||
"pytest-cov>=4.0.0",
|
||||
"coverage[toml]>=5,<=7.10.7"
|
||||
]
|
||||
mockupdb = [
|
||||
|
||||
@ -104,7 +104,7 @@ class OIDCTestBase(AsyncPyMongoTestCase):
|
||||
|
||||
@asynccontextmanager
|
||||
async def fail_point(self, command_args):
|
||||
cmd_on = SON([("configureFailPoint", "failCommand")])
|
||||
cmd_on = dict(configureFailPoint="failCommand", appName="auth_oidc")
|
||||
cmd_on.update(command_args)
|
||||
client = AsyncMongoClient(self.uri_admin)
|
||||
await client.admin.command(cmd_on)
|
||||
@ -112,7 +112,7 @@ class OIDCTestBase(AsyncPyMongoTestCase):
|
||||
yield
|
||||
finally:
|
||||
await client.admin.command(
|
||||
"configureFailPoint", cmd_on["configureFailPoint"], mode="off"
|
||||
"configureFailPoint", cmd_on["configureFailPoint"], mode="off", appName="auth_oidc"
|
||||
)
|
||||
await client.close()
|
||||
|
||||
|
||||
@ -652,6 +652,38 @@ class AsyncClientUnitTest(AsyncUnitTest):
|
||||
with self.assertWarns(UserWarning):
|
||||
self.simple_client(multi_host)
|
||||
|
||||
async def test_max_adaptive_retries(self):
|
||||
# Assert that max adaptive retries defaults to 2.
|
||||
c = self.simple_client(connect=False)
|
||||
self.assertEqual(c.options.max_adaptive_retries, 2)
|
||||
|
||||
# Assert that max adaptive retries can be configured through connection or client options.
|
||||
c = self.simple_client(connect=False, max_adaptive_retries=10)
|
||||
self.assertEqual(c.options.max_adaptive_retries, 10)
|
||||
|
||||
c = self.simple_client(connect=False, maxAdaptiveRetries=10)
|
||||
self.assertEqual(c.options.max_adaptive_retries, 10)
|
||||
|
||||
c = self.simple_client(host="mongodb://localhost/?maxAdaptiveRetries=10", connect=False)
|
||||
self.assertEqual(c.options.max_adaptive_retries, 10)
|
||||
|
||||
async def test_enable_overload_retargeting(self):
|
||||
# Assert that overload retargeting defaults to false.
|
||||
c = self.simple_client(connect=False)
|
||||
self.assertFalse(c.options.enable_overload_retargeting)
|
||||
|
||||
# Assert that overload retargeting can be enabled through connection or client options.
|
||||
c = self.simple_client(connect=False, enable_overload_retargeting=True)
|
||||
self.assertTrue(c.options.enable_overload_retargeting)
|
||||
|
||||
c = self.simple_client(connect=False, enableOverloadRetargeting=True)
|
||||
self.assertTrue(c.options.enable_overload_retargeting)
|
||||
|
||||
c = self.simple_client(
|
||||
host="mongodb://localhost/?enableOverloadRetargeting=true", connect=False
|
||||
)
|
||||
self.assertTrue(c.options.enable_overload_retargeting)
|
||||
|
||||
|
||||
class TestClient(AsyncIntegrationTest):
|
||||
def test_multiple_uris(self):
|
||||
@ -1034,7 +1066,7 @@ class TestClient(AsyncIntegrationTest):
|
||||
db_names = await self.client.list_database_names()
|
||||
self.assertIn("pymongo_test", db_names)
|
||||
self.assertIn("pymongo_test_mike", db_names)
|
||||
self.assertEqual(db_names, cmd_names)
|
||||
self.assertCountEqual(db_names, cmd_names)
|
||||
|
||||
async def test_drop_database(self):
|
||||
with self.assertRaises(TypeError):
|
||||
@ -2679,11 +2711,11 @@ class TestClientPool(AsyncMockClientTest):
|
||||
|
||||
await async_wait_until(lambda: len(c.nodes) == 1, "connect")
|
||||
self.assertEqual(await c.address, ("c", 3))
|
||||
# Assert that we create 1 pooled connection.
|
||||
# Wait for the pooled connection to be registered
|
||||
await listener.async_wait_for_event(monitoring.ConnectionReadyEvent, 1)
|
||||
self.assertEqual(listener.event_count(monitoring.ConnectionCreatedEvent), 1)
|
||||
arbiter = c._topology.get_server_by_address(("c", 3))
|
||||
self.assertEqual(len(arbiter.pool.conns), 1)
|
||||
await async_wait_until(lambda: len(arbiter.pool.conns) == 1, "create 1 pooled connection")
|
||||
# Arbiter pool is marked ready.
|
||||
self.assertEqual(listener.event_count(monitoring.PoolReadyEvent), 1)
|
||||
|
||||
|
||||
312
test/asynchronous/test_client_backpressure.py
Normal file
312
test/asynchronous/test_client_backpressure.py
Normal file
@ -0,0 +1,312 @@
|
||||
# Copyright 2025-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.
|
||||
|
||||
"""Test Client Backpressure spec."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from time import perf_counter
|
||||
from unittest.mock import patch
|
||||
|
||||
from pymongo.common import MAX_ADAPTIVE_RETRIES
|
||||
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
from test.asynchronous import (
|
||||
AsyncIntegrationTest,
|
||||
async_client_context,
|
||||
unittest,
|
||||
)
|
||||
from test.asynchronous.unified_format import generate_test_classes
|
||||
from test.utils_shared import EventListener, OvertCommandListener
|
||||
|
||||
from pymongo.errors import OperationFailure, PyMongoError
|
||||
|
||||
_IS_SYNC = False
|
||||
|
||||
# Mock a system overload error.
|
||||
mock_overload_error = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {"times": 1},
|
||||
"data": {
|
||||
"failCommands": ["find", "insert", "update"],
|
||||
"errorCode": 462, # IngressRequestRateLimitExceeded
|
||||
"errorLabels": ["RetryableError", "SystemOverloadedError"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_mock_overload_error(times: int):
|
||||
error = mock_overload_error.copy()
|
||||
error["mode"] = {"times": times}
|
||||
return error
|
||||
|
||||
|
||||
class TestBackpressure(AsyncIntegrationTest):
|
||||
RUN_ON_LOAD_BALANCER = True
|
||||
|
||||
@async_client_context.require_failCommand_appName
|
||||
async def test_retry_overload_error_command(self):
|
||||
await self.db.t.insert_one({"x": 1})
|
||||
|
||||
# Ensure command is retried on overload error.
|
||||
fail_many = get_mock_overload_error(MAX_ADAPTIVE_RETRIES)
|
||||
async with self.fail_point(fail_many):
|
||||
await self.db.command("find", "t")
|
||||
|
||||
# Ensure command stops retrying after MAX_ADAPTIVE_RETRIES.
|
||||
fail_too_many = get_mock_overload_error(MAX_ADAPTIVE_RETRIES + 1)
|
||||
async with self.fail_point(fail_too_many):
|
||||
with self.assertRaises(PyMongoError) as error:
|
||||
await self.db.command("find", "t")
|
||||
|
||||
self.assertIn("RetryableError", str(error.exception))
|
||||
self.assertIn("SystemOverloadedError", str(error.exception))
|
||||
|
||||
@async_client_context.require_failCommand_appName
|
||||
async def test_retry_overload_error_find(self):
|
||||
await self.db.t.insert_one({"x": 1})
|
||||
|
||||
# Ensure command is retried on overload error.
|
||||
fail_many = get_mock_overload_error(MAX_ADAPTIVE_RETRIES)
|
||||
async with self.fail_point(fail_many):
|
||||
await self.db.t.find_one()
|
||||
|
||||
# Ensure command stops retrying after MAX_ADAPTIVE_RETRIES.
|
||||
fail_too_many = get_mock_overload_error(MAX_ADAPTIVE_RETRIES + 1)
|
||||
async with self.fail_point(fail_too_many):
|
||||
with self.assertRaises(PyMongoError) as error:
|
||||
await self.db.t.find_one()
|
||||
|
||||
self.assertIn("RetryableError", str(error.exception))
|
||||
self.assertIn("SystemOverloadedError", str(error.exception))
|
||||
|
||||
@async_client_context.require_failCommand_appName
|
||||
async def test_retry_overload_error_insert_one(self):
|
||||
# Ensure command is retried on overload error.
|
||||
fail_many = get_mock_overload_error(MAX_ADAPTIVE_RETRIES)
|
||||
async with self.fail_point(fail_many):
|
||||
await self.db.t.insert_one({"x": 1})
|
||||
|
||||
# Ensure command stops retrying after MAX_ADAPTIVE_RETRIES.
|
||||
fail_too_many = get_mock_overload_error(MAX_ADAPTIVE_RETRIES + 1)
|
||||
async with self.fail_point(fail_too_many):
|
||||
with self.assertRaises(PyMongoError) as error:
|
||||
await self.db.t.insert_one({"x": 1})
|
||||
|
||||
self.assertIn("RetryableError", str(error.exception))
|
||||
self.assertIn("SystemOverloadedError", str(error.exception))
|
||||
|
||||
@async_client_context.require_failCommand_appName
|
||||
async def test_retry_overload_error_update_many(self):
|
||||
# Even though update_many is not a retryable write operation, it will
|
||||
# still be retried via the "RetryableError" error label.
|
||||
await self.db.t.insert_one({"x": 1})
|
||||
|
||||
# Ensure command is retried on overload error.
|
||||
fail_many = get_mock_overload_error(MAX_ADAPTIVE_RETRIES)
|
||||
async with self.fail_point(fail_many):
|
||||
await self.db.t.update_many({}, {"$set": {"x": 2}})
|
||||
|
||||
# Ensure command stops retrying after MAX_ADAPTIVE_RETRIES.
|
||||
fail_too_many = get_mock_overload_error(MAX_ADAPTIVE_RETRIES + 1)
|
||||
async with self.fail_point(fail_too_many):
|
||||
with self.assertRaises(PyMongoError) as error:
|
||||
await self.db.t.update_many({}, {"$set": {"x": 2}})
|
||||
|
||||
self.assertIn("RetryableError", str(error.exception))
|
||||
self.assertIn("SystemOverloadedError", str(error.exception))
|
||||
|
||||
@async_client_context.require_failCommand_appName
|
||||
async def test_retry_overload_error_getMore(self):
|
||||
coll = self.db.t
|
||||
await coll.insert_many([{"x": 1} for _ in range(10)])
|
||||
|
||||
# Ensure command is retried on overload error.
|
||||
fail_many = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {"times": MAX_ADAPTIVE_RETRIES},
|
||||
"data": {
|
||||
"failCommands": ["getMore"],
|
||||
"errorCode": 462, # IngressRequestRateLimitExceeded
|
||||
"errorLabels": ["RetryableError", "SystemOverloadedError"],
|
||||
},
|
||||
}
|
||||
cursor = coll.find(batch_size=2)
|
||||
await cursor.next()
|
||||
async with self.fail_point(fail_many):
|
||||
await cursor.to_list()
|
||||
|
||||
# Ensure command stops retrying after MAX_ADAPTIVE_RETRIES.
|
||||
fail_too_many = fail_many.copy()
|
||||
fail_too_many["mode"] = {"times": MAX_ADAPTIVE_RETRIES + 1}
|
||||
cursor = coll.find(batch_size=2)
|
||||
await cursor.next()
|
||||
async with self.fail_point(fail_too_many):
|
||||
with self.assertRaises(PyMongoError) as error:
|
||||
await cursor.to_list()
|
||||
|
||||
self.assertIn("RetryableError", str(error.exception))
|
||||
self.assertIn("SystemOverloadedError", str(error.exception))
|
||||
|
||||
|
||||
# Prose tests.
|
||||
class AsyncTestClientBackpressure(AsyncIntegrationTest):
|
||||
listener: EventListener
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.listener = OvertCommandListener()
|
||||
|
||||
@async_client_context.require_connection
|
||||
async def asyncSetUp(self) -> None:
|
||||
await super().asyncSetUp()
|
||||
self.listener.reset()
|
||||
self.app_name = self.__class__.__name__.lower()
|
||||
self.client = await self.async_rs_or_single_client(
|
||||
event_listeners=[self.listener], appName=self.app_name
|
||||
)
|
||||
|
||||
@patch("random.random")
|
||||
@async_client_context.require_failCommand_appName
|
||||
async def test_01_operation_retry_uses_exponential_backoff(self, random_func):
|
||||
# Drivers should test that retries do not occur immediately when a SystemOverloadedError is encountered.
|
||||
|
||||
# 1. let `client` be a `MongoClient`
|
||||
client = self.client
|
||||
|
||||
# 2. let `collection` be a collection
|
||||
collection = client.test.test
|
||||
|
||||
# 3. Now, run transactions without backoff:
|
||||
|
||||
# a. Configure the random number generator used for jitter to always return `0` -- this effectively disables backoff.
|
||||
random_func.return_value = 0
|
||||
|
||||
# b. Configure the following failPoint:
|
||||
fail_point = dict(
|
||||
mode="alwaysOn",
|
||||
data=dict(
|
||||
failCommands=["insert"],
|
||||
errorCode=2,
|
||||
errorLabels=["SystemOverloadedError", "RetryableError"],
|
||||
appName=self.app_name,
|
||||
),
|
||||
)
|
||||
async with self.fail_point(fail_point):
|
||||
# c. Execute the following command. Expect that the command errors. Measure the duration of the command execution.
|
||||
start0 = perf_counter()
|
||||
with self.assertRaises(OperationFailure):
|
||||
await collection.insert_one({"a": 1})
|
||||
end0 = perf_counter()
|
||||
|
||||
# d. Configure the random number generator used for jitter to always return `1`.
|
||||
random_func.return_value = 1
|
||||
|
||||
# e. Execute step c again.
|
||||
start1 = perf_counter()
|
||||
with self.assertRaises(OperationFailure):
|
||||
await collection.insert_one({"a": 1})
|
||||
end1 = perf_counter()
|
||||
|
||||
# f. Compare the times between the two runs.
|
||||
# The sum of 2 backoffs is 0.3 seconds. There is a 0.3-second window to account for potential variance between the two
|
||||
# runs.
|
||||
self.assertTrue(abs((end1 - start1) - (end0 - start0 + 0.3)) < 0.3)
|
||||
|
||||
@async_client_context.require_failCommand_appName
|
||||
async def test_03_overload_retries_limited(self):
|
||||
# Drivers should test that overload errors are retried a maximum of two times.
|
||||
|
||||
# 1. Let `client` be a `MongoClient`.
|
||||
client = self.client
|
||||
# 2. Let `coll` be a collection.
|
||||
coll = client.pymongo_test.coll
|
||||
|
||||
# 3. Configure the following failpoint:
|
||||
failpoint = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": "alwaysOn",
|
||||
"data": {
|
||||
"failCommands": ["find"],
|
||||
"errorCode": 462, # IngressRequestRateLimitExceeded
|
||||
"errorLabels": ["RetryableError", "SystemOverloadedError"],
|
||||
},
|
||||
}
|
||||
|
||||
# 4. Perform a find operation with `coll` that fails.
|
||||
async with self.fail_point(failpoint):
|
||||
with self.assertRaises(PyMongoError) as error:
|
||||
await coll.find_one({})
|
||||
|
||||
# 5. Assert that the raised error contains both the `RetryableError` and `SystemOverloadedError` error labels.
|
||||
self.assertIn("RetryableError", str(error.exception))
|
||||
self.assertIn("SystemOverloadedError", str(error.exception))
|
||||
|
||||
# 6. Assert that the total number of started commands is MAX_ADAPTIVE_RETRIES + 1.
|
||||
self.assertEqual(len(self.listener.started_events), MAX_ADAPTIVE_RETRIES + 1)
|
||||
|
||||
@async_client_context.require_failCommand_appName
|
||||
async def test_04_overload_retries_limited_configured(self):
|
||||
# Drivers should test that overload errors are retried a maximum of maxAdaptiveRetries times.
|
||||
max_retries = 1
|
||||
|
||||
# 1. Let `client` be a `MongoClient` with `maxAdaptiveRetries=1` and command event monitoring enabled.
|
||||
client = await self.async_single_client(
|
||||
maxAdaptiveRetries=max_retries, event_listeners=[self.listener]
|
||||
)
|
||||
# 2. Let `coll` be a collection.
|
||||
coll = client.pymongo_test.coll
|
||||
|
||||
# 3. Configure the following failpoint:
|
||||
failpoint = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": "alwaysOn",
|
||||
"data": {
|
||||
"failCommands": ["find"],
|
||||
"errorCode": 462, # IngressRequestRateLimitExceeded
|
||||
"errorLabels": ["RetryableError", "SystemOverloadedError"],
|
||||
},
|
||||
}
|
||||
|
||||
# 4. Perform a find operation with `coll` that fails.
|
||||
async with self.fail_point(failpoint):
|
||||
with self.assertRaises(PyMongoError) as error:
|
||||
await coll.find_one({})
|
||||
|
||||
# 5. Assert that the raised error contains both the `RetryableError` and `SystemOverloadedError` error labels.
|
||||
self.assertIn("RetryableError", str(error.exception))
|
||||
self.assertIn("SystemOverloadedError", str(error.exception))
|
||||
|
||||
# 6. Assert that the total number of started commands is max_retries + 1.
|
||||
self.assertEqual(len(self.listener.started_events), max_retries + 1)
|
||||
|
||||
|
||||
# Location of JSON test specifications.
|
||||
if _IS_SYNC:
|
||||
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "client-backpressure")
|
||||
else:
|
||||
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "client-backpressure")
|
||||
|
||||
globals().update(
|
||||
generate_test_classes(
|
||||
_TEST_PATH,
|
||||
module=__name__,
|
||||
)
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -219,6 +219,19 @@ class TestClientMetadataProse(AsyncIntegrationTest):
|
||||
# add same metadata again
|
||||
await self.check_metadata_added(client, "Framework", None, None)
|
||||
|
||||
async def test_handshake_documents_include_backpressure(self):
|
||||
# Create a `MongoClient` that is configured to record all handshake documents sent to the server as a part of
|
||||
# connection establishment.
|
||||
client = await self.async_rs_or_single_client("mongodb://" + self.server.address_string)
|
||||
|
||||
# Send a `ping` command to the server and verify that the command succeeds. This ensure that a connection is
|
||||
# established on all topologies. Note: MockupDB only supports standalone servers.
|
||||
await client.admin.command("ping")
|
||||
|
||||
# Assert that for every handshake document intercepted:
|
||||
# the document has a field `backpressure` whose value is `true`.
|
||||
self.assertEqual(self.handshake_req["backpressure"], True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@ -257,7 +257,6 @@ class TestCollation(AsyncIntegrationTest):
|
||||
self.assertEqual(
|
||||
ja_collation.document["locale"], indexes["japanese_version"]["collation"]["locale"]
|
||||
)
|
||||
self.assertNotIn("collation", indexes["simple"])
|
||||
await self.db.test.drop_index("fieldname_1")
|
||||
indexes = await self.db.test.index_information()
|
||||
self.assertIn("japanese_version", indexes)
|
||||
|
||||
@ -25,8 +25,10 @@ from asyncio import StreamReader, StreamWriter
|
||||
from pathlib import Path
|
||||
from test.asynchronous.helpers import ConcurrentRunner
|
||||
from test.asynchronous.utils import flaky
|
||||
from test.utils_shared import delay
|
||||
|
||||
from pymongo.asynchronous.pool import AsyncConnection
|
||||
from pymongo.errors import ConnectionFailure
|
||||
from pymongo.operations import _Op
|
||||
from pymongo.server_selectors import writable_server_selector
|
||||
|
||||
@ -70,7 +72,12 @@ from pymongo.errors import (
|
||||
)
|
||||
from pymongo.hello import Hello, HelloCompat
|
||||
from pymongo.helpers_shared import _check_command_response, _check_write_command_response
|
||||
from pymongo.monitoring import ServerHeartbeatFailedEvent, ServerHeartbeatStartedEvent
|
||||
from pymongo.monitoring import (
|
||||
ConnectionCheckOutFailedEvent,
|
||||
PoolClearedEvent,
|
||||
ServerHeartbeatFailedEvent,
|
||||
ServerHeartbeatStartedEvent,
|
||||
)
|
||||
from pymongo.server_description import SERVER_TYPE, ServerDescription
|
||||
from pymongo.topology_description import TOPOLOGY_TYPE
|
||||
|
||||
@ -131,6 +138,9 @@ async def got_app_error(topology, app_error):
|
||||
raise AssertionError
|
||||
except (AutoReconnect, NotPrimaryError, OperationFailure) as e:
|
||||
if when == "beforeHandshakeCompletes":
|
||||
# The pool would have added the SystemOverloadedError in this case.
|
||||
if isinstance(e, AutoReconnect):
|
||||
e._add_error_label("SystemOverloadedError")
|
||||
completed_handshake = False
|
||||
elif when == "afterHandshakeCompletes":
|
||||
completed_handshake = True
|
||||
@ -439,6 +449,59 @@ class TestPoolManagement(AsyncIntegrationTest):
|
||||
AsyncConnection.close_conn = original_close
|
||||
|
||||
|
||||
class TestPoolBackpressure(AsyncIntegrationTest):
|
||||
@async_client_context.require_version_min(7, 0, 0)
|
||||
async def test_connection_pool_is_not_cleared(self):
|
||||
listener = CMAPListener()
|
||||
|
||||
# Create a client that listens to CMAP events, with maxConnecting=100.
|
||||
client = await self.async_rs_or_single_client(maxConnecting=100, event_listeners=[listener])
|
||||
|
||||
# Enable the ingress rate limiter.
|
||||
await client.admin.command(
|
||||
"setParameter", 1, ingressConnectionEstablishmentRateLimiterEnabled=True
|
||||
)
|
||||
await client.admin.command("setParameter", 1, ingressConnectionEstablishmentRatePerSec=20)
|
||||
await client.admin.command(
|
||||
"setParameter", 1, ingressConnectionEstablishmentBurstCapacitySecs=1
|
||||
)
|
||||
await client.admin.command("setParameter", 1, ingressConnectionEstablishmentMaxQueueDepth=1)
|
||||
|
||||
# Disable the ingress rate limiter on teardown.
|
||||
# Sleep for 1 second before disabling to avoid the rate limiter.
|
||||
async def teardown():
|
||||
await asyncio.sleep(1)
|
||||
await client.admin.command(
|
||||
"setParameter", 1, ingressConnectionEstablishmentRateLimiterEnabled=False
|
||||
)
|
||||
|
||||
self.addAsyncCleanup(teardown)
|
||||
|
||||
# Make sure the collection has at least one document.
|
||||
await client.test.test.delete_many({})
|
||||
await client.test.test.insert_one({})
|
||||
|
||||
# Run a slow operation to tie up the connection.
|
||||
async def target():
|
||||
try:
|
||||
await client.test.test.find_one({"$where": delay(0.1)})
|
||||
except ConnectionFailure:
|
||||
pass
|
||||
|
||||
# Run 100 parallel operations that contend for connections.
|
||||
tasks = []
|
||||
for _ in range(100):
|
||||
tasks.append(ConcurrentRunner(target=target))
|
||||
for t in tasks:
|
||||
await t.start()
|
||||
for t in tasks:
|
||||
await t.join()
|
||||
|
||||
# Verify there were at least 10 connection checkout failed event but no pool cleared events.
|
||||
self.assertGreater(len(listener.events_by_type(ConnectionCheckOutFailedEvent)), 10)
|
||||
self.assertEqual(len(listener.events_by_type(PoolClearedEvent)), 0)
|
||||
|
||||
|
||||
class TestServerMonitoringMode(AsyncIntegrationTest):
|
||||
@async_client_context.require_no_load_balancer
|
||||
async def asyncSetUp(self):
|
||||
|
||||
@ -3322,6 +3322,7 @@ class TestAutomaticDecryptionKeys(AsyncEncryptionIntegrationTest):
|
||||
class TestExplicitTextEncryptionProse(AsyncEncryptionIntegrationTest):
|
||||
@async_client_context.require_no_standalone
|
||||
@async_client_context.require_version_min(8, 2, -1)
|
||||
@async_client_context.require_version_max(8, 99, 99)
|
||||
@async_client_context.require_libmongocrypt_min(1, 15, 1)
|
||||
@async_client_context.require_pymongocrypt_min(1, 16, 0)
|
||||
async def asyncSetUp(self):
|
||||
|
||||
@ -513,6 +513,39 @@ class TestPooling(_TestPoolingBase):
|
||||
str(error.exception),
|
||||
)
|
||||
|
||||
@async_client_context.require_failCommand_appName
|
||||
async def test_pool_backpressure_preserves_existing_connections(self):
|
||||
client = await self.async_rs_or_single_client()
|
||||
coll = client.pymongo_test.t
|
||||
pool = await async_get_pool(client)
|
||||
await coll.insert_many([{"x": 1} for _ in range(10)])
|
||||
t = SocketGetter(self.c, pool)
|
||||
await t.start()
|
||||
while t.state != "connection":
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert not t.sock.conn_closed()
|
||||
|
||||
# Mock a session establishment overload.
|
||||
mock_connection_fail = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {"times": 1},
|
||||
"data": {
|
||||
"closeConnection": True,
|
||||
},
|
||||
}
|
||||
|
||||
async with self.fail_point(mock_connection_fail):
|
||||
await coll.find_one({})
|
||||
|
||||
# Make sure the existing socket was not affected.
|
||||
assert not t.sock.conn_closed()
|
||||
|
||||
# Cleanup
|
||||
await t.release_conn()
|
||||
await t.join()
|
||||
await pool.close()
|
||||
|
||||
|
||||
class TestPoolMaxSize(_TestPoolingBase):
|
||||
async def test_max_pool_size(self):
|
||||
|
||||
@ -19,9 +19,12 @@ import os
|
||||
import pprint
|
||||
import sys
|
||||
import threading
|
||||
from test.asynchronous.utils import async_set_fail_point
|
||||
from test.asynchronous.utils import async_ensure_all_connected, async_set_fail_point
|
||||
from unittest import mock
|
||||
|
||||
from pymongo.errors import OperationFailure
|
||||
from pymongo import MongoClient
|
||||
from pymongo.common import MAX_ADAPTIVE_RETRIES
|
||||
from pymongo.errors import OperationFailure, PyMongoError
|
||||
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
@ -38,6 +41,7 @@ from test.utils_shared import (
|
||||
)
|
||||
|
||||
from pymongo.monitoring import (
|
||||
CommandFailedEvent,
|
||||
ConnectionCheckedOutEvent,
|
||||
ConnectionCheckOutFailedEvent,
|
||||
ConnectionCheckOutFailedReason,
|
||||
@ -145,6 +149,19 @@ class TestPoolPausedError(AsyncIntegrationTest):
|
||||
|
||||
|
||||
class TestRetryableReads(AsyncIntegrationTest):
|
||||
async def asyncSetUp(self) -> None:
|
||||
await super().asyncSetUp()
|
||||
self.setup_client = MongoClient(**async_client_context.client_options)
|
||||
self.addCleanup(self.setup_client.close)
|
||||
|
||||
# TODO: After PYTHON-4595 we can use async event handlers and remove this workaround.
|
||||
def configure_fail_point_sync(self, command_args, off=False) -> None:
|
||||
cmd = {"configureFailPoint": "failCommand", **command_args}
|
||||
if off:
|
||||
cmd["mode"] = "off"
|
||||
cmd.pop("data", None)
|
||||
self.setup_client.admin.command(cmd)
|
||||
|
||||
@async_client_context.require_multiple_mongoses
|
||||
@async_client_context.require_failCommand_fail_point
|
||||
async def test_retryable_reads_are_retried_on_a_different_mongos_when_one_is_available(self):
|
||||
@ -261,6 +278,248 @@ class TestRetryableReads(AsyncIntegrationTest):
|
||||
self.assertEqual(command_docs[0]["lsid"], command_docs[1]["lsid"])
|
||||
self.assertIsNot(command_docs[0], command_docs[1])
|
||||
|
||||
@async_client_context.require_replica_set
|
||||
@async_client_context.require_secondaries_count(1)
|
||||
@async_client_context.require_failCommand_fail_point
|
||||
@async_client_context.require_version_min(4, 4, 0)
|
||||
async def test_03_01_retryable_reads_caused_by_overload_errors_are_retried_on_a_different_replicaset_server_when_one_is_available_and_overload_retargeting_is_enabled(
|
||||
self
|
||||
):
|
||||
listener = OvertCommandListener()
|
||||
|
||||
# 1. Create a client `client` with `retryReads=true`, `readPreference=primaryPreferred`, `enableOverloadRetargeting=True`, and command event monitoring enabled.
|
||||
client = await self.async_rs_or_single_client(
|
||||
event_listeners=[listener],
|
||||
retryReads=True,
|
||||
readPreference="primaryPreferred",
|
||||
enableOverloadRetargeting=True,
|
||||
)
|
||||
|
||||
# Ensure the client has discovered all nodes.
|
||||
await async_ensure_all_connected(client)
|
||||
|
||||
# 2. Configure a fail point with the RetryableError and SystemOverloadedError error labels.
|
||||
command_args = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {"times": 1},
|
||||
"data": {
|
||||
"failCommands": ["find"],
|
||||
"errorLabels": ["RetryableError", "SystemOverloadedError"],
|
||||
"errorCode": 6,
|
||||
},
|
||||
}
|
||||
await async_set_fail_point(client, command_args)
|
||||
|
||||
# 3. Reset the command event monitor to clear the fail point command from its stored events.
|
||||
listener.reset()
|
||||
|
||||
# 4. Execute a `find` command with `client`.
|
||||
await client.t.t.find_one({})
|
||||
|
||||
# 5. Assert that one failed command event and one successful command event occurred.
|
||||
self.assertEqual(len(listener.failed_events), 1)
|
||||
self.assertEqual(len(listener.succeeded_events), 1)
|
||||
|
||||
# 6. Assert that both events occurred on different servers.
|
||||
assert listener.failed_events[0].connection_id != listener.succeeded_events[0].connection_id
|
||||
|
||||
@async_client_context.require_replica_set
|
||||
@async_client_context.require_secondaries_count(1)
|
||||
@async_client_context.require_failCommand_fail_point
|
||||
@async_client_context.require_version_min(4, 4, 0)
|
||||
async def test_03_02_retryable_reads_caused_by_non_overload_errors_are_retried_on_the_same_replicaset_server(
|
||||
self
|
||||
):
|
||||
listener = OvertCommandListener()
|
||||
|
||||
# 1. Create a client `client` with `retryReads=true`, `readPreference=primaryPreferred`, and command event monitoring enabled.
|
||||
client = await self.async_rs_or_single_client(
|
||||
event_listeners=[listener], retryReads=True, readPreference="primaryPreferred"
|
||||
)
|
||||
|
||||
# Ensure the client has discovered all nodes.
|
||||
await async_ensure_all_connected(client)
|
||||
|
||||
# 2. Configure a fail point with the RetryableError error label.
|
||||
command_args = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {"times": 1},
|
||||
"data": {
|
||||
"failCommands": ["find"],
|
||||
"errorLabels": ["RetryableError"],
|
||||
"errorCode": 6,
|
||||
},
|
||||
}
|
||||
await async_set_fail_point(client, command_args)
|
||||
|
||||
# 3. Reset the command event monitor to clear the fail point command from its stored events.
|
||||
listener.reset()
|
||||
|
||||
# 4. Execute a `find` command with `client`.
|
||||
await client.t.t.find_one({})
|
||||
|
||||
# 5. Assert that one failed command event and one successful command event occurred.
|
||||
self.assertEqual(len(listener.failed_events), 1)
|
||||
self.assertEqual(len(listener.succeeded_events), 1)
|
||||
|
||||
# 6. Assert that both events occurred the same server.
|
||||
assert listener.failed_events[0].connection_id == listener.succeeded_events[0].connection_id
|
||||
|
||||
@async_client_context.require_replica_set
|
||||
@async_client_context.require_secondaries_count(1)
|
||||
@async_client_context.require_failCommand_fail_point
|
||||
@async_client_context.require_version_min(4, 4, 0)
|
||||
async def test_03_03_retryable_reads_caused_by_overload_errors_are_retried_on_the_same_replicaset_server_when_one_is_available_and_overload_retargeting_is_disabled(
|
||||
self
|
||||
):
|
||||
listener = OvertCommandListener()
|
||||
|
||||
# 1. Create a client `client` with `retryReads=true`, `readPreference=primaryPreferred`, and command event monitoring enabled.
|
||||
client = await self.async_rs_or_single_client(
|
||||
event_listeners=[listener],
|
||||
retryReads=True,
|
||||
readPreference="primaryPreferred",
|
||||
)
|
||||
|
||||
# Ensure the client has discovered all nodes.
|
||||
await async_ensure_all_connected(client)
|
||||
|
||||
# 2. Configure a fail point with the RetryableError and SystemOverloadedError error labels.
|
||||
command_args = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {"times": 1},
|
||||
"data": {
|
||||
"failCommands": ["find"],
|
||||
"errorLabels": ["RetryableError", "SystemOverloadedError"],
|
||||
"errorCode": 6,
|
||||
},
|
||||
}
|
||||
await async_set_fail_point(client, command_args)
|
||||
|
||||
# 3. Reset the command event monitor to clear the fail point command from its stored events.
|
||||
listener.reset()
|
||||
|
||||
# 4. Execute a `find` command with `client`.
|
||||
await client.t.t.find_one({})
|
||||
|
||||
# 5. Assert that one failed command event and one successful command event occurred.
|
||||
self.assertEqual(len(listener.failed_events), 1)
|
||||
self.assertEqual(len(listener.succeeded_events), 1)
|
||||
|
||||
# 6. Assert that both events occurred on the same server.
|
||||
assert listener.failed_events[0].connection_id == listener.succeeded_events[0].connection_id
|
||||
|
||||
@async_client_context.require_failCommand_fail_point
|
||||
@async_client_context.require_version_min(4, 4, 0) # type:ignore[untyped-decorator]
|
||||
async def test_overload_then_nonoverload_retries_increased_reads(self) -> None:
|
||||
# Create a client.
|
||||
listener = OvertCommandListener()
|
||||
|
||||
# Configure the client to listen to CommandFailedEvents. In the attached listener, configure a fail point with error
|
||||
# code `91` (ShutdownInProgress) and `RetryableError` and `SystemOverloadedError` labels.
|
||||
overload_fail_point = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {"times": 1},
|
||||
"data": {
|
||||
"failCommands": ["find"],
|
||||
"errorLabels": ["RetryableError", "SystemOverloadedError"],
|
||||
"errorCode": 91,
|
||||
},
|
||||
}
|
||||
|
||||
# Configure a fail point with error code `91` (ShutdownInProgress) with only the `RetryableError` error label.
|
||||
non_overload_fail_point = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": "alwaysOn",
|
||||
"data": {
|
||||
"failCommands": ["find"],
|
||||
"errorCode": 91,
|
||||
"errorLabels": ["RetryableError"],
|
||||
},
|
||||
}
|
||||
|
||||
def failed(event: CommandFailedEvent) -> None:
|
||||
# Configure the fail point command only if the failed event is for the 91 error configured in step 2.
|
||||
if listener.failed_events:
|
||||
return
|
||||
assert event.failure["code"] == 91
|
||||
self.configure_fail_point_sync(non_overload_fail_point)
|
||||
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
|
||||
listener.failed_events.append(event)
|
||||
|
||||
listener.failed = failed
|
||||
|
||||
client = await self.async_rs_client(event_listeners=[listener])
|
||||
await client.test.test.insert_one({})
|
||||
|
||||
self.configure_fail_point_sync(overload_fail_point)
|
||||
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
|
||||
|
||||
with self.assertRaises(PyMongoError):
|
||||
await client.test.test.find_one()
|
||||
|
||||
started_finds = [e for e in listener.started_events if e.command_name == "find"]
|
||||
self.assertEqual(len(started_finds), MAX_ADAPTIVE_RETRIES + 1)
|
||||
|
||||
@async_client_context.require_failCommand_fail_point
|
||||
@async_client_context.require_version_min(4, 4, 0) # type:ignore[untyped-decorator]
|
||||
async def test_backoff_is_not_applied_for_non_overload_errors(self):
|
||||
if _IS_SYNC:
|
||||
mock_target = "pymongo.synchronous.helpers._RetryPolicy.backoff"
|
||||
else:
|
||||
mock_target = "pymongo.asynchronous.helpers._RetryPolicy.backoff"
|
||||
|
||||
# Create a client.
|
||||
listener = OvertCommandListener()
|
||||
|
||||
# Configure the client to listen to CommandFailedEvents. In the attached listener, configure a fail point with error
|
||||
# code `91` (ShutdownInProgress) and `RetryableError` and `SystemOverloadedError` labels.
|
||||
overload_fail_point = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {"times": 1},
|
||||
"data": {
|
||||
"failCommands": ["find"],
|
||||
"errorLabels": ["RetryableError", "SystemOverloadedError"],
|
||||
"errorCode": 91,
|
||||
},
|
||||
}
|
||||
|
||||
# Configure a fail point with error code `91` (ShutdownInProgress) with only the `RetryableError` error label.
|
||||
non_overload_fail_point = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": "alwaysOn",
|
||||
"data": {
|
||||
"failCommands": ["find"],
|
||||
"errorCode": 91,
|
||||
"errorLabels": ["RetryableError"],
|
||||
},
|
||||
}
|
||||
|
||||
def failed(event: CommandFailedEvent) -> None:
|
||||
# Configure the fail point command only if the failed event is for the 91 error configured in step 2.
|
||||
if listener.failed_events:
|
||||
return
|
||||
assert event.failure["code"] == 91
|
||||
self.configure_fail_point_sync(non_overload_fail_point)
|
||||
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
|
||||
listener.failed_events.append(event)
|
||||
|
||||
listener.failed = failed
|
||||
|
||||
client = await self.async_rs_client(event_listeners=[listener])
|
||||
await client.test.test.insert_one({})
|
||||
|
||||
self.configure_fail_point_sync(overload_fail_point)
|
||||
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
|
||||
|
||||
# Perform a findOne operation with coll. Expect the operation to fail.
|
||||
with mock.patch(mock_target, return_value=0) as mock_backoff:
|
||||
with self.assertRaises(PyMongoError):
|
||||
await client.test.test.find_one()
|
||||
|
||||
# Assert that backoff was applied only once for the initial overload error and not for the subsequent non-overload retryable errors.
|
||||
self.assertEqual(mock_backoff.call_count, 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@ -21,6 +21,9 @@ import pprint
|
||||
import sys
|
||||
import threading
|
||||
from test.asynchronous.utils import async_set_fail_point, flaky
|
||||
from unittest import mock
|
||||
|
||||
from pymongo.common import MAX_ADAPTIVE_RETRIES
|
||||
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
@ -43,14 +46,17 @@ from bson.codec_options import DEFAULT_CODEC_OPTIONS
|
||||
from bson.int64 import Int64
|
||||
from bson.raw_bson import RawBSONDocument
|
||||
from bson.son import SON
|
||||
from pymongo import MongoClient
|
||||
from pymongo.errors import (
|
||||
AutoReconnect,
|
||||
ConnectionFailure,
|
||||
OperationFailure,
|
||||
NotPrimaryError,
|
||||
PyMongoError,
|
||||
ServerSelectionTimeoutError,
|
||||
WriteConcernError,
|
||||
)
|
||||
from pymongo.monitoring import (
|
||||
CommandFailedEvent,
|
||||
CommandSucceededEvent,
|
||||
ConnectionCheckedOutEvent,
|
||||
ConnectionCheckOutFailedEvent,
|
||||
@ -601,5 +607,291 @@ class TestRetryableWritesTxnNumber(IgnoreDeprecationsTest):
|
||||
self.assertEqual(sent_txn_id, final_txn_id, msg)
|
||||
|
||||
|
||||
class TestErrorPropagationAfterEncounteringMultipleErrors(AsyncIntegrationTest):
|
||||
# Only run against replica sets as mongos does not propagate the NoWritesPerformed label to the drivers.
|
||||
@async_client_context.require_replica_set
|
||||
# Run against server versions 6.0 and above.
|
||||
@async_client_context.require_version_min(6, 0) # type: ignore[untyped-decorator]
|
||||
async def asyncSetUp(self) -> None:
|
||||
await super().asyncSetUp()
|
||||
self.setup_client = MongoClient(**async_client_context.default_client_options)
|
||||
self.addCleanup(self.setup_client.close)
|
||||
|
||||
# TODO: After PYTHON-4595 we can use async event handlers and remove this workaround.
|
||||
def configure_fail_point_sync(self, command_args, off=False) -> None:
|
||||
cmd = {"configureFailPoint": "failCommand"}
|
||||
cmd.update(command_args)
|
||||
if off:
|
||||
cmd["mode"] = "off"
|
||||
cmd.pop("data", None)
|
||||
self.setup_client.admin.command(cmd)
|
||||
|
||||
async def test_01_drivers_return_the_correct_error_when_receiving_only_errors_without_NoWritesPerformed(
|
||||
self
|
||||
) -> None:
|
||||
# Create a client with retryWrites=true.
|
||||
listener = OvertCommandListener()
|
||||
|
||||
# Configure a fail point with error code 91 (ShutdownInProgress) with the RetryableError and SystemOverloadedError error labels.
|
||||
command_args = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {"times": 1},
|
||||
"data": {
|
||||
"failCommands": ["insert"],
|
||||
"errorLabels": ["RetryableError", "SystemOverloadedError"],
|
||||
"errorCode": 91,
|
||||
},
|
||||
}
|
||||
|
||||
# Via the command monitoring CommandFailedEvent, configure a fail point with error code 10107 (NotWritablePrimary).
|
||||
command_args_inner = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": "alwaysOn",
|
||||
"data": {
|
||||
"failCommands": ["insert"],
|
||||
"errorCode": 10107,
|
||||
"errorLabels": ["RetryableError", "SystemOverloadedError"],
|
||||
},
|
||||
}
|
||||
|
||||
def failed(event: CommandFailedEvent) -> None:
|
||||
# Configure the 10107 fail point command only if the the failed event is for the 91 error configured in step 2.
|
||||
if listener.failed_events:
|
||||
return
|
||||
assert event.failure["code"] == 91
|
||||
self.configure_fail_point_sync(command_args_inner)
|
||||
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
|
||||
listener.failed_events.append(event)
|
||||
|
||||
listener.failed = failed
|
||||
|
||||
client = await self.async_rs_client(retryWrites=True, event_listeners=[listener])
|
||||
|
||||
self.configure_fail_point_sync(command_args)
|
||||
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
|
||||
|
||||
# Attempt an insertOne operation on any record for any database and collection.
|
||||
# Expect the insertOne to fail with a server error.
|
||||
with self.assertRaises(NotPrimaryError) as exc:
|
||||
await client.test.test.insert_one({})
|
||||
|
||||
# Assert that the error code of the server error is 10107.
|
||||
assert exc.exception.errors["code"] == 10107 # type:ignore[call-overload]
|
||||
|
||||
async def test_02_drivers_return_the_correct_error_when_receiving_only_errors_with_NoWritesPerformed(
|
||||
self
|
||||
) -> None:
|
||||
# Create a client with retryWrites=true.
|
||||
listener = OvertCommandListener()
|
||||
|
||||
# Configure a fail point with error code 91 (ShutdownInProgress) with the RetryableError and SystemOverloadedError error labels.
|
||||
command_args = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {"times": 1},
|
||||
"data": {
|
||||
"failCommands": ["insert"],
|
||||
"errorLabels": ["RetryableError", "SystemOverloadedError", "NoWritesPerformed"],
|
||||
"errorCode": 91,
|
||||
},
|
||||
}
|
||||
|
||||
# Via the command monitoring CommandFailedEvent, configure a fail point with error code `10107` (NotWritablePrimary)
|
||||
# and a NoWritesPerformed label.
|
||||
command_args_inner = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": "alwaysOn",
|
||||
"data": {
|
||||
"failCommands": ["insert"],
|
||||
"errorCode": 10107,
|
||||
"errorLabels": ["RetryableError", "SystemOverloadedError", "NoWritesPerformed"],
|
||||
},
|
||||
}
|
||||
|
||||
def failed(event: CommandFailedEvent) -> None:
|
||||
if listener.failed_events:
|
||||
return
|
||||
# Configure the 10107 fail point command only if the the failed event is for the 91 error configured in step 2.
|
||||
assert event.failure["code"] == 91
|
||||
self.configure_fail_point_sync(command_args_inner)
|
||||
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
|
||||
listener.failed_events.append(event)
|
||||
|
||||
listener.failed = failed
|
||||
|
||||
client = await self.async_rs_client(retryWrites=True, event_listeners=[listener])
|
||||
|
||||
self.configure_fail_point_sync(command_args)
|
||||
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
|
||||
|
||||
# Attempt an insertOne operation on any record for any database and collection.
|
||||
# Expect the insertOne to fail with a server error.
|
||||
with self.assertRaises(NotPrimaryError) as exc:
|
||||
await client.test.test.insert_one({})
|
||||
|
||||
# Assert that the error code of the server error is 91.
|
||||
assert exc.exception.errors["code"] == 91 # type:ignore[call-overload]
|
||||
|
||||
async def test_03_drivers_return_the_correct_error_when_receiving_some_errors_with_NoWritesPerformed_and_some_without_NoWritesPerformed(
|
||||
self
|
||||
) -> None:
|
||||
# Create a client with retryWrites=true.
|
||||
listener = OvertCommandListener()
|
||||
|
||||
# Configure the client to listen to CommandFailedEvents. In the attached listener, configure a fail point with error
|
||||
# code `91` (NotWritablePrimary) and the `NoWritesPerformed`, `RetryableError` and `SystemOverloadedError` labels.
|
||||
command_args_inner = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": "alwaysOn",
|
||||
"data": {
|
||||
"failCommands": ["insert"],
|
||||
"errorLabels": ["RetryableError", "SystemOverloadedError", "NoWritesPerformed"],
|
||||
"errorCode": 91,
|
||||
},
|
||||
}
|
||||
|
||||
# Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and
|
||||
# `SystemOverloadedError` error labels but without the `NoWritesPerformed` error label.
|
||||
command_args = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {"times": 1},
|
||||
"data": {
|
||||
"failCommands": ["insert"],
|
||||
"errorCode": 91,
|
||||
"errorLabels": ["RetryableError", "SystemOverloadedError"],
|
||||
},
|
||||
}
|
||||
|
||||
def failed(event: CommandFailedEvent) -> None:
|
||||
# Configure the fail point command only if the failed event is for the 91 error configured in step 2.
|
||||
if listener.failed_events:
|
||||
return
|
||||
assert event.failure["code"] == 91
|
||||
self.configure_fail_point_sync(command_args_inner)
|
||||
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
|
||||
listener.failed_events.append(event)
|
||||
|
||||
listener.failed = failed
|
||||
|
||||
client = await self.async_rs_client(retryWrites=True, event_listeners=[listener])
|
||||
|
||||
self.configure_fail_point_sync(command_args)
|
||||
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
|
||||
|
||||
# Attempt an insertOne operation on any record for any database and collection.
|
||||
# Expect the insertOne to fail with a server error.
|
||||
with self.assertRaises(PyMongoError) as exc:
|
||||
await client.test.test.insert_one({})
|
||||
|
||||
# Assert that the error code of the server error is 91.
|
||||
assert exc.exception.errors["code"] == 91
|
||||
# Assert that the error does not contain the error label `NoWritesPerformed`.
|
||||
assert "NoWritesPerformed" not in exc.exception.errors["errorLabels"]
|
||||
|
||||
async def test_overload_then_nonoverload_retries_increased_writes(self) -> None:
|
||||
# Create a client with retryWrites=true.
|
||||
listener = OvertCommandListener()
|
||||
|
||||
# Configure the client to listen to CommandFailedEvents. In the attached listener, configure a fail point with error
|
||||
# code `91` (ShutdownInProgress) and `RetryableError` and `SystemOverloadedError` labels.
|
||||
overload_fail_point = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {"times": 1},
|
||||
"data": {
|
||||
"failCommands": ["insert"],
|
||||
"errorLabels": ["RetryableError", "SystemOverloadedError"],
|
||||
"errorCode": 91,
|
||||
},
|
||||
}
|
||||
|
||||
# Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and `RetryableWriteError` error labels.
|
||||
non_overload_fail_point = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": "alwaysOn",
|
||||
"data": {
|
||||
"failCommands": ["insert"],
|
||||
"errorCode": 91,
|
||||
"errorLabels": ["RetryableError", "RetryableWriteError"],
|
||||
},
|
||||
}
|
||||
|
||||
def failed(event: CommandFailedEvent) -> None:
|
||||
# Configure the fail point command only if the failed event is for the 91 error configured in step 2.
|
||||
if listener.failed_events:
|
||||
return
|
||||
assert event.failure["code"] == 91
|
||||
self.configure_fail_point_sync(non_overload_fail_point)
|
||||
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
|
||||
listener.failed_events.append(event)
|
||||
|
||||
listener.failed = failed
|
||||
|
||||
client = await self.async_rs_client(retryWrites=True, event_listeners=[listener])
|
||||
|
||||
self.configure_fail_point_sync(overload_fail_point)
|
||||
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
|
||||
|
||||
with self.assertRaises(PyMongoError):
|
||||
await client.test.test.insert_one({"x": 1})
|
||||
|
||||
started_inserts = [e for e in listener.started_events if e.command_name == "insert"]
|
||||
self.assertEqual(len(started_inserts), MAX_ADAPTIVE_RETRIES + 1)
|
||||
|
||||
async def test_backoff_is_not_applied_for_non_overload_errors(self):
|
||||
if _IS_SYNC:
|
||||
mock_target = "pymongo.synchronous.helpers._RetryPolicy.backoff"
|
||||
else:
|
||||
mock_target = "pymongo.asynchronous.helpers._RetryPolicy.backoff"
|
||||
|
||||
# Create a client.
|
||||
listener = OvertCommandListener()
|
||||
|
||||
# Configure the client to listen to CommandFailedEvents. In the attached listener, configure a fail point with error
|
||||
# code `91` (ShutdownInProgress) and `RetryableError` and `SystemOverloadedError` labels.
|
||||
overload_fail_point = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {"times": 1},
|
||||
"data": {
|
||||
"failCommands": ["insert"],
|
||||
"errorLabels": ["RetryableError", "SystemOverloadedError"],
|
||||
"errorCode": 91,
|
||||
},
|
||||
}
|
||||
|
||||
# Configure a fail point with error code `91` (ShutdownInProgress) with only the `RetryableError` error label.
|
||||
non_overload_fail_point = {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": "alwaysOn",
|
||||
"data": {
|
||||
"failCommands": ["insert"],
|
||||
"errorCode": 91,
|
||||
"errorLabels": ["RetryableError", "RetryableWriteError"],
|
||||
},
|
||||
}
|
||||
|
||||
def failed(event: CommandFailedEvent) -> None:
|
||||
# Configure the fail point command only if the failed event is for the 91 error configured in step 2.
|
||||
if listener.failed_events:
|
||||
return
|
||||
assert event.failure["code"] == 91
|
||||
self.configure_fail_point_sync(non_overload_fail_point)
|
||||
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
|
||||
listener.failed_events.append(event)
|
||||
|
||||
listener.failed = failed
|
||||
|
||||
client = await self.async_rs_client(event_listeners=[listener])
|
||||
|
||||
self.configure_fail_point_sync(overload_fail_point)
|
||||
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
|
||||
|
||||
# Perform a findOne operation with coll. Expect the operation to fail.
|
||||
with mock.patch(mock_target, return_value=0) as mock_backoff:
|
||||
with self.assertRaises(PyMongoError):
|
||||
await client.test.test.insert_one({})
|
||||
|
||||
# Assert that backoff was applied only once for the initial overload error and not for the subsequent non-overload retryable errors.
|
||||
self.assertEqual(mock_backoff.call_count, 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
"""Test the client_session module."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import sys
|
||||
import time
|
||||
@ -24,8 +23,6 @@ from io import BytesIO
|
||||
from test.asynchronous.helpers import ExceptionCatchingTask
|
||||
from typing import Any, Callable, List, Set, Tuple
|
||||
|
||||
from pymongo.synchronous.mongo_client import MongoClient
|
||||
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
from test.asynchronous import (
|
||||
@ -45,7 +42,7 @@ from test.utils_shared import (
|
||||
|
||||
from bson import DBRef
|
||||
from gridfs.asynchronous.grid_file import AsyncGridFS, AsyncGridFSBucket
|
||||
from pymongo import ASCENDING, AsyncMongoClient, _csot, monitoring
|
||||
from pymongo import ASCENDING, AsyncMongoClient, monitoring
|
||||
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
|
||||
from pymongo.asynchronous.cursor import AsyncCursor
|
||||
from pymongo.asynchronous.helpers import anext
|
||||
@ -189,6 +186,52 @@ class TestSession(AsyncIntegrationTest):
|
||||
f"{f.__name__} did not return implicit session to pool",
|
||||
)
|
||||
|
||||
# Explicit bound session
|
||||
for f, args, kw in ops:
|
||||
async with client.start_session() as s:
|
||||
async with s.bind():
|
||||
listener.reset()
|
||||
s._materialize()
|
||||
last_use = s._server_session.last_use
|
||||
start = time.monotonic()
|
||||
self.assertLessEqual(last_use, start)
|
||||
# In case "f" modifies its inputs.
|
||||
args = copy.copy(args)
|
||||
kw = copy.copy(kw)
|
||||
await f(*args, **kw)
|
||||
self.assertGreaterEqual(len(listener.started_events), 1)
|
||||
for event in listener.started_events:
|
||||
self.assertIn(
|
||||
"lsid",
|
||||
event.command,
|
||||
f"{f.__name__} sent no lsid with {event.command_name}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
s.session_id,
|
||||
event.command["lsid"],
|
||||
f"{f.__name__} sent wrong lsid with {event.command_name}",
|
||||
)
|
||||
|
||||
self.assertFalse(s.has_ended)
|
||||
|
||||
self.assertTrue(s.has_ended)
|
||||
with self.assertRaisesRegex(InvalidOperation, "ended session"):
|
||||
async with s.bind():
|
||||
await f(*args, **kw)
|
||||
|
||||
# Test a session cannot be used on another client.
|
||||
async with self.client2.start_session() as s:
|
||||
async with s.bind():
|
||||
# In case "f" modifies its inputs.
|
||||
args = copy.copy(args)
|
||||
kw = copy.copy(kw)
|
||||
with self.assertRaisesRegex(
|
||||
InvalidOperation,
|
||||
"Only the client that created the bound session can perform operations within its context block",
|
||||
):
|
||||
await f(*args, **kw)
|
||||
|
||||
async def test_implicit_sessions_checkout(self):
|
||||
# "To confirm that implicit sessions only allocate their server session after a
|
||||
# successful connection checkout" test from Driver Sessions Spec.
|
||||
@ -825,6 +868,106 @@ class TestSession(AsyncIntegrationTest):
|
||||
async with client.start_session() as s:
|
||||
self.assertRaises(TypeError, lambda: copy.copy(s))
|
||||
|
||||
async def test_nested_session_binding(self):
|
||||
coll = self.client.pymongo_test.test
|
||||
await coll.insert_one({"x": 1})
|
||||
|
||||
session1 = self.client.start_session()
|
||||
session2 = self.client.start_session()
|
||||
session1._materialize()
|
||||
session2._materialize()
|
||||
try:
|
||||
self.listener.reset()
|
||||
# Uses implicit session
|
||||
await coll.find_one()
|
||||
implicit_lsid = self.listener.started_events[0].command.get("lsid")
|
||||
self.assertIsNotNone(implicit_lsid)
|
||||
self.assertNotEqual(implicit_lsid, session1.session_id)
|
||||
self.assertNotEqual(implicit_lsid, session2.session_id)
|
||||
|
||||
async with session1.bind(end_session=False):
|
||||
self.listener.reset()
|
||||
# Uses bound session1
|
||||
await coll.find_one()
|
||||
session1_lsid = self.listener.started_events[0].command.get("lsid")
|
||||
self.assertEqual(session1_lsid, session1.session_id)
|
||||
|
||||
async with session2.bind(end_session=False):
|
||||
self.listener.reset()
|
||||
# Uses bound session2
|
||||
await coll.find_one()
|
||||
session2_lsid = self.listener.started_events[0].command.get("lsid")
|
||||
self.assertEqual(session2_lsid, session2.session_id)
|
||||
self.assertNotEqual(session2_lsid, session1.session_id)
|
||||
|
||||
self.listener.reset()
|
||||
# Use bound session1 again
|
||||
await coll.find_one()
|
||||
session1_lsid = self.listener.started_events[0].command.get("lsid")
|
||||
self.assertEqual(session1_lsid, session1.session_id)
|
||||
self.assertNotEqual(session1_lsid, session2.session_id)
|
||||
|
||||
self.listener.reset()
|
||||
# Uses implicit session
|
||||
await coll.find_one()
|
||||
implicit_lsid = self.listener.started_events[0].command.get("lsid")
|
||||
self.assertIsNotNone(implicit_lsid)
|
||||
self.assertNotEqual(implicit_lsid, session1.session_id)
|
||||
self.assertNotEqual(implicit_lsid, session2.session_id)
|
||||
|
||||
finally:
|
||||
await session1.end_session()
|
||||
await session2.end_session()
|
||||
|
||||
async def test_session_binding_end_session(self):
|
||||
coll = self.client.pymongo_test.test
|
||||
await coll.insert_one({"x": 1})
|
||||
|
||||
async with self.client.start_session().bind() as s1:
|
||||
await coll.find_one()
|
||||
|
||||
self.assertTrue(s1.has_ended)
|
||||
|
||||
async with self.client.start_session().bind(end_session=False) as s2:
|
||||
await coll.find_one()
|
||||
|
||||
self.assertFalse(s2.has_ended)
|
||||
|
||||
await s2.end_session()
|
||||
|
||||
async def test_getmore_preserves_lsid_after_session_support_lost(self):
|
||||
listener = OvertCommandListener()
|
||||
client = await self.async_rs_or_single_client(event_listeners=[listener], maxPoolSize=1)
|
||||
coll = client.pymongo_test.test
|
||||
await coll.drop()
|
||||
await coll.insert_many([{"x": i} for i in range(10)])
|
||||
self.addAsyncCleanup(coll.drop)
|
||||
|
||||
async with client.start_session() as s:
|
||||
cursor = coll.find({}, batch_size=2, session=s)
|
||||
await anext(cursor)
|
||||
|
||||
find_event = next(e for e in listener.started_events if e.command_name == "find")
|
||||
lsid = find_event.command["lsid"]
|
||||
|
||||
# Simulate a node stepping down: mark idle connections as not supporting sessions.
|
||||
for server in client._topology._servers.values():
|
||||
for conn in server.pool.conns:
|
||||
conn.supports_sessions = False
|
||||
|
||||
listener.reset()
|
||||
await cursor.to_list()
|
||||
|
||||
getmore_events = [e for e in listener.started_events if e.command_name == "getMore"]
|
||||
self.assertGreater(len(getmore_events), 0, "expected at least one getMore command")
|
||||
for event in getmore_events:
|
||||
self.assertIn(
|
||||
"lsid", event.command, "getMore must include lsid when session is materialized"
|
||||
)
|
||||
self.assertEqual(
|
||||
lsid, event.command["lsid"], "getMore lsid must match the session lsid from find"
|
||||
)
|
||||
|
||||
|
||||
class TestCausalConsistency(AsyncUnitTest):
|
||||
listener: SessionTestListener
|
||||
|
||||
@ -48,19 +48,11 @@ from pymongo.write_concern import WriteConcern
|
||||
|
||||
_HAVE_PYOPENSSL = False
|
||||
try:
|
||||
# All of these must be available to use PyOpenSSL
|
||||
import OpenSSL
|
||||
import requests
|
||||
import service_identity
|
||||
|
||||
# Ensure service_identity>=18.1 is installed
|
||||
from service_identity.pyopenssl import verify_ip_address
|
||||
|
||||
from pymongo.ocsp_support import _load_trusted_ca_certs
|
||||
from pymongo import pyopenssl_context
|
||||
|
||||
_HAVE_PYOPENSSL = True
|
||||
except ImportError:
|
||||
_load_trusted_ca_certs = None # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
if HAVE_SSL:
|
||||
@ -136,11 +128,6 @@ class TestClientSSL(AsyncPyMongoTestCase):
|
||||
def test_use_pyopenssl_when_available(self):
|
||||
self.assertTrue(HAVE_PYSSL)
|
||||
|
||||
@unittest.skipUnless(_HAVE_PYOPENSSL, "Cannot test without PyOpenSSL")
|
||||
def test_load_trusted_ca_certs(self):
|
||||
trusted_ca_certs = _load_trusted_ca_certs(CA_BUNDLE_PEM)
|
||||
self.assertEqual(2, len(trusted_ca_certs))
|
||||
|
||||
|
||||
class TestSSL(AsyncIntegrationTest):
|
||||
saved_port: int
|
||||
|
||||
@ -16,9 +16,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
from io import BytesIO
|
||||
from unittest.mock import patch
|
||||
|
||||
import pymongo
|
||||
from gridfs.asynchronous.grid_file import AsyncGridFS, AsyncGridFSBucket
|
||||
from pymongo.asynchronous.pool import PoolState
|
||||
from pymongo.server_selectors import writable_server_selector
|
||||
@ -45,7 +49,9 @@ from pymongo.errors import (
|
||||
CollectionInvalid,
|
||||
ConfigurationError,
|
||||
ConnectionFailure,
|
||||
ExecutionTimeout,
|
||||
InvalidOperation,
|
||||
NetworkTimeout,
|
||||
OperationFailure,
|
||||
)
|
||||
from pymongo.operations import IndexModel, InsertOne
|
||||
@ -434,7 +440,7 @@ class TestTransactionsConvenientAPI(AsyncTransactionsBase):
|
||||
await self.configure_fail_point(client, command_args)
|
||||
|
||||
@async_client_context.require_transactions
|
||||
async def test_callback_raises_custom_error(self):
|
||||
async def test_1_callback_raises_custom_error(self):
|
||||
class _MyException(Exception):
|
||||
pass
|
||||
|
||||
@ -446,7 +452,7 @@ class TestTransactionsConvenientAPI(AsyncTransactionsBase):
|
||||
await s.with_transaction(raise_error)
|
||||
|
||||
@async_client_context.require_transactions
|
||||
async def test_callback_returns_value(self):
|
||||
async def test_2_callback_returns_value(self):
|
||||
async def callback(_):
|
||||
return "Foo"
|
||||
|
||||
@ -474,7 +480,7 @@ class TestTransactionsConvenientAPI(AsyncTransactionsBase):
|
||||
self.assertEqual(await s.with_transaction(callback), "Foo")
|
||||
|
||||
@async_client_context.require_transactions
|
||||
async def test_callback_not_retried_after_timeout(self):
|
||||
async def test_3_1_callback_not_retried_after_timeout(self):
|
||||
listener = OvertCommandListener()
|
||||
client = await self.async_rs_client(event_listeners=[listener])
|
||||
coll = client[self.db.name].test
|
||||
@ -495,14 +501,16 @@ class TestTransactionsConvenientAPI(AsyncTransactionsBase):
|
||||
listener.reset()
|
||||
async with client.start_session() as s:
|
||||
with PatchSessionTimeout(0):
|
||||
with self.assertRaises(OperationFailure):
|
||||
with self.assertRaises(NetworkTimeout) as context:
|
||||
await s.with_transaction(callback)
|
||||
|
||||
self.assertEqual(listener.started_command_names(), ["insert", "abortTransaction"])
|
||||
# Assert that the timeout error has the same labels as the error it wraps.
|
||||
self.assertTrue(context.exception.has_error_label("TransientTransactionError"))
|
||||
|
||||
@async_client_context.require_test_commands
|
||||
@async_client_context.require_transactions
|
||||
async def test_callback_not_retried_after_commit_timeout(self):
|
||||
async def test_3_2_callback_not_retried_after_commit_timeout(self):
|
||||
listener = OvertCommandListener()
|
||||
client = await self.async_rs_client(event_listeners=[listener])
|
||||
coll = client[self.db.name].test
|
||||
@ -529,14 +537,16 @@ class TestTransactionsConvenientAPI(AsyncTransactionsBase):
|
||||
|
||||
async with client.start_session() as s:
|
||||
with PatchSessionTimeout(0):
|
||||
with self.assertRaises(OperationFailure):
|
||||
with self.assertRaises(NetworkTimeout) as context:
|
||||
await s.with_transaction(callback)
|
||||
|
||||
self.assertEqual(listener.started_command_names(), ["insert", "commitTransaction"])
|
||||
# Assert that the timeout error has the same labels as the error it wraps.
|
||||
self.assertTrue(context.exception.has_error_label("TransientTransactionError"))
|
||||
|
||||
@async_client_context.require_test_commands
|
||||
@async_client_context.require_transactions
|
||||
async def test_commit_not_retried_after_timeout(self):
|
||||
async def test_3_3_commit_not_retried_after_timeout(self):
|
||||
listener = OvertCommandListener()
|
||||
client = await self.async_rs_client(event_listeners=[listener])
|
||||
coll = client[self.db.name].test
|
||||
@ -560,7 +570,7 @@ class TestTransactionsConvenientAPI(AsyncTransactionsBase):
|
||||
|
||||
async with client.start_session() as s:
|
||||
with PatchSessionTimeout(0):
|
||||
with self.assertRaises(ConnectionFailure):
|
||||
with self.assertRaises(NetworkTimeout) as context:
|
||||
await s.with_transaction(callback)
|
||||
|
||||
# One insert for the callback and two commits (includes the automatic
|
||||
@ -568,6 +578,40 @@ class TestTransactionsConvenientAPI(AsyncTransactionsBase):
|
||||
self.assertEqual(
|
||||
listener.started_command_names(), ["insert", "commitTransaction", "commitTransaction"]
|
||||
)
|
||||
# Assert that the timeout error has the same labels as the error it wraps.
|
||||
self.assertTrue(context.exception.has_error_label("UnknownTransactionCommitResult"))
|
||||
|
||||
@async_client_context.require_transactions
|
||||
async def test_callback_not_retried_after_csot_timeout(self):
|
||||
listener = OvertCommandListener()
|
||||
client = await self.async_rs_client(event_listeners=[listener])
|
||||
coll = client[self.db.name].test
|
||||
|
||||
async def callback(session):
|
||||
await coll.insert_one({}, session=session)
|
||||
err: dict = {
|
||||
"ok": 0,
|
||||
"errmsg": "Transaction 7819 has been aborted.",
|
||||
"code": 251,
|
||||
"codeName": "NoSuchTransaction",
|
||||
"errorLabels": ["TransientTransactionError"],
|
||||
}
|
||||
raise OperationFailure(err["errmsg"], err["code"], err)
|
||||
|
||||
# Create the collection.
|
||||
await coll.insert_one({})
|
||||
listener.reset()
|
||||
async with client.start_session() as s:
|
||||
with pymongo.timeout(1.0):
|
||||
with self.assertRaises(ExecutionTimeout):
|
||||
await s.with_transaction(callback)
|
||||
|
||||
# At least two attempts: the original and one or more retries.
|
||||
inserts = len([x for x in listener.started_command_names() if x == "insert"])
|
||||
aborts = len([x for x in listener.started_command_names() if x == "abortTransaction"])
|
||||
|
||||
self.assertGreaterEqual(inserts, 2)
|
||||
self.assertGreaterEqual(aborts, 2)
|
||||
|
||||
# Tested here because this supports Motor's convenient transactions API.
|
||||
@async_client_context.require_transactions
|
||||
@ -606,6 +650,63 @@ class TestTransactionsConvenientAPI(AsyncTransactionsBase):
|
||||
await s.with_transaction(callback)
|
||||
self.assertFalse(s.in_transaction)
|
||||
|
||||
@async_client_context.require_test_commands
|
||||
@async_client_context.require_transactions
|
||||
async def test_4_retry_backoff_is_enforced(self):
|
||||
client = async_client_context.client
|
||||
coll = client[self.db.name].test
|
||||
end = start = no_backoff_time = 0
|
||||
|
||||
# Make random.random always return 0 (no backoff)
|
||||
with patch.object(random, "random", return_value=0):
|
||||
# set fail point to trigger transaction failure and trigger backoff
|
||||
await self.set_fail_point(
|
||||
{
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {"times": 13},
|
||||
"data": {
|
||||
"failCommands": ["commitTransaction"],
|
||||
"errorCode": 251,
|
||||
},
|
||||
}
|
||||
)
|
||||
self.addAsyncCleanup(
|
||||
self.set_fail_point, {"configureFailPoint": "failCommand", "mode": "off"}
|
||||
)
|
||||
|
||||
async def callback(session):
|
||||
await coll.insert_one({}, session=session)
|
||||
|
||||
start = time.monotonic()
|
||||
async with self.client.start_session() as s:
|
||||
await s.with_transaction(callback)
|
||||
end = time.monotonic()
|
||||
no_backoff_time = end - start
|
||||
|
||||
# Make random.random always return 1 (max backoff)
|
||||
with patch.object(random, "random", return_value=1):
|
||||
# set fail point to trigger transaction failure and trigger backoff
|
||||
await self.set_fail_point(
|
||||
{
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {
|
||||
"times": 13
|
||||
}, # sufficiently high enough such that the time effect of backoff is noticeable
|
||||
"data": {
|
||||
"failCommands": ["commitTransaction"],
|
||||
"errorCode": 251,
|
||||
},
|
||||
}
|
||||
)
|
||||
self.addAsyncCleanup(
|
||||
self.set_fail_point, {"configureFailPoint": "failCommand", "mode": "off"}
|
||||
)
|
||||
start = time.monotonic()
|
||||
async with self.client.start_session() as s:
|
||||
await s.with_transaction(callback)
|
||||
end = time.monotonic()
|
||||
self.assertLess(abs(end - start - (no_backoff_time + 2.2)), 1) # sum of 13 backoffs is 2.2
|
||||
|
||||
|
||||
class TestOptionsInsideTransactionProse(AsyncTransactionsBase):
|
||||
@async_client_context.require_transactions
|
||||
|
||||
@ -1464,11 +1464,6 @@ class UnifiedSpecTestMixinV1(AsyncIntegrationTest):
|
||||
self.assertListEqual(sorted_expected_documents, actual_documents)
|
||||
|
||||
async def run_scenario(self, spec, uri=None):
|
||||
# Kill all sessions before and after each test to prevent an open
|
||||
# transaction (from a test failure) from blocking collection/database
|
||||
# operations during test set up and tear down.
|
||||
await self.kill_all_sessions()
|
||||
|
||||
# Handle flaky tests.
|
||||
flaky_tests = [
|
||||
("PYTHON-5170", ".*test_discovery_and_monitoring.*"),
|
||||
@ -1504,6 +1499,15 @@ class UnifiedSpecTestMixinV1(AsyncIntegrationTest):
|
||||
if skip_reason is not None:
|
||||
raise unittest.SkipTest(f"{skip_reason}")
|
||||
|
||||
# Kill all sessions after each test with transactions to prevent an open
|
||||
# transaction (from a test failure) from blocking collection/database
|
||||
# operations during test set up and tear down.
|
||||
for op in spec["operations"]:
|
||||
name = op["name"]
|
||||
if name == "startTransaction" or name == "withTransaction":
|
||||
self.addAsyncCleanup(self.kill_all_sessions)
|
||||
break
|
||||
|
||||
# process createEntities
|
||||
self._uri = uri
|
||||
self.entity_map = EntityMapUtil(self)
|
||||
|
||||
@ -16,43 +16,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import os
|
||||
import time
|
||||
import unittest
|
||||
from collections import abc
|
||||
from inspect import iscoroutinefunction
|
||||
from test.asynchronous import AsyncIntegrationTest, async_client_context, client_knobs
|
||||
from test.asynchronous import async_client_context
|
||||
from test.asynchronous.helpers import ConcurrentRunner
|
||||
from test.utils_shared import (
|
||||
CMAPListener,
|
||||
CompareType,
|
||||
EventListener,
|
||||
OvertCommandListener,
|
||||
ScenarioDict,
|
||||
ServerAndTopologyEventListener,
|
||||
camel_to_snake,
|
||||
camel_to_snake_args,
|
||||
parse_spec_options,
|
||||
prepare_spec_arguments,
|
||||
)
|
||||
from typing import List
|
||||
from test.utils_shared import ScenarioDict
|
||||
|
||||
from bson import ObjectId, decode, encode, json_util
|
||||
from bson.binary import Binary
|
||||
from bson.int64 import Int64
|
||||
from bson.son import SON
|
||||
from gridfs import GridFSBucket
|
||||
from gridfs.asynchronous.grid_file import AsyncGridFSBucket
|
||||
from pymongo.asynchronous import client_session
|
||||
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
|
||||
from pymongo.asynchronous.cursor import AsyncCursor
|
||||
from pymongo.errors import AutoReconnect, BulkWriteError, OperationFailure, PyMongoError
|
||||
from bson import json_util
|
||||
from pymongo.lock import _async_cond_wait, _async_create_condition, _async_create_lock
|
||||
from pymongo.read_concern import ReadConcern
|
||||
from pymongo.read_preferences import ReadPreference
|
||||
from pymongo.results import BulkWriteResult, _WriteResult
|
||||
from pymongo.write_concern import WriteConcern
|
||||
|
||||
_IS_SYNC = False
|
||||
|
||||
@ -219,597 +189,3 @@ class AsyncSpecTestCreator:
|
||||
self._create_tests()
|
||||
else:
|
||||
asyncio.run(self._create_tests())
|
||||
|
||||
|
||||
class AsyncSpecRunner(AsyncIntegrationTest):
|
||||
mongos_clients: List
|
||||
knobs: client_knobs
|
||||
listener: EventListener
|
||||
|
||||
async def asyncSetUp(self) -> None:
|
||||
await super().asyncSetUp()
|
||||
self.mongos_clients = []
|
||||
|
||||
# Speed up the tests by decreasing the heartbeat frequency.
|
||||
self.knobs = client_knobs(heartbeat_frequency=0.1, min_heartbeat_interval=0.1)
|
||||
self.knobs.enable()
|
||||
self.targets = {}
|
||||
self.listener = None # type: ignore
|
||||
self.pool_listener = None
|
||||
self.server_listener = None
|
||||
self.maxDiff = None
|
||||
|
||||
async def asyncTearDown(self) -> None:
|
||||
self.knobs.disable()
|
||||
|
||||
async def set_fail_point(self, command_args):
|
||||
clients = self.mongos_clients if self.mongos_clients else [self.client]
|
||||
for client in clients:
|
||||
await self.configure_fail_point(client, command_args)
|
||||
|
||||
async def targeted_fail_point(self, session, fail_point):
|
||||
"""Run the targetedFailPoint test operation.
|
||||
|
||||
Enable the fail point on the session's pinned mongos.
|
||||
"""
|
||||
clients = {c.address: c for c in self.mongos_clients}
|
||||
client = clients[session._pinned_address]
|
||||
await self.configure_fail_point(client, fail_point)
|
||||
self.addAsyncCleanup(self.set_fail_point, {"mode": "off"})
|
||||
|
||||
def assert_session_pinned(self, session):
|
||||
"""Run the assertSessionPinned test operation.
|
||||
|
||||
Assert that the given session is pinned.
|
||||
"""
|
||||
self.assertIsNotNone(session._transaction.pinned_address)
|
||||
|
||||
def assert_session_unpinned(self, session):
|
||||
"""Run the assertSessionUnpinned test operation.
|
||||
|
||||
Assert that the given session is not pinned.
|
||||
"""
|
||||
self.assertIsNone(session._pinned_address)
|
||||
self.assertIsNone(session._transaction.pinned_address)
|
||||
|
||||
async def assert_collection_exists(self, database, collection):
|
||||
"""Run the assertCollectionExists test operation."""
|
||||
db = self.client[database]
|
||||
self.assertIn(collection, await db.list_collection_names())
|
||||
|
||||
async def assert_collection_not_exists(self, database, collection):
|
||||
"""Run the assertCollectionNotExists test operation."""
|
||||
db = self.client[database]
|
||||
self.assertNotIn(collection, await db.list_collection_names())
|
||||
|
||||
async def assert_index_exists(self, database, collection, index):
|
||||
"""Run the assertIndexExists test operation."""
|
||||
coll = self.client[database][collection]
|
||||
self.assertIn(index, [doc["name"] async for doc in await coll.list_indexes()])
|
||||
|
||||
async def assert_index_not_exists(self, database, collection, index):
|
||||
"""Run the assertIndexNotExists test operation."""
|
||||
coll = self.client[database][collection]
|
||||
self.assertNotIn(index, [doc["name"] async for doc in await coll.list_indexes()])
|
||||
|
||||
async def wait(self, ms):
|
||||
"""Run the "wait" test operation."""
|
||||
await asyncio.sleep(ms / 1000.0)
|
||||
|
||||
def assertErrorLabelsContain(self, exc, expected_labels):
|
||||
labels = [l for l in expected_labels if exc.has_error_label(l)]
|
||||
self.assertEqual(labels, expected_labels)
|
||||
|
||||
def assertErrorLabelsOmit(self, exc, omit_labels):
|
||||
for label in omit_labels:
|
||||
self.assertFalse(
|
||||
exc.has_error_label(label), msg=f"error labels should not contain {label}"
|
||||
)
|
||||
|
||||
async def kill_all_sessions(self):
|
||||
clients = self.mongos_clients if self.mongos_clients else [self.client]
|
||||
for client in clients:
|
||||
try:
|
||||
await client.admin.command("killAllSessions", [])
|
||||
except (OperationFailure, AutoReconnect):
|
||||
# "operation was interrupted" by killing the command's
|
||||
# own session.
|
||||
# On 8.0+ killAllSessions sometimes returns a network error.
|
||||
pass
|
||||
|
||||
def check_command_result(self, expected_result, result):
|
||||
# Only compare the keys in the expected result.
|
||||
filtered_result = {}
|
||||
for key in expected_result:
|
||||
try:
|
||||
filtered_result[key] = result[key]
|
||||
except KeyError:
|
||||
pass
|
||||
self.assertEqual(filtered_result, expected_result)
|
||||
|
||||
# TODO: factor the following function with test_crud.py.
|
||||
def check_result(self, expected_result, result):
|
||||
if isinstance(result, _WriteResult):
|
||||
for res in expected_result:
|
||||
prop = camel_to_snake(res)
|
||||
# SPEC-869: Only BulkWriteResult has upserted_count.
|
||||
if prop == "upserted_count" and not isinstance(result, BulkWriteResult):
|
||||
if result.upserted_id is not None:
|
||||
upserted_count = 1
|
||||
else:
|
||||
upserted_count = 0
|
||||
self.assertEqual(upserted_count, expected_result[res], prop)
|
||||
elif prop == "inserted_ids":
|
||||
# BulkWriteResult does not have inserted_ids.
|
||||
if isinstance(result, BulkWriteResult):
|
||||
self.assertEqual(len(expected_result[res]), result.inserted_count)
|
||||
else:
|
||||
# InsertManyResult may be compared to [id1] from the
|
||||
# crud spec or {"0": id1} from the retryable write spec.
|
||||
ids = expected_result[res]
|
||||
if isinstance(ids, dict):
|
||||
ids = [ids[str(i)] for i in range(len(ids))]
|
||||
|
||||
self.assertEqual(ids, result.inserted_ids, prop)
|
||||
elif prop == "upserted_ids":
|
||||
# Convert indexes from strings to integers.
|
||||
ids = expected_result[res]
|
||||
expected_ids = {}
|
||||
for str_index in ids:
|
||||
expected_ids[int(str_index)] = ids[str_index]
|
||||
self.assertEqual(expected_ids, result.upserted_ids, prop)
|
||||
else:
|
||||
self.assertEqual(getattr(result, prop), expected_result[res], prop)
|
||||
|
||||
return True
|
||||
else:
|
||||
|
||||
def _helper(expected_result, result):
|
||||
if isinstance(expected_result, abc.Mapping):
|
||||
for i in expected_result.keys():
|
||||
self.assertEqual(expected_result[i], result[i])
|
||||
|
||||
elif isinstance(expected_result, list):
|
||||
for i, k in zip(expected_result, result):
|
||||
_helper(i, k)
|
||||
else:
|
||||
self.assertEqual(expected_result, result)
|
||||
|
||||
_helper(expected_result, result)
|
||||
return None
|
||||
|
||||
def get_object_name(self, op):
|
||||
"""Allow subclasses to override handling of 'object'
|
||||
|
||||
Transaction spec says 'object' is required.
|
||||
"""
|
||||
return op["object"]
|
||||
|
||||
@staticmethod
|
||||
def parse_options(opts):
|
||||
return parse_spec_options(opts)
|
||||
|
||||
async def run_operation(self, sessions, collection, operation):
|
||||
original_collection = collection
|
||||
name = camel_to_snake(operation["name"])
|
||||
if name == "run_command":
|
||||
name = "command"
|
||||
elif name == "download_by_name":
|
||||
name = "open_download_stream_by_name"
|
||||
elif name == "download":
|
||||
name = "open_download_stream"
|
||||
elif name == "map_reduce":
|
||||
self.skipTest("PyMongo does not support mapReduce")
|
||||
elif name == "count":
|
||||
self.skipTest("PyMongo does not support count")
|
||||
|
||||
database = collection.database
|
||||
collection = database.get_collection(collection.name)
|
||||
if "collectionOptions" in operation:
|
||||
collection = collection.with_options(
|
||||
**self.parse_options(operation["collectionOptions"])
|
||||
)
|
||||
|
||||
object_name = self.get_object_name(operation)
|
||||
if object_name == "gridfsbucket":
|
||||
# Only create the GridFSBucket when we need it (for the gridfs
|
||||
# retryable reads tests).
|
||||
obj = AsyncGridFSBucket(database, bucket_name=collection.name)
|
||||
else:
|
||||
objects = {
|
||||
"client": database.client,
|
||||
"database": database,
|
||||
"collection": collection,
|
||||
"testRunner": self,
|
||||
}
|
||||
objects.update(sessions)
|
||||
obj = objects[object_name]
|
||||
|
||||
# Combine arguments with options and handle special cases.
|
||||
arguments = operation.get("arguments", {})
|
||||
arguments.update(arguments.pop("options", {}))
|
||||
self.parse_options(arguments)
|
||||
|
||||
cmd = getattr(obj, name)
|
||||
|
||||
with_txn_callback = functools.partial(
|
||||
self.run_operations, sessions, original_collection, in_with_transaction=True
|
||||
)
|
||||
prepare_spec_arguments(operation, arguments, name, sessions, with_txn_callback)
|
||||
|
||||
if name == "run_on_thread":
|
||||
args = {"sessions": sessions, "collection": collection}
|
||||
args.update(arguments)
|
||||
arguments = args
|
||||
|
||||
if not _IS_SYNC and iscoroutinefunction(cmd):
|
||||
result = await cmd(**dict(arguments))
|
||||
else:
|
||||
result = cmd(**dict(arguments))
|
||||
# Cleanup open change stream cursors.
|
||||
if name == "watch":
|
||||
self.addAsyncCleanup(result.close)
|
||||
|
||||
if name == "aggregate":
|
||||
if arguments["pipeline"] and "$out" in arguments["pipeline"][-1]:
|
||||
# Read from the primary to ensure causal consistency.
|
||||
out = collection.database.get_collection(
|
||||
arguments["pipeline"][-1]["$out"], read_preference=ReadPreference.PRIMARY
|
||||
)
|
||||
return out.find()
|
||||
if "download" in name:
|
||||
result = Binary(result.read())
|
||||
|
||||
if isinstance(result, AsyncCursor) or isinstance(result, AsyncCommandCursor):
|
||||
return await result.to_list()
|
||||
|
||||
return result
|
||||
|
||||
def allowable_errors(self, op):
|
||||
"""Allow encryption spec to override expected error classes."""
|
||||
return (PyMongoError,)
|
||||
|
||||
async def _run_op(self, sessions, collection, op, in_with_transaction):
|
||||
expected_result = op.get("result")
|
||||
if expect_error(op):
|
||||
with self.assertRaises(self.allowable_errors(op), msg=op["name"]) as context:
|
||||
await self.run_operation(sessions, collection, op.copy())
|
||||
exc = context.exception
|
||||
if expect_error_message(expected_result):
|
||||
if isinstance(exc, BulkWriteError):
|
||||
errmsg = str(exc.details).lower()
|
||||
else:
|
||||
errmsg = str(exc).lower()
|
||||
self.assertIn(expected_result["errorContains"].lower(), errmsg)
|
||||
if expect_error_code(expected_result):
|
||||
self.assertEqual(expected_result["errorCodeName"], exc.details.get("codeName"))
|
||||
if expect_error_labels_contain(expected_result):
|
||||
self.assertErrorLabelsContain(exc, expected_result["errorLabelsContain"])
|
||||
if expect_error_labels_omit(expected_result):
|
||||
self.assertErrorLabelsOmit(exc, expected_result["errorLabelsOmit"])
|
||||
if expect_timeout_error(expected_result):
|
||||
self.assertIsInstance(exc, PyMongoError)
|
||||
if not exc.timeout:
|
||||
# Re-raise the exception for better diagnostics.
|
||||
raise exc
|
||||
|
||||
# Reraise the exception if we're in the with_transaction
|
||||
# callback.
|
||||
if in_with_transaction:
|
||||
raise context.exception
|
||||
else:
|
||||
result = await self.run_operation(sessions, collection, op.copy())
|
||||
if "result" in op:
|
||||
if op["name"] == "runCommand":
|
||||
self.check_command_result(expected_result, result)
|
||||
else:
|
||||
self.check_result(expected_result, result)
|
||||
|
||||
async def run_operations(self, sessions, collection, ops, in_with_transaction=False):
|
||||
for op in ops:
|
||||
await self._run_op(sessions, collection, op, in_with_transaction)
|
||||
|
||||
# TODO: factor with test_command_monitoring.py
|
||||
def check_events(self, test, listener, session_ids):
|
||||
events = listener.started_events
|
||||
if not len(test["expectations"]):
|
||||
return
|
||||
|
||||
# Give a nicer message when there are missing or extra events
|
||||
cmds = decode_raw([event.command for event in events])
|
||||
self.assertEqual(len(events), len(test["expectations"]), cmds)
|
||||
for i, expectation in enumerate(test["expectations"]):
|
||||
event_type = next(iter(expectation))
|
||||
event = events[i]
|
||||
|
||||
# The tests substitute 42 for any number other than 0.
|
||||
if event.command_name == "getMore" and event.command["getMore"]:
|
||||
event.command["getMore"] = Int64(42)
|
||||
elif event.command_name == "killCursors":
|
||||
event.command["cursors"] = [Int64(42)]
|
||||
elif event.command_name == "update":
|
||||
# TODO: remove this once PYTHON-1744 is done.
|
||||
# Add upsert and multi fields back into expectations.
|
||||
updates = expectation[event_type]["command"]["updates"]
|
||||
for update in updates:
|
||||
update.setdefault("upsert", False)
|
||||
update.setdefault("multi", False)
|
||||
|
||||
# Replace afterClusterTime: 42 with actual afterClusterTime.
|
||||
expected_cmd = expectation[event_type]["command"]
|
||||
expected_read_concern = expected_cmd.get("readConcern")
|
||||
if expected_read_concern is not None:
|
||||
time = expected_read_concern.get("afterClusterTime")
|
||||
if time == 42:
|
||||
actual_time = event.command.get("readConcern", {}).get("afterClusterTime")
|
||||
if actual_time is not None:
|
||||
expected_read_concern["afterClusterTime"] = actual_time
|
||||
|
||||
recovery_token = expected_cmd.get("recoveryToken")
|
||||
if recovery_token == 42:
|
||||
expected_cmd["recoveryToken"] = CompareType(dict)
|
||||
|
||||
# Replace lsid with a name like "session0" to match test.
|
||||
if "lsid" in event.command:
|
||||
for name, lsid in session_ids.items():
|
||||
if event.command["lsid"] == lsid:
|
||||
event.command["lsid"] = name
|
||||
break
|
||||
|
||||
for attr, expected in expectation[event_type].items():
|
||||
actual = getattr(event, attr)
|
||||
expected = wrap_types(expected)
|
||||
if isinstance(expected, dict):
|
||||
for key, val in expected.items():
|
||||
if val is None:
|
||||
if key in actual:
|
||||
self.fail(f"Unexpected key [{key}] in {actual!r}")
|
||||
elif key not in actual:
|
||||
self.fail(f"Expected key [{key}] in {actual!r}")
|
||||
else:
|
||||
self.assertEqual(
|
||||
val, decode_raw(actual[key]), f"Key [{key}] in {actual}"
|
||||
)
|
||||
else:
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def maybe_skip_scenario(self, test):
|
||||
if test.get("skipReason"):
|
||||
self.skipTest(test.get("skipReason"))
|
||||
|
||||
def get_scenario_db_name(self, scenario_def):
|
||||
"""Allow subclasses to override a test's database name."""
|
||||
return scenario_def["database_name"]
|
||||
|
||||
def get_scenario_coll_name(self, scenario_def):
|
||||
"""Allow subclasses to override a test's collection name."""
|
||||
return scenario_def["collection_name"]
|
||||
|
||||
def get_outcome_coll_name(self, outcome, collection):
|
||||
"""Allow subclasses to override outcome collection."""
|
||||
return collection.name
|
||||
|
||||
async def run_test_ops(self, sessions, collection, test):
|
||||
"""Added to allow retryable writes spec to override a test's
|
||||
operation.
|
||||
"""
|
||||
await self.run_operations(sessions, collection, test["operations"])
|
||||
|
||||
def parse_client_options(self, opts):
|
||||
"""Allow encryption spec to override a clientOptions parsing."""
|
||||
return opts
|
||||
|
||||
async def setup_scenario(self, scenario_def):
|
||||
"""Allow specs to override a test's setup."""
|
||||
db_name = self.get_scenario_db_name(scenario_def)
|
||||
coll_name = self.get_scenario_coll_name(scenario_def)
|
||||
documents = scenario_def["data"]
|
||||
|
||||
# Setup the collection with as few majority writes as possible.
|
||||
db = async_client_context.client.get_database(db_name)
|
||||
coll_exists = bool(await db.list_collection_names(filter={"name": coll_name}))
|
||||
if coll_exists:
|
||||
await db[coll_name].delete_many({})
|
||||
# Only use majority wc only on the final write.
|
||||
wc = WriteConcern(w="majority")
|
||||
if documents:
|
||||
db.get_collection(coll_name, write_concern=wc).insert_many(documents)
|
||||
elif not coll_exists:
|
||||
# Ensure collection exists.
|
||||
await db.create_collection(coll_name, write_concern=wc)
|
||||
|
||||
async def run_scenario(self, scenario_def, test):
|
||||
self.maybe_skip_scenario(test)
|
||||
|
||||
# Kill all sessions before and after each test to prevent an open
|
||||
# transaction (from a test failure) from blocking collection/database
|
||||
# operations during test set up and tear down.
|
||||
await self.kill_all_sessions()
|
||||
self.addAsyncCleanup(self.kill_all_sessions)
|
||||
await self.setup_scenario(scenario_def)
|
||||
database_name = self.get_scenario_db_name(scenario_def)
|
||||
collection_name = self.get_scenario_coll_name(scenario_def)
|
||||
# SPEC-1245 workaround StaleDbVersion on distinct
|
||||
for c in self.mongos_clients:
|
||||
await c[database_name][collection_name].distinct("x")
|
||||
|
||||
# Configure the fail point before creating the client.
|
||||
if "failPoint" in test:
|
||||
fp = test["failPoint"]
|
||||
await self.set_fail_point(fp)
|
||||
self.addAsyncCleanup(
|
||||
self.set_fail_point, {"configureFailPoint": fp["configureFailPoint"], "mode": "off"}
|
||||
)
|
||||
|
||||
listener = OvertCommandListener()
|
||||
pool_listener = CMAPListener()
|
||||
server_listener = ServerAndTopologyEventListener()
|
||||
# Create a new client, to avoid interference from pooled sessions.
|
||||
client_options = self.parse_client_options(test["clientOptions"])
|
||||
use_multi_mongos = test["useMultipleMongoses"]
|
||||
host = None
|
||||
if use_multi_mongos:
|
||||
if async_client_context.load_balancer:
|
||||
host = async_client_context.MULTI_MONGOS_LB_URI
|
||||
elif async_client_context.is_mongos:
|
||||
host = async_client_context.mongos_seeds()
|
||||
client = await self.async_rs_client(
|
||||
h=host, event_listeners=[listener, pool_listener, server_listener], **client_options
|
||||
)
|
||||
self.scenario_client = client
|
||||
self.listener = listener
|
||||
self.pool_listener = pool_listener
|
||||
self.server_listener = server_listener
|
||||
|
||||
# Create session0 and session1.
|
||||
sessions = {}
|
||||
session_ids = {}
|
||||
for i in range(2):
|
||||
# Don't attempt to create sessions if they are not supported by
|
||||
# the running server version.
|
||||
if not async_client_context.sessions_enabled:
|
||||
break
|
||||
session_name = "session%d" % i
|
||||
opts = camel_to_snake_args(test["sessionOptions"][session_name])
|
||||
if "default_transaction_options" in opts:
|
||||
txn_opts = self.parse_options(opts["default_transaction_options"])
|
||||
txn_opts = client_session.TransactionOptions(**txn_opts)
|
||||
opts["default_transaction_options"] = txn_opts
|
||||
|
||||
s = client.start_session(**dict(opts))
|
||||
|
||||
sessions[session_name] = s
|
||||
# Store lsid so we can access it after end_session, in check_events.
|
||||
session_ids[session_name] = s.session_id
|
||||
|
||||
self.addAsyncCleanup(end_sessions, sessions)
|
||||
|
||||
collection = client[database_name][collection_name]
|
||||
await self.run_test_ops(sessions, collection, test)
|
||||
|
||||
await end_sessions(sessions)
|
||||
|
||||
self.check_events(test, listener, session_ids)
|
||||
|
||||
# Disable fail points.
|
||||
if "failPoint" in test:
|
||||
fp = test["failPoint"]
|
||||
await self.set_fail_point(
|
||||
{"configureFailPoint": fp["configureFailPoint"], "mode": "off"}
|
||||
)
|
||||
|
||||
# Assert final state is expected.
|
||||
outcome = test["outcome"]
|
||||
expected_c = outcome.get("collection")
|
||||
if expected_c is not None:
|
||||
outcome_coll_name = self.get_outcome_coll_name(outcome, collection)
|
||||
|
||||
# Read from the primary with local read concern to ensure causal
|
||||
# consistency.
|
||||
outcome_coll = async_client_context.client[collection.database.name].get_collection(
|
||||
outcome_coll_name,
|
||||
read_preference=ReadPreference.PRIMARY,
|
||||
read_concern=ReadConcern("local"),
|
||||
)
|
||||
actual_data = await outcome_coll.find(sort=[("_id", 1)]).to_list()
|
||||
|
||||
# The expected data needs to be the left hand side here otherwise
|
||||
# CompareType(Binary) doesn't work.
|
||||
self.assertEqual(wrap_types(expected_c["data"]), actual_data)
|
||||
|
||||
|
||||
def expect_any_error(op):
|
||||
if isinstance(op, dict):
|
||||
return op.get("error")
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def expect_error_message(expected_result):
|
||||
if isinstance(expected_result, dict):
|
||||
return isinstance(expected_result["errorContains"], str)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def expect_error_code(expected_result):
|
||||
if isinstance(expected_result, dict):
|
||||
return expected_result["errorCodeName"]
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def expect_error_labels_contain(expected_result):
|
||||
if isinstance(expected_result, dict):
|
||||
return expected_result["errorLabelsContain"]
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def expect_error_labels_omit(expected_result):
|
||||
if isinstance(expected_result, dict):
|
||||
return expected_result["errorLabelsOmit"]
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def expect_timeout_error(expected_result):
|
||||
if isinstance(expected_result, dict):
|
||||
return expected_result["isTimeoutError"]
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def expect_error(op):
|
||||
expected_result = op.get("result")
|
||||
return (
|
||||
expect_any_error(op)
|
||||
or expect_error_message(expected_result)
|
||||
or expect_error_code(expected_result)
|
||||
or expect_error_labels_contain(expected_result)
|
||||
or expect_error_labels_omit(expected_result)
|
||||
or expect_timeout_error(expected_result)
|
||||
)
|
||||
|
||||
|
||||
async def end_sessions(sessions):
|
||||
for s in sessions.values():
|
||||
# Aborts the transaction if it's open.
|
||||
await s.end_session()
|
||||
|
||||
|
||||
def decode_raw(val):
|
||||
"""Decode RawBSONDocuments in the given container."""
|
||||
if isinstance(val, (list, abc.Mapping)):
|
||||
return decode(encode({"v": val}))["v"]
|
||||
return val
|
||||
|
||||
|
||||
TYPES = {
|
||||
"binData": Binary,
|
||||
"long": Int64,
|
||||
"int": int,
|
||||
"string": str,
|
||||
"objectId": ObjectId,
|
||||
"object": dict,
|
||||
"array": list,
|
||||
}
|
||||
|
||||
|
||||
def wrap_types(val):
|
||||
"""Support $$type assertion in command results."""
|
||||
if isinstance(val, list):
|
||||
return [wrap_types(v) for v in val]
|
||||
if isinstance(val, abc.Mapping):
|
||||
typ = val.get("$$type")
|
||||
if typ:
|
||||
if isinstance(typ, str):
|
||||
types = TYPES[typ]
|
||||
else:
|
||||
types = tuple(TYPES[t] for t in typ)
|
||||
return CompareType(types)
|
||||
d = {}
|
||||
for key in val:
|
||||
d[key] = wrap_types(val[key])
|
||||
return d
|
||||
return val
|
||||
|
||||
@ -42,6 +42,91 @@
|
||||
}
|
||||
],
|
||||
"tests": [
|
||||
{
|
||||
"description": "disambiguatedPaths is not present when showExpandedEvents is false/unset",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "6.1.0",
|
||||
"maxServerVersion": "8.1.99",
|
||||
"topologies": [
|
||||
"replicaset",
|
||||
"load-balanced",
|
||||
"sharded"
|
||||
],
|
||||
"serverless": "forbid"
|
||||
},
|
||||
{
|
||||
"minServerVersion": "8.2.1",
|
||||
"topologies": [
|
||||
"replicaset",
|
||||
"load-balanced",
|
||||
"sharded"
|
||||
],
|
||||
"serverless": "forbid"
|
||||
}
|
||||
],
|
||||
"operations": [
|
||||
{
|
||||
"name": "insertOne",
|
||||
"object": "collection0",
|
||||
"arguments": {
|
||||
"document": {
|
||||
"_id": 1,
|
||||
"a": {
|
||||
"1": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "createChangeStream",
|
||||
"object": "collection0",
|
||||
"arguments": {
|
||||
"pipeline": []
|
||||
},
|
||||
"saveResultAsEntity": "changeStream0"
|
||||
},
|
||||
{
|
||||
"name": "updateOne",
|
||||
"object": "collection0",
|
||||
"arguments": {
|
||||
"filter": {
|
||||
"_id": 1
|
||||
},
|
||||
"update": {
|
||||
"$set": {
|
||||
"a.1": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "iterateUntilDocumentOrError",
|
||||
"object": "changeStream0",
|
||||
"expectResult": {
|
||||
"operationType": "update",
|
||||
"ns": {
|
||||
"db": "database0",
|
||||
"coll": "collection0"
|
||||
},
|
||||
"updateDescription": {
|
||||
"updatedFields": {
|
||||
"$$exists": true
|
||||
},
|
||||
"removedFields": {
|
||||
"$$exists": true
|
||||
},
|
||||
"truncatedArrays": {
|
||||
"$$exists": true
|
||||
},
|
||||
"disambiguatedPaths": {
|
||||
"$$exists": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "disambiguatedPaths is present on updateDescription when an ambiguous path is present",
|
||||
"operations": [
|
||||
|
||||
@ -63,47 +63,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "nsType is present when creating timeseries",
|
||||
"operations": [
|
||||
{
|
||||
"name": "dropCollection",
|
||||
"object": "database0",
|
||||
"arguments": {
|
||||
"collection": "foo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "createChangeStream",
|
||||
"object": "database0",
|
||||
"arguments": {
|
||||
"pipeline": [],
|
||||
"showExpandedEvents": true
|
||||
},
|
||||
"saveResultAsEntity": "changeStream0"
|
||||
},
|
||||
{
|
||||
"name": "createCollection",
|
||||
"object": "database0",
|
||||
"arguments": {
|
||||
"collection": "foo",
|
||||
"timeseries": {
|
||||
"timeField": "time",
|
||||
"metaField": "meta",
|
||||
"granularity": "minutes"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "iterateUntilDocumentOrError",
|
||||
"object": "changeStream0",
|
||||
"expectResult": {
|
||||
"operationType": "create",
|
||||
"nsType": "timeseries"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "nsType is present when creating views",
|
||||
"operations": [
|
||||
|
||||
111
test/client-backpressure/backpressure-connection-checkin.json
Normal file
111
test/client-backpressure/backpressure-connection-checkin.json
Normal file
@ -0,0 +1,111 @@
|
||||
{
|
||||
"description": "tests that connections are returned to the pool on retry attempts for overload errors",
|
||||
"schemaVersion": "1.3",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "4.4",
|
||||
"topologies": [
|
||||
"replicaset",
|
||||
"sharded",
|
||||
"load-balanced"
|
||||
]
|
||||
}
|
||||
],
|
||||
"createEntities": [
|
||||
{
|
||||
"client": {
|
||||
"id": "client",
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"connectionCheckedOutEvent",
|
||||
"connectionCheckedInEvent"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"client": {
|
||||
"id": "fail_point_client",
|
||||
"useMultipleMongoses": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"database": {
|
||||
"id": "database",
|
||||
"client": "client",
|
||||
"databaseName": "backpressure-connection-checkin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"collection": {
|
||||
"id": "collection",
|
||||
"database": "database",
|
||||
"collectionName": "coll"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tests": [
|
||||
{
|
||||
"description": "overload error retry attempts return connections to the pool",
|
||||
"operations": [
|
||||
{
|
||||
"name": "failPoint",
|
||||
"object": "testRunner",
|
||||
"arguments": {
|
||||
"client": "fail_point_client",
|
||||
"failPoint": {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": "alwaysOn",
|
||||
"data": {
|
||||
"failCommands": [
|
||||
"find"
|
||||
],
|
||||
"errorLabels": [
|
||||
"RetryableError",
|
||||
"SystemOverloadedError"
|
||||
],
|
||||
"errorCode": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "find",
|
||||
"object": "collection",
|
||||
"arguments": {
|
||||
"filter": {}
|
||||
},
|
||||
"expectError": {
|
||||
"isError": true,
|
||||
"isClientError": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"expectEvents": [
|
||||
{
|
||||
"client": "client",
|
||||
"eventType": "cmap",
|
||||
"events": [
|
||||
{
|
||||
"connectionCheckedOutEvent": {}
|
||||
},
|
||||
{
|
||||
"connectionCheckedInEvent": {}
|
||||
},
|
||||
{
|
||||
"connectionCheckedOutEvent": {}
|
||||
},
|
||||
{
|
||||
"connectionCheckedInEvent": {}
|
||||
},
|
||||
{
|
||||
"connectionCheckedOutEvent": {}
|
||||
},
|
||||
{
|
||||
"connectionCheckedInEvent": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
4553
test/client-backpressure/backpressure-retry-loop.json
Normal file
4553
test/client-backpressure/backpressure-retry-loop.json
Normal file
File diff suppressed because it is too large
Load Diff
2569
test/client-backpressure/backpressure-retry-max-attempts.json
Normal file
2569
test/client-backpressure/backpressure-retry-max-attempts.json
Normal file
File diff suppressed because it is too large
Load Diff
253
test/client-backpressure/getMore-retried.json
Normal file
253
test/client-backpressure/getMore-retried.json
Normal file
@ -0,0 +1,253 @@
|
||||
{
|
||||
"description": "getMore-retried-backpressure",
|
||||
"schemaVersion": "1.3",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "4.4"
|
||||
}
|
||||
],
|
||||
"createEntities": [
|
||||
{
|
||||
"client": {
|
||||
"id": "client0",
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent",
|
||||
"commandFailedEvent",
|
||||
"commandSucceededEvent"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"client": {
|
||||
"id": "failPointClient",
|
||||
"useMultipleMongoses": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"database": {
|
||||
"id": "db",
|
||||
"client": "client0",
|
||||
"databaseName": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"collection": {
|
||||
"id": "coll",
|
||||
"database": "db",
|
||||
"collectionName": "default"
|
||||
}
|
||||
}
|
||||
],
|
||||
"initialData": [
|
||||
{
|
||||
"databaseName": "default",
|
||||
"collectionName": "default",
|
||||
"documents": [
|
||||
{
|
||||
"a": 1
|
||||
},
|
||||
{
|
||||
"a": 2
|
||||
},
|
||||
{
|
||||
"a": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tests": [
|
||||
{
|
||||
"description": "getMores are retried",
|
||||
"operations": [
|
||||
{
|
||||
"name": "failPoint",
|
||||
"object": "testRunner",
|
||||
"arguments": {
|
||||
"client": "failPointClient",
|
||||
"failPoint": {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": {
|
||||
"times": 2
|
||||
},
|
||||
"data": {
|
||||
"failCommands": [
|
||||
"getMore"
|
||||
],
|
||||
"errorLabels": [
|
||||
"RetryableError",
|
||||
"SystemOverloadedError"
|
||||
],
|
||||
"errorCode": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "find",
|
||||
"object": "coll",
|
||||
"arguments": {
|
||||
"batchSize": 2,
|
||||
"filter": {},
|
||||
"sort": {
|
||||
"a": 1
|
||||
}
|
||||
},
|
||||
"expectResult": [
|
||||
{
|
||||
"a": 1
|
||||
},
|
||||
{
|
||||
"a": 2
|
||||
},
|
||||
{
|
||||
"a": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"expectEvents": [
|
||||
{
|
||||
"client": "client0",
|
||||
"events": [
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"commandName": "find"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandSucceededEvent": {
|
||||
"commandName": "find"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"commandName": "getMore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandFailedEvent": {
|
||||
"commandName": "getMore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"commandName": "getMore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandFailedEvent": {
|
||||
"commandName": "getMore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"commandName": "getMore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandSucceededEvent": {
|
||||
"commandName": "getMore"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "getMores are retried maxAttempts=2 times",
|
||||
"operations": [
|
||||
{
|
||||
"name": "failPoint",
|
||||
"object": "testRunner",
|
||||
"arguments": {
|
||||
"client": "failPointClient",
|
||||
"failPoint": {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": "alwaysOn",
|
||||
"data": {
|
||||
"failCommands": [
|
||||
"getMore"
|
||||
],
|
||||
"errorLabels": [
|
||||
"RetryableError",
|
||||
"SystemOverloadedError"
|
||||
],
|
||||
"errorCode": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "find",
|
||||
"arguments": {
|
||||
"batchSize": 2,
|
||||
"filter": {}
|
||||
},
|
||||
"object": "coll",
|
||||
"expectError": {
|
||||
"isError": true,
|
||||
"isClientError": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"expectEvents": [
|
||||
{
|
||||
"client": "client0",
|
||||
"events": [
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"commandName": "find"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandSucceededEvent": {
|
||||
"commandName": "find"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"commandName": "getMore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandFailedEvent": {
|
||||
"commandName": "getMore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"commandName": "getMore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandFailedEvent": {
|
||||
"commandName": "getMore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"commandName": "getMore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandFailedEvent": {
|
||||
"commandName": "getMore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"commandName": "killCursors"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandSucceededEvent": {
|
||||
"commandName": "killCursors"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -4,6 +4,7 @@
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "8.2.0",
|
||||
"maxServerVersion": "8.99.99",
|
||||
"topologies": [
|
||||
"replicaset",
|
||||
"sharded",
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "8.2.0",
|
||||
"maxServerVersion": "8.99.99",
|
||||
"topologies": [
|
||||
"replicaset",
|
||||
"sharded",
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "8.2.0",
|
||||
"maxServerVersion": "8.99.99",
|
||||
"topologies": [
|
||||
"replicaset",
|
||||
"sharded",
|
||||
|
||||
@ -126,7 +126,7 @@
|
||||
],
|
||||
"tests": [
|
||||
{
|
||||
"description": "Insert QE suffixPreview",
|
||||
"description": "Insert QE substringPreview",
|
||||
"operations": [
|
||||
{
|
||||
"name": "insertOne",
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "8.2.0",
|
||||
"maxServerVersion": "8.99.99",
|
||||
"topologies": [
|
||||
"replicaset",
|
||||
"sharded",
|
||||
|
||||
@ -0,0 +1,485 @@
|
||||
{
|
||||
"description": "fle2v2-InsertFind-keyAltName",
|
||||
"schemaVersion": "1.25",
|
||||
"runOnRequirements": [
|
||||
{
|
||||
"minServerVersion": "7.0.0",
|
||||
"topologies": [
|
||||
"replicaset",
|
||||
"sharded",
|
||||
"load-balanced"
|
||||
],
|
||||
"csfle": {
|
||||
"minLibmongocryptVersion": "1.18.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"createEntities": [
|
||||
{
|
||||
"client": {
|
||||
"id": "client0",
|
||||
"autoEncryptOpts": {
|
||||
"keyVaultNamespace": "keyvault.datakeys",
|
||||
"kmsProviders": {
|
||||
"local": {
|
||||
"key": "Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk"
|
||||
}
|
||||
},
|
||||
"encryptedFieldsMap": {
|
||||
"default.default": {
|
||||
"fields": [
|
||||
{
|
||||
"path": "encryptedIndexed",
|
||||
"bsonType": "string",
|
||||
"queries": {
|
||||
"queryType": "equality",
|
||||
"contention": {
|
||||
"$numberLong": "0"
|
||||
}
|
||||
},
|
||||
"keyAltName": "altname"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"database": {
|
||||
"id": "db",
|
||||
"client": "client0",
|
||||
"databaseName": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"collection": {
|
||||
"id": "coll",
|
||||
"database": "db",
|
||||
"collectionName": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client": {
|
||||
"id": "client_unencrypted",
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"database": {
|
||||
"id": "db_unencrypted",
|
||||
"client": "client_unencrypted",
|
||||
"databaseName": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"collection": {
|
||||
"id": "coll_unencrypted",
|
||||
"database": "db_unencrypted",
|
||||
"collectionName": "default"
|
||||
}
|
||||
}
|
||||
],
|
||||
"initialData": [
|
||||
{
|
||||
"databaseName": "default",
|
||||
"collectionName": "default",
|
||||
"documents": [],
|
||||
"createOptions": {
|
||||
"encryptedFields": {
|
||||
"fields": [
|
||||
{
|
||||
"keyId": {
|
||||
"$binary": {
|
||||
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
|
||||
"subType": "04"
|
||||
}
|
||||
},
|
||||
"path": "encryptedIndexed",
|
||||
"bsonType": "string",
|
||||
"queries": {
|
||||
"queryType": "equality",
|
||||
"contention": {
|
||||
"$numberLong": "0"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"databaseName": "keyvault",
|
||||
"collectionName": "datakeys",
|
||||
"documents": [
|
||||
{
|
||||
"_id": {
|
||||
"$binary": {
|
||||
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
|
||||
"subType": "04"
|
||||
}
|
||||
},
|
||||
"keyMaterial": {
|
||||
"$binary": {
|
||||
"base64": "sHe0kz57YW7v8g9VP9sf/+K1ex4JqKc5rf/URX3n3p8XdZ6+15uXPaSayC6adWbNxkFskuMCOifDoTT+rkqMtFkDclOy884RuGGtUysq3X7zkAWYTKi8QAfKkajvVbZl2y23UqgVasdQu3OVBQCrH/xY00nNAs/52e958nVjBuzQkSb1T8pKJAyjZsHJ60+FtnfafDZSTAIBJYn7UWBCwQ==",
|
||||
"subType": "00"
|
||||
}
|
||||
},
|
||||
"creationDate": {
|
||||
"$date": {
|
||||
"$numberLong": "1648914851981"
|
||||
}
|
||||
},
|
||||
"updateDate": {
|
||||
"$date": {
|
||||
"$numberLong": "1648914851981"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"$numberInt": "0"
|
||||
},
|
||||
"masterKey": {
|
||||
"provider": "local"
|
||||
},
|
||||
"keyAltNames": [
|
||||
"altname"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tests": [
|
||||
{
|
||||
"description": "Insert and find FLE2 indexed field",
|
||||
"operations": [
|
||||
{
|
||||
"name": "insertOne",
|
||||
"arguments": {
|
||||
"document": {
|
||||
"_id": 1,
|
||||
"encryptedIndexed": "123"
|
||||
}
|
||||
},
|
||||
"object": "coll"
|
||||
},
|
||||
{
|
||||
"name": "find",
|
||||
"arguments": {
|
||||
"filter": {
|
||||
"encryptedIndexed": "123"
|
||||
}
|
||||
},
|
||||
"object": "coll",
|
||||
"expectResult": [
|
||||
{
|
||||
"_id": 1,
|
||||
"encryptedIndexed": "123"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "find",
|
||||
"object": "coll_unencrypted",
|
||||
"arguments": {
|
||||
"filter": {}
|
||||
},
|
||||
"expectResult": [
|
||||
{
|
||||
"_id": 1,
|
||||
"encryptedIndexed": {
|
||||
"$$type": "binData"
|
||||
},
|
||||
"__safeContent__": [
|
||||
{
|
||||
"$binary": {
|
||||
"base64": "31eCYlbQoVboc5zwC8IoyJVSkag9PxREka8dkmbXJeY=",
|
||||
"subType": "00"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"expectEvents": [
|
||||
{
|
||||
"client": "client0",
|
||||
"events": [
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"find": "datakeys",
|
||||
"filter": {
|
||||
"$or": [
|
||||
{
|
||||
"_id": {
|
||||
"$in": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"keyAltNames": {
|
||||
"$in": [
|
||||
"altname"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"$db": "keyvault",
|
||||
"readConcern": {
|
||||
"level": "majority"
|
||||
}
|
||||
},
|
||||
"commandName": "find"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"insert": "default",
|
||||
"documents": [
|
||||
{
|
||||
"_id": 1,
|
||||
"encryptedIndexed": {
|
||||
"$$type": "binData"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ordered": true,
|
||||
"encryptionInformation": {
|
||||
"type": 1,
|
||||
"schema": {
|
||||
"default.default": {
|
||||
"escCollection": "enxcol_.default.esc",
|
||||
"ecocCollection": "enxcol_.default.ecoc",
|
||||
"fields": [
|
||||
{
|
||||
"keyId": {
|
||||
"$binary": {
|
||||
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
|
||||
"subType": "04"
|
||||
}
|
||||
},
|
||||
"path": "encryptedIndexed",
|
||||
"bsonType": "string",
|
||||
"queries": {
|
||||
"queryType": "equality",
|
||||
"contention": {
|
||||
"$numberLong": "0"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"commandName": "insert"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"find": "default",
|
||||
"filter": {
|
||||
"encryptedIndexed": {
|
||||
"$eq": {
|
||||
"$binary": {
|
||||
"base64": "DIkAAAAFZAAgAAAAAPGmZcUzdE/FPILvRSyAScGvZparGI2y9rJ/vSBxgCujBXMAIAAAAACi1RjmndKqgnXy7xb22RzUbnZl1sOZRXPOC0KcJkAxmQVsACAAAAAApJtKPW4+o9B7gAynNLL26jtlB4+hq5TXResijcYet8USY20AAAAAAAAAAAAA",
|
||||
"subType": "06"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"encryptionInformation": {
|
||||
"type": 1,
|
||||
"schema": {
|
||||
"default.default": {
|
||||
"escCollection": "enxcol_.default.esc",
|
||||
"ecocCollection": "enxcol_.default.ecoc",
|
||||
"fields": [
|
||||
{
|
||||
"keyId": {
|
||||
"$binary": {
|
||||
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
|
||||
"subType": "04"
|
||||
}
|
||||
},
|
||||
"path": "encryptedIndexed",
|
||||
"bsonType": "string",
|
||||
"queries": {
|
||||
"queryType": "equality",
|
||||
"contention": {
|
||||
"$numberLong": "0"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"commandName": "find"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Create translates keyAltName",
|
||||
"operations": [
|
||||
{
|
||||
"name": "dropCollection",
|
||||
"object": "db",
|
||||
"arguments": {
|
||||
"collection": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "createCollection",
|
||||
"object": "db",
|
||||
"arguments": {
|
||||
"collection": "default"
|
||||
}
|
||||
}
|
||||
],
|
||||
"expectEvents": [
|
||||
{
|
||||
"client": "client0",
|
||||
"events": [
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"drop": "enxcol_.default.esc"
|
||||
},
|
||||
"commandName": "drop"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"drop": "enxcol_.default.ecoc"
|
||||
},
|
||||
"commandName": "drop"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"drop": "default"
|
||||
},
|
||||
"commandName": "drop"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"create": "enxcol_.default.esc",
|
||||
"clusteredIndex": {
|
||||
"key": {
|
||||
"_id": 1
|
||||
},
|
||||
"unique": true
|
||||
}
|
||||
},
|
||||
"commandName": "create"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"create": "enxcol_.default.ecoc",
|
||||
"clusteredIndex": {
|
||||
"key": {
|
||||
"_id": 1
|
||||
},
|
||||
"unique": true
|
||||
}
|
||||
},
|
||||
"commandName": "create"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"find": "datakeys",
|
||||
"filter": {
|
||||
"$or": [
|
||||
{
|
||||
"_id": {
|
||||
"$in": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"keyAltNames": {
|
||||
"$in": [
|
||||
"altname"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"$db": "keyvault",
|
||||
"readConcern": {
|
||||
"level": "majority"
|
||||
}
|
||||
},
|
||||
"commandName": "find"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"create": "default",
|
||||
"encryptedFields": {
|
||||
"fields": [
|
||||
{
|
||||
"path": "encryptedIndexed",
|
||||
"bsonType": "string",
|
||||
"queries": {
|
||||
"queryType": "equality",
|
||||
"contention": {
|
||||
"$numberLong": "0"
|
||||
}
|
||||
},
|
||||
"keyId": {
|
||||
"$binary": {
|
||||
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
|
||||
"subType": "04"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"commandName": "create"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"command": {
|
||||
"createIndexes": "default",
|
||||
"indexes": [
|
||||
{
|
||||
"name": "__safeContent___1",
|
||||
"key": {
|
||||
"__safeContent__": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"commandName": "createIndexes"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -27,7 +27,8 @@
|
||||
"awaitMinPoolSizeMS": 10000,
|
||||
"useMultipleMongoses": false,
|
||||
"observeEvents": [
|
||||
"commandStartedEvent"
|
||||
"commandStartedEvent",
|
||||
"commandFailedEvent"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -188,6 +189,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandFailedEvent": {
|
||||
"commandName": "insert"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"commandName": "abortTransaction",
|
||||
@ -206,6 +212,105 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "withTransaction surfaces a timeout after exhausting transient transaction retries, retaining the last transient error as the timeout cause.",
|
||||
"operations": [
|
||||
{
|
||||
"name": "failPoint",
|
||||
"object": "testRunner",
|
||||
"arguments": {
|
||||
"client": "failPointClient",
|
||||
"failPoint": {
|
||||
"configureFailPoint": "failCommand",
|
||||
"mode": "alwaysOn",
|
||||
"data": {
|
||||
"failCommands": [
|
||||
"insert"
|
||||
],
|
||||
"blockConnection": true,
|
||||
"blockTimeMS": 25,
|
||||
"errorCode": 24,
|
||||
"errorLabels": [
|
||||
"TransientTransactionError"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "withTransaction",
|
||||
"object": "session",
|
||||
"arguments": {
|
||||
"callback": [
|
||||
{
|
||||
"name": "insertOne",
|
||||
"object": "collection",
|
||||
"arguments": {
|
||||
"document": {
|
||||
"_id": 1
|
||||
},
|
||||
"session": "session"
|
||||
},
|
||||
"expectError": {
|
||||
"isError": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectError": {
|
||||
"isTimeoutError": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"expectEvents": [
|
||||
{
|
||||
"client": "client",
|
||||
"ignoreExtraEvents": true,
|
||||
"events": [
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"commandName": "insert"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandFailedEvent": {
|
||||
"commandName": "insert"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"commandName": "abortTransaction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandFailedEvent": {
|
||||
"commandName": "abortTransaction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"commandName": "insert"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandFailedEvent": {
|
||||
"commandName": "insert"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandStartedEvent": {
|
||||
"commandName": "abortTransaction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandFailedEvent": {
|
||||
"commandName": "abortTransaction"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Mark server unknown on network timeout application error (beforeHandshakeCompletes)",
|
||||
"description": "Ignore network timeout application error (beforeHandshakeCompletes)",
|
||||
"applicationErrors": [
|
||||
{
|
||||
"address": "a:27017",
|
||||
@ -97,14 +97,22 @@
|
||||
"outcome": {
|
||||
"servers": {
|
||||
"a:27017": {
|
||||
"type": "Unknown",
|
||||
"topologyVersion": null,
|
||||
"type": "RSPrimary",
|
||||
"setName": "rs",
|
||||
"topologyVersion": {
|
||||
"processId": {
|
||||
"$oid": "000000000000000000000001"
|
||||
},
|
||||
"counter": {
|
||||
"$numberLong": "1"
|
||||
}
|
||||
},
|
||||
"pool": {
|
||||
"generation": 1
|
||||
"generation": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"topologyType": "ReplicaSetNoPrimary",
|
||||
"topologyType": "ReplicaSetWithPrimary",
|
||||
"logicalSessionTimeoutMinutes": null,
|
||||
"setName": "rs"
|
||||
}
|
||||
|
||||
@ -0,0 +1,167 @@
|
||||
{
|
||||
"description": "Static setVersion (DSC) is compatible with both pre and post DRIVERS-2412",
|
||||
"uri": "mongodb://a/?replicaSet=rs",
|
||||
"phases": [
|
||||
{
|
||||
"responses": [
|
||||
[
|
||||
"a:27017",
|
||||
{
|
||||
"ok": 1,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": true,
|
||||
"hosts": [
|
||||
"a:27017",
|
||||
"b:27017"
|
||||
],
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000005"
|
||||
},
|
||||
"minWireVersion": 0,
|
||||
"maxWireVersion": 17
|
||||
}
|
||||
],
|
||||
[
|
||||
"b:27017",
|
||||
{
|
||||
"ok": 1,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": false,
|
||||
"secondary": true,
|
||||
"hosts": [
|
||||
"a:27017",
|
||||
"b:27017"
|
||||
],
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"minWireVersion": 0,
|
||||
"maxWireVersion": 17
|
||||
}
|
||||
]
|
||||
],
|
||||
"outcome": {
|
||||
"servers": {
|
||||
"a:27017": {
|
||||
"type": "RSPrimary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000005"
|
||||
}
|
||||
},
|
||||
"b:27017": {
|
||||
"type": "RSSecondary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": null
|
||||
}
|
||||
},
|
||||
"topologyType": "ReplicaSetWithPrimary",
|
||||
"logicalSessionTimeoutMinutes": null,
|
||||
"setName": "rs",
|
||||
"maxSetVersion": 1,
|
||||
"maxElectionId": {
|
||||
"$oid": "000000000000000000000005"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"responses": [
|
||||
[
|
||||
"b:27017",
|
||||
{
|
||||
"ok": 1,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": true,
|
||||
"hosts": [
|
||||
"a:27017",
|
||||
"b:27017"
|
||||
],
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000006"
|
||||
},
|
||||
"minWireVersion": 0,
|
||||
"maxWireVersion": 17
|
||||
}
|
||||
]
|
||||
],
|
||||
"outcome": {
|
||||
"servers": {
|
||||
"a:27017": {
|
||||
"type": "Unknown",
|
||||
"setName": null,
|
||||
"setVersion": null,
|
||||
"electionId": null
|
||||
},
|
||||
"b:27017": {
|
||||
"type": "RSPrimary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000006"
|
||||
}
|
||||
}
|
||||
},
|
||||
"topologyType": "ReplicaSetWithPrimary",
|
||||
"logicalSessionTimeoutMinutes": null,
|
||||
"setName": "rs",
|
||||
"maxSetVersion": 1,
|
||||
"maxElectionId": {
|
||||
"$oid": "000000000000000000000006"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"responses": [
|
||||
[
|
||||
"a:27017",
|
||||
{
|
||||
"ok": 1,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": true,
|
||||
"hosts": [
|
||||
"a:27017",
|
||||
"b:27017"
|
||||
],
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000005"
|
||||
},
|
||||
"minWireVersion": 0,
|
||||
"maxWireVersion": 17
|
||||
}
|
||||
]
|
||||
],
|
||||
"outcome": {
|
||||
"servers": {
|
||||
"a:27017": {
|
||||
"type": "Unknown",
|
||||
"setName": null,
|
||||
"setVersion": null,
|
||||
"electionId": null
|
||||
},
|
||||
"b:27017": {
|
||||
"type": "RSPrimary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000006"
|
||||
}
|
||||
}
|
||||
},
|
||||
"topologyType": "ReplicaSetWithPrimary",
|
||||
"logicalSessionTimeoutMinutes": null,
|
||||
"setName": "rs",
|
||||
"maxSetVersion": 1,
|
||||
"maxElectionId": {
|
||||
"$oid": "000000000000000000000006"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,227 @@
|
||||
{
|
||||
"description": "Member list is updated when setVersion and electionId remain the same",
|
||||
"uri": "mongodb://a/?replicaSet=rs",
|
||||
"phases": [
|
||||
{
|
||||
"responses": [
|
||||
[
|
||||
"a:27017",
|
||||
{
|
||||
"ok": 1,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": true,
|
||||
"hosts": [
|
||||
"a:27017",
|
||||
"b:27017"
|
||||
],
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000001"
|
||||
},
|
||||
"minWireVersion": 0,
|
||||
"maxWireVersion": 17
|
||||
}
|
||||
],
|
||||
[
|
||||
"b:27017",
|
||||
{
|
||||
"ok": 1,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": false,
|
||||
"secondary": true,
|
||||
"hosts": [
|
||||
"a:27017",
|
||||
"b:27017"
|
||||
],
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"minWireVersion": 0,
|
||||
"maxWireVersion": 17
|
||||
}
|
||||
]
|
||||
],
|
||||
"outcome": {
|
||||
"servers": {
|
||||
"a:27017": {
|
||||
"type": "RSPrimary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000001"
|
||||
}
|
||||
},
|
||||
"b:27017": {
|
||||
"type": "RSSecondary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": null
|
||||
}
|
||||
},
|
||||
"topologyType": "ReplicaSetWithPrimary",
|
||||
"logicalSessionTimeoutMinutes": null,
|
||||
"setName": "rs",
|
||||
"maxSetVersion": 1,
|
||||
"maxElectionId": {
|
||||
"$oid": "000000000000000000000001"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"responses": [
|
||||
[
|
||||
"a:27017",
|
||||
{
|
||||
"ok": 1,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": true,
|
||||
"hosts": [
|
||||
"a:27017",
|
||||
"b:27017",
|
||||
"c:27017"
|
||||
],
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000001"
|
||||
},
|
||||
"minWireVersion": 0,
|
||||
"maxWireVersion": 17
|
||||
}
|
||||
]
|
||||
],
|
||||
"outcome": {
|
||||
"servers": {
|
||||
"a:27017": {
|
||||
"type": "RSPrimary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000001"
|
||||
}
|
||||
},
|
||||
"b:27017": {
|
||||
"type": "RSSecondary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": null
|
||||
},
|
||||
"c:27017": {
|
||||
"type": "Unknown",
|
||||
"setName": null,
|
||||
"setVersion": null,
|
||||
"electionId": null
|
||||
}
|
||||
},
|
||||
"topologyType": "ReplicaSetWithPrimary",
|
||||
"logicalSessionTimeoutMinutes": null,
|
||||
"setName": "rs",
|
||||
"maxSetVersion": 1,
|
||||
"maxElectionId": {
|
||||
"$oid": "000000000000000000000001"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"responses": [
|
||||
[
|
||||
"c:27017",
|
||||
{
|
||||
"ok": 1,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": false,
|
||||
"secondary": true,
|
||||
"hosts": [
|
||||
"a:27017",
|
||||
"b:27017",
|
||||
"c:27017"
|
||||
],
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"minWireVersion": 0,
|
||||
"maxWireVersion": 17
|
||||
}
|
||||
]
|
||||
],
|
||||
"outcome": {
|
||||
"servers": {
|
||||
"a:27017": {
|
||||
"type": "RSPrimary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000001"
|
||||
}
|
||||
},
|
||||
"b:27017": {
|
||||
"type": "RSSecondary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": null
|
||||
},
|
||||
"c:27017": {
|
||||
"type": "RSSecondary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": null
|
||||
}
|
||||
},
|
||||
"topologyType": "ReplicaSetWithPrimary",
|
||||
"logicalSessionTimeoutMinutes": null,
|
||||
"setName": "rs",
|
||||
"maxSetVersion": 1,
|
||||
"maxElectionId": {
|
||||
"$oid": "000000000000000000000001"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"responses": [
|
||||
[
|
||||
"a:27017",
|
||||
{
|
||||
"ok": 1,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": true,
|
||||
"hosts": [
|
||||
"a:27017",
|
||||
"b:27017"
|
||||
],
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000001"
|
||||
},
|
||||
"minWireVersion": 0,
|
||||
"maxWireVersion": 17
|
||||
}
|
||||
]
|
||||
],
|
||||
"outcome": {
|
||||
"servers": {
|
||||
"a:27017": {
|
||||
"type": "RSPrimary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000001"
|
||||
}
|
||||
},
|
||||
"b:27017": {
|
||||
"type": "RSSecondary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": null
|
||||
}
|
||||
},
|
||||
"topologyType": "ReplicaSetWithPrimary",
|
||||
"logicalSessionTimeoutMinutes": null,
|
||||
"setName": "rs",
|
||||
"maxSetVersion": 1,
|
||||
"maxElectionId": {
|
||||
"$oid": "000000000000000000000001"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,167 @@
|
||||
{
|
||||
"description": "DSC to ASC reverse migration - ASC primary with higher setVersion is accepted",
|
||||
"uri": "mongodb://a/?replicaSet=rs",
|
||||
"phases": [
|
||||
{
|
||||
"responses": [
|
||||
[
|
||||
"a:27017",
|
||||
{
|
||||
"ok": 1,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": true,
|
||||
"hosts": [
|
||||
"a:27017",
|
||||
"b:27017"
|
||||
],
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000005"
|
||||
},
|
||||
"minWireVersion": 0,
|
||||
"maxWireVersion": 17
|
||||
}
|
||||
],
|
||||
[
|
||||
"b:27017",
|
||||
{
|
||||
"ok": 1,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": false,
|
||||
"secondary": true,
|
||||
"hosts": [
|
||||
"a:27017",
|
||||
"b:27017"
|
||||
],
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"minWireVersion": 0,
|
||||
"maxWireVersion": 17
|
||||
}
|
||||
]
|
||||
],
|
||||
"outcome": {
|
||||
"servers": {
|
||||
"a:27017": {
|
||||
"type": "RSPrimary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000005"
|
||||
}
|
||||
},
|
||||
"b:27017": {
|
||||
"type": "RSSecondary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": null
|
||||
}
|
||||
},
|
||||
"topologyType": "ReplicaSetWithPrimary",
|
||||
"logicalSessionTimeoutMinutes": null,
|
||||
"setName": "rs",
|
||||
"maxSetVersion": 1,
|
||||
"maxElectionId": {
|
||||
"$oid": "000000000000000000000005"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"responses": [
|
||||
[
|
||||
"b:27017",
|
||||
{
|
||||
"ok": 1,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": true,
|
||||
"hosts": [
|
||||
"a:27017",
|
||||
"b:27017"
|
||||
],
|
||||
"setName": "rs",
|
||||
"setVersion": 1000,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000006"
|
||||
},
|
||||
"minWireVersion": 0,
|
||||
"maxWireVersion": 17
|
||||
}
|
||||
]
|
||||
],
|
||||
"outcome": {
|
||||
"servers": {
|
||||
"a:27017": {
|
||||
"type": "Unknown",
|
||||
"setName": null,
|
||||
"setVersion": null,
|
||||
"electionId": null
|
||||
},
|
||||
"b:27017": {
|
||||
"type": "RSPrimary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1000,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000006"
|
||||
}
|
||||
}
|
||||
},
|
||||
"topologyType": "ReplicaSetWithPrimary",
|
||||
"logicalSessionTimeoutMinutes": null,
|
||||
"setName": "rs",
|
||||
"maxSetVersion": 1000,
|
||||
"maxElectionId": {
|
||||
"$oid": "000000000000000000000006"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"responses": [
|
||||
[
|
||||
"a:27017",
|
||||
{
|
||||
"ok": 1,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": true,
|
||||
"hosts": [
|
||||
"a:27017",
|
||||
"b:27017"
|
||||
],
|
||||
"setName": "rs",
|
||||
"setVersion": 1,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000005"
|
||||
},
|
||||
"minWireVersion": 0,
|
||||
"maxWireVersion": 17
|
||||
}
|
||||
]
|
||||
],
|
||||
"outcome": {
|
||||
"servers": {
|
||||
"a:27017": {
|
||||
"type": "Unknown",
|
||||
"setName": null,
|
||||
"setVersion": null,
|
||||
"electionId": null
|
||||
},
|
||||
"b:27017": {
|
||||
"type": "RSPrimary",
|
||||
"setName": "rs",
|
||||
"setVersion": 1000,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000006"
|
||||
}
|
||||
}
|
||||
},
|
||||
"topologyType": "ReplicaSetWithPrimary",
|
||||
"logicalSessionTimeoutMinutes": null,
|
||||
"setName": "rs",
|
||||
"maxSetVersion": 1000,
|
||||
"maxElectionId": {
|
||||
"$oid": "000000000000000000000006"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
{
|
||||
"description": "ASC to DSC forward migration - DSC uses setVersionASC + 1 to prevent false stale detection",
|
||||
"uri": "mongodb://a/?replicaSet=rs",
|
||||
"phases": [
|
||||
{
|
||||
"responses": [
|
||||
[
|
||||
"a:27017",
|
||||
{
|
||||
"ok": 1,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": true,
|
||||
"hosts": [
|
||||
"a:27017",
|
||||
"b:27017"
|
||||
],
|
||||
"setName": "rs",
|
||||
"setVersion": 10,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000005"
|
||||
},
|
||||
"minWireVersion": 0,
|
||||
"maxWireVersion": 17
|
||||
}
|
||||
],
|
||||
[
|
||||
"b:27017",
|
||||
{
|
||||
"ok": 1,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": false,
|
||||
"secondary": true,
|
||||
"hosts": [
|
||||
"a:27017",
|
||||
"b:27017"
|
||||
],
|
||||
"setName": "rs",
|
||||
"setVersion": 10,
|
||||
"minWireVersion": 0,
|
||||
"maxWireVersion": 17
|
||||
}
|
||||
]
|
||||
],
|
||||
"outcome": {
|
||||
"servers": {
|
||||
"a:27017": {
|
||||
"type": "RSPrimary",
|
||||
"setName": "rs",
|
||||
"setVersion": 10,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000005"
|
||||
}
|
||||
},
|
||||
"b:27017": {
|
||||
"type": "RSSecondary",
|
||||
"setName": "rs",
|
||||
"setVersion": 10,
|
||||
"electionId": null
|
||||
}
|
||||
},
|
||||
"topologyType": "ReplicaSetWithPrimary",
|
||||
"logicalSessionTimeoutMinutes": null,
|
||||
"setName": "rs",
|
||||
"maxSetVersion": 10,
|
||||
"maxElectionId": {
|
||||
"$oid": "000000000000000000000005"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"responses": [
|
||||
[
|
||||
"a:27017",
|
||||
{
|
||||
"ok": 1,
|
||||
"helloOk": true,
|
||||
"isWritablePrimary": true,
|
||||
"hosts": [
|
||||
"a:27017",
|
||||
"b:27017"
|
||||
],
|
||||
"setName": "rs",
|
||||
"setVersion": 11,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000006"
|
||||
},
|
||||
"minWireVersion": 0,
|
||||
"maxWireVersion": 17
|
||||
}
|
||||
]
|
||||
],
|
||||
"outcome": {
|
||||
"servers": {
|
||||
"a:27017": {
|
||||
"type": "RSPrimary",
|
||||
"setName": "rs",
|
||||
"setVersion": 11,
|
||||
"electionId": {
|
||||
"$oid": "000000000000000000000006"
|
||||
}
|
||||
},
|
||||
"b:27017": {
|
||||
"type": "RSSecondary",
|
||||
"setName": "rs",
|
||||
"setVersion": 10,
|
||||
"electionId": null
|
||||
}
|
||||
},
|
||||
"topologyType": "ReplicaSetWithPrimary",
|
||||
"logicalSessionTimeoutMinutes": null,
|
||||
"setName": "rs",
|
||||
"maxSetVersion": 11,
|
||||
"maxElectionId": {
|
||||
"$oid": "000000000000000000000006"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
{
|
||||
"topology_description": {
|
||||
"type": "ReplicaSetNoPrimary",
|
||||
"servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "c:27017",
|
||||
"avg_rtt_ms": 100,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "tokyo"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"operation": "read",
|
||||
"read_preference": {
|
||||
"mode": "Nearest",
|
||||
"tag_sets": [
|
||||
{
|
||||
"data_center": "nyc"
|
||||
}
|
||||
]
|
||||
},
|
||||
"deprioritized_servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"suitable_servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"in_latency_window": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
{
|
||||
"topology_description": {
|
||||
"type": "ReplicaSetNoPrimary",
|
||||
"servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "c:27017",
|
||||
"avg_rtt_ms": 100,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "tokyo"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"operation": "read",
|
||||
"read_preference": {
|
||||
"mode": "PrimaryPreferred",
|
||||
"tag_sets": [
|
||||
{
|
||||
"data_center": "nyc"
|
||||
}
|
||||
]
|
||||
},
|
||||
"deprioritized_servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"suitable_servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"in_latency_window": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
{
|
||||
"topology_description": {
|
||||
"type": "ReplicaSetNoPrimary",
|
||||
"servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "c:27017",
|
||||
"avg_rtt_ms": 100,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "tokyo"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"operation": "read",
|
||||
"read_preference": {
|
||||
"mode": "Secondary",
|
||||
"tag_sets": [
|
||||
{
|
||||
"data_center": "nyc"
|
||||
}
|
||||
]
|
||||
},
|
||||
"deprioritized_servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"suitable_servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"in_latency_window": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
{
|
||||
"topology_description": {
|
||||
"type": "ReplicaSetNoPrimary",
|
||||
"servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "c:27017",
|
||||
"avg_rtt_ms": 100,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "tokyo"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"operation": "read",
|
||||
"read_preference": {
|
||||
"mode": "SecondaryPreferred",
|
||||
"tag_sets": [
|
||||
{
|
||||
"data_center": "nyc"
|
||||
}
|
||||
]
|
||||
},
|
||||
"deprioritized_servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"suitable_servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"in_latency_window": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
{
|
||||
"topology_description": {
|
||||
"type": "ReplicaSetWithPrimary",
|
||||
"servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "c:27017",
|
||||
"avg_rtt_ms": 100,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "tokyo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "a:27017",
|
||||
"avg_rtt_ms": 26,
|
||||
"type": "RSPrimary",
|
||||
"tags": {
|
||||
"data_center": "tokyo"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"operation": "read",
|
||||
"read_preference": {
|
||||
"mode": "Nearest",
|
||||
"tag_sets": [
|
||||
{
|
||||
"data_center": "nyc"
|
||||
}
|
||||
]
|
||||
},
|
||||
"deprioritized_servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"suitable_servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"in_latency_window": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
{
|
||||
"topology_description": {
|
||||
"type": "ReplicaSetWithPrimary",
|
||||
"servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "tokyo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "c:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "tokyo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "a:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSPrimary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"operation": "read",
|
||||
"read_preference": {
|
||||
"mode": "PrimaryPreferred",
|
||||
"tag_sets": [
|
||||
{
|
||||
"data_center": "nyc"
|
||||
}
|
||||
]
|
||||
},
|
||||
"deprioritized_servers": [
|
||||
{
|
||||
"address": "a:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSPrimary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"suitable_servers": [
|
||||
{
|
||||
"address": "a:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSPrimary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"in_latency_window": [
|
||||
{
|
||||
"address": "a:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSPrimary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
{
|
||||
"topology_description": {
|
||||
"type": "ReplicaSetWithPrimary",
|
||||
"servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "c:27017",
|
||||
"avg_rtt_ms": 100,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "tokyo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "a:27017",
|
||||
"avg_rtt_ms": 26,
|
||||
"type": "RSPrimary",
|
||||
"tags": {
|
||||
"data_center": "tokyo"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"operation": "read",
|
||||
"read_preference": {
|
||||
"mode": "Secondary",
|
||||
"tag_sets": [
|
||||
{
|
||||
"data_center": "nyc"
|
||||
}
|
||||
]
|
||||
},
|
||||
"deprioritized_servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"suitable_servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"in_latency_window": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
{
|
||||
"topology_description": {
|
||||
"type": "ReplicaSetWithPrimary",
|
||||
"servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "c:27017",
|
||||
"avg_rtt_ms": 100,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "tokyo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "a:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSPrimary",
|
||||
"tags": {
|
||||
"data_center": "tokyo"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"operation": "read",
|
||||
"read_preference": {
|
||||
"mode": "SecondaryPreferred",
|
||||
"tag_sets": [
|
||||
{
|
||||
"data_center": "nyc"
|
||||
}
|
||||
]
|
||||
},
|
||||
"deprioritized_servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary",
|
||||
"tags": {
|
||||
"data_center": "nyc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"suitable_servers": [
|
||||
{
|
||||
"address": "a:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSPrimary",
|
||||
"tags": {
|
||||
"data_center": "tokyo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"in_latency_window": [
|
||||
{
|
||||
"address": "a:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSPrimary",
|
||||
"tags": {
|
||||
"data_center": "tokyo"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
{
|
||||
"topology_description": {
|
||||
"type": "ReplicaSetWithPrimary",
|
||||
"servers": [
|
||||
{
|
||||
"address": "a:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSPrimary"
|
||||
},
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary"
|
||||
}
|
||||
]
|
||||
},
|
||||
"operation": "read",
|
||||
"read_preference": {
|
||||
"mode": "SecondaryPreferred",
|
||||
"tag_sets": [
|
||||
{
|
||||
"data_center": "nyc"
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"suitable_servers": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary"
|
||||
}
|
||||
],
|
||||
"in_latency_window": [
|
||||
{
|
||||
"address": "b:27017",
|
||||
"avg_rtt_ms": 5,
|
||||
"type": "RSSecondary"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -104,14 +104,16 @@ class OIDCTestBase(PyMongoTestCase):
|
||||
|
||||
@contextmanager
|
||||
def fail_point(self, command_args):
|
||||
cmd_on = SON([("configureFailPoint", "failCommand")])
|
||||
cmd_on = dict(configureFailPoint="failCommand", appName="auth_oidc")
|
||||
cmd_on.update(command_args)
|
||||
client = MongoClient(self.uri_admin)
|
||||
client.admin.command(cmd_on)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
client.admin.command("configureFailPoint", cmd_on["configureFailPoint"], mode="off")
|
||||
client.admin.command(
|
||||
"configureFailPoint", cmd_on["configureFailPoint"], mode="off", appName="auth_oidc"
|
||||
)
|
||||
client.close()
|
||||
|
||||
|
||||
|
||||
169
test/test_azure_helpers.py
Normal file
169
test/test_azure_helpers.py
Normal file
@ -0,0 +1,169 @@
|
||||
# Copyright 2026-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.
|
||||
|
||||
"""Unit tests for _azure_helpers.py.
|
||||
|
||||
These tests mock urlopen to avoid requiring a live Azure IMDS endpoint.
|
||||
Integration tests that exercise the real endpoint are gated by environment
|
||||
variables in test_on_demand_csfle.py and test_auth_oidc.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
from pymongo._azure_helpers import _get_azure_response
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _mock_urlopen(status: int, body: str):
|
||||
"""Context manager that patches ``urllib.request.urlopen`` with a fake response."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.__enter__ = lambda s: s
|
||||
mock_response.__exit__ = MagicMock(return_value=False)
|
||||
mock_response.status = status
|
||||
mock_response.read.return_value = body.encode("utf8")
|
||||
|
||||
with patch("urllib.request.urlopen", return_value=mock_response) as mock_open:
|
||||
yield mock_open
|
||||
|
||||
|
||||
class TestGetAzureResponse(unittest.TestCase):
|
||||
def _call(self, resource="https://example.com/", client_id=None, timeout=5):
|
||||
return _get_azure_response(resource, client_id=client_id, timeout=timeout)
|
||||
|
||||
def test_success_without_client_id(self):
|
||||
body = json.dumps({"access_token": "tok", "expires_in": "3600"})
|
||||
with _mock_urlopen(200, body) as mock_open:
|
||||
result = self._call()
|
||||
|
||||
self.assertEqual(result["access_token"], "tok")
|
||||
self.assertEqual(result["expires_in"], "3600")
|
||||
|
||||
# Verify client_id was NOT added to the URL
|
||||
url = mock_open.call_args[0][0].full_url
|
||||
self.assertNotIn("client_id", url)
|
||||
|
||||
def test_success_with_client_id(self):
|
||||
body = json.dumps({"access_token": "tok", "expires_in": "3600"})
|
||||
with _mock_urlopen(200, body) as mock_open:
|
||||
result = self._call(client_id="my-client-id")
|
||||
|
||||
self.assertEqual(result["access_token"], "tok")
|
||||
url = mock_open.call_args[0][0].full_url
|
||||
self.assertIn("client_id=my-client-id", url)
|
||||
|
||||
def test_url_contains_resource_and_api_version(self):
|
||||
body = json.dumps({"access_token": "tok", "expires_in": "3600"})
|
||||
with _mock_urlopen(200, body) as mock_open:
|
||||
self._call(resource="https://test-resource.example.com")
|
||||
|
||||
url = mock_open.call_args[0][0].full_url
|
||||
self.assertIn("api-version=2018-02-01", url)
|
||||
self.assertIn("resource=https://test-resource.example.com", url)
|
||||
|
||||
def test_request_headers(self):
|
||||
body = json.dumps({"access_token": "tok", "expires_in": "3600"})
|
||||
with _mock_urlopen(200, body) as mock_open:
|
||||
self._call()
|
||||
|
||||
request = mock_open.call_args[0][0]
|
||||
self.assertEqual(request.get_header("Metadata"), "true")
|
||||
self.assertEqual(request.get_header("Accept"), "application/json")
|
||||
|
||||
def test_urlopen_exception_raises_value_error(self):
|
||||
with patch("urllib.request.urlopen", side_effect=OSError("connection refused")):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
self._call()
|
||||
|
||||
self.assertIn("Failed to acquire IMDS access token", str(ctx.exception))
|
||||
|
||||
def test_non_200_status_raises_value_error(self):
|
||||
body = json.dumps({"error": "something went wrong"})
|
||||
with _mock_urlopen(400, body):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
self._call()
|
||||
|
||||
self.assertIn("Failed to acquire IMDS access token", str(ctx.exception))
|
||||
|
||||
def test_non_json_body_raises_value_error(self):
|
||||
with _mock_urlopen(200, "not-json"):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
self._call()
|
||||
|
||||
self.assertIn("Azure IMDS response must be in JSON format", str(ctx.exception))
|
||||
|
||||
def test_missing_access_token_raises_value_error(self):
|
||||
body = json.dumps({"expires_in": "3600"})
|
||||
with _mock_urlopen(200, body):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
self._call()
|
||||
|
||||
self.assertIn("access_token", str(ctx.exception))
|
||||
|
||||
def test_missing_expires_in_raises_value_error(self):
|
||||
body = json.dumps({"access_token": "tok"})
|
||||
with _mock_urlopen(200, body):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
self._call()
|
||||
|
||||
self.assertIn("expires_in", str(ctx.exception))
|
||||
|
||||
def test_empty_access_token_raises_value_error(self):
|
||||
body = json.dumps({"access_token": "", "expires_in": "3600"})
|
||||
with _mock_urlopen(200, body):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
self._call()
|
||||
|
||||
self.assertIn("access_token", str(ctx.exception))
|
||||
|
||||
def test_empty_expires_in_raises_value_error(self):
|
||||
body = json.dumps({"access_token": "tok", "expires_in": ""})
|
||||
with _mock_urlopen(200, body):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
self._call()
|
||||
|
||||
self.assertIn("expires_in", str(ctx.exception))
|
||||
|
||||
def test_timeout_passed_to_urlopen(self):
|
||||
body = json.dumps({"access_token": "tok", "expires_in": "3600"})
|
||||
with _mock_urlopen(200, body) as mock_open:
|
||||
self._call(timeout=42)
|
||||
|
||||
_, kwargs = mock_open.call_args
|
||||
self.assertEqual(kwargs["timeout"], 42)
|
||||
|
||||
def test_client_id_is_url_encoded(self):
|
||||
"""Ensure special characters in client_id are percent-encoded."""
|
||||
body = json.dumps({"access_token": "tok", "expires_in": "3600"})
|
||||
with _mock_urlopen(200, body) as mock_open:
|
||||
self._call(client_id="id with spaces&special=chars")
|
||||
|
||||
url = mock_open.call_args[0][0].full_url
|
||||
# '&' and '=' must be percent-encoded so they don't inject extra query params
|
||||
self.assertIn("client_id=id%20with%20spaces%26special%3Dchars", url)
|
||||
# The encoded client_id should not introduce a raw '&'
|
||||
# Count params: api-version, resource, client_id — exactly 3
|
||||
query_string = url.split("?", 1)[1]
|
||||
self.assertEqual(query_string.count("&"), 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user