Merge lp:~salgado/storm/fix-timeout-tracer into lp:storm

Proposed by Guilherme Salgado
Status: Merged
Merged at revision: 450
Proposed branch: lp:~salgado/storm/fix-timeout-tracer
Merge into: lp:storm
Diff against target: 314 lines (+165/-38)
4 files modified
storm/database.py (+32/-26)
storm/tracer.py (+32/-0)
tests/database.py (+20/-4)
tests/tracer.py (+81/-8)
To merge this branch: bzr merge lp:~salgado/storm/fix-timeout-tracer
Reviewer Review Type Date Requested Status
Gavin Panella Approve
Christopher Armstrong (community) Approve
Review via email: mp+112403@code.launchpad.net

Description of the change

Add connection commit/rollback tracer hooks and use them in the TimeoutTracer
to reset connection._timeout_tracer_remaining_time.

We currently don't reset connection._timeout_tracer_remaining_time ever, so
when the connection is reused in a new transaction the TimeoutTracer will not
call set_statement_timeout if connection._timeout_tracer_remaining_time is
greater than what's returned by self.get_remaining_time().

This is specially likely to happen if it's the first query you run in the
transaction that times out, as in that case when the transaction rolls back
connection._timeout_tracer_remaining_time will hold a value very close to your
total transaction timeout.

Oh, and I've also removed some duplicated code (a test method and StubConnection).

To post a comment you must log in.
lp:~salgado/storm/fix-timeout-tracer updated
451. By Guilherme Salgado

Remove some commented out code I left by accident

Revision history for this message
Christopher Armstrong (radix) wrote :

[1] I would prefer for the new tests in test_tracer (test_connection_commit_hook and test_connection_rollback_hook) to be blackbox instead of whitebox, since it doesn't look like that should be too hard (or at least *less* whiteboxy; something that actually invokes some connection logic and observes that the bug that you observed does not occur). Even if you don't change it, it looks like storm has a convention of using "test_wb_" for whitebox tests which you should use here.

[2] There are no tests for the changes to DebugTracer (and there is already a suite of tests for it, so it should be easy to add).

Looks good!

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

Nothing to add to what Christopher has already said. Nice :)

review: Approve
lp:~salgado/storm/fix-timeout-tracer updated
452. By Guilherme Salgado

A couple tests to the new DebugTracer methods

453. By Guilherme Salgado

Rewrite test_connection_commit_hook and test_connection_rollback_hook to be black box; we now use a real DB connection and execute/commit/rollback, making sure the statement timeout is set when it should

Revision history for this message
Guilherme Salgado (salgado) wrote :

On 27/06/12 21:31, Christopher Armstrong wrote:
> Review: Approve
>
> [1] I would prefer for the new tests in test_tracer (test_connection_commit_hook and test_connection_rollback_hook) to be blackbox instead of whitebox, since it doesn't look like that should be too hard (or at least *less* whiteboxy; something that actually invokes some connection logic and observes that the bug that you observed does not occur). Even if you don't change it, it looks like storm has a convention of using "test_wb_" for whitebox tests which you should use here.

Ok, I rewrote them. They're now in a separate TestCase since I'm calling
conn.execute()/etc against a real DB, so I'm not sure tests/tracer.py is
the right home for that. Also, I think I could simplify its
setUp/tearDown a bit by inheriting from DatabaseTest, if you think it's
worth it.

> [2] There are no tests for the changes to DebugTracer (and there is already a suite of tests for it, so it should be easy to add).

I completely missed that; just added a couple more tests there.

Thanks for the review!

Revision history for this message
Christopher Armstrong (radix) wrote :

