Merge lp:~allenap/storm/oneiric-admin-shutdown-bug-871596 into lp:storm

Proposed by Gavin Panella
Status: Superseded
Proposed branch: lp:~allenap/storm/oneiric-admin-shutdown-bug-871596
Merge into: lp:storm
Diff against target: 921 lines (+626/-110)
9 files modified
.bzrignore (+3/-0)
Makefile (+9/-4)
README (+26/-2)
ez_setup.py (+284/-0)
setup.py (+36/-20)
storm/databases/postgres.py (+41/-15)
test (+11/-68)
tests/__init__.py (+96/-0)
tests/databases/postgres.py (+120/-1)
To merge this branch: bzr merge lp:~allenap/storm/oneiric-admin-shutdown-bug-871596
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
Storm Developers Pending
Review via email: mp+79575@code.launchpad.net

This proposal has been superseded by a proposal from 2011-10-19.

Description of the change

This branch adds three tests for disconnection errors:

- When the connection is terminated with pg_terminate_backend.

- When the connection is terminated with pg_terminate_backend and the
  connection goes via pgbouncer.

- When the connection is terminated because pgbouncer is terminated.

It then updates is_disconnection_error to match those situations.

Additionally, under Oneiric, some disconnection tests were failing
anyway, so is_disconnection_error has been updated to catch those too.

This branch adds two optional test dependencies: python-fixtures and
python-pgbouncer. If these are not available the tests will not be
attempted.

To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) wrote :

Thanks for working on this!

So the only time you ever saw the 57P01 error was with pgbouncer? That is interesting as in the past we often never saw the error codes at all (hence the string matching).

The code seems good. Just a few suggestions and points for discussion:

- Rather than check just for 57P01, I think we might as well check for the following list:

pg_connection_failure_codes = frozenset([
    '08006', # CONNECTION FAILURE
    '08001', # SQLCLIENT UNABLE TO ESTABLISH SQLCONNECTION
    '08004', # SQLSERVER REJECTED ESTABLISHMENT OF SQLCONNECTION
    '53300', # TOO MANY CONNECTIONS
    '57000', # OPERATOR INTERVENTION
    '57014', # QUERY CANCELED
    '57P01', # ADMIN SHUTDOWN
    '57P02', # CRASH SHUTDOWN
    '57P03', # CANNOT CONNECT NOW
    ])

- Should we document installing python-pgbouncer using the Python egg from pypi rather than checking out the Bazaar branch and using a symlink hack?

- Should python-fixtures be an optional dependency? Maybe if it gets used elsewhere.

- Do we want python-pgbouncer and fixtures as optional dependencies for running the PostgreSQL tests? I understand not needing the environment setup for testing every database backend, but there seems little point for allowing people to run a subset of the tests for a particular DB backend.

review: Approve
427. By Gavin Panella

Match many more pgcodes, as suggested by stub.

428. By Gavin Panella

Fix lint in setup.py.

429. By Gavin Panella

Use the test_suite and tests_require setuptools extensions to declare dependencies on pgbouncer and fixtures.

430. By Gavin Panella

Define all test dependencies and use setuptools for make check.

431. By Gavin Panella

Delete eggs in make clean.

432. By Gavin Panella

make check no longer depends on build.

433. By Gavin Panella

Also depend on timeline.

434. By Gavin Panella

Go completely over to the Dark, ahem, setuptools side.

435. By Gavin Panella

Make test use eggs installed by python setup.py test.

436. By Gavin Panella

New develop target to dry-run the tests so that dependencies are installed.

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

> Thanks for working on this!
>
> So the only time you ever saw the 57P01 error was with pgbouncer? That is
> interesting as in the past we often never saw the error codes at all (hence
> the string matching).

Yeah, I don't fully understand what was happening there.

>
> The code seems good. Just a few suggestions and points for discussion:
>
> - Rather than check just for 57P01, I think we might as well check for the
> following list:
>
> pg_connection_failure_codes = frozenset([
> '08006', # CONNECTION FAILURE
> '08001', # SQLCLIENT UNABLE TO ESTABLISH SQLCONNECTION
> '08004', # SQLSERVER REJECTED ESTABLISHMENT OF SQLCONNECTION
> '53300', # TOO MANY CONNECTIONS
> '57000', # OPERATOR INTERVENTION
> '57014', # QUERY CANCELED
> '57P01', # ADMIN SHUTDOWN
> '57P02', # CRASH SHUTDOWN
> '57P03', # CANNOT CONNECT NOW
> ])

Tip top, done.

>
> - Should we document installing python-pgbouncer using the Python egg from
> pypi rather than checking out the Bazaar branch and using a symlink hack?
>
> - Should python-fixtures be an optional dependency? Maybe if it gets used
> elsewhere.
>
> - Do we want python-pgbouncer and fixtures as optional dependencies for
> running the PostgreSQL tests? I understand not needing the environment setup
> for testing every database backend, but there seems little point for allowing
> people to run a subset of the tests for a particular DB backend.

I've switched everything to use setuptools, and have defined all the
test dependencies in setup.py. I've switched make check over to use
setuptools' testing support, and I've also made the ./test script use
the eggs that setuptools downloads. There's also an additional develop
Makefile target that downloads the test dependencies without running
the tests.

