mongo 2.9.2 → 2.10.0.rc0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/lib/mongo.rb +1 -0
  5. data/lib/mongo/auth/user/view.rb +4 -4
  6. data/lib/mongo/bulk_write.rb +14 -8
  7. data/lib/mongo/bulk_write/result.rb +1 -1
  8. data/lib/mongo/bulk_write/result_combiner.rb +2 -2
  9. data/lib/mongo/bulk_write/transformable.rb +17 -9
  10. data/lib/mongo/client.rb +107 -16
  11. data/lib/mongo/cluster.rb +47 -25
  12. data/lib/mongo/cluster/topology/replica_set_no_primary.rb +1 -1
  13. data/lib/mongo/cluster_time.rb +139 -0
  14. data/lib/mongo/collection.rb +84 -25
  15. data/lib/mongo/collection/view.rb +7 -3
  16. data/lib/mongo/collection/view/aggregation.rb +4 -4
  17. data/lib/mongo/collection/view/builder/aggregation.rb +31 -6
  18. data/lib/mongo/collection/view/builder/find_command.rb +4 -1
  19. data/lib/mongo/collection/view/builder/map_reduce.rb +4 -1
  20. data/lib/mongo/collection/view/change_stream.rb +54 -66
  21. data/lib/mongo/collection/view/iterable.rb +2 -2
  22. data/lib/mongo/collection/view/map_reduce.rb +6 -4
  23. data/lib/mongo/collection/view/readable.rb +36 -16
  24. data/lib/mongo/collection/view/writable.rb +68 -22
  25. data/lib/mongo/cursor.rb +87 -20
  26. data/lib/mongo/database.rb +47 -43
  27. data/lib/mongo/database/view.rb +54 -11
  28. data/lib/mongo/error.rb +13 -4
  29. data/lib/mongo/error/invalid_write_concern.rb +2 -2
  30. data/lib/mongo/error/operation_failure.rb +65 -11
  31. data/lib/mongo/error/parser.rb +41 -8
  32. data/lib/mongo/grid/fs_bucket.rb +26 -6
  33. data/lib/mongo/grid/stream/read.rb +9 -2
  34. data/lib/mongo/grid/stream/write.rb +21 -5
  35. data/lib/mongo/index/view.rb +3 -3
  36. data/lib/mongo/lint.rb +10 -3
  37. data/lib/mongo/operation.rb +2 -0
  38. data/lib/mongo/operation/aggregate/result.rb +19 -6
  39. data/lib/mongo/operation/collections_info.rb +1 -1
  40. data/lib/mongo/operation/get_more/result.rb +9 -0
  41. data/lib/mongo/operation/list_collections/command.rb +1 -3
  42. data/lib/mongo/operation/list_collections/op_msg.rb +1 -2
  43. data/lib/mongo/operation/parallel_scan/command.rb +4 -1
  44. data/lib/mongo/operation/parallel_scan/op_msg.rb +4 -1
  45. data/lib/mongo/operation/result.rb +27 -4
  46. data/lib/mongo/operation/shared/executable.rb +19 -5
  47. data/lib/mongo/operation/shared/executable_no_validate.rb +1 -2
  48. data/lib/mongo/operation/shared/executable_transaction_label.rb +0 -9
  49. data/lib/mongo/operation/shared/polymorphic_result.rb +9 -1
  50. data/lib/mongo/operation/shared/result/aggregatable.rb +2 -2
  51. data/lib/mongo/operation/shared/sessions_supported.rb +42 -32
  52. data/lib/mongo/operation/shared/specifiable.rb +40 -0
  53. data/lib/mongo/operation/shared/unpinnable.rb +39 -0
  54. data/lib/mongo/operation/shared/write.rb +1 -1
  55. data/lib/mongo/protocol/update.rb +6 -2
  56. data/lib/mongo/retryable.rb +79 -39
  57. data/lib/mongo/server/connection.rb +10 -3
  58. data/lib/mongo/server/description.rb +25 -1
  59. data/lib/mongo/server/monitor/connection.rb +1 -1
  60. data/lib/mongo/server_selector.rb +10 -0
  61. data/lib/mongo/server_selector/selectable.rb +172 -32
  62. data/lib/mongo/session.rb +654 -581
  63. data/lib/mongo/session/session_pool.rb +1 -1
  64. data/lib/mongo/socket.rb +7 -28
  65. data/lib/mongo/socket/ssl.rb +26 -1
  66. data/lib/mongo/socket/tcp.rb +3 -0
  67. data/lib/mongo/socket/unix.rb +3 -0
  68. data/lib/mongo/uri.rb +112 -265
  69. data/lib/mongo/uri/srv_protocol.rb +4 -1
  70. data/lib/mongo/version.rb +1 -1
  71. data/lib/mongo/write_concern.rb +10 -29
  72. data/lib/mongo/write_concern/acknowledged.rb +12 -0
  73. data/lib/mongo/write_concern/base.rb +17 -13
  74. data/lib/mongo/write_concern/unacknowledged.rb +12 -0
  75. data/spec/atlas/atlas_connectivity_spec.rb +7 -37
  76. data/spec/atlas/operations_spec.rb +25 -0
  77. data/spec/integration/change_stream_examples_spec.rb +45 -31
  78. data/spec/integration/change_stream_spec.rb +305 -5
  79. data/spec/integration/client_spec.rb +44 -0
  80. data/spec/integration/command_monitoring_spec.rb +1 -0
  81. data/spec/integration/command_spec.rb +7 -1
  82. data/spec/integration/mmapv1_spec.rb +28 -0
  83. data/spec/integration/mongos_pinning_spec.rb +34 -0
  84. data/spec/integration/operation_failure_code_spec.rb +2 -2
  85. data/spec/integration/{read_concern.rb → read_concern_spec.rb} +7 -1
  86. data/spec/integration/read_preference_spec.rb +485 -0
  87. data/spec/integration/retryable_writes_spec.rb +8 -19
  88. data/spec/integration/sdam_error_handling_spec.rb +1 -1
  89. data/spec/integration/sdam_events_spec.rb +2 -2
  90. data/spec/integration/server_description_spec.rb +14 -17
  91. data/spec/integration/server_selector_spec.rb +7 -3
  92. data/spec/integration/server_spec.rb +48 -0
  93. data/spec/integration/ssl_uri_options_spec.rb +1 -1
  94. data/spec/integration/step_down_spec.rb +10 -4
  95. data/spec/integration/transactions_examples_spec.rb +11 -10
  96. data/spec/lite_spec_helper.rb +19 -16
  97. data/spec/mongo/auth/scram/negotiation_spec.rb +11 -8
  98. data/spec/mongo/bulk_write/ordered_combiner_spec.rb +6 -6
  99. data/spec/mongo/bulk_write/unordered_combiner_spec.rb +4 -4
  100. data/spec/mongo/bulk_write_spec.rb +12 -2
  101. data/spec/mongo/client_construction_spec.rb +160 -8
  102. data/spec/mongo/client_spec.rb +5 -4
  103. data/spec/mongo/cluster_spec.rb +6 -6
  104. data/spec/mongo/cluster_time_spec.rb +148 -0
  105. data/spec/mongo/collection/view/aggregation_spec.rb +34 -15
  106. data/spec/mongo/collection/view/change_stream_spec.rb +62 -3
  107. data/spec/mongo/collection/view/map_reduce_spec.rb +7 -5
  108. data/spec/mongo/collection/view/readable_spec.rb +4 -4
  109. data/spec/mongo/collection_spec.rb +331 -14
  110. data/spec/mongo/cursor_spec.rb +117 -5
  111. data/spec/mongo/database_spec.rb +240 -8
  112. data/spec/mongo/error/operation_failure_spec.rb +47 -1
  113. data/spec/mongo/error/parser_spec.rb +160 -23
  114. data/spec/mongo/operation/insert/bulk_spec.rb +2 -1
  115. data/spec/mongo/operation/result_spec.rb +27 -0
  116. data/spec/mongo/operation/update/bulk_spec.rb +1 -0
  117. data/spec/mongo/retryable_spec.rb +2 -0
  118. data/spec/mongo/server/app_metadata_spec.rb +2 -2
  119. data/spec/mongo/server/connection_spec.rb +13 -17
  120. data/spec/mongo/server/monitor/connection_spec.rb +13 -10
  121. data/spec/mongo/server_selector_spec.rb +34 -2
  122. data/spec/mongo/session/session_pool_spec.rb +14 -3
  123. data/spec/mongo/session_spec.rb +3 -3
  124. data/spec/mongo/session_transaction_spec.rb +4 -3
  125. data/spec/mongo/socket/ssl_spec.rb +19 -5
  126. data/spec/mongo/socket_spec.rb +1 -62
  127. data/spec/mongo/uri/srv_protocol_spec.rb +14 -20
  128. data/spec/mongo/uri_option_parsing_spec.rb +94 -8
  129. data/spec/mongo/uri_spec.rb +23 -10
  130. data/spec/mongo/write_concern_spec.rb +56 -3
  131. data/spec/spec_tests/change_streams_spec.rb +2 -1
  132. data/spec/spec_tests/cmap_spec.rb +1 -1
  133. data/spec/spec_tests/crud_spec.rb +12 -2
  134. data/spec/spec_tests/data/change_streams/change-streams-errors.yml +24 -1
  135. data/spec/spec_tests/data/change_streams/change-streams.yml +172 -3
  136. data/spec/spec_tests/data/command_monitoring/bulkWrite.yml +1 -1
  137. data/spec/spec_tests/data/command_monitoring/updateMany.yml +0 -2
  138. data/spec/spec_tests/data/command_monitoring/updateOne.yml +0 -5
  139. data/spec/spec_tests/data/crud/read/aggregate-out.yml +0 -6
  140. data/spec/spec_tests/data/crud/read/count-empty.yml +29 -0
  141. data/spec/spec_tests/data/crud/write/bulkWrite-arrayFilters.yml +1 -0
  142. data/spec/spec_tests/data/crud/write/bulkWrite-collation.yml +101 -0
  143. data/spec/spec_tests/data/crud/write/bulkWrite.yml +401 -0
  144. data/spec/spec_tests/data/crud/write/insertMany.yml +58 -2
  145. data/spec/spec_tests/data/crud/write/updateMany-arrayFilters.yml +3 -0
  146. data/spec/spec_tests/data/crud/write/updateOne-arrayFilters.yml +6 -1
  147. data/spec/spec_tests/data/crud_v2/aggregate-merge.yml +103 -0
  148. data/spec/spec_tests/data/crud_v2/aggregate-out-readConcern.yml +110 -0
  149. data/spec/spec_tests/data/crud_v2/bulkWrite-arrayFilters.yml +81 -0
  150. data/spec/spec_tests/data/crud_v2/db-aggregate.yml +38 -0
  151. data/spec/spec_tests/data/crud_v2/updateWithPipelines.yml +92 -0
  152. data/spec/spec_tests/data/retryable_writes/insertOne-serverErrors.yml +2 -2
  153. data/spec/spec_tests/data/transactions/abort.yml +3 -0
  154. data/spec/spec_tests/data/transactions/bulk.yml +3 -8
  155. data/spec/spec_tests/data/transactions/causal-consistency.yml +3 -8
  156. data/spec/spec_tests/data/transactions/commit.yml +3 -1
  157. data/spec/spec_tests/data/transactions/count.yml +3 -0
  158. data/spec/spec_tests/data/transactions/delete.yml +3 -0
  159. data/spec/spec_tests/data/transactions/error-labels.yml +4 -1
  160. data/spec/spec_tests/data/transactions/errors-client.yml +56 -0
  161. data/spec/spec_tests/data/transactions/errors.yml +3 -0
  162. data/spec/spec_tests/data/transactions/findOneAndDelete.yml +3 -0
  163. data/spec/spec_tests/data/transactions/findOneAndReplace.yml +3 -0
  164. data/spec/spec_tests/data/transactions/findOneAndUpdate.yml +3 -0
  165. data/spec/spec_tests/data/transactions/insert.yml +3 -0
  166. data/spec/spec_tests/data/transactions/isolation.yml +3 -0
  167. data/spec/spec_tests/data/transactions/mongos-pin-auto.yml +1671 -0
  168. data/spec/spec_tests/data/transactions/mongos-recovery-token.yml +347 -0
  169. data/spec/spec_tests/data/transactions/pin-mongos.yml +557 -0
  170. data/spec/spec_tests/data/transactions/read-concern.yml +3 -0
  171. data/spec/spec_tests/data/transactions/read-pref.yml +3 -0
  172. data/spec/spec_tests/data/transactions/reads.yml +3 -0
  173. data/spec/spec_tests/data/transactions/retryable-abort.yml +5 -2
  174. data/spec/spec_tests/data/transactions/retryable-commit.yml +4 -1
  175. data/spec/spec_tests/data/transactions/retryable-writes.yml +3 -0
  176. data/spec/spec_tests/data/transactions/run-command.yml +3 -0
  177. data/spec/spec_tests/data/transactions/transaction-options.yml +6 -0
  178. data/spec/spec_tests/data/transactions/update.yml +3 -8
  179. data/spec/spec_tests/data/transactions/write-concern.yml +348 -38
  180. data/spec/spec_tests/data/transactions_api/callback-aborts.yml +6 -0
  181. data/spec/spec_tests/data/transactions_api/callback-commits.yml +5 -0
  182. data/spec/spec_tests/data/transactions_api/callback-retry.yml +7 -2
  183. data/spec/spec_tests/data/transactions_api/commit-retry.yml +70 -15
  184. data/spec/spec_tests/data/transactions_api/commit-transienttransactionerror-4.2.yml +3 -0
  185. data/spec/spec_tests/data/transactions_api/commit-transienttransactionerror.yml +3 -0
  186. data/spec/spec_tests/data/transactions_api/commit-writeconcernerror.yml +59 -109
  187. data/spec/spec_tests/data/transactions_api/commit.yml +5 -0
  188. data/spec/spec_tests/data/transactions_api/transaction-options.yml +10 -0
  189. data/spec/spec_tests/retryable_reads_spec.rb +5 -2
  190. data/spec/spec_tests/retryable_writes_spec.rb +5 -2
  191. data/spec/spec_tests/sdam_monitoring_spec.rb +3 -3
  192. data/spec/spec_tests/sdam_spec.rb +2 -2
  193. data/spec/spec_tests/transactions_api_spec.rb +1 -67
  194. data/spec/spec_tests/transactions_spec.rb +2 -66
  195. data/spec/support/authorization.rb +4 -0
  196. data/spec/support/change_streams.rb +30 -10
  197. data/spec/support/change_streams/operation.rb +27 -0
  198. data/spec/support/client_registry.rb +44 -25
  199. data/spec/support/cluster_config.rb +25 -14
  200. data/spec/support/cluster_tools.rb +32 -10
  201. data/spec/support/command_monitoring.rb +1 -1
  202. data/spec/support/common_shortcuts.rb +30 -0
  203. data/spec/support/connection_string.rb +8 -3
  204. data/spec/support/constraints.rb +34 -0
  205. data/spec/support/crud.rb +31 -16
  206. data/spec/support/crud/context.rb +23 -0
  207. data/spec/support/crud/operation.rb +311 -14
  208. data/spec/support/crud/spec.rb +2 -1
  209. data/spec/support/crud/test.rb +24 -27
  210. data/spec/support/crud/test_base.rb +22 -0
  211. data/spec/support/crud/verifier.rb +15 -1
  212. data/spec/support/event_subscriber.rb +12 -0
  213. data/spec/support/sdam_formatter_integration.rb +12 -6
  214. data/spec/support/shared/server_selector.rb +10 -0
  215. data/spec/support/shared/session.rb +13 -12
  216. data/spec/support/spec_config.rb +32 -22
  217. data/spec/support/spec_setup.rb +2 -2
  218. data/spec/support/transactions.rb +87 -0
  219. data/spec/support/transactions/context.rb +33 -0
  220. data/spec/support/transactions/operation.rb +99 -349
  221. data/spec/support/transactions/spec.rb +1 -3
  222. data/spec/support/transactions/test.rb +110 -49
  223. data/spec/support/utils.rb +74 -1
  224. metadata +52 -10
  225. metadata.gz.sig +0 -0
  226. data/spec/support/crud/read.rb +0 -265
  227. data/spec/support/crud/write.rb +0 -284