I think the branch looks great now, +1.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'storm/database.py'
2--- storm/database.py 2012-03-02 20:36:48 +0000
3+++ storm/database.py 2012-06-28 14:23:21 +0000
4@@ -281,15 +281,18 @@
5 Reconnection happens automatically on rollback.
6
7 """
8- self._ensure_connected()
9- if xid:
10- raw_xid = self._raw_xid(xid)
11- self._check_disconnect(self._raw_connection.tpc_commit, raw_xid)
12- elif self._two_phase_transaction:
13- self._check_disconnect(self._raw_connection.tpc_commit)
14- self._two_phase_transaction = False
15- else:
16- self._check_disconnect(self._raw_connection.commit)
17+ try:
18+ self._ensure_connected()
19+ if xid:
20+ raw_xid = self._raw_xid(xid)
21+ self._check_disconnect(self._raw_connection.tpc_commit, raw_xid)
22+ elif self._two_phase_transaction:
23+ self._check_disconnect(self._raw_connection.tpc_commit)
24+ self._two_phase_transaction = False
25+ else:
26+ self._check_disconnect(self._raw_connection.commit)
27+ finally:
28+ self._check_disconnect(trace, "connection_commit", self, xid)
29
30 def recover(self):
31 """Return a list of L{Xid}s representing pending transactions."""
32@@ -305,27 +308,30 @@
33 transaction to rollback. This form should be called outside
34 of a transaction, and is intended for use in recovery.
35 """
36- if self._state == STATE_CONNECTED:
37- try:
38- if xid:
39- raw_xid = self._raw_xid(xid)
40- self._raw_connection.tpc_rollback(raw_xid)
41- elif self._two_phase_transaction:
42- self._raw_connection.tpc_rollback()
43+ try:
44+ if self._state == STATE_CONNECTED:
45+ try:
46+ if xid:
47+ raw_xid = self._raw_xid(xid)
48+ self._raw_connection.tpc_rollback(raw_xid)
49+ elif self._two_phase_transaction:
50+ self._raw_connection.tpc_rollback()
51+ else:
52+ self._raw_connection.rollback()
53+ except Error, exc:
54+ if self.is_disconnection_error(exc):
55+ self._raw_connection = None
56+ self._state = STATE_RECONNECT
57+ self._two_phase_transaction = False
58+ else:
59+ raise
60 else:
61- self._raw_connection.rollback()
62- except Error, exc:
63- if self.is_disconnection_error(exc):
64- self._raw_connection = None
65- self._state = STATE_RECONNECT
66 self._two_phase_transaction = False
67- else:
68- raise
69 else:
70 self._two_phase_transaction = False
71- else:
72- self._two_phase_transaction = False
73- self._state = STATE_RECONNECT
74+ self._state = STATE_RECONNECT
75+ finally:
76+ self._check_disconnect(trace, "connection_rollback", self, xid)
77
78 @staticmethod
79 def to_database(params):
80
81=== modified file 'storm/tracer.py'
82--- storm/tracer.py 2012-03-21 14:43:26 +0000
83+++ storm/tracer.py 2012-06-28 14:23:21 +0000
84@@ -42,6 +42,16 @@
85 self._stream.write("[%s] DONE\n" % time)
86 self._stream.flush()
87
88+ def connection_commit(self, connection, xid=None):
89+ time = datetime.now().isoformat()[11:]
90+ self._stream.write("[%s] COMMIT xid=%s\n" % (time, xid))
91+ self._stream.flush()
92+
93+ def connection_rollback(self, connection, xid=None):
94+ time = datetime.now().isoformat()[11:]
95+ self._stream.write("[%s] ROLLBACK xid=%s\n" % (time, xid))
96+ self._stream.flush()
97+
98
99 class TimeoutTracer(object):
100 """Provide a timeout facility for connections to prevent rogue operations.
101@@ -87,6 +97,28 @@
102 raise NotImplementedError("%s.connection_raw_execute_error() must be "
103 "implemented" % self.__class__.__name__)
104
105+ def connection_commit(self, connection, xid=None):
106+ """Reset Connection._timeout_tracer_remaining_time.
107+
108+ @param connection: The L{Connection} to the database.
109+ @param xid: Optionally the L{Xid} of a previously prepared
110+ transaction to commit.
111+ """
112+ self._reset_timeout_tracer_remaining_time(connection)
113+
114+ def connection_rollback(self, connection, xid=None):
115+ """Reset Connection._timeout_tracer_remaining_time.
116+
117+ @param connection: The L{Connection} to the database.
118+ @param xid: Optionally the L{Xid} of a previously prepared
119+ transaction to rollback.
120+ """
121+ self._reset_timeout_tracer_remaining_time(connection)
122+
123+ def _reset_timeout_tracer_remaining_time(self, connection):
124+ """Set connection._timeout_tracer_remaining_time to 0."""
125+ connection._timeout_tracer_remaining_time = 0
126+
127 def set_statement_timeout(self, raw_cursor, remaining_time):
128 """Perform the timeout setup in the raw cursor.
129
130
131=== modified file 'tests/database.py'
132--- tests/database.py 2011-09-22 09:16:49 +0000
133+++ tests/database.py 2012-06-28 14:23:21 +0000
134@@ -118,6 +118,12 @@
135 self.seen.append(("ERROR", connection, type(raw_cursor),
136 statement, params, error))
137
138+ def connection_commit(self, connection, xid=None):
139+ self.seen.append(("COMMIT", connection, xid))
140+
141+ def connection_rollback(self, connection, xid=None):
142+ self.seen.append(("ROLLBACK", connection, xid))
143+
144
145 class DatabaseTest(TestHelper):
146
147@@ -159,10 +165,6 @@
148 self.assertEquals(self.executed, [("something", marker), "RCLOSE"])
149
150 def test_execute_convert_param_style(self):
151- self.connection.execute("'?' ? '?' ? '?'")
152- self.assertEquals(self.executed, [("'?' ? '?' ? '?'", marker)])
153-
154- def test_execute_convert_param_style(self):
155 class MyConnection(Connection):
156 param_mark = "%s"
157 connection = MyConnection(self.database)
158@@ -282,10 +284,24 @@
159 self.connection.commit()
160 self.assertEquals(self.executed, ["COMMIT"])
161
162+ def test_commit_tracing(self):
163+ self.assertMethodsMatch(FakeTracer, DebugTracer)
164+ tracer = FakeTracer()
165+ install_tracer(tracer)
166+ self.connection.commit()
167+ self.assertEquals(tracer.seen, [("COMMIT", self.connection, None)])
168+
169 def test_rollback(self):
170 self.connection.rollback()
171 self.assertEquals(self.executed, ["ROLLBACK"])
172
173+ def test_rollback_tracing(self):
174+ self.assertMethodsMatch(FakeTracer, DebugTracer)
175+ tracer = FakeTracer()
176+ install_tracer(tracer)
177+ self.connection.rollback()
178+ self.assertEquals(tracer.seen, [("ROLLBACK", self.connection, None)])
179+
180 def test_close(self):
181 self.connection.close()
182 self.assertEquals(self.executed, ["CCLOSE"])
183
184=== modified file 'tests/tracer.py'
185--- tests/tracer.py 2012-03-21 15:59:24 +0000
186+++ tests/tracer.py 2012-06-28 14:23:21 +0000
187@@ -1,4 +1,5 @@
188 import datetime
189+import os
190 import sys
191 from unittest import TestCase
192
193@@ -23,7 +24,7 @@
194 remove_tracer_type, remove_all_tracers, debug,
195 BaseStatementTracer, DebugTracer, TimeoutTracer,
196 TimelineTracer, TimeoutError, _tracers)
197-from storm.database import Connection
198+from storm.database import Connection, create_database
199 from storm.expr import Variable
200
201 from tests.helper import TestHelper
202@@ -209,6 +210,24 @@
203 self.tracer.connection_raw_execute_success(connection, raw_cursor,
204 statement, params)
205
206+ def test_connection_commit(self):
207+ self.stream.write("[04:05:06.000007] COMMIT xid=None\n")
208+ self.stream.flush()
209+ self.mocker.replay()
210+
211+ connection = "CONNECTION"
212+
213+ self.tracer.connection_commit(connection)
214+
215+ def test_connection_rollback(self):
216+ self.stream.write("[04:05:06.000007] ROLLBACK xid=None\n")
217+ self.stream.flush()
218+ self.mocker.replay()
219+
220+ connection = "CONNECTION"
221+
222+ self.tracer.connection_rollback(connection)
223+
224
225 class TimeoutTracerTestBase(TestHelper):
226
227@@ -363,12 +382,73 @@
228 self.execute()
229
230
231+class TimeoutTracerWithDBTest(TestHelper):
232+
233+ def setUp(self):
234+ super(TimeoutTracerWithDBTest, self).setUp()
235+ self.tracer = StuckInTimeTimeoutTracer(10)
236+ install_tracer(self.tracer)
237+ database = create_database(os.environ["STORM_POSTGRES_URI"])
238+ self.connection = database.connect()
239+
240+ def tearDown(self):
241+ super(TimeoutTracerWithDBTest, self).tearDown()
242+ remove_tracer(self.tracer)
243+ self.connection.close()
244+
245+ def is_supported(self):
246+ return bool(os.environ.get("STORM_POSTGRES_URI"))
247+
248+ def test_timeout_set_on_beginning_of_new_transaction__commit(self):
249+ """Check that we set the statement timeout before the first query of a
250+ transaction regardless of the remaining time left by previous
251+ transactions.
252+
253+ When we reuse a connection for a different transaction, the remaining
254+ time of a previous transaction (which is stored in the connection)
255+ could cause the first query in that transaction to run with no
256+ timeout. This test makes sure that doesn't happen.
257+ """
258+ self.connection.execute('SELECT 1')
259+ self.assertEqual([10], self.tracer.set_statement_timeout_calls)
260+
261+ self.connection.commit()
262+
263+ self.connection.execute('SELECT 1')
264+ self.assertEqual([10, 10], self.tracer.set_statement_timeout_calls)
265+
266+ def test_timeout_set_on_beginning_of_new_transaction__rollback(self):
267+ """Same as the test above, but here we rollback the first tx."""
268+ self.connection.execute('SELECT 1')
269+ self.assertEqual([10], self.tracer.set_statement_timeout_calls)
270+
271+ self.connection.rollback()
272+
273+ self.connection.execute('SELECT 1')
274+ self.assertEqual([10, 10], self.tracer.set_statement_timeout_calls)
275+
276+
277+class StuckInTimeTimeoutTracer(TimeoutTracer):
278+
279+ def __init__(self, fixed_remaining_time):
280+ super(StuckInTimeTimeoutTracer, self).__init__()
281+ self.set_statement_timeout_calls = []
282+ self.fixed_remaining_time = fixed_remaining_time
283+
284+ def get_remaining_time(self):
285+ return self.fixed_remaining_time
286+
287+ def set_statement_timeout(self, raw_cursor, remaining_time):
288+ self.set_statement_timeout_calls.append(remaining_time)
289+
290+
291 class StubConnection(Connection):
292
293 def __init__(self):
294 self._database = None
295 self._event = None
296 self._raw_connection = None
297+ self.name = 'Foo'
298
299
300 class BaseStatementTracerTest(TestCase):
301@@ -502,13 +582,6 @@
302 self.assertEqual('statement', self.timeline.actions[-1].detail)
303
304 def test_category_from_prefix_and_connection_name(self):
305- class StubConnection(Connection):
306-
307- def __init__(self):
308- self._database = None
309- self._event = None
310- self._raw_connection = None
311- self.name = 'Foo'
312 tracer = TimelineTracer(self.factory, prefix='bar-')
313 tracer._expanded_raw_execute(StubConnection(), 'cursor', 'statement')
314 self.assertEqual('bar-Foo', self.timeline.actions[-1].category)

Subscribers

People subscribed via source and target branches

to status/vote changes: