17 except ImportError: |
17 except ImportError: |
18 import dummy_thread as thread |
18 import dummy_thread as thread |
19 try: |
19 try: |
20 from functools import wraps |
20 from functools import wraps |
21 except ImportError: |
21 except ImportError: |
22 from django.utils.functional import wraps # Python 2.3, 2.4 fallback. |
22 from django.utils.functional import wraps # Python 2.4 fallback. |
23 from django.db import connection |
23 from django.db import connections, DEFAULT_DB_ALIAS |
24 from django.conf import settings |
24 from django.conf import settings |
25 |
25 |
26 class TransactionManagementError(Exception): |
26 class TransactionManagementError(Exception): |
27 """ |
27 """ |
28 This exception is thrown when something bad happens with transaction |
28 This exception is thrown when something bad happens with transaction |
29 management. |
29 management. |
30 """ |
30 """ |
31 pass |
31 pass |
32 |
32 |
33 # The states are dictionaries of lists. The key to the dict is the current |
33 # The states are dictionaries of dictionaries of lists. The key to the outer |
34 # thread and the list is handled as a stack of values. |
34 # dict is the current thread, and the key to the inner dictionary is the |
|
35 # connection alias and the list is handled as a stack of values. |
35 state = {} |
36 state = {} |
36 savepoint_state = {} |
37 savepoint_state = {} |
37 |
38 |
38 # The dirty flag is set by *_unless_managed functions to denote that the |
39 # The dirty flag is set by *_unless_managed functions to denote that the |
39 # code under transaction management has changed things to require a |
40 # code under transaction management has changed things to require a |
40 # database commit. |
41 # database commit. |
|
42 # This is a dictionary mapping thread to a dictionary mapping connection |
|
43 # alias to a boolean. |
41 dirty = {} |
44 dirty = {} |
42 |
45 |
43 def enter_transaction_management(managed=True): |
46 def enter_transaction_management(managed=True, using=None): |
44 """ |
47 """ |
45 Enters transaction management for a running thread. It must be balanced with |
48 Enters transaction management for a running thread. It must be balanced with |
46 the appropriate leave_transaction_management call, since the actual state is |
49 the appropriate leave_transaction_management call, since the actual state is |
47 managed as a stack. |
50 managed as a stack. |
48 |
51 |
49 The state and dirty flag are carried over from the surrounding block or |
52 The state and dirty flag are carried over from the surrounding block or |
50 from the settings, if there is no surrounding block (dirty is always false |
53 from the settings, if there is no surrounding block (dirty is always false |
51 when no current block is running). |
54 when no current block is running). |
52 """ |
55 """ |
53 thread_ident = thread.get_ident() |
56 if using is None: |
54 if thread_ident in state and state[thread_ident]: |
57 using = DEFAULT_DB_ALIAS |
55 state[thread_ident].append(state[thread_ident][-1]) |
58 connection = connections[using] |
56 else: |
59 thread_ident = thread.get_ident() |
57 state[thread_ident] = [] |
60 if thread_ident in state and state[thread_ident].get(using): |
58 state[thread_ident].append(settings.TRANSACTIONS_MANAGED) |
61 state[thread_ident][using].append(state[thread_ident][using][-1]) |
59 if thread_ident not in dirty: |
62 else: |
60 dirty[thread_ident] = False |
63 state.setdefault(thread_ident, {}) |
|
64 state[thread_ident][using] = [settings.TRANSACTIONS_MANAGED] |
|
65 if thread_ident not in dirty or using not in dirty[thread_ident]: |
|
66 dirty.setdefault(thread_ident, {}) |
|
67 dirty[thread_ident][using] = False |
61 connection._enter_transaction_management(managed) |
68 connection._enter_transaction_management(managed) |
62 |
69 |
63 def leave_transaction_management(): |
70 def leave_transaction_management(using=None): |
64 """ |
71 """ |
65 Leaves transaction management for a running thread. A dirty flag is carried |
72 Leaves transaction management for a running thread. A dirty flag is carried |
66 over to the surrounding block, as a commit will commit all changes, even |
73 over to the surrounding block, as a commit will commit all changes, even |
67 those from outside. (Commits are on connection level.) |
74 those from outside. (Commits are on connection level.) |
68 """ |
75 """ |
69 connection._leave_transaction_management(is_managed()) |
76 if using is None: |
70 thread_ident = thread.get_ident() |
77 using = DEFAULT_DB_ALIAS |
71 if thread_ident in state and state[thread_ident]: |
78 connection = connections[using] |
72 del state[thread_ident][-1] |
79 connection._leave_transaction_management(is_managed(using=using)) |
|
80 thread_ident = thread.get_ident() |
|
81 if thread_ident in state and state[thread_ident].get(using): |
|
82 del state[thread_ident][using][-1] |
73 else: |
83 else: |
74 raise TransactionManagementError("This code isn't under transaction management") |
84 raise TransactionManagementError("This code isn't under transaction management") |
75 if dirty.get(thread_ident, False): |
85 if dirty.get(thread_ident, {}).get(using, False): |
76 rollback() |
86 rollback(using=using) |
77 raise TransactionManagementError("Transaction managed block ended with pending COMMIT/ROLLBACK") |
87 raise TransactionManagementError("Transaction managed block ended with pending COMMIT/ROLLBACK") |
78 dirty[thread_ident] = False |
88 dirty[thread_ident][using] = False |
79 |
89 |
80 def is_dirty(): |
90 def is_dirty(using=None): |
81 """ |
91 """ |
82 Returns True if the current transaction requires a commit for changes to |
92 Returns True if the current transaction requires a commit for changes to |
83 happen. |
93 happen. |
84 """ |
94 """ |
85 return dirty.get(thread.get_ident(), False) |
95 if using is None: |
86 |
96 using = DEFAULT_DB_ALIAS |
87 def set_dirty(): |
97 return dirty.get(thread.get_ident(), {}).get(using, False) |
|
98 |
|
99 def set_dirty(using=None): |
88 """ |
100 """ |
89 Sets a dirty flag for the current thread and code streak. This can be used |
101 Sets a dirty flag for the current thread and code streak. This can be used |
90 to decide in a managed block of code to decide whether there are open |
102 to decide in a managed block of code to decide whether there are open |
91 changes waiting for commit. |
103 changes waiting for commit. |
92 """ |
104 """ |
93 thread_ident = thread.get_ident() |
105 if using is None: |
94 if thread_ident in dirty: |
106 using = DEFAULT_DB_ALIAS |
95 dirty[thread_ident] = True |
107 thread_ident = thread.get_ident() |
|
108 if thread_ident in dirty and using in dirty[thread_ident]: |
|
109 dirty[thread_ident][using] = True |
96 else: |
110 else: |
97 raise TransactionManagementError("This code isn't under transaction management") |
111 raise TransactionManagementError("This code isn't under transaction management") |
98 |
112 |
99 def set_clean(): |
113 def set_clean(using=None): |
100 """ |
114 """ |
101 Resets a dirty flag for the current thread and code streak. This can be used |
115 Resets a dirty flag for the current thread and code streak. This can be used |
102 to decide in a managed block of code to decide whether a commit or rollback |
116 to decide in a managed block of code to decide whether a commit or rollback |
103 should happen. |
117 should happen. |
104 """ |
118 """ |
105 thread_ident = thread.get_ident() |
119 if using is None: |
106 if thread_ident in dirty: |
120 using = DEFAULT_DB_ALIAS |
107 dirty[thread_ident] = False |
121 thread_ident = thread.get_ident() |
|
122 if thread_ident in dirty and using in dirty[thread_ident]: |
|
123 dirty[thread_ident][using] = False |
108 else: |
124 else: |
109 raise TransactionManagementError("This code isn't under transaction management") |
125 raise TransactionManagementError("This code isn't under transaction management") |
110 clean_savepoints() |
126 clean_savepoints(using=using) |
111 |
127 |
112 def clean_savepoints(): |
128 def clean_savepoints(using=None): |
113 thread_ident = thread.get_ident() |
129 if using is None: |
114 if thread_ident in savepoint_state: |
130 using = DEFAULT_DB_ALIAS |
115 del savepoint_state[thread_ident] |
131 thread_ident = thread.get_ident() |
116 |
132 if thread_ident in savepoint_state and using in savepoint_state[thread_ident]: |
117 def is_managed(): |
133 del savepoint_state[thread_ident][using] |
|
134 |
|
135 def is_managed(using=None): |
118 """ |
136 """ |
119 Checks whether the transaction manager is in manual or in auto state. |
137 Checks whether the transaction manager is in manual or in auto state. |
120 """ |
138 """ |
121 thread_ident = thread.get_ident() |
139 if using is None: |
122 if thread_ident in state: |
140 using = DEFAULT_DB_ALIAS |
123 if state[thread_ident]: |
141 thread_ident = thread.get_ident() |
124 return state[thread_ident][-1] |
142 if thread_ident in state and using in state[thread_ident]: |
|
143 if state[thread_ident][using]: |
|
144 return state[thread_ident][using][-1] |
125 return settings.TRANSACTIONS_MANAGED |
145 return settings.TRANSACTIONS_MANAGED |
126 |
146 |
127 def managed(flag=True): |
147 def managed(flag=True, using=None): |
128 """ |
148 """ |
129 Puts the transaction manager into a manual state: managed transactions have |
149 Puts the transaction manager into a manual state: managed transactions have |
130 to be committed explicitly by the user. If you switch off transaction |
150 to be committed explicitly by the user. If you switch off transaction |
131 management and there is a pending commit/rollback, the data will be |
151 management and there is a pending commit/rollback, the data will be |
132 commited. |
152 commited. |
133 """ |
153 """ |
134 thread_ident = thread.get_ident() |
154 if using is None: |
135 top = state.get(thread_ident, None) |
155 using = DEFAULT_DB_ALIAS |
|
156 connection = connections[using] |
|
157 thread_ident = thread.get_ident() |
|
158 top = state.get(thread_ident, {}).get(using, None) |
136 if top: |
159 if top: |
137 top[-1] = flag |
160 top[-1] = flag |
138 if not flag and is_dirty(): |
161 if not flag and is_dirty(using=using): |
139 connection._commit() |
162 connection._commit() |
140 set_clean() |
163 set_clean(using=using) |
141 else: |
164 else: |
142 raise TransactionManagementError("This code isn't under transaction management") |
165 raise TransactionManagementError("This code isn't under transaction management") |
143 |
166 |
144 def commit_unless_managed(): |
167 def commit_unless_managed(using=None): |
145 """ |
168 """ |
146 Commits changes if the system is not in managed transaction mode. |
169 Commits changes if the system is not in managed transaction mode. |
147 """ |
170 """ |
148 if not is_managed(): |
171 if using is None: |
|
172 using = DEFAULT_DB_ALIAS |
|
173 connection = connections[using] |
|
174 if not is_managed(using=using): |
149 connection._commit() |
175 connection._commit() |
150 clean_savepoints() |
176 clean_savepoints(using=using) |
151 else: |
177 else: |
152 set_dirty() |
178 set_dirty(using=using) |
153 |
179 |
154 def rollback_unless_managed(): |
180 def rollback_unless_managed(using=None): |
155 """ |
181 """ |
156 Rolls back changes if the system is not in managed transaction mode. |
182 Rolls back changes if the system is not in managed transaction mode. |
157 """ |
183 """ |
158 if not is_managed(): |
184 if using is None: |
|
185 using = DEFAULT_DB_ALIAS |
|
186 connection = connections[using] |
|
187 if not is_managed(using=using): |
159 connection._rollback() |
188 connection._rollback() |
160 else: |
189 else: |
161 set_dirty() |
190 set_dirty(using=using) |
162 |
191 |
163 def commit(): |
192 def commit(using=None): |
164 """ |
193 """ |
165 Does the commit itself and resets the dirty flag. |
194 Does the commit itself and resets the dirty flag. |
166 """ |
195 """ |
|
196 if using is None: |
|
197 using = DEFAULT_DB_ALIAS |
|
198 connection = connections[using] |
167 connection._commit() |
199 connection._commit() |
168 set_clean() |
200 set_clean(using=using) |
169 |
201 |
170 def rollback(): |
202 def rollback(using=None): |
171 """ |
203 """ |
172 This function does the rollback itself and resets the dirty flag. |
204 This function does the rollback itself and resets the dirty flag. |
173 """ |
205 """ |
|
206 if using is None: |
|
207 using = DEFAULT_DB_ALIAS |
|
208 connection = connections[using] |
174 connection._rollback() |
209 connection._rollback() |
175 set_clean() |
210 set_clean(using=using) |
176 |
211 |
177 def savepoint(): |
212 def savepoint(using=None): |
178 """ |
213 """ |
179 Creates a savepoint (if supported and required by the backend) inside the |
214 Creates a savepoint (if supported and required by the backend) inside the |
180 current transaction. Returns an identifier for the savepoint that will be |
215 current transaction. Returns an identifier for the savepoint that will be |
181 used for the subsequent rollback or commit. |
216 used for the subsequent rollback or commit. |
182 """ |
217 """ |
183 thread_ident = thread.get_ident() |
218 if using is None: |
184 if thread_ident in savepoint_state: |
219 using = DEFAULT_DB_ALIAS |
185 savepoint_state[thread_ident].append(None) |
220 connection = connections[using] |
186 else: |
221 thread_ident = thread.get_ident() |
187 savepoint_state[thread_ident] = [None] |
222 if thread_ident in savepoint_state and using in savepoint_state[thread_ident]: |
|
223 savepoint_state[thread_ident][using].append(None) |
|
224 else: |
|
225 savepoint_state.setdefault(thread_ident, {}) |
|
226 savepoint_state[thread_ident][using] = [None] |
188 tid = str(thread_ident).replace('-', '') |
227 tid = str(thread_ident).replace('-', '') |
189 sid = "s%s_x%d" % (tid, len(savepoint_state[thread_ident])) |
228 sid = "s%s_x%d" % (tid, len(savepoint_state[thread_ident][using])) |
190 connection._savepoint(sid) |
229 connection._savepoint(sid) |
191 return sid |
230 return sid |
192 |
231 |
193 def savepoint_rollback(sid): |
232 def savepoint_rollback(sid, using=None): |
194 """ |
233 """ |
195 Rolls back the most recent savepoint (if one exists). Does nothing if |
234 Rolls back the most recent savepoint (if one exists). Does nothing if |
196 savepoints are not supported. |
235 savepoints are not supported. |
197 """ |
236 """ |
198 if thread.get_ident() in savepoint_state: |
237 if using is None: |
|
238 using = DEFAULT_DB_ALIAS |
|
239 connection = connections[using] |
|
240 thread_ident = thread.get_ident() |
|
241 if thread_ident in savepoint_state and using in savepoint_state[thread_ident]: |
199 connection._savepoint_rollback(sid) |
242 connection._savepoint_rollback(sid) |
200 |
243 |
201 def savepoint_commit(sid): |
244 def savepoint_commit(sid, using=None): |
202 """ |
245 """ |
203 Commits the most recent savepoint (if one exists). Does nothing if |
246 Commits the most recent savepoint (if one exists). Does nothing if |
204 savepoints are not supported. |
247 savepoints are not supported. |
205 """ |
248 """ |
206 if thread.get_ident() in savepoint_state: |
249 if using is None: |
|
250 using = DEFAULT_DB_ALIAS |
|
251 connection = connections[using] |
|
252 thread_ident = thread.get_ident() |
|
253 if thread_ident in savepoint_state and using in savepoint_state[thread_ident]: |
207 connection._savepoint_commit(sid) |
254 connection._savepoint_commit(sid) |
208 |
255 |
209 ############## |
256 ############## |
210 # DECORATORS # |
257 # DECORATORS # |
211 ############## |
258 ############## |
212 |
259 |
213 def autocommit(func): |
260 def autocommit(using=None): |
214 """ |
261 """ |
215 Decorator that activates commit on save. This is Django's default behavior; |
262 Decorator that activates commit on save. This is Django's default behavior; |
216 this decorator is useful if you globally activated transaction management in |
263 this decorator is useful if you globally activated transaction management in |
217 your settings file and want the default behavior in some view functions. |
264 your settings file and want the default behavior in some view functions. |
218 """ |
265 """ |
219 def _autocommit(*args, **kw): |
266 def inner_autocommit(func, db=None): |
220 try: |
267 def _autocommit(*args, **kw): |
221 enter_transaction_management(managed=False) |
268 try: |
222 managed(False) |
269 enter_transaction_management(managed=False, using=db) |
223 return func(*args, **kw) |
270 managed(False, using=db) |
224 finally: |
271 return func(*args, **kw) |
225 leave_transaction_management() |
272 finally: |
226 return wraps(func)(_autocommit) |
273 leave_transaction_management(using=db) |
227 |
274 return wraps(func)(_autocommit) |
228 def commit_on_success(func): |
275 |
|
276 # Note that although the first argument is *called* `using`, it |
|
277 # may actually be a function; @autocommit and @autocommit('foo') |
|
278 # are both allowed forms. |
|
279 if using is None: |
|
280 using = DEFAULT_DB_ALIAS |
|
281 if callable(using): |
|
282 return inner_autocommit(using, DEFAULT_DB_ALIAS) |
|
283 return lambda func: inner_autocommit(func, using) |
|
284 |
|
285 |
|
286 def commit_on_success(using=None): |
229 """ |
287 """ |
230 This decorator activates commit on response. This way, if the view function |
288 This decorator activates commit on response. This way, if the view function |
231 runs successfully, a commit is made; if the viewfunc produces an exception, |
289 runs successfully, a commit is made; if the viewfunc produces an exception, |
232 a rollback is made. This is one of the most common ways to do transaction |
290 a rollback is made. This is one of the most common ways to do transaction |
233 control in web apps. |
291 control in web apps. |
234 """ |
292 """ |
235 def _commit_on_success(*args, **kw): |
293 def inner_commit_on_success(func, db=None): |
236 try: |
294 def _commit_on_success(*args, **kw): |
237 enter_transaction_management() |
|
238 managed(True) |
|
239 try: |
295 try: |
240 res = func(*args, **kw) |
296 enter_transaction_management(using=db) |
241 except: |
297 managed(True, using=db) |
242 # All exceptions must be handled here (even string ones). |
298 try: |
243 if is_dirty(): |
299 res = func(*args, **kw) |
244 rollback() |
300 except: |
245 raise |
301 # All exceptions must be handled here (even string ones). |
246 else: |
302 if is_dirty(using=db): |
247 if is_dirty(): |
303 rollback(using=db) |
248 commit() |
304 raise |
249 return res |
305 else: |
250 finally: |
306 if is_dirty(using=db): |
251 leave_transaction_management() |
307 try: |
252 return wraps(func)(_commit_on_success) |
308 commit(using=db) |
253 |
309 except: |
254 def commit_manually(func): |
310 rollback(using=db) |
|
311 raise |
|
312 return res |
|
313 finally: |
|
314 leave_transaction_management(using=db) |
|
315 return wraps(func)(_commit_on_success) |
|
316 |
|
317 # Note that although the first argument is *called* `using`, it |
|
318 # may actually be a function; @autocommit and @autocommit('foo') |
|
319 # are both allowed forms. |
|
320 if using is None: |
|
321 using = DEFAULT_DB_ALIAS |
|
322 if callable(using): |
|
323 return inner_commit_on_success(using, DEFAULT_DB_ALIAS) |
|
324 return lambda func: inner_commit_on_success(func, using) |
|
325 |
|
326 def commit_manually(using=None): |
255 """ |
327 """ |
256 Decorator that activates manual transaction control. It just disables |
328 Decorator that activates manual transaction control. It just disables |
257 automatic transaction control and doesn't do any commit/rollback of its |
329 automatic transaction control and doesn't do any commit/rollback of its |
258 own -- it's up to the user to call the commit and rollback functions |
330 own -- it's up to the user to call the commit and rollback functions |
259 themselves. |
331 themselves. |
260 """ |
332 """ |
261 def _commit_manually(*args, **kw): |
333 def inner_commit_manually(func, db=None): |
262 try: |
334 def _commit_manually(*args, **kw): |
263 enter_transaction_management() |
335 try: |
264 managed(True) |
336 enter_transaction_management(using=db) |
265 return func(*args, **kw) |
337 managed(True, using=db) |
266 finally: |
338 return func(*args, **kw) |
267 leave_transaction_management() |
339 finally: |
268 |
340 leave_transaction_management(using=db) |
269 return wraps(func)(_commit_manually) |
341 |
|
342 return wraps(func)(_commit_manually) |
|
343 |
|
344 # Note that although the first argument is *called* `using`, it |
|
345 # may actually be a function; @autocommit and @autocommit('foo') |
|
346 # are both allowed forms. |
|
347 if using is None: |
|
348 using = DEFAULT_DB_ALIAS |
|
349 if callable(using): |
|
350 return inner_commit_manually(using, DEFAULT_DB_ALIAS) |
|
351 return lambda func: inner_commit_manually(func, using) |