From 909912b0866aaf820da24b9a416f4e50c2d09eb0 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Thu, 18 Sep 2025 19:24:31 +0200 Subject: [PATCH 01/40] WPB-19712: Allow team admin to update the channels to user-group association --- integration/test/API/Brig.hs | 5 ++ integration/test/Test/UserGroup.hs | 53 +++++++++++++++++++ libs/wire-api/src/Wire/API/Error/Brig.hs | 3 ++ .../src/Wire/API/Routes/Public/Brig.hs | 5 +- ...-user_group_conversations_create_table.sql | 8 +++ .../src/Wire/UserGroupStore.hs | 1 + .../src/Wire/UserGroupStore/Postgres.hs | 35 +++++++++++- .../src/Wire/UserGroupSubsystem.hs | 1 + .../Wire/UserGroupSubsystem/Interpreter.hs | 24 ++++++++- .../Wire/MockInterpreters/UserGroupStore.hs | 24 ++++++++- postgres-schema.sql | 29 +++++++++- services/brig/src/Brig/API/Public.hs | 4 +- 12 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 libs/wire-subsystems/postgres-migrations/202509190851023-user_group_conversations_create_table.sql diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index b7ef7bc465..bce2531ada 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -1061,6 +1061,11 @@ getUserGroup user gid = do req <- baseRequest user Brig Versioned $ joinHttpPath ["user-groups", gid] submit "GET" req +updateUserGroupChannels :: (MakesValue user) => user -> String -> [String] -> App Response +updateUserGroupChannels user gid convIds = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["user-groups", gid, "channels"] + submit "PUT" $ req & addJSONObject ["channels" .= convIds] + data GetUserGroupsArgs = GetUserGroupsArgs { q :: Maybe String, sortByKeys :: Maybe String, diff --git a/integration/test/Test/UserGroup.hs b/integration/test/Test/UserGroup.hs index 34eaf5ccbd..c6dc526faa 100644 --- a/integration/test/Test/UserGroup.hs +++ b/integration/test/Test/UserGroup.hs @@ -373,3 +373,56 @@ testUserGroupMembersCount = do resp.status `shouldMatchInt` 200 resp.json %. "page.0.membersCount" `shouldMatchInt` 2 resp.json %. "total" `shouldMatchInt` 1 + +testUserGroupUpdateChannels :: (HasCallStack) => App () +testUserGroupUpdateChannels = do + (alice, tid, [_bob]) <- createTeam OwnDomain 2 + + ug <- + createUserGroup alice (object ["name" .= "none", "members" .= (mempty :: [String])]) + >>= getJSON 200 + gid <- ug %. "id" & asString + + convId <- + postConversation alice (defProteus {team = Just tid}) + >>= getJSON 201 + >>= objConvId + updateUserGroupChannels alice gid [convId.id_] >>= assertSuccess + + -- bobId <- asString $ bob %. "id" + bindResponse (getUserGroup alice gid) $ \resp -> do + resp.status `shouldMatchInt` 200 + +-- FUTUREWORK: check the actual associated channels +-- resp.json %. "members" `shouldMatch` [bobId] + +testUserGroupUpdateChannelsNonAdmin :: (HasCallStack) => App () +testUserGroupUpdateChannelsNonAdmin = do + (alice, tid, [bob]) <- createTeam OwnDomain 2 + + ug <- + createUserGroup alice (object ["name" .= "none", "members" .= (mempty :: [String])]) + >>= getJSON 200 + gid <- ug %. "id" & asString + + convId <- + postConversation alice (defProteus {team = Just tid}) + >>= getJSON 201 + >>= objConvId + updateUserGroupChannels bob gid [convId.id_] >>= assertLabel 404 "user-group-not-found" + +testUserGroupUpdateChannelsNonExisting :: (HasCallStack) => App () +testUserGroupUpdateChannelsNonExisting = do + (alice, tid, _) <- createTeam OwnDomain 1 + (bob, _, _) <- createTeam OwnDomain 1 + + ug <- + createUserGroup alice (object ["name" .= "none", "members" .= (mempty :: [String])]) + >>= getJSON 200 + gid <- ug %. "id" & asString + + convId <- + postConversation alice (defProteus {team = Just tid}) + >>= getJSON 201 + >>= objConvId + updateUserGroupChannels bob gid [convId.id_] >>= assertLabel 404 "user-group-not-found" diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index 856e5f9520..4c11d0840e 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -118,6 +118,7 @@ data BrigError | UserGroupNotFound | UserGroupNotATeamAdmin | UserGroupMemberIsNotInTheSameTeam + | UserGroupChannelNotFound | DuplicateEntry | MLSInvalidLeafNodeSignature @@ -351,6 +352,8 @@ type instance MapError 'UserGroupNotFound = 'StaticError 404 "user-group-not-fou type instance MapError 'UserGroupNotATeamAdmin = 'StaticError 403 "user-group-write-forbidden" "Only team admins can create, update, or delete user groups." +type instance MapError 'UserGroupChannelNotFound = 'StaticError 404 "user-group-channel-not-found" "Specified Channel does not exists or does not belongs to the team" + type instance MapError 'UserGroupMemberIsNotInTheSameTeam = 'StaticError 400 "user-group-invalid" "Only team members of the same team can be added to a user group." type instance MapError 'DuplicateEntry = 'StaticError 409 "duplicate-entry" "Entry already exists" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index b54f9db2a8..3c59836110 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -418,8 +418,11 @@ type UserGroupAPI = ) :<|> Named "update-user-group-channels" - ( Summary "[STUB] Update user group channels. Replaces the channels with the given list." + ( Summary "Replaces the channels with the given list." :> From 'V12 + :> CanThrow 'UserGroupNotFound + :> CanThrow 'UserGroupNotATeamAdmin + :> CanThrow 'UserGroupNotFound :> ZLocalUser :> "user-groups" :> Capture "gid" UserGroupId diff --git a/libs/wire-subsystems/postgres-migrations/202509190851023-user_group_conversations_create_table.sql b/libs/wire-subsystems/postgres-migrations/202509190851023-user_group_conversations_create_table.sql new file mode 100644 index 0000000000..f63e3745f0 --- /dev/null +++ b/libs/wire-subsystems/postgres-migrations/202509190851023-user_group_conversations_create_table.sql @@ -0,0 +1,8 @@ +CREATE TABLE public.user_group_channel ( + user_group_id uuid NOT NULL, + conv_id uuid NOT NULL, + PRIMARY KEY (user_group_id, conv_id) +); + +ALTER TABLE ONLY public.user_group_channel + ADD CONSTRAINT fk_user_group_channel FOREIGN KEY (user_group_id) REFERENCES public.user_group(id) ON DELETE CASCADE; diff --git a/libs/wire-subsystems/src/Wire/UserGroupStore.hs b/libs/wire-subsystems/src/Wire/UserGroupStore.hs index 8dad8471e7..951ac27b45 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupStore.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupStore.hs @@ -37,5 +37,6 @@ data UserGroupStore m a where AddUser :: UserGroupId -> UserId -> UserGroupStore m () UpdateUsers :: UserGroupId -> Vector UserId -> UserGroupStore m () RemoveUser :: UserGroupId -> UserId -> UserGroupStore m () + UpdateUserGroupChannels :: UserGroupId -> Vector ConvId -> UserGroupStore m () makeSem ''UserGroupStore diff --git a/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs index ed216cffd6..3249df272e 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs @@ -29,7 +29,7 @@ import Polysemy.Error (Error, throw) import Polysemy.Input import Wire.API.Pagination import Wire.API.User.Profile -import Wire.API.UserGroup +import Wire.API.UserGroup hiding (UpdateUserGroupChannels) import Wire.API.UserGroup.Pagination import Wire.UserGroupStore (PaginationState (..), UserGroupPageRequest (..), UserGroupStore (..), toSortBy) @@ -53,6 +53,7 @@ interpretUserGroupStoreToPostgres = AddUser gid uid -> addUser gid uid UpdateUsers gid uids -> updateUsers gid uids RemoveUser gid uid -> removeUser gid uid + UpdateUserGroupChannels gid convIds -> updateUserGroupChannels gid convIds updateUsers :: (UserGroupStorePostgresEffectConstraints r) => UserGroupId -> Vector UserId -> Sem r () updateUsers gid uids = do @@ -408,6 +409,38 @@ removeUser = delete from user_group_member where user_group_id = ($1 :: uuid) and user_id = ($2 :: uuid) |] +updateUserGroupChannels :: + forall r. + (UserGroupStorePostgresEffectConstraints r) => + UserGroupId -> + Vector ConvId -> + Sem r () +updateUserGroupChannels gid convIds = do + pool <- input + eitherErrorOrUnit <- liftIO $ use pool session + either throw pure eitherErrorOrUnit + where + session :: Session () + session = TxSessions.transaction TxSessions.Serializable TxSessions.Write $ do + Tx.statement (gid, convIds) deleteStatement + Tx.statement (gid, convIds) insertStatement + + deleteStatement :: Statement (UserGroupId, Vector ConvId) () + deleteStatement = + lmap + (bimap toUUID (fmap toUUID)) + $ [resultlessStatement| + delete from user_group_channel where user_group_id = ($1 :: uuid) and conv_id not in (SELECT unnest($2 :: uuid[])) + |] + + insertStatement :: Statement (UserGroupId, Vector ConvId) () + insertStatement = + lmap (bimap (fmap (.toUUID)) (fmap (.toUUID)) . uncurry toRelationTable) $ + [resultlessStatement| + insert into user_group_channel (user_group_id, conv_id) select * from unnest ($1 :: uuid[], $2 :: uuid[]) + on conflict (user_group_id, conv_id) do nothing + |] + crudUser :: forall r. (UserGroupStorePostgresEffectConstraints r) => diff --git a/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs b/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs index 20ef54fe13..836933038b 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs @@ -32,5 +32,6 @@ data UserGroupSubsystem m a where AddUsers :: UserId -> UserGroupId -> Vector UserId -> UserGroupSubsystem m () UpdateUsers :: UserId -> UserGroupId -> Vector UserId -> UserGroupSubsystem m () RemoveUser :: UserId -> UserGroupId -> UserId -> UserGroupSubsystem m () + UpdateChannels :: UserId -> UserGroupId -> Vector ConvId -> UserGroupSubsystem m () makeSem ''UserGroupSubsystem diff --git a/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs index e0655b786d..74f8765af4 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs @@ -23,6 +23,7 @@ import Wire.API.UserEvent import Wire.API.UserGroup import Wire.API.UserGroup.Pagination import Wire.Error +import Wire.GalleyAPIAccess (GalleyAPIAccess, getTeamConv) import Wire.NotificationSubsystem import Wire.TeamSubsystem import Wire.UserGroupStore (PaginationState (..), UserGroupPageRequest (..)) @@ -36,7 +37,8 @@ interpretUserGroupSubsystem :: Member Store.UserGroupStore r, Member (Input (Local ())) r, Member NotificationSubsystem r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member GalleyAPIAccess r ) => InterpreterFor UserGroupSubsystem r interpretUserGroupSubsystem = interpret $ \case @@ -50,11 +52,13 @@ interpretUserGroupSubsystem = interpret $ \case AddUsers adder groupId addeeIds -> addUsers adder groupId addeeIds UpdateUsers updater groupId uids -> updateUsers updater groupId uids RemoveUser remover groupId removeeId -> removeUser remover groupId removeeId + UpdateChannels performer groupId channelIds -> updateChannels performer groupId channelIds data UserGroupSubsystemError = UserGroupNotATeamAdmin | UserGroupMemberIsNotInTheSameTeam | UserGroupNotFound + | UserGroupChannelNotFound deriving (Show, Eq) userGroupSubsystemErrorToHttpError :: UserGroupSubsystemError -> HttpError @@ -63,6 +67,7 @@ userGroupSubsystemErrorToHttpError = UserGroupNotATeamAdmin -> errorToWai @E.UserGroupNotATeamAdmin UserGroupMemberIsNotInTheSameTeam -> errorToWai @E.UserGroupMemberIsNotInTheSameTeam UserGroupNotFound -> errorToWai @E.UserGroupNotFound + UserGroupChannelNotFound -> errorToWai @E.UserGroupChannelNotFound createUserGroup :: ( Member UserSubsystem r, @@ -328,3 +333,20 @@ removeUser remover groupId removeeId = do pushNotifications [ mkEvent remover (UserGroupUpdated groupId) admins ] + +updateChannels :: + ( Member UserSubsystem r, + Member Store.UserGroupStore r, + Member (Error UserGroupSubsystemError) r, + Member TeamSubsystem r, + Member GalleyAPIAccess r + ) => + UserId -> + UserGroupId -> + Vector ConvId -> + Sem r () +updateChannels performer groupId channelIds = do + void $ getUserGroup performer groupId >>= note UserGroupNotFound + teamId <- getTeamAsAdmin performer >>= note UserGroupNotATeamAdmin + traverse_ (getTeamConv performer teamId >=> note UserGroupChannelNotFound) channelIds + Store.updateUserGroupChannels groupId channelIds diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs index 8be5da9a0e..935197ffe2 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs @@ -7,9 +7,11 @@ module Wire.MockInterpreters.UserGroupStore where import Control.Lens ((%~), _2) +import Data.Domain (Domain (Domain)) import Data.Id import Data.Json.Util import Data.Map qualified as Map +import Data.Qualified (Qualified (Qualified)) import Data.Text qualified as T import Data.Time.Clock import Data.Vector (Vector, fromList) @@ -21,7 +23,7 @@ import Polysemy.State import System.Random (StdGen, mkStdGen) import Wire.API.Pagination import Wire.API.User -import Wire.API.UserGroup +import Wire.API.UserGroup hiding (UpdateUserGroupChannels) import Wire.API.UserGroup.Pagination import Wire.MockInterpreters.Now import Wire.MockInterpreters.Random @@ -62,6 +64,7 @@ userGroupStoreTestInterpreter = AddUser gid uid -> addUserImpl gid uid UpdateUsers gid uids -> updateUsersImpl gid uids RemoveUser gid uid -> removeUserImpl gid uid + UpdateUserGroupChannels gid convIds -> updateUserGroupChannelsImpl gid convIds updateUsersImpl :: (UserGroupStoreInMemEffectConstraints r) => UserGroupId -> Vector UserId -> Sem r () updateUsersImpl gid uids = do @@ -179,6 +182,25 @@ removeUserImpl gid uid = do modifyUserGroupsGidOnly gid (Map.alter f) +updateUserGroupChannelsImpl :: + (UserGroupStoreInMemEffectConstraints r) => + UserGroupId -> + Vector ConvId -> + Sem r () +updateUserGroupChannelsImpl gid convIds = do + let f :: Maybe UserGroup -> Maybe UserGroup + f Nothing = Nothing + f (Just g) = + Just + ( g + { channels = Identity $ Just $ flip Qualified (Domain "") <$> convIds, + channelsCount = Just $ length convIds + } :: + UserGroup + ) + + modifyUserGroupsGidOnly gid (Map.alter f) + ---------------------------------------------------------------------- modifyUserGroupsGidOnly :: diff --git a/postgres-schema.sql b/postgres-schema.sql index c23af1aa97..9075fe312f 100644 --- a/postgres-schema.sql +++ b/postgres-schema.sql @@ -92,9 +92,20 @@ CREATE TABLE public.user_group_member ( user_id uuid NOT NULL ); - ALTER TABLE public.user_group_member OWNER TO "wire-server"; +-- +-- Name: user_group_channel; Type: TABLE; Schema: public; Owner: wire-server +-- + +CREATE TABLE public.user_group_channel ( + user_group_id uuid NOT NULL, + conv_id uuid NOT NULL +); + + +ALTER TABLE public.user_group_channel OWNER TO "wire-server"; + -- -- Name: collaborators collaborators_pkey; Type: CONSTRAINT; Schema: public; Owner: wire-server -- @@ -119,6 +130,14 @@ ALTER TABLE ONLY public.user_group_member ADD CONSTRAINT user_group_member_pkey PRIMARY KEY (user_group_id, user_id); +-- +-- Name: user_group_channel user_group_member_pkey; Type: CONSTRAINT; Schema: public; Owner: wire-server +-- + +ALTER TABLE ONLY public.user_group_channel + ADD CONSTRAINT user_group_channel_pkey PRIMARY KEY (user_group_id, conv_id); + + -- -- Name: user_group user_group_pkey; Type: CONSTRAINT; Schema: public; Owner: wire-server -- @@ -149,6 +168,14 @@ ALTER TABLE ONLY public.user_group_member ADD CONSTRAINT fk_user_group FOREIGN KEY (user_group_id) REFERENCES public.user_group(id) ON DELETE CASCADE; +-- +-- Name: user_group_channel fk_user_group; Type: FK CONSTRAINT; Schema: public; Owner: wire-server +-- + +ALTER TABLE ONLY public.user_group_channel + ADD CONSTRAINT fk_user_group_channel FOREIGN KEY (user_group_id) REFERENCES public.user_group(id) ON DELETE CASCADE; + + -- -- Name: SCHEMA public; Type: ACL; Schema: -; Owner: wire-server -- diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 259ab37c89..7ad2ef84a0 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -1711,8 +1711,8 @@ removeUserFromGroup lusr gid mid = lift . liftSem $ UserGroup.removeUser (tUnqua updateUserGroupMembers :: (_) => Local UserId -> UserGroupId -> UpdateUserGroupMembers -> Handler r () updateUserGroupMembers lusr gid gupd = lift . liftSem $ UserGroup.updateUsers (tUnqualified lusr) gid gupd.members -updateUserGroupChannels :: Local UserId -> UserGroupId -> UpdateUserGroupChannels -> Handler r () -updateUserGroupChannels _ _ _ = pure () +updateUserGroupChannels :: (_) => Local UserId -> UserGroupId -> UpdateUserGroupChannels -> Handler r () +updateUserGroupChannels lusr gid upd = lift . liftSem $ UserGroup.updateChannels (tUnqualified lusr) gid upd.channels checkUserGroupNameAvailable :: Local UserId -> CheckUserGroupName -> Handler r UserGroupNameAvailability checkUserGroupNameAvailable _ _ = pure $ UserGroupNameAvailability True From b4af1e1b4c256cc234e67d999595c8192fafc5dc Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Thu, 18 Sep 2025 19:24:31 +0200 Subject: [PATCH 02/40] WPB-19712: Allow team admin to update the channels to user-group association --- changelog.d/2-features/WPB-19713 | 1 + integration/test/API/Brig.hs | 5 +++ integration/test/Test/UserGroup.hs | 3 +- .../src/Wire/API/Routes/Public/Brig.hs | 5 +-- .../src/Wire/UserGroupStore.hs | 1 + .../src/Wire/UserGroupStore/Postgres.hs | 23 +++++++++++++ .../src/Wire/UserGroupSubsystem.hs | 1 + .../Wire/UserGroupSubsystem/Interpreter.hs | 15 +++++++++ .../Wire/MockInterpreters/UserGroupStore.hs | 11 ++++++- postgres-schema.sql | 11 +++++++ services/brig/brig.cabal | 3 +- services/brig/default.nix | 1 + services/brig/src/Brig/API/Public.hs | 33 +++++++++++++++++-- 13 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 changelog.d/2-features/WPB-19713 diff --git a/changelog.d/2-features/WPB-19713 b/changelog.d/2-features/WPB-19713 new file mode 100644 index 0000000000..cdbf1fd8a2 --- /dev/null +++ b/changelog.d/2-features/WPB-19713 @@ -0,0 +1 @@ +Implement `channels` and `channelsCount` in `user-groups` endpoints. diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index bce2531ada..9963e05709 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -1061,6 +1061,11 @@ getUserGroup user gid = do req <- baseRequest user Brig Versioned $ joinHttpPath ["user-groups", gid] submit "GET" req +getUserGroupWithChannels :: (MakesValue user) => user -> String -> App Response +getUserGroupWithChannels user gid = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["user-groups", gid] + submit "GET" $ req & addQueryParams [("include_channels", "true")] + updateUserGroupChannels :: (MakesValue user) => user -> String -> [String] -> App Response updateUserGroupChannels user gid convIds = do req <- baseRequest user Brig Versioned $ joinHttpPath ["user-groups", gid, "channels"] diff --git a/integration/test/Test/UserGroup.hs b/integration/test/Test/UserGroup.hs index c6dc526faa..6764c8f60e 100644 --- a/integration/test/Test/UserGroup.hs +++ b/integration/test/Test/UserGroup.hs @@ -390,8 +390,9 @@ testUserGroupUpdateChannels = do updateUserGroupChannels alice gid [convId.id_] >>= assertSuccess -- bobId <- asString $ bob %. "id" - bindResponse (getUserGroup alice gid) $ \resp -> do + bindResponse (getUserGroupWithChannels alice gid) $ \resp -> do resp.status `shouldMatchInt` 200 + resp.json %. "channels" `shouldMatch` [object ["id" .= convId.id_, "domain" .= convId.domain]] -- FUTUREWORK: check the actual associated channels -- resp.json %. "members" `shouldMatch` [bobId] diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 3c59836110..255572678b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -314,7 +314,7 @@ type UserGroupAPI = ) :<|> Named "get-user-group" - ( Summary "[STUB] (channels in response not implemented)" + ( Summary "Fetch a group accessible from the logged-in user" :> From 'V10 :> ZLocalUser :> CanThrow 'UserGroupNotFound @@ -331,7 +331,7 @@ type UserGroupAPI = ) :<|> Named "get-user-groups" - ( Summary "[STUB] (channelsCount not implemented)" + ( Summary "Fetch groups accessible from the logged-in user" :> From 'V10 :> ZLocalUser :> "user-groups" @@ -342,6 +342,7 @@ type UserGroupAPI = :> QueryParam' '[Optional, Strict, LastSeenNameDesc] "last_seen_name" UserGroupName :> QueryParam' '[Optional, Strict, LastSeenCreatedAtDesc] "last_seen_created_at" UTCTimeMillis :> QueryParam' '[Optional, Strict, LastSeenIdDesc] "last_seen_id" UserGroupId + :> QueryFlag "include_channels" :> QueryFlag "include_member_count" :> Get '[JSON] UserGroupPage ) diff --git a/libs/wire-subsystems/src/Wire/UserGroupStore.hs b/libs/wire-subsystems/src/Wire/UserGroupStore.hs index 951ac27b45..6256e02672 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupStore.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupStore.hs @@ -38,5 +38,6 @@ data UserGroupStore m a where UpdateUsers :: UserGroupId -> Vector UserId -> UserGroupStore m () RemoveUser :: UserGroupId -> UserId -> UserGroupStore m () UpdateUserGroupChannels :: UserGroupId -> Vector ConvId -> UserGroupStore m () + ListUserGroupChannels :: UserGroupId -> UserGroupStore m (Vector ConvId) makeSem ''UserGroupStore diff --git a/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs index 3249df272e..48d49bdab7 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs @@ -54,6 +54,7 @@ interpretUserGroupStoreToPostgres = UpdateUsers gid uids -> updateUsers gid uids RemoveUser gid uid -> removeUser gid uid UpdateUserGroupChannels gid convIds -> updateUserGroupChannels gid convIds + ListUserGroupChannels gid -> listUserGroupChannels gid updateUsers :: (UserGroupStorePostgresEffectConstraints r) => UserGroupId -> Vector UserId -> Sem r () updateUsers gid uids = do @@ -441,6 +442,28 @@ updateUserGroupChannels gid convIds = do on conflict (user_group_id, conv_id) do nothing |] +listUserGroupChannels :: + forall r. + (UserGroupStorePostgresEffectConstraints r) => + UserGroupId -> + Sem r (Vector ConvId) +listUserGroupChannels gid = do + pool <- input + eitherErrorOrUnit <- liftIO $ use pool session + either throw pure eitherErrorOrUnit + where + session :: Session (Vector ConvId) + session = statement gid selectStatement + + selectStatement :: Statement UserGroupId (Vector ConvId) + selectStatement = + dimap + toUUID + (fmap Id) + [vectorStatement| + select (conv_id :: uuid) from user_group_channel where user_group_id = ($1 :: uuid) + |] + crudUser :: forall r. (UserGroupStorePostgresEffectConstraints r) => diff --git a/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs b/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs index 836933038b..2148827d5d 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs @@ -33,5 +33,6 @@ data UserGroupSubsystem m a where UpdateUsers :: UserId -> UserGroupId -> Vector UserId -> UserGroupSubsystem m () RemoveUser :: UserId -> UserGroupId -> UserId -> UserGroupSubsystem m () UpdateChannels :: UserId -> UserGroupId -> Vector ConvId -> UserGroupSubsystem m () + ListChannels :: UserId -> UserGroupId -> UserGroupSubsystem m (Vector ConvId) makeSem ''UserGroupSubsystem diff --git a/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs index 74f8765af4..a1b63dcd7a 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs @@ -53,6 +53,7 @@ interpretUserGroupSubsystem = interpret $ \case UpdateUsers updater groupId uids -> updateUsers updater groupId uids RemoveUser remover groupId removeeId -> removeUser remover groupId removeeId UpdateChannels performer groupId channelIds -> updateChannels performer groupId channelIds + ListChannels performer groupId -> listChannels performer groupId data UserGroupSubsystemError = UserGroupNotATeamAdmin @@ -350,3 +351,17 @@ updateChannels performer groupId channelIds = do teamId <- getTeamAsAdmin performer >>= note UserGroupNotATeamAdmin traverse_ (getTeamConv performer teamId >=> note UserGroupChannelNotFound) channelIds Store.updateUserGroupChannels groupId channelIds + +listChannels :: + ( Member UserSubsystem r, + Member Store.UserGroupStore r, + Member (Error UserGroupSubsystemError) r, + Member TeamSubsystem r + ) => + UserId -> + UserGroupId -> + Sem r (Vector ConvId) +listChannels performer groupId = do + void $ getUserGroup performer groupId >>= note UserGroupNotFound + void $ getUserTeam performer >>= note UserGroupNotATeamAdmin + Store.listUserGroupChannels groupId diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs index 935197ffe2..59c901df94 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs @@ -11,7 +11,7 @@ import Data.Domain (Domain (Domain)) import Data.Id import Data.Json.Util import Data.Map qualified as Map -import Data.Qualified (Qualified (Qualified)) +import Data.Qualified import Data.Text qualified as T import Data.Time.Clock import Data.Vector (Vector, fromList) @@ -65,6 +65,7 @@ userGroupStoreTestInterpreter = UpdateUsers gid uids -> updateUsersImpl gid uids RemoveUser gid uid -> removeUserImpl gid uid UpdateUserGroupChannels gid convIds -> updateUserGroupChannelsImpl gid convIds + ListUserGroupChannels gid -> listUserGroupChannelsImpl gid updateUsersImpl :: (UserGroupStoreInMemEffectConstraints r) => UserGroupId -> Vector UserId -> Sem r () updateUsersImpl gid uids = do @@ -201,6 +202,14 @@ updateUserGroupChannelsImpl gid convIds = do modifyUserGroupsGidOnly gid (Map.alter f) +listUserGroupChannelsImpl :: + (UserGroupStoreInMemEffectConstraints r) => + UserGroupId -> + Sem r (Vector ConvId) +listUserGroupChannelsImpl gid = + foldMap (fmap qUnqualified) . (runIdentity . (.channels) . snd <=< find ((== gid) . snd . fst) . Map.toList) + <$> get @(Map (TeamId, UserGroupId) UserGroup) + ---------------------------------------------------------------------- modifyUserGroupsGidOnly :: diff --git a/postgres-schema.sql b/postgres-schema.sql index 9075fe312f..70443f919e 100644 --- a/postgres-schema.sql +++ b/postgres-schema.sql @@ -104,6 +104,17 @@ CREATE TABLE public.user_group_channel ( ); + +-- +-- Name: user_group_channel; Type: TABLE; Schema: public; Owner: wire-server +-- + +CREATE TABLE public.user_group_channel ( + user_group_id uuid NOT NULL, + conv_id uuid NOT NULL +); + + ALTER TABLE public.user_group_channel OWNER TO "wire-server"; -- diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 67449f7bb0..4d921d809b 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -218,7 +218,7 @@ library , amqp , async >=2.1 , auto-update >=0.1 - , base >=4 && <5 + , base >=4 && <5 , base-prelude , base16-bytestring >=0.1 , base64-bytestring >=1.0 @@ -314,6 +314,7 @@ library , uri-bytestring >=0.2 , utf8-string , uuid >=1.3.5 + , vector >=0.13.2.0 , wai >=3.0 , wai-extra >=3.0 , wai-middleware-gunzip >=0.0.2 diff --git a/services/brig/default.nix b/services/brig/default.nix index 550ed4212b..5827352d74 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -266,6 +266,7 @@ mkDerivation { uri-bytestring utf8-string uuid + vector wai wai-extra wai-middleware-gunzip diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 7ad2ef84a0..9d620de83c 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -85,6 +85,7 @@ import Data.Qualified import Data.Range import Data.Schema () import Data.Text.Encoding qualified as Text +import Data.Vector qualified as Vector import Data.ZAuth.CryptoSign (CryptoSign) import Data.ZAuth.Token qualified as ZAuth import FileEmbedLzma @@ -1676,7 +1677,20 @@ createUserGroup :: (_) => Local UserId -> NewUserGroup -> Handler r UserGroup createUserGroup lusr newUserGroup = lift . liftSem $ UserGroup.createGroup (tUnqualified lusr) newUserGroup getUserGroup :: (_) => Local UserId -> UserGroupId -> Bool -> Handler r (Maybe UserGroup) -getUserGroup lusr ugid _ = lift . liftSem $ UserGroup.getGroup (tUnqualified lusr) ugid +getUserGroup lusr ugid includeChannels = + lift . liftSem $ do + mUserGroup <- UserGroup.getGroup (tUnqualified lusr) ugid + if includeChannels + then forM mUserGroup $ \userGroup -> do + fetchedChannels <- + fmap (tUntagged . qualifyAs lusr) + <$> UserGroup.listChannels (tUnqualified lusr) userGroup.id_ + pure + userGroup + { channels = Identity $ Just fetchedChannels, + channelsCount = Just $ Vector.length fetchedChannels + } + else pure mUserGroup getUserGroups :: (_) => @@ -1689,9 +1703,22 @@ getUserGroups :: Maybe UTCTimeMillis -> Maybe UserGroupId -> Bool -> + Bool -> Handler r UserGroupPage -getUserGroups lusr q sortByKeys sortOrder pSize mLastName mLastCreatedAt mLastId includeMemberCount = - lift . liftSem $ UserGroup.getGroups (tUnqualified lusr) q sortByKeys sortOrder pSize mLastName mLastCreatedAt mLastId includeMemberCount +getUserGroups lusr q sortByKeys sortOrder pSize mLastName mLastCreatedAt mLastId includeChannels includeMemberCount = + lift . liftSem $ do + userGroups <- UserGroup.getGroups (tUnqualified lusr) q sortByKeys sortOrder pSize mLastName mLastCreatedAt mLastId includeMemberCount + if includeChannels + then do + newPage <- + forM userGroups.page $ \userGroup -> do + fetchedChannels <- UserGroup.listChannels (tUnqualified lusr) userGroup.id_ + pure + userGroup + { channelsCount = Just $ Vector.length fetchedChannels + } + pure userGroups {page = newPage} + else pure userGroups updateUserGroup :: (_) => Local UserId -> UserGroupId -> UserGroupUpdate -> (Handler r) () updateUserGroup lusr gid gupd = lift . liftSem $ UserGroup.updateGroup (tUnqualified lusr) gid gupd From 05cb59ef5c88025c86e0fdb9a8b9f487fd0a2c9c Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 30 Sep 2025 12:57:59 +0000 Subject: [PATCH 03/40] job types --- libs/types-common/src/Data/Id.hs | 7 ++ libs/wire-api/src/Wire/API/BackgroundJobs.hs | 91 +++++++++++++++++++ .../unit/Test/Wire/API/Roundtrip/Aeson.hs | 4 +- libs/wire-api/wire-api.cabal | 1 + 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 libs/wire-api/src/Wire/API/BackgroundJobs.hs diff --git a/libs/types-common/src/Data/Id.hs b/libs/types-common/src/Data/Id.hs index ebd24fc53b..de8f980093 100644 --- a/libs/types-common/src/Data/Id.hs +++ b/libs/types-common/src/Data/Id.hs @@ -37,6 +37,7 @@ module Data.Id ServiceId, TeamId, ScimTokenId, + JobId, parseIdFromText, idToText, idObjectSchema, @@ -111,6 +112,7 @@ data IdTag | OAuthClient | OAuthRefreshToken | Challenge + | Job idTagName :: IdTag -> Text idTagName Asset = "Asset" @@ -125,6 +127,7 @@ idTagName ScimToken = "ScimToken" idTagName OAuthClient = "OAuthClient" idTagName OAuthRefreshToken = "OAuthRefreshToken" idTagName Challenge = "Challenge" +idTagName Job = "Job" class KnownIdTag (t :: IdTag) where idTagValue :: IdTag @@ -151,6 +154,8 @@ instance KnownIdTag 'OAuthClient where idTagValue = OAuthClient instance KnownIdTag 'OAuthRefreshToken where idTagValue = OAuthRefreshToken +instance KnownIdTag 'Job where idTagValue = Job + type AssetId = Id 'Asset type InvitationId = Id 'Invitation @@ -177,6 +182,8 @@ type OAuthRefreshTokenId = Id 'OAuthRefreshToken type ChallengeId = Id 'Challenge +type JobId = Id 'Job + -- Id ------------------------------------------------------------------------- data NoId = NoId deriving (Eq, Show, Generic) diff --git a/libs/wire-api/src/Wire/API/BackgroundJobs.hs b/libs/wire-api/src/Wire/API/BackgroundJobs.hs new file mode 100644 index 0000000000..90b6424efe --- /dev/null +++ b/libs/wire-api/src/Wire/API/BackgroundJobs.hs @@ -0,0 +1,91 @@ +{-# LANGUAGE StrictData #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.API.BackgroundJobs + ( Job (..), + JobType (..), + publishBackgroundJob, + backendJobsRoutingKey, + ) +where + +import Control.Lens ((?~)) +import Data.Aeson qualified as Aeson +import Data.Id (JobId, idToText) +import Data.OpenApi qualified as S +import Data.Schema +import Imports +import Network.AMQP qualified as Q +import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) + +data JobType + = JobNoop + deriving stock (Eq, Ord, Show, Generic) + deriving (Arbitrary) via GenericUniform JobType + +instance ToSchema JobType where + schema = + enum @Text "JobType" $ + mconcat + [ element "noop" JobNoop + ] + +-- | Background job envelope. Payload is a free-form JSON object. +data Job = Job + { jobId :: JobId, + requestId :: Text, + jobType :: JobType, + payload :: Aeson.Object + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via GenericUniform Job + deriving (Aeson.ToJSON, Aeson.FromJSON, S.ToSchema) via Schema Job + +instance ToSchema Job where + schema = + object "Job" $ + Job + <$> jobId .= field "id" schema + <*> requestId .= field "requestId" schema + <*> jobType .= field "type" schema + <*> payload .= field "payload" payloadSchema + +payloadSchema :: ValueSchema NamedSwaggerDoc Aeson.Object +payloadSchema = mkSchema sdoc Aeson.parseJSON (Just . Aeson.toJSON) + where + sdoc = + swaggerDoc @Aeson.Object + & S.schema . S.title ?~ "Payload" + & S.schema . S.description ?~ "Job payload as a JSON object" + +-- | Publish a job to the default exchange with routing key "background-jobs". +-- Sets content-type to application/json and message_id to the Job.id. +publishBackgroundJob :: Q.Channel -> Job -> IO () +publishBackgroundJob chan job = do + let msg = + Q.newMsg + { Q.msgBody = Aeson.encode job, + Q.msgContentType = Just "application/json", + Q.msgID = Just (idToText job.jobId), + Q.msgCorrelationID = Just job.requestId + } + void $ Q.publishMsg chan "" backendJobsRoutingKey msg + +backendJobsRoutingKey :: Text +backendJobsRoutingKey = "background-jobs" diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs index 57cf9e9fda..6908d49992 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs @@ -27,6 +27,7 @@ import Test.Tasty qualified as T import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (.&&.), (===)) import Type.Reflection (typeRep) import Wire.API.Asset qualified as Asset +import Wire.API.BackgroundJobs qualified as BackgroundJobs import Wire.API.Call.Config qualified as Call.Config import Wire.API.Connection qualified as Connection import Wire.API.Conversation qualified as Conversation @@ -356,7 +357,8 @@ tests = testRoundTrip @TeamsIntra.TeamStatus, testRoundTrip @TeamsIntra.TeamStatusUpdate, testRoundTrip @TeamsIntra.TeamData, - testRoundTrip @TeamsIntra.TeamName + testRoundTrip @TeamsIntra.TeamName, + testRoundTrip @BackgroundJobs.Job ] testRoundTrip :: diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 29004088e0..17e175b766 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -72,6 +72,7 @@ library Wire.API.App Wire.API.ApplyMods Wire.API.Asset + Wire.API.BackgroundJobs Wire.API.Bot Wire.API.Bot.Service Wire.API.Call.Config From 2af71d8d8ff0f1318635ab98ffa3abb1afe1a890 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 30 Sep 2025 13:33:18 +0000 Subject: [PATCH 04/40] job consumer scaffold --- libs/wire-api/src/Wire/API/BackgroundJobs.hs | 12 ++-- .../background-worker/background-worker.cabal | 1 + .../src/Wire/BackgroundWorker.hs | 7 +- .../src/Wire/BackgroundWorker/Env.hs | 8 ++- .../Wire/BackgroundWorker/Jobs/Consumer.hs | 70 +++++++++++++++++++ .../src/Wire/BackgroundWorker/Options.hs | 15 +++- 6 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs diff --git a/libs/wire-api/src/Wire/API/BackgroundJobs.hs b/libs/wire-api/src/Wire/API/BackgroundJobs.hs index 90b6424efe..d18ea60cea 100644 --- a/libs/wire-api/src/Wire/API/BackgroundJobs.hs +++ b/libs/wire-api/src/Wire/API/BackgroundJobs.hs @@ -21,7 +21,8 @@ module Wire.API.BackgroundJobs ( Job (..), JobType (..), publishBackgroundJob, - backendJobsRoutingKey, + backgroundJobsRoutingKey, + backgroundJobsQueueName, ) where @@ -85,7 +86,10 @@ publishBackgroundJob chan job = do Q.msgID = Just (idToText job.jobId), Q.msgCorrelationID = Just job.requestId } - void $ Q.publishMsg chan "" backendJobsRoutingKey msg + void $ Q.publishMsg chan "" backgroundJobsRoutingKey msg -backendJobsRoutingKey :: Text -backendJobsRoutingKey = "background-jobs" +backgroundJobsRoutingKey :: Text +backgroundJobsRoutingKey = backgroundJobsQueueName + +backgroundJobsQueueName :: Text +backgroundJobsQueueName = "background-jobs" diff --git a/services/background-worker/background-worker.cabal b/services/background-worker/background-worker.cabal index 25079919e4..1acdcd7c4f 100644 --- a/services/background-worker/background-worker.cabal +++ b/services/background-worker/background-worker.cabal @@ -16,6 +16,7 @@ library Wire.BackgroundWorker Wire.BackgroundWorker.Env Wire.BackgroundWorker.Health + Wire.BackgroundWorker.Jobs.Consumer Wire.BackgroundWorker.Options Wire.BackgroundWorker.Util Wire.DeadUserNotificationWatcher diff --git a/services/background-worker/src/Wire/BackgroundWorker.hs b/services/background-worker/src/Wire/BackgroundWorker.hs index e6110fb438..4fe9e2aefc 100644 --- a/services/background-worker/src/Wire/BackgroundWorker.hs +++ b/services/background-worker/src/Wire/BackgroundWorker.hs @@ -14,6 +14,7 @@ import Util.Options import Wire.BackendNotificationPusher qualified as BackendNotificationPusher import Wire.BackgroundWorker.Env import Wire.BackgroundWorker.Health qualified as Health +import Wire.BackgroundWorker.Jobs.Consumer qualified as Jobs import Wire.BackgroundWorker.Options import Wire.DeadUserNotificationWatcher qualified as DeadUserNotificationWatcher @@ -29,10 +30,14 @@ run opts = do runAppT env $ withNamedLogger "dead-user-notification-watcher" $ DeadUserNotificationWatcher.startWorker amqpEP + cleanupJobs <- + runAppT env $ + withNamedLogger "background-job-consumer" $ + Jobs.startWorker amqpEP let -- cleanup will run in a new thread when the signal is caught, so we need to use IORefs and -- specific exception types to message threads to clean up cleanup = do - concurrently_ cleanupDeadUserNotifWatcher cleanupBackendNotifPusher + concurrently_ cleanupDeadUserNotifWatcher (concurrently_ cleanupBackendNotifPusher cleanupJobs) let server = defaultServer (T.unpack $ opts.backgroundWorker.host) opts.backgroundWorker.port env.logger let settings = newSettings server -- Additional cleanup when shutting down via signals. diff --git a/services/background-worker/src/Wire/BackgroundWorker/Env.hs b/services/background-worker/src/Wire/BackgroundWorker/Env.hs index 93711e9d9b..eab31067f9 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Env.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Env.hs @@ -30,12 +30,14 @@ type IsWorking = Bool data Worker = BackendNotificationPusher | DeadUserNotificationWatcher + | BackgroundJobConsumer deriving (Eq, Ord) workerName :: Worker -> Text workerName = \case BackendNotificationPusher -> "backend-notification-pusher" DeadUserNotificationWatcher -> "dead-user-notification-watcher" + BackgroundJobConsumer -> "background-job-consumer" data Env = Env { http2Manager :: Http2Manager, @@ -47,6 +49,8 @@ data Env = Env defederationTimeout :: ResponseTimeout, backendNotificationMetrics :: BackendNotificationMetrics, backendNotificationsConfig :: BackendNotificationsConfig, + -- TODO(leif): configmaps and helm charts + backgroundJobsConfig :: BackgroundJobsConfig, workerRunningGauge :: Vector Text Gauge, statuses :: IORef (Map Worker IsWorking), cassandra :: ClientState @@ -86,10 +90,12 @@ mkEnv opts = do statuses <- newIORef $ Map.fromList - [ (BackendNotificationPusher, False) + [ (BackendNotificationPusher, False), + (BackgroundJobConsumer, False) ] backendNotificationMetrics <- mkBackendNotificationMetrics let backendNotificationsConfig = opts.backendNotificationPusher + backgroundJobsConfig = opts.backgroundJobs workerRunningGauge <- mkWorkerRunningGauge pure Env {..} diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs new file mode 100644 index 0000000000..80f79482ef --- /dev/null +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs @@ -0,0 +1,70 @@ +module Wire.BackgroundWorker.Jobs.Consumer (startWorker, BackgroundJobsMetrics (..)) where + +import Data.Map.Strict qualified as Map +import Imports +import Network.AMQP qualified as Q +import Network.AMQP.Extended +import Network.AMQP.Types qualified as QT +import Prometheus +import System.Logger.Class qualified as Log +import UnliftIO +import Wire.API.BackgroundJobs (backgroundJobsQueueName) +import Wire.BackgroundWorker.Env +import Wire.BackgroundWorker.Options +import Wire.BackgroundWorker.Util (CleanupAction) + +ensureBackgroundJobsQueue :: Q.Channel -> IO () +ensureBackgroundJobsQueue chan = do + let headers = + QT.FieldTable + ( Map.fromList + [ ("x-queue-type", QT.FVString "quorum") + ] + ) + q = + Q.newQueue + { Q.queueName = backgroundJobsQueueName, + Q.queueDurable = True, + Q.queueAutoDelete = False, + Q.queueExclusive = False, + Q.queueHeaders = headers + } + void $ Q.declareQueue chan q + +data BackgroundJobsMetrics = BackgroundJobsMetrics + { workersBusy :: Gauge, + concurrencyConfigured :: Gauge + } + +mkMetrics :: IO BackgroundJobsMetrics +mkMetrics = + BackgroundJobsMetrics + <$> register (gauge $ Info {metricName = "wire_background_jobs_workers_busy", metricHelp = "In-flight background jobs"}) + <*> register (gauge $ Info {metricName = "wire_background_jobs_concurrency_configured", metricHelp = "Configured concurrency for this process"}) + +startWorker :: AmqpEndpoint -> AppT IO CleanupAction +startWorker rabbitmqOpts = do + env <- ask + let cfg = env.backgroundJobsConfig + metrics <- liftIO mkMetrics + markAsNotWorking BackgroundJobConsumer + void . async . liftIO $ + openConnectionWithRetries env.logger rabbitmqOpts (Just "background-job-consumer") $ + RabbitMqHooks + { onNewChannel = \chan -> do + -- declare queue and set prefetch to concurrency + ensureBackgroundJobsQueue chan + Q.qos chan 0 (fromIntegral cfg.concurrency) False + -- set gauges + setGauge metrics.concurrencyConfigured (fromIntegral cfg.concurrency) + runAppT env $ markAsWorking BackgroundJobConsumer, + onChannelException = \_ -> runAppT env $ markAsNotWorking BackgroundJobConsumer, + onConnectionClose = runAppT env $ markAsNotWorking BackgroundJobConsumer + } + pure $ runAppT env $ cleanup + where + cleanup :: AppT IO () + cleanup = do + -- nothing to close explicitly; the AMQP helper closes channel/connection on shutdown + Log.info $ Log.msg (Log.val "Background job consumer cleanup") + markAsNotWorking BackgroundJobConsumer diff --git a/services/background-worker/src/Wire/BackgroundWorker/Options.hs b/services/background-worker/src/Wire/BackgroundWorker/Options.hs index f9055d89e0..b8af0e966d 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Options.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Options.hs @@ -15,7 +15,8 @@ data Opts = Opts -- | Seconds, Nothing for no timeout defederationTimeout :: Maybe Int, backendNotificationPusher :: BackendNotificationsConfig, - cassandra :: CassandraOpts + cassandra :: CassandraOpts, + backgroundJobs :: BackgroundJobsConfig } deriving (Show, Generic) @@ -48,3 +49,15 @@ instance FromJSON RabbitMqOpts where <$> ( (Right <$> parseJSON v) <|> (Left <$> parseJSON v) ) + +data BackgroundJobsConfig = BackgroundJobsConfig + { -- | Maximum parallel jobs processed by this process + concurrency :: Int, + -- | Per-attempt timeout (seconds) + jobTimeout :: Int, + -- | Total attempts including first run + maxAttempts :: Int + } + deriving (Show, Generic) + +instance FromJSON BackgroundJobsConfig From d1b656bf50beb914a32c70cebb5408c9e8abd9fe Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 30 Sep 2025 14:38:55 +0000 Subject: [PATCH 05/40] implement basic handler for jobs --- .../background-worker/background-worker.cabal | 2 + .../Wire/BackgroundWorker/Jobs/Consumer.hs | 78 +++++++++++++++++-- .../Wire/BackgroundWorker/Jobs/Registry.hs | 18 +++++ 3 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs diff --git a/services/background-worker/background-worker.cabal b/services/background-worker/background-worker.cabal index 1acdcd7c4f..c32f1fb6b5 100644 --- a/services/background-worker/background-worker.cabal +++ b/services/background-worker/background-worker.cabal @@ -17,6 +17,7 @@ library Wire.BackgroundWorker.Env Wire.BackgroundWorker.Health Wire.BackgroundWorker.Jobs.Consumer + Wire.BackgroundWorker.Jobs.Registry Wire.BackgroundWorker.Options Wire.BackgroundWorker.Util Wire.DeadUserNotificationWatcher @@ -49,6 +50,7 @@ library , servant-client , servant-server , text + , time , tinylog , transformers , transformers-base diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs index 80f79482ef..3deba18ecc 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs @@ -1,15 +1,20 @@ module Wire.BackgroundWorker.Jobs.Consumer (startWorker, BackgroundJobsMetrics (..)) where +import Control.Retry +import Data.Aeson qualified as Aeson import Data.Map.Strict qualified as Map +import Data.Time.Clock.POSIX (getPOSIXTime) import Imports import Network.AMQP qualified as Q import Network.AMQP.Extended +import Network.AMQP.Lifted qualified as QL import Network.AMQP.Types qualified as QT import Prometheus import System.Logger.Class qualified as Log import UnliftIO -import Wire.API.BackgroundJobs (backgroundJobsQueueName) +import Wire.API.BackgroundJobs import Wire.BackgroundWorker.Env +import Wire.BackgroundWorker.Jobs.Registry (dispatchJob, jobTypeLabel) import Wire.BackgroundWorker.Options import Wire.BackgroundWorker.Util (CleanupAction) @@ -33,14 +38,28 @@ ensureBackgroundJobsQueue chan = do data BackgroundJobsMetrics = BackgroundJobsMetrics { workersBusy :: Gauge, - concurrencyConfigured :: Gauge + concurrencyConfigured :: Gauge, + jobsReceived :: Vector Text Counter, + jobsStarted :: Vector Text Counter, + jobsSucceeded :: Vector Text Counter, + jobsFailed :: Vector Text Counter, + jobsInvalid :: Vector Text Counter, + jobsRedelivered :: Vector Text Counter, + jobDuration :: Vector Text Histogram } mkMetrics :: IO BackgroundJobsMetrics -mkMetrics = - BackgroundJobsMetrics - <$> register (gauge $ Info {metricName = "wire_background_jobs_workers_busy", metricHelp = "In-flight background jobs"}) - <*> register (gauge $ Info {metricName = "wire_background_jobs_concurrency_configured", metricHelp = "Configured concurrency for this process"}) +mkMetrics = do + workersBusy <- register (gauge $ Info {metricName = "wire_background_jobs_workers_busy", metricHelp = "In-flight background jobs"}) + concurrencyConfigured <- register (gauge $ Info {metricName = "wire_background_jobs_concurrency_configured", metricHelp = "Configured concurrency for this process"}) + jobsReceived <- register (vector "job_type" $ counter $ Info "wire_background_jobs_received_total" "Jobs received") + jobsStarted <- register (vector "job_type" $ counter $ Info "wire_background_jobs_started_total" "Jobs started") + jobsSucceeded <- register (vector "job_type" $ counter $ Info "wire_background_jobs_succeeded_total" "Jobs succeeded") + jobsFailed <- register (vector "job_type" $ counter $ Info "wire_background_jobs_failed_total" "Jobs failed") + jobsInvalid <- register (vector "job_type" $ counter $ Info "wire_background_jobs_invalid_total" "Invalid jobs received") + jobsRedelivered <- register (vector "job_type" $ counter $ Info "wire_background_jobs_redelivered_total" "Jobs marked redelivered by broker") + jobDuration <- register (vector "job_type" $ histogram (Info "wire_background_jobs_duration_seconds" "Job duration seconds") defaultBuckets) + pure BackgroundJobsMetrics {workersBusy, concurrencyConfigured, jobsReceived, jobsStarted, jobsSucceeded, jobsFailed, jobsInvalid, jobsRedelivered, jobDuration} startWorker :: AmqpEndpoint -> AppT IO CleanupAction startWorker rabbitmqOpts = do @@ -57,6 +76,8 @@ startWorker rabbitmqOpts = do Q.qos chan 0 (fromIntegral cfg.concurrency) False -- set gauges setGauge metrics.concurrencyConfigured (fromIntegral cfg.concurrency) + -- start consuming with manual ack + void $ QL.consumeMsgs chan backgroundJobsQueueName Q.Ack (void . runAppT env . handleDelivery metrics cfg) runAppT env $ markAsWorking BackgroundJobConsumer, onChannelException = \_ -> runAppT env $ markAsNotWorking BackgroundJobConsumer, onConnectionClose = runAppT env $ markAsNotWorking BackgroundJobConsumer @@ -68,3 +89,48 @@ startWorker rabbitmqOpts = do -- nothing to close explicitly; the AMQP helper closes channel/connection on shutdown Log.info $ Log.msg (Log.val "Background job consumer cleanup") markAsNotWorking BackgroundJobConsumer + +handleDelivery :: BackgroundJobsMetrics -> BackgroundJobsConfig -> (Q.Message, Q.Envelope) -> AppT IO () +handleDelivery metrics cfg (msg, env) = do + case Aeson.eitherDecode @Job (Q.msgBody msg) of + Left err -> do + withLabel metrics.jobsInvalid "invalid" incCounter + Log.err $ Log.msg (Log.val "Invalid background job JSON") . Log.field "error" err + liftIO $ threadDelay 300000 -- avoid tight redelivery loop + liftIO $ Q.rejectEnv env True + Right job -> do + let lbl = jobTypeLabel job.jobType + when (Q.envRedelivered env) $ withLabel metrics.jobsRedelivered lbl incCounter + withLabel metrics.jobsReceived lbl incCounter + UnliftIO.bracket_ (incGauge metrics.workersBusy) (decGauge metrics.workersBusy) $ do + outcome <- runAttempts lbl job + case outcome of + Right () -> do + withLabel metrics.jobsSucceeded lbl incCounter + liftIO $ Q.ackEnv env + Left e -> do + withLabel metrics.jobsFailed lbl incCounter + Log.err $ Log.msg (Log.val "Background job failed after retries") . Log.field "error" e + liftIO $ Q.rejectEnv env False + where + runAttempts :: Text -> Job -> AppT IO (Either Text ()) + runAttempts lbl job = do + let retries = max 0 (cfg.maxAttempts - 1) + policy = limitRetries retries <> fullJitterBackoff 100000 -- 100ms base + retrying policy shouldRetry $ \_rs -> do + withLabel metrics.jobsStarted lbl incCounter + t0 <- liftIO getPOSIXTime + r <- runWithTimeout cfg.jobTimeout (dispatchJob job) + t1 <- liftIO getPOSIXTime + let dur = realToFrac (t1 - t0) :: Double + withLabel metrics.jobDuration lbl (`observe` dur) + pure r + where + shouldRetry _ (Right _) = pure False + shouldRetry _ (Left _) = pure True + +-- | Run an action with timeout (seconds); throws on expiry. +runWithTimeout :: Int -> AppT IO (Either Text ()) -> AppT IO (Either Text ()) +runWithTimeout secs action = do + m <- timeout (fromIntegral secs * 1000000) action + pure $ fromMaybe (Left "job timeout") m diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs new file mode 100644 index 0000000000..03506ba105 --- /dev/null +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs @@ -0,0 +1,18 @@ +module Wire.BackgroundWorker.Jobs.Registry + ( dispatchJob, + jobTypeLabel, + ) +where + +import Imports +import Wire.API.BackgroundJobs (Job (..), JobType (..)) +import Wire.BackgroundWorker.Env (AppT) + +jobTypeLabel :: JobType -> Text +jobTypeLabel = \case + JobNoop -> "noop" + +dispatchJob :: Job -> AppT IO (Either Text ()) +dispatchJob job = + case job.jobType of + JobNoop -> pure (Right ()) From 0638cd3a70cf2d3bd4fe3fba64ed6a45c32c17ca Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 30 Sep 2025 14:46:26 +0000 Subject: [PATCH 06/40] clean up --- libs/wire-api/src/Wire/API/BackgroundJobs.hs | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/libs/wire-api/src/Wire/API/BackgroundJobs.hs b/libs/wire-api/src/Wire/API/BackgroundJobs.hs index d18ea60cea..2e5c384161 100644 --- a/libs/wire-api/src/Wire/API/BackgroundJobs.hs +++ b/libs/wire-api/src/Wire/API/BackgroundJobs.hs @@ -17,16 +17,8 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.API.BackgroundJobs - ( Job (..), - JobType (..), - publishBackgroundJob, - backgroundJobsRoutingKey, - backgroundJobsQueueName, - ) -where +module Wire.API.BackgroundJobs where -import Control.Lens ((?~)) import Data.Aeson qualified as Aeson import Data.Id (JobId, idToText) import Data.OpenApi qualified as S @@ -65,15 +57,7 @@ instance ToSchema Job where <$> jobId .= field "id" schema <*> requestId .= field "requestId" schema <*> jobType .= field "type" schema - <*> payload .= field "payload" payloadSchema - -payloadSchema :: ValueSchema NamedSwaggerDoc Aeson.Object -payloadSchema = mkSchema sdoc Aeson.parseJSON (Just . Aeson.toJSON) - where - sdoc = - swaggerDoc @Aeson.Object - & S.schema . S.title ?~ "Payload" - & S.schema . S.description ?~ "Job payload as a JSON object" + <*> payload .= field "payload" jsonObject -- | Publish a job to the default exchange with routing key "background-jobs". -- Sets content-type to application/json and message_id to the Job.id. From c7903c76a26d754099acbc5e935b9341ed31c9d2 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Tue, 30 Sep 2025 18:12:33 +0200 Subject: [PATCH 07/40] refactor: some clean ups --- libs/wire-api/src/Wire/API/BackgroundJobs.hs | 35 ++++++++++---- .../background-worker/background-worker.cabal | 2 + .../Wire/BackgroundWorker/Jobs/Consumer.hs | 48 +++++-------------- .../Wire/BackgroundWorker/Jobs/Registry.hs | 10 ++-- 4 files changed, 47 insertions(+), 48 deletions(-) diff --git a/libs/wire-api/src/Wire/API/BackgroundJobs.hs b/libs/wire-api/src/Wire/API/BackgroundJobs.hs index 2e5c384161..6b2b0ca647 100644 --- a/libs/wire-api/src/Wire/API/BackgroundJobs.hs +++ b/libs/wire-api/src/Wire/API/BackgroundJobs.hs @@ -21,20 +21,22 @@ module Wire.API.BackgroundJobs where import Data.Aeson qualified as Aeson import Data.Id (JobId, idToText) +import Data.Map.Strict qualified as Map import Data.OpenApi qualified as S import Data.Schema import Imports import Network.AMQP qualified as Q +import Network.AMQP.Types qualified as QT import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) -data JobType +data JobPayload = JobNoop deriving stock (Eq, Ord, Show, Generic) - deriving (Arbitrary) via GenericUniform JobType + deriving (Arbitrary) via GenericUniform JobPayload -instance ToSchema JobType where +instance ToSchema JobPayload where schema = - enum @Text "JobType" $ + enum @Text "JobPayload" $ mconcat [ element "noop" JobNoop ] @@ -43,8 +45,7 @@ instance ToSchema JobType where data Job = Job { jobId :: JobId, requestId :: Text, - jobType :: JobType, - payload :: Aeson.Object + payload :: JobPayload } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via GenericUniform Job @@ -56,8 +57,7 @@ instance ToSchema Job where Job <$> jobId .= field "id" schema <*> requestId .= field "requestId" schema - <*> jobType .= field "type" schema - <*> payload .= field "payload" jsonObject + <*> payload .= field "payload" schema -- | Publish a job to the default exchange with routing key "background-jobs". -- Sets content-type to application/json and message_id to the Job.id. @@ -70,6 +70,7 @@ publishBackgroundJob chan job = do Q.msgID = Just (idToText job.jobId), Q.msgCorrelationID = Just job.requestId } + ensureBackgroundJobsQueue chan void $ Q.publishMsg chan "" backgroundJobsRoutingKey msg backgroundJobsRoutingKey :: Text @@ -77,3 +78,21 @@ backgroundJobsRoutingKey = backgroundJobsQueueName backgroundJobsQueueName :: Text backgroundJobsQueueName = "background-jobs" + +ensureBackgroundJobsQueue :: Q.Channel -> IO () +ensureBackgroundJobsQueue chan = do + let headers = + QT.FieldTable + ( Map.fromList + [ ("x-queue-type", QT.FVString "quorum") + ] + ) + q = + Q.newQueue + { Q.queueName = backgroundJobsQueueName, + Q.queueDurable = True, + Q.queueAutoDelete = False, + Q.queueExclusive = False, + Q.queueHeaders = headers + } + void $ Q.declareQueue chan q diff --git a/services/background-worker/background-worker.cabal b/services/background-worker/background-worker.cabal index c32f1fb6b5..006b09289a 100644 --- a/services/background-worker/background-worker.cabal +++ b/services/background-worker/background-worker.cabal @@ -37,8 +37,10 @@ library , bytestring-conversion , cassandra-util , containers + , data-timeout , exceptions , extended + , extra , HsOpenSSL , http-client , http2-manager diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs index 3deba18ecc..3d8308e33b 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs @@ -1,41 +1,25 @@ +{-# LANGUAGE RecordWildCards #-} + module Wire.BackgroundWorker.Jobs.Consumer (startWorker, BackgroundJobsMetrics (..)) where +import Control.Concurrent.Timeout qualified as Timeout import Control.Retry import Data.Aeson qualified as Aeson -import Data.Map.Strict qualified as Map -import Data.Time.Clock.POSIX (getPOSIXTime) +import Data.Timeout import Imports import Network.AMQP qualified as Q import Network.AMQP.Extended import Network.AMQP.Lifted qualified as QL -import Network.AMQP.Types qualified as QT import Prometheus import System.Logger.Class qualified as Log +import System.Time.Extra (duration) import UnliftIO import Wire.API.BackgroundJobs import Wire.BackgroundWorker.Env -import Wire.BackgroundWorker.Jobs.Registry (dispatchJob, jobTypeLabel) +import Wire.BackgroundWorker.Jobs.Registry (dispatchJob, jobPayloadLabel) import Wire.BackgroundWorker.Options import Wire.BackgroundWorker.Util (CleanupAction) -ensureBackgroundJobsQueue :: Q.Channel -> IO () -ensureBackgroundJobsQueue chan = do - let headers = - QT.FieldTable - ( Map.fromList - [ ("x-queue-type", QT.FVString "quorum") - ] - ) - q = - Q.newQueue - { Q.queueName = backgroundJobsQueueName, - Q.queueDurable = True, - Q.queueAutoDelete = False, - Q.queueExclusive = False, - Q.queueHeaders = headers - } - void $ Q.declareQueue chan q - data BackgroundJobsMetrics = BackgroundJobsMetrics { workersBusy :: Gauge, concurrencyConfigured :: Gauge, @@ -59,7 +43,7 @@ mkMetrics = do jobsInvalid <- register (vector "job_type" $ counter $ Info "wire_background_jobs_invalid_total" "Invalid jobs received") jobsRedelivered <- register (vector "job_type" $ counter $ Info "wire_background_jobs_redelivered_total" "Jobs marked redelivered by broker") jobDuration <- register (vector "job_type" $ histogram (Info "wire_background_jobs_duration_seconds" "Job duration seconds") defaultBuckets) - pure BackgroundJobsMetrics {workersBusy, concurrencyConfigured, jobsReceived, jobsStarted, jobsSucceeded, jobsFailed, jobsInvalid, jobsRedelivered, jobDuration} + pure BackgroundJobsMetrics {..} startWorker :: AmqpEndpoint -> AppT IO CleanupAction startWorker rabbitmqOpts = do @@ -96,10 +80,10 @@ handleDelivery metrics cfg (msg, env) = do Left err -> do withLabel metrics.jobsInvalid "invalid" incCounter Log.err $ Log.msg (Log.val "Invalid background job JSON") . Log.field "error" err - liftIO $ threadDelay 300000 -- avoid tight redelivery loop + Timeout.threadDelay (3 # Second) -- avoid tight redelivery loop liftIO $ Q.rejectEnv env True Right job -> do - let lbl = jobTypeLabel job.jobType + let lbl = jobPayloadLabel job.payload when (Q.envRedelivered env) $ withLabel metrics.jobsRedelivered lbl incCounter withLabel metrics.jobsReceived lbl incCounter UnliftIO.bracket_ (incGauge metrics.workersBusy) (decGauge metrics.workersBusy) $ do @@ -119,18 +103,12 @@ handleDelivery metrics cfg (msg, env) = do policy = limitRetries retries <> fullJitterBackoff 100000 -- 100ms base retrying policy shouldRetry $ \_rs -> do withLabel metrics.jobsStarted lbl incCounter - t0 <- liftIO getPOSIXTime - r <- runWithTimeout cfg.jobTimeout (dispatchJob job) - t1 <- liftIO getPOSIXTime - let dur = realToFrac (t1 - t0) :: Double + (dur, r) <- + duration $ + fromMaybe (Left "job timeout") + <$> timeout (fromIntegral $ fromIntegral cfg.jobTimeout #> Second) (dispatchJob job) withLabel metrics.jobDuration lbl (`observe` dur) pure r where shouldRetry _ (Right _) = pure False shouldRetry _ (Left _) = pure True - --- | Run an action with timeout (seconds); throws on expiry. -runWithTimeout :: Int -> AppT IO (Either Text ()) -> AppT IO (Either Text ()) -runWithTimeout secs action = do - m <- timeout (fromIntegral secs * 1000000) action - pure $ fromMaybe (Left "job timeout") m diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs index 03506ba105..4428b77dab 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs @@ -1,18 +1,18 @@ module Wire.BackgroundWorker.Jobs.Registry ( dispatchJob, - jobTypeLabel, + jobPayloadLabel, ) where import Imports -import Wire.API.BackgroundJobs (Job (..), JobType (..)) +import Wire.API.BackgroundJobs (Job (..), JobPayload (..)) import Wire.BackgroundWorker.Env (AppT) -jobTypeLabel :: JobType -> Text -jobTypeLabel = \case +jobPayloadLabel :: JobPayload -> Text +jobPayloadLabel = \case JobNoop -> "noop" dispatchJob :: Job -> AppT IO (Either Text ()) dispatchJob job = - case job.jobType of + case job.payload of JobNoop -> pure (Right ()) From 0954e7f513bb8990aac62115a4a4c779c3bbe570 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Wed, 1 Oct 2025 12:16:04 +0200 Subject: [PATCH 08/40] feat: add jobs defiinitions, fix schema and validate settings --- libs/wire-api/src/Wire/API/BackgroundJobs.hs | 97 ++++++++++++++++++- .../Wire/BackgroundWorker/Jobs/Consumer.hs | 9 +- .../Wire/BackgroundWorker/Jobs/Registry.hs | 4 + .../src/Wire/BackgroundWorker/Options.hs | 17 ++-- 4 files changed, 109 insertions(+), 18 deletions(-) diff --git a/libs/wire-api/src/Wire/API/BackgroundJobs.hs b/libs/wire-api/src/Wire/API/BackgroundJobs.hs index 6b2b0ca647..c1222e6e3d 100644 --- a/libs/wire-api/src/Wire/API/BackgroundJobs.hs +++ b/libs/wire-api/src/Wire/API/BackgroundJobs.hs @@ -1,4 +1,5 @@ {-# LANGUAGE StrictData #-} +{-# LANGUAGE TemplateHaskell #-} -- This file is part of the Wire Server implementation. -- @@ -19,8 +20,11 @@ module Wire.API.BackgroundJobs where +import Control.Arrow ((&&&)) +import Control.Lens (makePrisms) import Data.Aeson qualified as Aeson -import Data.Id (JobId, idToText) +import Data.Default (Default, def) +import Data.Id import Data.Map.Strict qualified as Map import Data.OpenApi qualified as S import Data.Schema @@ -29,18 +33,101 @@ import Network.AMQP qualified as Q import Network.AMQP.Types qualified as QT import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) +-- * Jobs definition + data JobPayload = JobNoop - deriving stock (Eq, Ord, Show, Generic) + | JobSyncUserGroupAndChannel SyncUserGroupAndChannel + | JobSyncUserGroup SyncUserGroup + deriving stock (Eq, Show, Generic) deriving (Arbitrary) via GenericUniform JobPayload -instance ToSchema JobPayload where +instance Default JobPayload where + def = JobNoop + +data JobPayloadTag + = JobNoopTag + | JobSyncUserGroupAndChannelTag + | JobSyncUserGroupTag + deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) + deriving (Arbitrary) via GenericUniform JobPayloadTag + +instance ToSchema JobPayloadTag where schema = - enum @Text "JobPayload" $ + enum @Text "JobPayloadTag" $ mconcat - [ element "noop" JobNoop + [ element "noop" JobNoopTag, + element "sync-user-group-and-channel" JobSyncUserGroupAndChannelTag, + element "sync-user-group" JobSyncUserGroupTag ] +jobPayloadTag :: JobPayload -> JobPayloadTag +jobPayloadTag = + \case + JobNoop -> JobNoopTag + JobSyncUserGroupAndChannel {} -> JobSyncUserGroupAndChannelTag + JobSyncUserGroup {} -> JobSyncUserGroupTag + +jobPayloadTagSchema :: ObjectSchema SwaggerDoc JobPayloadTag +jobPayloadTagSchema = field "type" schema + +data SyncUserGroupAndChannel = SyncUserGroupAndChannel + { userGroupId :: UserGroupId, + convId :: ConvId, + actor :: UserId + } + deriving (Show, Eq, Generic) + deriving (Aeson.ToJSON, Aeson.FromJSON) via (Schema SyncUserGroupAndChannel) + deriving (Arbitrary) via GenericUniform SyncUserGroupAndChannel + +instance ToSchema SyncUserGroupAndChannel where + schema = + object "SyncUserGroupAndChannel" $ + SyncUserGroupAndChannel + <$> (.userGroupId) .= field "userGroupId" schema + <*> (.convId) .= field "convId" schema + <*> (.actor) .= field "actor" schema + +data SyncUserGroup = SyncUserGroup + { userGroupId :: UserGroupId, + actor :: UserId + } + deriving (Show, Eq, Generic) + deriving (Aeson.ToJSON, Aeson.FromJSON) via (Schema SyncUserGroup) + deriving (Arbitrary) via GenericUniform SyncUserGroup + +instance ToSchema SyncUserGroup where + schema = + object "SyncUserGroup" $ + SyncUserGroup + <$> (.userGroupId) .= field "userGroupId" schema + <*> (.actor) .= field "actor" schema + +makePrisms ''JobPayload + +jobPayloadObjectSchema :: ObjectSchema SwaggerDoc JobPayload +jobPayloadObjectSchema = + snd + <$> (jobPayloadTag &&& id) + .= bind + (fst .= jobPayloadTagSchema) + (snd .= dispatch jobPayloadDataSchema) + where + jobPayloadDataSchema :: JobPayloadTag -> ObjectSchema SwaggerDoc JobPayload + jobPayloadDataSchema = \case + JobNoopTag -> tag _JobNoop (pure ()) + JobSyncUserGroupAndChannelTag -> tag _JobSyncUserGroupAndChannel (field "payload" schema) + JobSyncUserGroupTag -> tag _JobSyncUserGroup (field "payload" schema) + +instance ToSchema JobPayload where + schema = object "JobPayload" jobPayloadObjectSchema + +deriving via (Schema JobPayload) instance Aeson.FromJSON JobPayload + +deriving via (Schema JobPayload) instance Aeson.ToJSON JobPayload + +deriving via (Schema JobPayload) instance S.ToSchema JobPayload + -- | Background job envelope. Payload is a free-form JSON object. data Job = Job { jobId :: JobId, diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs index 3d8308e33b..b4208b6105 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs @@ -5,6 +5,7 @@ module Wire.BackgroundWorker.Jobs.Consumer (startWorker, BackgroundJobsMetrics ( import Control.Concurrent.Timeout qualified as Timeout import Control.Retry import Data.Aeson qualified as Aeson +import Data.Range (Range (fromRange)) import Data.Timeout import Imports import Network.AMQP qualified as Q @@ -57,9 +58,9 @@ startWorker rabbitmqOpts = do { onNewChannel = \chan -> do -- declare queue and set prefetch to concurrency ensureBackgroundJobsQueue chan - Q.qos chan 0 (fromIntegral cfg.concurrency) False + Q.qos chan 0 (fromIntegral $ fromRange cfg.concurrency) False -- set gauges - setGauge metrics.concurrencyConfigured (fromIntegral cfg.concurrency) + setGauge metrics.concurrencyConfigured (fromIntegral $ fromRange cfg.concurrency) -- start consuming with manual ack void $ QL.consumeMsgs chan backgroundJobsQueueName Q.Ack (void . runAppT env . handleDelivery metrics cfg) runAppT env $ markAsWorking BackgroundJobConsumer, @@ -99,14 +100,14 @@ handleDelivery metrics cfg (msg, env) = do where runAttempts :: Text -> Job -> AppT IO (Either Text ()) runAttempts lbl job = do - let retries = max 0 (cfg.maxAttempts - 1) + let retries = max 0 (fromRange cfg.maxAttempts - 1) policy = limitRetries retries <> fullJitterBackoff 100000 -- 100ms base retrying policy shouldRetry $ \_rs -> do withLabel metrics.jobsStarted lbl incCounter (dur, r) <- duration $ fromMaybe (Left "job timeout") - <$> timeout (fromIntegral $ fromIntegral cfg.jobTimeout #> Second) (dispatchJob job) + <$> timeout (fromIntegral $ fromIntegral (fromRange cfg.jobTimeout) #> Second) (dispatchJob job) withLabel metrics.jobDuration lbl (`observe` dur) pure r where diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs index 4428b77dab..afab2dbfe3 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs @@ -11,8 +11,12 @@ import Wire.BackgroundWorker.Env (AppT) jobPayloadLabel :: JobPayload -> Text jobPayloadLabel = \case JobNoop -> "noop" + JobSyncUserGroupAndChannel {} -> "sync-user-group-and-channel" + JobSyncUserGroup {} -> "sync-user-group" dispatchJob :: Job -> AppT IO (Either Text ()) dispatchJob job = case job.payload of JobNoop -> pure (Right ()) + JobSyncUserGroupAndChannel {} -> pure $ Left "TODO: to be implemented" + JobSyncUserGroup {} -> pure $ Left "TODO: to be implemented" diff --git a/services/background-worker/src/Wire/BackgroundWorker/Options.hs b/services/background-worker/src/Wire/BackgroundWorker/Options.hs index b8af0e966d..2bd8a00d52 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Options.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Options.hs @@ -1,6 +1,8 @@ module Wire.BackgroundWorker.Options where import Data.Aeson +import Data.Range (Range) +import GHC.Generics import Imports import Network.AMQP.Extended import System.Logger.Extended @@ -19,8 +21,7 @@ data Opts = Opts backgroundJobs :: BackgroundJobsConfig } deriving (Show, Generic) - -instance FromJSON Opts + deriving (FromJSON) via Generically Opts data BackendNotificationsConfig = BackendNotificationsConfig { -- | Minimum amount of time (in microseconds) to wait before doing the first @@ -37,8 +38,7 @@ data BackendNotificationsConfig = BackendNotificationsConfig remotesRefreshInterval :: Int } deriving (Show, Generic) - -instance FromJSON BackendNotificationsConfig + deriving (FromJSON) via Generically BackendNotificationsConfig newtype RabbitMqOpts = RabbitMqOpts {unRabbitMqOpts :: Either AmqpEndpoint RabbitMqAdminOpts} deriving (Show) @@ -52,12 +52,11 @@ instance FromJSON RabbitMqOpts where data BackgroundJobsConfig = BackgroundJobsConfig { -- | Maximum parallel jobs processed by this process - concurrency :: Int, + concurrency :: Range 1 1000 Int, -- | Per-attempt timeout (seconds) - jobTimeout :: Int, + jobTimeout :: Range 1 1000 Int, -- | Total attempts including first run - maxAttempts :: Int + maxAttempts :: Range 1 1000 Int } deriving (Show, Generic) - -instance FromJSON BackgroundJobsConfig + deriving (FromJSON) via Generically BackgroundJobsConfig From 42f8567386f90bb916b10c1a9d8f18b606450db2 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Wed, 1 Oct 2025 16:39:03 +0200 Subject: [PATCH 09/40] feat: add Effect and Interpreters --- libs/types-common/src/Data/Id.hs | 1 + libs/wire-api/src/Wire/API/BackgroundJobs.hs | 5 +- .../src/Wire/BackgroundJobsSubsystem.hs | 31 +++++++++++ .../BackgroundJobsSubsystem/Interpreter.hs | 55 +++++++++++++++++++ .../src/Wire/BackgroundJobsSubsystem/Null.hs | 27 +++++++++ libs/wire-subsystems/wire-subsystems.cabal | 3 + 6 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem.hs create mode 100644 libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Interpreter.hs create mode 100644 libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Null.hs diff --git a/libs/types-common/src/Data/Id.hs b/libs/types-common/src/Data/Id.hs index de8f980093..09737080ca 100644 --- a/libs/types-common/src/Data/Id.hs +++ b/libs/types-common/src/Data/Id.hs @@ -440,6 +440,7 @@ newtype RequestId = RequestId Generic, ToBytes ) + deriving newtype (Arbitrary) defRequestId :: (IsString s) => s defRequestId = "N/A" diff --git a/libs/wire-api/src/Wire/API/BackgroundJobs.hs b/libs/wire-api/src/Wire/API/BackgroundJobs.hs index c1222e6e3d..631f9c8a6a 100644 --- a/libs/wire-api/src/Wire/API/BackgroundJobs.hs +++ b/libs/wire-api/src/Wire/API/BackgroundJobs.hs @@ -28,6 +28,7 @@ import Data.Id import Data.Map.Strict qualified as Map import Data.OpenApi qualified as S import Data.Schema +import Data.Text.Encoding qualified as T import Imports import Network.AMQP qualified as Q import Network.AMQP.Types qualified as QT @@ -131,7 +132,7 @@ deriving via (Schema JobPayload) instance S.ToSchema JobPayload -- | Background job envelope. Payload is a free-form JSON object. data Job = Job { jobId :: JobId, - requestId :: Text, + requestId :: RequestId, payload :: JobPayload } deriving stock (Eq, Show, Generic) @@ -155,7 +156,7 @@ publishBackgroundJob chan job = do { Q.msgBody = Aeson.encode job, Q.msgContentType = Just "application/json", Q.msgID = Just (idToText job.jobId), - Q.msgCorrelationID = Just job.requestId + Q.msgCorrelationID = Just $ T.decodeUtf8 job.requestId.unRequestId } ensureBackgroundJobsQueue chan void $ Q.publishMsg chan "" backgroundJobsRoutingKey msg diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem.hs new file mode 100644 index 0000000000..7995b85209 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem.hs @@ -0,0 +1,31 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . +{-# LANGUAGE TemplateHaskell #-} + +module Wire.BackgroundJobsSubsystem where + +import Data.Id +import Polysemy +import Wire.API.BackgroundJobs + +data BackgroundJobsSubsystem m a where + PublishJob :: + JobId -> + JobPayload -> + BackgroundJobsSubsystem m () + +makeSem ''BackgroundJobsSubsystem diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Interpreter.hs new file mode 100644 index 0000000000..b950075399 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Interpreter.hs @@ -0,0 +1,55 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . +module Wire.BackgroundJobsSubsystem.Interpreter where + +import Data.Id +import Imports +import Network.AMQP qualified as Q +import Polysemy +import Polysemy.Input +import Wire.API.BackgroundJobs +import Wire.BackgroundJobsSubsystem (BackgroundJobsSubsystem (..)) + +interpretBackgroundJobsRabbitMQ :: + ( Member (Embed IO) r, + Member (Input RequestId) r, + Member (Input Q.Channel) r + ) => + InterpreterFor BackgroundJobsSubsystem r +interpretBackgroundJobsRabbitMQ = + interpret $ + \case + PublishJob jobId jobPayload -> publishJob jobId jobPayload + +publishJob :: + ( Member (Embed IO) r, + Member (Input RequestId) r, + Member (Input Q.Channel) r + ) => + JobId -> + JobPayload -> + Sem r () +publishJob jobId jobPayload = do + requestId <- input + channel <- input + let job = + Job + { payload = jobPayload, + jobId = jobId, + requestId = requestId + } + liftIO $ publishBackgroundJob channel job diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Null.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Null.hs new file mode 100644 index 0000000000..eb21e62607 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Null.hs @@ -0,0 +1,27 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . +module Wire.BackgroundJobsSubsystem.Null where + +import Imports +import Polysemy +import Wire.BackgroundJobsSubsystem (BackgroundJobsSubsystem (..)) + +interpretBackgroundJobsNoConfig :: InterpreterFor BackgroundJobsSubsystem r +interpretBackgroundJobsNoConfig = + interpret $ + \case + PublishJob {} -> pure () diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index ff514fc848..2f7aab178c 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -182,6 +182,9 @@ library Wire.AuthenticationSubsystem.Interpreter Wire.AuthenticationSubsystem.ZAuth Wire.AWS + Wire.BackgroundJobsSubsystem + Wire.BackgroundJobsSubsystem.Interpreter + Wire.BackgroundJobsSubsystem.Null Wire.BlockListStore Wire.BlockListStore.Cassandra Wire.BrigAPIAccess From 9a54bf087800ca1eb467537114a8609f0df5990b Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 2 Oct 2025 06:44:30 +0000 Subject: [PATCH 10/40] update cabal and nix dependencies --- services/background-worker/background-worker.cabal | 1 - services/background-worker/default.nix | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/services/background-worker/background-worker.cabal b/services/background-worker/background-worker.cabal index 006b09289a..e381ca2a7f 100644 --- a/services/background-worker/background-worker.cabal +++ b/services/background-worker/background-worker.cabal @@ -52,7 +52,6 @@ library , servant-client , servant-server , text - , time , tinylog , transformers , transformers-base diff --git a/services/background-worker/default.nix b/services/background-worker/default.nix index 7ef4b6ab45..d9f918acec 100644 --- a/services/background-worker/default.nix +++ b/services/background-worker/default.nix @@ -11,8 +11,10 @@ , cassandra-util , containers , data-default +, data-timeout , exceptions , extended +, extra , federator , gitignoreSource , HsOpenSSL @@ -57,8 +59,10 @@ mkDerivation { bytestring-conversion cassandra-util containers + data-timeout exceptions extended + extra HsOpenSSL http-client http2-manager From f315a2a97cd0c17ee847633f36be6848850f0773 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 2 Oct 2025 07:00:41 +0000 Subject: [PATCH 11/40] moved and removed some code --- libs/wire-api/src/Wire/API/BackgroundJobs.hs | 20 +++++++------------ .../Wire/BackgroundWorker/Jobs/Consumer.hs | 2 +- .../Wire/BackgroundWorker/Jobs/Registry.hs | 8 -------- 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/libs/wire-api/src/Wire/API/BackgroundJobs.hs b/libs/wire-api/src/Wire/API/BackgroundJobs.hs index 631f9c8a6a..336dfa5951 100644 --- a/libs/wire-api/src/Wire/API/BackgroundJobs.hs +++ b/libs/wire-api/src/Wire/API/BackgroundJobs.hs @@ -23,7 +23,6 @@ module Wire.API.BackgroundJobs where import Control.Arrow ((&&&)) import Control.Lens (makePrisms) import Data.Aeson qualified as Aeson -import Data.Default (Default, def) import Data.Id import Data.Map.Strict qualified as Map import Data.OpenApi qualified as S @@ -34,21 +33,19 @@ import Network.AMQP qualified as Q import Network.AMQP.Types qualified as QT import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) --- * Jobs definition - data JobPayload - = JobNoop - | JobSyncUserGroupAndChannel SyncUserGroupAndChannel + = JobSyncUserGroupAndChannel SyncUserGroupAndChannel | JobSyncUserGroup SyncUserGroup deriving stock (Eq, Show, Generic) deriving (Arbitrary) via GenericUniform JobPayload -instance Default JobPayload where - def = JobNoop +jobPayloadLabel :: JobPayload -> Text +jobPayloadLabel p = case jobPayloadTag p of + JobSyncUserGroupAndChannelTag -> "sync-user-group-and-channel" + JobSyncUserGroupTag -> "sync-user-group" data JobPayloadTag - = JobNoopTag - | JobSyncUserGroupAndChannelTag + = JobSyncUserGroupAndChannelTag | JobSyncUserGroupTag deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) deriving (Arbitrary) via GenericUniform JobPayloadTag @@ -57,15 +54,13 @@ instance ToSchema JobPayloadTag where schema = enum @Text "JobPayloadTag" $ mconcat - [ element "noop" JobNoopTag, - element "sync-user-group-and-channel" JobSyncUserGroupAndChannelTag, + [ element "sync-user-group-and-channel" JobSyncUserGroupAndChannelTag, element "sync-user-group" JobSyncUserGroupTag ] jobPayloadTag :: JobPayload -> JobPayloadTag jobPayloadTag = \case - JobNoop -> JobNoopTag JobSyncUserGroupAndChannel {} -> JobSyncUserGroupAndChannelTag JobSyncUserGroup {} -> JobSyncUserGroupTag @@ -116,7 +111,6 @@ jobPayloadObjectSchema = where jobPayloadDataSchema :: JobPayloadTag -> ObjectSchema SwaggerDoc JobPayload jobPayloadDataSchema = \case - JobNoopTag -> tag _JobNoop (pure ()) JobSyncUserGroupAndChannelTag -> tag _JobSyncUserGroupAndChannel (field "payload" schema) JobSyncUserGroupTag -> tag _JobSyncUserGroup (field "payload" schema) diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs index b4208b6105..c4860d6e18 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs @@ -17,7 +17,7 @@ import System.Time.Extra (duration) import UnliftIO import Wire.API.BackgroundJobs import Wire.BackgroundWorker.Env -import Wire.BackgroundWorker.Jobs.Registry (dispatchJob, jobPayloadLabel) +import Wire.BackgroundWorker.Jobs.Registry import Wire.BackgroundWorker.Options import Wire.BackgroundWorker.Util (CleanupAction) diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs index afab2dbfe3..436f28025e 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs @@ -1,6 +1,5 @@ module Wire.BackgroundWorker.Jobs.Registry ( dispatchJob, - jobPayloadLabel, ) where @@ -8,15 +7,8 @@ import Imports import Wire.API.BackgroundJobs (Job (..), JobPayload (..)) import Wire.BackgroundWorker.Env (AppT) -jobPayloadLabel :: JobPayload -> Text -jobPayloadLabel = \case - JobNoop -> "noop" - JobSyncUserGroupAndChannel {} -> "sync-user-group-and-channel" - JobSyncUserGroup {} -> "sync-user-group" - dispatchJob :: Job -> AppT IO (Either Text ()) dispatchJob job = case job.payload of - JobNoop -> pure (Right ()) JobSyncUserGroupAndChannel {} -> pure $ Left "TODO: to be implemented" JobSyncUserGroup {} -> pure $ Left "TODO: to be implemented" From 66e322f762f70ff695752832bf892c8a6bc0ae02 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 2 Oct 2025 11:05:18 +0000 Subject: [PATCH 12/40] background job runner --- libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem.hs | 8 ++++---- .../src/Wire/BackgroundJobsSubsystem/Interpreter.hs | 4 ++++ .../src/Wire/BackgroundJobsSubsystem/Null.hs | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem.hs index 7995b85209..e3ba3d9c54 100644 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem.hs @@ -19,13 +19,13 @@ module Wire.BackgroundJobsSubsystem where import Data.Id +import Data.Text +import Imports import Polysemy import Wire.API.BackgroundJobs data BackgroundJobsSubsystem m a where - PublishJob :: - JobId -> - JobPayload -> - BackgroundJobsSubsystem m () + PublishJob :: JobId -> JobPayload -> BackgroundJobsSubsystem m () + RunJob :: Job -> BackgroundJobsSubsystem m (Either Text ()) makeSem ''BackgroundJobsSubsystem diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Interpreter.hs index b950075399..1815930a12 100644 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Interpreter.hs @@ -34,6 +34,10 @@ interpretBackgroundJobsRabbitMQ = interpret $ \case PublishJob jobId jobPayload -> publishJob jobId jobPayload + RunJob job -> runJob job + +runJob :: Job -> Sem r (Either Text ()) +runJob _ = pure $ Left "TODO: to be implemented" publishJob :: ( Member (Embed IO) r, diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Null.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Null.hs index eb21e62607..b6f15185c4 100644 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Null.hs +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Null.hs @@ -25,3 +25,4 @@ interpretBackgroundJobsNoConfig = interpret $ \case PublishJob {} -> pure () + RunJob _ -> pure $ pure () From 9c0d7a97aebca5a8e8999d9f2e918b326c6d9c6c Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 2 Oct 2025 11:11:58 +0000 Subject: [PATCH 13/40] split job publisher and runner --- .../src/Wire/BackgroundJobsPublisher.hs | 13 ++++ .../BackgroundJobsPublisher/Interpreter.hs | 38 ++++++++++++ .../src/Wire/BackgroundJobsPublisher/Null.hs | 9 +++ .../src/Wire/BackgroundJobsRunner.hs | 12 ++++ .../Wire/BackgroundJobsRunner/Interpreter.hs | 10 ++++ .../src/Wire/BackgroundJobsSubsystem.hs | 31 ---------- .../BackgroundJobsSubsystem/Interpreter.hs | 59 ------------------- .../src/Wire/BackgroundJobsSubsystem/Null.hs | 28 --------- libs/wire-subsystems/wire-subsystems.cabal | 8 ++- 9 files changed, 87 insertions(+), 121 deletions(-) create mode 100644 libs/wire-subsystems/src/Wire/BackgroundJobsPublisher.hs create mode 100644 libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/Interpreter.hs create mode 100644 libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/Null.hs create mode 100644 libs/wire-subsystems/src/Wire/BackgroundJobsRunner.hs create mode 100644 libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs delete mode 100644 libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem.hs delete mode 100644 libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Interpreter.hs delete mode 100644 libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Null.hs diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher.hs new file mode 100644 index 0000000000..189f4a555c --- /dev/null +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher.hs @@ -0,0 +1,13 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.BackgroundJobsPublisher where + +import Data.Id +import Imports +import Polysemy +import Wire.API.BackgroundJobs (JobPayload) + +data BackgroundJobsPublisher m a where + PublishJob :: JobId -> JobPayload -> BackgroundJobsPublisher m () + +makeSem ''BackgroundJobsPublisher diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/Interpreter.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/Interpreter.hs new file mode 100644 index 0000000000..9f00ed66e9 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/Interpreter.hs @@ -0,0 +1,38 @@ +module Wire.BackgroundJobsPublisher.Interpreter where + +import Data.Id (JobId, RequestId) +import Imports +import Network.AMQP qualified as Q +import Polysemy +import Polysemy.Input +import Wire.API.BackgroundJobs +import Wire.BackgroundJobsPublisher (BackgroundJobsPublisher (..)) + +interpretBackgroundJobsPublisherRabbitMQ :: + ( Member (Embed IO) r, + Member (Input RequestId) r, + Member (Input Q.Channel) r + ) => + InterpreterFor BackgroundJobsPublisher r +interpretBackgroundJobsPublisherRabbitMQ = + interpret $ \case + PublishJob jobId jobPayload -> publishJob jobId jobPayload + +publishJob :: + ( Member (Embed IO) r, + Member (Input RequestId) r, + Member (Input Q.Channel) r + ) => + JobId -> + JobPayload -> + Sem r () +publishJob jobId jobPayload = do + requestId <- input + channel <- input + let job = + Job + { payload = jobPayload, + jobId = jobId, + requestId = requestId + } + liftIO $ publishBackgroundJob channel job diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/Null.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/Null.hs new file mode 100644 index 0000000000..87cc32e530 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/Null.hs @@ -0,0 +1,9 @@ +module Wire.BackgroundJobsPublisher.Null where + +import Imports +import Polysemy +import Wire.BackgroundJobsPublisher (BackgroundJobsPublisher (..)) + +interpretBackgroundJobsPublisherNoConfig :: InterpreterFor BackgroundJobsPublisher r +interpretBackgroundJobsPublisherNoConfig = interpret $ \case + PublishJob {} -> pure () diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner.hs new file mode 100644 index 0000000000..ba814f954a --- /dev/null +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner.hs @@ -0,0 +1,12 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.BackgroundJobsRunner where + +import Imports +import Polysemy +import Wire.API.BackgroundJobs (Job) + +data BackgroundJobsRunner m a where + RunJob :: Job -> BackgroundJobsRunner m (Either Text ()) + +makeSem ''BackgroundJobsRunner diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs new file mode 100644 index 0000000000..e0b5479a1e --- /dev/null +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs @@ -0,0 +1,10 @@ +module Wire.BackgroundJobsRunner.Interpreter where + +import Imports +import Polysemy +import Wire.API.BackgroundJobs (Job) +import Wire.BackgroundJobsRunner (BackgroundJobsRunner (..)) + +interpretBackgroundJobsRunner :: InterpreterFor BackgroundJobsRunner r +interpretBackgroundJobsRunner = interpret $ \case + RunJob _ -> pure (Left "TODO: implement BackgroundJobsRunner.runJob") diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem.hs deleted file mode 100644 index e3ba3d9c54..0000000000 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem.hs +++ /dev/null @@ -1,31 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2025 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . -{-# LANGUAGE TemplateHaskell #-} - -module Wire.BackgroundJobsSubsystem where - -import Data.Id -import Data.Text -import Imports -import Polysemy -import Wire.API.BackgroundJobs - -data BackgroundJobsSubsystem m a where - PublishJob :: JobId -> JobPayload -> BackgroundJobsSubsystem m () - RunJob :: Job -> BackgroundJobsSubsystem m (Either Text ()) - -makeSem ''BackgroundJobsSubsystem diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Interpreter.hs deleted file mode 100644 index 1815930a12..0000000000 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Interpreter.hs +++ /dev/null @@ -1,59 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2025 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . -module Wire.BackgroundJobsSubsystem.Interpreter where - -import Data.Id -import Imports -import Network.AMQP qualified as Q -import Polysemy -import Polysemy.Input -import Wire.API.BackgroundJobs -import Wire.BackgroundJobsSubsystem (BackgroundJobsSubsystem (..)) - -interpretBackgroundJobsRabbitMQ :: - ( Member (Embed IO) r, - Member (Input RequestId) r, - Member (Input Q.Channel) r - ) => - InterpreterFor BackgroundJobsSubsystem r -interpretBackgroundJobsRabbitMQ = - interpret $ - \case - PublishJob jobId jobPayload -> publishJob jobId jobPayload - RunJob job -> runJob job - -runJob :: Job -> Sem r (Either Text ()) -runJob _ = pure $ Left "TODO: to be implemented" - -publishJob :: - ( Member (Embed IO) r, - Member (Input RequestId) r, - Member (Input Q.Channel) r - ) => - JobId -> - JobPayload -> - Sem r () -publishJob jobId jobPayload = do - requestId <- input - channel <- input - let job = - Job - { payload = jobPayload, - jobId = jobId, - requestId = requestId - } - liftIO $ publishBackgroundJob channel job diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Null.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Null.hs deleted file mode 100644 index b6f15185c4..0000000000 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsSubsystem/Null.hs +++ /dev/null @@ -1,28 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2025 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . -module Wire.BackgroundJobsSubsystem.Null where - -import Imports -import Polysemy -import Wire.BackgroundJobsSubsystem (BackgroundJobsSubsystem (..)) - -interpretBackgroundJobsNoConfig :: InterpreterFor BackgroundJobsSubsystem r -interpretBackgroundJobsNoConfig = - interpret $ - \case - PublishJob {} -> pure () - RunJob _ -> pure $ pure () diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 2f7aab178c..977c4f9657 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -182,9 +182,11 @@ library Wire.AuthenticationSubsystem.Interpreter Wire.AuthenticationSubsystem.ZAuth Wire.AWS - Wire.BackgroundJobsSubsystem - Wire.BackgroundJobsSubsystem.Interpreter - Wire.BackgroundJobsSubsystem.Null + Wire.BackgroundJobsPublisher + Wire.BackgroundJobsPublisher.Interpreter + Wire.BackgroundJobsPublisher.Null + Wire.BackgroundJobsRunner + Wire.BackgroundJobsRunner.Interpreter Wire.BlockListStore Wire.BlockListStore.Cassandra Wire.BrigAPIAccess From aa1962660145293da48a2c688b21a9eb1db3607e Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 2 Oct 2025 11:20:09 +0000 Subject: [PATCH 14/40] interpret job runner from background worker --- services/background-worker/background-worker.cabal | 2 ++ services/background-worker/default.nix | 4 ++++ .../src/Wire/BackgroundWorker/Jobs/Registry.hs | 12 +++++++----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/services/background-worker/background-worker.cabal b/services/background-worker/background-worker.cabal index e381ca2a7f..031f19a8ea 100644 --- a/services/background-worker/background-worker.cabal +++ b/services/background-worker/background-worker.cabal @@ -47,6 +47,7 @@ library , imports , metrics-wai , monad-control + , polysemy , prometheus-client , retry , servant-client @@ -60,6 +61,7 @@ library , wai-utilities , wire-api , wire-api-federation + , wire-subsystems default-extensions: AllowAmbiguousTypes diff --git a/services/background-worker/default.nix b/services/background-worker/default.nix index d9f918acec..c713987b6a 100644 --- a/services/background-worker/default.nix +++ b/services/background-worker/default.nix @@ -27,6 +27,7 @@ , lib , metrics-wai , monad-control +, polysemy , prometheus-client , QuickCheck , retry @@ -44,6 +45,7 @@ , wai-utilities , wire-api , wire-api-federation +, wire-subsystems }: mkDerivation { pname = "background-worker"; @@ -69,6 +71,7 @@ mkDerivation { imports metrics-wai monad-control + polysemy prometheus-client retry servant-client @@ -82,6 +85,7 @@ mkDerivation { wai-utilities wire-api wire-api-federation + wire-subsystems ]; executableHaskellDepends = [ HsOpenSSL imports types-common ]; testHaskellDepends = [ diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs index 436f28025e..aa3a29b9a0 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs @@ -4,11 +4,13 @@ module Wire.BackgroundWorker.Jobs.Registry where import Imports -import Wire.API.BackgroundJobs (Job (..), JobPayload (..)) +import Polysemy (run) +import Wire.API.BackgroundJobs (Job (..)) +import Wire.BackgroundJobsRunner (runJob) +import Wire.BackgroundJobsRunner.Interpreter (interpretBackgroundJobsRunner) import Wire.BackgroundWorker.Env (AppT) dispatchJob :: Job -> AppT IO (Either Text ()) -dispatchJob job = - case job.payload of - JobSyncUserGroupAndChannel {} -> pure $ Left "TODO: to be implemented" - JobSyncUserGroup {} -> pure $ Left "TODO: to be implemented" +dispatchJob job = do + let res = run $ interpretBackgroundJobsRunner (runJob job) + pure res From e53e1527bd0a78fdf2fbcf9286c4665ea2b413aa Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 2 Oct 2025 11:37:13 +0000 Subject: [PATCH 15/40] config maps, helm charts, default values, docs --- .../templates/configmap.yaml | 4 ++++ charts/background-worker/values.yaml | 9 ++++++++ charts/integration/templates/configmap.yaml | 5 +++++ .../src/developer/reference/config-options.md | 21 +++++++++++++++++++ hack/helm_vars/wire-server/values.yaml.gotmpl | 4 ++++ .../background-worker.integration.yaml | 6 ++++++ .../src/Wire/BackgroundWorker/Env.hs | 1 - 7 files changed, 49 insertions(+), 1 deletion(-) diff --git a/charts/background-worker/templates/configmap.yaml b/charts/background-worker/templates/configmap.yaml index 1a0e37e609..e1a5104cd8 100644 --- a/charts/background-worker/templates/configmap.yaml +++ b/charts/background-worker/templates/configmap.yaml @@ -48,4 +48,8 @@ data: backendNotificationPusher: {{toYaml .backendNotificationPusher | indent 6 }} + {{- with .backgroundJobs }} + backgroundJobs: +{{ toYaml . | indent 6 }} + {{- end }} {{- end }} diff --git a/charts/background-worker/values.yaml b/charts/background-worker/values.yaml index a0117c9363..48dae0ca3b 100644 --- a/charts/background-worker/values.yaml +++ b/charts/background-worker/values.yaml @@ -37,6 +37,15 @@ config: pushBackoffMaxWait: 300000000 # microseconds, so 300s remotesRefreshInterval: 300000000 # microseconds, so 300s + # Background jobs consumer configuration + backgroundJobs: + # Maximum number of in-flight jobs per process + concurrency: 8 + # Per-attempt timeout in seconds + jobTimeout: 60 + # Total attempts, including the first try + maxAttempts: 3 + secrets: {} podSecurityContext: diff --git a/charts/integration/templates/configmap.yaml b/charts/integration/templates/configmap.yaml index 84aa623acb..5402a048f2 100644 --- a/charts/integration/templates/configmap.yaml +++ b/charts/integration/templates/configmap.yaml @@ -56,6 +56,11 @@ data: backgroundWorker: host: backgroundWorker.{{ .Release.Namespace }}.svc.cluster.local port: 8080 + # Background jobs defaults for integration tests + backgroundJobs: + concurrency: 4 + jobTimeout: 5 + maxAttempts: 3 stern: host: stern.{{ .Release.Namespace }}.svc.cluster.local diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 02d66cd070..f8601b5415 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -1669,3 +1669,24 @@ gundeck: settings: cellsEventQueue: "cells_events" ``` +## Background worker: Background jobs + +The background worker consumes jobs from RabbitMQ to process tasks asynchronously. The following configuration controls the consumer’s behavior: + +Internal YAML file and Helm values (under `background-worker.config`): + +```yaml +backgroundJobs: + # Maximum number of in-flight jobs per process + concurrency: 8 + # Per-attempt timeout in seconds + jobTimeout: 60 + # Total attempts including the first run + maxAttempts: 3 +``` + +Notes: + +- `concurrency` controls the AMQP prefetch and caps parallel handler execution per process. +- `jobTimeout` bounds each attempt; timed‑out attempts are retried until `maxAttempts` is reached. +- `maxAttempts` is total tries (first run plus retries). On final failure, the job is dropped (NACK requeue=false) and counted in metrics. diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 1216b96925..08fd67c992 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -603,6 +603,10 @@ background-worker: pushBackoffMinWait: 1000 # 1ms pushBackoffMaxWait: 500000 # 0.5s remotesRefreshInterval: 1000000 # 1s + backgroundJobs: + concurrency: 8 + jobTimeout: 60 + maxAttempts: 3 cassandra: host: {{ .Values.cassandraHost }} replicaCount: 1 diff --git a/services/background-worker/background-worker.integration.yaml b/services/background-worker/background-worker.integration.yaml index 60dc23a926..dd709f71f0 100644 --- a/services/background-worker/background-worker.integration.yaml +++ b/services/background-worker/background-worker.integration.yaml @@ -28,3 +28,9 @@ backendNotificationPusher: pushBackoffMinWait: 1000 # 1ms pushBackoffMaxWait: 1000000 # 1s remotesRefreshInterval: 10000 # 10ms + +# Background jobs consumer configuration for integration +backgroundJobs: + concurrency: 4 + jobTimeout: 5 + maxAttempts: 3 diff --git a/services/background-worker/src/Wire/BackgroundWorker/Env.hs b/services/background-worker/src/Wire/BackgroundWorker/Env.hs index eab31067f9..d4ea9b68af 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Env.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Env.hs @@ -49,7 +49,6 @@ data Env = Env defederationTimeout :: ResponseTimeout, backendNotificationMetrics :: BackendNotificationMetrics, backendNotificationsConfig :: BackendNotificationsConfig, - -- TODO(leif): configmaps and helm charts backgroundJobsConfig :: BackgroundJobsConfig, workerRunningGauge :: Vector Text Gauge, statuses :: IORef (Map Worker IsWorking), From 49f443e045fc5d1b841018e7cab0a7d089024167 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 2 Oct 2025 11:49:14 +0000 Subject: [PATCH 16/40] reduce invalid message sleep --- .../src/Wire/BackgroundWorker/Jobs/Consumer.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs index c4860d6e18..f9def8f2ed 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs @@ -7,6 +7,7 @@ import Control.Retry import Data.Aeson qualified as Aeson import Data.Range (Range (fromRange)) import Data.Timeout +import Data.Timeout (TimeoutUnit (MilliSecond)) import Imports import Network.AMQP qualified as Q import Network.AMQP.Extended @@ -81,7 +82,7 @@ handleDelivery metrics cfg (msg, env) = do Left err -> do withLabel metrics.jobsInvalid "invalid" incCounter Log.err $ Log.msg (Log.val "Invalid background job JSON") . Log.field "error" err - Timeout.threadDelay (3 # Second) -- avoid tight redelivery loop + Timeout.threadDelay (200 # MilliSecond) -- avoid tight redelivery loop liftIO $ Q.rejectEnv env True Right job -> do let lbl = jobPayloadLabel job.payload From ed353a2030d49e3a16799aa4808a8a95b84b01a5 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 2 Oct 2025 12:09:07 +0000 Subject: [PATCH 17/40] fix linting issue --- .../background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs index f9def8f2ed..ced82d055b 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs @@ -7,7 +7,6 @@ import Control.Retry import Data.Aeson qualified as Aeson import Data.Range (Range (fromRange)) import Data.Timeout -import Data.Timeout (TimeoutUnit (MilliSecond)) import Imports import Network.AMQP qualified as Q import Network.AMQP.Extended From 4b83b635f0bf5ecb127ca99df55c654f08b5cb87 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 2 Oct 2025 13:15:00 +0000 Subject: [PATCH 18/40] fix arbitrary instance of request id --- libs/types-common/src/Data/Id.hs | 4 +++- libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/libs/types-common/src/Data/Id.hs b/libs/types-common/src/Data/Id.hs index 09737080ca..ad7f4526ff 100644 --- a/libs/types-common/src/Data/Id.hs +++ b/libs/types-common/src/Data/Id.hs @@ -440,7 +440,9 @@ newtype RequestId = RequestId Generic, ToBytes ) - deriving newtype (Arbitrary) + +instance Arbitrary RequestId where + arbitrary = RequestId . UUID.toASCIIBytes <$> arbitrary @UUID defRequestId :: (IsString s) => s defRequestId = "N/A" diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs index 6908d49992..91f136c22b 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs @@ -367,7 +367,7 @@ testRoundTrip :: T.TestTree testRoundTrip = testProperty msg trip where - msg = show (typeRep @a) + msg = show (typeRep @a) <> " JSON roundtrip" trip (v :: a) = counterexample (show $ toJSON v) $ Right v === (parseEither parseJSON . toJSON) v From 6053f5d00e7ed0a637e5d17c9aaac695bcf79578 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 2 Oct 2025 13:36:41 +0000 Subject: [PATCH 19/40] wip --- .../src/Wire/BackgroundJobsPublisher.hs | 1 - .../Wire/BackgroundJobsRunner/Interpreter.hs | 18 ++++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher.hs index 189f4a555c..9c65214dfc 100644 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher.hs +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher.hs @@ -3,7 +3,6 @@ module Wire.BackgroundJobsPublisher where import Data.Id -import Imports import Polysemy import Wire.API.BackgroundJobs (JobPayload) diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs index e0b5479a1e..b1acc94fa7 100644 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs @@ -2,9 +2,23 @@ module Wire.BackgroundJobsRunner.Interpreter where import Imports import Polysemy -import Wire.API.BackgroundJobs (Job) +import Wire.API.BackgroundJobs import Wire.BackgroundJobsRunner (BackgroundJobsRunner (..)) +import Data.Id interpretBackgroundJobsRunner :: InterpreterFor BackgroundJobsRunner r interpretBackgroundJobsRunner = interpret $ \case - RunJob _ -> pure (Left "TODO: implement BackgroundJobsRunner.runJob") + RunJob job -> runJob job + +runJob :: Job -> Sem r (Either Text ()) +runJob job = case job.payload of + JobSyncUserGroupAndChannel payload -> runSyncUserGroupAndChannel job.jobId job.requestId payload + JobSyncUserGroup payload -> runSyncUserGroup job.jobId job.requestId payload + +runSyncUserGroupAndChannel :: JobId -> RequestId -> SyncUserGroupAndChannel -> Sem r (Either Text ()) +runSyncUserGroupAndChannel _jid _rid _job = pure $ Left "TODO: to be implemented" + +runSyncUserGroup :: JobId -> RequestId -> SyncUserGroup -> Sem r (Either Text ()) +runSyncUserGroup _jid _rid _job = pure $ Left "TODO: to be implemented" + + From ac34b18a3f46e4466af74509b2f9eabdbfc325b8 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 2 Oct 2025 13:52:22 +0000 Subject: [PATCH 20/40] fix warnings in background worker tests --- .../test/Test/Wire/BackendNotificationPusherSpec.hs | 12 ++++++++++++ services/background-worker/test/Test/Wire/Util.hs | 10 +++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs index 9e88cbe9f0..edef9cbae3 100644 --- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs +++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs @@ -330,6 +330,12 @@ spec = do rabbitmqVHost = "test-vhost" defederationTimeout = responseTimeoutNone backendNotificationsConfig = BackendNotificationsConfig 1000 500000 1000 + backgroundJobsConfig = + BackgroundJobsConfig + { concurrency = toRange (Proxy @1), + jobTimeout = toRange (Proxy @100), + maxAttempts = toRange (Proxy @3) + } backendNotificationMetrics <- mkBackendNotificationMetrics workerRunningGauge <- mkWorkerRunningGauge @@ -349,6 +355,12 @@ spec = do rabbitmqVHost = "test-vhost" defederationTimeout = responseTimeoutNone backendNotificationsConfig = BackendNotificationsConfig 1000 500000 1000 + backgroundJobsConfig = + BackgroundJobsConfig + { concurrency = toRange (Proxy @1), + jobTimeout = toRange (Proxy @100), + maxAttempts = toRange (Proxy @3) + } backendNotificationMetrics <- mkBackendNotificationMetrics workerRunningGauge <- mkWorkerRunningGauge domainsThread <- async $ runAppT Env {..} $ getRemoteDomains (fromJust rabbitmqAdminClient) diff --git a/services/background-worker/test/Test/Wire/Util.hs b/services/background-worker/test/Test/Wire/Util.hs index cb4eeef5f9..d84d8170c4 100644 --- a/services/background-worker/test/Test/Wire/Util.hs +++ b/services/background-worker/test/Test/Wire/Util.hs @@ -2,8 +2,10 @@ module Test.Wire.Util where +import Data.Proxy +import Data.Range import Imports -import Network.HTTP.Client +import Network.HTTP.Client hiding (Proxy) import System.Logger.Class qualified as Logger import Util.Options (Endpoint (..)) import Wire.BackgroundWorker.Env hiding (federatorInternal) @@ -24,6 +26,12 @@ testEnv = do rabbitmqVHost = undefined defederationTimeout = responseTimeoutNone backendNotificationsConfig = BackendNotificationsConfig 1000 500000 1000 + backgroundJobsConfig = + BackgroundJobsConfig + { concurrency = toRange (Proxy @1), + jobTimeout = toRange (Proxy @100), + maxAttempts = toRange (Proxy @3) + } pure Env {..} runTestAppT :: AppT IO a -> Int -> IO a From ed0aac7079e44b1b1ad5029de7e9b236c741750b Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 2 Oct 2025 14:58:49 +0000 Subject: [PATCH 21/40] implement sync user group job --- .../src/Wire/BackgroundJobsRunner.hs | 2 +- .../Wire/BackgroundJobsRunner/Interpreter.hs | 58 ++++++++++++++++--- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner.hs index ba814f954a..afc6c86779 100644 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner.hs +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner.hs @@ -7,6 +7,6 @@ import Polysemy import Wire.API.BackgroundJobs (Job) data BackgroundJobsRunner m a where - RunJob :: Job -> BackgroundJobsRunner m (Either Text ()) + RunJob :: Job -> BackgroundJobsRunner m () makeSem ''BackgroundJobsRunner diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs index b1acc94fa7..21d420cc14 100644 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs @@ -1,24 +1,64 @@ module Wire.BackgroundJobsRunner.Interpreter where +import Data.Id +import Data.Qualified import Imports import Polysemy +import Polysemy.Error import Wire.API.BackgroundJobs +import Wire.API.UserGroup +import Wire.BackgroundJobsPublisher import Wire.BackgroundJobsRunner (BackgroundJobsRunner (..)) -import Data.Id +import Wire.UserGroupStore (UserGroupStore, getUserGroup) +import Wire.UserStore + +data BackgroundJobError = BackgroundJobUserTeamNotFound + deriving stock (Show, Eq) -interpretBackgroundJobsRunner :: InterpreterFor BackgroundJobsRunner r +instance Exception BackgroundJobError + +interpretBackgroundJobsRunner :: + ( Member UserGroupStore r, + Member UserStore r, + Member (Error BackgroundJobError) r, + Member BackgroundJobsPublisher r, + Member (Embed IO) r + ) => + InterpreterFor BackgroundJobsRunner r interpretBackgroundJobsRunner = interpret $ \case RunJob job -> runJob job -runJob :: Job -> Sem r (Either Text ()) +runJob :: + ( Member UserGroupStore r, + Member UserStore r, + Member (Error BackgroundJobError) r, + Member BackgroundJobsPublisher r, + Member (Embed IO) r + ) => + Job -> + Sem r () runJob job = case job.payload of JobSyncUserGroupAndChannel payload -> runSyncUserGroupAndChannel job.jobId job.requestId payload JobSyncUserGroup payload -> runSyncUserGroup job.jobId job.requestId payload -runSyncUserGroupAndChannel :: JobId -> RequestId -> SyncUserGroupAndChannel -> Sem r (Either Text ()) -runSyncUserGroupAndChannel _jid _rid _job = pure $ Left "TODO: to be implemented" - -runSyncUserGroup :: JobId -> RequestId -> SyncUserGroup -> Sem r (Either Text ()) -runSyncUserGroup _jid _rid _job = pure $ Left "TODO: to be implemented" - +runSyncUserGroupAndChannel :: JobId -> RequestId -> SyncUserGroupAndChannel -> Sem r () +runSyncUserGroupAndChannel _jid _rid _job = pure () +runSyncUserGroup :: + ( Member UserGroupStore r, + Member UserStore r, + Member (Error BackgroundJobError) r, + Member BackgroundJobsPublisher r, + Member (Embed IO) r + ) => + JobId -> + RequestId -> + SyncUserGroup -> + Sem r () +runSyncUserGroup _jid _rid job = do + teamId <- getUserTeam job.actor >>= note BackgroundJobUserTeamNotFound + channels <- fromMaybe mempty . (foldMap (runIdentity . (.channels))) <$> getUserGroup teamId job.userGroupId + for_ channels $ \chan -> do + let job' = SyncUserGroupAndChannel job.userGroupId (qUnqualified chan) job.actor + jobId <- liftIO randomId + publishJob jobId (JobSyncUserGroupAndChannel job') From a09bc11bd6d694035125423a80ceb327b8e0c01f Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Thu, 2 Oct 2025 18:20:54 +0200 Subject: [PATCH 22/40] fix: rebase --- libs/wire-subsystems/src/Wire/BackgroundJobsRunner.hs | 1 - .../background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner.hs index afc6c86779..4515563ef1 100644 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner.hs +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner.hs @@ -2,7 +2,6 @@ module Wire.BackgroundJobsRunner where -import Imports import Polysemy import Wire.API.BackgroundJobs (Job) diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs index aa3a29b9a0..9a7e902c1f 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs @@ -13,4 +13,5 @@ import Wire.BackgroundWorker.Env (AppT) dispatchJob :: Job -> AppT IO (Either Text ()) dispatchJob job = do let res = run $ interpretBackgroundJobsRunner (runJob job) + -- TODO pure res From 4dcbb28cc2e2d2165cd94c8efcb23f3680a0a246 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Fri, 3 Oct 2025 18:11:52 +0200 Subject: [PATCH 23/40] feat: call the jobs, run the jobs, add config options --- .../polysemy-wire-zoo/polysemy-wire-zoo.cabal | 1 + libs/polysemy-wire-zoo/src/Wire/Sem/Random.hs | 4 +- .../src/Wire/Sem/Random/IO.hs | 1 + .../src/Wire/Sem/Random/Null.hs | 41 ++++++++++++ .../BackgroundJobsPublisher/Interpreter.hs | 38 ----------- .../Wire/BackgroundJobsPublisher/RabbitMQ.hs | 53 +++++++++++++++ .../Wire/UserGroupSubsystem/Interpreter.hs | 66 +++++++++++++++---- .../test/unit/Wire/MockInterpreters/Random.hs | 1 + .../UserGroupSubsystem/InterpreterSpec.hs | 12 +++- libs/wire-subsystems/wire-subsystems.cabal | 2 +- .../background-worker/background-worker.cabal | 1 + .../src/Wire/BackgroundWorker/Env.hs | 11 +++- .../Wire/BackgroundWorker/Jobs/Registry.hs | 29 ++++++-- .../src/Wire/BackgroundWorker/Options.hs | 7 +- .../Wire/BackendNotificationPusherSpec.hs | 4 ++ .../background-worker/test/Test/Wire/Util.hs | 4 +- services/brig/src/Brig/App.hs | 8 ++- .../brig/src/Brig/CanonicalInterpreter.hs | 4 ++ 18 files changed, 221 insertions(+), 66 deletions(-) create mode 100644 libs/polysemy-wire-zoo/src/Wire/Sem/Random/Null.hs delete mode 100644 libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/Interpreter.hs create mode 100644 libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/RabbitMQ.hs diff --git a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal index 42d0ef800a..87dc102d65 100644 --- a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal +++ b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal @@ -33,6 +33,7 @@ library Wire.Sem.Paging.Cassandra Wire.Sem.Random Wire.Sem.Random.IO + Wire.Sem.Random.Null other-modules: Paths_polysemy_wire_zoo hs-source-dirs: src diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Random.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Random.hs index 8cc1ef3386..20e6d02466 100644 --- a/libs/polysemy-wire-zoo/src/Wire/Sem/Random.hs +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Random.hs @@ -21,6 +21,7 @@ module Wire.Sem.Random ( Random (..), bytes, uuid, + newId, scimTokenId, liftRandom, nDigitNumber, @@ -28,7 +29,7 @@ module Wire.Sem.Random where import Crypto.Random.Types -import Data.Id (ScimTokenId) +import Data.Id (Id, ScimTokenId) import Data.UUID (UUID) import Imports import Polysemy @@ -36,6 +37,7 @@ import Polysemy data Random m a where Bytes :: Int -> Random m ByteString Uuid :: Random m UUID + NewId :: Random m (Id a) ScimTokenId :: Random m ScimTokenId LiftRandom :: (forall mr. (MonadRandom mr) => mr a) -> Random m a NDigitNumber :: Int -> Random m Integer diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Random/IO.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Random/IO.hs index d073d267e8..53d75a904e 100644 --- a/libs/polysemy-wire-zoo/src/Wire/Sem/Random/IO.hs +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Random/IO.hs @@ -35,6 +35,7 @@ randomToIO :: randomToIO = interpret $ \case Bytes i -> embed $ randBytes i Uuid -> embed $ UUID.nextRandom + NewId -> embed $ randomId @IO ScimTokenId -> embed $ randomId @IO LiftRandom m -> embed @IO $ m NDigitNumber n -> embed $ randIntegerZeroToNMinusOne (10 ^ n) diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Random/Null.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Random/Null.hs new file mode 100644 index 0000000000..26f6f06816 --- /dev/null +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Random/Null.hs @@ -0,0 +1,41 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.Sem.Random.Null + ( randomToNull, + ) +where + +import Crypto.Random +import Data.Id (Id (..)) +import qualified Data.UUID as UUID +import Imports +import Polysemy +import Wire.Sem.Random (Random (..)) + +randomToNull :: + Sem (Random ': r) a -> + Sem r a +randomToNull = interpret $ \case + Bytes i -> pure $ mconcat $ replicate i "0" + Uuid -> pure UUID.nil + NewId -> pure $ Id UUID.nil + ScimTokenId -> pure $ Id UUID.nil + LiftRandom m -> pure $ fst $ withDRG (drgNewSeed $ seedFromInteger 0) m + NDigitNumber n -> pure $ 10 ^ n diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/Interpreter.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/Interpreter.hs deleted file mode 100644 index 9f00ed66e9..0000000000 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/Interpreter.hs +++ /dev/null @@ -1,38 +0,0 @@ -module Wire.BackgroundJobsPublisher.Interpreter where - -import Data.Id (JobId, RequestId) -import Imports -import Network.AMQP qualified as Q -import Polysemy -import Polysemy.Input -import Wire.API.BackgroundJobs -import Wire.BackgroundJobsPublisher (BackgroundJobsPublisher (..)) - -interpretBackgroundJobsPublisherRabbitMQ :: - ( Member (Embed IO) r, - Member (Input RequestId) r, - Member (Input Q.Channel) r - ) => - InterpreterFor BackgroundJobsPublisher r -interpretBackgroundJobsPublisherRabbitMQ = - interpret $ \case - PublishJob jobId jobPayload -> publishJob jobId jobPayload - -publishJob :: - ( Member (Embed IO) r, - Member (Input RequestId) r, - Member (Input Q.Channel) r - ) => - JobId -> - JobPayload -> - Sem r () -publishJob jobId jobPayload = do - requestId <- input - channel <- input - let job = - Job - { payload = jobPayload, - jobId = jobId, - requestId = requestId - } - liftIO $ publishBackgroundJob channel job diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/RabbitMQ.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/RabbitMQ.hs new file mode 100644 index 0000000000..c1fbfaf7d4 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/RabbitMQ.hs @@ -0,0 +1,53 @@ +module Wire.BackgroundJobsPublisher.RabbitMQ where + +import Data.Id (JobId, RequestId) +import Imports +import Network.AMQP qualified as Q +import Polysemy +import Polysemy.Input +import Wire.API.BackgroundJobs +import Wire.BackgroundJobsPublisher (BackgroundJobsPublisher (..)) +import Wire.BackgroundJobsPublisher.Null (interpretBackgroundJobsPublisherNoConfig) + +interpretBackgroundJobsPublisherRabbitMQOptional :: + ( Member (Embed IO) r + ) => + RequestId -> + Maybe (MVar Q.Channel) -> + InterpreterFor BackgroundJobsPublisher r +interpretBackgroundJobsPublisherRabbitMQOptional requestId = + \case + Nothing -> interpretBackgroundJobsPublisherNoConfig + Just channelRef -> + runInputSem (readMVar channelRef) + . interpretBackgroundJobsPublisherRabbitMQ requestId + . raiseUnder + +interpretBackgroundJobsPublisherRabbitMQ :: + ( Member (Embed IO) r, + Member (Input Q.Channel) r + ) => + RequestId -> + InterpreterFor BackgroundJobsPublisher r +interpretBackgroundJobsPublisherRabbitMQ requestId = + interpret $ \case + PublishJob jobId jobPayload -> do + channel <- input + publishJob requestId channel jobId jobPayload + +publishJob :: + ( Member (Embed IO) r + ) => + RequestId -> + Q.Channel -> + JobId -> + JobPayload -> + Sem r () +publishJob requestId channel jobId jobPayload = do + let job = + Job + { payload = jobPayload, + jobId = jobId, + requestId = requestId + } + liftIO $ publishBackgroundJob channel job diff --git a/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs index a1b63dcd7a..9cae4bdba9 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs @@ -12,6 +12,7 @@ import Imports import Polysemy import Polysemy.Error import Polysemy.Input (Input, input) +import Wire.API.BackgroundJobs import Wire.API.Error import Wire.API.Error.Brig qualified as E import Wire.API.Pagination @@ -22,9 +23,11 @@ import Wire.API.User import Wire.API.UserEvent import Wire.API.UserGroup import Wire.API.UserGroup.Pagination +import Wire.BackgroundJobsPublisher import Wire.Error import Wire.GalleyAPIAccess (GalleyAPIAccess, getTeamConv) import Wire.NotificationSubsystem +import Wire.Sem.Random qualified as Random import Wire.TeamSubsystem import Wire.UserGroupStore (PaginationState (..), UserGroupPageRequest (..)) import Wire.UserGroupStore qualified as Store @@ -32,13 +35,15 @@ import Wire.UserGroupSubsystem (UserGroupSubsystem (..)) import Wire.UserSubsystem (UserSubsystem, getLocalUserProfiles, getUserTeam) interpretUserGroupSubsystem :: - ( Member UserSubsystem r, + ( Member Random.Random r, + Member UserSubsystem r, Member (Error UserGroupSubsystemError) r, Member Store.UserGroupStore r, Member (Input (Local ())) r, Member NotificationSubsystem r, Member TeamSubsystem r, - Member GalleyAPIAccess r + Member GalleyAPIAccess r, + Member BackgroundJobsPublisher r ) => InterpreterFor UserGroupSubsystem r interpretUserGroupSubsystem = interpret $ \case @@ -71,12 +76,14 @@ userGroupSubsystemErrorToHttpError = UserGroupChannelNotFound -> errorToWai @E.UserGroupChannelNotFound createUserGroup :: - ( Member UserSubsystem r, + ( Member Random.Random r, + Member UserSubsystem r, Member (Error UserGroupSubsystemError) r, Member Store.UserGroupStore r, Member (Input (Local ())) r, Member NotificationSubsystem r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member BackgroundJobsPublisher r ) => UserId -> NewUserGroup -> @@ -97,6 +104,7 @@ createUserGroup creator newGroup = do pushNotifications [ mkEvent creator (UserGroupCreated ug.id_) admins ] + triggerSyncUserGroup creator ug.id_ pure ug getTeamAsAdmin :: @@ -245,11 +253,13 @@ deleteGroup deleter groupId = throw UserGroupNotATeamAdmin addUser :: - ( Member UserSubsystem r, + ( Member Random.Random r, + Member UserSubsystem r, Member Store.UserGroupStore r, Member (Error UserGroupSubsystemError) r, Member NotificationSubsystem r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member BackgroundJobsPublisher r ) => UserId -> UserGroupId -> @@ -265,13 +275,16 @@ addUser adder groupId addeeId = do pushNotifications [ mkEvent adder (UserGroupUpdated groupId) admins ] + triggerSyncUserGroup adder groupId addUsers :: - ( Member UserSubsystem r, + ( Member Random.Random r, + Member UserSubsystem r, Member Store.UserGroupStore r, Member (Error UserGroupSubsystemError) r, Member NotificationSubsystem r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member BackgroundJobsPublisher r ) => UserId -> UserGroupId -> @@ -291,12 +304,16 @@ addUsers adder groupId addeeIds = do [ mkEvent adder (UserGroupUpdated groupId) admins ] + triggerSyncUserGroup adder groupId + updateUsers :: - ( Member UserSubsystem r, + ( Member Random.Random r, + Member UserSubsystem r, Member Store.UserGroupStore r, Member (Error UserGroupSubsystemError) r, Member NotificationSubsystem r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member BackgroundJobsPublisher r ) => UserId -> UserGroupId -> @@ -312,13 +329,16 @@ updateUsers updater groupId uids = do pushNotifications [ mkEvent updater (UserGroupUpdated groupId) admins ] + triggerSyncUserGroup updater groupId removeUser :: - ( Member UserSubsystem r, + ( Member Random.Random r, + Member UserSubsystem r, Member Store.UserGroupStore r, Member (Error UserGroupSubsystemError) r, Member NotificationSubsystem r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member BackgroundJobsPublisher r ) => UserId -> UserGroupId -> @@ -334,13 +354,16 @@ removeUser remover groupId removeeId = do pushNotifications [ mkEvent remover (UserGroupUpdated groupId) admins ] + triggerSyncUserGroup remover groupId updateChannels :: - ( Member UserSubsystem r, + ( Member Random.Random r, + Member UserSubsystem r, Member Store.UserGroupStore r, Member (Error UserGroupSubsystemError) r, Member TeamSubsystem r, - Member GalleyAPIAccess r + Member GalleyAPIAccess r, + Member BackgroundJobsPublisher r ) => UserId -> UserGroupId -> @@ -351,6 +374,7 @@ updateChannels performer groupId channelIds = do teamId <- getTeamAsAdmin performer >>= note UserGroupNotATeamAdmin traverse_ (getTeamConv performer teamId >=> note UserGroupChannelNotFound) channelIds Store.updateUserGroupChannels groupId channelIds + triggerSyncUserGroup performer groupId listChannels :: ( Member UserSubsystem r, @@ -365,3 +389,17 @@ listChannels performer groupId = do void $ getUserGroup performer groupId >>= note UserGroupNotFound void $ getUserTeam performer >>= note UserGroupNotATeamAdmin Store.listUserGroupChannels groupId + +triggerSyncUserGroup :: + ( Member Random.Random r, + Member BackgroundJobsPublisher r + ) => + UserId -> + UserGroupId -> + Sem r () +triggerSyncUserGroup performer groupId = do + jobId <- Random.newId + publishJob + jobId + $ JobSyncUserGroup + SyncUserGroup {userGroupId = groupId, actor = performer} diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/Random.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/Random.hs index 6352edfe9d..870d813b0e 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/Random.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/Random.hs @@ -14,6 +14,7 @@ randomToStatefulStdGen = interpret $ \case Bytes n -> do fromShort <$> withStatefulGen (genShortByteString n) Uuid -> withStatefulGen random + NewId -> Id <$> withStatefulGen random ScimTokenId -> Id <$> withStatefulGen random LiftRandom m -> do seedInt <- withStatefulGen (random @Int) diff --git a/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs index a6d75423ba..155a309d57 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs @@ -38,9 +38,13 @@ import Wire.API.UserEvent import Wire.API.UserGroup import Wire.API.UserGroup.Pagination import Wire.Arbitrary +import Wire.BackgroundJobsPublisher qualified as BackgroundJobsPublisher +import Wire.BackgroundJobsPublisher.Null qualified as BackgroundJobsPublisher import Wire.GalleyAPIAccess import Wire.MockInterpreters as Mock import Wire.NotificationSubsystem +import Wire.Sem.Random qualified as Random +import Wire.Sem.Random.Null qualified as Random import Wire.TeamSubsystem import Wire.TeamSubsystem.GalleyAPI import Wire.UserGroupSubsystem @@ -56,8 +60,10 @@ type AllDependencies = `Append` '[ Input (Local ()), MockNow, NotificationSubsystem, + BackgroundJobsPublisher.BackgroundJobsPublisher, State [Push], - Error UserGroupSubsystemError + Error UserGroupSubsystemError, + Random.Random ] runDependenciesFailOnError :: (HasCallStack) => [User] -> Map TeamId [TeamMember] -> Sem AllDependencies (IO ()) -> IO () @@ -70,8 +76,10 @@ runDependencies :: Either UserGroupSubsystemError a runDependencies initialUsers initialTeams = run + . Random.randomToNull . runError . evalState mempty + . BackgroundJobsPublisher.interpretBackgroundJobsPublisherNoConfig . inMemoryNotificationSubsystemInterpreter . evalState defaultTime . runInputConst (toLocalUnsafe (Domain "example.com") ()) @@ -87,8 +95,10 @@ runDependenciesWithReturnState :: Either UserGroupSubsystemError ([Push], a) runDependenciesWithReturnState initialUsers initialTeams = run + . Random.randomToNull . runError . runState mempty + . BackgroundJobsPublisher.interpretBackgroundJobsPublisherNoConfig . inMemoryNotificationSubsystemInterpreter . evalState defaultTime . runInputConst (toLocalUnsafe (Domain "example.com") ()) diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 977c4f9657..19d3a2baf0 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -183,8 +183,8 @@ library Wire.AuthenticationSubsystem.ZAuth Wire.AWS Wire.BackgroundJobsPublisher - Wire.BackgroundJobsPublisher.Interpreter Wire.BackgroundJobsPublisher.Null + Wire.BackgroundJobsPublisher.RabbitMQ Wire.BackgroundJobsRunner Wire.BackgroundJobsRunner.Interpreter Wire.BlockListStore diff --git a/services/background-worker/background-worker.cabal b/services/background-worker/background-worker.cabal index 031f19a8ea..758881060e 100644 --- a/services/background-worker/background-worker.cabal +++ b/services/background-worker/background-worker.cabal @@ -41,6 +41,7 @@ library , exceptions , extended , extra + , hasql-pool , HsOpenSSL , http-client , http2-manager diff --git a/services/background-worker/src/Wire/BackgroundWorker/Env.hs b/services/background-worker/src/Wire/BackgroundWorker/Env.hs index d4ea9b68af..5c488217d8 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Env.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Env.hs @@ -10,7 +10,10 @@ import Control.Monad.Catch import Control.Monad.Trans.Control import Data.Map.Strict qualified as Map import HTTP2.Client.Manager +import Hasql.Pool qualified as HasqlPool +import Hasql.Pool.Extended (initPostgresPool) import Imports +import Network.AMQP qualified as Q import Network.AMQP.Extended import Network.HTTP.Client import Network.RabbitMqAdmin qualified as RabbitMqAdmin @@ -52,7 +55,9 @@ data Env = Env backgroundJobsConfig :: BackgroundJobsConfig, workerRunningGauge :: Vector Text Gauge, statuses :: IORef (Map Worker IsWorking), - cassandra :: ClientState + cassandra :: ClientState, + hasqlPool :: HasqlPool.Pool, + backgroundJobsQueue :: MVar Q.Channel } data BackendNotificationMetrics = BackendNotificationMetrics @@ -96,6 +101,10 @@ mkEnv opts = do let backendNotificationsConfig = opts.backendNotificationPusher backgroundJobsConfig = opts.backgroundJobs workerRunningGauge <- mkWorkerRunningGauge + hasqlPool <- initPostgresPool opts.postgresql opts.postgresqlPassword + backgroundJobsQueue <- + mkRabbitMqChannelMVar logger (Just "background-worker") $ + either id demoteOpts opts.rabbitmq.unRabbitMqOpts pure Env {..} initHttp2Manager :: IO Http2Manager diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs index 9a7e902c1f..1ffe22382e 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs @@ -3,15 +3,32 @@ module Wire.BackgroundWorker.Jobs.Registry ) where +import Data.Text qualified as T +import Hasql.Pool (Pool, UsageError) import Imports -import Polysemy (run) +import Polysemy +import Polysemy.Error +import Polysemy.Input import Wire.API.BackgroundJobs (Job (..)) +import Wire.BackgroundJobsPublisher.RabbitMQ (interpretBackgroundJobsPublisherRabbitMQ) import Wire.BackgroundJobsRunner (runJob) -import Wire.BackgroundJobsRunner.Interpreter (interpretBackgroundJobsRunner) -import Wire.BackgroundWorker.Env (AppT) +import Wire.BackgroundJobsRunner.Interpreter hiding (runJob) +import Wire.BackgroundWorker.Env (AppT, Env (..)) +import Wire.UserGroupStore.Postgres (interpretUserGroupStoreToPostgres) +import Wire.UserStore.Cassandra (interpretUserStoreCassandra) dispatchJob :: Job -> AppT IO (Either Text ()) dispatchJob job = do - let res = run $ interpretBackgroundJobsRunner (runJob job) - -- TODO - pure res + env <- ask @Env + liftIO $ + runM $ + runError $ + mapError @BackgroundJobError (T.pack . show) $ + mapError @UsageError (T.pack . show) $ + runInputConst @Pool env.hasqlPool $ + interpretUserStoreCassandra env.cassandra $ + interpretUserGroupStoreToPostgres $ + runInputSem (readMVar env.backgroundJobsQueue) $ + interpretBackgroundJobsPublisherRabbitMQ job.requestId $ + interpretBackgroundJobsRunner $ + runJob job diff --git a/services/background-worker/src/Wire/BackgroundWorker/Options.hs b/services/background-worker/src/Wire/BackgroundWorker/Options.hs index 2bd8a00d52..c804928b2f 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Options.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Options.hs @@ -18,7 +18,12 @@ data Opts = Opts defederationTimeout :: Maybe Int, backendNotificationPusher :: BackendNotificationsConfig, cassandra :: CassandraOpts, - backgroundJobs :: BackgroundJobsConfig + backgroundJobs :: BackgroundJobsConfig, + -- | Postgresql settings, the key values must be in libpq format. + -- https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS + -- TODO set config + postgresql :: !(Map Text Text), + postgresqlPassword :: !(Maybe FilePathSecrets) } deriving (Show, Generic) deriving (FromJSON) via Generically Opts diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs index edef9cbae3..aeeca284b9 100644 --- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs +++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs @@ -336,6 +336,8 @@ spec = do jobTimeout = toRange (Proxy @100), maxAttempts = toRange (Proxy @3) } + hasqlPool = undefined + backgroundJobsQueue = undefined backendNotificationMetrics <- mkBackendNotificationMetrics workerRunningGauge <- mkWorkerRunningGauge @@ -361,6 +363,8 @@ spec = do jobTimeout = toRange (Proxy @100), maxAttempts = toRange (Proxy @3) } + hasqlPool = undefined + backgroundJobsQueue = undefined backendNotificationMetrics <- mkBackendNotificationMetrics workerRunningGauge <- mkWorkerRunningGauge domainsThread <- async $ runAppT Env {..} $ getRemoteDomains (fromJust rabbitmqAdminClient) diff --git a/services/background-worker/test/Test/Wire/Util.hs b/services/background-worker/test/Test/Wire/Util.hs index d84d8170c4..96623a8a07 100644 --- a/services/background-worker/test/Test/Wire/Util.hs +++ b/services/background-worker/test/Test/Wire/Util.hs @@ -16,7 +16,7 @@ testEnv :: IO Env testEnv = do http2Manager <- initHttp2Manager logger <- Logger.new Logger.defSettings - let cassandra = undefined + let cassandra = undefined -- TODO statuses <- newIORef mempty backendNotificationMetrics <- mkBackendNotificationMetrics workerRunningGauge <- mkWorkerRunningGauge @@ -32,6 +32,8 @@ testEnv = do jobTimeout = toRange (Proxy @100), maxAttempts = toRange (Proxy @3) } + hasqlPool = undefined -- TODO + backgroundJobsQueue = undefined -- TODO pure Env {..} runTestAppT :: AppT IO a -> Int -> IO a diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 7b2f67979d..6b2411232f 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -71,6 +71,7 @@ module Brig.App disabledVersionsLens, enableSFTFederationLens, rateLimitEnvLens, + backgroundJobsQueueLens, initZAuth, initLogger, initPostgresPool, @@ -218,7 +219,8 @@ data Env = Env rabbitmqChannel :: Maybe (MVar Q.Channel), disabledVersions :: Set Version, enableSFTFederation :: Maybe Bool, - rateLimitEnv :: RateLimitEnv + rateLimitEnv :: RateLimitEnv, + backgroundJobsQueue :: Maybe (MVar Q.Channel) } makeLensesWith (lensRules & lensField .~ suffixNamer) ''Env @@ -280,6 +282,7 @@ newEnv opts = do idxEnv <- mkIndexEnv opts.elasticsearch lgr (Opt.galley opts) mgr rateLimitEnv <- newRateLimitEnv opts.settings.passwordHashingRateLimit hasqlPool <- initPostgresPool opts.postgresql opts.postgresqlPassword + backgroundJobsQueue <- traverse (Q.mkRabbitMqChannelMVar lgr (Just "brig")) opts.rabbitmq pure $! Env { cargohold = mkEndpoint $ opts.cargohold, @@ -319,7 +322,8 @@ newEnv opts = do rabbitmqChannel = rabbitChan, disabledVersions = allDisabledVersions, enableSFTFederation = opts.multiSFT, - rateLimitEnv + rateLimitEnv, + backgroundJobsQueue } where emailConn _ (Opt.EmailAWS aws) = pure (Just aws, Nothing) diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 6e968f75ae..0fb1acf4f7 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -45,6 +45,8 @@ import Wire.AppSubsystem.Interpreter import Wire.AuthenticationSubsystem import Wire.AuthenticationSubsystem.Config import Wire.AuthenticationSubsystem.Interpreter +import Wire.BackgroundJobsPublisher (BackgroundJobsPublisher) +import Wire.BackgroundJobsPublisher.RabbitMQ (interpretBackgroundJobsPublisherRabbitMQOptional) import Wire.BlockListStore import Wire.BlockListStore.Cassandra import Wire.DeleteQueue @@ -153,6 +155,7 @@ type BrigLowerLevelEffects = DeleteQueue, Wire.Events.Events, NotificationSubsystem, + BackgroundJobsPublisher, RateLimit, UserGroupStore, Error AppSubsystemError, @@ -363,6 +366,7 @@ runBrigToIO e (AppT ma) = do . mapError appSubsystemErrorToHttpError . interpretUserGroupStoreToPostgres . interpretRateLimit e.rateLimitEnv + . interpretBackgroundJobsPublisherRabbitMQOptional e.requestId e.backgroundJobsQueue . runNotificationSubsystemGundeck (defaultNotificationSubsystemConfig e.requestId) . runEvents . runDeleteQueue e.internalEvents From c0c61ef69aadb9b704f6d55677df5fc2066eb368 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Fri, 3 Oct 2025 18:36:16 +0200 Subject: [PATCH 24/40] fix: hlint --- libs/polysemy-wire-zoo/src/Wire/Sem/Random/Null.hs | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Random/Null.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Random/Null.hs index 26f6f06816..7d19eff66c 100644 --- a/libs/polysemy-wire-zoo/src/Wire/Sem/Random/Null.hs +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Random/Null.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2025 Wire Swiss GmbH From c0f8a25903f271675b95ab43e794d18c124aee77 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Fri, 3 Oct 2025 18:53:32 +0200 Subject: [PATCH 25/40] fix: nix files --- services/background-worker/default.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/background-worker/default.nix b/services/background-worker/default.nix index c713987b6a..e3266a2416 100644 --- a/services/background-worker/default.nix +++ b/services/background-worker/default.nix @@ -17,6 +17,7 @@ , extra , federator , gitignoreSource +, hasql-pool , HsOpenSSL , hspec , http-client @@ -65,6 +66,7 @@ mkDerivation { exceptions extended extra + hasql-pool HsOpenSSL http-client http2-manager From 975475b1644f30341b6104f096656b4b85bd28d1 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Fri, 3 Oct 2025 23:48:10 +0200 Subject: [PATCH 26/40] fix: config in yamls --- charts/background-worker/templates/configmap.yaml | 5 +++++ hack/helm_vars/wire-server/values.yaml.gotmpl | 5 +++++ .../background-worker/background-worker.integration.yaml | 6 ++++++ 3 files changed, 16 insertions(+) diff --git a/charts/background-worker/templates/configmap.yaml b/charts/background-worker/templates/configmap.yaml index e1a5104cd8..cdf5d18c5a 100644 --- a/charts/background-worker/templates/configmap.yaml +++ b/charts/background-worker/templates/configmap.yaml @@ -53,3 +53,8 @@ data: {{ toYaml . | indent 6 }} {{- end }} {{- end }} + + postgresql: {{ toYaml .postgresql | nindent 6 }} + {{- if hasKey $.Values.secrets "pgPassword" }} + postgresqlPassword: /etc/wire/background-worker/secrets/pgPassword + {{- end }} diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 08fd67c992..6cc5a02f37 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -623,6 +623,11 @@ background-worker: tlsCaSecretRef: name: "rabbitmq-certificate" key: "ca.crt" + postgresql: + host: "postgresql" + port: "5432" + user: wire-server + dbname: wire-server secrets: rabbitmq: username: {{ .Values.rabbitmqUsername }} diff --git a/services/background-worker/background-worker.integration.yaml b/services/background-worker/background-worker.integration.yaml index dd709f71f0..b072498809 100644 --- a/services/background-worker/background-worker.integration.yaml +++ b/services/background-worker/background-worker.integration.yaml @@ -34,3 +34,9 @@ backgroundJobs: concurrency: 4 jobTimeout: 5 maxAttempts: 3 + +postgresql: + host: "postgresql" + port: "5432" + user: wire-server + dbname: wire-server From aca00364dc0b7d65d7c04aabd24b77dc9c8f7e9e Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Mon, 6 Oct 2025 10:23:33 +0200 Subject: [PATCH 27/40] refactor: move publish from wire-api to wire-subsystem --- libs/wire-api/src/Wire/API/BackgroundJobs.hs | 15 --------------- .../src/Wire/BackgroundJobsPublisher/RabbitMQ.hs | 16 ++++++++++++++-- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/libs/wire-api/src/Wire/API/BackgroundJobs.hs b/libs/wire-api/src/Wire/API/BackgroundJobs.hs index 336dfa5951..a3d7474c14 100644 --- a/libs/wire-api/src/Wire/API/BackgroundJobs.hs +++ b/libs/wire-api/src/Wire/API/BackgroundJobs.hs @@ -27,7 +27,6 @@ import Data.Id import Data.Map.Strict qualified as Map import Data.OpenApi qualified as S import Data.Schema -import Data.Text.Encoding qualified as T import Imports import Network.AMQP qualified as Q import Network.AMQP.Types qualified as QT @@ -141,20 +140,6 @@ instance ToSchema Job where <*> requestId .= field "requestId" schema <*> payload .= field "payload" schema --- | Publish a job to the default exchange with routing key "background-jobs". --- Sets content-type to application/json and message_id to the Job.id. -publishBackgroundJob :: Q.Channel -> Job -> IO () -publishBackgroundJob chan job = do - let msg = - Q.newMsg - { Q.msgBody = Aeson.encode job, - Q.msgContentType = Just "application/json", - Q.msgID = Just (idToText job.jobId), - Q.msgCorrelationID = Just $ T.decodeUtf8 job.requestId.unRequestId - } - ensureBackgroundJobsQueue chan - void $ Q.publishMsg chan "" backgroundJobsRoutingKey msg - backgroundJobsRoutingKey :: Text backgroundJobsRoutingKey = backgroundJobsQueueName diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/RabbitMQ.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/RabbitMQ.hs index c1fbfaf7d4..2ce9126ed9 100644 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/RabbitMQ.hs +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/RabbitMQ.hs @@ -1,6 +1,8 @@ module Wire.BackgroundJobsPublisher.RabbitMQ where -import Data.Id (JobId, RequestId) +import Data.Aeson qualified as Aeson +import Data.Id (JobId, RequestId (..), idToText) +import Data.Text.Encoding qualified as T import Imports import Network.AMQP qualified as Q import Polysemy @@ -50,4 +52,14 @@ publishJob requestId channel jobId jobPayload = do jobId = jobId, requestId = requestId } - liftIO $ publishBackgroundJob channel job + msg = + Q.newMsg + { Q.msgBody = Aeson.encode job, + Q.msgContentType = Just "application/json", + Q.msgID = Just (idToText job.jobId), + Q.msgCorrelationID = Just $ T.decodeUtf8 job.requestId.unRequestId + } + + liftIO $ do + ensureBackgroundJobsQueue channel + void $ Q.publishMsg channel "" backgroundJobsRoutingKey msg From 86b8fc95db75daf55ca0037bccf153bfbfa28ad9 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Mon, 6 Oct 2025 11:20:27 +0200 Subject: [PATCH 28/40] fix: k8s configs --- charts/background-worker/templates/configmap.yaml | 3 ++- hack/helm_vars/wire-server/values.yaml.gotmpl | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/charts/background-worker/templates/configmap.yaml b/charts/background-worker/templates/configmap.yaml index cdf5d18c5a..3e1475aa5d 100644 --- a/charts/background-worker/templates/configmap.yaml +++ b/charts/background-worker/templates/configmap.yaml @@ -54,7 +54,8 @@ data: {{- end }} {{- end }} - postgresql: {{ toYaml .postgresql | nindent 6 }} + postgresql: +{{ toYaml .postgresql | nindent 6 }} {{- if hasKey $.Values.secrets "pgPassword" }} postgresqlPassword: /etc/wire/background-worker/secrets/pgPassword {{- end }} diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 6cc5a02f37..8db38a8f52 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -632,6 +632,7 @@ background-worker: rabbitmq: username: {{ .Values.rabbitmqUsername }} password: {{ .Values.rabbitmqPassword }} + pgPassword: "posty-the-gres" integration: ingress: From 26c4936670bdcdd6fb4dca69781171aba23c7269 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Mon, 6 Oct 2025 11:37:21 +0200 Subject: [PATCH 29/40] fix: k8s configs --- charts/background-worker/templates/configmap.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/background-worker/templates/configmap.yaml b/charts/background-worker/templates/configmap.yaml index 3e1475aa5d..ada8513482 100644 --- a/charts/background-worker/templates/configmap.yaml +++ b/charts/background-worker/templates/configmap.yaml @@ -54,8 +54,8 @@ data: {{- end }} {{- end }} - postgresql: + postgresql: {{ toYaml .postgresql | nindent 6 }} {{- if hasKey $.Values.secrets "pgPassword" }} - postgresqlPassword: /etc/wire/background-worker/secrets/pgPassword + postgresqlPassword: /etc/wire/background-worker/secrets/pgPassword {{- end }} From cc46c4f0acb19d7fb5eb46cffa58ea0ed3ad22cd Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Mon, 6 Oct 2025 12:34:10 +0200 Subject: [PATCH 30/40] fix: k8s configs --- charts/background-worker/templates/configmap.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/charts/background-worker/templates/configmap.yaml b/charts/background-worker/templates/configmap.yaml index ada8513482..9703b9dfd0 100644 --- a/charts/background-worker/templates/configmap.yaml +++ b/charts/background-worker/templates/configmap.yaml @@ -54,8 +54,7 @@ data: {{- end }} {{- end }} - postgresql: -{{ toYaml .postgresql | nindent 6 }} + postgresql: {{ toYaml .postgresql | nindent 6 }} {{- if hasKey $.Values.secrets "pgPassword" }} postgresqlPassword: /etc/wire/background-worker/secrets/pgPassword {{- end }} From f3666c63f835efad95fbb50ac3b4b6b26b290d9d Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 6 Oct 2025 12:04:36 +0000 Subject: [PATCH 31/40] fix: k8s config --- charts/background-worker/templates/configmap.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/charts/background-worker/templates/configmap.yaml b/charts/background-worker/templates/configmap.yaml index 9703b9dfd0..b7a0114665 100644 --- a/charts/background-worker/templates/configmap.yaml +++ b/charts/background-worker/templates/configmap.yaml @@ -52,9 +52,9 @@ data: backgroundJobs: {{ toYaml . | indent 6 }} {{- end }} - {{- end }} - - postgresql: {{ toYaml .postgresql | nindent 6 }} - {{- if hasKey $.Values.secrets "pgPassword" }} + postgresql: +{{ toYaml .postgresql | indent 6 }} + {{- if hasKey $.Values.secrets "pgPassword" }} postgresqlPassword: /etc/wire/background-worker/secrets/pgPassword + {{- end }} {{- end }} From 3b719fe5ec7270e799afa9a06058421002ec62f5 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 6 Oct 2025 12:12:00 +0000 Subject: [PATCH 32/40] refactor --- .../Wire/BackgroundWorker/Jobs/Registry.hs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs index 1ffe22382e..469076681c 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs @@ -20,15 +20,16 @@ import Wire.UserStore.Cassandra (interpretUserStoreCassandra) dispatchJob :: Job -> AppT IO (Either Text ()) dispatchJob job = do env <- ask @Env - liftIO $ - runM $ - runError $ - mapError @BackgroundJobError (T.pack . show) $ - mapError @UsageError (T.pack . show) $ - runInputConst @Pool env.hasqlPool $ - interpretUserStoreCassandra env.cassandra $ - interpretUserGroupStoreToPostgres $ - runInputSem (readMVar env.backgroundJobsQueue) $ - interpretBackgroundJobsPublisherRabbitMQ job.requestId $ - interpretBackgroundJobsRunner $ - runJob job + liftIO $ runInterpreters env $ runJob job + where + runInterpreters env = + runM + . runError + . mapError @BackgroundJobError (T.pack . show) + . mapError @UsageError (T.pack . show) + . runInputConst @Pool env.hasqlPool + . interpretUserStoreCassandra env.cassandra + . interpretUserGroupStoreToPostgres + . runInputSem (readMVar env.backgroundJobsQueue) + . interpretBackgroundJobsPublisherRabbitMQ job.requestId + . interpretBackgroundJobsRunner From e9252adc352e981d1d0be5cd276983a7dd92de9b Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 6 Oct 2025 12:35:30 +0000 Subject: [PATCH 33/40] missing wiring of postgres config in background-worker --- charts/background-worker/templates/deployment.yaml | 2 ++ charts/background-worker/templates/secret.yaml | 3 +++ charts/background-worker/values.yaml | 12 ++++++++++++ .../src/Wire/BackgroundWorker/Options.hs | 1 - 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/charts/background-worker/templates/deployment.yaml b/charts/background-worker/templates/deployment.yaml index aeeab1ecc5..da77d57b76 100644 --- a/charts/background-worker/templates/deployment.yaml +++ b/charts/background-worker/templates/deployment.yaml @@ -58,6 +58,8 @@ spec: {{- toYaml .Values.podSecurityContext | nindent 12 }} {{- end }} volumeMounts: + - name: "background-worker-secrets" + mountPath: "/etc/wire/background-worker/secrets" - name: "background-worker-config" mountPath: "/etc/wire/background-worker/conf" {{- if eq (include "useCassandraTLS" .Values.config) "true" }} diff --git a/charts/background-worker/templates/secret.yaml b/charts/background-worker/templates/secret.yaml index 25a22ce67e..dfde355db9 100644 --- a/charts/background-worker/templates/secret.yaml +++ b/charts/background-worker/templates/secret.yaml @@ -15,4 +15,7 @@ data: {{- with .Values.secrets }} rabbitmqUsername: {{ .rabbitmq.username | b64enc | quote }} rabbitmqPassword: {{ .rabbitmq.password | b64enc | quote }} + {{- if .pgPassword }} + pgPassword: {{ .pgPassword | b64enc | quote }} + {{- end }} {{- end }} diff --git a/charts/background-worker/values.yaml b/charts/background-worker/values.yaml index 48dae0ca3b..f9100f681c 100644 --- a/charts/background-worker/values.yaml +++ b/charts/background-worker/values.yaml @@ -19,6 +19,17 @@ config: logLevel: Info logFormat: StructuredJSON enableFederation: false # keep in sync with brig, cargohold and galley charts' config.enableFederation as well as wire-server chart's tags.federation + # Postgres connection settings + # + # Values are described in https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS + # To set the password via a background-worker secret see `secrets.pgPassword`. + # + # Below is an example configuration used in CI tests. + postgresql: + host: postgresql # DNS name without protocol + port: "5432" + user: wire-server + dbname: wire-server rabbitmq: host: rabbitmq port: 5672 @@ -47,6 +58,7 @@ config: maxAttempts: 3 secrets: {} + # pgPassword: podSecurityContext: allowPrivilegeEscalation: false diff --git a/services/background-worker/src/Wire/BackgroundWorker/Options.hs b/services/background-worker/src/Wire/BackgroundWorker/Options.hs index c804928b2f..55ed162f9c 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Options.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Options.hs @@ -21,7 +21,6 @@ data Opts = Opts backgroundJobs :: BackgroundJobsConfig, -- | Postgresql settings, the key values must be in libpq format. -- https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS - -- TODO set config postgresql :: !(Map Text Text), postgresqlPassword :: !(Maybe FilePathSecrets) } From fb9ce89dc1679f5ca33e95d3b644128803bb7a4c Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 6 Oct 2025 12:45:35 +0000 Subject: [PATCH 34/40] removed todos --- services/background-worker/test/Test/Wire/Util.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/background-worker/test/Test/Wire/Util.hs b/services/background-worker/test/Test/Wire/Util.hs index 96623a8a07..5f69f93189 100644 --- a/services/background-worker/test/Test/Wire/Util.hs +++ b/services/background-worker/test/Test/Wire/Util.hs @@ -16,7 +16,7 @@ testEnv :: IO Env testEnv = do http2Manager <- initHttp2Manager logger <- Logger.new Logger.defSettings - let cassandra = undefined -- TODO + let cassandra = undefined statuses <- newIORef mempty backendNotificationMetrics <- mkBackendNotificationMetrics workerRunningGauge <- mkWorkerRunningGauge @@ -32,8 +32,8 @@ testEnv = do jobTimeout = toRange (Proxy @100), maxAttempts = toRange (Proxy @3) } - hasqlPool = undefined -- TODO - backgroundJobsQueue = undefined -- TODO + hasqlPool = undefined + backgroundJobsQueue = undefined pure Env {..} runTestAppT :: AppT IO a -> Int -> IO a From e967a416f1b631fe1097651221c90af515bed455 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 6 Oct 2025 13:22:26 +0000 Subject: [PATCH 35/40] configure brig cassandra client for background worker --- charts/background-worker/README.md | 16 +++++++++++++- .../background-worker/templates/_helpers.tpl | 22 +++++++++++++------ .../templates/cassandra-secret.yaml | 19 ++++++++++++++-- .../templates/configmap.yaml | 13 +++++++++-- .../templates/deployment.yaml | 21 +++++++++++++----- charts/background-worker/values.yaml | 3 +++ hack/helm_vars/wire-server/values.yaml.gotmpl | 10 +++++++++ .../background-worker.integration.yaml | 6 +++++ .../src/Wire/BackgroundWorker/Env.hs | 6 +++-- .../Wire/BackgroundWorker/Jobs/Registry.hs | 2 +- .../src/Wire/BackgroundWorker/Options.hs | 1 + .../src/Wire/DeadUserNotificationWatcher.hs | 2 +- .../Wire/BackendNotificationPusherSpec.hs | 6 +++-- .../background-worker/test/Test/Wire/Util.hs | 3 ++- 14 files changed, 105 insertions(+), 25 deletions(-) diff --git a/charts/background-worker/README.md b/charts/background-worker/README.md index 55e379a4ed..6ccc8b582f 100644 --- a/charts/background-worker/README.md +++ b/charts/background-worker/README.md @@ -1,5 +1,19 @@ -Note that background-worker depends on some provisioned storage, namely: +Note that background-worker depends on some provisioned storage/services, namely: - rabbitmq +- postgresql +- cassandra (two clusters) + +PostgreSQL configuration +- Set connection parameters under `config.postgresql` (libpq keywords: `host`, `port`, `user`, `dbname`, etc.). +- Provide the password via `secrets.pgPassword`; it is mounted at `/etc/wire/background-worker/secrets/pgPassword` and referenced from the configmap. + +Cassandra configuration +- Background-worker connects to two Cassandra clusters: + - `config.cassandra` (keyspace: `gundeck`) for the dead user notification watcher. + - `config.cassandraBrig` (keyspace: `brig`) for the user store. +- TLS may be configured via either a reference (`tlsCaSecretRef`) or inline CA (`tlsCa`) for each cluster. Secrets mount under: + - `/etc/wire/background-worker/cassandra-gundeck` + - `/etc/wire/background-worker/cassandra-brig` These are dealt with independently from this chart. diff --git a/charts/background-worker/templates/_helpers.tpl b/charts/background-worker/templates/_helpers.tpl index 96bf8dd1b8..8c09767491 100644 --- a/charts/background-worker/templates/_helpers.tpl +++ b/charts/background-worker/templates/_helpers.tpl @@ -8,18 +8,26 @@ {{- (semverCompare ">= 1.24-0" (include "kubeVersion" .)) -}} {{- end -}} -{{- define "useCassandraTLS" -}} +{{- define "useGundeckCassandraTLS" -}} {{ or (hasKey .cassandra "tlsCa") (hasKey .cassandra "tlsCaSecretRef") }} {{- end -}} -{{/* Return a Dict of TLS CA secret name and key -This is used to switch between provided secret (e.g. by cert-manager) and -created one (in case the CA is provided as PEM string.) -*/}} -{{- define "tlsSecretRef" -}} +{{- define "useBrigCassandraTLS" -}} +{{ or (hasKey .cassandraBrig "tlsCa") (hasKey .cassandraBrig "tlsCaSecretRef") }} +{{- end -}} + +{{- define "gundeckTlsSecretRef" -}} {{- if .cassandra.tlsCaSecretRef -}} {{ .cassandra.tlsCaSecretRef | toYaml }} {{- else }} -{{- dict "name" "background-worker-cassandra" "key" "ca.pem" | toYaml -}} +{{- dict "name" "background-worker-cassandra-gundeck" "key" "ca.pem" | toYaml -}} +{{- end -}} +{{- end -}} + +{{- define "brigTlsSecretRef" -}} +{{- if .cassandraBrig.tlsCaSecretRef -}} +{{ .cassandraBrig.tlsCaSecretRef | toYaml }} +{{- else }} +{{- dict "name" "background-worker-cassandra-brig" "key" "ca.pem" | toYaml -}} {{- end -}} {{- end -}} diff --git a/charts/background-worker/templates/cassandra-secret.yaml b/charts/background-worker/templates/cassandra-secret.yaml index d5d9c61dfc..03018c0228 100644 --- a/charts/background-worker/templates/cassandra-secret.yaml +++ b/charts/background-worker/templates/cassandra-secret.yaml @@ -1,9 +1,9 @@ -{{/* Secret for the provided Cassandra TLS CA. */}} +{{/* Secrets for provided Cassandra TLS CAs */}} {{- if not (empty .Values.config.cassandra.tlsCa) }} apiVersion: v1 kind: Secret metadata: - name: background-worker-cassandra + name: background-worker-cassandra-gundeck labels: app: background-worker chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} @@ -13,3 +13,18 @@ type: Opaque data: ca.pem: {{ .Values.config.cassandra.tlsCa | b64enc | quote }} {{- end }} +{{- if not (empty .Values.config.cassandraBrig.tlsCa) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: background-worker-cassandra-brig + labels: + app: background-worker + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: Opaque +data: + ca.pem: {{ .Values.config.cassandraBrig.tlsCa | b64enc | quote }} +{{- end }} diff --git a/charts/background-worker/templates/configmap.yaml b/charts/background-worker/templates/configmap.yaml index b7a0114665..0fc87fb351 100644 --- a/charts/background-worker/templates/configmap.yaml +++ b/charts/background-worker/templates/configmap.yaml @@ -26,8 +26,17 @@ data: host: {{ .cassandra.host }} port: 9042 keyspace: gundeck - {{- if eq (include "useCassandraTLS" .) "true" }} - tlsCa: /etc/wire/background-worker/cassandra/{{- (include "tlsSecretRef" . | fromYaml).key }} + {{- if eq (include "useGundeckCassandraTLS" .) "true" }} + tlsCa: /etc/wire/background-worker/cassandra-gundeck/{{- (include "gundeckTlsSecretRef" . | fromYaml).key }} + {{- end }} + + cassandraBrig: + endpoint: + host: {{ .cassandraBrig.host }} + port: 9042 + keyspace: brig + {{- if eq (include "useBrigCassandraTLS" .) "true" }} + tlsCa: /etc/wire/background-worker/cassandra-brig/{{- (include "brigTlsSecretRef" . | fromYaml).key }} {{- end }} {{- with .rabbitmq }} diff --git a/charts/background-worker/templates/deployment.yaml b/charts/background-worker/templates/deployment.yaml index da77d57b76..75c6bd6a65 100644 --- a/charts/background-worker/templates/deployment.yaml +++ b/charts/background-worker/templates/deployment.yaml @@ -39,10 +39,15 @@ spec: - name: "background-worker-secrets" secret: secretName: "background-worker" - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} - - name: "background-worker-cassandra" + {{- if eq (include "useGundeckCassandraTLS" .Values.config) "true" }} + - name: "background-worker-cassandra-gundeck" secret: - secretName: {{ (include "tlsSecretRef" .Values.config | fromYaml).name }} + secretName: {{ (include "gundeckTlsSecretRef" .Values.config | fromYaml).name }} + {{- end }} + {{- if eq (include "useBrigCassandraTLS" .Values.config) "true" }} + - name: "background-worker-cassandra-brig" + secret: + secretName: {{ (include "brigTlsSecretRef" .Values.config | fromYaml).name }} {{- end }} {{- if .Values.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" @@ -62,9 +67,13 @@ spec: mountPath: "/etc/wire/background-worker/secrets" - name: "background-worker-config" mountPath: "/etc/wire/background-worker/conf" - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} - - name: "background-worker-cassandra" - mountPath: "/etc/wire/background-worker/cassandra" + {{- if eq (include "useGundeckCassandraTLS" .Values.config) "true" }} + - name: "background-worker-cassandra-gundeck" + mountPath: "/etc/wire/background-worker/cassandra-gundeck" + {{- end }} + {{- if eq (include "useBrigCassandraTLS" .Values.config) "true" }} + - name: "background-worker-cassandra-brig" + mountPath: "/etc/wire/background-worker/cassandra-brig" {{- end }} {{- if .Values.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" diff --git a/charts/background-worker/values.yaml b/charts/background-worker/values.yaml index f9100f681c..0abef0f43b 100644 --- a/charts/background-worker/values.yaml +++ b/charts/background-worker/values.yaml @@ -40,8 +40,11 @@ config: # tlsCaSecretRef: # name: # key: + # Cassandra clusters used by background-worker cassandra: host: aws-cassandra + cassandraBrig: + host: aws-cassandra backendNotificationPusher: pushBackoffMinWait: 10000 # in microseconds, so 10ms diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 8db38a8f52..13eb08989a 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -58,6 +58,7 @@ brig: teamCreatorWelcome: https://teams.wire.com/login teamMemberWelcome: https://wire.com/download accountPages: https://account.wire.com + # Background-worker uses Brig's Cassandra keyspace. cassandra: host: {{ .Values.cassandraHost }} replicaCount: 1 @@ -607,6 +608,7 @@ background-worker: concurrency: 8 jobTimeout: 60 maxAttempts: 3 + # Cassandra clusters used by background-worker cassandra: host: {{ .Values.cassandraHost }} replicaCount: 1 @@ -615,6 +617,14 @@ background-worker: name: "cassandra-jks-keystore" key: "ca.crt" {{- end }} + cassandraBrig: + host: {{ .Values.cassandraHost }} + replicaCount: 1 + {{- if .Values.useK8ssandraSSL.enabled }} + tlsCaSecretRef: + name: "cassandra-jks-keystore" + key: "ca.crt" + {{- end }} rabbitmq: port: 5671 adminPort: 15671 diff --git a/services/background-worker/background-worker.integration.yaml b/services/background-worker/background-worker.integration.yaml index b072498809..55e76688b0 100644 --- a/services/background-worker/background-worker.integration.yaml +++ b/services/background-worker/background-worker.integration.yaml @@ -14,6 +14,12 @@ cassandra: port: 9042 keyspace: gundeck_test +cassandraBrig: + endpoint: + host: 127.0.0.1 + port: 9042 + keyspace: brig_test + rabbitmq: host: 127.0.0.1 port: 5671 diff --git a/services/background-worker/src/Wire/BackgroundWorker/Env.hs b/services/background-worker/src/Wire/BackgroundWorker/Env.hs index 5c488217d8..753d0e25fa 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Env.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Env.hs @@ -55,7 +55,8 @@ data Env = Env backgroundJobsConfig :: BackgroundJobsConfig, workerRunningGauge :: Vector Text Gauge, statuses :: IORef (Map Worker IsWorking), - cassandra :: ClientState, + gundeckCassandra :: ClientState, + brigCassandra :: ClientState, hasqlPool :: HasqlPool.Pool, backgroundJobsQueue :: MVar Q.Channel } @@ -80,7 +81,8 @@ mkWorkerRunningGauge = mkEnv :: Opts -> IO Env mkEnv opts = do logger <- Log.mkLogger opts.logLevel Nothing opts.logFormat - cassandra <- defInitCassandra opts.cassandra logger + gundeckCassandra <- defInitCassandra opts.cassandra logger + brigCassandra <- defInitCassandra opts.cassandraBrig logger http2Manager <- initHttp2Manager httpManager <- newManager defaultManagerSettings let federatorInternal = opts.federatorInternal diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs index 469076681c..b3a8f931b9 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs @@ -28,7 +28,7 @@ dispatchJob job = do . mapError @BackgroundJobError (T.pack . show) . mapError @UsageError (T.pack . show) . runInputConst @Pool env.hasqlPool - . interpretUserStoreCassandra env.cassandra + . interpretUserStoreCassandra env.brigCassandra . interpretUserGroupStoreToPostgres . runInputSem (readMVar env.backgroundJobsQueue) . interpretBackgroundJobsPublisherRabbitMQ job.requestId diff --git a/services/background-worker/src/Wire/BackgroundWorker/Options.hs b/services/background-worker/src/Wire/BackgroundWorker/Options.hs index 55ed162f9c..14f5eb94b4 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Options.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Options.hs @@ -18,6 +18,7 @@ data Opts = Opts defederationTimeout :: Maybe Int, backendNotificationPusher :: BackendNotificationsConfig, cassandra :: CassandraOpts, + cassandraBrig :: CassandraOpts, backgroundJobs :: BackgroundJobsConfig, -- | Postgresql settings, the key values must be in libpq format. -- https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS diff --git a/services/background-worker/src/Wire/DeadUserNotificationWatcher.hs b/services/background-worker/src/Wire/DeadUserNotificationWatcher.hs index 802f89eda6..d4bb3c1d0a 100644 --- a/services/background-worker/src/Wire/DeadUserNotificationWatcher.hs +++ b/services/background-worker/src/Wire/DeadUserNotificationWatcher.hs @@ -34,7 +34,7 @@ startConsumer chan = do env <- ask markAsWorking DeadUserNotificationWatcher - cassandra <- asks (.cassandra) + cassandra <- asks (.gundeckCassandra) void . lift $ Q.declareQueue diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs index aeeca284b9..639f0e2126 100644 --- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs +++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs @@ -322,7 +322,8 @@ spec = do ] logger <- Logger.new Logger.defSettings httpManager <- newManager defaultManagerSettings - let cassandra = undefined + let gundeckCassandra = undefined + brigCassandra = undefined let federatorInternal = Endpoint "localhost" 8097 http2Manager = undefined statuses = undefined @@ -348,9 +349,10 @@ spec = do it "should retry fetching domains if a request fails" $ do mockAdmin <- newMockRabbitMqAdmin True ["backend-notifications.foo.example"] logger <- Logger.new Logger.defSettings - let cassandra = undefined httpManager <- newManager defaultManagerSettings let federatorInternal = Endpoint "localhost" 8097 + gundeckCassandra = undefined + brigCassandra = undefined http2Manager = undefined statuses = undefined rabbitmqAdminClient = Just $ mockRabbitMqAdminClient mockAdmin diff --git a/services/background-worker/test/Test/Wire/Util.hs b/services/background-worker/test/Test/Wire/Util.hs index 5f69f93189..0fc43f503e 100644 --- a/services/background-worker/test/Test/Wire/Util.hs +++ b/services/background-worker/test/Test/Wire/Util.hs @@ -16,7 +16,8 @@ testEnv :: IO Env testEnv = do http2Manager <- initHttp2Manager logger <- Logger.new Logger.defSettings - let cassandra = undefined + let gundeckCassandra = undefined + brigCassandra = undefined statuses <- newIORef mempty backendNotificationMetrics <- mkBackendNotificationMetrics workerRunningGauge <- mkWorkerRunningGauge From b040bda44f70173d0c4b652c18559418ce157343 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 6 Oct 2025 14:46:18 +0000 Subject: [PATCH 36/40] fix testTemporaryQueuesAreDeletedAfterUse test --- integration/test/Test/Events.hs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/integration/test/Test/Events.hs b/integration/test/Test/Events.hs index 5eaabf5354..40782657bd 100644 --- a/integration/test/Test/Events.hs +++ b/integration/test/Test/Events.hs @@ -157,11 +157,12 @@ testTemporaryQueuesAreDeletedAfterUse = do aliceClientQueue = Queue {name = fromString aliceClientQueueName, vhost = fromString beResource.berVHost} deadNotifsQueue = Queue {name = fromString "dead-user-notifications", vhost = fromString beResource.berVHost} cellsEventsQueue = Queue {name = fromString "cells_events", vhost = fromString beResource.berVHost} + backgroundJobsQueue = Queue {name = fromString "background-jobs", vhost = fromString beResource.berVHost} -- Wait for queue for the new client to be created eventually $ do queuesBeforeWS <- rabbitmqAdmin.listQueuesByVHost (fromString beResource.berVHost) (fromString "") True 100 1 - queuesBeforeWS.items `shouldMatchSet` [deadNotifsQueue, cellsEventsQueue, aliceClientQueue] + queuesBeforeWS.items `shouldMatchSet` [deadNotifsQueue, cellsEventsQueue, aliceClientQueue, backgroundJobsQueue] runCodensity (createEventsWebSocket alice Nothing) $ \ws -> do handle <- randomHandle @@ -169,7 +170,7 @@ testTemporaryQueuesAreDeletedAfterUse = do queuesDuringWS <- rabbitmqAdmin.listQueuesByVHost (fromString beResource.berVHost) (fromString "") True 100 1 addJSONToFailureContext "queuesDuringWS" queuesDuringWS $ do - length queuesDuringWS.items `shouldMatchInt` 4 + length queuesDuringWS.items `shouldMatchInt` 5 -- We cannot use 'assertEvent' here because there is a race between the temp -- queue being created and rabbitmq fanning out the previous events. @@ -183,7 +184,7 @@ testTemporaryQueuesAreDeletedAfterUse = do eventually $ do queuesAfterWS <- rabbitmqAdmin.listQueuesByVHost (fromString beResource.berVHost) (fromString "") True 100 1 - queuesAfterWS.items `shouldMatchSet` [deadNotifsQueue, cellsEventsQueue, aliceClientQueue] + queuesAfterWS.items `shouldMatchSet` [deadNotifsQueue, cellsEventsQueue, aliceClientQueue, backgroundJobsQueue] testSendMessageNoReturnToSenderWithConsumableNotificationsProteus :: (HasCallStack) => App () testSendMessageNoReturnToSenderWithConsumableNotificationsProteus = do From 583b8789055aba24b2857b2a3dd157729dc16402 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 6 Oct 2025 15:33:01 +0000 Subject: [PATCH 37/40] wip: debugging bg jobs --- integration/test/Test/UserGroup.hs | 4 ++-- .../src/Wire/BackgroundJobsRunner/Interpreter.hs | 6 +++++- .../src/Wire/BackgroundWorker/Jobs/Consumer.hs | 14 ++++++++++---- .../src/Wire/BackgroundWorker/Jobs/Registry.hs | 2 ++ 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/integration/test/Test/UserGroup.hs b/integration/test/Test/UserGroup.hs index 6764c8f60e..4c0a2cb79c 100644 --- a/integration/test/Test/UserGroup.hs +++ b/integration/test/Test/UserGroup.hs @@ -374,8 +374,8 @@ testUserGroupMembersCount = do resp.json %. "page.0.membersCount" `shouldMatchInt` 2 resp.json %. "total" `shouldMatchInt` 1 -testUserGroupUpdateChannels :: (HasCallStack) => App () -testUserGroupUpdateChannels = do +testUserGroupUpdateChannelsSucceeds :: (HasCallStack) => App () +testUserGroupUpdateChannelsSucceeds = do (alice, tid, [_bob]) <- createTeam OwnDomain 2 ug <- diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs index 21d420cc14..4dab7d4590 100644 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs @@ -2,6 +2,7 @@ module Wire.BackgroundJobsRunner.Interpreter where import Data.Id import Data.Qualified +import Debug.Trace import Imports import Polysemy import Polysemy.Error @@ -42,7 +43,9 @@ runJob job = case job.payload of JobSyncUserGroup payload -> runSyncUserGroup job.jobId job.requestId payload runSyncUserGroupAndChannel :: JobId -> RequestId -> SyncUserGroupAndChannel -> Sem r () -runSyncUserGroupAndChannel _jid _rid _job = pure () +runSyncUserGroupAndChannel _jid _rid _job = do + traceM "Running SyncUserGroupAndChannel job" + pure () runSyncUserGroup :: ( Member UserGroupStore r, @@ -56,6 +59,7 @@ runSyncUserGroup :: SyncUserGroup -> Sem r () runSyncUserGroup _jid _rid job = do + traceM "======================== Running SyncUserGroup job =======================" teamId <- getUserTeam job.actor >>= note BackgroundJobUserTeamNotFound channels <- fromMaybe mempty . (foldMap (runIdentity . (.channels))) <$> getUserGroup teamId job.userGroupId for_ channels $ \chan -> do diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs index ced82d055b..4130f299c6 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs @@ -61,11 +61,17 @@ startWorker rabbitmqOpts = do Q.qos chan 0 (fromIntegral $ fromRange cfg.concurrency) False -- set gauges setGauge metrics.concurrencyConfigured (fromIntegral $ fromRange cfg.concurrency) - -- start consuming with manual ack + -- start consuming with manual ack and keep the channel alive void $ QL.consumeMsgs chan backgroundJobsQueueName Q.Ack (void . runAppT env . handleDelivery metrics cfg) - runAppT env $ markAsWorking BackgroundJobConsumer, - onChannelException = \_ -> runAppT env $ markAsNotWorking BackgroundJobConsumer, - onConnectionClose = runAppT env $ markAsNotWorking BackgroundJobConsumer + runAppT env $ markAsWorking BackgroundJobConsumer + forever $ threadDelay maxBound, + onChannelException = \_ -> do + -- mark not working; TODO: only log unexpected exceptions + runAppT env $ markAsNotWorking BackgroundJobConsumer, + onConnectionClose = + runAppT env $ do + markAsNotWorking BackgroundJobConsumer + Log.info $ Log.msg (Log.val "RabbitMQ connection closed for background job consumer") } pure $ runAppT env $ cleanup where diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs index b3a8f931b9..2c90824fcd 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs @@ -4,6 +4,7 @@ module Wire.BackgroundWorker.Jobs.Registry where import Data.Text qualified as T +import Debug.Trace (traceM) import Hasql.Pool (Pool, UsageError) import Imports import Polysemy @@ -20,6 +21,7 @@ import Wire.UserStore.Cassandra (interpretUserStoreCassandra) dispatchJob :: Job -> AppT IO (Either Text ()) dispatchJob job = do env <- ask @Env + traceM $ "======================== Dispatching job " <> show job.jobId <> " ========================" liftIO $ runInterpreters env $ runJob job where runInterpreters env = From e702d0dbde2b87e1ffe1c04f90a0ea889ed190f3 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Mon, 6 Oct 2025 19:14:42 +0200 Subject: [PATCH 38/40] fix: backgroung timeout second translation --- .../src/Wire/BackgroundWorker/Jobs/Consumer.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs index 4130f299c6..4ab007cca8 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Consumer.hs @@ -113,7 +113,7 @@ handleDelivery metrics cfg (msg, env) = do (dur, r) <- duration $ fromMaybe (Left "job timeout") - <$> timeout (fromIntegral $ fromIntegral (fromRange cfg.jobTimeout) #> Second) (dispatchJob job) + <$> timeout (fromRange cfg.jobTimeout * 1000000) (dispatchJob job) withLabel metrics.jobDuration lbl (`observe` dur) pure r where From cc0794403883f29c207bf601dd2a3cd4096491a4 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Mon, 6 Oct 2025 19:23:24 +0200 Subject: [PATCH 39/40] fix: integration tests background-worker postgresql params --- .../background-worker/background-worker.integration.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/background-worker/background-worker.integration.yaml b/services/background-worker/background-worker.integration.yaml index 55e76688b0..8add630a58 100644 --- a/services/background-worker/background-worker.integration.yaml +++ b/services/background-worker/background-worker.integration.yaml @@ -42,7 +42,8 @@ backgroundJobs: maxAttempts: 3 postgresql: - host: "postgresql" + host: 127.0.0.1 port: "5432" user: wire-server - dbname: wire-server + dbname: backendA + password: posty-the-gres From aad7bca18e41c6188b4f3cbf354eb6eddb294585 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 7 Oct 2025 08:10:14 +0000 Subject: [PATCH 40/40] fix fetching the channels for a group --- integration/test/Test/UserGroup.hs | 10 ++++++---- .../Wire/BackgroundJobsRunner/Interpreter.hs | 20 +++++++++---------- .../Wire/BackgroundWorker/Jobs/Registry.hs | 2 -- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/integration/test/Test/UserGroup.hs b/integration/test/Test/UserGroup.hs index 4c0a2cb79c..340b8a65c6 100644 --- a/integration/test/Test/UserGroup.hs +++ b/integration/test/Test/UserGroup.hs @@ -383,16 +383,18 @@ testUserGroupUpdateChannelsSucceeds = do >>= getJSON 200 gid <- ug %. "id" & asString - convId <- - postConversation alice (defProteus {team = Just tid}) + convIds <- + replicateM 2 + $ postConversation alice (defProteus {team = Just tid}) >>= getJSON 201 >>= objConvId - updateUserGroupChannels alice gid [convId.id_] >>= assertSuccess + updateUserGroupChannels alice gid ((.id_) <$> convIds) >>= assertSuccess -- bobId <- asString $ bob %. "id" bindResponse (getUserGroupWithChannels alice gid) $ \resp -> do resp.status `shouldMatchInt` 200 - resp.json %. "channels" `shouldMatch` [object ["id" .= convId.id_, "domain" .= convId.domain]] + actual <- resp.json %. "channels" >>= asList >>= traverse objQid + actual `shouldMatchSet` for convIds objQid -- FUTUREWORK: check the actual associated channels -- resp.json %. "members" `shouldMatch` [bobId] diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs index 4dab7d4590..340605f4be 100644 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs @@ -1,16 +1,13 @@ module Wire.BackgroundJobsRunner.Interpreter where import Data.Id -import Data.Qualified -import Debug.Trace import Imports import Polysemy import Polysemy.Error import Wire.API.BackgroundJobs -import Wire.API.UserGroup import Wire.BackgroundJobsPublisher import Wire.BackgroundJobsRunner (BackgroundJobsRunner (..)) -import Wire.UserGroupStore (UserGroupStore, getUserGroup) +import Wire.UserGroupStore (UserGroupStore, listUserGroupChannels) import Wire.UserStore data BackgroundJobError = BackgroundJobUserTeamNotFound @@ -44,7 +41,6 @@ runJob job = case job.payload of runSyncUserGroupAndChannel :: JobId -> RequestId -> SyncUserGroupAndChannel -> Sem r () runSyncUserGroupAndChannel _jid _rid _job = do - traceM "Running SyncUserGroupAndChannel job" pure () runSyncUserGroup :: @@ -59,10 +55,14 @@ runSyncUserGroup :: SyncUserGroup -> Sem r () runSyncUserGroup _jid _rid job = do - traceM "======================== Running SyncUserGroup job =======================" - teamId <- getUserTeam job.actor >>= note BackgroundJobUserTeamNotFound - channels <- fromMaybe mempty . (foldMap (runIdentity . (.channels))) <$> getUserGroup teamId job.userGroupId + void $ getUserTeam job.actor >>= note BackgroundJobUserTeamNotFound + channels <- listUserGroupChannels job.userGroupId for_ channels $ \chan -> do - let job' = SyncUserGroupAndChannel job.userGroupId (qUnqualified chan) job.actor + let syncUserGroupAndChannel = + SyncUserGroupAndChannel + { userGroupId = job.userGroupId, + convId = chan, + actor = job.actor + } jobId <- liftIO randomId - publishJob jobId (JobSyncUserGroupAndChannel job') + publishJob jobId (JobSyncUserGroupAndChannel syncUserGroupAndChannel) diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs index 2c90824fcd..b3a8f931b9 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs @@ -4,7 +4,6 @@ module Wire.BackgroundWorker.Jobs.Registry where import Data.Text qualified as T -import Debug.Trace (traceM) import Hasql.Pool (Pool, UsageError) import Imports import Polysemy @@ -21,7 +20,6 @@ import Wire.UserStore.Cassandra (interpretUserStoreCassandra) dispatchJob :: Job -> AppT IO (Either Text ()) dispatchJob job = do env <- ask @Env - traceM $ "======================== Dispatching job " <> show job.jobId <> " ========================" liftIO $ runInterpreters env $ runJob job where runInterpreters env =