On 2016-12-17, at 23:18, Glyph Lefkowitz <gl...@twistedmatrix.com> wrote:

> On Dec 17, 2016, at 6:11 AM, exvito here <ex.vitor...@gmail.com> wrote:

[...]

>> Thanks for your input Cory and Glyph.
>> 
>> I do agree that a well written protocol should not
>> self.transport.write after connectionLost -- it does not make any kind
>> of sense to do that. It's just that the one I was debugging was doing
>> it in its app-level keep alive messages triggered by
>> IReactorTime.callLater which wasn't properly cancelled on
>> connectionLost. This, in turn, lead to unpexpected app-level keep
>> alive timeouts after disconnection which, while acceptable (depending
>> on app/protocol design), were having a negative impact on the overall
>> connection/protocol teardown and cleanup.
>> 
>> Having said this, the fact that self.transport.write does not complain
>> after connectionLost just made my analysis and debugging more
>> difficult (yes, I was aware that it is non-blocking -- thanks Cory --
>> and that when talking to the network the only confirmation of delivery
>> must be obtained from a received message stating that fact).
>> 
>> All in all, I was just looking for confirmation from a conceptual /
>> design standpoint. That was my purpose and it is now clear. Thanks.
>> 
>> Going a little bit further, in this context, I wonder if my
>> understanding of the Protocol / Transport objects lifecycle is 100%
>> clear. I think there is a one to one relationship in the sense that,
>> simply put, on an incoming connection the underlying machinery
>> instantiates a Protocol via the Factory's buildProtocol method,
>> instantiates a Transport object and then calls Protocol.makeConnection
>> to "attach" the transport to the protocol; when the connection is
>> closed, the Transport calls connectionLost on the Protocol and, at
>> some point, those two objects are no longer "usable" -- they've done
>> their job and will be destroyed if no app level references are kept to
>> them.
> 
> They're not "destroyed", exactly.
> 
> The way to think about this is that the reactor, while running, is always on 
> the stack.  The stack is the root of what's currently 'active' in the garbage 
> collector.  The reactor then has references to various things - in this case, 
> TCP transports and timed DelayedCall objects.  The transport has a reference 
> to the protocol, which is what keeps both of them "usable".  The DelayedCall 
> has a reference to your function, and your function to anything it needs to 
> do its work.
> 
> Things can be removed from and added back to the reactor at any time - for 
> example, a transport may be removed without being closed by calling 
> .stopReading() and .stopWriting() on it.  If it gets garbage collected, its 
> socket will be closed, but because the reactor didn't close it, you won't get 
> a normal connectionLost notification.

Sure, I had that clear in my understanding, thanks for the additional details 
on the stopReading/stopWriting -- I'm not familiar with low lever reactor 
details and, maybe, who knows, one day I'll put myself to the interesting 
exercise/challenge of playing around writing a reactor. Just to improve my 
understanding of these details.


> So rather than thinking of a Transport as having a precisely-defined 
> lifecycle - although it does have certain states it goes through; 
> connecting/connected/disconnecting/disconnected - the best way to think about 
> it is as a graph of objects that the reactor is doing stuff to.

The bottom line (in my mind) is: is it correct or not to assume that, by 
design, once a Transport instance has been disconnected that same instance will 
never be connected again, regardless of when exactly that object is destroyed 
or garbage collected?


>> If this is correct, in particular the fact that a given Transport
>> instance is not reusable after disconnection (if it is, could you
>> please point out such a case?), wouldn't it be valuable to log a
>> warning when write after disconnect is called? (agreed, raising an
>> Exception would be too much and break compatiblity). I find two
>> reasons for that: on one hand, given that it would have made my
>> debugging easier, it may help other developers pin point such cases;
>> on the other, the fact that Twisted itself logs messages on several
>> occasions, such as, for example, the default noisy Factory on doStart
>> / doStop. Adding to that, the fact that, if I'm reading the code
>> correctly, ITransport.write just throws away the data for TCP
>> transports since it falls back to FileDescriptor.write that simply
>> returns if "not self.connected or self._writeDisconnected" in
>> src/twisted/internet/abstract.py.
> 
> If you start logging a warning when write is called on a closed transport, 
> then that means any code that wants to be warnings-clean needs to check to 
> see if the transport is writable before writing to it.  This adds a lot of 
> complexity to that sort of message-formatting code, and potentially makes it 
> less efficient. After all, if you have to check each write() call, that will 
> strongly encourage you to make your message-formatting happen entirely in 
> memory, so you always have a big string that represents the whole message, 
> just in case a transport.write() in the middle would encounter an error.

I understand that and find it perfectly acceptable. I just tend to err on the 
extra-cautious side when facing data-loss / data-integrity possibilities and, 
in the many scenarios I've used Twisted in, performance was never an issue 
while semantic correctness and implementation resiliency tend to need fixes and 
improvements. :)

One could argue, though, that someone looking for warnings-clean code (who 
doesn't, even if the real world and interfacing with it most often prevents 
it?) might like to know that their "perfect" (lots of quotes here!) 
warnings-clean code writes won't go anywhere after connectionLost which, in 
some cases, may help pinpoint an underlying design / implementation issue.

I'd like to underline, though, that I'm not insisting on the "log warning" 
aspect I raised before. Like I said, the status quo is very good, thank you. 
Just sharing a few thoughts.


> More importantly, when users start seeing a warning like this, they often 
> develop an incorrect intuition about what's going on with the bytes; one of 
> the _most_ frequently asked questions about how to write Twisted protocols is 
> "how do I tell if my bytes got to the other end without sending an 
> application-level acknowledgement".  The answer, of course, is "that's 
> impossible" but if users see that sometimes it warns them when the 
> connection's closed, it reinforces the idea that there must be a way.
> 
> If you call .write, there is always the possibility that the connection 
> _looks_ open to Twisted, but in fact is closed on the wire, and those bytes 
> will be lost.  So it's important to me that we behave the same way in both 
> cases.

Yes, I can relate to that common misconception regarding delivery confirmation, 
throughout the years, working with other people. Networking is non-trivival and 
there are lots of parts out of application control that can behave in 
unexpected ways and/or go wrong.


> That said, it's not like there's no room for improvement here!  The solution 
> _I'd_ really like to see for this problem is improved visualization of what 
> the reactor is doing.  Something that drew a graph of the reactor, and open 
> connections, and pending timed calls would allow you to instantly see that 
> your timed call was not connected to an active connection, and correct your 
> code.

Well, trial already does that in part, doesn't it? It complains loudly about 
unclean reactor state after each test. It just doesn't relate timed calls to 
connections, which sounds like a very non-obvious challenge to handle at the 
reactor level: how could the reactor know that a given timed call callback will 
make use of a particular connection?

That does sound like a very interesting capability, though.


>> Thoughts? (or corrections, of course?)
>> 
>> Again, thanks for your time and input.
>> Regards,
>> 
>> PS: I don't mean to start a huge discussion on the topic or question
>> the current design. It is very nice as it is, thank you! I'm just
>> looking for improving my understanding and, humbly, contributing back.
> 
> 
> Don't worry, the design is pretty fixed - and if you managed to provoke such 
> a discussion, then maybe it does need to change!  But I doubt it :)

I'm glad it is pretty fixed. That has always given me the confidence to work 
with it throughout the years. Thanks and congratulations for a very solid piece 
of code!

Having said this, again very humbly, who knows what the future may bring! ;)


Thanks for taking the time to respond.
Regards,
--
exvito


_______________________________________________
Twisted-Python mailing list
Twisted-Python@twistedmatrix.com
http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-python

Reply via email to