This means you can do make check from a fresh branch of Storm and run
*all* the tests without further intervention (other than doing the
one-off package installations and database set-up). This has already
shown me that there's another Django disconnection test failure that I
need to address.

437. By Gavin Panella

develop is PHONY.

438. By Gavin Panella

If we're relying on setuptools we don't need our own find_packages().

439. By Gavin Panella

Remove _trial_temp in make clean.

440. By Gavin Panella

Document make develop in README.

Revision history for this message
Robert Collins (lifeless) wrote :

van.pg can do postgresql cluster setup as a test fixture, if you want :)

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

> van.pg can do postgresql cluster setup as a test fixture, if you
> want :)

Indeed, I do want! I did some work on van.pg last week or the week
before, but it was a little more complicated than I had imagined... or
I was misunderstanding. I won't attempt it again in this branch, but I
feel like I might have a much better chance of success with the things
I've learnt doing this.

Revision history for this message
Stuart Bishop (stub) wrote :

On Wed, Oct 19, 2011 at 3:42 PM, Gavin Panella
<email address hidden> wrote:
>> van.pg can do postgresql cluster setup as a test fixture, if you
>> want :)
>
> Indeed, I do want! I did some work on van.pg last week or the week
> before, but it was a little more complicated than I had imagined... or
> I was misunderstanding. I won't attempt it again in this branch, but I
> feel like I might have a much better chance of success with the things
> I've learnt doing this.

Yer, separate branch for that or this will never land :)

--
Stuart Bishop <email address hidden>
http://www.stuartbishop.net/

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

I'm going to split this branch into two, to help review.

441. By Gavin Panella

Merged go-setuptools into oneiric-admin-shutdown-bug-871596.

442. By Gavin Panella

Revert previous revision.

443. By Gavin Panella

Include ez_setup.py in tarballs.

444. By Gavin Panella

Rely upon pgbouncer 0.0.6 or later, owing to unreliable shutdown problems in earlier versions.

445. By Gavin Panella

Deal with a weird ProgrammingError when connecting via pgbouncer.

446. By Gavin Panella

QUERY CANCELED is not a disconnection.

447. By Gavin Panella

Merged go-setuptools into oneiric-admin-shutdown-bug-871596, resolving conflicts.

448. By Gavin Panella

Merged go-setuptools into oneiric-admin-shutdown-bug-871596, resolving conflicts.

449. By Gavin Panella

Merged go-setuptools into oneiric-admin-shutdown-bug-871596.

450. By Gavin Panella

Use has_fixtures from tests/__init__.py.

451. By Gavin Panella

Update NEWS.

452. By Gavin Panella

pgbouncer 0.0.7 is needed.

453. By Gavin Panella

Add eggs to path before importing tests.

454. By Gavin Panella

