Skip to content

Commit adc6977

Browse files
Do not deliver client specific notifiations to temporary clients (#4703)
* gundeck: Never push a notification directly to temp clients Also: Only push a notification to RabbitMQ when the user has any clients which support consumable notifications. This way RabbitMQ wouldn't think there are notifications meant for no queues. * Delete unnecessary constructor RecipientClientsTemporaryOnly * changelog * Apply suggestions from code review Co-authored-by: Sven Tennie <[email protected]> --------- Co-authored-by: Sven Tennie <[email protected]>
1 parent 0a1133c commit adc6977

File tree

6 files changed

+61
-157
lines changed

6 files changed

+61
-157
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Do not deliver client specific notifications to temporary clients.

integration/test/Test/Events.hs

Lines changed: 22 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import Data.ProtoLens.Labels ()
2121
import Data.Proxy (Proxy (..))
2222
import qualified Data.Text as Text
2323
import Data.Timeout
24-
import MLS.Util
2524
import Network.AMQP.Extended
2625
import Network.RabbitMqAdmin
2726
import qualified Network.WebSockets as WS
@@ -142,43 +141,35 @@ testConsumeTempEvents = do
142141

143142
ackEvent ws e
144143

145-
testConsumeTempEventsWithoutOwnClient :: (HasCallStack) => App ()
146-
testConsumeTempEventsWithoutOwnClient = do
147-
[alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain]
148-
149-
runCodensity (createEventsWebSocket alice Nothing) $ \ws -> do
150-
handle <- randomHandle
151-
putHandle bob handle >>= assertSuccess
152-
153-
-- We cannot use 'assertEvent' here because there is a race between the temp
154-
-- queue being created and rabbitmq fanning out the previous events.
155-
void $ assertFindsEvent ws $ \e -> do
156-
e %. "type" `shouldMatch` "event"
157-
e %. "data.event.payload.0.type" `shouldMatch` "user.update"
158-
e %. "data.event.payload.0.user.id" `shouldMatch` objId bob
159-
e %. "data.event.payload.0.user.handle" `shouldMatch` handle
160-
161-
ackEvent ws e
162-
163144
testTemporaryQueuesAreDeletedAfterUse :: (HasCallStack) => App ()
164145
testTemporaryQueuesAreDeletedAfterUse = do
165146
startDynamicBackendsReturnResources [def] $ \[beResource] -> do
166147
let domain = beResource.berDomain
167148
rabbitmqAdmin <- mkRabbitMqAdminClientForResource beResource
168-
queuesBeforeWS <- rabbitmqAdmin.listQueuesByVHost (fromString beResource.berVHost) (fromString "") True 100 1
169-
let deadNotifsQueue = Queue {name = fromString "dead-user-notifications", vhost = fromString beResource.berVHost}
149+
[alice, bob] <- createAndConnectUsers [domain, domain]
150+
151+
-- Create client for alice, so their temp websocket works
152+
aliceClient <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201
153+
aliceId <- asString $ alice %. "qualified_id.id"
154+
aliceClientId <- asString $ aliceClient %. "id"
155+
156+
let aliceClientQueueName = "user-notifications." <> aliceId <> "." <> aliceClientId
157+
aliceClientQueue = Queue {name = fromString aliceClientQueueName, vhost = fromString beResource.berVHost}
158+
deadNotifsQueue = Queue {name = fromString "dead-user-notifications", vhost = fromString beResource.berVHost}
170159
cellsEventsQueue = Queue {name = fromString "cells_events", vhost = fromString beResource.berVHost}
171-
queuesBeforeWS.items `shouldMatchSet` [deadNotifsQueue, cellsEventsQueue]
172160

173-
[alice, bob] <- createAndConnectUsers [domain, domain]
161+
-- Wait for queue for the new client to be created
162+
eventually $ do
163+
queuesBeforeWS <- rabbitmqAdmin.listQueuesByVHost (fromString beResource.berVHost) (fromString "") True 100 1
164+
queuesBeforeWS.items `shouldMatchSet` [deadNotifsQueue, cellsEventsQueue, aliceClientQueue]
174165

175166
runCodensity (createEventsWebSocket alice Nothing) $ \ws -> do
176167
handle <- randomHandle
177168
putHandle bob handle >>= assertSuccess
178169

179170
queuesDuringWS <- rabbitmqAdmin.listQueuesByVHost (fromString beResource.berVHost) (fromString "") True 100 1
180171
addJSONToFailureContext "queuesDuringWS" queuesDuringWS $ do
181-
length queuesDuringWS.items `shouldMatchInt` 3
172+
length queuesDuringWS.items `shouldMatchInt` 4
182173

183174
-- We cannot use 'assertEvent' here because there is a race between the temp
184175
-- queue being created and rabbitmq fanning out the previous events.
@@ -190,49 +181,9 @@ testTemporaryQueuesAreDeletedAfterUse = do
190181

191182
ackEvent ws e
192183

193-
-- Use let binding here so 'shouldMatchEventually' retries the whole request
194-
let queuesAfterWSM = rabbitmqAdmin.listQueuesByVHost (fromString beResource.berVHost) (fromString "") True 100 1
195-
eventually $ fmap (.items) queuesAfterWSM `shouldMatchSet` [deadNotifsQueue, cellsEventsQueue]
196-
197-
testMLSTempEvents :: (HasCallStack) => App ()
198-
testMLSTempEvents = do
199-
[alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain]
200-
clients@[alice1, _, _] <-
201-
traverse
202-
( createMLSClient
203-
def
204-
{ clientArgs =
205-
def
206-
{ acapabilities = Just ["consumable-notifications"]
207-
}
208-
}
209-
)
210-
[alice, bob, bob]
211-
212-
traverse_ (uploadNewKeyPackage def) clients
213-
convId <- createNewGroup def alice1
214-
215-
runCodensity (createEventsWebSocket bob Nothing) $ \ws -> do
216-
commit <- createAddCommit alice1 convId [bob]
217-
void $ postMLSCommitBundle commit.sender (mkBundle commit) >>= getJSON 201
218-
219-
-- FUTUREWORK: we should not rely on events arriving in this particular order
220-
221-
-- We cannot use 'assertEvent' here because there is a race between the temp
222-
-- queue being created and rabbitmq fanning out the previous events.
223-
void $ assertFindsEvent ws $ \e -> do
224-
e %. "type" `shouldMatch` "event"
225-
e %. "data.event.payload.0.type" `shouldMatch` "conversation.member-join"
226-
user <- assertOne =<< (e %. "data.event.payload.0.data.users" & asList)
227-
user %. "qualified_id" `shouldMatch` (bob %. "qualified_id")
228-
ackEvent ws e
229-
230-
void $ assertEvent ws $ \e -> do
231-
e %. "type" `shouldMatch` "event"
232-
e %. "data.event.payload.0.type" `shouldMatch` "conversation.mls-welcome"
233-
ackEvent ws e
234-
235-
assertNoEvent_ ws
184+
eventually $ do
185+
queuesAfterWS <- rabbitmqAdmin.listQueuesByVHost (fromString beResource.berVHost) (fromString "") True 100 1
186+
queuesAfterWS.items `shouldMatchSet` [deadNotifsQueue, cellsEventsQueue, aliceClientQueue]
236187

237188
testSendMessageNoReturnToSenderWithConsumableNotificationsProteus :: (HasCallStack) => App ()
238189
testSendMessageNoReturnToSenderWithConsumableNotificationsProteus = do
@@ -280,7 +231,7 @@ testEventsForSpecificClients = do
280231
ws1 <- createEventsWebSocket alice (Just cid1)
281232
wsTemp <- createEventsWebSocket alice Nothing
282233
lift $ do
283-
forM_ [ws1, wsTemp] consumeAllEvents
234+
void $ consumeAllEvents ws1
284235

285236
let eventForClient1 =
286237
object
@@ -297,17 +248,12 @@ testEventsForSpecificClients = do
297248
assertEvent ws1 $ \e ->
298249
e %. "data.event.payload.0.hello" `shouldMatch` "client1"
299250

300-
assertEvent wsTemp $ \e -> do
301-
e %. "data.event.payload.0.hello" `shouldMatch` "client1"
302-
ackEvent wsTemp e
303-
304-
assertEvent wsTemp $ \e -> do
305-
e %. "data.event.payload.0.hello" `shouldMatch` "client2"
306-
ackEvent wsTemp e
307-
308251
addFailureContext "client 1 should not get any events meant for client 2"
309252
$ assertNoEvent_ ws1
310253

254+
addFailureContext "temp client should not get any events meant solely for client 1 or 2"
255+
$ assertNoEvent_ wsTemp
256+
311257
testConsumeEventsForDifferentUsers :: (HasCallStack) => App ()
312258
testConsumeEventsForDifferentUsers = do
313259
alice <- randomUser OwnDomain def

libs/wire-api/src/Wire/API/Push/V2.hs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -112,20 +112,9 @@ data RecipientClients
112112
RecipientClientsAll
113113
| -- | An explicit list of clients
114114
RecipientClientsSome (List1 ClientId)
115-
| -- | Only temporary clients receive these events.
116-
-- It not supposed to be used by consumers of gundeck, only here for
117-
-- internal gundeck use.
118-
--
119-
-- This is a little hack to make the 'splitPush' function work properly so
120-
-- that temporary clients receive notifications over rabbitmq even when there
121-
-- are no real clients which support consumable-notifications. This should
122-
-- go away when we drop support for Cassandra.
123-
RecipientClientsTemporaryOnly
124115
deriving (Eq, Show, Ord, Generic)
125116
deriving (FromJSON, ToJSON, S.ToSchema) via (Schema RecipientClients)
126117

127-
-- | This doesn't produce the 'RecipientClientsTemporaryOnly' case becasue we
128-
-- don't expect it to come from outside gundeck.
129118
instance Arbitrary RecipientClients where
130119
arbitrary =
131120
oneof [allClients, someClients]
@@ -136,15 +125,6 @@ instance Arbitrary RecipientClients where
136125
firstClientId <- arbitrary
137126
(List1.list1 firstClientId . filter (/= firstClientId) . Set.toList <$> setOf' arbitrary)
138127

139-
instance Semigroup RecipientClients where
140-
RecipientClientsAll <> _ = RecipientClientsAll
141-
_ <> RecipientClientsAll = RecipientClientsAll
142-
RecipientClientsSome cs1 <> RecipientClientsSome cs2 =
143-
RecipientClientsSome (cs1 <> cs2)
144-
RecipientClientsTemporaryOnly <> x =
145-
x
146-
x <> RecipientClientsTemporaryOnly = x
147-
148128
instance ToSchema Recipient where
149129
schema =
150130
object "Recipient" $
@@ -163,7 +143,6 @@ instance ToSchema RecipientClients where
163143
& (S.schema . S.description ?~ "List of clientIds. Empty means `all clients`.")
164144

165145
i :: A.Value -> A.Parser RecipientClients
166-
i (A.String "temp-only") = pure RecipientClientsTemporaryOnly
167146
i v =
168147
parseJSON @[ClientId] v >>= \case
169148
[] -> pure RecipientClientsAll
@@ -174,7 +153,6 @@ instance ToSchema RecipientClients where
174153
pure . \case
175154
RecipientClientsSome cs -> toJSON cs
176155
RecipientClientsAll -> A.Array mempty
177-
RecipientClientsTemporaryOnly -> A.String "temp-only"
178156

179157
makeLenses ''Recipient
180158

services/gundeck/src/Gundeck/Push.hs

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ import Util.Options
7474
import Wire.API.Internal.Notification
7575
import Wire.API.Notification
7676
import Wire.API.Presence (Presence (..))
77-
import Wire.API.Presence qualified as Presence
7877
import Wire.API.Push.Token qualified as Public
7978
import Wire.API.Push.V2
8079
import Wire.API.User (UserSet (..))
@@ -146,17 +145,19 @@ instance MonadMapAsync Gundeck where
146145
Nothing -> mapAsync f l
147146
Just chunkSize -> concat <$> mapM (mapAsync f) (List.chunksOf chunkSize l)
148147

149-
splitPushes :: (MonadPushAll m) => [Push] -> m ([Push], [Push])
148+
splitPushes :: (MonadPushAll m) => [Push] -> m ([Push], [Push], UserClientsFull)
150149
splitPushes ps = do
151150
allUserClients <- mpaGetClients (Set.unions $ map (\p -> Set.map (._recipientId) $ p._pushRecipients) ps)
152-
pure . partitionHereThere $ map (splitPush allUserClients) ps
151+
let (rabbitmqPushes, legacyPushes) = partitionHereThere $ map (splitPush allUserClients) ps
152+
pure (rabbitmqPushes, legacyPushes, allUserClients)
153153

154154
-- | Split a push into rabbitmq and legacy push. This code exists to help with
155155
-- migration. Once it is completed and old APIs are not supported anymore we can
156156
-- assume everything is meant for RabbitMQ and stop splitting.
157157
splitPush ::
158158
UserClientsFull ->
159159
Push ->
160+
-- | These rabbitmqPush cassandraPush
160161
These Push Push
161162
splitPush clientsFull p = do
162163
let (rabbitmqRecipients, legacyRecipients) =
@@ -177,7 +178,6 @@ splitPush clientsFull p = do
177178
RecipientClientsSome cs ->
178179
Set.filter (\c -> c.clientId `elem` toList cs) allClients
179180
RecipientClientsAll -> allClients
180-
RecipientClientsTemporaryOnly -> mempty
181181
(rabbitmqClients, legacyClients) = Set.partition supportsConsumableNotifications relevantClients
182182
rabbitmqClientIds = (.clientId) <$> Set.toList rabbitmqClients
183183
legacyClientIds = (.clientId) <$> Set.toList legacyClients
@@ -190,13 +190,16 @@ splitPush clientsFull p = do
190190
-- We return all clients for RabbitMQ even if there are no real
191191
-- clients so a temporary client can still read the notifications on
192192
-- RabbitMQ.
193-
(These rcpt {_recipientClients = RecipientClientsTemporaryOnly} rcpt)
193+
That rcpt
194194
(_, []) ->
195-
(This rcpt)
195+
This rcpt
196196
(r : rs, l : ls) ->
197-
These
198-
rcpt {_recipientClients = RecipientClientsSome $ list1 r rs}
199-
rcpt {_recipientClients = RecipientClientsSome $ list1 l ls}
197+
let rabbitMqRecipients = case rcpt._recipientClients of
198+
RecipientClientsAll -> RecipientClientsAll
199+
RecipientClientsSome _ -> RecipientClientsSome $ list1 r rs
200+
in These
201+
rcpt {_recipientClients = rabbitMqRecipients}
202+
rcpt {_recipientClients = RecipientClientsSome $ list1 l ls}
200203

201204
getClients :: Set UserId -> Gundeck UserClientsFull
202205
getClients uids = do
@@ -221,13 +224,13 @@ getClients uids = do
221224
pushAll :: (MonadPushAll m, MonadNativeTargets m, MonadMapAsync m, Log.MonadLogger m) => [Push] -> m ()
222225
pushAll pushes = do
223226
Log.debug $ msg (val "pushing") . Log.field "pushes" (Aeson.encode pushes)
224-
(rabbitmqPushes, legacyPushes) <- splitPushes pushes
227+
(rabbitmqPushes, legacyPushes, allUserClients) <- splitPushes pushes
225228

226229
legacyNotifs <- mapM mkNewNotification legacyPushes
227-
pushAllLegacy legacyNotifs
230+
pushAllLegacy legacyNotifs allUserClients
228231

229232
rabbitmqNotifs <- mapM mkNewNotification rabbitmqPushes
230-
pushAllViaRabbitMq rabbitmqNotifs
233+
pushAllViaRabbitMq rabbitmqNotifs allUserClients
231234

232235
-- Note that Cells needs all notifications because it doesn't matter whether
233236
-- some recipients have rabbitmq clients or not.
@@ -240,8 +243,8 @@ pushAll pushes = do
240243

241244
-- | Construct and send a single bulk push request to the client. Write the 'Notification's from
242245
-- the request to C*. Trigger native pushes for all delivery failures notifications.
243-
pushAllLegacy :: (MonadPushAll m, MonadNativeTargets m, MonadMapAsync m) => [NewNotification] -> m ()
244-
pushAllLegacy newNotifications = do
246+
pushAllLegacy :: (MonadPushAll m, MonadNativeTargets m, MonadMapAsync m) => [NewNotification] -> UserClientsFull -> m ()
247+
pushAllLegacy newNotifications userClientsFull = do
245248
-- persist push request
246249
let cassandraTargets :: [CassandraTargets]
247250
cassandraTargets = map mkCassandraTargets newNotifications
@@ -257,11 +260,14 @@ pushAllLegacy newNotifications = do
257260
wsTargets <- mapM mkWSTargets newNotifications
258261
resp <- compilePushResps wsTargets <$> mpaBulkPush (compilePushReq <$> wsTargets)
259262
-- native push
260-
forM_ resp $ \((notif :: Notification, psh :: Push), alreadySent :: [Presence]) ->
261-
pushNativeWithBudget notif psh alreadySent
262-
263-
pushNativeWithBudget :: (MonadMapAsync m, MonadPushAll m, MonadNativeTargets m) => Notification -> Push -> [Presence] -> m ()
264-
pushNativeWithBudget notif psh alreadySent = do
263+
forM_ resp $ \((notif :: Notification, psh :: Push), alreadySent :: [Presence]) -> do
264+
let alreadySentClients = Set.fromList $ mapMaybe (\p -> (p.userId,) <$> p.clientId) alreadySent
265+
rabbitmqClients = Map.map (Set.filter supportsConsumableNotifications) userClientsFull.userClientsFull
266+
rabbitmqClientIds = Map.foldMapWithKey (\uid clients -> Set.map (\c -> (uid, c.clientId)) clients) rabbitmqClients
267+
pushNativeWithBudget notif psh (Set.toList $ Set.union alreadySentClients rabbitmqClientIds)
268+
269+
pushNativeWithBudget :: (MonadMapAsync m, MonadPushAll m, MonadNativeTargets m) => Notification -> Push -> [(UserId, ClientId)] -> m ()
270+
pushNativeWithBudget notif psh dontPush = do
265271
perPushConcurrency <- mntgtPerPushConcurrency
266272
let rcps' = nativeTargetsRecipients psh
267273
cost = maybe (length rcps') (min (length rcps')) perPushConcurrency
@@ -272,14 +278,16 @@ pushNativeWithBudget notif psh alreadySent = do
272278
-- to cassandra and SNS are limited to 'perNativePushConcurrency' in parallel.
273279
unless (psh ^. pushTransient) $
274280
mpaRunWithBudget cost () $
275-
mpaPushNative notif (psh ^. pushNativePriority) =<< nativeTargets psh rcps' alreadySent
281+
mpaPushNative notif (psh ^. pushNativePriority) =<< nativeTargets psh rcps' dontPush
276282

277-
pushAllViaRabbitMq :: (MonadPushAll m, MonadMapAsync m, MonadNativeTargets m) => [NewNotification] -> m ()
278-
pushAllViaRabbitMq newNotifs = do
283+
pushAllViaRabbitMq :: (MonadPushAll m, MonadMapAsync m, MonadNativeTargets m) => [NewNotification] -> UserClientsFull -> m ()
284+
pushAllViaRabbitMq newNotifs userClientsFull = do
279285
for_ newNotifs $ pushViaRabbitMq
280286
mpaForkIO $ do
281-
for_ newNotifs $ \newNotif ->
282-
pushNativeWithBudget newNotif.nnNotification newNotif.nnPush []
287+
for_ newNotifs $ \newNotif -> do
288+
let cassandraClients = Map.map (Set.filter $ not . supportsConsumableNotifications) userClientsFull.userClientsFull
289+
cassandraClientIds = Map.foldMapWithKey (\uid clients -> Set.map (\c -> (uid, c.clientId)) clients) cassandraClients
290+
pushNativeWithBudget newNotif.nnNotification newNotif.nnPush (Set.toList $ cassandraClientIds)
283291

284292
pushViaRabbitMq :: (MonadPushAll m) => NewNotification -> m ()
285293
pushViaRabbitMq newNotif = do
@@ -291,11 +299,7 @@ pushViaRabbitMq newNotif = do
291299
RecipientClientsAll ->
292300
Set.singleton $ userRoutingKey r._recipientId
293301
RecipientClientsSome (toList -> cs) ->
294-
Set.fromList $
295-
temporaryRoutingKey r._recipientId
296-
: map (clientRoutingKey r._recipientId) cs
297-
RecipientClientsTemporaryOnly ->
298-
Set.singleton $ temporaryRoutingKey r._recipientId
302+
Set.fromList $ map (clientRoutingKey r._recipientId) cs
299303
for_ routingKeys $ \routingKey ->
300304
mpaPublishToRabbitMq userNotificationExchangeName routingKey qMsg
301305

@@ -382,7 +386,6 @@ mkCassandraTargets NewNotification {..} =
382386
-- clients are stored in cassandra as a list with a notification. empty list
383387
-- is interpreted as "all clients" by 'Gundeck.Notification.Data.toNotif'.
384388
RecipientClientsSome cs -> Just $ toList cs
385-
RecipientClientsTemporaryOnly -> Nothing
386389
pure $ target (r ^. recipientId) & targetClients .~ clients
387390

388391
-- | Information needed to push notifications over websockets and/or native
@@ -465,9 +468,9 @@ nativeTargets ::
465468
(MonadNativeTargets m, MonadMapAsync m) =>
466469
Push ->
467470
[Recipient] ->
468-
[Presence] ->
471+
[(UserId, ClientId)] ->
469472
m [Address]
470-
nativeTargets psh rcps' alreadySent =
473+
nativeTargets psh rcps' dontPush =
471474
mntgtMapAsync addresses rcps' >>= fmap concat . mapM check
472475
where
473476
addresses :: Recipient -> m [Address]
@@ -483,14 +486,9 @@ nativeTargets psh rcps' alreadySent =
483486
-- Is the client not whitelisted?
484487
| not (whitelistedOrNoWhitelist a) = False
485488
-- Include client if not found in already served presences.
486-
| otherwise = isNothing (List.find (isOnline a) alreadySent)
487-
isOnline a x =
488-
a ^. addrUser == Presence.userId x
489-
&& (a ^. addrConn == Presence.connId x || equalClient a x)
490-
equalClient a x = Just (a ^. addrClient) == Presence.clientId x
489+
| otherwise = not $ List.elem (a ^. addrUser, a ^. addrClient) dontPush -- (List.find (isOnline a) alreadySent)
491490
eligibleClient _ RecipientClientsAll = True
492491
eligibleClient a (RecipientClientsSome cs) = (a ^. addrClient) `elem` cs
493-
eligibleClient _ RecipientClientsTemporaryOnly = False
494492
whitelistedOrNoWhitelist a =
495493
null (psh ^. pushConnections)
496494
|| a ^. addrConn `elem` psh ^. pushConnections

0 commit comments

Comments
 (0)