Merge lp:~bzr/bzr/transport_post_connect_hook into lp:bzr

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merged at revision: 6405
Proposed branch: lp:~bzr/bzr/transport_post_connect_hook
Merge into: lp:bzr
Prerequisite: lp:~spiv/bzr/hooks-refactoring
Diff against target: 347 lines (+99/-106)
7 files modified
bzrlib/hooks.py (+1/-0)
bzrlib/smart/medium.py (+5/-0)
bzrlib/tests/__init__.py (+14/-10)
bzrlib/tests/per_transport.py (+34/-0)
bzrlib/tests/test_transport.py (+23/-0)
bzrlib/tests/transport_util.py (+4/-95)
bzrlib/transport/__init__.py (+18/-1)
To merge this branch: bzr merge lp:~bzr/bzr/transport_post_connect_hook
Reviewer Review Type Date Requested Status
Jelmer Vernooij (community) Approve
Vincent Ladeuil Needs Information
Martin Packman Pending
Review via email: mp+85735@code.launchpad.net

This proposal supersedes a proposal from 2010-10-14.

Commit message

Add a post_connect hook for transports.

Description of the change

Creates a new post_connect hook for transports, and uses it in the testing framework to ensure transports are disconnected when the test finishes so we don't leak resources. This replaces the current hack that overrides get_transport which was incomplete and causing other problems. See the mailing list thread for more:

<https://lists.ubuntu.com/archives/bazaar/2010q4/070386.html>

I'm not really sure how ready this is, but putting up an mp seems as good a way of getting feedback as bugging people on IRC.

The hook is hurled together based on lp:~parthm/bzr/173274-export-hooks if there are opinions on how it should be written differently I'd like to know.

Todo:
  News. I'm leaving this till landing actually happens to lessen pain of flux.
  Other documentation changes?
  Some tests that don't suck. Suggestions welcome.

(originally by mgz, updated to merge bzr.dev)

To post a comment you must log in.
Revision history for this message
Vincent Ladeuil (vila) wrote : Posted in a previous version of this proposal

129 + for hook in self.hooks["post_connect"]:
130 + # GZ 2010-10-14: Should the hook be passed the new connection and
131 + # credentials too or does opaque really mean that?
132 + hook(self)

The hook already receives 'self' so it can access the connection/credentials if needed.
But they are specific to each transport class...

The tests are ok. We know the hook is heavily exercised or we get leaks anyway.

If you really really want to add tests you can check what happens when several transports share a connection, but even that sounds overkill.

Write the NEWS entry, we'll see what is available when your patch lands. Don't forget the hooks-help.txt file.

I'll ping people on the pre-requisites.

review: Approve
Revision history for this message
Martin Packman (gz) wrote : Posted in a previous version of this proposal

So, very nearly this exact hook exists in bzrlib.tests.transport_util but in a hackier tests only fashion. Basically that whole file can now go, but Vincent pointed out it has some sftp-or-ftp logic depending on if paramiko is installed that may as well stay put for the moment.

Revision history for this message
Martin Packman (gz) wrote : Posted in a previous version of this proposal

Just having a hook turned out to be insufficient, and results in worse hangs than the old get_transport hack as can be seen from the nearly four hour babune runtime last night:

<http://babune.ladeuil.net:24842/job/selftest-subset-windows/11/>

The problem is that RemoteTransport classes don't use _set_connection and generally behave rather differently. Throwing in a hook point in __init__ when a new medium is built gets us to a leak-free 40 minute runtime:

<http://babune.ladeuil.net:24842/job/selftest-subset-windows/13/>

However the semantics are not quite correct there. Even if we don't have to worry about remote reconnections (do we?) the post_connect hook is happening before the real connection, and potentially twice in the RemoteHTTPTransport case. Combined with the existing confusion over how many times disconnect gets called, it suggests this hook probably isn't sane enough to be generally useful. We do want the leak problem fixed asap though, so if anyone has any clever ideas...

Revision history for this message
Vincent Ladeuil (vila) wrote : Posted in a previous version of this proposal
Download full text (4.0 KiB)

>>>>> Martin [gz] <email address hidden> writes:

    > Just having a hook turned out to be insufficient, and results in
    > worse hangs than the old get_transport hack as can be seen from
    > the nearly four hour babune runtime last night:
    > <http://babune.ladeuil.net:24842/job/selftest-subset-windows/11/>

    > The problem is that RemoteTransport classes don't use
    > _set_connection and generally behave rather differently. Throwing
    > in a hook point in __init__ when a new medium is built gets us to
    > a leak-free 40 minute runtime:

    > <http://babune.ladeuil.net:24842/job/selftest-subset-windows/13/>

