From 32494ab61621157471244d6ecbf430dc161e2ab3 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:24:47 +0100 Subject: [PATCH 1/7] Bump cryptography from `46.0.7` to `50.0.1` to resolve several CVEs; bump `pyOpenSSL` as well (#20253) --- changelog.d/20253.misc | 1 + poetry.lock | 120 +++++++++++++++++++---------------------- 2 files changed, 56 insertions(+), 65 deletions(-) create mode 100644 changelog.d/20253.misc diff --git a/changelog.d/20253.misc b/changelog.d/20253.misc new file mode 100644 index 0000000000..54bf15e0e9 --- /dev/null +++ b/changelog.d/20253.misc @@ -0,0 +1 @@ +Bump `cryptography` from `46.0.7` to `50.0.1` to resolve `CVE-2026-69248`, `CVE-2026-69249` and `CVE-2026-69247`, none of which affected Synapse. Also bump `pyOpenSSL` from `26.0.0` to `26.4.0` for compatibility with newer `cryptography` versions. \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 193041e3cf..9c11454366 100644 --- a/poetry.lock +++ b/poetry.lock @@ -454,76 +454,66 @@ files = [ [[package]] name = "cryptography" -version = "46.0.7" +version = "50.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = "!=3.9.0,!=3.9.1,>=3.8" +python-versions = "!=3.9.0,!=3.9.1,>=3.9" groups = ["main", "dev"] files = [ - {file = "cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b"}, - {file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85"}, - {file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e"}, - {file = "cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457"}, - {file = "cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b"}, - {file = "cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2"}, - {file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e"}, - {file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee"}, - {file = "cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298"}, - {file = "cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb"}, - {file = "cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0"}, - {file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85"}, - {file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e"}, - {file = "cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246"}, - {file = "cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4"}, - {file = "cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5"}, + {file = "cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a"}, + {file = "cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959"}, + {file = "cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b"}, + {file = "cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648"}, + {file = "cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3"}, + {file = "cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6"}, + {file = "cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149"}, + {file = "cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf"}, + {file = "cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239"}, + {file = "cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558"}, + {file = "cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e"}, + {file = "cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b"}, + {file = "cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20"}, ] [package.dependencies] -cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} +cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""} typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] -docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox[uv] (>=2024.4.15)"] -pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] -sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==46.0.7)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] -test-randomorder = ["pytest-randomly"] [[package]] name = "defusedxml" @@ -2392,19 +2382,19 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", " [[package]] name = "pyopenssl" -version = "26.0.0" +version = "26.4.0" description = "Python wrapper module around the OpenSSL library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81"}, - {file = "pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc"}, + {file = "pyopenssl-26.4.0-py3-none-any.whl", hash = "sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c"}, + {file = "pyopenssl-26.4.0.tar.gz", hash = "sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7"}, ] [package.dependencies] -cryptography = ">=46.0.0,<47" -typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\" and python_version >= \"3.8\""} +cryptography = ">=49.0.0,<51" +typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\""} [package.extras] docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"] From 81bc4c8c74bae7d815473837355bdf51f18ccc43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:51:06 +0000 Subject: [PATCH 2/7] Bump pyjwt from 2.13.0 to 2.14.0 (#20291) Signed-off-by: dependabot[bot] --- poetry.lock | 60 ++++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9c11454366..ee805bbacd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -31,7 +31,7 @@ description = "The ultimate Python library in building OAuth and OpenID Connect optional = true python-versions = ">=3.9" groups = ["main"] -markers = "extra == \"oidc\" or extra == \"jwt\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"jwt\" or extra == \"oidc\"" files = [ {file = "authlib-1.6.12-py2.py3-none-any.whl", hash = "sha256:e9229ad7fde610b139dd12f5edbe97eab9ee78bfb85691247e767727850b99ab"}, {file = "authlib-1.6.12.tar.gz", hash = "sha256:0656d8482f28fc8221929d5f35b2bde5d13e10555ebc06b4561b0d622e83b1bd"}, @@ -62,7 +62,7 @@ description = "Backport of CPython tarfile module" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version < \"3.12\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version < \"3.12\"" files = [ {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, @@ -522,7 +522,7 @@ description = "XML bomb protection for Python stdlib modules" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" groups = ["main"] -markers = "extra == \"saml2\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"saml2\"" files = [ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, @@ -547,7 +547,7 @@ description = "XPath 1.0/2.0/3.0/3.1 parsers and selectors for ElementTree and l optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"saml2\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"saml2\"" files = [ {file = "elementpath-4.8.0-py3-none-any.whl", hash = "sha256:5393191f84969bcf8033b05ec4593ef940e58622ea13cefe60ecefbbf09d58d9"}, {file = "elementpath-4.8.0.tar.gz", hash = "sha256:5822a2560d99e2633d95f78694c7ff9646adaa187db520da200a8e9479dc46ae"}, @@ -597,7 +597,7 @@ description = "Python wrapper for hiredis" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"redis\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"redis\"" files = [ {file = "hiredis-3.3.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:f525734382a47f9828c9d6a1501522c78d5935466d8e2be1a41ba40ca5bb922b"}, {file = "hiredis-3.3.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:6e2e1024f0a021777740cb7c633a0efb2c4a4bc570f508223a8dcbcf79f99ef9"}, @@ -880,7 +880,7 @@ description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version < \"3.12\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version < \"3.12\"" files = [ {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, @@ -921,7 +921,7 @@ description = "Jaeger Python OpenTracing Tracer implementation" optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"opentracing\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"opentracing\"" files = [ {file = "jaeger-client-4.8.0.tar.gz", hash = "sha256:3157836edab8e2c209bd2d6ae61113db36f7ee399e66b1dcbb715d87ab49bfe0"}, ] @@ -1113,7 +1113,7 @@ description = "A strictly RFC 4510 conforming LDAP V3 pure Python client library optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"matrix-synapse-ldap3\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"matrix-synapse-ldap3\"" files = [ {file = "ldap3-2.9.1-py2.py3-none-any.whl", hash = "sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70"}, {file = "ldap3-2.9.1.tar.gz", hash = "sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f"}, @@ -1230,7 +1230,7 @@ description = "Powerful and Pythonic XML processing library combining libxml2/li optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"url-preview\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"url-preview\"" files = [ {file = "lxml-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41dcc4c7b10484257cbd6c37b83ddb26df2b0e5aff5ac00d095689015af868ec"}, {file = "lxml-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a31286dbb5e74c8e9a5344465b77ab4c5bd511a253b355b5ca2fae7e579fafec"}, @@ -1538,7 +1538,7 @@ description = "An LDAP3 auth provider for Synapse" optional = true python-versions = ">=3.10" groups = ["main"] -markers = "extra == \"matrix-synapse-ldap3\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"matrix-synapse-ldap3\"" files = [ {file = "matrix_synapse_ldap3-0.4.0-py3-none-any.whl", hash = "sha256:bf080037230d2af5fd3639cb87266de65c1cad7a68ea206278c5b4bf9c1a17f3"}, {file = "matrix_synapse_ldap3-0.4.0.tar.gz", hash = "sha256:cff52ba780170de5e6e8af42863d2648ee23f3bf0a9fea6db52372f9fc00be2b"}, @@ -1823,7 +1823,7 @@ description = "OpenTracing API for Python. See documentation at http://opentraci optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"opentracing\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"opentracing\"" files = [ {file = "opentracing-2.4.0.tar.gz", hash = "sha256:a173117e6ef580d55874734d1fa7ecb6f3655160b8b8974a2a1e98e5ec9c840d"}, ] @@ -2018,7 +2018,7 @@ description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = true python-versions = ">=3.9" groups = ["main"] -markers = "extra == \"postgres\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"postgres\"" files = [ {file = "psycopg2-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:103e857f46bb76908768ead4e2d0ba1d1a130e7b8ed77d3ae91e8b33481813e8"}, {file = "psycopg2-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:210daed32e18f35e3140a1ebe059ac29209dd96468f2f7559aa59f75ee82a5cb"}, @@ -2036,7 +2036,7 @@ description = ".. image:: https://travis-ci.org/chtd/psycopg2cffi.svg?branch=mas optional = true python-versions = "*" groups = ["main"] -markers = "platform_python_implementation == \"PyPy\" and (extra == \"postgres\" or extra == \"all\")" +markers = "platform_python_implementation == \"PyPy\" and (extra == \"all\" or extra == \"postgres\")" files = [ {file = "psycopg2cffi-2.9.0.tar.gz", hash = "sha256:7e272edcd837de3a1d12b62185eb85c45a19feda9e62fa1b120c54f9e8d35c52"}, ] @@ -2052,7 +2052,7 @@ description = "A Simple library to enable psycopg2 compatability" optional = true python-versions = "*" groups = ["main"] -markers = "platform_python_implementation == \"PyPy\" and (extra == \"postgres\" or extra == \"all\")" +markers = "platform_python_implementation == \"PyPy\" and (extra == \"all\" or extra == \"postgres\")" files = [ {file = "psycopg2cffi-compat-1.1.tar.gz", hash = "sha256:d25e921748475522b33d13420aad5c2831c743227dc1f1f2585e0fdb5c914e05"}, ] @@ -2292,14 +2292,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" -version = "2.13.0" +version = "2.15.1" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728"}, - {file = "pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423"}, + {file = "pyjwt-2.15.1-py3-none-any.whl", hash = "sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193"}, + {file = "pyjwt-2.15.1.tar.gz", hash = "sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8"}, ] [package.dependencies] @@ -2332,7 +2332,7 @@ description = "A development tool to measure, monitor and analyze the memory beh optional = true python-versions = ">=3.6" groups = ["main"] -markers = "extra == \"cache-memory\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"cache-memory\"" files = [ {file = "Pympler-1.0.1-py3-none-any.whl", hash = "sha256:d260dda9ae781e1eab6ea15bacb84015849833ba5555f141d2d9b7b7473b307d"}, {file = "Pympler-1.0.1.tar.gz", hash = "sha256:993f1a3599ca3f4fcd7160c7545ad06310c9e12f70174ae7ae8d4e25f6c5d3fa"}, @@ -2464,7 +2464,7 @@ description = "Python implementation of SAML Version 2 Standard" optional = true python-versions = ">=3.9,<4.0" groups = ["main"] -markers = "extra == \"saml2\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"saml2\"" files = [ {file = "pysaml2-7.5.0-py3-none-any.whl", hash = "sha256:bc6627cc344476a83c757f440a73fda1369f13b6fda1b4e16bca63ffbabb5318"}, {file = "pysaml2-7.5.0.tar.gz", hash = "sha256:f36871d4e5ee857c6b85532e942550d2cf90ea4ee943d75eb681044bbc4f54f7"}, @@ -2489,7 +2489,7 @@ description = "Extensions to the standard Python datetime module" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main"] -markers = "extra == \"saml2\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"saml2\"" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -2517,7 +2517,7 @@ description = "World timezone definitions, modern and historical" optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"saml2\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"saml2\"" files = [ {file = "pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a"}, {file = "pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1"}, @@ -2921,7 +2921,7 @@ description = "Python client for Sentry (https://sentry.io)" optional = true python-versions = ">=3.6" groups = ["main"] -markers = "extra == \"sentry\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"sentry\"" files = [ {file = "sentry_sdk-2.57.0-py2.py3-none-any.whl", hash = "sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585"}, {file = "sentry_sdk-2.57.0.tar.gz", hash = "sha256:4be8d1e71c32fb27f79c577a337ac8912137bba4bcbc64a4ec1da4d6d8dc5199"}, @@ -3121,7 +3121,7 @@ description = "Tornado IOLoop Backed Concurrent Futures" optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"opentracing\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"opentracing\"" files = [ {file = "threadloop-1.0.2-py2-none-any.whl", hash = "sha256:5c90dbefab6ffbdba26afb4829d2a9df8275d13ac7dc58dccb0e279992679599"}, {file = "threadloop-1.0.2.tar.gz", hash = "sha256:8b180aac31013de13c2ad5c834819771992d350267bddb854613ae77ef571944"}, @@ -3137,7 +3137,7 @@ description = "Python bindings for the Apache Thrift RPC system" optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"opentracing\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"opentracing\"" files = [ {file = "thrift-0.24.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efe85c4508adaf6c9e6f7fac2b1c3c9beb4b39b19c375519966335a70b28e64a"}, {file = "thrift-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2baaec0c5cd7ba3eace54b26d81fdf0f5a85010468687cf4a131871d65abfed"}, @@ -3247,7 +3247,7 @@ description = "Tornado is a Python web framework and asynchronous networking lib optional = true python-versions = ">=3.9" groups = ["main"] -markers = "extra == \"opentracing\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"opentracing\"" files = [ {file = "tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163"}, {file = "tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100"}, @@ -3324,7 +3324,7 @@ keyring = {version = ">=21.2.0", markers = "platform_machine != \"ppc64le\" and packaging = ">=24.0" readme-renderer = ">=35.0" requests = ">=2.20" -requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" +requests-toolbelt = ">=0.8.0,!=0.9.0" rfc3986 = ">=1.4.0" rich = ">=12.0.0" urllib3 = ">=1.26.0" @@ -3379,7 +3379,7 @@ description = "non-blocking redis client for python" optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"redis\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"redis\"" files = [ {file = "txredisapi-1.4.12-py3-none-any.whl", hash = "sha256:c698fff24a0b82e8932ef18c5623496b0a8b5195e4e94bf8524800a00ca6b7c1"}, {file = "txredisapi-1.4.12.tar.gz", hash = "sha256:98e2440ff2e297c9048c5f9d516e7b1302d447cd9cd4568c986c35a8523de3c7"}, @@ -3640,7 +3640,7 @@ description = "An XML Schema validator and decoder" optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"saml2\" or extra == \"all\"" +markers = "extra == \"all\" or extra == \"saml2\"" files = [ {file = "xmlschema-2.5.1-py3-none-any.whl", hash = "sha256:ec2b2a15c8896c1fcd14dcee34ca30032b99456c3c43ce793fdb9dca2fb4b869"}, {file = "xmlschema-2.5.1.tar.gz", hash = "sha256:4f7497de6c8b6dc2c28ad7b9ed6e21d186f4afe248a5bea4f54eedab4da44083"}, @@ -3661,7 +3661,7 @@ description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version < \"3.12\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version < \"3.12\"" files = [ {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, From b3b5f3b61d04af61881f774995a62b1e6052b5bf Mon Sep 17 00:00:00 2001 From: Paul Chobert Date: Wed, 30 Sep 2026 17:17:12 +0200 Subject: [PATCH 3/7] Advertise support for Matrix v1.15 (#20286) Synapse now implements every item in the Matrix v1.15 changelog (all ticked in #18731), so it can advertise `v1.15`. Closes #18731. --- changelog.d/20286.feature | 1 + rust/src/handlers/versions.rs | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/20286.feature diff --git a/changelog.d/20286.feature b/changelog.d/20286.feature new file mode 100644 index 0000000000..6331fe13f9 --- /dev/null +++ b/changelog.d/20286.feature @@ -0,0 +1 @@ +Advertise support for Matrix v1.15. diff --git a/rust/src/handlers/versions.rs b/rust/src/handlers/versions.rs index 0ac24de951..e3419d5020 100644 --- a/rust/src/handlers/versions.rs +++ b/rust/src/handlers/versions.rs @@ -158,6 +158,7 @@ async fn build_versions_response( "v1.12".to_string(), "v1.13".to_string(), "v1.14".to_string(), + "v1.15".to_string(), ]), unstable_features: unstable_feature_map, }) From a4d1f7b6270b65b11d8030eda29192b392f466c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:44:26 +0000 Subject: [PATCH 4/7] Bump the all-github-actions group with 2 updates (#20297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the all-github-actions group with 2 updates: [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) and [docker/build-push-action](https://github.com/docker/build-push-action). Updates `docker/setup-buildx-action` from 4.3.0 to 4.4.1
Release notes

Sourced from docker/setup-buildx-action's releases.

v4.4.1

Full Changelog: https://github.com/docker/setup-buildx-action/compare/v4.4.0...v4.4.1

v4.4.0

Full Changelog: https://github.com/docker/setup-buildx-action/compare/v4.3.0...v4.4.0

Commits
  • f87e599 Merge pull request #624 from crazy-max/skip-pull-with-endpoint
  • e700274 chore: update generated content
  • 3061c91 skip BuildKit image pre-pulls for explicit endpoints
  • 594f3bf Merge pull request #609 from crazy-max/pull-buildkit-image-before-create
  • bd6e702 chore: update generated content
  • 6268c9d pull BuildKit image before builder creation
  • e823525 Merge pull request #621 from docker/dependabot/github_actions/codeql-actions-...
  • 533ed8e build(deps): bump the codeql-actions group with 2 updates
  • bedaf13 Merge pull request #620 from crazy-max/shared-error-helpers
  • d5079fb chore: update generated content
  • Additional commits viewable in compare view

Updates `docker/build-push-action` from 7.3.0 to 7.4.0
Release notes

Sourced from docker/build-push-action's releases.

v7.4.0

Full Changelog: https://github.com/docker/build-push-action/compare/v7.3.0...v7.4.0

Commits
  • c3c9e26 Merge pull request #1621 from docker/dependabot/npm_and_yarn/docker/actions-t...
  • 459b674 [dependabot skip] chore: update generated content
  • 4dedcb2 chore(deps): Bump @​docker/actions-toolkit from 0.99.0 to 0.100.0
  • 379bf63 Merge pull request #1620 from crazy-max/buildx-error-message
  • 9877975 chore: update generated content
  • 7ed0556 use the shared Buildx error summary helper
  • 91670ba Merge pull request #1618 from docker/dependabot/npm_and_yarn/docker/actions-t...
  • 80dbc86 [dependabot skip] chore: update generated content
  • 50cac3a chore(deps): Bump @​docker/actions-toolkit from 0.98.0 to 0.99.0
  • 03b4d6c Merge pull request #1617 from crazy-max/fix-metadata-workflow-commands
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 6 +++--- .github/workflows/release-artifacts.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5e8ad3b18a..127f6bf224 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -87,7 +87,7 @@ jobs: - name: Build and push by digest id: build - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: push: true labels: | @@ -183,7 +183,7 @@ jobs: password: ${{ steps.import-secrets.outputs.OCI_PASSWORD }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - name: Install Cosign uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml index 75ceb0f533..bfc5d05c34 100644 --- a/.github/workflows/release-artifacts.yml +++ b/.github/workflows/release-artifacts.yml @@ -67,7 +67,7 @@ jobs: - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - name: Set up docker layer caching uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 From cc7b24e5cb9a6298a94f7bf1c8453275b3a7782f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:26:16 +0000 Subject: [PATCH 5/7] Bump tornado from 6.5.7 to 6.5.9 (#20301) Signed-off-by: dependabot[bot] --- poetry.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/poetry.lock b/poetry.lock index ee805bbacd..e53679baf5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3242,23 +3242,23 @@ markers = {main = "python_version < \"3.11\""} [[package]] name = "tornado" -version = "6.5.7" +version = "6.5.9" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"all\" or extra == \"opentracing\"" files = [ - {file = "tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163"}, - {file = "tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100"}, - {file = "tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972"}, - {file = "tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b"}, - {file = "tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92"}, - {file = "tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5"}, - {file = "tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4"}, - {file = "tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4"}, - {file = "tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796"}, - {file = "tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2"}, + {file = "tornado-6.5.9-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:dea3bee433b73d6312d993ce89ad5f2b8d3ed9fde56e79ba5342b9edb78d342a"}, + {file = "tornado-6.5.9-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:22bed49cdb55292d1d58f46008ba2cc3020e5b84995ef6ae749676c8b5309514"}, + {file = "tornado-6.5.9-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0cc0876a58eee9143476f24901b1b8eb4d52443283ade2b6bca650dd1da04f84"}, + {file = "tornado-6.5.9-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc0d703cc7ec92b47ebfe236c97cd765c1f2e45744a2e156e6677e54c948274d"}, + {file = "tornado-6.5.9-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:93f9ab0226ef625b94cacb7981a5af104f1735ea02f92282abe5c389109fb31d"}, + {file = "tornado-6.5.9-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5801bbf94dfde8d21087b6aa0b17f3a5dab6110dfe44e7e4492df5de393ac15b"}, + {file = "tornado-6.5.9-cp39-abi3-win32.whl", hash = "sha256:dca9f377e80efe5dd4d52a2776b67492a81a39b2d931de169b6b36d0c1da444c"}, + {file = "tornado-6.5.9-cp39-abi3-win_amd64.whl", hash = "sha256:f810730ac71eadf009e6e5cb6d534d096887310f5649cff19d7e0d19c0bf4ee7"}, + {file = "tornado-6.5.9-cp39-abi3-win_arm64.whl", hash = "sha256:b5f4d92798337260c6d9f9f0e400c7539dd5e55cf93479a3f8508c9aa09c2b3b"}, + {file = "tornado-6.5.9.tar.gz", hash = "sha256:4d868544ebdf2fc155239a74397f65ebfea9b4d3635296c96b5dfb0701c354f5"}, ] [[package]] From d2b5372ee3564a9f171f48d3b42deb1269e57f59 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:28:27 +0000 Subject: [PATCH 6/7] Bump urllib3 from 2.7.0 to 2.8.0 (#20300) Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index e53679baf5..ae9e819f5c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3605,14 +3605,14 @@ files = [ [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, - {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, + {file = "urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3"}, + {file = "urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63"}, ] [package.extras] From 5a8c4b3990aa997fb5a9d24769b98ae7ee5b35ba Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 2 Oct 2026 09:48:14 +0100 Subject: [PATCH 7/7] Fix bug where `/sync` would not return membership changes due to lazy-loaded membership cache (#20289) In `/sync` there is a mode called lazy loaded (LL) members. In this mode `/sync` does not return all memberships in the room, instead it only returns a) membership changes, and b) the memberships of senders of events in the timeline (if not previously sent). Synapse implements a lazy-loaded member cache to avoid repeatedly sending down the same memberships when a single user sends multiple events. It does this by storing for each user/device the memberships it has previously returned in an LRU cache. Note that it is fine for the server to return the same membership, clients handle that fine, but it is a waste of bandwidth. The current lazy-loaded membership cache has three intertwined flaws: 1. It does not handle retries or clients re-using old tokens. 2. It incorrectly applies the membership cache to membership changes, not just memberships added by lazy-loading. 3. It does not correctly handle multiple "sync streams" from the same device. In this PR we explicitly fix the first two issues in two commits, and the third issue is mitigated: 1. Alongside each membership in the LL cache we store the `next_batch` position of the response that carried it. A client that processed a response syncs from its `next_batch` next. When a request arrives with a `since` token, we delete every membership recorded after that token, as they were sent in responses the client never processed. Memberships recorded at or before `since` are kept. 2. We always return memberships that have changed, even if they appear in the LL member cache. These changes also half mitigate the issue of running multiple `/sync` loops per-device (though we do not see this in the wild), as we'd at least always return the changed membership states. Fixes #19978 and #20278, supersedes #20279 --- changelog.d/20289.bugfix | 1 + synapse/handlers/sync.py | 206 +++++++++++++++----- tests/handlers/test_sync.py | 80 +++++++- tests/rest/client/test_sync.py | 333 ++++++++++++++++++++++++++++++++- 4 files changed, 574 insertions(+), 46 deletions(-) create mode 100644 changelog.d/20289.bugfix diff --git a/changelog.d/20289.bugfix b/changelog.d/20289.bugfix new file mode 100644 index 0000000000..74b7d1f0aa --- /dev/null +++ b/changelog.d/20289.bugfix @@ -0,0 +1 @@ +Fix bug where membership events would not be returned in `/sync` when using lazy-loaded members and using an old token for `since`. diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index a3ebf8bb1d..2eb5eb79c6 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -23,6 +23,7 @@ import itertools import json import logging import os +from collections import OrderedDict from typing import ( TYPE_CHECKING, AbstractSet, @@ -74,6 +75,7 @@ from synapse.types import ( MutableStateMap, Requester, RoomStreamToken, + StateKey, StateMap, StrCollection, StreamKeyType, @@ -130,6 +132,96 @@ LAZY_LOADED_PROFILE_FIELDS_CACHE_DIGEST_SIZE = 16 LAZY_LOADED_PROFILE_FIELDS_CACHE_DIGEST_KEY = os.urandom(32) +@attr.s(slots=True, auto_attribs=True) +class LazyLoadedMembersCache: + """The membership events a lazy-loading client has been sent, so that later + syncs can leave them out. One per (user, device). + + A member may only be left out if the client processed the response that + carried it. Each entry therefore records the `next_batch` position of the + response that carried it. A client that has processed a response syncs + from its `next_batch` next, so a request from `since` means the client + processed every response up to `since` and none after it. `start_request` + forgets the entries recorded after `since`. They were in responses the + client lost, by timing out a slow request and retrying it (#19978) or by + reloading from an older persisted token (#20278). Entries recorded at or + before `since` stay, so a client resuming from a token a few minutes old is + only sent again what it lost in those minutes. An initial sync forgets + everything. + + Forgetting everything whenever `since` is before the newest entry would be + simpler, and `_newest` alone would suffice. But Element Web only persists + its sync token every few minutes and resumes from it on every reload, so + that would re-send every cached member on each reload. + + The notifier may build several results for one long-poll and returns only + the first non-empty one. An empty result carries no members, so it records + nothing. + + Leaving out a member the client never received loses it for good. A + `state_after` client does not apply state from the timeline, and a `state` + client only sees the membership changes that fall inside the timeline. + """ + + # user_id -> (event_id of the membership we last sent, room position of the + # `next_batch` of the response that carried it). Least recently used first. + # + # Not an `LruCache`, as that cannot be iterated over and `start_request` + # needs to check every entry's position against `since`. + _sent: OrderedDict[str, tuple[str, RoomStreamToken]] = attr.Factory(OrderedDict) + + # An upper bound on the positions of the entries, so that `start_request` + # need not look at them when nothing was recorded after `since`. That is the + # steady state, where `since` is the `next_batch` of the last response. + _newest: RoomStreamToken | None = None + + def start_request(self, since_token: StreamToken | None) -> None: + """Forget the members the client did not receive. Called at the start of + each sync request for this device.""" + if since_token is None: + self._sent.clear() + self._newest = None + return + + # Check for the common case where nothing has been recorded after + # `since`. + since = since_token.room_key + if self._newest is None or self._newest.is_before_or_eq(since): + return + + # Remove all entries that were sent after `since`. This only has + # `LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE` entries. + for user_id, (_, sent_at) in list(self._sent.items()): + if not sent_at.is_before_or_eq(since): + del self._sent[user_id] + + # Everything left is at or before `since`. + self._newest = since + + def was_sent(self, user_id: str, event_id: str) -> bool: + """Whether `event_id` is the membership of `user_id` we last sent.""" + entry = self._sent.get(user_id) + if entry is None or entry[0] != event_id: + return False + self._sent.move_to_end(user_id) + return True + + def mark_sent(self, user_id: str, event_id: str, next_batch: StreamToken) -> None: + """Record that `event_id` is being sent in the response ending at + `next_batch`.""" + sent_at = next_batch.room_key + self._sent[user_id] = (event_id, sent_at) + self._sent.move_to_end(user_id) + while len(self._sent) > LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE: + self._sent.popitem(last=False) + + # Only move the bound forwards. Positions taken by this process only + # ever advance, but a response that started earlier may record its + # entries after a later one has. + if self._newest is None or self._newest.is_before_or_eq(sent_at): + self._newest = sent_at + + SyncRequestKey = tuple[Any, ...] @@ -345,9 +437,9 @@ class SyncHandler: timeout=hs.config.caches.sync_response_cache_duration, ) - # ExpiringCache((User, Device)) -> LruCache(user_id => event_id) + # ExpiringCache((User, Device)) -> LazyLoadedMembersCache self.lazy_loaded_members_cache: ExpiringCache[ - tuple[str, str | None], LruCache[str, str] + tuple[str, str | None], LazyLoadedMembersCache ] = ExpiringCache( cache_name="lazy_loaded_members_cache", server_name=self.server_name, @@ -1088,7 +1180,7 @@ class SyncHandler: member_ids[hero_id] for hero_id in summary["m.heroes"] if ( - cache.get(hero_id) != member_ids[hero_id] + not cache.was_sent(hero_id, member_ids[hero_id]) and hero_id not in existing_members ) ] @@ -1096,27 +1188,21 @@ class SyncHandler: missing_hero_state = await self.store.get_events(missing_hero_event_ids) for s in missing_hero_state.values(): - cache.set(s.state_key, s.event_id) + cache.mark_sent(s.state_key, s.event_id, now_token) state[(EventTypes.Member, s.state_key)] = s return summary def get_lazy_loaded_members_cache( self, cache_key: tuple[str, str | None] - ) -> LruCache[str, str]: - # FIXME: This cache may be subject to losing members in the case that - # a sync is interrupted and retried, see https://github.com/element-hq/synapse/issues/19978 - cache: LruCache[str, str] | None = self.lazy_loaded_members_cache.get(cache_key) + ) -> LazyLoadedMembersCache: + cache = self.lazy_loaded_members_cache.get(cache_key) if cache is None: - logger.debug("creating LruCache for %r", cache_key) - cache = LruCache( - max_size=LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE, - clock=self.clock, - server_name=self.server_name, - ) + logger.debug("creating LazyLoadedMembersCache for %r", cache_key) + cache = LazyLoadedMembersCache() self.lazy_loaded_members_cache[cache_key] = cache else: - logger.debug("found LruCache for %r", cache_key) + logger.debug("found LazyLoadedMembersCache for %r", cache_key) return cache def get_lazy_loaded_profile_fields_cache( @@ -1155,6 +1241,7 @@ class SyncHandler: sync_config: SyncConfig, since_token: StreamToken | None, end_token: StreamToken, + now_token: StreamToken, full_state: bool, joined: bool, ) -> MutableStateMap[EventBase]: @@ -1169,6 +1256,7 @@ class SyncHandler: end_token: Token of the end of the current batch. Normally this will be the same as the global "now_token", but if the user has left the room, the point just after their leave event. + now_token: The `next_batch` token of the response being built. full_state: Whether to force returning the full state. `lazy_load_members` still applies when `full_state` is `True`. joined: whether the user is currently joined to the room @@ -1254,6 +1342,11 @@ class SyncHandler: # sync's timeline and the start of the current sync's timeline. # See the docstring above for details. state_ids: StateMap[str] + # The keys in `state_ids` whose state changed since `since_token`. + # These are sent whether or not the lazy-loaded members cache says + # the client has them. The rest are memberships fetched for + # timeline senders, which the client may already have. + changed_keys: set[StateKey] = set() # We need to know whether the state we fetch may be partial, so check # whether the room is partial stated *before* fetching it. is_partial_state_room = await self.store.is_partial_state_room(room_id) @@ -1273,7 +1366,10 @@ class SyncHandler: # is indeed the case. assert since_token is not None - state_ids = await self._compute_state_delta_for_incremental_sync( + ( + state_ids, + changed_keys, + ) = await self._compute_state_delta_for_incremental_sync( room_id, sync_config, batch, @@ -1309,30 +1405,28 @@ class SyncHandler: cache_key = (sync_config.user.to_string(), sync_config.device_id) cache = self.get_lazy_loaded_members_cache(cache_key) - # if it's a new sync sequence, then assume the client has had - # amnesia and doesn't want any recent lazy-loaded members - # de-duplicated. - if since_token is None: - logger.debug("clearing LruCache for %r", cache_key) - cache.clear() - else: - # only send members which aren't in our LruCache (either - # because they're new to this client or have been pushed out - # of the cache) - logger.debug("filtering state from %r...", state_ids) - state_ids = { - t: event_id - for t, event_id in state_ids.items() - if cache.get(t[1]) != event_id - } - logger.debug("...to %r", state_ids) + # Only send members which aren't in our cache (either because + # they're new to this client or have been pushed out of the + # cache). For an initial sync the cache has already been + # cleared. + # + # A state change since `since_token` is always sent, regardless + # of whether it was previously sent to the client (e.g due to a + # client retrying the request). + logger.debug("filtering state from %r...", state_ids) + state_ids = { + t: event_id + for t, event_id in state_ids.items() + if t in changed_keys or not cache.was_sent(t[1], event_id) + } + logger.debug("...to %r", state_ids) - # add any member IDs we are about to send into our LruCache + # add any member IDs we are about to send into our cache for t, event_id in itertools.chain( state_ids.items(), timeline_state.items() ): if t[0] == EventTypes.Member: - cache.set(t[1], event_id) + cache.mark_sent(t[1], event_id, now_token) state: dict[str, EventBase] = {} if state_ids: @@ -1505,14 +1599,16 @@ class SyncHandler: members_to_fetch: set[str] | None, timeline_state: StateMap[str], joined: bool, - ) -> StateMap[str]: + ) -> tuple[StateMap[str], set[StateKey]]: """Calculate the state events to be included in an incremental sync response. If lazy-loading of membership events is enabled (as indicated by `members_to_fetch` being not-`None`), the result will include the membership events for each member in `members_to_fetch`. The caller (`compute_state_delta`) is responsible for keeping track of which membership - events we have already sent to the client, and hence ripping them out. + events we have already sent to the client, and hence ripping them out. It + may only rip out entries that are not state changes, so this also returns + the keys whose state changed since `since_token`. Note that whether this returns the state at the start or the end of the batch depends on `sync_config.use_state_after` (c.f. MSC4222). @@ -1533,7 +1629,10 @@ class SyncHandler: Returns: A map from (type, state_key) to event_id, for each event that we believe - should be included in the `state` or `state_after` part of the sync response. + should be included in the `state` or `state_after` part of the sync + response, and the subset of its keys whose state changed since + `since_token`. The other keys are memberships fetched for timeline + senders, which the client may have been sent before. """ if members_to_fetch is not None: # Lazy-loading is enabled. Only return the state that is needed. @@ -1549,6 +1648,7 @@ class SyncHandler: # timeline. If at the end we can just use the current state delta stream. if sync_config.use_state_after: delta_state_ids: MutableStateMap[str] = {} + changed_keys: set[StateKey] = set() if members_to_fetch: # We're lazy-loading, so the client might need some more member @@ -1603,9 +1703,11 @@ class SyncHandler: # Note that deltas are in stream ordering, so if there are # multiple deltas for a given type/state_key we'll always pick # the latest one. - delta_state_ids[(delta.event_type, delta.state_key)] = delta.event_id + key = (delta.event_type, delta.state_key) + delta_state_ids[key] = delta.event_id + changed_keys.add(key) - return delta_state_ids + return delta_state_ids, changed_keys # For a non-gappy sync if the events in the timeline are simply a linear # chain (i.e. no merging/branching of the graph), then we know the state @@ -1653,7 +1755,9 @@ class SyncHandler: await_full_state=False, ) ) - return state_ids + # A linear, non-gappy timeline means the only changes to the `state` + # are lazy-loaded members. + return state_ids, set() if batch: state_at_timeline_start = ( @@ -1712,7 +1816,16 @@ class SyncHandler: lazy_load_members=lazy_load_members, ) - return state_ids + # Everything that differs from the state at the previous sync is a + # change. What is left is the memberships of timeline senders that + # `_calculate_state` adds back for lazy loading. + changed_keys = { + key + for key, event_id in state_ids.items() + if state_at_previous_sync.get(key) != event_id + } + + return state_ids, changed_keys async def _find_missing_partial_state_memberships( self, @@ -2650,7 +2763,13 @@ class SyncHandler: """ since_token = sync_result_builder.since_token - user_id = sync_result_builder.sync_config.user.to_string() + sync_config = sync_result_builder.sync_config + user_id = sync_config.user.to_string() + + if sync_config.filter_collection.lazy_load_members(): + self.get_lazy_loaded_members_cache( + (user_id, sync_config.device_id) + ).start_request(since_token) blocks_all_rooms = ( sync_result_builder.sync_config.filter_collection.blocks_all_rooms() @@ -3318,6 +3437,7 @@ class SyncHandler: sync_config, since_token, room_builder.end_token, + now_token, full_state=full_state, joined=room_builder.rtype == "joined", ) diff --git a/tests/handlers/test_sync.py b/tests/handlers/test_sync.py index 2f0107dd0a..1002a0374c 100644 --- a/tests/handlers/test_sync.py +++ b/tests/handlers/test_sync.py @@ -33,7 +33,9 @@ from synapse.api.room_versions import RoomVersion, RoomVersions from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.handlers.sync import ( + LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE, LAZY_LOADED_PROFILE_FIELDS_CACHE_MAX_AGE, + LazyLoadedMembersCache, SyncConfig, SyncRequestKey, SyncResult, @@ -48,6 +50,7 @@ from synapse.types import ( MultiWriterStreamToken, RoomStreamToken, StreamKeyType, + StreamToken, UserID, create_requester, ) @@ -2947,7 +2950,7 @@ class SyncStateAfterTestCase(tests.unittest.HomeserverTestCase): # Calculating the incrementals state will return the second state, and not the # first. - state = self.get_success( + state, _ = self.get_success( self.sync_handler._compute_state_delta_for_incremental_sync( room_id=joined_room, sync_config=generate_sync_config(user, use_state_after=True), @@ -2979,7 +2982,7 @@ class SyncStateAfterTestCase(tests.unittest.HomeserverTestCase): since_token = self.hs.get_event_sources().get_current_token() end_stream_token = self.hs.get_event_sources().get_current_token() - state = self.get_success( + state, _ = self.get_success( self.sync_handler._compute_state_delta_for_incremental_sync( room_id=joined_room, sync_config=generate_sync_config(user, use_state_after=True), @@ -2995,3 +2998,76 @@ class SyncStateAfterTestCase(tests.unittest.HomeserverTestCase): ) self.assertEqual(state, {}) + + +class LazyLoadedMembersCacheTestCase(tests.unittest.TestCase): + """Direct tests of the per-device cache of lazy-loaded members sent.""" + + @staticmethod + def _token(stream: int) -> StreamToken: + """A token whose room position is `stream`. Only the room position + matters to the cache.""" + return StreamToken.START.copy_and_replace( + StreamKeyType.ROOM, RoomStreamToken(stream=stream) + ) + + def test_forgets_only_entries_after_since(self) -> None: + """A request from `since` forgets the entries recorded after it and + keeps the rest.""" + cache = LazyLoadedMembersCache() + cache.mark_sent("@a:test", "$a", self._token(5)) + cache.mark_sent("@b:test", "$b", self._token(7)) + + # The client synced past both responses. + cache.start_request(self._token(7)) + self.assertTrue(cache.was_sent("@a:test", "$a")) + self.assertTrue(cache.was_sent("@b:test", "$b")) + + # The client lost the response that ended at 7. + cache.start_request(self._token(6)) + self.assertTrue(cache.was_sent("@a:test", "$a")) + self.assertFalse(cache.was_sent("@b:test", "$b")) + + # A later request from an even older token forgets the rest. + cache.start_request(self._token(4)) + self.assertFalse(cache.was_sent("@a:test", "$a")) + + def test_initial_sync_forgets_everything(self) -> None: + """An initial sync has no `since`, and the client starts from nothing.""" + cache = LazyLoadedMembersCache() + cache.mark_sent("@a:test", "$a", self._token(5)) + cache.start_request(None) + self.assertFalse(cache.was_sent("@a:test", "$a")) + + def test_different_event_is_not_sent(self) -> None: + """An entry only covers the membership event it was recorded with, so + a changed membership counts as not sent.""" + cache = LazyLoadedMembersCache() + cache.mark_sent("@a:test", "$a1", self._token(5)) + self.assertFalse(cache.was_sent("@a:test", "$a2")) + + def test_out_of_order_marks_are_still_forgotten(self) -> None: + """A response that started earlier can record its entries after a + later one. Its entries must still be forgotten by a request from + before either response.""" + cache = LazyLoadedMembersCache() + cache.mark_sent("@b:test", "$b", self._token(7)) + cache.mark_sent("@a:test", "$a", self._token(5)) + + cache.start_request(self._token(6)) + self.assertTrue(cache.was_sent("@a:test", "$a")) + self.assertFalse(cache.was_sent("@b:test", "$b")) + + def test_evicts_least_recently_used(self) -> None: + """The cache holds `LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE` entries and + evicts the least recently used one. A hit counts as a use.""" + cache = LazyLoadedMembersCache() + for i in range(LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE): + cache.mark_sent(f"@u{i}:test", f"$u{i}", self._token(1)) + # Touch the oldest entry so that it is no longer the eviction candidate. + self.assertTrue(cache.was_sent("@u0:test", "$u0")) + + cache.mark_sent("@new:test", "$new", self._token(1)) + self.assertTrue(cache.was_sent("@u0:test", "$u0")) + self.assertFalse(cache.was_sent("@u1:test", "$u1")) + self.assertTrue(cache.was_sent("@new:test", "$new")) diff --git a/tests/rest/client/test_sync.py b/tests/rest/client/test_sync.py index 51ffabe75d..20fdacdc01 100644 --- a/tests/rest/client/test_sync.py +++ b/tests/rest/client/test_sync.py @@ -45,7 +45,7 @@ from tests.federation.transport.test_knocking import ( KnockingStrippedStateEventHelperMixin, ) from tests.rest.client.test_rooms import make_request_with_cancellation_test -from tests.server import TimedOutException +from tests.server import FakeChannel, TimedOutException from tests.test_utils.event_injection import ( inject_event, persist_message_and_state_event_in_one_batch, @@ -1684,3 +1684,334 @@ class SyncStateAfterArchivedRoomTestCase(unittest.HomeserverTestCase): ], [bob_member_event_id_at_leave], ) + + +class SyncLazyLoadedMembersCacheTestCase(unittest.HomeserverTestCase): + """Tests for the per-device cache of lazy-loaded members already sent. + + A membership left out of a response as already sent is lost for good if + the client never processed the response that carried it. The cache must + only trust a response once the client has synced from its `next_batch`. + """ + + servlets = [ + synapse.rest.admin.register_servlets, + room.register_servlets, + login.register_servlets, + sync.register_servlets, + ] + + def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: + self.alice = self.register_user("alice", "password") + self.alice_device = "alice_device" + self.alice_tok = self.login("alice", "password", device_id=self.alice_device) + self.bob = self.register_user("bob", "password") + self.bob_tok = self.login("bob", "password") + + self.get_success( + hs.get_datastores().main.set_features_for_user( + self.alice, {ExperimentalFeature.MSC4222: True} + ) + ) + + # Named room, so that hero calculation doesn't inject current + # membership state into the response and confuse the assertions. + self.room_id = self.helper.create_room_as( + self.alice, tok=self.alice_tok, extra_content={"name": "test room"} + ) + self.helper.join(self.room_id, self.bob, tok=self.bob_tok) + + # Push bob's join out of the initial sync's timeline, so that his + # membership has not been sent to the client when a test starts. + for i in range(10): + self.helper.send(self.room_id, body=f"pad {i}", tok=self.alice_tok) + + def _sync_request( + self, + use_state_after: bool, + since: str | None = None, + *, + timeline_limit: int = 10, + timeline_not_types: list[str] | None = None, + timeout_ms: int = 0, + await_result: bool = True, + ) -> FakeChannel: + """Make a lazy-loading `/sync` request as alice.""" + timeline_filter: JsonDict = {"limit": timeline_limit} + if timeline_not_types is not None: + timeline_filter["not_types"] = timeline_not_types + sync_filter: JsonDict = { + "room": { + "timeline": timeline_filter, + "state": {"lazy_load_members": True}, + } + } + url = f"/sync?filter={json.dumps(sync_filter)}&timeout={timeout_ms}" + if use_state_after: + url += "&org.matrix.msc4222.use_state_after=true" + if since is not None: + url += f"&since={since}" + return self.make_request( + "GET", url, access_token=self.alice_tok, await_result=await_result + ) + + def _sync( + self, + use_state_after: bool, + since: str | None = None, + *, + timeline_limit: int = 10, + timeline_not_types: list[str] | None = None, + ) -> JsonDict: + """Sync as alice and return the response body.""" + channel = self._sync_request( + use_state_after, + since, + timeline_limit=timeline_limit, + timeline_not_types=timeline_not_types, + ) + self.assertEqual(channel.code, 200, channel.result) + return channel.json_body + + def _room(self, response: JsonDict) -> JsonDict: + """Extract the room dict for the test room from a sync response.""" + rooms = response["rooms"].get("join", {}) + self.assertIn(self.room_id, rooms, f"room missing from sync: {response}") + return rooms[self.room_id] + + def _timeline_ids(self, response: JsonDict) -> list[str]: + return [e["event_id"] for e in self._room(response)["timeline"]["events"]] + + def _state_events( + self, response: JsonDict, use_state_after: bool + ) -> list[JsonDict]: + """Extract the list of state events from the test room in a sync response.""" + key = "org.matrix.msc4222.state_after" if use_state_after else "state" + return self._room(response)[key]["events"] + + def _state_ids(self, response: JsonDict, use_state_after: bool) -> list[str]: + """Extract the list of state event IDs from the test room in a sync + response.""" + return [e["event_id"] for e in self._state_events(response, use_state_after)] + + def _members_in_state(self, response: JsonDict, use_state_after: bool) -> list[str]: + """Extract the list of member state keys from the test room in a sync + response.""" + return [ + e["state_key"] + for e in self._state_events(response, use_state_after) + if e["type"] == EventTypes.Member + ] + + @parameterized.expand([("state", False), ("state_after", True)]) + def test_member_sent_once_when_acknowledged( + self, _: str, use_state_after: bool + ) -> None: + """A lazy-loaded member is sent the first time it is needed and left + out once the client has synced past the response that carried it.""" + t0 = self._sync(use_state_after)["next_batch"] + + self.helper.send(self.room_id, body="one", tok=self.bob_tok) + response = self._sync(use_state_after, t0) + self.assertEqual(self._members_in_state(response, use_state_after), [self.bob]) + t1 = response["next_batch"] + + self.helper.send(self.room_id, body="two", tok=self.bob_tok) + response = self._sync(use_state_after, t1) + self.assertEqual(self._members_in_state(response, use_state_after), []) + + @parameterized.expand([("state", False), ("state_after", True)]) + def test_member_not_resent_on_resync_from_token_that_carried_it( + self, _: str, use_state_after: bool + ) -> None: + """A client that resumes from an older token is only sent again the + members carried by the responses after that token. + + A member sent in the response that ended at the resumed-from token was + processed by the client before it saved that token, so it must not be + sent again. matrix-js-sdk saves its sync store every few minutes and + resumes from the saved token on reload, so this is the common case. + """ + t0 = self._sync(use_state_after)["next_batch"] + + # bob's membership is carried by the response that ends at `t1`. + self.helper.send(self.room_id, body="one", tok=self.bob_tok) + response = self._sync(use_state_after, t0) + self.assertEqual(self._members_in_state(response, use_state_after), [self.bob]) + t1 = response["next_batch"] + + # The client processes further responses, then reloads from `t1`. + self.helper.send(self.room_id, body="two", tok=self.bob_tok) + self._sync(use_state_after, t1) + self.helper.send(self.room_id, body="three", tok=self.bob_tok) + response = self._sync(use_state_after, t1) + self.assertEqual( + self._members_in_state(response, use_state_after), + [], + f"bob's membership re-sent on resume from the token that carried " + f"it: {response}", + ) + + @parameterized.expand([("state", False), ("state_after", True)]) + def test_member_resent_on_retry_from_same_token( + self, _: str, use_state_after: bool + ) -> None: + """A client that gives up on a slow request and retries it must get the + lazy-loaded members again. It never processed the first response.""" + t0 = self._sync(use_state_after)["next_batch"] + + self.helper.send(self.room_id, body="hello", tok=self.bob_tok) + for attempt in range(2): + response = self._sync(use_state_after, t0) + self.assertEqual( + self._members_in_state(response, use_state_after), + [self.bob], + f"bob's membership missing on attempt {attempt}: {response}", + ) + + def test_membership_change_resent_on_resync_from_older_token(self) -> None: + """A membership change must be in `state_after` every time the sync + window covers it, even if it has been sent before. + + Clients do not persist every `since` token. matrix-js-sdk saves its + sync store every few minutes, so after a reload it resumes from a token + older than the one the change was first delivered on. A `state_after` + client never applies state from the timeline, so if the change is left + out of `state_after` it keeps the stale membership until it clears its + cache. A kicked user stays shown as joined. + """ + t0 = self._sync(use_state_after=True)["next_batch"] + + self.helper.send(self.room_id, body="hello", tok=self.bob_tok) + t1 = self._sync(use_state_after=True, since=t0)["next_batch"] + + kick = self.helper.change_membership( + self.room_id, self.alice, self.bob, "leave", tok=self.alice_tok + ) + + # The live client sees the kick. + response = self._sync(use_state_after=True, since=t1) + self.assertIn(kick["event_id"], self._state_ids(response, use_state_after=True)) + + # The client reloads from the older persisted token. + response = self._sync(use_state_after=True, since=t0) + self.assertIn(kick["event_id"], self._timeline_ids(response)) + self.assertIn( + kick["event_id"], + self._state_ids(response, use_state_after=True), + f"membership change missing from state_after on re-sync from an " + f"older token: {response}", + ) + + def test_gappy_membership_change_resent_on_resync_from_older_token( + self, + ) -> None: + """The same for `state`. A membership change that falls in the gap of + a limited timeline is only conveyed by `state`, so it must be there + every time the sync window covers it.""" + t0 = self._sync(use_state_after=False, timeline_limit=1)["next_batch"] + + self.helper.send(self.room_id, body="hello", tok=self.bob_tok) + t1 = self._sync(use_state_after=False, since=t0, timeline_limit=1)["next_batch"] + + kick = self.helper.change_membership( + self.room_id, self.alice, self.bob, "leave", tok=self.alice_tok + ) + # A later message, so that the kick falls into the gap of the + # one-event timeline. + self.helper.send(self.room_id, body="after", tok=self.alice_tok) + + # The live client sees the kick in `state`. + response = self._sync(use_state_after=False, since=t1, timeline_limit=1) + self.assertTrue(self._room(response)["timeline"]["limited"]) + self.assertNotIn(kick["event_id"], self._timeline_ids(response)) + self.assertIn( + kick["event_id"], self._state_ids(response, use_state_after=False) + ) + + # The client reloads from the older persisted token. + response = self._sync(use_state_after=False, since=t0, timeline_limit=1) + self.assertNotIn(kick["event_id"], self._timeline_ids(response)) + self.assertIn( + kick["event_id"], + self._state_ids(response, use_state_after=False), + f"membership change missing from state on re-sync from an older " + f"token: {response}", + ) + + @parameterized.expand([("state", False), ("state_after", True)]) + def test_member_not_resent_after_empty_long_poll_wakeup( + self, _: str, use_state_after: bool + ) -> None: + """A long-poll that wakes up with nothing to send keeps waiting. The + response it eventually returns must still leave out the members the + client already has. + + The notifier calls back into the sync handler on every wakeup within + one request. An empty result is not a response the client lost. + """ + not_types = ["m.spurious"] + t0 = self._sync(use_state_after, timeline_not_types=not_types)["next_batch"] + + self.helper.send(self.room_id, body="one", tok=self.bob_tok) + response = self._sync(use_state_after, t0, timeline_not_types=not_types) + self.assertEqual(self._members_in_state(response, use_state_after), [self.bob]) + t1 = response["next_batch"] + + channel = self._sync_request( + use_state_after, + t1, + timeline_not_types=not_types, + timeout_ms=30000, + await_result=False, + ) + + # Wakes the long-poll, but the filter drops the event, so the result is + # empty and the notifier waits again. + self.helper.send_event( + self.room_id, type="m.spurious", content={}, tok=self.bob_tok + ) + self.assertFalse(channel.is_finished()) + + self.helper.send(self.room_id, body="two", tok=self.bob_tok) + channel.await_result() + self.assertEqual(channel.code, 200, channel.result) + response = channel.json_body + self.assertEqual(len(self._timeline_ids(response)), 1) + self.assertEqual(self._members_in_state(response, use_state_after), []) + + @parameterized.expand([("state", False), ("state_after", True)]) + def test_membership_change_sent_when_cache_claims_it_was( + self, _: str, use_state_after: bool + ) -> None: + """A membership change since `since` is sent even when the cache says + the client already has it. + + The cache only decides whether to leave out the memberships fetched for + timeline senders. It can be wrong: a retry of a request can run while + the original is still being built, and the original then marks members + the retry has to send. A change must never depend on it. + """ + t0 = self._sync(use_state_after, timeline_limit=1)["next_batch"] + + kick = self.helper.change_membership( + self.room_id, self.alice, self.bob, "leave", tok=self.alice_tok + ) + # A later message, so that the kick falls into the gap of the + # one-event timeline and only `state` can carry it. + self.helper.send(self.room_id, body="after the kick", tok=self.alice_tok) + + # Claim the kick was already sent in the response that ended at `t0`. + # Syncing from `t0` does not forget this, as the client has acknowledged + # that response. + t0_token = self.get_success( + StreamToken.from_string(self.hs.get_datastores().main, t0) + ) + cache = self.hs.get_sync_handler().get_lazy_loaded_members_cache( + (self.alice, self.alice_device) + ) + cache.mark_sent(self.bob, kick["event_id"], t0_token) + + response = self._sync(use_state_after, t0, timeline_limit=1) + self.assertNotIn(kick["event_id"], self._timeline_ids(response)) + self.assertIn(kick["event_id"], self._state_ids(response, use_state_after))