Merge lp:~allenap/python-pgbouncer/reliable-shutdown into lp:python-pgbouncer

Proposed by Gavin Panella
Status: Merged
Merged at revision: 9
Proposed branch: lp:~allenap/python-pgbouncer/reliable-shutdown
Merge into: lp:python-pgbouncer
Diff against target: 301 lines (+110/-82)
4 files modified
.bzrignore (+2/-0)
README (+14/-2)
pgbouncer/fixture.py (+64/-41)
pgbouncer/tests.py (+30/-39)
To merge this branch: bzr merge lp:~allenap/python-pgbouncer/reliable-shutdown
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+80382@code.launchpad.net

Commit message

Ensure that pgbouncer has shutdown when stop() returns.

Description of the change

While working on lp:~allenap/storm/oneiric-admin-shutdown-bug-871596
I've had problems with the pgbouncer not being quite shutdown right
after calling stop(). This branch changes the way that pgbouncer is
started so that we can be confident that we have stopped it later on.

List of changes:

- Does not start pgbouncer with -d (--daemon) so that the Popen object
  can be used to directly monitor and control the process. Previously
  it was done at one remove, reading in the daemon's pid from a pid
  file.

- Raise an exception if pgbouncer does not stop with 5 seconds of
  calling stop(). Previously it would wait for pgbouncer to stop but
  then do nothing.

- Adds the pgbouncer output (from stdout and stderr) as a fixture
  detail. This means that tests that use the fixture can get
  interesting debug information when the test fails.

- Fixes the tests so that they are always loaded into an
  OptimizingTestSuite. If the tests were run as suggested in the
  README they were being loaded into a standard TestSuite.

- Refactors the tests so that they're a lot shorter and a bit easier
  to see what they're trying to demonstrate.

- Update the README.

To post a comment you must log in.
15. By Gavin Panella

Fix test_unix_sockets to actually connect via a Unix socket.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

I'm not done reviewing this yet, but a few questions first:

 * I guess if the test dies, pgbouncer will now die as well so you don't need a pidfile to set things right during a later run?

 * Why treat the files you open as binary? Take a pidfile, for instance. I don't suppose it'll ever matter, but isn't that a text file? Adding the "b" to the open modes seems to confuse more than help.

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

> I'm not done reviewing this yet, but a few questions first:

Thanks for looking at it :)

> * I guess if the test dies, pgbouncer will now die as well so you don't need
> a pidfile to set things right during a later run?

Well, I don't know if there are any greater guarantees than
before. Even if we/pgbouncer arranges for SIGHUP to be sent on parent
death it will only cause pgbouncer to reload its config.

I suppose I could add an atexit handler to kill child processes.

> * Why treat the files you open as binary? Take a pidfile, for instance. I
> don't suppose it'll ever matter, but isn't that a text file? Adding the "b"
> to the open modes seems to confuse more than help.

Just a habit. The Python docs state that using "b" improves
portability. Not that it's needed, but it does no harm, and the
"features" of text mode are not needed anyway.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (9.2 KiB)

Thanks for fixing this, by the way. I see some really good stuff in here, both functionally and in terms of code quality.

=== modified file 'pgbouncer/fixture.py'
--- pgbouncer/fixture.py 2011-09-12 23:38:28 +0000
+++ pgbouncer/fixture.py 2011-10-25 20:23:23 +0000

@@ -111,46 +111,56 @@
                 authfile.write('"%s" "%s"\n' % user_creds)

     def stop(self):
- if self.process_pid is None:
- return
- os.kill(self.process_pid, signal.SIGTERM)
- # Wait for the shutdown to occur
+ if self.process is None:
+ # pgbouncer has not been started.
+ return
+ if self.process.poll() is not None:
+ # pgbouncer has exited already.
+ return
+ # Terminate and wait.
+ self.process.terminate()
         start = time.time()
         stop = start + 5.0
         while time.time() < stop:
- if not os.path.isfile(self.pidpath):
- self.process_pid = None
- return
- # If its not going away, we might want to raise an error, but for now
- # it seems reliable.
+ if self.process.poll() is None:
+ time.sleep(0.1)
+ else:
+ break
+ else:
+ raise Exception(
+ 'timeout waiting for pgbouncer to exit')

