@@ -57,6 +57,7 @@ defmodule Mongo do
57
57
use Bitwise
58
58
use Mongo.Messages
59
59
60
+ import Mongo.Session , only: [ in_read_session: 3 , in_write_session: 3 ]
60
61
alias Mongo.Query
61
62
alias Mongo.Topology
62
63
alias Mongo.UrlParser
@@ -67,6 +68,7 @@ defmodule Mongo do
67
68
alias Mongo.Error
68
69
69
70
@ timeout 15_000
71
+ @ retry_timeout_seconds 120
70
72
71
73
@ type conn :: DbConnection.Conn
72
74
@ type collection :: String . t ( )
@@ -239,6 +241,153 @@ defmodule Mongo do
239
241
<< u0 :: 48 , @ uuid_v4 :: 4 , u1 :: 12 , @ variant10 :: 2 , u2 :: 62 >>
240
242
end
241
243
244
+ @ doc """
245
+ Convenient function for running multiple write commands in a transaction.
246
+
247
+ In case of `TransientTransactionError` or `UnknownTransactionCommitResult` the function will retry the whole transaction or
248
+ the commit of the transaction. You can specify a timeout (`:transaction_retry_timeout_s`) to limit the time of repeating.
249
+ The default value is 120 seconds. If you don't wait so long, you call `with_transaction` with the
250
+ option `transaction_retry_timeout_s: 10`. In this case after 10 seconds of retrying, the function will return
251
+ an error.
252
+
253
+ ## Example
254
+
255
+ {:ok, ids} = Mongo.transaction(top, fn ->
256
+ {:ok, %InsertOneResult{:inserted_id => id1}} = Mongo.insert_one(top, "dogs", %{name: "Greta"})
257
+ {:ok, %InsertOneResult{:inserted_id => id2}} = Mongo.insert_one(top, "dogs", %{name: "Waldo"})
258
+ {:ok, %InsertOneResult{:inserted_id => id3}} = Mongo.insert_one(top, "dogs", %{name: "Tom"})
259
+ {:ok, [id1, id2, id3]}
260
+ end, transaction_retry_timeout_s: 10)
261
+
262
+ If transaction/3 is called inside another transaction, the function is simply executed, without wrapping the new transaction call in any way.
263
+ If there is an error in the inner transaction and the error is rescued, or the inner transaction is aborted (abort_transaction/1),
264
+ the whole outer transaction is aborted, guaranteeing nothing will be committed.
265
+ """
266
+ @ spec transaction ( GenServer . server ( ) , function ) :: { :ok , any ( ) } | :error | { :error , term }
267
+ def transaction ( topology_pid , fun , opts \\ [ ] ) do
268
+ :session
269
+ |> Process . get ( )
270
+ |> do_transaction ( topology_pid , fun , opts )
271
+ end
272
+
273
+ defp do_transaction ( nil , topology_pid , fun , opts ) do
274
+ ## try catch
275
+ with { :ok , session } <- Session . start_session ( topology_pid , :write , opts ) do
276
+ Process . put ( :session , session )
277
+
278
+ try do
279
+ run_in_transaction ( topology_pid , session , fun , DateTime . utc_now ( ) , opts )
280
+ rescue
281
+ error ->
282
+ { :error , error }
283
+ after
284
+ Session . end_session ( topology_pid , session )
285
+ Process . delete ( :session )
286
+ end
287
+ end
288
+ end
289
+
290
+ defp do_transaction ( _session , _topology_pid , fun , _opts ) when is_function ( fun , 0 ) do
291
+ fun . ( )
292
+ end
293
+
294
+ defp do_transaction ( _session , _topology_pid , fun , opts ) when is_function ( fun , 1 ) do
295
+ fun . ( opts )
296
+ end
297
+
298
+ defp run_in_transaction ( topology_pid , session , fun , start_time , opts ) do
299
+ Session . start_transaction ( session )
300
+
301
+ case run_function ( fun , Keyword . merge ( opts , session: session ) ) do
302
+ :ok ->
303
+ handle_commit ( session , start_time )
304
+
305
+ { :ok , result } ->
306
+ handle_commit ( session , start_time , result )
307
+
308
+ { :error , error } ->
309
+ ## check in case of an error while processing transaction
310
+ Session . abort_transaction ( session )
311
+ timeout = opts [ :transaction_retry_timeout_s ] || @ retry_timeout_seconds
312
+
313
+ case Error . has_label ( error , "TransientTransactionError" ) && DateTime . diff ( DateTime . utc_now ( ) , start_time , :second ) < timeout do
314
+ true ->
315
+ run_in_transaction ( topology_pid , session , fun , start_time , opts )
316
+
317
+ false ->
318
+ { :error , error }
319
+ end
320
+
321
+ :error ->
322
+ Session . abort_transaction ( session )
323
+ :error
324
+
325
+ other ->
326
+ ## everything else is an error
327
+ Session . abort_transaction ( session )
328
+ { :error , other }
329
+ end
330
+ end
331
+
332
+ ##
333
+ # calling the function and wrapping it to catch exceptions
334
+ #
335
+ defp run_function ( fun , _opts ) when is_function ( fun , 0 ) do
336
+ try do
337
+ fun . ( )
338
+ rescue
339
+ reason -> { :error , reason }
340
+ end
341
+ end
342
+
343
+ defp run_function ( fun , opts ) when is_function ( fun , 1 ) do
344
+ try do
345
+ fun . ( opts )
346
+ rescue
347
+ reason -> { :error , reason }
348
+ end
349
+ end
350
+
351
+ defp handle_commit ( session , start_time ) do
352
+ case Session . commit_transaction ( session , start_time ) do
353
+ ## everything is okay
354
+ :ok ->
355
+ :ok
356
+
357
+ error ->
358
+ ## the rest is an error
359
+ Session . abort_transaction ( session )
360
+ error
361
+ end
362
+ end
363
+
364
+ defp handle_commit ( session , start_time , result ) do
365
+ case Session . commit_transaction ( session , start_time ) do
366
+ ## everything is okay
367
+ :ok ->
368
+ { :ok , result }
369
+
370
+ error ->
371
+ ## the rest is an error
372
+ Session . abort_transaction ( session )
373
+ error
374
+ end
375
+ end
376
+
377
+ def abort_transaction ( reason ) do
378
+ :session
379
+ |> Process . get ( )
380
+ |> abort_transaction ( reason )
381
+ end
382
+
383
+ def abort_transaction ( nil , reason ) do
384
+ raise Mongo.Error . exception ( "Aborting transaction (#{ inspect ( reason ) } ) is not allowed, because there is no active transaction!" )
385
+ end
386
+
387
+ def abort_transaction ( _session , reason ) do
388
+ raise Mongo.Error . exception ( "Aborting transaction, reason #{ inspect ( reason ) } " )
389
+ end
390
+
242
391
@ doc """
243
392
Creates a change stream cursor on collections.
244
393
@@ -427,55 +576,45 @@ defmodule Mongo do
427
576
## check, if retryable reads are enabled
428
577
opts = Mongo . retryable_reads ( opts )
429
578
430
- with { :ok , session } <- Session . start_implicit_session ( topology_pid , :read , opts ) ,
431
- result <- exec_command_session ( session , cmd , opts ) ,
432
- :ok <- Session . end_implict_session ( topology_pid , session ) do
433
- case result do
434
- { :error , error } ->
435
- cond do
436
- Error . not_writable_primary_or_recovering? ( error , opts ) ->
437
- ## in case of explicitly
438
- issue_command ( topology_pid , cmd , :read , Keyword . put ( opts , :retry_counter , 2 ) )
579
+ case in_read_session ( topology_pid , & exec_command_session ( & 1 , cmd , & 2 ) , opts ) do
580
+ { :ok , doc } ->
581
+ { :ok , doc }
439
582
440
- Error . should_retry_read ( error , cmd , opts ) ->
441
- issue_command ( topology_pid , cmd , :read , Keyword . put ( opts , :read_counter , 2 ) )
583
+ { :error , error } ->
584
+ cond do
585
+ Error . not_writable_primary_or_recovering? ( error , opts ) ->
586
+ ## in case of explicitly
587
+ issue_command ( topology_pid , cmd , :read , Keyword . put ( opts , :retry_counter , 2 ) )
442
588
443
- true ->
444
- { :error , error }
445
- end
589
+ Error . should_retry_read ( error , cmd , opts ) ->
590
+ issue_command ( topology_pid , cmd , :read , Keyword . put ( opts , :read_counter , 2 ) )
446
591
447
- _other ->
448
- result
449
- end
450
- else
451
- _ -> { :error , Mongo.Error . exception ( "Command processing error" ) }
592
+ true ->
593
+ { :error , error }
594
+ end
452
595
end
453
596
end
454
597
455
598
def issue_command ( topology_pid , cmd , :write , opts ) do
456
599
## check, if retryable reads are enabled
457
600
opts = Mongo . retryable_writes ( opts , acknowledged? ( cmd [ :writeConcerns ] ) )
458
601
459
- with { :ok , session } <- Session . start_implicit_session ( topology_pid , :write , opts ) ,
460
- result <- exec_command_session ( session , cmd , opts ) ,
461
- :ok <- Session . end_implict_session ( topology_pid , session ) do
462
- case result do
463
- { :error , error } ->
464
- cond do
465
- Error . not_writable_primary_or_recovering? ( error , opts ) ->
466
- ## in case of explicitly
467
- issue_command ( topology_pid , cmd , :read , Keyword . put ( opts , :retry_counter , 2 ) )
602
+ case in_write_session ( topology_pid , & exec_command_session ( & 1 , cmd , & 2 ) , opts ) do
603
+ { :ok , doc } ->
604
+ { :ok , doc }
468
605
469
- Error . should_retry_write ( error , cmd , opts ) ->
470
- issue_command ( topology_pid , cmd , :write , Keyword . put ( opts , :write_counter , 2 ) )
606
+ { :error , error } ->
607
+ cond do
608
+ Error . not_writable_primary_or_recovering? ( error , opts ) ->
609
+ ## in case of explicitly
610
+ issue_command ( topology_pid , cmd , :read , Keyword . put ( opts , :retry_counter , 2 ) )
471
611
472
- true ->
473
- { :error , error }
474
- end
612
+ Error . should_retry_write ( error , cmd , opts ) ->
613
+ issue_command ( topology_pid , cmd , :write , Keyword . put ( opts , :write_counter , 2 ) )
475
614
476
- result ->
477
- result
478
- end
615
+ true ->
616
+ { :error , error }
617
+ end
479
618
end
480
619
end
481
620
@@ -1389,16 +1528,16 @@ defmodule Mongo do
1389
1528
Convenient function that drops the database `name`.
1390
1529
"""
1391
1530
@ spec drop_database ( GenServer . server ( ) , String . t ( ) | nil ) :: :ok | { :error , Mongo.Error . t ( ) }
1392
- def drop_database ( topology_pid , name \\ nil )
1531
+ def drop_database ( topology_pid , name , opts \\ [ ] )
1393
1532
1394
- def drop_database ( topology_pid , nil ) do
1395
- with { :ok , _ } <- Mongo . issue_command ( topology_pid , [ dropDatabase: 1 ] , :write , [ ] ) do
1533
+ def drop_database ( topology_pid , nil , opts ) do
1534
+ with { :ok , _ } <- Mongo . issue_command ( topology_pid , [ dropDatabase: 1 ] , :write , opts ) do
1396
1535
:ok
1397
1536
end
1398
1537
end
1399
1538
1400
- def drop_database ( topology_pid , name ) do
1401
- with { :ok , _ } <- Mongo . issue_command ( topology_pid , [ dropDatabase: 1 ] , :write , database: name ) do
1539
+ def drop_database ( topology_pid , name , opts ) do
1540
+ with { :ok , _ } <- Mongo . issue_command ( topology_pid , [ dropDatabase: 1 ] , :write , Keyword . put ( opts , :database , name ) ) do
1402
1541
:ok
1403
1542
end
1404
1543
end
@@ -1442,7 +1581,7 @@ defmodule Mongo do
1442
1581
def retryable_reads ( opts ) do
1443
1582
case opts [ :read_counter ] do
1444
1583
nil ->
1445
- case opts [ :retryable_reads ] == true && opts [ :session ] == nil do
1584
+ case opts [ :retryable_reads ] == true && get_session ( opts ) == nil do
1446
1585
true -> opts ++ [ read_counter: 1 ]
1447
1586
false -> opts
1448
1587
end
@@ -1651,4 +1790,8 @@ defmodule Mongo do
1651
1790
defp command_color ( :commitTransaction ) , do: :magenta
1652
1791
defp command_color ( :configureFailPoint ) , do: :blue
1653
1792
defp command_color ( _ ) , do: nil
1793
+
1794
+ def get_session ( opts ) do
1795
+ Process . get ( :session ) || opts [ :session ]
1796
+ end
1654
1797
end
0 commit comments