Ok, so this validates the approach of calling disconnect for all
transports that have been able to connect to their server and ensures
all code paths are covered. As such, I'm tempted to accept the patch as
is waiting for a better solution in a followon.

From a design point of view, I think this outlines the divergence
between the smart transport and the others and came from the time where
we implemented connection sharing.

The smart transport has a medium object that implements
_ensure_connection() and disconnect() whereas the other transports use a
_SharedConnection object which is used a data container by the
transport, but the transport object implements connect_xxx() and
disconnect() while calling _set_connection() when the connection is
established (including reconnections).

So while the post_connect hook can be implemented for the transports
objects, it can't be properly implemented for smart transports where the
transport object is not available at the medium level when the
connection occur (in theory we could pass it done but I'm worried about
ref cycles there).

This hints as connection hook instead of a transport hook but this
doesn't play well with the transport objects who consider the connection
as an opaque attribute so far.

    > However the semantics are not quite correct there. Even if we
    > don't have to worry about remote reconnections (do we?) the
    > post_connect hook is happening before the real connection, and
    > potentially twice in the RemoteHTTPTransport case. Combined with
    > the existing confusion over how many times disconnect gets called,

I think the confusion comes the fact that disconnect() should be
implemented at the transport level since it is defined as closing the
connection even if there are other transports sharing this connection
(as opposed to closing the connection when the *last* transport using
the connection requires it).

In the test suite, all transports sharing a connection calls
disconnect() to ensure we don't get leaks but that's the expected
behaviour.

    > it suggests this hook probably isn't sane enough to be generally
    > useful.

Indeed, it lies for the smart transports as it's called *before* the
connection occurs.

    > We do want the leak problem fixed asap though, so if anyone has
    > any clever ideas...

One alternative would be to add a 'created' hook for transports which,
for the tests, will also call disconnect(). From your hack_transport
branch we know that calling disconnect() for all created transports is
enough to fix the leaks ev...

Read more...

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

FWIW This no longer causes hangs with bzr.dev. I'm looking at using the post_connect hook to test the number of connections made in various blackbox tests.

Revision history for this message
Vincent Ladeuil (vila) wrote :

Wow, this is so old... A few thoughts from reading the comments:
- the hangs referred to in the discussion for the previous version of this proposal are now fixed
- the issue about smart transport and connected transports being different beasts is still existing

Revision history for this message
Vincent Ladeuil (vila) wrote :

So, I think the same objection still stands: the hook is called for smart
transports *before* the connection occurs which is misleading for a
post_connect hook wanting to precisely track the effective connections.

We can rename it to connection_set...

But in any case, we should explain in the hook documentation that in some
cases, the hook will be called for a transport that hasn't establish its
connection yet.

Not pretty.

This also means that when using RemoteHTTPTransport the hook is most
probably called twice, one for the smart transport and one for the http
transport.

Basically we want to push the hook down at the medium level with some
reference to its transport (which is a good way to introduce ref-cycles ;),
wait, didn't I mention that already ?). There may be a neat trick waiting to
be found but it escapes me right now...

Looking at the smart mediums:

- the Pipe one can arguably call the hook at init time.

- the ssh based ones want to call the hook in _ensure_connection

- the http one... should not call the hook since it's backing http transport
  will. Or should it still call the hook ? There are arguments both ways
  depending on what the hook is used for...

- SmartClientAlreadyConnectedSocketMedium doesn't need to call the hook by
  definition (and the the comment in the overriding _ensure_connection is a
  hint that we are on the right track).

May be we should just focus on the test needs for now and consider yet
another hook ?

Ignoring all cloning issues and connection issues, the original need was to
disconnect all created transports without worrying about disconnecting
unused transport not transport reused multiple times.

@Jelmer: What is your need ? A precise number of connections ?

review: Needs Information
Revision history for this message
Martin Packman (gz) wrote :

Have addressed the smart transport case by moving the hook callback down into the medium classes where the connection actually happens. The one wrinkle here is that the medium then gets passes rather than transport. I think this is preferable to having the medium and transport hold references to each other, and the medium classes also have a disconnect method. This should make the hook useful, without actually needing to restructure the transport classes quite yet.