Depend on testtools >= 0.9.8; newer versions of python-fixtures needs it.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2009-10-08 07:38:11 +0000
3+++ .bzrignore 2011-10-19 22:12:25 +0000
4@@ -11,3 +11,6 @@
5 debian/python-storm.substvars
6 apidoc
7 _trial_temp
8+*.egg
9+TAGS
10+tags
11
12=== modified file 'Makefile'
13--- Makefile 2009-11-24 18:34:34 +0000
14+++ Makefile 2011-10-19 22:12:25 +0000
15@@ -1,7 +1,7 @@
16 PYTHON ?= python
17 PYDOCTOR ?= pydoctor
18
19-TEST_COMMAND = $(PYTHON) test
20+TEST_COMMAND = $(PYTHON) setup.py test
21
22 STORM_POSTGRES_URI = postgres:storm_test
23 STORM_POSTGRES_HOST_URI = postgres://localhost/storm_test
24@@ -18,8 +18,11 @@
25 build:
26 $(PYTHON) setup.py build_ext -i
27
28-check: build
29- # Run the tests once with C extensions and once without them.
30+develop:
31+ $(TEST_COMMAND) --quiet --dry-run
32+
33+check:
34+ @ # Run the tests once with C extensions and once without them.
35 $(TEST_COMMAND) && STORM_CEXTENSIONS=0 $(TEST_COMMAND)
36
37 doc:
38@@ -36,8 +39,10 @@
39 rm -rf debian/files
40 rm -rf debian/python-storm
41 rm -rf debian/python-storm.*
42+ rm -rf *.egg
43+ rm -rf _trial_temp
44 find . -name "*.so" -type f -exec rm -f {} \;
45 find . -name "*.pyc" -type f -exec rm -f {} \;
46 find . -name "*~" -type f -exec rm -f {} \;
47
48-.PHONY: all build test
49+.PHONY: all build check clean develop doc release
50
51=== modified file 'README'
52--- README 2009-10-08 07:23:43 +0000
53+++ README 2011-10-19 22:12:25 +0000
54@@ -98,8 +98,10 @@
55 you will need to install MySQL and PostgreSQL, along with the
56 related Python database drivers:
57
58- $ sudo apt-get install python-mysqldb python-psycopg2 mysql-server \
59- postgresql build-essential
60+ $ sudo apt-get install \
61+ python-mysqldb mysql-server \
62+ postgresql pgbouncer \
63+ build-essential
64
65 These will take a few minutes to download (its a bit under 200MB all
66 together). Once the download is complete, a screen called
67@@ -110,6 +112,28 @@
68 asked to enter a password multiple times. Leave it blank in each
69 case.
70
71+The Python dependencies for running tests can mostly be installed with
72+apt-get:
73+
74+ $ apt-get install \
75+ python-django python-fixtures python-psycopg2 \
76+ python-testresources python-transaction python-twisted \
77+ python-zope.component python-zope.security
78+
79+Two modules - pgbouncer and timeline - are not yet packaged in
80+Ubuntu. These can be installed from PyPI:
81+
82+ http://pypi.python.org/pypi/pgbouncer
83+ http://pypi.python.org/pypi/timeline
84+
85+Alternatively, dependencies can be downloaded as eggs into the current
86+directory with:
87+
88+ $ make develop
89+
90+This ensures that all dependencies are available, downloading from
91+PyPI as appropriate.
92+
93 Setting up database users and access security
94 ---------------------------------------------
95
96
97=== added file 'ez_setup.py'
98--- ez_setup.py 1970-01-01 00:00:00 +0000
99+++ ez_setup.py 2011-10-19 22:12:25 +0000
100@@ -0,0 +1,284 @@
101+#!python
102+"""Bootstrap setuptools installation
103+
104+If you want to use setuptools in your package's setup.py, just include this
105+file in the same directory with it, and add this to the top of your setup.py::
106+
107+ from ez_setup import use_setuptools
108+ use_setuptools()
109+
110+If you want to require a specific version of setuptools, set a download
111+mirror, or use an alternate download directory, you can do so by supplying
112+the appropriate options to ``use_setuptools()``.
113+
114+This file can also be run as a script to install or upgrade setuptools.
115+"""
116+import sys
117+DEFAULT_VERSION = "0.6c11"
118+DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
119+
120+md5_data = {
121+ 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
122+ 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
123+ 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
124+ 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
125+ 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
126+ 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
127+ 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
128+ 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
129+ 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
130+ 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
131+ 'setuptools-0.6c10-py2.3.egg': 'ce1e2ab5d3a0256456d9fc13800a7090',
132+ 'setuptools-0.6c10-py2.4.egg': '57d6d9d6e9b80772c59a53a8433a5dd4',
133+ 'setuptools-0.6c10-py2.5.egg': 'de46ac8b1c97c895572e5e8596aeb8c7',
134+ 'setuptools-0.6c10-py2.6.egg': '58ea40aef06da02ce641495523a0b7f5',
135+ 'setuptools-0.6c11-py2.3.egg': '2baeac6e13d414a9d28e7ba5b5a596de',
136+ 'setuptools-0.6c11-py2.4.egg': 'bd639f9b0eac4c42497034dec2ec0c2b',
137+ 'setuptools-0.6c11-py2.5.egg': '64c94f3bf7a72a13ec83e0b24f2749b2',
138+ 'setuptools-0.6c11-py2.6.egg': 'bfa92100bd772d5a213eedd356d64086',
139+ 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
140+ 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
141+ 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
142+ 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
143+ 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
144+ 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
145+ 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
146+ 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
147+ 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
148+ 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
149+ 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
150+ 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
151+ 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
152+ 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
153+ 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
154+ 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
155+ 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
156+ 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
157+ 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
158+ 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
159+ 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
160+ 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
161+ 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
162+ 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a',
163+}
164+
165+import sys, os
166+try: from hashlib import md5
167+except ImportError: from md5 import md5
168+
169+def _validate_md5(egg_name, data):
170+ if egg_name in md5_data:
171+ digest = md5(data).hexdigest()
172+ if digest != md5_data[egg_name]:
173+ print >>sys.stderr, (
174+ "md5 validation of %s failed! (Possible download problem?)"
175+ % egg_name
176+ )
177+ sys.exit(2)
178+ return data
179+
180+def use_setuptools(
181+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
182+ download_delay=15
183+):
184+ """Automatically find/download setuptools and make it available on sys.path
185+
186+ `version` should be a valid setuptools version number that is available
187+ as an egg for download under the `download_base` URL (which should end with
188+ a '/'). `to_dir` is the directory where setuptools will be downloaded, if
189+ it is not already available. If `download_delay` is specified, it should
190+ be the number of seconds that will be paused before initiating a download,
191+ should one be required. If an older version of setuptools is installed,
192+ this routine will print a message to ``sys.stderr`` and raise SystemExit in
193+ an attempt to abort the calling script.
194+ """
195+ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
196+ def do_download():
197+ egg = download_setuptools(version, download_base, to_dir, download_delay)
198+ sys.path.insert(0, egg)
199+ import setuptools; setuptools.bootstrap_install_from = egg
200+ try:
201+ import pkg_resources
202+ except ImportError:
203+ return do_download()
204+ try:
205+ pkg_resources.require("setuptools>="+version); return
206+ except pkg_resources.VersionConflict, e:
207+ if was_imported:
208+ print >>sys.stderr, (
209+ "The required version of setuptools (>=%s) is not available, and\n"
210+ "can't be installed while this script is running. Please install\n"
211+ " a more recent version first, using 'easy_install -U setuptools'."
212+ "\n\n(Currently using %r)"
213+ ) % (version, e.args[0])
214+ sys.exit(2)
215+ except pkg_resources.DistributionNotFound:
216+ pass
217+
218+ del pkg_resources, sys.modules['pkg_resources'] # reload ok
219+ return do_download()
220+
221+def download_setuptools(
222+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
223+ delay = 15
224+):
225+ """Download setuptools from a specified location and return its filename
226+
227+ `version` should be a valid setuptools version number that is available
228+ as an egg for download under the `download_base` URL (which should end
229+ with a '/'). `to_dir` is the directory where the egg will be downloaded.
230+ `delay` is the number of seconds to pause before an actual download attempt.
231+ """
232+ import urllib2, shutil
233+ egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
234+ url = download_base + egg_name
235+ saveto = os.path.join(to_dir, egg_name)
236+ src = dst = None
237+ if not os.path.exists(saveto): # Avoid repeated downloads
238+ try:
239+ from distutils import log
240+ if delay:
241+ log.warn("""
242+---------------------------------------------------------------------------
243+This script requires setuptools version %s to run (even to display
244+help). I will attempt to download it for you (from
245+%s), but
246+you may need to enable firewall access for this script first.
247+I will start the download in %d seconds.
248+
249+(Note: if this machine does not have network access, please obtain the file
250+
251+ %s
252+
253+and place it in this directory before rerunning this script.)
254+---------------------------------------------------------------------------""",
255+ version, download_base, delay, url
256+ ); from time import sleep; sleep(delay)
257+ log.warn("Downloading %s", url)
258+ src = urllib2.urlopen(url)
259+ # Read/write all in one block, so we don't create a corrupt file
260+ # if the download is interrupted.
261+ data = _validate_md5(egg_name, src.read())
262+ dst = open(saveto,"wb"); dst.write(data)
263+ finally:
264+ if src: src.close()
265+ if dst: dst.close()
266+ return os.path.realpath(saveto)
267+
268+
269+
270+
271+
272+
273+
274+
275+
276+
277+
278+
279+
280+
281+
282+
283+
284+
285+
286+
287+
288+
289+
290+
291+
292+
293+
294+
295+
296+
297+
298+
299+
300+
301+
302+
303+def main(argv, version=DEFAULT_VERSION):
304+ """Install or upgrade setuptools and EasyInstall"""
305+ try:
306+ import setuptools
307+ except ImportError:
308+ egg = None
309+ try:
310+ egg = download_setuptools(version, delay=0)
311+ sys.path.insert(0,egg)
312+ from setuptools.command.easy_install import main
313+ return main(list(argv)+[egg]) # we're done here
314+ finally:
315+ if egg and os.path.exists(egg):
316+ os.unlink(egg)
317+ else:
318+ if setuptools.__version__ == '0.0.1':
319+ print >>sys.stderr, (
320+ "You have an obsolete version of setuptools installed. Please\n"
321+ "remove it from your system entirely before rerunning this script."
322+ )
323+ sys.exit(2)
324+
325+ req = "setuptools>="+version
326+ import pkg_resources
327+ try:
328+ pkg_resources.require(req)
329+ except pkg_resources.VersionConflict:
330+ try:
331+ from setuptools.command.easy_install import main
332+ except ImportError:
333+ from easy_install import main
334+ main(list(argv)+[download_setuptools(delay=0)])
335+ sys.exit(0) # try to force an exit
336+ else:
337+ if argv:
338+ from setuptools.command.easy_install import main
339+ main(argv)
340+ else:
341+ print "Setuptools version",version,"or greater has been installed."
342+ print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
343+
344+def update_md5(filenames):
345+ """Update our built-in md5 registry"""
346+
347+ import re
348+
349+ for name in filenames:
350+ base = os.path.basename(name)
351+ f = open(name,'rb')
352+ md5_data[base] = md5(f.read()).hexdigest()
353+ f.close()
354+
355+ data = [" %r: %r,\n" % it for it in md5_data.items()]
356+ data.sort()
357+ repl = "".join(data)
358+
359+ import inspect
360+ srcfile = inspect.getsourcefile(sys.modules[__name__])
361+ f = open(srcfile, 'rb'); src = f.read(); f.close()
362+
363+ match = re.search("\nmd5_data = {\n([^}]+)}", src)
364+ if not match:
365+ print >>sys.stderr, "Internal error!"
366+ sys.exit(2)
367+
368+ src = src[:match.start(1)] + repl + src[match.end(1):]
369+ f = open(srcfile,'w')
370+ f.write(src)
371+ f.close()
372+
373+
374+if __name__=='__main__':
375+ if len(sys.argv)>2 and sys.argv[1]=='--md5update':
376+ update_md5(sys.argv[2:])
377+ else:
378+ main(sys.argv[1:])
379+
380+
381+
382+
383+
384+
385
386=== modified file 'setup.py'
387--- setup.py 2011-08-31 17:04:09 +0000
388+++ setup.py 2011-10-19 22:12:25 +0000
389@@ -2,10 +2,10 @@
390 import os
391 import re
392
393-try:
394- from setuptools import setup, Extension
395-except ImportError:
396- from distutils.core import setup, Extension
397+import ez_setup
398+ez_setup.use_setuptools()
399+
400+from setuptools import setup, Extension, find_packages
401
402
403 if os.path.isfile("MANIFEST"):
404@@ -19,20 +19,12 @@
405 open("storm/__init__.py").read()).group(1)
406
407
408-def find_packages():
409- # implement a simple find_packages so we don't have to depend on
410- # setuptools
411- packages = []
412- for directory, subdirectories, files in os.walk("storm"):
413- if '__init__.py' in files:
414- packages.append(directory.replace(os.sep, '.'))
415- return packages
416-
417-
418 setup(
419 name="storm",
420 version=VERSION,
421- description="Storm is an object-relational mapper (ORM) for Python developed at Canonical.",
422+ description=(
423+ "Storm is an object-relational mapper (ORM) for Python "
424+ "developed at Canonical."),
425 author="Gustavo Niemeyer",
426 author_email="gustavo@niemeyer.net",
427 maintainer="Storm Developers",
428@@ -41,18 +33,42 @@
429 url="https://storm.canonical.com",
430 download_url="https://launchpad.net/storm/+download",
431 packages=find_packages(),
432- zip_safe=False,
433- include_package_data=True,
434 package_data={"": ["*.zcml"]},
435 classifiers=[
436 "Development Status :: 5 - Production/Stable",
437 "Intended Audience :: Developers",
438- "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
439+ ("License :: OSI Approved :: GNU Library or "
440+ "Lesser General Public License (LGPL)"),
441 "Programming Language :: Python",
442 "Topic :: Database",
443 "Topic :: Database :: Front-Ends",
444 "Topic :: Software Development :: Libraries :: Python Modules",
445 ],
446 ext_modules=(BUILD_CEXTENSIONS and
447- [Extension("storm.cextensions", ["storm/cextensions.c"])])
448-)
449+ [Extension("storm.cextensions", ["storm/cextensions.c"])]),
450+ # The following options are specific to setuptools but ignored (with a
451+ # warning) by distutils.
452+ include_package_data=True,
453+ zip_safe=False,
454+ test_suite = "tests.find_tests",
455+ tests_require=[
456+ # Versions based on Lucid, where packaged.
457+ "django >= 1.1.1",
458+ "fixtures >= 0.3.5",
459+ # pgbouncer (the Python module) is not yet packaged in Ubuntu.
460+ "pgbouncer >= 0.0.5",
461+ "psycopg2 >= 2.0.13",
462+ "testresources >= 0.2.4",
463+ # timeline is not yet packaged in Ubuntu.
464+ "timeline >= 0.0.2",
465+ "transaction >= 1.0.0",
466+ "twisted >= 10.0.0",
467+ "zope.component >= 3.8.0",
468+ # zope.component 3.11.0 requires a version of zope.interface that no
469+ # version of Ubuntu yet packages. The following rule exists for the
470+ # sake of convenience rather than necessity, for the situation where
471+ # zope.interface is installed via a package but zope.component is not.
472+ "zope.component < 3.11.0",
473+ "zope.security >= 3.7.2",
474+ ],
475+ )
476
477=== modified file 'storm/databases/postgres.py'
478--- storm/databases/postgres.py 2011-09-22 09:16:49 +0000
479+++ storm/databases/postgres.py 2011-10-19 22:12:25 +0000
480@@ -47,7 +47,7 @@
481 from storm.database import Database, Connection, Result
482 from storm.exceptions import (
483 install_exceptions, DatabaseError, DatabaseModuleError, InterfaceError,
484- OperationalError, ProgrammingError, TimeoutError)
485+ OperationalError, ProgrammingError, TimeoutError, Error)
486 from storm.tracer import TimeoutTracer
487
488
489@@ -217,6 +217,19 @@
490 return And(*equals)
491
492
493+pg_connection_failure_codes = frozenset([
494+ '08006', # CONNECTION FAILURE
495+ '08001', # SQLCLIENT UNABLE TO ESTABLISH SQLCONNECTION
496+ '08004', # SQLSERVER REJECTED ESTABLISHMENT OF SQLCONNECTION
497+ '53300', # TOO MANY CONNECTIONS
498+ '57000', # OPERATOR INTERVENTION
499+ '57014', # QUERY CANCELED
500+ '57P01', # ADMIN SHUTDOWN
501+ '57P02', # CRASH SHUTDOWN
502+ '57P03', # CANNOT CONNECT NOW
503+ ])
504+
505+
506 class PostgresConnection(Connection):
507
508 result_factory = PostgresResult
509@@ -277,22 +290,35 @@
510 yield param
511
512 def is_disconnection_error(self, exc, extra_disconnection_errors=()):
513- if not isinstance(exc, (InterfaceError, OperationalError,
514- ProgrammingError, extra_disconnection_errors)):
515- return False
516-
517- # XXX: 2007-09-17 jamesh
518- # The last message is for the benefit of old versions of
519- # psycopg2 (prior to 2.0.7) which have a bug related to
520- # stripping the error severity from the message.
521- msg = str(exc)
522- return ("server closed the connection unexpectedly" in msg or
523+ # Attempt to use pgcode to determine the nature of the error. This is
524+ # more reliable than string matching because it is not affected by
525+ # locale settings. Fall through if pgcode is not available.
526+ if isinstance(exc, Error):
527+ pgcode = getattr(exc, "pgcode", None)
528+ if pgcode in pg_connection_failure_codes:
529+ return True
530+
531+ disconnection_errors = (
532+ InterfaceError, OperationalError, ProgrammingError,
533+ extra_disconnection_errors)
534+
535+ if isinstance(exc, disconnection_errors):
536+ msg = str(exc)
537+ return (
538+ "connection already closed" in msg or
539+ "connection not open" in msg or
540 "could not connect to server" in msg or
541+ "could not receive data from server" in msg or
542+ "losed the connection unexpectedly" in msg or
543 "no connection to the server" in msg or
544- "connection not open" in msg or
545- "connection already closed" in msg or
546- "losed the connection unexpectedly" in msg or
547- "could not receive data from server" in msg)
548+ "terminating connection due to administrator" in msg)
549+ elif isinstance(exc, DatabaseError):
550+ msg = str(exc)
551+ return (
552+ "EOF detected" in msg or
553+ "server closed the connection unexpectedly" in msg)
554+ else:
555+ return False
556
557
558 class Postgres(Database):
559
560=== modified file 'test'
561--- test 2011-09-13 10:43:40 +0000
562+++ test 2011-10-19 22:12:25 +0000
563@@ -19,6 +19,7 @@
564 # You should have received a copy of the GNU Lesser General Public License
565 # along with this program. If not, see <http://www.gnu.org/licenses/>.
566 #
567+import glob
568 import optparse
569 import unittest
570 import doctest
571@@ -28,73 +29,11 @@
572 import tests
573
574
575-def find_tests(testpaths=()):
576- """Find all test paths, or test paths contained in the provided sequence.
577-
578- @param testpaths: If provided, only tests in the given sequence will
579- be considered. If not provided, all tests are
580- considered.
581- @return: a test suite containing the requested tests.
582- """
583- suite = unittest.TestSuite()
584- topdir = os.path.abspath(os.path.dirname(__file__))
585- testdir = os.path.dirname(tests.__file__)
586- testpaths = set(testpaths)
587- for root, dirnames, filenames in os.walk(testdir):
588- for filename in filenames:
589- filepath = os.path.join(root, filename)
590- relpath = filepath[len(topdir)+1:]
591-
592- if (filename == "__init__.py" or filename.endswith(".pyc") or
593- relpath == os.path.join("tests", "conftest.py")):
594- # Skip non-tests.
595- continue
596-
597- if testpaths:
598- # Skip any tests not in testpaths.
599- for testpath in testpaths:
600- if relpath.startswith(testpath):
601- break
602- else:
603- continue
604-
605- if filename.endswith(".py"):
606- modpath = relpath.replace(os.path.sep, ".")[:-3]
607- module = __import__(modpath, None, None, [""])
608- suite.addTest(
609- unittest.defaultTestLoader.loadTestsFromModule(module))
610- elif filename.endswith(".txt"):
611- load_test = True
612- if relpath == os.path.join("tests", "zope", "README.txt"):
613- # Special case the inclusion of the Zope-dependent
614- # ZStorm doctest.
615- import tests.zope as ztest
616- load_test = (
617- ztest.has_transaction and
618- ztest.has_zope_component and
619- ztest.has_zope_security)
620- if load_test:
621- parent_path = os.path.dirname(relpath).replace(
622- os.path.sep, ".")
623- parent_module = __import__(parent_path, None, None, [""])
624- suite.addTest(doctest.DocFileSuite(
625- os.path.basename(relpath),
626- module_relative=True,
627- package=parent_module,
628- optionflags=doctest.ELLIPSIS))
629-
630- return suite
631-
632-
633-def parse_sys_argv():
634- """Extract any arguments not starting with '-' from sys.argv."""
635- testpaths = []
636- for i in range(len(sys.argv)-1,0,-1):
637- arg = sys.argv[i]
638- if not arg.startswith("-"):
639- testpaths.append(arg)
640- del sys.argv[i]
641- return testpaths
642+def add_eggs_to_path():
643+ here = os.path.dirname(__file__)
644+ egg_paths = glob.glob(os.path.join(here, "*.egg"))
645+ sys.path[:0] = map(os.path.abspath, egg_paths)
646+
647
648 def test_with_runner(runner):
649 usage = "test.py [options] [<test filename>, ...]"
650@@ -107,7 +46,11 @@
651 if opts.verbose:
652 runner.verbosity = 2
653
654- suite = find_tests(args)
655+ # python setup.py test [--dry-run] puts $package.egg directories in the
656+ # top directory, so we add them to sys.path here for convenience.
657+ add_eggs_to_path()
658+
659+ suite = tests.find_tests(args)
660 result = runner.run(suite)
661 return not result.wasSuccessful()
662
663
664=== modified file 'tests/__init__.py'
665--- tests/__init__.py 2008-06-20 14:08:30 +0000
666+++ tests/__init__.py 2011-10-19 22:12:25 +0000
667@@ -0,0 +1,96 @@
668+#
669+# Copyright (c) 2011 Canonical
670+#
671+# Written by Gustavo Niemeyer <gustavo@niemeyer.net>
672+#
673+# This file is part of Storm Object Relational Mapper.
674+#
675+# Storm is free software; you can redistribute it and/or modify
676+# it under the terms of the GNU Lesser General Public License as
677+# published by the Free Software Foundation; either version 2.1 of
678+# the License, or (at your option) any later version.
679+#
680+# Storm is distributed in the hope that it will be useful,
681+# but WITHOUT ANY WARRANTY; without even the implied warranty of
682+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
683+# GNU Lesser General Public License for more details.
684+#
685+# You should have received a copy of the GNU Lesser General Public License
686+# along with this program. If not, see <http://www.gnu.org/licenses/>.
687+#
688+
689+__all__ = [
690+ 'find_tests',
691+ 'has_fixtures',
692+ ]
693+
694+import doctest
695+import os
696+import unittest
697+
698+try:
699+ import fixtures
700+ fixtures # Silence lint.
701+except ImportError:
702+ has_fixtures = False
703+else:
704+ has_fixtures = True
705+
706+
707+def find_tests(testpaths=()):
708+ """Find all test paths, or test paths contained in the provided sequence.
709+
710+ @param testpaths: If provided, only tests in the given sequence will
711+ be considered. If not provided, all tests are
712+ considered.
713+ @return: a test suite containing the requested tests.
714+ """
715+ suite = unittest.TestSuite()
716+ topdir = os.path.abspath(
717+ os.path.join(os.path.dirname(__file__), os.pardir))
718+ testdir = os.path.dirname(__file__)
719+ testpaths = set(testpaths)
720+ for root, dirnames, filenames in os.walk(testdir):
721+ for filename in filenames:
722+ filepath = os.path.join(root, filename)
723+ relpath = filepath[len(topdir)+1:]
724+
725+ if (filename == "__init__.py" or filename.endswith(".pyc") or
726+ relpath == os.path.join("tests", "conftest.py")):
727+ # Skip non-tests.
728+ continue
729+
730+ if testpaths:
731+ # Skip any tests not in testpaths.
732+ for testpath in testpaths:
733+ if relpath.startswith(testpath):
734+ break
735+ else:
736+ continue
737+
738+ if filename.endswith(".py"):
739+ modpath = relpath.replace(os.path.sep, ".")[:-3]
740+ module = __import__(modpath, None, None, [""])
741+ suite.addTest(
742+ unittest.defaultTestLoader.loadTestsFromModule(module))
743+ elif filename.endswith(".txt"):
744+ load_test = True
745+ if relpath == os.path.join("tests", "zope", "README.txt"):
746+ # Special case the inclusion of the Zope-dependent
747+ # ZStorm doctest.
748+ import tests.zope as ztest
749+ load_test = (
750+ ztest.has_transaction and
751+ ztest.has_zope_component and
752+ ztest.has_zope_security)
753+ if load_test:
754+ parent_path = os.path.dirname(relpath).replace(
755+ os.path.sep, ".")
756+ parent_module = __import__(parent_path, None, None, [""])
757+ suite.addTest(doctest.DocFileSuite(
758+ os.path.basename(relpath),
759+ module_relative=True,
760+ package=parent_module,
761+ optionflags=doctest.ELLIPSIS))
762+
763+ return suite
764
765=== modified file 'tests/databases/postgres.py'
766--- tests/databases/postgres.py 2011-02-25 10:26:26 +0000
767+++ tests/databases/postgres.py 2011-10-19 22:12:25 +0000
768@@ -22,26 +22,53 @@
769 import os
770
771 from storm.databases.postgres import (
772- Postgres, compile, currval, Returning, PostgresTimeoutTracer)
773+ Postgres, compile, currval, Returning, PostgresTimeoutTracer, make_dsn)
774 from storm.database import create_database
775 from storm.exceptions import InterfaceError, ProgrammingError
776 from storm.variables import DateTimeVariable, RawStrVariable
777 from storm.variables import ListVariable, IntVariable, Variable
778 from storm.properties import Int
779+from storm.exceptions import DisconnectionError
780 from storm.expr import (Union, Select, Insert, Alias, SQLRaw, State,
781 Sequence, Like, Column, COLUMN)
782 from storm.tracer import install_tracer, TimeoutError
783+from storm.uri import URI
784
785 # We need the info to register the 'type' compiler. In normal
786 # circumstances this is naturally imported.
787 import storm.info
788+storm # Silence lint.
789
790+from tests import has_fixtures
791 from tests.databases.base import (
792 DatabaseTest, DatabaseDisconnectionTest, UnsupportedDatabaseTest)
793 from tests.expr import column1, column2, column3, elem1, table1, TrackContext
794 from tests.tracer import TimeoutTracerTestBase
795 from tests.helper import TestHelper
796
797+try:
798+ import pgbouncer
799+except ImportError:
800+ has_pgbouncer = False
801+else:
802+ has_pgbouncer = True
803+
804+
805+def terminate_other_backends(connection):
806+ """Terminate all connections to the database except the one given."""
807+ connection.execute(
808+ "SELECT pg_terminate_backend(procpid)"
809+ " FROM pg_stat_activity"
810+ " WHERE datname = current_database()"
811+ " AND procpid != pg_backend_pid()")
812+
813+
814+def terminate_all_backends(database):
815+ """Terminate all connections to the given database."""
816+ connection = database.connect()
817+ terminate_other_backends(connection)
818+ connection.close()
819+
820
821 class PostgresTest(DatabaseTest, TestHelper):
822
823@@ -542,6 +569,98 @@
824 self.fail('Exception should have been swallowed: %s' % repr(exc))
825
826
827+class PostgresDisconnectionTestWithoutProxyBase(object):
828+ # DatabaseDisconnectionTest uses a socket proxy to simulate broken
829+ # connections. This class tests some other causes of disconnection.
830+
831+ database_uri = None
832+
833+ def is_supported(self):
834+ return bool(self.database_uri)
835+
836+ def setUp(self):
837+ super(PostgresDisconnectionTestWithoutProxyBase, self).setUp()
838+ self.database = create_database(self.database_uri)
839+
840+ def test_terminated_backend(self):
841+ # The error raised when trying to use a connection that has been
842+ # terminated at the server is considered a disconnection error.
843+ connection = self.database.connect()
844+ terminate_all_backends(self.database)
845+ self.assertRaises(
846+ DisconnectionError, connection.execute,
847+ "SELECT current_database()")
848+
849+
850+class PostgresDisconnectionTestWithoutProxyUnixSockets(
851+ PostgresDisconnectionTestWithoutProxyBase, TestHelper):
852+ """Disconnection tests using Unix sockets."""
853+
854+ database_uri = os.environ.get("STORM_POSTGRES_URI")
855+
856+
857+class PostgresDisconnectionTestWithoutProxyTCPSockets(
858+ PostgresDisconnectionTestWithoutProxyBase, TestHelper):
859+ """Disconnection tests using TCP sockets."""
860+
861+ database_uri = os.environ.get("STORM_POSTGRES_HOST_URI")
862+
863+
864+class PostgresDisconnectionTestWithPGBouncerBase(object):
865+ # Connecting via pgbouncer <http://pgfoundry.org/projects/pgbouncer>
866+ # introduces new possible causes of disconnections.
867+
868+ def is_supported(self):
869+ return (
870+ has_fixtures and has_pgbouncer and
871+ bool(os.environ.get("STORM_POSTGRES_HOST_URI")))
872+
873+ def setUp(self):
874+ super(PostgresDisconnectionTestWithPGBouncerBase, self).setUp()
875+ database_uri = URI(os.environ["STORM_POSTGRES_HOST_URI"])
876+ database_user = database_uri.username or os.environ['USER']
877+ database_dsn = make_dsn(database_uri)
878+ # Create a pgbouncer fixture.
879+ self.pgbouncer = pgbouncer.fixture.PGBouncerFixture()
880+ self.pgbouncer.databases[database_uri.database] = database_dsn
881+ self.pgbouncer.users[database_user] = "trusted"
882+ self.pgbouncer.admin_users = [database_user]
883+ self.useFixture(self.pgbouncer)
884+ # Create a Database that uses pgbouncer.
885+ pgbouncer_uri = database_uri.copy()
886+ pgbouncer_uri.host = self.pgbouncer.host
887+ pgbouncer_uri.port = self.pgbouncer.port
888+ self.database = create_database(pgbouncer_uri)
889+
890+ def test_terminated_backend(self):
891+ # The error raised when trying to use a connection through pgbouncer
892+ # that has been terminated at the server is considered a disconnection
893+ # error.
894+ connection = self.database.connect()
895+ terminate_all_backends(self.database)
896+ self.assertRaises(
897+ DisconnectionError, connection.execute,
898+ "SELECT current_database()")
899+
900+ def test_pgbouncer_stopped(self):
901+ # The error raised from a connection that is no longer connected
902+ # because pgbouncer has been immediately shutdown (via SIGTERM; see
903+ # man 1 pgbouncer) is considered a disconnection error.
904+ connection = self.database.connect()
905+ self.pgbouncer.stop()
906+ self.assertRaises(
907+ DisconnectionError, connection.execute,
908+ "SELECT current_database()")
909+
910+
911+if has_fixtures:
912+ # Upgrade to full test case class with fixtures.
913+ from fixtures import TestWithFixtures
914+ class PostgresDisconnectionTestWithPGBouncer(
915+ PostgresDisconnectionTestWithPGBouncerBase,
916+ TestWithFixtures, TestHelper): pass
917+
918+
919 class PostgresTimeoutTracerTest(TimeoutTracerTestBase):
920
921 tracer_class = PostgresTimeoutTracer

Subscribers

People subscribed via source and target branches

to status/vote changes: