@@ -61,15 +61,51 @@ class SessionOptions(object):
6161 :Parameters:
6262 - `causal_consistency` (optional): If True (the default), read
6363 operations are causally ordered within the session.
64+ - `auto_start_transaction` (optional): If True, any operation using
65+ the session begins a transaction if none is in progress.
6466 """
65- def __init__ (self , causal_consistency = True ):
67+ # TODO: accept a TransactionOptions.
68+ def __init__ (self ,
69+ causal_consistency = True ,
70+ auto_start_transaction = False ):
6671 self ._causal_consistency = causal_consistency
72+ self ._auto_start_transaction = auto_start_transaction
6773
6874 @property
6975 def causal_consistency (self ):
7076 """Whether causal consistency is configured."""
7177 return self ._causal_consistency
7278
79+ @property
80+ def auto_start_transaction (self ):
81+ """Whether the session is configured to always start a transaction."""
82+ return self ._auto_start_transaction
83+
84+
85+ class TransactionOptions (object ):
86+ """Options for :meth:`ClientSession.start_transaction`.
87+
88+ :Parameters:
89+ - `read_concern`: The :class:`~read_concern.ReadConcern` to use for this
90+ transaction.
91+ - `write_concern`: The :class:`~write_concern.WriteConcern` to use for
92+ this transaction.
93+ """
94+ def __init__ (self , read_concern = None , write_concern = None ):
95+ # TODO: validate arguments.
96+ self ._read_concern = read_concern
97+ self ._write_concern = write_concern
98+
99+ @property
100+ def read_concern (self ):
101+ """This transaction's :class:`~read_concern.ReadConcern`."""
102+ return self ._read_concern
103+
104+ @property
105+ def write_concern (self ):
106+ """This transaction's :class:`~write_concern.WriteConcern`."""
107+ return self ._write_concern
108+
73109
74110class ClientSession (object ):
75111 """A session for ordering sequential operations."""
@@ -81,27 +117,41 @@ def __init__(self, client, server_session, options, authset):
81117 self ._authset = authset
82118 self ._cluster_time = None
83119 self ._operation_time = None
120+ self ._transaction_options = None # Current transaction's options.
121+ if self .options .auto_start_transaction :
122+ # TODO: Get transaction options from self.options.
123+ self ._transaction_options = TransactionOptions ()
124+ self ._server_session .start_transaction ()
84125
85126 def end_session (self ):
86- """Finish this session.
127+ """Finish this session. If a transaction has started, abort it.
87128
88129 It is an error to use the session or any derived
89130 :class:`~pymongo.database.Database`,
90131 :class:`~pymongo.collection.Collection`, or
91132 :class:`~pymongo.cursor.Cursor` after the session has ended.
92133 """
93- self ._end_session (True )
134+ self ._end_session (lock = True , abort_txn = True )
94135
95- def _end_session (self , lock ):
136+ def _end_session (self , lock , abort_txn ):
96137 if self ._server_session is not None :
97- self ._client ._return_server_session (self ._server_session , lock )
98- self ._server_session = None
138+ try :
139+ if self .in_transaction :
140+ if abort_txn :
141+ self .abort_txn ()
142+ else :
143+ self .commit_transaction ()
144+ finally :
145+ self ._client ._return_server_session (self ._server_session , lock )
146+ self ._server_session = None
99147
100148 def __enter__ (self ):
101149 return self
102150
103151 def __exit__ (self , exc_type , exc_val , exc_tb ):
104- self .end_session ()
152+ # Abort when exiting with an exception, otherwise commit.
153+ # TODO: test and document this.
154+ self ._end_session (lock = True , abort_txn = exc_val is not None )
105155
106156 @property
107157 def client (self ):
@@ -137,6 +187,45 @@ def operation_time(self):
137187 """
138188 return self ._operation_time
139189
190+ def start_transaction (self , ** kwargs ):
191+ """Start a multi-statement transaction.
192+
193+ Takes the same arguments as :class:`TransactionOptions`.
194+
195+ Do not use this method if the session is configured to automatically
196+ start a transaction.
197+ """
198+ self ._transaction_options = TransactionOptions (** kwargs )
199+ self ._server_session .start_transaction ()
200+
201+ def commit_transaction (self ):
202+ """Commit a multi-statement transaction."""
203+ self ._finish_transaction ("commitTransaction" )
204+
205+ def abort_txn (self ):
206+ """Abort a multi-statement transaction."""
207+ assert False , "Not implemented" # Await server.
208+ self ._finish_transaction ("abortTransaction" )
209+
210+ def _finish_transaction (self , command_name ):
211+ if (self .options .auto_start_transaction
212+ and self ._server_session .statement_id == 0 ):
213+ # Not really started.
214+ return
215+
216+ try :
217+ # TODO: retryable. And it's weird to pass parse_write_concern_error
218+ # from outside database.py.
219+ self ._client .admin .command (
220+ command_name ,
221+ txnNumber = self ._server_session .transaction_id ,
222+ session = self ,
223+ write_concern = self ._transaction_options .write_concern ,
224+ parse_write_concern_error = True )
225+ finally :
226+ self ._server_session .reset_transaction ()
227+ self ._transaction_options = None
228+
140229 def _advance_cluster_time (self , cluster_time ):
141230 """Internal cluster time helper."""
142231 if self ._cluster_time is None :
@@ -186,19 +275,28 @@ def has_ended(self):
186275 """True if this session is finished."""
187276 return self ._server_session is None
188277
189- def _use_lsid (self ):
278+ @property
279+ def in_transaction (self ):
280+ """True if this session has an active multi-statement transaction."""
281+ return (self ._server_session is not None
282+ and self ._server_session .in_transaction )
283+
284+ def _apply_to (self , command , is_retryable ):
190285 # Internal function.
191286 if self ._server_session is None :
192287 raise InvalidOperation ("Cannot use ended session" )
193288
194- return self ._server_session .use_lsid ()
289+ if self .options .auto_start_transaction and not self .in_transaction :
290+ self .start_transaction ()
291+
292+ self ._server_session .apply_to (command , is_retryable )
195293
196- def _transaction_id (self ):
294+ def _advance_statement_id (self , n ):
197295 # Internal function.
198296 if self ._server_session is None :
199297 raise InvalidOperation ("Cannot use ended session" )
200298
201- return self ._server_session .transaction_id ( )
299+ self ._server_session .advance_statement_id ( n )
202300
203301 def _retry_transaction_id (self ):
204302 # Internal function.
@@ -213,23 +311,53 @@ def __init__(self):
213311 # Ensure id is type 4, regardless of CodecOptions.uuid_representation.
214312 self .session_id = {'id' : Binary (uuid .uuid4 ().bytes , 4 )}
215313 self .last_use = monotonic .time ()
314+ self .in_transaction = False
216315 self ._transaction_id = 0
316+ self .statement_id = 0
217317
218318 def timed_out (self , session_timeout_minutes ):
219319 idle_seconds = monotonic .time () - self .last_use
220320
221321 # Timed out if we have less than a minute to live.
222322 return idle_seconds > (session_timeout_minutes - 1 ) * 60
223323
224- def use_lsid (self ):
324+ def apply_to (self , command , is_retryable ):
325+ command ['lsid' ] = self .session_id
326+
327+ if is_retryable :
328+ self ._transaction_id += 1
329+ command ['txnNumber' ] = self .transaction_id
330+ elif self .in_transaction :
331+ command ['txnNumber' ] = self .transaction_id
332+ # TODO: Allow stmtId for find/getMore, SERVER-33213.
333+ name = next (iter (command ))
334+ if name not in ('find' , 'getMore' ):
335+ command ['stmtId' ] = self .statement_id
336+ if self .statement_id == 0 :
337+ command ['readConcern' ] = {'level' : 'snapshot' }
338+ command ['autocommit' ] = False
339+ self .statement_id += 1
340+
225341 self .last_use = monotonic .time ()
226- return self .session_id
227342
343+ def advance_statement_id (self , n ):
344+ # Every command advances the statement id by 1 already.
345+ self .statement_id += (n - 1 )
346+
347+ @property
228348 def transaction_id (self ):
229- """Monotonically increasing positive 64-bit integer."""
230- self ._transaction_id += 1
349+ """Positive 64-bit integer."""
231350 return Int64 (self ._transaction_id )
232351
352+ def start_transaction (self ):
353+ self ._transaction_id += 1
354+ self .statement_id = 0
355+ self .in_transaction = True
356+
357+ def reset_transaction (self ):
358+ self .in_transaction = False
359+ self .statement_id = 0
360+
233361 def retry_transaction_id (self ):
234362 self ._transaction_id -= 1
235363
0 commit comments