Revision history for this message
Jelmer Vernooij (jelmer) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/hooks.py'
--- bzrlib/hooks.py 2011-12-19 13:23:58 +0000
+++ bzrlib/hooks.py 2011-12-23 19:43:24 +0000
@@ -83,6 +83,7 @@
83 ('bzrlib.smart.client', '_SmartClient.hooks', 'SmartClientHooks'),83 ('bzrlib.smart.client', '_SmartClient.hooks', 'SmartClientHooks'),
84 ('bzrlib.smart.server', 'SmartTCPServer.hooks', 'SmartServerHooks'),84 ('bzrlib.smart.server', 'SmartTCPServer.hooks', 'SmartServerHooks'),
85 ('bzrlib.status', 'hooks', 'StatusHooks'),85 ('bzrlib.status', 'hooks', 'StatusHooks'),
86 ('bzrlib.transport', 'Transport.hooks', 'TransportHooks'),
86 ('bzrlib.version_info_formats.format_rio', 'RioVersionInfoBuilder.hooks',87 ('bzrlib.version_info_formats.format_rio', 'RioVersionInfoBuilder.hooks',
87 'RioVersionInfoBuilderHooks'),88 'RioVersionInfoBuilderHooks'),
88 ('bzrlib.merge_directive', 'BaseMergeDirective.hooks',89 ('bzrlib.merge_directive', 'BaseMergeDirective.hooks',
8990
=== modified file 'bzrlib/smart/medium.py'
--- bzrlib/smart/medium.py 2011-12-19 13:23:58 +0000
+++ bzrlib/smart/medium.py 2011-12-23 19:43:24 +0000
@@ -43,6 +43,7 @@
43 debug,43 debug,
44 errors,44 errors,
45 trace,45 trace,
46 transport,
46 ui,47 ui,
47 urlutils,48 urlutils,
48 )49 )
@@ -1021,6 +1022,8 @@
1021 raise AssertionError(1022 raise AssertionError(
1022 "Unexpected io_kind %r from %r"1023 "Unexpected io_kind %r from %r"
1023 % (io_kind, self._ssh_connection))1024 % (io_kind, self._ssh_connection))
1025 for hook in transport.Transport.hooks["post_connect"]:
1026 hook(self)
10241027
1025 def _flush(self):1028 def _flush(self):
1026 """See SmartClientStreamMedium._flush()."""1029 """See SmartClientStreamMedium._flush()."""
@@ -1129,6 +1132,8 @@
1129 raise errors.ConnectionError("failed to connect to %s:%d: %s" %1132 raise errors.ConnectionError("failed to connect to %s:%d: %s" %
1130 (self._host, port, err_msg))1133 (self._host, port, err_msg))
1131 self._connected = True1134 self._connected = True
1135 for hook in transport.Transport.hooks["post_connect"]:
1136 hook(self)
11321137
11331138
1134class SmartClientAlreadyConnectedSocketMedium(SmartClientSocketMedium):1139class SmartClientAlreadyConnectedSocketMedium(SmartClientSocketMedium):
11351140
=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py 2011-12-18 12:46:49 +0000
+++ bzrlib/tests/__init__.py 2011-12-23 19:43:24 +0000
@@ -2734,16 +2734,20 @@
27342734
2735 def setUp(self):2735 def setUp(self):
2736 super(TestCaseWithMemoryTransport, self).setUp()2736 super(TestCaseWithMemoryTransport, self).setUp()
2737 # Ensure that ConnectedTransport doesn't leak sockets2737
2738 def get_transport_from_url_with_cleanup(*args, **kwargs):2738 def _add_disconnect_cleanup(transport):
2739 t = orig_get_transport_from_url(*args, **kwargs)2739 """Schedule disconnection of given transport at test cleanup
2740 if isinstance(t, _mod_transport.ConnectedTransport):2740
2741 self.addCleanup(t.disconnect)2741 This needs to happen for all connected transports or leaks occur.
2742 return t2742
27432743 Note reconnections may mean we call disconnect multiple times per
2744 orig_get_transport_from_url = self.overrideAttr(2744 transport which is suboptimal but seems harmless.
2745 _mod_transport, 'get_transport_from_url',2745 """
2746 get_transport_from_url_with_cleanup)2746 self.addCleanup(transport.disconnect)
2747
2748 _mod_transport.Transport.hooks.install_named_hook('post_connect',
2749 _add_disconnect_cleanup, None)
2750
2747 self._make_test_root()2751 self._make_test_root()
2748 self.addCleanup(os.chdir, os.getcwdu())2752 self.addCleanup(os.chdir, os.getcwdu())
2749 self.makeAndChdirToTestDir()2753 self.makeAndChdirToTestDir()
27502754
=== modified file 'bzrlib/tests/per_transport.py'
--- bzrlib/tests/per_transport.py 2011-11-17 18:06:46 +0000
+++ bzrlib/tests/per_transport.py 2011-12-23 19:43:24 +0000
@@ -53,9 +53,11 @@
53from bzrlib.tests.test_transport import TestTransportImplementation53from bzrlib.tests.test_transport import TestTransportImplementation
54from bzrlib.transport import (54from bzrlib.transport import (
55 ConnectedTransport,55 ConnectedTransport,
56 Transport,
56 _get_transport_modules,57 _get_transport_modules,
57 )58 )
58from bzrlib.transport.memory import MemoryTransport59from bzrlib.transport.memory import MemoryTransport
60from bzrlib.transport.remote import RemoteTransport
5961
6062
61def get_transport_test_permutations(module):63def get_transport_test_permutations(module):
@@ -1836,3 +1838,35 @@
1836 self.build_tree([needlessly_escaped_dir], transport=t1)1838 self.build_tree([needlessly_escaped_dir], transport=t1)
1837 t2 = t1.clone(needlessly_escaped_dir)1839 t2 = t1.clone(needlessly_escaped_dir)
1838 self.assertEqual(t1.base + "-.09AZ_az~/", t2.base)1840 self.assertEqual(t1.base + "-.09AZ_az~/", t2.base)
1841
1842 def test_hook_post_connection_one(self):
1843 """Fire post_connect hook after a ConnectedTransport is first used"""
1844 log = []
1845 Transport.hooks.install_named_hook("post_connect", log.append, None)
1846 t = self.get_transport()
1847 self.assertEqual([], log)
1848 t.has("non-existant")
1849 if isinstance(t, RemoteTransport):
1850 self.assertEqual([t.get_smart_medium()], log)
1851 elif isinstance(t, ConnectedTransport):
1852 self.assertEqual([t], log)
1853 else:
1854 self.assertEqual([], log)
1855
1856 def test_hook_post_connection_multi(self):
1857 """Fire post_connect hook once per unshared underlying connection"""
1858 log = []
1859 Transport.hooks.install_named_hook("post_connect", log.append, None)
1860 t1 = self.get_transport()
1861 t2 = t1.clone(".")
1862 t3 = self.get_transport()
1863 self.assertEqual([], log)
1864 t1.has("x")
1865 t2.has("x")
1866 t3.has("x")
1867 if isinstance(t1, RemoteTransport):
1868 self.assertEqual([t.get_smart_medium() for t in [t1, t3]], log)
1869 elif isinstance(t1, ConnectedTransport):
1870 self.assertEqual([t1, t3], log)
1871 else:
1872 self.assertEqual([], log)
18391873
=== modified file 'bzrlib/tests/test_transport.py'
--- bzrlib/tests/test_transport.py 2011-12-05 14:21:55 +0000
+++ bzrlib/tests/test_transport.py 2011-12-23 19:43:24 +0000
@@ -443,6 +443,29 @@
443 self.assertEqual('chroot-%d:///' % id(server), server.get_url())443 self.assertEqual('chroot-%d:///' % id(server), server.get_url())
444444
445445
446class TestHooks(tests.TestCase):
447 """Basic tests for transport hooks"""
448
449 def _get_connected_transport(self):
450 return transport.ConnectedTransport("bogus:nowhere")
451
452 def test_transporthooks_initialisation(self):
453 """Check all expected transport hook points are set up"""
454 hookpoint = transport.TransportHooks()
455 self.assertTrue("post_connect" in hookpoint,
456 "post_connect not in %s" % (hookpoint,))
457
458 def test_post_connect(self):
459 """Ensure the post_connect hook is called when _set_transport is"""
460 calls = []
461 transport.Transport.hooks.install_named_hook("post_connect",
462 calls.append, None)
463 t = self._get_connected_transport()
464 self.assertLength(0, calls)
465 t._set_connection("connection", "auth")
466 self.assertEqual(calls, [t])
467
468
446class PathFilteringDecoratorTransportTest(tests.TestCase):469class PathFilteringDecoratorTransportTest(tests.TestCase):
447 """Pathfilter decoration specific tests."""470 """Pathfilter decoration specific tests."""
448471
449472
=== modified file 'bzrlib/tests/transport_util.py'
--- bzrlib/tests/transport_util.py 2011-08-19 22:34:02 +0000
+++ bzrlib/tests/transport_util.py 2011-12-23 19:43:24 +0000
@@ -14,125 +14,34 @@
14# along with this program; if not, write to the Free Software14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1616
17import bzrlib.hooks
18from bzrlib import transport
19from bzrlib.tests import features17from bzrlib.tests import features
2018
21# SFTPTransport offers better performances but relies on paramiko, if paramiko19# SFTPTransport offers better performances but relies on paramiko, if paramiko
22# is not available, we fallback to FtpTransport20# is not available, we fallback to FtpTransport
23if features.paramiko.available():21if features.paramiko.available():
24 from bzrlib.tests import test_sftp_transport22 from bzrlib.tests import test_sftp_transport
25 from bzrlib.transport import sftp23 from bzrlib.transport import sftp, Transport
26 _backing_scheme = 'sftp'24 _backing_scheme = 'sftp'
27 _backing_transport_class = sftp.SFTPTransport25 _backing_transport_class = sftp.SFTPTransport
28 _backing_test_class = test_sftp_transport.TestCaseWithSFTPServer26 _backing_test_class = test_sftp_transport.TestCaseWithSFTPServer
29else:27else:
30 from bzrlib.transport import ftp28 from bzrlib.transport import ftp, Transport
31 from bzrlib.tests import test_ftp_transport29 from bzrlib.tests import test_ftp_transport
32 _backing_scheme = 'ftp'30 _backing_scheme = 'ftp'
33 _backing_transport_class = ftp.FtpTransport31 _backing_transport_class = ftp.FtpTransport
34 _backing_test_class = test_ftp_transport.TestCaseWithFTPServer32 _backing_test_class = test_ftp_transport.TestCaseWithFTPServer
3533
36from bzrlib.transport import (
37 ConnectedTransport,
38 register_transport,
39 register_urlparse_netloc_protocol,
40 unregister_transport,
41 _unregister_urlparse_netloc_protocol,
42 )
43
44
45
46class TransportHooks(bzrlib.hooks.Hooks):
47 """Dict-mapping hook name to a list of callables for transport hooks"""
48
49 def __init__(self):
50 super(TransportHooks, self).__init__("bzrlib.tests.transport_util",
51 "InstrumentedTransport.hooks")
52 # Invoked when the transport has just created a new connection.
53 # The api signature is (transport, connection, credentials)
54 self['_set_connection'] = []
55
56_hooked_scheme = 'hooked'
57
58def _change_scheme_in(url, actual, desired):
59 if not url.startswith(actual + '://'):
60 raise AssertionError('url "%r" does not start with "%r]"'
61 % (url, actual))
62 return desired + url[len(actual):]
63
64
65class InstrumentedTransport(_backing_transport_class):
66 """Instrumented transport class to test commands behavior"""
67
68 hooks = TransportHooks()
69
70 def __init__(self, base, _from_transport=None):
71 if not base.startswith(_hooked_scheme + '://'):
72 raise ValueError(base)
73 # We need to trick the backing transport class about the scheme used
74 # We'll do the reverse when we need to talk to the backing server
75 fake_base = _change_scheme_in(base, _hooked_scheme, _backing_scheme)
76 super(InstrumentedTransport, self).__init__(
77 fake_base, _from_transport=_from_transport)
78 # The following is needed to minimize the effects of our trick above
79 # while retaining the best compatibility.
80 self._parsed_url.scheme = _hooked_scheme
81 super(ConnectedTransport, self).__init__(str(self._parsed_url))
82
83
84class ConnectionHookedTransport(InstrumentedTransport):
85 """Transport instrumented to inspect connections"""
86
87 def _set_connection(self, connection, credentials):
88 """Called when a new connection is created """
89 super(ConnectionHookedTransport, self)._set_connection(connection,
90 credentials)
91 for hook in self.hooks['_set_connection']:
92 hook(self, connection, credentials)
93
9434
95class TestCaseWithConnectionHookedTransport(_backing_test_class):35class TestCaseWithConnectionHookedTransport(_backing_test_class):
9636
97 def setUp(self):37 def setUp(self):
98 register_urlparse_netloc_protocol(_hooked_scheme)
99 register_transport(_hooked_scheme, ConnectionHookedTransport)
100 self.addCleanup(unregister_transport, _hooked_scheme,
101 ConnectionHookedTransport)
102 self.addCleanup(_unregister_urlparse_netloc_protocol, _hooked_scheme)
103 super(TestCaseWithConnectionHookedTransport, self).setUp()38 super(TestCaseWithConnectionHookedTransport, self).setUp()
104 self.reset_connections()39 self.reset_connections()
105 # Add the 'hooked' url to the permitted url list.
106 # XXX: See TestCase.start_server. This whole module shouldn't need to
107 # exist - a bug has been filed on that. once its cleanedup/removed, the
108 # standard test support code will work and permit the server url
109 # correctly.
110 url = self.get_url()
111 t = transport.get_transport_from_url(url)
112 if t.base.endswith('work/'):
113 t = t.clone('../..')
114 self.permit_url(t.base)
115
116 def get_url(self, relpath=None):
117 super_self = super(TestCaseWithConnectionHookedTransport, self)
118 url = super_self.get_url(relpath)
119 # Replace the backing scheme by our own (see
120 # InstrumentedTransport.__init__)
121 url = _change_scheme_in(url, _backing_scheme, _hooked_scheme)
122 return url
12340
124 def start_logging_connections(self):41 def start_logging_connections(self):
125 self.overrideAttr(InstrumentedTransport, 'hooks', TransportHooks())42 Transport.hooks.install_named_hook('post_connect',
126 # We preserved the hooks class attribute. Now we install our hook.43 self.connections.append, None)
127 ConnectionHookedTransport.hooks.install_named_hook(
128 '_set_connection', self._collect_connection, None)
12944
130 def reset_connections(self):45 def reset_connections(self):
131 self.connections = []46 self.connections = []
13247
133 def _collect_connection(self, transport, connection, credentials):
134 # Note: uncomment the following line and use 'bt' under pdb, that will
135 # identify all the connections made including the extraneous ones.
136 # import pdb; pdb.set_trace()
137 self.connections.append(connection)
138
13948
=== modified file 'bzrlib/transport/__init__.py'
--- bzrlib/transport/__init__.py 2011-12-19 10:58:39 +0000
+++ bzrlib/transport/__init__.py 2011-12-23 19:43:24 +0000
@@ -52,7 +52,10 @@
52from bzrlib.trace import (52from bzrlib.trace import (
53 mutter,53 mutter,
54 )54 )
55from bzrlib import registry55from bzrlib import (
56 hooks,
57 registry,
58 )
5659
5760
58# a dictionary of open file streams. Keys are absolute paths, values are61# a dictionary of open file streams. Keys are absolute paths, values are
@@ -283,6 +286,16 @@
283 self.transport.append_bytes(self.relpath, bytes)286 self.transport.append_bytes(self.relpath, bytes)
284287
285288
289class TransportHooks(hooks.Hooks):
290 """Mapping of hook names to registered callbacks for transport hooks"""
291 def __init__(self):
292 super(TransportHooks, self).__init__()
293 self.add_hook("post_connect",
294 "Called after a new connection is established or a reconnect "
295 "occurs. The sole argument passed is either the connected "
296 "transport or smart medium instance.", (2, 5))
297
298
286class Transport(object):299class Transport(object):
287 """This class encapsulates methods for retrieving or putting a file300 """This class encapsulates methods for retrieving or putting a file
288 from/to a storage location.301 from/to a storage location.
@@ -307,6 +320,8 @@
307 # where the biggest benefit between combining reads and320 # where the biggest benefit between combining reads and
308 # and seeking is. Consider a runtime auto-tune.321 # and seeking is. Consider a runtime auto-tune.
309 _bytes_to_read_before_seek = 0322 _bytes_to_read_before_seek = 0
323
324 hooks = TransportHooks()
310325
311 def __init__(self, base):326 def __init__(self, base):
312 super(Transport, self).__init__()327 super(Transport, self).__init__()
@@ -1496,6 +1511,8 @@
1496 """1511 """
1497 self._shared_connection.connection = connection1512 self._shared_connection.connection = connection
1498 self._shared_connection.credentials = credentials1513 self._shared_connection.credentials = credentials
1514 for hook in self.hooks["post_connect"]:
1515 hook(self)
14991516
1500 def _get_connection(self):1517 def _get_connection(self):
1501 """Returns the transport specific connection object."""1518 """Returns the transport specific connection object."""