Some say that the pattern of “comment announcing block of code, block of code” is an indicator that you're probably better off extracting separate methods. I think in this case the terminate-and-wait loop is a bit mechanical and potentially worth extracting. It also gives you a bit more freedom to do things like a direct “return” on success, and to unit test the loop's corner cases. The while / if / sleep / else / break / else / raise loop, while small, may be just a bit too easy to break in maintenance.

Oh, and while you're at it, could you capitalize and punctuate that error message? It helps confirm to the hurried reader that the message is an independent sentence, rather than a continuation of an earlier statement (or of a traceback line) that the eye should skip over.

     def start(self):
         self.addCleanup(self.stop)

         # Add /usr/sbin if necessary to the PATH for magic just-works
         # behavior with Ubuntu.
- env = None
+ env = os.environ.copy()
         if not self.pgbouncer.startswith('/'):
- path = os.environ['PATH'].split(':')
+ path = env['PATH'].split(os.pathsep)

Better, because simpler.

             if '/usr/sbin' not in path:
                 path.append('/usr/sbin')
- env = os.environ.copy()
- env['PATH'] = ':'.join(path)
-
- outputfile = open(self.outputpath, 'wt')
- self.process = subprocess.Popen(
- [self.pgbouncer, '-d', self.inipath], env=env,
- stdout=outputfile.fileno(), stderr=outputfile.fileno())
- self.process.communicate()
- # Wait up to 5 seconds for the pid file to exist
+ env['PATH'] = os.pathsep.join(path)
+
+ with open(self.outputpath, "wb") as outputfile:
+ with open(os.devnull, "rb") as devnull:
+ self....

Read more...

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

> > * I guess if the test dies, pgbouncer will now die as well so you don't
> need
> > a pidfile to set things right during a later run?
>
> Well, I don't know if there are any greater guarantees than
> before. Even if we/pgbouncer arranges for SIGHUP to be sent on parent
> death it will only cause pgbouncer to reload its config.
>
> I suppose I could add an atexit handler to kill child processes.

If pgbouncer is a child of the test process, it should die along with the process. Don't mess with atexit if you can avoid it. It'd be good to give this a manual try though, just in case. You can probably just insert a long sleep and issue a manual “kill -segv <pid>” to see what happens if the test process dies without any python exit handling whatsoever.

> > * Why treat the files you open as binary? Take a pidfile, for instance. I
> > don't suppose it'll ever matter, but isn't that a text file? Adding the "b"
> > to the open modes seems to confuse more than help.
>
> Just a habit. The Python docs state that using "b" improves
> portability. Not that it's needed, but it does no harm, and the
> "features" of text mode are not needed anyway.

AFAIK it only improves portability for files that aren't pure text!

Revision history for this message
Gavin Panella (allenap) wrote :
Download full text (4.8 KiB)

[...]
> Some say that the pattern of “comment announcing block of code, block of code”
> is an indicator that you're probably better off extracting separate methods.
> I think in this case the terminate-and-wait loop is a bit mechanical and
> potentially worth extracting. It also gives you a bit more freedom to do
> things like a direct “return” on success, and to unit test the loop's corner
> cases. The while / if / sleep / else / break / else / raise loop, while
> small, may be just a bit too easy to break in maintenance.

I've factored some of it out into a new countdown() function. I've
stuck with break instead of return, just because. I don't have strong
feelings about early returns, but here I marginally prefer the break.

> Oh, and while you're at it, could you capitalize and punctuate that error
> message? It helps confirm to the hurried reader that the message is an
> independent sentence, rather than a continuation of an earlier statement (or
> of a traceback line) that the eye should skip over.

Done.

[...]
> Very similar to what you were doing in stop(). Once again I think this is
> easier to deal with if you extract it. There's probably a reusable thingy
> somewhere for this basic structure, but I don't suppose it's worth digging up.

Yep, changed as for stop().

> === modified file 'pgbouncer/tests.py'
> --- pgbouncer/tests.py 2011-09-05 15:01:52 +0000
> +++ pgbouncer/tests.py 2011-10-25 20:23:23 +0000
> @@ -15,7 +15,7 @@
> # along with this program. If not, see <http://www.gnu.org/licenses/>.
>
> import os
> -from unittest import main, TestLoader
> +import unittest
>
> import fixtures
> import psycopg2
> @@ -28,10 +28,12 @@
> # A 'just-works' workaround for Ubuntu not exposing initdb to the main PATH.
> os.environ['PATH'] = os.environ['PATH'] + ':/usr/lib/postgresql/8.4/bin'
>
> +
> +test_loader = testresources.TestLoader()
> +
> +
> def test_suite():
> - result = testresources.OptimisingTestSuite()
> - result.addTest(TestLoader().loadTestsFromName(__name__))
> - return result
> + return test_loader.loadTestsFromName(__name__)
>
>
> Not that I have any idea what I'm talking about, but is there any risk that
> this global initialization might cause trouble for “make lint” checks (which I
> believe import the test as a module)?

TestLoader.__init__() is not overridden from object() and
loadTestsFromName() does not mutate anything, so this is safe.
However, test_loader.discover() does mutate self so I've changed this
to create a new TestLoader as needed, Just In Case.