@@ -20,36 +20,196 @@ module Mongo
20
20
  # A logical session representing a set of sequential operations executed
21
21
  # by an application that are related in some way.
22
22
  #
23
+ # @note Session objects are not thread-safe. An application may use a session
24
+ # from only one thread or process at a time.
25
+ #
23
26
  # @since 2.5.0
24
27
  class Session
25
28
  extend Forwardable
26
29
  include Retryable
27
30
  include Loggable
31
+ include ClusterTime::Consumer
32
+
33
+ # Initialize a Session.
34
+ #
35
+ # @note Applications should use Client#start_session to begin a session.
36
+ #
37
+ # @example
38
+ # Session.new(server_session, client, options)
39
+ #
40
+ # @param [ ServerSession ] server_session The server session this session is associated with.
41
+ # @param [ Client ] client The client through which this session is created.
42
+ # @param [ Hash ] options The options for this session.
43
+ #
44
+ # @option options [ true|false ] :causal_consistency Whether to enable
45
+ # causal consistency for this session.
46
+ # @option options [ Hash ] :default_transaction_options Options to pass
47
+ # to start_transaction by default, can contain any of the options that
48
+ # start_transaction accepts.
49
+ # @option options [ true|false ] :implicit For internal driver use only -
50
+ # specifies whether the session is implicit.
51
+ # @option options [ Hash ] :read_preference The read preference options hash,
52
+ # with the following optional keys:
53
+ # - *:mode* -- the read preference as a string or symbol; valid values are
54
+ # *:primary*, *:primary_preferred*, *:secondary*, *:secondary_preferred*
55
+ # and *:nearest*.
56
+ #
57
+ # @since 2.5.0
58
+ # @api private
59
+ def initialize(server_session, client, options = {})
60
+ @server_session = server_session
61
+ options = options.dup
62
+
63
+ @client = client.use(:admin)
64
+ @options = options.freeze
65
+ @cluster_time = nil
66
+ @state = NO_TRANSACTION_STATE
67
+ end
28
68
 
