@@ -37,7 +37,7 @@ instance (
37
37
initialState = DataSyncController
38
38
39
39
run = do
40
- setState DataSyncReady { subscriptions = HashMap. empty, transactions = HashMap. empty }
40
+ setState DataSyncReady { subscriptions = HashMap. empty, transactions = HashMap. empty, asyncs = [] }
41
41
42
42
ensureRLSEnabled <- makeCachedEnsureRLSEnabled
43
43
installTableChangeTriggers <- ChangeNotifications. makeCachedInstallTableChangeTriggers
@@ -56,6 +56,8 @@ instance (
56
56
sendJSON DataSyncResult { result, requestId }
57
57
58
58
handleMessage CreateDataSubscription { query, requestId } = do
59
+ ensureBelowSubscriptionsLimit
60
+
59
61
tableNameRLS <- ensureRLSEnabled (get # table query)
60
62
61
63
subscriptionId <- UUID. nextRandom
@@ -111,22 +113,23 @@ instance (
111
113
when isWatchingRecord do
112
114
sendJSON DidDelete { subscriptionId, id }
113
115
116
+ let subscribe = PGListener.subscribeJSON (ChangeNotifications.channelName tableNameRLS) callback pgListener
117
+ let unsubscribe subscription = PGListener.unsubscribe subscription pgListener
114
118
115
- channelSubscription <- pgListener
116
- |> PGListener.subscribeJSON (ChangeNotifications.channelName tableNameRLS) callback
119
+ Exception.bracket subscribe unsubscribe \channelSubscription -> do
120
+ close <- MVar.newEmptyMVar
121
+ modifyIORef' ?state (\state -> state |> modify #subscriptions (HashMap.insert subscriptionId close))
117
122
118
- modifyIORef' ?state (\state -> state |> modify #subscriptions (HashMap.insert subscriptionId Subscription { id = subscriptionId, channelSubscription }))
123
+ sendJSON DidCreateDataSubscription { subscriptionId, requestId, result }
119
124
120
- sendJSON DidCreateDataSubscription { subscriptionId, requestId, result }
125
+ MVar.takeMVar close
121
126
122
127
handleMessage DeleteDataSubscription { requestId, subscriptionId } = do
123
128
DataSyncReady { subscriptions } <- getState
124
- let maybeSubscription :: Maybe Subscription = HashMap.lookup subscriptionId subscriptions
129
+ let (Just closeSignalMVar) = HashMap.lookup subscriptionId subscriptions
125
130
126
131
-- Cancel table watcher
127
- case maybeSubscription of
128
- Just subscription -> pgListener |> PGListener.unsubscribe (get #channelSubscription subscription)
129
- Nothing -> pure ()
132
+ MVar.putMVar closeSignalMVar ()
130
133
131
134
modifyIORef' ?state (\state -> state |> modify #subscriptions (HashMap.delete subscriptionId))
132
135
@@ -260,35 +263,49 @@ instance (
260
263
ensureBelowTransactionLimit
261
264
262
265
transactionId <- UUID.nextRandom
263
-
264
- (connection, localPool) <- ?modelContext
265
- |> get #connectionPool
266
- |> Pool.takeResource
267
266
268
- let transaction = DataSyncTransaction
269
- { id = transactionId
270
- , connection
271
- , releaseConnection = Pool.putResource localPool connection
272
- }
273
267
274
- let globalModelContext = ?modelContext
275
- let ?modelContext = globalModelContext { transactionConnection = Just connection } in sqlExecWithRLS " BEGIN " ()
268
+ let takeConnection = ?modelContext
269
+ |> get #connectionPool
270
+ |> Pool.takeResource
271
+
272
+ let releaseConnection (connection, localPool) = do
273
+ PG.execute connection " ROLLBACK " () -- Make sure there's no pending transaction in case something went wrong
274
+ Pool.putResource localPool connection
275
+
276
+ Exception.bracket takeConnection releaseConnection \(connection, localPool) -> do
277
+ transactionSignal <- MVar.newEmptyMVar
278
+
279
+ let globalModelContext = ?modelContext
280
+ let ?modelContext = globalModelContext { transactionConnection = Just connection } in sqlExecWithRLS " BEGIN " ()
276
281
277
- modifyIORef' ?state (\state -> state |> modify #transactions (HashMap.insert transactionId transaction))
282
+ let transaction = DataSyncTransaction
283
+ { id = transactionId
284
+ , connection
285
+ , close = transactionSignal
286
+ }
278
287
279
- sendJSON DidStartTransaction { requestId, transactionId }
288
+ modifyIORef' ?state (\state -> state |> modify #transactions (HashMap.insert transactionId transaction))
289
+
290
+ sendJSON DidStartTransaction { requestId, transactionId }
291
+
292
+ MVar.takeMVar transactionSignal
293
+
294
+ modifyIORef' ?state (\state -> state |> modify #transactions (HashMap.delete transactionId))
280
295
281
296
handleMessage RollbackTransaction { requestId, id } = do
282
- sqlExecWithRLSAndTransactionId (Just id) " ROLLBACK " ()
297
+ DataSyncTransaction { id, close } <- findTransactionById id
283
298
284
- closeTransaction id
299
+ sqlExecWithRLSAndTransactionId (Just id) " ROLLBACK " ()
300
+ MVar.putMVar close ()
285
301
286
302
sendJSON DidRollbackTransaction { requestId, transactionId = id }
287
303
288
304
handleMessage CommitTransaction { requestId, id } = do
289
- sqlExecWithRLSAndTransactionId (Just id) " COMMIT " ()
305
+ DataSyncTransaction { id, close } <- findTransactionById id
290
306
291
- closeTransaction id
307
+ sqlExecWithRLSAndTransactionId (Just id) " COMMIT " ()
308
+ MVar.putMVar close ()
292
309
293
310
sendJSON DidCommitTransaction { requestId, transactionId = id }
294
311
@@ -301,22 +318,24 @@ instance (
301
318
Right decodedMessage -> do
302
319
let requestId = get #requestId decodedMessage
303
320
304
- -- Handle the messages in an async way
305
- -- This increases throughput as multiple queries can be fetched
306
- -- in parallel
307
- async do
308
- result <- Exception.try (handleMessage decodedMessage)
309
-
310
- case result of
311
- Left (e :: Exception.SomeException) -> do
312
- let errorMessage = case fromException e of
313
- Just (enhancedSqlError :: EnhancedSqlError) -> cs (get #sqlErrorMsg (get #sqlError enhancedSqlError))
314
- Nothing -> cs (displayException e)
315
- Log.error (tshow e)
316
- sendJSON DataSyncError { requestId, errorMessage }
317
- Right result -> pure ()
318
-
319
- pure ()
321
+ Exception.mask \r estore -> do
322
+ -- Handle the messages in an async way
323
+ -- This increases throughput as multiple queries can be fetched
324
+ -- in parallel
325
+ handlerProcess <- async $ restore do
326
+ result <- Exception.try (handleMessage decodedMessage)
327
+
328
+ case result of
329
+ Left (e :: Exception.SomeException) -> do
330
+ let errorMessage = case fromException e of
331
+ Just (enhancedSqlError :: EnhancedSqlError) -> cs (get #sqlErrorMsg (get #sqlError enhancedSqlError))
332
+ Nothing -> cs (displayException e)
333
+ Log.error (tshow e)
334
+ sendJSON DataSyncError { requestId, errorMessage }
335
+ Right result -> pure ()
336
+
337
+ modifyIORef' ?state (\state -> state |> modify #asyncs (handlerProcess:))
338
+ pure ()
320
339
Left errorMessage -> sendJSON FailedToDecodeMessageError { errorMessage = cs errorMessage }
321
340
322
341
onClose = cleanupAllSubscriptions
@@ -327,16 +346,7 @@ cleanupAllSubscriptions = do
327
346
let pgListener = ?applicationContext |> get #pgListener
328
347
329
348
case state of
330
- DataSyncReady { subscriptions, transactions } -> do
331
- let channelSubscriptions = subscriptions
332
- |> HashMap.elems
333
- |> map (get #channelSubscription)
334
- forEach channelSubscriptions \channelSubscription -> do
335
- pgListener |> PGListener.unsubscribe channelSubscription
336
-
337
- forEach (HashMap.elems transactions) (get #releaseConnection)
338
-
339
- pure ()
349
+ DataSyncReady { asyncs } -> forEach asyncs uninterruptibleCancel
340
350
_ -> pure ()
341
351
342
352
changesToValue :: [ChangeNotifications.Change] -> Value
@@ -369,11 +379,6 @@ findTransactionById transactionId = do
369
379
Just transaction -> pure transaction
370
380
Nothing -> error " No transaction with that id "
371
381
372
- closeTransaction transactionId = do
373
- DataSyncTransaction { releaseConnection } <- findTransactionById transactionId
374
- modifyIORef' ?state (\state -> state |> modify #transactions (HashMap.delete transactionId))
375
- releaseConnection
376
-
377
382
-- | Allow max 10 concurrent transactions per connection to avoid running out of database connections
378
383
--
379
384
-- Each transaction removes a database connection from the connection pool. If we don't limit the transactions,
@@ -389,6 +394,14 @@ ensureBelowTransactionLimit = do
389
394
when (transactionCount >= maxTransactionsPerConnection) do
390
395
error (" You've reached the transaction limit of " <> tshow maxTransactionsPerConnection <> " transactions" )
391
396
397
+ ensureBelowSubscriptionsLimit :: (?state :: IORef DataSyncController) => IO ()
398
+ ensureBelowSubscriptionsLimit = do
399
+ subscriptions <- get #subscriptions <$> readIORef ?state
400
+ let subscriptionsCount = HashMap.size subscriptions
401
+ let maxSubscriptionsPerConnection = 128
402
+ when (subscriptionsCount >= maxSubscriptionsPerConnection) do
403
+ error (" You've reached the subscriptions limit of " <> tshow maxSubscriptionsPerConnection <> " subscriptions" )
404
+
392
405
sqlQueryWithRLSAndTransactionId ::
393
406
( ?modelContext :: ModelContext
394
407
, PG.ToRow parameters
@@ -423,8 +436,11 @@ sqlExecWithRLSAndTransactionId transactionId theQuery theParams = runInModelCont
423
436
$(deriveFromJSON defaultOptions 'DataSyncQuery)
424
437
$(deriveToJSON defaultOptions 'DataSyncResult)
425
438
426
- instance SetField " subscriptions" DataSyncController (HashMap UUID Subscription ) where
439
+ instance SetField " subscriptions" DataSyncController (HashMap UUID (MVar.MVar ()) ) where
427
440
setField subscriptions record = record { subscriptions }
428
441
429
442
instance SetField " transactions" DataSyncController (HashMap UUID DataSyncTransaction) where
430
- setField transactions record = record { transactions }
443
+ setField transactions record = record { transactions }
444
+
445
+ instance SetField " asyncs" DataSyncController [Async ()] where
446
+ setField asyncs record = record { asyncs }
0 commit comments