>
>
> @@ -49,15 +51,22 @@
>
> resources = [('db', DatabaseManager(initialize_sql=setup_user))]
>
> + def setUp(self):
> + super(TestFixture, self).setUp()
> + self.bouncer = PGBouncerFixture()
> + self.bouncer.databases[self.db.database] = 'host=' + self.db.host
> + self.bouncer.users['user1'] = ''
>
> You're doing this test a world of good. Still, it's a shame to jump through
> the “super” hoop *and* create implicit state *and* make an explicit call
> whenever you use the fixture.
>
> Why not fold all of it into a single explicit call? What I mean is:
>
> def makeBouncerFixture(self, ...

Read more...

16. By Gavin Panella

Factor out the countdown code.

17. By Gavin Panella

Simplify the countdown code.

18. By Gavin Panella

Improve docstrings and exception messages.

19. By Gavin Panella

Create TestLoader as needed.

20. By Gavin Panella

Simplify connect().

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

[...]
> If pgbouncer is a child of the test process, it should die along
> with the process. Don't mess with atexit if you can avoid it. It'd
> be good to give this a manual try though, just in case. You can
> probably just insert a long sleep and issue a manual “kill -segv
> <pid>” to see what happens if the test process dies without any
> python exit handling whatsoever.

SIGSEGV and SIGTERM leave pgbouncer running, bug SIGINT allows
clean-ups to run (because it's a KeyboardInterrupt) so that pgbouncer
gets terminated. I think this is acceptable.

Rob has previously warned (w.r.t. rabbitfixture) not to be too clever
with cleaning stuff up from earlier. He advised something along the
lines of: it's better to let these things go awry and thus draw our
attention to leaks rather than patch things over.

> > > * Why treat the files you open as binary? Take a pidfile, for
> > > instance. I don't suppose it'll ever matter, but isn't that a
> > > text file? Adding the "b" to the open modes seems to confuse
> > > more than help.
> >
> > Just a habit. The Python docs state that using "b" improves
> > portability. Not that it's needed, but it does no harm, and the
> > "features" of text mode are not needed anyway.
>
> AFAIK it only improves portability for files that aren't pure text!

Though there is no different on Linux (afaik), treating every file as
binary reduces surprises on other platforms. Having a distinction
between text and binary reminds me of FTP ASCII mode and what a
horror-show that is.

When writing to a file I rarely use print, for example; I prefer to
use .write(), which means I must think about and insert line-endings
myself. I guess this _might_ tempt me to use text mode for writing a
file, but more often than not I'd simply use Unix line-endings. Only
notepad.exe is likely to have any problem opening files like that.

If I want to read a file line by line that could have any line-ending,
I will either slurp it in and call splitlines(), or use universal
newlines.

In the fixture there are three places where I use binary mode: opening
the output file for pgbouncer, opening the input file (/dev/null) for
pgbouncer, and for reading the PID file.

The first two mimic shell redirections, which must use binary mode (or
else piping tar to gzip would break on Windows).

The PID file is slurped in and immediately stripped (and is expected
to be no more than one line anyway) so line-endings are irrelevant.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2011-07-18 03:31:27 +0000
+++ .bzrignore 2011-10-26 14:08:31 +0000
@@ -6,3 +6,5 @@
6./parts6./parts
7./eggs7./eggs
8./download-cache8./download-cache
9TAGS
10tags
911
=== modified file 'README'
--- README 2011-09-05 14:35:29 +0000
+++ README 2011-10-26 14:08:31 +0000
@@ -30,12 +30,18 @@
30* python-fixtures (https://launchpad.net/python-fixtures or30* python-fixtures (https://launchpad.net/python-fixtures or
31 http://pypi.python.org/pypi/fixtures)31 http://pypi.python.org/pypi/fixtures)
3232
33* testtools (http://pypi.python.org/pypi/testtools)
34
33Testing Dependencies35Testing Dependencies
34====================36====================
3537
38In addition to the above, the tests also depend on:
39
40* psycopg2 (http://pypi.python.org/pypi/psycopg2)
41
36* subunit (http://pypi.python.org/pypi/python-subunit) (optional)42* subunit (http://pypi.python.org/pypi/python-subunit) (optional)
3743
38* testtools (http://pypi.python.org/pypi/testtools)44* testresources (http://pypi.python.org/pypi/testresources)
3945
40* van.pg (http://pypi.python.org/pypi/van.pg)46* van.pg (http://pypi.python.org/pypi/van.pg)
4147
@@ -75,4 +81,10 @@
75immediately available, you can use ./bootstrap.py to create bin/buildout, then81immediately available, you can use ./bootstrap.py to create bin/buildout, then
76bin/py to get a python interpreter with the dependencies available.82bin/py to get a python interpreter with the dependencies available.
7783
78To run the tests, run 'bin/py pgbouncer/tests.py'.84To run the tests, run either:
85
86 $ bin/py pgbouncer/tests.py
87
88or:
89
90 $ bin/py -m testtools.run pgbouncer.tests.test_suite
7991
=== modified file 'pgbouncer/fixture.py'
--- pgbouncer/fixture.py 2011-09-12 23:38:28 +0000
+++ pgbouncer/fixture.py 2011-10-26 14:08:31 +0000
@@ -18,13 +18,31 @@
18 'PGBouncerFixture',18 'PGBouncerFixture',
19 ]19 ]
2020
21import itertools
21import os.path22import os.path
22import socket23import socket
23import signal
24import subprocess24import subprocess
25import time25import time
2626
27from fixtures import Fixture, TempDir27from fixtures import Fixture, TempDir
28from testtools.content import content_from_file
29
30
31def countdown(duration=5.0, sleep=0.1):
32 """Provide a countdown iterator that sleeps between iterations.
33
34 Yields the current iteration count, starting from 1.
35 """
36 start = time.time()
37 stop = start + duration
38 for iteration in itertools.count(1):
39 now = time.time()
40 if now < stop:
41 yield iteration
42 time.sleep(sleep)
43 else:
44 break
45
2846
29def _allocate_ports(n=1):47def _allocate_ports(n=1):
30 """Allocate `n` unused ports.48 """Allocate `n` unused ports.
@@ -46,12 +64,16 @@
46class PGBouncerFixture(Fixture):64class PGBouncerFixture(Fixture):
47 """Programmatically configure and run pgbouncer.65 """Programmatically configure and run pgbouncer.
4866
49 >>> Minimal usage:67 Minimal usage:
50 >>> bouncer = PGBouncerFixture()68
51 >>> bouncer.databases['mydb'] = 'host=hostname dbname=foo'69 >>> bouncer = PGBouncerFixture()
52 >>> bouncer.users['user1'] = 'credentials'70 >>> bouncer.databases['mydb'] = 'host=hostname dbname=foo'
53 >>> with bouncer:71 >>> bouncer.users['user1'] = 'credentials'
54 ... # Can now connect to bouncer.host port=bouncer.port user=user172 >>> with bouncer:
73 ... connection = psycopg2.connect(
74 ... database="mydb", host=bouncer.host, port=bouncer.port,
75 ... user="user1", password="credentials")
76
55 """77 """
5678
57 def __init__(self):79 def __init__(self):
@@ -76,7 +98,6 @@
76 self.port = _allocate_ports()[0]98 self.port = _allocate_ports()[0]
77 self.configdir = self.useFixture(TempDir())99 self.configdir = self.useFixture(TempDir())
78 self.auth_type = 'trust'100 self.auth_type = 'trust'
79 self.process_pid = None
80 self.setUpConf()101 self.setUpConf()
81 self.start()102 self.start()
82103
@@ -111,46 +132,48 @@
111 authfile.write('"%s" "%s"\n' % user_creds)132 authfile.write('"%s" "%s"\n' % user_creds)
112133
113 def stop(self):134 def stop(self):
114 if self.process_pid is None:135 if self.process is None:
115 return136 # pgbouncer has not been started.
116 os.kill(self.process_pid, signal.SIGTERM)137 return
117 # Wait for the shutdown to occur138 if self.process.poll() is not None:
118 start = time.time()139 # pgbouncer has exited already.
119 stop = start + 5.0140 return
120 while time.time() < stop:141 self.process.terminate()
121 if not os.path.isfile(self.pidpath):142 for iteration in countdown():
122 self.process_pid = None143 if self.process.poll() is not None:
123 return144 break
124 # If its not going away, we might want to raise an error, but for now145 else:
125 # it seems reliable.146 raise Exception(
147 'Time-out waiting for pgbouncer to exit.')
126148
127 def start(self):149 def start(self):
128 self.addCleanup(self.stop)150 self.addCleanup(self.stop)
129151
130 # Add /usr/sbin if necessary to the PATH for magic just-works152 # Add /usr/sbin if necessary to the PATH for magic just-works
131 # behavior with Ubuntu.153 # behavior with Ubuntu.
132 env = None154 env = os.environ.copy()
133 if not self.pgbouncer.startswith('/'):155 if not self.pgbouncer.startswith('/'):
134 path = os.environ['PATH'].split(':')156 path = env['PATH'].split(os.pathsep)
135 if '/usr/sbin' not in path:157 if '/usr/sbin' not in path:
136 path.append('/usr/sbin')158 path.append('/usr/sbin')
137 env = os.environ.copy()159 env['PATH'] = os.pathsep.join(path)
138 env['PATH'] = ':'.join(path)160
139161 with open(self.outputpath, "wb") as outputfile:
140 outputfile = open(self.outputpath, 'wt')162 with open(os.devnull, "rb") as devnull:
141 self.process = subprocess.Popen(163 self.process = subprocess.Popen(
142 [self.pgbouncer, '-d', self.inipath], env=env,164 [self.pgbouncer, self.inipath], env=env, stdin=devnull,
143 stdout=outputfile.fileno(), stderr=outputfile.fileno())165 stdout=outputfile, stderr=outputfile)
144 self.process.communicate()166
145 # Wait up to 5 seconds for the pid file to exist167 self.addDetail(
146 start = time.time()168 os.path.basename(self.outputpath),
147 stop = start + 5.0169 content_from_file(self.outputpath))
148 while time.time() < stop:170
171 # Wait for the PID file to appear.
172 for iteration in countdown():
149 if os.path.isfile(self.pidpath):173 if os.path.isfile(self.pidpath):
150 try:174 with open(self.pidpath, "rb") as pidfile:
151 self.process_pid = int(file(self.pidpath, 'rt').read())175 if pidfile.read().strip().isdigit():
152 except ValueError:176 break
153 # Empty pid files -> ValueError.177 else:
154 continue178 raise Exception(
155 return179 'Time-out waiting for pgbouncer to create PID file.')
156 raise Exception('timeout waiting for pgbouncer to create pid file')
157180
=== modified file 'pgbouncer/tests.py'
--- pgbouncer/tests.py 2011-09-05 15:01:52 +0000
+++ pgbouncer/tests.py 2011-10-26 14:08:31 +0000
@@ -15,7 +15,7 @@
15# along with this program. If not, see <http://www.gnu.org/licenses/>.15# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
17import os17import os
18from unittest import main, TestLoader18import unittest
1919
20import fixtures20import fixtures
21import psycopg221import psycopg2
@@ -28,10 +28,10 @@
28# A 'just-works' workaround for Ubuntu not exposing initdb to the main PATH.28# A 'just-works' workaround for Ubuntu not exposing initdb to the main PATH.
29os.environ['PATH'] = os.environ['PATH'] + ':/usr/lib/postgresql/8.4/bin'29os.environ['PATH'] = os.environ['PATH'] + ':/usr/lib/postgresql/8.4/bin'
3030
31
31def test_suite():32def test_suite():
32 result = testresources.OptimisingTestSuite()33 loader = testresources.TestLoader()
33 result.addTest(TestLoader().loadTestsFromName(__name__))34 return loader.loadTestsFromName(__name__)
34 return result
3535
3636
37class ResourcedTestCase(testtools.TestCase, testresources.ResourcedTestCase):37class ResourcedTestCase(testtools.TestCase, testresources.ResourcedTestCase):
@@ -49,15 +49,21 @@
4949
50 resources = [('db', DatabaseManager(initialize_sql=setup_user))]50 resources = [('db', DatabaseManager(initialize_sql=setup_user))]
5151
52 def setUp(self):
53 super(TestFixture, self).setUp()
54 self.bouncer = PGBouncerFixture()
55 self.bouncer.databases[self.db.database] = 'host=' + self.db.host
56 self.bouncer.users['user1'] = ''
57
58 def connect(self, host=None):
59 return psycopg2.connect(
60 host=(self.bouncer.host if host is None else host),
61 port=self.bouncer.port, database=self.db.database,
62 user='user1')
63
52 def test_dynamic_port_allocation(self):64 def test_dynamic_port_allocation(self):
53 bouncer = PGBouncerFixture()65 self.useFixture(self.bouncer)
54 db = self.db66 self.connect().close()
55 bouncer.databases[db.database] = 'host=%s' % (db.host,)
56 bouncer.users['user1'] = ''
57 with bouncer:
58 conn = psycopg2.connect(host=bouncer.host, port=bouncer.port,
59 user='user1', database=db.database)
60 conn.close()
6167
62 def test_stop_start_facility(self):68 def test_stop_start_facility(self):
63 # Once setup the fixture can be stopped, and started again, retaining69 # Once setup the fixture can be stopped, and started again, retaining
@@ -65,36 +71,21 @@
65 # potentially be used by a different process, so this isn't perfect,71 # potentially be used by a different process, so this isn't perfect,
66 # but its pretty reliable as a test helper, and manual port allocation72 # but its pretty reliable as a test helper, and manual port allocation
67 # outside the dynamic range should be fine.73 # outside the dynamic range should be fine.
68 bouncer = PGBouncerFixture()74 self.useFixture(self.bouncer)
69 db = self.db75 self.bouncer.stop()
70 bouncer.databases[db.database] = 'host=%s' % (db.host,)76 self.assertRaises(psycopg2.OperationalError, self.connect)
71 bouncer.users['user1'] = ''77 self.bouncer.start()
72 def check_connect():78 self.connect().close()
73 conn = psycopg2.connect(host=bouncer.host, port=bouncer.port,
74 user='user1', database=db.database)
75 conn.close()
76 with bouncer:
77 current_port = bouncer.port
78 bouncer.stop()
79 self.assertRaises(psycopg2.OperationalError, check_connect)
80 bouncer.start()
81 check_connect()
8279
83 def test_unix_sockets(self):80 def test_unix_sockets(self):
84 db = self.db
85 unix_socket_dir = self.useFixture(fixtures.TempDir()).path81 unix_socket_dir = self.useFixture(fixtures.TempDir()).path
86 bouncer = PGBouncerFixture()82 self.bouncer.unix_socket_dir = unix_socket_dir
87 bouncer.databases[db.database] = 'host=%s' % (db.host,)83 self.useFixture(self.bouncer)
88 bouncer.users['user1'] = ''84 # Connect to pgbouncer via a Unix domain socket. We don't
89 bouncer.unix_socket_dir = unix_socket_dir85 # care how pgbouncer connects to PostgreSQL.
90 with bouncer:86 self.connect(host=unix_socket_dir).close()
91 # Connect to pgbouncer via a Unix domain socket. We don't
92 # care how pgbouncer connects to PostgreSQL.
93 conn = psycopg2.connect(
94 host=unix_socket_dir, user='user1',
95 database=db.database, port=bouncer.port)
96 conn.close()
9787
9888
99if __name__ == "__main__":89if __name__ == "__main__":
100 main(testLoader=TestLoader())90 loader = testresources.TestLoader()
91 unittest.main(testLoader=loader)

Subscribers

People subscribed via source and target branches

to status/vote changes: