From cb294993d1fe1b061070c3aee9829a9384620367 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Tue, 11 Aug 2026 11:17:14 +0200 Subject: [PATCH 1/2] fix(federator): record mock-federator requests atomically The mock federator recorded each served request with a non-atomic modifyIORef append. When galley fans out federated calls concurrently per remote domain, multiple Warp workers performed read-modify-write on the shared IORef and lost updates. The dropped write made testBulkGetQualifiedConvs (post conversations/list/v2) intermittently fail: the response was served but the request unrecorded, so the assertion over receivedRequests decoded to Nothing. Replace the non-atomic modifyIORef with atomicModifyIORef', and add a regression test that fires 50 concurrent requests at the mock and asserts all are recorded (fails intermittently before the fix, deterministic after). --- services/federator/default.nix | 1 + services/federator/federator.cabal | 1 + services/federator/src/Federator/MockServer.hs | 2 +- .../federator/test/unit/Test/Federator/Client.hs | 16 ++++++++++++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/services/federator/default.nix b/services/federator/default.nix index fa8eac96bda..e0d3e9d7235 100644 --- a/services/federator/default.nix +++ b/services/federator/default.nix @@ -168,6 +168,7 @@ mkDerivation { ]; testHaskellDepends = [ aeson + async base bytestring bytestring-conversion diff --git a/services/federator/federator.cabal b/services/federator/federator.cabal index 872f9911236..2a8181f5f18 100644 --- a/services/federator/federator.cabal +++ b/services/federator/federator.cabal @@ -380,6 +380,7 @@ test-suite federator-tests build-depends: aeson + , async , base , bytestring , bytestring-conversion diff --git a/services/federator/src/Federator/MockServer.hs b/services/federator/src/Federator/MockServer.hs index 828a87bcb1a..31094692c8f 100644 --- a/services/federator/src/Federator/MockServer.hs +++ b/services/federator/src/Federator/MockServer.hs @@ -151,7 +151,7 @@ mockInternalRequest remoteCalls mock targetDomain component (RPC path) req cont if path == "api-version" then pure $ MockResponse Wai.status200 "application/json" (Aeson.encode (VersionInfo mock.versions)) else do - modifyIORef remoteCalls (<> [fedRequest]) + atomicModifyIORef' remoteCalls (\xs -> (xs <> [fedRequest], ())) fromException @MockException . handle (throw . handleException) $ mock.handler fedRequest diff --git a/services/federator/test/unit/Test/Federator/Client.hs b/services/federator/test/unit/Test/Federator/Client.hs index 8bc976fe6a9..0660390b393 100644 --- a/services/federator/test/unit/Test/Federator/Client.hs +++ b/services/federator/test/unit/Test/Federator/Client.hs @@ -35,6 +35,7 @@ module Test.Federator.Client (tests) where +import Control.Concurrent.Async (replicateConcurrently_) import Control.Exception hiding (handle) import Control.Monad.Codensity import Control.Monad.Except @@ -92,6 +93,7 @@ tests = testGroup "HTTP2 client" [ testCase "testResponseHeaders" testResponseHeaders, + testCase "testConcurrentRequestsAllRecorded" testConcurrentRequestsAllRecorded, testCase "testStreaming" testStreaming ] ] @@ -248,6 +250,20 @@ testResponseHeaders = do responseStatusCode resp @?= HTTP.status200 lookup "X-Foo" (toList (responseHeaders resp)) @?= Just "bar" +testConcurrentRequestsAllRecorded :: IO () +testConcurrentRequestsAllRecorded = do + (_, sentRequests) <- + withTempMockFederator def $ \port -> do + let req = + HTTP2.requestBuilder + HTTP.methodPost + "/rpc/target.example.com/brig/test" + [("Wire-Origin-Domain", "origin.example.com"), (federationRequestIdHeaderName, "rid")] + "body" + mgr <- defaultHttp2Manager + replicateConcurrently_ 50 (performHTTP2Request mgr (False, "127.0.0.1", port) req) + length sentRequests @?= 50 + testStreaming :: IO () testStreaming = withInfiniteMockServer $ \port -> do let req = HTTP2.requestBuilder HTTP.methodPost "test" [] mempty From 7f986de73255fe7a4806487090623c76c5be5cd2 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Tue, 11 Aug 2026 12:42:32 +0200 Subject: [PATCH 2/2] Hello CI