mongo 2.9.2 → 2.10.0.rc0

Sign up to get free protection for your applications and to get access to all the features.
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