Merge lp:~allenap/storm/oneiric-admin-shutdown-bug-871596 into lp:storm
- oneiric-admin-shutdown-bug-871596
- Merge into trunk
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 | ||||
Related bugs: |
|
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.
Commit message
Description of the change
This branch adds three tests for disconnection errors:
- When the connection is terminated with pg_terminate_
- When the connection is terminated with pg_terminate_
connection goes via pgbouncer.
- When the connection is terminated because pgbouncer is terminated.
It then updates is_disconnectio
Additionally, under Oneiric, some disconnection tests were failing
anyway, so is_disconnectio
This branch adds two optional test dependencies: python-fixtures and
python-pgbouncer. If these are not available the tests will not be
attempted.
- 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.
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_
> '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.
Robert Collins (lifeless) wrote : | # |
van.pg can do postgresql cluster setup as a test fixture, if you want :)
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.
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://
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
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 |
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.