29
- # Get the options for this session.
69
+ # @return [ Hash ] The options for this session.
30
70
  #
31
71
  # @since 2.5.0
32
72
  attr_reader :options
33
73
 
34
- # Get the client through which this session was created.
74
+ # @return [ Client ] The client through which this session was created.
35
75
  #
36
76
  # @since 2.5.1
37
77
  attr_reader :client
38
78
 
39
- # The cluster time for this session.
79
+ def cluster
80
+ @client.cluster
81
+ end
82
+
83
+ # @return [ BSON::Timestamp ] The latest seen operation time for this session.
40
84
  #
41
85
  # @since 2.5.0
42
- attr_reader :cluster_time
86
+ attr_reader :operation_time
87
+
88
+ # @return [ Hash ] The options for the transaction currently being executed
89
+ # on this session.
90
+ #
91
+ # @since 2.6.0
92
+ def txn_options
93
+ @txn_options or raise ArgumentError, "There is no active transaction"
94
+ end
95
+
96
+ # Is this session an implicit one (not user-created).
97
+ #
98
+ # @example Is the session implicit?
99
+ # session.implicit?
100
+ #
101
+ # @return [ true, false ] Whether this session is implicit.
102
+ #
103
+ # @since 2.5.1
104
+ def implicit?
105
+ @implicit ||= !!(@options.key?(:implicit) && @options[:implicit] == true)
106
+ end
107
+
108
+ # Is this session an explicit one (i.e. user-created).
109
+ #
110
+ # @example Is the session explicit?
111
+ # session.explicit?
112
+ #
113
+ # @return [ true, false ] Whether this session is explicit.
114
+ #
115
+ # @since 2.5.2
116
+ def explicit?
117
+ !implicit?
118
+ end
119
+
120
+ # Whether reads executed with this session can be retried according to
121
+ # the modern retryable reads specification.
122
+ #
123
+ # If this method returns true, the modern retryable reads have been
124
+ # requested by the application. If the server selected for a read operation
125
+ # supports modern retryable reads, they will be used for that particular
126
+ # operation. If the server selected for a read operation does not support
127
+ # modern retryable reads, the read will not be retried.
128
+ #
129
+ # If this method returns false, legacy retryable reads have been requested
130
+ # by the application. Legacy retryable read logic will be used regardless
131
+ # of server version of the server(s) that the client is connected to.
132
+ # The number of read retries is given by :max_read_retries client option,
133
+ # which is 1 by default and can be set to 0 to disable legacy read retries.
134
+ #
135
+ # @api private
136
+ def retry_reads?
137
+ client.options[:retry_reads] != false
138
+ end
43
139
 
44
- # The latest seen operation time for this session.
140
+ # Will writes executed with this session be retried.
141
+ #
142
+ # @example Will writes be retried.
143
+ # session.retry_writes?
144
+ #
145
+ # @return [ true, false ] If writes will be retried.
146
+ #
147
+ # @note Retryable writes are only available on server versions at least 3.6
148
+ # and with sharded clusters or replica sets.
45
149
  #
46
150
  # @since 2.5.0
47
- attr_reader :operation_time
151
+ def retry_writes?
152
+ !!client.options[:retry_writes] && (cluster.replica_set? || cluster.sharded?)
153
+ end
48
154
 
49
- # The options for the transaction currently being executed on the session.
155
+ # Get the read preference the session will use in the currently
156
+ # active transaction.
157
+ #
158
+ # This is a driver style hash with underscore keys.
159
+ #
160
+ # @example Get the transaction's read preference
161
+ # session.txn_read_preference
162
+ #
163
+ # @return [ Hash ] The read preference of the transaction.
50
164
  #
51
165
  # @since 2.6.0
52
- attr_reader :txn_options
166
+ def txn_read_preference
167
+ rp = txn_options[:read] ||
168
+ @client.read_preference
169
+ Mongo::Lint.validate_underscore_read_preference(rp)
170
+ rp
171
+ end
172
+
173
+ # Whether this session has ended.
174
+ #
175
+ # @example
176
+ # session.ended?
177
+ #
178
+ # @return [ true, false ] Whether the session has ended.
179
+ #
180
+ # @since 2.5.0
181
+ def ended?
182
+ @server_session.nil?
183
+ end
184
+
185
+ # Get the server session id of this session, if the session was not ended.
186
+ # If the session was ended, returns nil.
187
+ #
188
+ # @example Get the session id.
189
+ # session.session_id
190
+ #
191
+ # @return [ BSON::Document ] The server session id.
192
+ #
193
+ # @since 2.5.0
194
+ def session_id
195
+ if ended?
196
+ raise Error::SessionEnded
197
+ end
198
+
199
+ @server_session.session_id
200
+ end
201
+
202
+ # @return [ Server | nil ] The server (which should be a mongos) that this
203
+ # session is pinned to, if any.
204
+ #
205
+ # @api private
206
+ attr_reader :pinned_server
207
+
208
+ # @return [ BSON::Document | nil ] Recovery token for the sharded
209
+ # transaction being executed on this session, if any.
210
+ #
211
+ # @api private
212
+ attr_accessor :recovery_token
53
213
 
54
214
  # Error message indicating that the session was retrieved from a client with a different cluster than that of the
55
215
  # client through which it is currently being used.
@@ -98,53 +258,12 @@ module Mongo
98
258
  # @since 2.6.0
99
259
  TRANSACTION_ABORTED_STATE = :transaction_aborted
100
260
 
261
+ # @api private
101
262
  UNLABELED_WRITE_CONCERN_CODES = [
102
263
  79, # UnknownReplWriteConcern
103
264
  100, # CannotSatisfyWriteConcern,
104
265
  ].freeze
105
266
 
106
- # Initialize a Session.
107
- #
108
- # @note Applications should use Client#start_session to begin a session.
109
- #
110
- # @example
111
- # Session.new(server_session, client, options)
112
- #
113
- # @param [ ServerSession ] server_session The server session this session is associated with.
114
- # @param [ Client ] client The client through which this session is created.
115
- # @param [ Hash ] options The options for this session.
116
- #
117
- # @option options [ true|false ] :causal_consistency Whether to enable
118
- # causal consistency for this session.
119
- # @option options [ Hash ] :default_transaction_options Options to pass
120
- # to start_transaction by default, can contain any of the options that
121
- # start_transaction accepts.
122
- # @option options [ true|false ] :implicit For internal driver use only -
123
- # specifies whether the session is implicit.
124
- # @option options [ Hash ] :read_preference The read preference options hash,
125
- # with the following optional keys:
126
- # - *:mode* -- the read preference as a string or symbol; valid values are
127
- # *:primary*, *:primary_preferred*, *:secondary*, *:secondary_preferred*
128
- # and *:nearest*.
129
- #
130
- # @since 2.5.0
131
- # @api private
132
- def initialize(server_session, client, options = {})
133
- @server_session = server_session
134
- options = options.dup
135
-
136
- # Because the read preference will need to be inserted into a command as a string, we convert
137
- # it from a symbol immediately upon receiving it.
138
- if options[:read_preference] && options[:read_preference][:mode]
139
- options[:read_preference][:mode] = options[:read_preference][:mode].to_s
140
- end
141
-
142
- @client = client.use(:admin)
143
- @options = options.freeze
144
- @cluster_time = nil
145
- @state = NO_TRANSACTION_STATE
146
- end
147
-
148
267
  # Get a formatted string for use in inspection.
149
268
  #
150
269
  # @example Inspect the session object.
@@ -159,6 +278,16 @@ module Mongo
159
278
 
160
279
  # End this session.
161
280
  #
281
+ # If there is an in-progress transaction on this session, the transaction
282
+ # is aborted. The server session associated with this session is returned
283
+ # to the server session pool. Finally, this session is marked ended and
284
+ # is no longer usable.
285
+ #
286
+ # If this session is already ended, this method does nothing.
287
+ #
288
+ # Note that this method does not directly issue an endSessions command
289
+ # to this server, contrary to what its name might suggest.
290
+ #
162
291
  # @example
163
292
  # session.end_session
164
293
  #
@@ -179,377 +308,142 @@ module Mongo
179
308
  @server_session = nil
180
309
  end
181
310
 
182
- # Whether this session has ended.
183
- #
184
- # @example
185
- # session.ended?
311
+ # Executes the provided block in a transaction, retrying as necessary.
186
312
  #
187
- # @return [ true, false ] Whether the session has ended.
313
+ # Returns the return value of the block.
188
314
  #
189
- # @since 2.5.0
190
- def ended?
191
- @server_session.nil?
192
- end
193
-
194
- # Add the autocommit field to a command document if applicable.
315
+ # Exact number of retries and when they are performed are implementation
316
+ # details of the driver; the provided block should be idempotent, and
317
+ # should be prepared to be called more than once. The driver may retry
318
+ # the commit command within an active transaction or it may repeat the
319
+ # transaction and invoke the block again, depending on the error
320
+ # encountered if any. Note also that the retries may be executed against
321
+ # different servers.
195
322
  #
196
- # @example
197
- # session.add_autocommit!(cmd)
323
+ # Transactions cannot be nested - InvalidTransactionOperation will be raised
324
+ # if this method is called when the session already has an active transaction.
198
325
  #
199
- # @return [ Hash, BSON::Document ] The command document.
326
+ # Exceptions raised by the block which are not derived from Mongo::Error
327
+ # stop processing, abort the transaction and are propagated out of
328
+ # with_transaction. Exceptions derived from Mongo::Error may be
329
+ # handled by with_transaction, resulting in retries of the process.
200
330
  #
201
- # @since 2.6.0
202
- # @api private
203
- def add_autocommit!(command)
204
- command.tap do |c|
205
- c[:autocommit] = false if in_transaction?
206
- end
207
- end
208
-
209
- # Add this session's id to a command document.
331
+ # Currently, with_transaction will retry commits and block invocations
332
+ # until at least 120 seconds have passed since with_transaction started
333
+ # executing. This timeout is not configurable and may change in a future
334
+ # driver version.
210
335
  #
211
- # @example
212
- # session.add_id!(cmd)
336
+ # @note with_transaction contains a loop, therefore the if with_transaction
337
+ # itself is placed in a loop, its block should not call next or break to
338
+ # control the outer loop because this will instead affect the loop in
339
+ # with_transaction. The driver will warn and abort the transaction
340
+ # if it detects this situation.
213
341
  #
214
- # @return [ Hash, BSON::Document ] The command document.
342
+ # @example Execute a statement in a transaction
343
+ # session.with_transaction(write_concern: {w: :majority}) do
344
+ # collection.update_one({ id: 3 }, { '$set' => { status: 'Inactive'} },
345
+ # session: session)
215
346
  #
216
- # @since 2.5.0
217
- # @api private
218
- def add_id!(command)
219
- command.merge!(lsid: session_id)
220
- end
221
-
222
- # Add the startTransaction field to a command document if applicable.
347
+ # end
223
348
  #
224
- # @example
225
- # session.add_start_transaction!(cmd)
226
- #
227
- # @return [ Hash, BSON::Document ] The command document.
228
- #
229
- # @since 2.6.0
230
- # @api private
231
- def add_start_transaction!(command)
232
- command.tap do |c|
233
- if starting_transaction?
234
- c[:startTransaction] = true
235
- end
236
- end
237
- end
238
-
239
- # Add the transaction number to a command document if applicable.
240
- #
241
- # @example
242
- # session.add_txn_num!(cmd)
243
- #
244
- # @return [ Hash, BSON::Document ] The command document.
349
+ # @example Execute a statement in a transaction, limiting total time consumed
350
+ # Timeout.timeout(5) do
351
+ # session.with_transaction(write_concern: {w: :majority}) do
352
+ # collection.update_one({ id: 3 }, { '$set' => { status: 'Inactive'} },
353
+ # session: session)
245
354
  #
246
- # @since 2.6.0
247
- # @api private
248
- def add_txn_num!(command)
249
- command.tap do |c|
250
- c[:txnNumber] = BSON::Int64.new(@server_session.txn_num) if in_transaction?
251
- end
252
- end
253
-
254
- # Add the transactions options if applicable.
355
+ # end
356
+ # end
255
357
  #
256
- # @example
257
- # session.add_txn_opts!(cmd)
358
+ # @param [ Hash ] options The options for the transaction being started.
359
+ # These are the same options that start_transaction accepts.
258
360
  #
259
- # @return [ Hash, BSON::Document ] The command document.
361
+ # @raise [ Error::InvalidTransactionOperation ] If a transaction is already in
362
+ # progress or if the write concern is unacknowledged.
260
363
  #
261
- # @since 2.6.0
262
- # @api private
263
- def add_txn_opts!(command, read)
264
- command.tap do |c|
265
- # The read preference should be added for all read operations.
266
- if read && txn_read_pref = txn_read_preference
267
- Mongo::Lint.validate_underscore_read_preference(txn_read_pref)
268
- txn_read_pref = txn_read_pref.dup
269
- txn_read_pref[:mode] = txn_read_pref[:mode].to_s.gsub(/(_\w)/) { |match| match[1].upcase }
270
- Mongo::Lint.validate_camel_case_read_preference(txn_read_pref)
271
- c['$readPreference'] = txn_read_pref
272
- end
273
-
274
- # The read concern should be added to any command that starts a transaction.
275
- if starting_transaction?
276
- # https://jira.mongodb.org/browse/SPEC-1161: transaction's
277
- # read concern overrides collection/database/client read concerns,
278
- # even if transaction's read concern is not set.
279
- # Read concern here is the one sent to the server and may
280
- # include afterClusterTime.
281
- if rc = c[:readConcern]
282
- rc = rc.dup
283
- rc.delete(:level)
284
- end
285
- if txn_read_concern
286
- if rc
287
- rc.update(txn_read_concern)
288
- else
289
- rc = txn_read_concern.dup
290
- end
291
- end
292
- if rc.nil? || rc.empty?
293
- c.delete(:readConcern)
294
- else
295
- c[:readConcern ] = rc
296
- end
297
- end
298
-
299
- # We need to send the read concern level as a string rather than a symbol.
300
- if c[:readConcern] && c[:readConcern][:level]
301
- c[:readConcern][:level] = c[:readConcern][:level].to_s
364
+ # @since 2.7.0
365
+ def with_transaction(options=nil)
366
+ # Non-configurable 120 second timeout for the entire operation
367
+ deadline = Time.now + 120
368
+ transaction_in_progress = false
369
+ loop do
370
+ commit_options = {}
371
+ if options
372
+ commit_options[:write_concern] = options[:write_concern]
302
373
  end
303
-
304
- # The write concern should be added to any abortTransaction or commitTransaction command.
305
- if (c[:abortTransaction] || c[:commitTransaction])
306
- if @already_committed
307
- wc = BSON::Document.new(c[:writeConcern] || txn_write_concern || {})
308
- wc.merge!(w: :majority)
309
- wc[:wtimeout] ||= 10000
310
- c[:writeConcern] = wc
311
- elsif txn_write_concern
312
- c[:writeConcern] ||= txn_write_concern
374
+ start_transaction(options)
375
+ transaction_in_progress = true
376
+ begin
377
+ rv = yield self
378
+ rescue Exception => e
379
+ if within_states?(STARTING_TRANSACTION_STATE, TRANSACTION_IN_PROGRESS_STATE)
380
+ abort_transaction
381
+ transaction_in_progress = false
313
382
  end
314
- end
315
-
316
- # A non-numeric write concern w value needs to be sent as a string rather than a symbol.
317
- if c[:writeConcern] && c[:writeConcern][:w] && c[:writeConcern][:w].is_a?(Symbol)
318
- c[:writeConcern][:w] = c[:writeConcern][:w].to_s
319
- end
320
- end
321
- end
322
-
323
- # Remove the read concern and/or write concern from the command if not applicable.
324
- #
325
- # @example
326
- # session.suppress_read_write_concern!(cmd)
327
- #
328
- # @return [ Hash, BSON::Document ] The command document.
329
- #
330
- # @since 2.6.0
331
- # @api private
332
- def suppress_read_write_concern!(command)
333
- command.tap do |c|
334
- next unless in_transaction?
335
-
336
- c.delete(:readConcern) unless starting_transaction?
337
- c.delete(:writeConcern) unless c[:commitTransaction] || c[:abortTransaction]
338
- end
339
- end
340
-
341
- # Ensure that the read preference of a command primary.
342
- #
343
- # @example
344
- # session.validate_read_preference!(command)
345
- #
346
- # @raise [ Mongo::Error::InvalidTransactionOperation ] If the read preference of the command is
347
- # not primary.
348
- #
349
- # @since 2.6.0
350
- # @api private
351
- def validate_read_preference!(command)
352
- return unless in_transaction? && non_primary_read_preference_mode?(command)
353
-
354
- raise Mongo::Error::InvalidTransactionOperation.new(
355
- Mongo::Error::InvalidTransactionOperation::INVALID_READ_PREFERENCE)
356
- end
357
-
358
- # Update the state of the session due to a (non-commit and non-abort) operation being run.
359
- #
360
- # @since 2.6.0
361
- # @api private
362
- def update_state!
363
- case @state
364
- when STARTING_TRANSACTION_STATE
365
- @state = TRANSACTION_IN_PROGRESS_STATE
366
- when TRANSACTION_COMMITTED_STATE, TRANSACTION_ABORTED_STATE
367
- @state = NO_TRANSACTION_STATE
368
- end
369
- end
370
-
371
- # Validate the session.
372
- #
373
- # @example
374
- # session.validate!(cluster)
375
- #
376
- # @param [ Cluster ] cluster The cluster the session is attempted to be used with.
377
- #
378
- # @return [ nil ] nil if the session is valid.
379
- #
380
- # @raise [ Mongo::Error::InvalidSession ] Raise error if the session is not valid.
381
- #
382
- # @since 2.5.0
383
- # @api private
384
- def validate!(cluster)
385
- check_matching_cluster!(cluster)
386
- check_if_ended!
387
- self
388
- end
389
-
390
- # Process a response from the server that used this session.
391
- #
392
- # @example Process a response from the server.
393
- # session.process(result)
394
- #
395
- # @param [ Operation::Result ] result The result from the operation.
396
- #
397
- # @return [ Operation::Result ] The result.
398
- #
399
- # @since 2.5.0
400
- # @api private
401
- def process(result)
402
- unless implicit?
403
- set_operation_time(result)
404
- set_cluster_time(result)
405
- end
406
- @server_session.set_last_use!
407
- result
408
- end
409
-
410
- # Advance the cached cluster time document for this session.
411
- #
412
- # @example Advance the cluster time.
413
- # session.advance_cluster_time(doc)
414
- #
415
- # @param [ BSON::Document, Hash ] new_cluster_time The new cluster time.
416
- #
417
- # @return [ BSON::Document, Hash ] The new cluster time.
418
- #
419
- # @since 2.5.0
420
- def advance_cluster_time(new_cluster_time)
421
- if @cluster_time
422
- @cluster_time = [ @cluster_time, new_cluster_time ].max_by { |doc| doc[Cluster::CLUSTER_TIME] }
423
- else
424
- @cluster_time = new_cluster_time
425
- end
426
- end
427
-
428
- # Advance the cached operation time for this session.
429
- #
430
- # @example Advance the operation time.
431
- # session.advance_operation_time(timestamp)
432
- #
433
- # @param [ BSON::Timestamp ] new_operation_time The new operation time.
434
- #
435
- # @return [ BSON::Timestamp ] The max operation time, considering the current and new times.
436
- #
437
- # @since 2.5.0
438
- def advance_operation_time(new_operation_time)
439
- if @operation_time
440
- @operation_time = [ @operation_time, new_operation_time ].max
441
- else
442
- @operation_time = new_operation_time
443
- end
444
- end
445
-
446
- # Whether reads executed with this session can be retried according to
447
- # the modern retryable reads specification.
448
- #
449
- # If this method returns true, the modern retryable reads have been
450
- # requested by the application. If the server selected for a read operation
451
- # supports modern retryable reads, they will be used for that particular
452
- # operation. If the server selected for a read operation does not support
453
- # modern retryable reads, the read will not be retried.
454
- #
455
- # If this method returns false, legacy retryable reads have been requested
456
- # by the application. Legacy retryable read logic will be used regardless
457
- # of server version of the server(s) that the client is connected to.
458
- # The number of read retries is given by :max_read_retries client option,
459
- # which is 1 by default and can be set to 0 to disable legacy read retries.
460
- #
461
- # @api private
462
- def retry_reads?
463
- client.options[:retry_reads] != false
464
- end
465
-
466
- # Will writes executed with this session be retried.
467
- #
468
- # @example Will writes be retried.
469
- # session.retry_writes?
470
- #
471
- # @return [ true, false ] If writes will be retried.
472
- #
473
- # @note Retryable writes are only available on server versions at least 3.6
474
- # and with sharded clusters or replica sets.
475
- #
476
- # @since 2.5.0
477
- def retry_writes?
478
- !!client.options[:retry_writes] && (cluster.replica_set? || cluster.sharded?)
479
- end
480
-
481
- # Get the server session id of this session, if the session was not ended.
482
- # If the session was ended, returns nil.
483
- #
484
- # @example Get the session id.
485
- # session.session_id
486
- #
487
- # @return [ BSON::Document ] The server session id.
488
- #
489
- # @since 2.5.0
490
- def session_id
491
- if ended?
492
- raise Error::SessionEnded
493
- end
494
-
495
- @server_session.session_id
496
- end
497
-
498
- # Increment and return the next transaction number.
499
- #
500
- # @example Get the next transaction number.
501
- # session.next_txn_num
502
- #
503
- # @return [ Integer ] The next transaction number.
504
- #
505
- # @since 2.5.0
506
- # @api private
507
- def next_txn_num
508
- if ended?
509
- raise Error::SessionEnded
510
- end
511
-
512
- @server_session.next_txn_num
513
- end
514
383
 
515
- # Get the current transaction number.
516
- #
517
- # @example Get the current transaction number.
518
- # session.txn_num
519
- #
520
- # @return [ Integer ] The current transaction number.
521
- #
522
- # @since 2.6.0
523
- def txn_num
524
- if ended?
525
- raise Error::SessionEnded
526
- end
384
+ if Time.now >= deadline
385
+ transaction_in_progress = false
386
+ raise
387
+ end
527
388
 
528
- @server_session.txn_num
529
- end
389
+ if e.is_a?(Mongo::Error) && e.label?('TransientTransactionError')
390
+ next
391
+ end
530
392
 
531
- # Is this session an implicit one (not user-created).
532
- #
533
- # @example Is the session implicit?
534
- # session.implicit?
535
- #
536
- # @return [ true, false ] Whether this session is implicit.
537
- #
538
- # @since 2.5.1
539
- def implicit?
540
- @implicit ||= !!(@options.key?(:implicit) && @options[:implicit] == true)
541
- end
393
+ raise
394
+ else
395
+ if within_states?(TRANSACTION_ABORTED_STATE, NO_TRANSACTION_STATE, TRANSACTION_COMMITTED_STATE)
396
+ transaction_in_progress = false
397
+ return rv
398
+ end
542
399
 
543
- # Is this session an explicit one (i.e. user-created).
544
- #
545
- # @example Is the session explicit?
546
- # session.explicit?
547
- #
548
- # @return [ true, false ] Whether this session is explicit.
549
- #
550
- # @since 2.5.2
551
- def explicit?
552
- @explicit ||= !implicit?
400
+ begin
401
+ commit_transaction(commit_options)
402
+ transaction_in_progress = false
403
+ return rv
404
+ rescue Mongo::Error => e
405
+ if e.label?('UnknownTransactionCommitResult')
406
+ if Time.now >= deadline ||
407
+ e.is_a?(Error::OperationFailure) && e.max_time_ms_expired?
408
+ then
409
+ transaction_in_progress = false
410
+ raise
411
+ end
412
+ wc_options = case v = commit_options[:write_concern]
413
+ when WriteConcern::Base
414
+ v.options
415
+ when nil
416
+ {}
417
+ else
418
+ v
419
+ end
420
+ commit_options[:write_concern] = wc_options.merge(w: :majority)
421
+ retry
422
+ elsif e.label?('TransientTransactionError')
423
+ if Time.now >= deadline
424
+ transaction_in_progress = false
425
+ raise
426
+ end
427
+ next
428
+ else
429
+ transaction_in_progress = false
430
+ raise
431
+ end
432
+ end
433
+ end
434
+ end
435
+
436
+ # No official return value, but return true so that in interactive
437
+ # use the method hints that it succeeded.
438
+ true
439
+ ensure
440
+ if transaction_in_progress
441
+ log_warn('with_transaction callback altered with_transaction loop, aborting transaction')
442
+ begin
443
+ abort_transaction
444
+ rescue Error::OperationFailure, Error::InvalidTransactionOperation
445
+ end
446
+ end
553
447
  end
554
448
 
555
449
  # Places subsequent operations in this session into a new transaction.
@@ -562,6 +456,8 @@ module Mongo
562
456
  #
563
457
  # @param [ Hash ] options The options for the transaction being started.
564
458
  #
459
+ # @option options [ Integer ] :max_commit_time_ms The maximum amount of
460
+ # time to allow a single commitTransaction command to run, in milliseconds.
565
461
  # @option options [ Hash ] read_concern The read concern options hash,
566
462
  # with the following optional keys:
567
463
  # - *:level* -- the read preference level as a symbol; valid values
@@ -580,6 +476,18 @@ module Mongo
580
476
  def start_transaction(options = nil)
581
477
  if options
582
478
  Lint.validate_read_concern_option(options[:read_concern])
479
+
480
+ =begin
481
+ # It would be handy to detect invalid read preferences here, but
482
+ # some of the spec tests require later detection of invalid read prefs.
483
+ # Maybe we can do this when lint mode is on.
484
+ mode = options[:read] && options[:read][:mode].to_s
485
+ if mode && mode != 'primary'
486
+ raise Mongo::Error::InvalidTransactionOperation.new(
487
+ "read preference in a transaction must be primary (requested: #{mode})"
488
+ )
489
+ end
490
+ =end
583
491
  end
584
492
 
585
493
  check_if_ended!
@@ -589,16 +497,24 @@ module Mongo
589
497
  Mongo::Error::InvalidTransactionOperation::TRANSACTION_ALREADY_IN_PROGRESS)
590
498
  end
591
499
 
500
+ unpin
501
+
592
502
  next_txn_num
593
- @txn_options = options || @options[:default_transaction_options] || {}
503
+ @txn_options = (@options[:default_transaction_options] || {}).merge(options || {})
594
504
 
595
- if txn_write_concern && WriteConcern.send(:unacknowledged?, txn_write_concern)
505
+ if txn_write_concern && !WriteConcern.get(txn_write_concern).acknowledged?
596
506
  raise Mongo::Error::InvalidTransactionOperation.new(
597
507
  Mongo::Error::InvalidTransactionOperation::UNACKNOWLEDGED_WRITE_CONCERN)
598
508
  end
599
509
 
600
510
  @state = STARTING_TRANSACTION_STATE
601
511
  @already_committed = false
512
+
513
+ # This method has no explicit return value.
514
+ # We could return nil here but true indicates to the user that the
515
+ # operation succeeded. This is intended for interactive use.
516
+ # Note that the return value is not documented.
517
+ true
602
518
  end
603
519
 
604
520
  # Commit the currently active transaction on the session.
@@ -636,6 +552,7 @@ module Mongo
636
552
  @last_commit_skipped = true
637
553
  else
638
554
  @last_commit_skipped = false
555
+ @committing_transaction = true
639
556
 
640
557
  write_concern = options[:write_concern] || txn_options[:write_concern]
641
558
  if write_concern && !write_concern.is_a?(WriteConcern::Base)
@@ -651,30 +568,24 @@ module Mongo
651
568
  write_concern = WriteConcern.get(w: :majority, wtimeout: 10000)
652
569
  end
653
570
  end
654
- Operation::Command.new(
571
+ spec = {
655
572
  selector: { commitTransaction: 1 },
656
573
  db_name: 'admin',
657
574
  session: self,
658
575
  txn_num: txn_num,
659
576
  write_concern: write_concern,
660
- ).execute(server)
577
+ }
578
+ Operation::Command.new(spec).execute(server)
661
579
  end
662
580
  end
663
- rescue Mongo::Error::NoServerAvailable, Mongo::Error::SocketError => e
664
- e.send(:add_label, Mongo::Error::UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)
665
- raise e
666
- rescue Mongo::Error::OperationFailure => e
667
- err_doc = e.instance_variable_get(:@result).send(:first_document)
668
-
669
- if e.write_retryable? || (err_doc['writeConcernError'] &&
670
- !UNLABELED_WRITE_CONCERN_CODES.include?(err_doc['writeConcernError']['code']))
671
- e.send(:add_label, Mongo::Error::UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)
672
- end
673
-
674
- raise e
675
581
  ensure
676
582
  @state = TRANSACTION_COMMITTED_STATE
583
+ @committing_transaction = false
677
584
  end
585
+
586
+ # No official return value, but return true so that in interactive
587
+ # use the method hints that it succeeded.
588
+ true
678
589
  end
679
590
 
680
591
  # Abort the currently active transaction without making any changes to the database.
@@ -721,180 +632,365 @@ module Mongo
721
632
  @state = TRANSACTION_ABORTED_STATE
722
633
  raise
723
634
  end
635
+
636
+ # No official return value, but return true so that in interactive
637
+ # use the method hints that it succeeded.
638
+ true
639
+ end
640
+
641
+ # @api private
642
+ def starting_transaction?
643
+ within_states?(STARTING_TRANSACTION_STATE)
644
+ end
645
+
646
+ # Whether or not the session is currently in a transaction.
647
+ #
648
+ # @example Is the session in a transaction?
649
+ # session.in_transaction?
650
+ #
651
+ # @return [ true | false ] Whether or not the session in a transaction.
652
+ #
653
+ # @since 2.6.0
654
+ def in_transaction?
655
+ within_states?(STARTING_TRANSACTION_STATE, TRANSACTION_IN_PROGRESS_STATE)
656
+ end
657
+
658
+ # @return [ true | false ] Whether the session is currently committing a
659
+ # transaction.
660
+ #
661
+ # @api private
662
+ def committing_transaction?
663
+ !!@committing_transaction
664
+ end
665
+
666
+ # Pins this session to the specified server, which should be a mongos.
667
+ #
668
+ # @param [ Server ] server The server to pin this session to.
669
+ #
670
+ # @api private
671
+ def pin(server)
672
+ if server.nil?
673
+ raise ArgumentError, 'Cannot pin to a nil server'
674
+ end
675
+ if Lint.enabled?
676
+ unless server.mongos?
677
+ raise Error::LintError, "Attempted to pin the session to server #{server.summary} which is not a mongos"
678
+ end
679
+ end
680
+ @pinned_server = server
681
+ end
682
+
683
+ # Unpins this session from the pinned server, if the session was pinned.
684
+ #
685
+ # @api private
686
+ def unpin
687
+ @pinned_server = nil
688
+ end
689
+
690
+ # Unpins this session from the pinned server, if the session was pinned
691
+ # and the specified exception instance and the session's transaction state
692
+ # require it to be unpinned.
693
+ #
694
+ # The exception instance should already have all of the labels set on it
695
+ # (both client- and server-side generated ones).
696
+ #
697
+ # @param [ Error ] The exception instance to process.
698
+ #
699
+ # @api private
700
+ def unpin_maybe(error)
701
+ if !within_states?(Session::NO_TRANSACTION_STATE) &&
702
+ error.label?('TransientTransactionError')
703
+ then
704
+ unpin
705
+ end
706
+
707
+ if committing_transaction? &&
708
+ error.label?('UnknownTransactionCommitResult')
709
+ then
710
+ unpin
711
+ end
712
+ end
713
+
714
+ # Add the autocommit field to a command document if applicable.
715
+ #
716
+ # @example
717
+ # session.add_autocommit!(cmd)
718
+ #
719
+ # @return [ Hash, BSON::Document ] The command document.
720
+ #
721
+ # @since 2.6.0
722
+ # @api private
723
+ def add_autocommit!(command)
724
+ command.tap do |c|
725
+ c[:autocommit] = false if in_transaction?
726
+ end
727
+ end
728
+
729
+ # Add this session's id to a command document.
730
+ #
731
+ # @example
732
+ # session.add_id!(cmd)
733
+ #
734
+ # @return [ Hash, BSON::Document ] The command document.
735
+ #
736
+ # @since 2.5.0
737
+ # @api private
738
+ def add_id!(command)
739
+ command.merge!(lsid: session_id)
740
+ end
741
+
742
+ # Add the startTransaction field to a command document if applicable.
743
+ #
744
+ # @example
745
+ # session.add_start_transaction!(cmd)
746
+ #
747
+ # @return [ Hash, BSON::Document ] The command document.
748
+ #
749
+ # @since 2.6.0
750
+ # @api private
751
+ def add_start_transaction!(command)
752
+ command.tap do |c|
753
+ if starting_transaction?
754
+ c[:startTransaction] = true
755
+ end
756
+ end
757
+ end
758
+
759
+ # Add the transaction number to a command document if applicable.
760
+ #
761
+ # @example
762
+ # session.add_txn_num!(cmd)
763
+ #
764
+ # @return [ Hash, BSON::Document ] The command document.
765
+ #
766
+ # @since 2.6.0
767
+ # @api private
768
+ def add_txn_num!(command)
769
+ command.tap do |c|
770
+ c[:txnNumber] = BSON::Int64.new(@server_session.txn_num) if in_transaction?
771
+ end
772
+ end
773
+
774
+ # Add the transactions options if applicable.
775
+ #
776
+ # @example
777
+ # session.add_txn_opts!(cmd)
778
+ #
779
+ # @return [ Hash, BSON::Document ] The command document.
780
+ #
781
+ # @since 2.6.0
782
+ # @api private
783
+ def add_txn_opts!(command, read)
784
+ command.tap do |c|
785
+ # The read concern should be added to any command that starts a transaction.
786
+ if starting_transaction?
787
+ # https://jira.mongodb.org/browse/SPEC-1161: transaction's
788
+ # read concern overrides collection/database/client read concerns,
789
+ # even if transaction's read concern is not set.
790
+ # Read concern here is the one sent to the server and may
791
+ # include afterClusterTime.
792
+ if rc = c[:readConcern]
793
+ rc = rc.dup
794
+ rc.delete(:level)
795
+ end
796
+ if txn_read_concern
797
+ if rc
798
+ rc.update(txn_read_concern)
799
+ else
800
+ rc = txn_read_concern.dup
801
+ end
802
+ end
803
+ if rc.nil? || rc.empty?
804
+ c.delete(:readConcern)
805
+ else
806
+ c[:readConcern ] = Options::Mapper.transform_values_to_strings(rc)
807
+ end
808
+ end
809
+
810
+ # We need to send the read concern level as a string rather than a symbol.
811
+ if c[:readConcern]
812
+ c[:readConcern] = Options::Mapper.transform_values_to_strings(c[:readConcern])
813
+ end
814
+
815
+ if c[:commitTransaction]
816
+ if max_time_ms = txn_options[:max_commit_time_ms]
817
+ c[:maxTimeMS] = max_time_ms
818
+ end
819
+ end
820
+
821
+ # The write concern should be added to any abortTransaction or commitTransaction command.
822
+ if (c[:abortTransaction] || c[:commitTransaction])
823
+ if @already_committed
824
+ wc = BSON::Document.new(c[:writeConcern] || txn_write_concern || {})
825
+ wc.merge!(w: :majority)
826
+ wc[:wtimeout] ||= 10000
827
+ c[:writeConcern] = wc
828
+ elsif txn_write_concern
829
+ c[:writeConcern] ||= txn_write_concern
830
+ end
831
+ end
832
+
833
+ # A non-numeric write concern w value needs to be sent as a string rather than a symbol.
834
+ if c[:writeConcern] && c[:writeConcern][:w] && c[:writeConcern][:w].is_a?(Symbol)
835
+ c[:writeConcern][:w] = c[:writeConcern][:w].to_s
836
+ end
837
+ end
724
838
  end
725
839
 
726
- # Whether or not the session is currently in a transaction.
840
+ # Remove the read concern and/or write concern from the command if not applicable.
727
841
  #
728
- # @example Is the session in a transaction?
729
- # session.in_transaction?
842
+ # @example
843
+ # session.suppress_read_write_concern!(cmd)
730
844
  #
731
- # @return [ true | false ] Whether or not the session in a transaction.
845
+ # @return [ Hash, BSON::Document ] The command document.
732
846
  #
733
847
  # @since 2.6.0
734
- def in_transaction?
735
- within_states?(STARTING_TRANSACTION_STATE, TRANSACTION_IN_PROGRESS_STATE)
848
+ # @api private
849
+ def suppress_read_write_concern!(command)
850
+ command.tap do |c|
851
+ next unless in_transaction?
852
+
853
+ c.delete(:readConcern) unless starting_transaction?
854
+ c.delete(:writeConcern) unless c[:commitTransaction] || c[:abortTransaction]
855
+ end
736
856
  end
737
857
 
738
- # Executes the provided block in a transaction, retrying as necessary.
858
+ # Ensure that the read preference of a command primary.
739
859
  #
740
- # Returns the return value of the block.
860
+ # @example
861
+ # session.validate_read_preference!(command)
741
862
  #
742
- # Exact number of retries and when they are performed are implementation
743
- # details of the driver; the provided block should be idempotent, and
744
- # should be prepared to be called more than once. The driver may retry
745
- # the commit command within an active transaction or it may repeat the
746
- # transaction and invoke the block again, depending on the error
747
- # encountered if any. Note also that the retries may be executed against
748
- # different servers.
863
+ # @raise [ Mongo::Error::InvalidTransactionOperation ] If the read preference of the command is
864
+ # not primary.
749
865
  #
750
- # Transactions cannot be nested - InvalidTransactionOperation will be raised
751
- # if this method is called when the session already has an active transaction.
866
+ # @since 2.6.0
867
+ # @api private
868
+ def validate_read_preference!(command)
869
+ return unless in_transaction?
870
+ return unless command['$readPreference']
871
+
872
+ mode = command['$readPreference']['mode'] || command['$readPreference'][:mode]
873
+
874
+ if mode && mode != 'primary'
875
+ raise Mongo::Error::InvalidTransactionOperation.new(
876
+ "read preference in a transaction must be primary (requested: #{mode})"
877
+ )
878
+ end
879
+ end
880
+
881
+ # Update the state of the session due to a (non-commit and non-abort) operation being run.
752
882
  #
753
- # Exceptions raised by the block which are not derived from Mongo::Error
754
- # stop processing, abort the transaction and are propagated out of
755
- # with_transaction. Exceptions derived from Mongo::Error may be
756
- # handled by with_transaction, resulting in retries of the process.
883
+ # @since 2.6.0
884
+ # @api private
885
+ def update_state!
886
+ case @state
887
+ when STARTING_TRANSACTION_STATE
888
+ @state = TRANSACTION_IN_PROGRESS_STATE
889
+ when TRANSACTION_COMMITTED_STATE, TRANSACTION_ABORTED_STATE
890
+ @state = NO_TRANSACTION_STATE
891
+ end
892
+ end
893
+
894
+ # Validate the session.
757
895
  #
758
- # Currently, with_transaction will retry commits and block invocations
759
- # until at least 120 seconds have passed since with_transaction started
760
- # executing. This timeout is not configurable and may change in a future
761
- # driver version.
896
+ # @example
897
+ # session.validate!(cluster)
762
898
  #
763
- # @note with_transaction contains a loop, therefore the if with_transaction
764
- # itself is placed in a loop, its block should not call next or break to
765
- # control the outer loop because this will instead affect the loop in
766
- # with_transaction. The driver will warn and abort the transaction
767
- # if it detects this situation.
899
+ # @param [ Cluster ] cluster The cluster the session is attempted to be used with.
768
900
  #
769
- # @example Execute a statement in a transaction
770
- # session.with_transaction(write_concern: {w: :majority}) do
771
- # collection.update_one({ id: 3 }, { '$set' => { status: 'Inactive'} },
772
- # session: session)
901
+ # @return [ nil ] nil if the session is valid.
773
902
  #
774
- # end
903
+ # @raise [ Mongo::Error::InvalidSession ] Raise error if the session is not valid.
775
904
  #
776
- # @example Execute a statement in a transaction, limiting total time consumed
777
- # Timeout.timeout(5) do
778
- # session.with_transaction(write_concern: {w: :majority}) do
779
- # collection.update_one({ id: 3 }, { '$set' => { status: 'Inactive'} },
780
- # session: session)
905
+ # @since 2.5.0
906
+ # @api private
907
+ def validate!(cluster)
908
+ check_matching_cluster!(cluster)
909
+ check_if_ended!
910
+ self
911
+ end
912
+
913
+ # Process a response from the server that used this session.
781
914
  #
782
- # end
783
- # end
915
+ # @example Process a response from the server.
916
+ # session.process(result)
784
917
  #
785
- # @param [ Hash ] options The options for the transaction being started.
786
- # These are the same options that start_transaction accepts.
918
+ # @param [ Operation::Result ] result The result from the operation.
787
919
  #
788
- # @raise [ Error::InvalidTransactionOperation ] If a transaction is already in
789
- # progress or if the write concern is unacknowledged.
920
+ # @return [ Operation::Result ] The result.
790
921
  #
791
- # @since 2.7.0
792
- def with_transaction(options=nil)
793
- # Non-configurable 120 second timeout for the entire operation
794
- deadline = Time.now + 120
795
- transaction_in_progress = false
796
- loop do
797
- commit_options = {}
798
- if options
799
- commit_options[:write_concern] = options[:write_concern]
922
+ # @since 2.5.0
923
+ # @api private
924
+ def process(result)
925
+ unless implicit?
926
+ set_operation_time(result)
927
+ if cluster_time_doc = result.cluster_time
928
+ advance_cluster_time(cluster_time_doc)
800
929
  end
801
- start_transaction(options)
802
- transaction_in_progress = true
803
- begin
804
- rv = yield self
805
- rescue Exception => e
806
- if within_states?(STARTING_TRANSACTION_STATE, TRANSACTION_IN_PROGRESS_STATE)
807
- abort_transaction
808
- transaction_in_progress = false
809
- end
810
-
811
- if Time.now >= deadline
812
- transaction_in_progress = false
813
- raise
814
- end
815
-
816
- if e.is_a?(Mongo::Error) && e.label?(Mongo::Error::TRANSIENT_TRANSACTION_ERROR_LABEL)
817
- next
818
- end
819
-
820
- raise
821
- else
822
- if within_states?(TRANSACTION_ABORTED_STATE, NO_TRANSACTION_STATE, TRANSACTION_COMMITTED_STATE)
823
- transaction_in_progress = false
824
- return rv
825
- end
930
+ end
931
+ @server_session.set_last_use!
826
932
 
827
- begin
828
- commit_transaction(commit_options)
829
- transaction_in_progress = false
830
- return rv
831
- rescue Mongo::Error => e
832
- if e.label?(Mongo::Error::UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)
833
- # WriteConcernFailed
834
- if e.is_a?(Mongo::Error::OperationFailure) && e.code == 64 && e.wtimeout?
835
- transaction_in_progress = false
836
- raise
837
- end
838
- if Time.now >= deadline
839
- transaction_in_progress = false
840
- raise
841
- end
842
- wc_options = case v = commit_options[:write_concern]
843
- when WriteConcern::Base
844
- v.options
845
- when nil
846
- {}
847
- else
848
- v
849
- end
850
- commit_options[:write_concern] = wc_options.merge(w: :majority)
851
- retry
852
- elsif e.label?(Mongo::Error::TRANSIENT_TRANSACTION_ERROR_LABEL)
853
- if Time.now >= deadline
854
- transaction_in_progress = false
855
- raise
856
- end
857
- next
858
- else
859
- transaction_in_progress = false
860
- raise
861
- end
862
- end
933
+ if doc = result.reply && result.reply.documents.first
934
+ if doc[:recoveryToken]
935
+ self.recovery_token = doc[:recoveryToken]
863
936
  end
864
937
  end
865
- ensure
866
- if transaction_in_progress
867
- log_warn('with_transaction callback altered with_transaction loop, aborting transaction')
868
- begin
869
- abort_transaction
870
- rescue Error::OperationFailure, Error::InvalidTransactionOperation
871
- end
938
+
939
+ result
940
+ end
941
+
942
+ # Advance the cached operation time for this session.
943
+ #
944
+ # @example Advance the operation time.
945
+ # session.advance_operation_time(timestamp)
946
+ #
947
+ # @param [ BSON::Timestamp ] new_operation_time The new operation time.
948
+ #
949
+ # @return [ BSON::Timestamp ] The max operation time, considering the current and new times.
950
+ #
951
+ # @since 2.5.0
952
+ def advance_operation_time(new_operation_time)
953
+ if @operation_time
954
+ @operation_time = [ @operation_time, new_operation_time ].max
955
+ else
956
+ @operation_time = new_operation_time
872
957
  end
873
958
  end
874
959
 
875
- # Get the read preference the session will use in the currently
876
- # active transaction.
960
+ # Increment and return the next transaction number.
877
961
  #
878
- # This is a driver style hash with underscore keys.
962
+ # @example Get the next transaction number.
963
+ # session.next_txn_num
879
964
  #
880
- # @example Get the transaction's read preference
881
- # session.txn_read_preference
965
+ # @return [ Integer ] The next transaction number.
882
966
  #
883
- # @return [ Hash ] The read preference of the transaction.
967
+ # @since 2.5.0
968
+ # @api private
969
+ def next_txn_num
970
+ if ended?
971
+ raise Error::SessionEnded
972
+ end
973
+
974
+ @server_session.next_txn_num
975
+ end
976
+
977
+ # Get the current transaction number.
978
+ #
979
+ # @example Get the current transaction number.
980
+ # session.txn_num
981
+ #
982
+ # @return [ Integer ] The current transaction number.
884
983
  #
885
984
  # @since 2.6.0
886
- def txn_read_preference
887
- rp = txn_options && txn_options[:read_preference] ||
888
- @client.read_preference
889
- Mongo::Lint.validate_underscore_read_preference(rp)
890
- rp
891
- end
985
+ def txn_num
986
+ if ended?
987
+ raise Error::SessionEnded
988
+ end
892
989
 
893
- def cluster
894
- @client.cluster
990
+ @server_session.txn_num
895
991
  end
896
992
 
897
- protected
993
+ private
898
994
 
899
995
  # Get the read concern the session will use when starting a transaction.
900
996
  #
@@ -908,19 +1004,13 @@ module Mongo
908
1004
  # @since 2.9.0
909
1005
  def txn_read_concern
910
1006
  # Read concern is inherited from client but not db or collection.
911
- txn_options && txn_options[:read_concern] || @client.read_concern
1007
+ txn_options[:read_concern] || @client.read_concern
912
1008
  end
913
1009
 
914
- private
915
-
916
1010
  def within_states?(*states)
917
1011
  states.include?(@state)
918
1012
  end
919
1013
 
920
- def starting_transaction?
921
- within_states?(STARTING_TRANSACTION_STATE)
922
- end
923
-
924
1014
  def check_if_no_transaction!
925
1015
  return unless within_states?(NO_TRANSACTION_STATE)
926
1016
 
@@ -929,17 +1019,10 @@ module Mongo
929
1019
  end
930
1020
 
931
1021
  def txn_write_concern
932
- (txn_options && txn_options[:write_concern]) ||
1022
+ txn_options[:write_concern] ||
933
1023
  (@client.write_concern && @client.write_concern.options)
934
1024
  end
935
1025
 
936
- def non_primary_read_preference_mode?(command)
937
- return false unless command['$readPreference']
938
-
939
- mode = command['$readPreference']['mode'] || command['$readPreference'][:mode]
940
- mode && mode != 'primary'
941
- end
942
-
943
1026
  # Returns causal consistency document if the last operation time is
944
1027
  # known and causal consistency is enabled, otherwise returns nil.
945
1028
  def causal_consistency_doc
@@ -964,16 +1047,6 @@ module Mongo
964
1047
  end
965
1048
  end
966
1049
 
967
- def set_cluster_time(result)
968
- if cluster_time_doc = result.cluster_time
969
- if @cluster_time.nil?
970
- @cluster_time = cluster_time_doc
971
- elsif cluster_time_doc[Cluster::CLUSTER_TIME] > @cluster_time[Cluster::CLUSTER_TIME]
972
- @cluster_time = cluster_time_doc
973
- end
974
- end
975
- end
976
-
977
1050
  def check_if_ended!
978
1051
  raise Mongo::Error::InvalidSession.new(SESSION_ENDED_ERROR_MSG) if ended?
979
1052
  end